Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
import java.util.List;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;

import org.mapstruct.ap.internal.model.common.Assignment;
import org.mapstruct.ap.internal.model.common.FormattingParameters;
Expand All @@ -21,6 +26,7 @@
import org.mapstruct.ap.internal.model.source.SelectionParameters;
import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.NullabilityResolver;
import org.mapstruct.ap.internal.util.Strings;

import static org.mapstruct.ap.internal.util.Collections.first;
Expand Down Expand Up @@ -75,6 +81,33 @@ public final M build() {
Type sourceElementType = getElementType( sourceParameterType );
Type targetElementType = getElementType( resultType );

TypeMirror sourceElementTypeMirror = getElementTypeMirror( sourceParameterType.getTypeMirror() );
Element sourceParameterElement = first( method.getSourceParameters() ).getElement();
if ( sourceParameterElement != null
&& containsJSpecifyTypeUseAnnotation( sourceParameterElement.asType() ) ) {
sourceElementTypeMirror = getElementTypeMirror( sourceParameterElement.asType() );
}
TypeMirror targetElementTypeMirror = getElementTypeMirror( resultType.getTypeMirror() );
if ( method.getExecutable() != null
&& containsJSpecifyTypeUseAnnotation( method.getExecutable().getReturnType() ) ) {
targetElementTypeMirror = getElementTypeMirror( method.getExecutable().getReturnType() );
}
NullabilityResolver.Nullability sourceElementNullability = ctx.getNullabilityInMapperScope(
sourceElementTypeMirror
);
NullabilityResolver.Nullability targetElementNullability = ctx.getNullabilityInMapperScope(
targetElementTypeMirror
);
Boolean elementNullCheck = ctx.getNullabilityResolver().requiresNullCheck(
sourceElementNullability,
targetElementNullability
);
boolean includeElementNullCheck = elementNullCheck != null
? elementNullCheck
: sourceElementNullability == NullabilityResolver.Nullability.NULLABLE;
boolean setElementExplicitlyToNull = sourceElementNullability == NullabilityResolver.Nullability.NULLABLE
&& targetElementNullability != NullabilityResolver.Nullability.NON_NULL;

String loopVariableName =
Strings.getSafeVariableName( sourceElementType.getName(), method.getParameterNames() );

Expand Down Expand Up @@ -124,7 +157,7 @@ public final M build() {
forgedMethod.addThrownTypes( assignment.getThrownTypes() );
}
}
assignment = getWrapper( assignment, method );
assignment = getWrapper( assignment, method, includeElementNullCheck, setElementExplicitlyToNull );

// mapNullToDefault — a JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`.
// Forcing is unconditional here (unlike BeanMappingMethod): when the source is @NonNull the template skips
Expand Down Expand Up @@ -188,6 +221,42 @@ private Assignment forge(SourceRHS sourceRHS, Type sourceType, Type targetType)
return assignment;
}

private static TypeMirror getElementTypeMirror(TypeMirror typeMirror) {
if ( typeMirror.getKind() == TypeKind.ARRAY ) {
return ( (ArrayType) typeMirror ).getComponentType();
}
if ( typeMirror.getKind() == TypeKind.DECLARED ) {
List<? extends TypeMirror> typeArguments = ( (DeclaredType) typeMirror ).getTypeArguments();
if ( !typeArguments.isEmpty() ) {
return typeArguments.get( 0 );
}
}
return typeMirror;
}

private static boolean containsJSpecifyTypeUseAnnotation(TypeMirror typeMirror) {
if ( !typeMirror.getAnnotationMirrors().isEmpty() ) {
for ( AnnotationMirror annotationMirror : typeMirror.getAnnotationMirrors() ) {
String annotationName = annotationMirror.getAnnotationType().toString();
if ( "org.jspecify.annotations.Nullable".equals( annotationName )
|| "org.jspecify.annotations.NonNull".equals( annotationName ) ) {
return true;
}
}
}
if ( typeMirror.getKind() == TypeKind.ARRAY ) {
return containsJSpecifyTypeUseAnnotation( ( (ArrayType) typeMirror ).getComponentType() );
}
if ( typeMirror.getKind() == TypeKind.DECLARED ) {
for ( TypeMirror typeArgument : ( (DeclaredType) typeMirror ).getTypeArguments() ) {
if ( containsJSpecifyTypeUseAnnotation( typeArgument ) ) {
return true;
}
}
}
return false;
}

protected abstract M instantiateMappingMethod(Method method, Collection<String> existingVariables,
Assignment assignment, MethodReference factoryMethod,
boolean mapNullToDefault, String loopVariableName,
Expand All @@ -197,7 +266,8 @@ protected abstract M instantiateMappingMethod(Method method, Collection<String>

protected abstract Type getElementType(Type parameterType);

protected abstract Assignment getWrapper(Assignment assignment, Method method);
protected abstract Assignment getWrapper(Assignment assignment, Method method, boolean includeSourceNullCheck,
boolean setExplicitlyToNull);

@Override
protected boolean shouldUsePropertyNamesInHistory() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,27 @@ protected Type getElementType(Type parameterType) {
}

@Override
protected Assignment getWrapper(Assignment assignment, Method method) {
protected Assignment getWrapper(Assignment assignment, Method method, boolean includeSourceNullCheck,
boolean setExplicitlyToNull) {
Type resultType = method.getResultType();
// target accessor is setter, so decorate assignment as setter
if ( resultType.isArrayType() ) {
return new LocalVarWrapper( assignment, method.getThrownTypes(), resultType, false );
}
else {
return new SetterWrapper( assignment, method.getThrownTypes(), false );
return new LocalVarWrapper(
assignment,
method.getThrownTypes(),
resultType,
false,
includeSourceNullCheck,
setExplicitlyToNull
);
}
return new SetterWrapper(
assignment,
method.getThrownTypes(),
false,
includeSourceNullCheck,
setExplicitlyToNull
);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;

import org.mapstruct.ap.internal.model.common.Assignment;
import org.mapstruct.ap.internal.model.common.FormattingParameters;
Expand Down Expand Up @@ -225,6 +226,18 @@ public NullabilityResolver.Nullability getNullabilityInMapperScope(Element eleme
() -> typeFactory.getType( mapperTypeElement.asType() ).isNullMarked() );
}

/**
* Resolves the JSpecify nullability of a nested type-use position in the mapper's scope.
*
* @param typeMirror the type-use position to inspect
* @return the resolved nullability
*/
public NullabilityResolver.Nullability getNullabilityInMapperScope(TypeMirror typeMirror) {
return nullabilityResolver.getNullability(
typeMirror,
() -> typeFactory.getType( mapperTypeElement.asType() ).isNullMarked() );
}

/**
* Whether the return type of the given mapping method is JSpecify {@code @NonNull} (directly or via a
* {@code @NullMarked} scope). When it is, a mapping method must not generate {@code return null}, so callers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ protected Type getElementType(Type parameterType) {
}

@Override
protected Assignment getWrapper(Assignment assignment, Method method) {
protected Assignment getWrapper(Assignment assignment, Method method, boolean includeSourceNullCheck,
boolean setExplicitlyToNull) {
return new Java8FunctionWrapper( assignment );
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,25 @@ public class LocalVarWrapper extends AssignmentWrapper {

private final List<Type> thrownTypesToExclude;
private final Type targetType;
private final boolean includeSourceNullCheck;
private final boolean setExplicitlyToNull;

public LocalVarWrapper(Assignment decoratedAssignment, List<Type> thrownTypesToExclude, Type targetType,
boolean fieldAssignment) {
super( decoratedAssignment, fieldAssignment );
this.thrownTypesToExclude = thrownTypesToExclude;
this.targetType = targetType;
this.includeSourceNullCheck = false;
this.setExplicitlyToNull = false;
}

public LocalVarWrapper(Assignment decoratedAssignment, List<Type> thrownTypesToExclude, Type targetType,
boolean fieldAssignment, boolean includeSourceNullCheck, boolean setExplicitlyToNull) {
super( decoratedAssignment, fieldAssignment );
this.thrownTypesToExclude = thrownTypesToExclude;
this.targetType = targetType;
this.includeSourceNullCheck = includeSourceNullCheck;
this.setExplicitlyToNull = setExplicitlyToNull;
}

@Override
Expand All @@ -52,4 +65,12 @@ public Set<Type> getImportTypes() {
return imported;
}

public boolean isIncludeSourceNullCheck() {
return includeSourceNullCheck;
}

public boolean isSetExplicitlyToNull() {
return setExplicitlyToNull;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ public SetterWrapper(Assignment rhs, List<Type> thrownTypesToExclude, boolean fi
this.nullCastType = null;
}

public SetterWrapper(Assignment rhs, List<Type> thrownTypesToExclude, boolean fieldAssignment,
boolean includeSourceNullCheck, boolean setExplicitlyToNull) {
super( rhs, fieldAssignment );
this.thrownTypesToExclude = thrownTypesToExclude;
this.includeSourceNullCheck = includeSourceNullCheck;
this.setExplicitlyToNull = setExplicitlyToNull;
this.setExplicitlyToDefault = false;
this.mustCastForNull = false;
this.nullCastType = null;
}

@Override
public List<Type> getThrownTypes() {
List<Type> parentThrownTypes = super.getThrownTypes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,28 @@ else if ( element instanceof VariableElement ) {
return Nullability.UNKNOWN;
}

/**
* Determines the nullability of a type-use position. Unlike an accessor element, a nested type-use position
* has no declaration element to inspect, so only annotations on the supplied type mirror and the enclosing
* JSpecify scope are considered.
*
* @param typeMirror the type-use position to inspect; may be {@code null}
* @param enclosingTypeNullMarked supplier for the enclosing JSpecify scope; must be non-{@code null}
* @return the nullability state
*/
public Nullability getNullability(TypeMirror typeMirror, BooleanSupplier enclosingTypeNullMarked) {
if ( !enabled || typeMirror == null ) {
return Nullability.UNKNOWN;
}

Nullability result = getNullabilityFromTypeMirror( typeMirror );
if ( result != Nullability.UNKNOWN ) {
return result;
}

return enclosingTypeNullMarked.getAsBoolean() ? Nullability.NON_NULL : Nullability.UNKNOWN;
}

/**
* Determines the nullability of a write-accessor element — either a setter method or a field.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,28 @@
-->
<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.LocalVarWrapper" -->
<#import "../macro/CommonMacros.ftl" as lib>
<#if (thrownTypes?size == 0) >
<#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/></#if> ${ext.targetWriteAccessorName} = <@_assignment/>;
<#if includeSourceNullCheck>
if ( <#if sourceLocalVarName??>${sourceLocalVarName}<#else>${sourceReference}</#if> != null ) {
<@_writeAssignment/>
}
<#if setExplicitlyToNull>
else {
${ext.targetWriteAccessorName} = ${ext.targetType.null};
}
</#if>
<#else>
<#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/> ${ext.targetWriteAccessorName};</#if>
<@lib.handleExceptions>
${ext.targetWriteAccessorName} = <@_assignment/>;
</@lib.handleExceptions>
<@_writeAssignment/>
</#if>
<#macro _writeAssignment>
<#if (thrownTypes?size == 0) >
<#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/></#if> ${ext.targetWriteAccessorName} = <@_assignment/>;
<#else>
<#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/> ${ext.targetWriteAccessorName};</#if>
<@lib.handleExceptions>
${ext.targetWriteAccessorName} = <@_assignment/>;
</@lib.handleExceptions>
</#if>
</#macro>
<#macro _assignment>
<@includeModel object=assignment
targetBeanName=ext.targetBeanName
Expand All @@ -23,4 +37,4 @@
targetWriteAccessorName=ext.targetWriteAccessorName
targetPropertyName=ext.targetPropertyName
targetType=ext.targetType/>
</#macro>
</#macro>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.nullcheck.jspecify;

import java.util.List;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@NullMarked
@Mapper
public interface JSpecifyTypeUseMapper {

JSpecifyTypeUseMapper INSTANCE = Mappers.getMapper( JSpecifyTypeUseMapper.class );

JSpecifyTypeUseTargetBean map(JSpecifyTypeUseSourceBean source);

JSpecifyTypeUseTargetElement map(JSpecifyTypeUseSourceElement source);

List<@Nullable JSpecifyTypeUseTargetElement> mapElements(
List<@Nullable JSpecifyTypeUseSourceElement> source
);

List<List<@Nullable JSpecifyTypeUseTargetElement>> mapNested(
List<List<@Nullable JSpecifyTypeUseSourceElement>> source
);
}
Loading