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 @@ -334,6 +334,12 @@ This rule applies uniformly to bean, iterable, map, and stream mapping methods.

When the return type of a mapping method is `@NonNull` (directly or via a `@NullMarked` scope), MapStruct forces `NullValueMappingStrategy.RETURN_DEFAULT` semantics. The generated method returns a default-constructed target (bean methods), an empty collection (`Iterable` / array mappings), an empty map (`Map` mappings), or `Stream.empty()` (stream mappings) rather than `null`, so the return contract is never violated. This rule applies regardless of the explicit `NullValueMappingStrategy` setting.

==== Reused mapping methods

When a property assignment reuses a mapping method, MapStruct considers both sides of that method's JSpecify contract. A nullable source is guarded before calling a method whose first source parameter is `@NonNull`. A method whose first source parameter is `@Nullable` and whose return type is `@NonNull` is called directly, even when the source and target properties are nullable or non-null. For a chain of methods, the innermost input contract and outermost result contract are used.

JSpecify annotations refine null-check generation only; they do not participate in mapping-method overload selection. If annotations make several methods otherwise applicable, use qualifiers to select the intended method.

==== Constructor parameter constraint

If a property mapping would assign a potentially `null` source value to a `@NonNull` constructor parameter, MapStruct raises a *compilation error*. Neither inserting a null check (which would leave the variable at `null` and violate the contract) nor passing the value through is safe. Provide a `defaultValue` or `defaultExpression` on the `@Mapping` to satisfy the parameter when the source is absent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public class CollectionAssignmentBuilder {
private NullValueCheckStrategyGem nvcs;
private NullValuePropertyMappingStrategyGem nvpms;
private NullabilityResolver.Nullability sourceJSpecifyNullability = NullabilityResolver.Nullability.UNKNOWN;
private NullabilityResolver.Nullability targetJSpecifyNullability = NullabilityResolver.Nullability.UNKNOWN;

public CollectionAssignmentBuilder mappingBuilderContext(MappingBuilderContext ctx) {
this.ctx = ctx;
Expand Down Expand Up @@ -145,6 +146,15 @@ public CollectionAssignmentBuilder sourceJSpecifyNullability(
return this;
}

public CollectionAssignmentBuilder targetJSpecifyNullability(
NullabilityResolver.Nullability targetJSpecifyNullability
) {
this.targetJSpecifyNullability = targetJSpecifyNullability != null
? targetJSpecifyNullability
: NullabilityResolver.Nullability.UNKNOWN;
return this;
}

public Assignment build() {
Assignment result = assignment;

Expand Down Expand Up @@ -286,12 +296,45 @@ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs) {
return true;
}

if ( nvcs == ALWAYS ) {
// NullValueCheckStrategy is ALWAYS -> do a null check
if ( nvpms == SET_TO_DEFAULT || nvpms == IGNORE ) {
// NullValuePropertyMappingStrategy requires a source null check before applying its behavior.
return true;
}

if ( rhs.getType().isConverted() ) {
// A type conversion is applied, so a null check is required before invoking it.
return true;
}

if ( rhs.getType().isDirect() ) {
// Direct collection assignment uses a copy constructor and must not receive null.
return true;
}

NullabilityResolver.Nullability parameterNullability = rhs.getSourceParameterNullability();
NullabilityResolver.Nullability resultNullability = rhs.getResultNullability();
Boolean jspecifyDecision = ctx.getNullabilityResolver().requiresNullCheck(
sourceJSpecifyNullability,
targetJSpecifyNullability,
parameterNullability,
resultNullability
);
if ( jspecifyDecision != null ) {
ctx.getMessager().note( 2,
jspecifyDecision && parameterNullability == NullabilityResolver.Nullability.NON_NULL
? Message.PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK_NON_NULL_PARAM
: jspecifyDecision
? Message.PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK
: Message.PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK,
targetPropertyName,
sourceJSpecifyNullability,
targetJSpecifyNullability
);
return jspecifyDecision;
}

if ( nvcs == ALWAYS ) {
// NullValueCheckStrategy is ALWAYS -> do a null check
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.mapstruct.ap.internal.model.common.ModelElement;
import org.mapstruct.ap.internal.model.common.PresenceCheck;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.util.NullabilityResolver.Nullability;

/**
* An inline conversion from an optional source to it's value.
Expand Down Expand Up @@ -63,6 +64,16 @@ public Type getSourceType() {
return conversionAssignment.getSourceType();
}

@Override
public Nullability getSourceParameterNullability() {
return conversionAssignment.getSourceParameterNullability();
}

@Override
public Nullability getResultNullability() {
return conversionAssignment.getResultNullability();
}

@Override
public String createUniqueVarName(String desiredName) {
return conversionAssignment.createUniqueVarName( desiredName );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.source.Method;
import org.mapstruct.ap.internal.model.source.builtin.BuiltInMethod;
import org.mapstruct.ap.internal.util.NullabilityResolver.Nullability;
import org.mapstruct.ap.internal.util.Strings;

/**
Expand Down Expand Up @@ -63,6 +64,8 @@ public class MethodReference extends ModelElement implements Assignment {
private final boolean isStatic;
private final boolean isConstructor;
private final boolean isMethodChaining;
private final Nullability sourceParameterNullability;
private final Nullability resultNullability;

/**
* Creates a new reference to the given method.
Expand All @@ -75,6 +78,12 @@ public class MethodReference extends ModelElement implements Assignment {
*/
protected MethodReference(Method method, MapperReference declaringMapper, Parameter providingParameter,
List<ParameterBinding> parameterBindings) {
this( method, declaringMapper, providingParameter, parameterBindings, null, null );
}

private MethodReference(Method method, MapperReference declaringMapper, Parameter providingParameter,
List<ParameterBinding> parameterBindings, Nullability sourceParameterNullability,
Nullability resultNullability) {
this.declaringMapper = declaringMapper;
this.sourceParameters = Parameter.getSourceParameters( method.getParameters() );
this.returnType = method.getReturnType();
Expand All @@ -100,6 +109,8 @@ protected MethodReference(Method method, MapperReference declaringMapper, Parame
this.isConstructor = false;
this.methodsToChain = Collections.emptyList();
this.isMethodChaining = false;
this.sourceParameterNullability = sourceParameterNullability;
this.resultNullability = resultNullability;
}

private MethodReference(BuiltInMethod method, ConversionContext contextParam) {
Expand All @@ -118,6 +129,8 @@ private MethodReference(BuiltInMethod method, ConversionContext contextParam) {
this.isConstructor = false;
this.methodsToChain = Collections.emptyList();
this.isMethodChaining = false;
this.sourceParameterNullability = null;
this.resultNullability = null;
}

private MethodReference(String name, Type definingType, boolean isStatic) {
Expand All @@ -136,6 +149,8 @@ private MethodReference(String name, Type definingType, boolean isStatic) {
this.isConstructor = false;
this.methodsToChain = Collections.emptyList();
this.isMethodChaining = false;
this.sourceParameterNullability = null;
this.resultNullability = null;
}

private MethodReference(Type definingType, List<ParameterBinding> parameterBindings) {
Expand All @@ -153,6 +168,8 @@ private MethodReference(Type definingType, List<ParameterBinding> parameterBindi
this.isConstructor = true;
this.methodsToChain = Collections.emptyList();
this.isMethodChaining = false;
this.sourceParameterNullability = null;
this.resultNullability = null;

if ( parameterBindings.isEmpty() ) {
this.importTypes = Collections.emptySet();
Expand Down Expand Up @@ -186,6 +203,8 @@ private MethodReference(MethodReference... references) {
this.isConstructor = false;
this.methodsToChain = Arrays.asList( references );
this.isMethodChaining = true;
this.sourceParameterNullability = null;
this.resultNullability = null;
}

public MapperReference getDeclaringMapper() {
Expand All @@ -208,6 +227,22 @@ public Assignment getAssignment() {
return assignment;
}

@Override
public Nullability getSourceParameterNullability() {
if ( assignment != null ) {
Nullability nestedNullability = assignment.getSourceParameterNullability();
if ( nestedNullability != null ) {
return nestedNullability;
}
}
return sourceParameterNullability;
}

@Override
public Nullability getResultNullability() {
return resultNullability;
}

public String getName() {
return name;
}
Expand Down Expand Up @@ -422,6 +457,19 @@ public static MethodReference forMapperReference(Method method, MapperReference
return new MethodReference( method, declaringMapper, null, parameterBindings );
}

public static MethodReference forMapperReference(Method method, MapperReference declaringMapper,
List<ParameterBinding> parameterBindings, Nullability sourceParameterNullability,
Nullability resultNullability) {
return new MethodReference(
method,
declaringMapper,
null,
parameterBindings,
sourceParameterNullability,
resultNullability
);
}

public static MethodReference forStaticBuilder(String builderCreationMethod, Type definingType) {
return new MethodReference( builderCreationMethod, definingType, true );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,11 @@ public PropertyMapping build() {
NullabilityResolver.Nullability targetNullability = ctx.getNullabilityResolver().getSetterNullability(
targetWriteAccessor.getElement(), this::targetDeclaringTypeIsNullMarked
);
if ( sourceNullability != NullabilityResolver.Nullability.NON_NULL
NullabilityResolver.Nullability effectiveAssignmentNullability = getAssignmentResultNullability(
assignment,
sourceNullability
);
if ( effectiveAssignmentNullability != NullabilityResolver.Nullability.NON_NULL
&& targetNullability == NullabilityResolver.Nullability.NON_NULL ) {
ctx.getMessager().printMessage(
method.getExecutable(),
Expand Down Expand Up @@ -615,12 +619,21 @@ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs, Type targetTyp
NullabilityResolver.Nullability targetNullability = resolver.getSetterNullability(
targetWriteAccessor.getElement(), this::targetDeclaringTypeIsNullMarked
);
Boolean jspecifyDecision = resolver.requiresNullCheck( sourceNullability, targetNullability );
NullabilityResolver.Nullability parameterNullability = rhs.getSourceParameterNullability();
NullabilityResolver.Nullability resultNullability = rhs.getResultNullability();
Boolean jspecifyDecision = resolver.requiresNullCheck(
sourceNullability,
targetNullability,
parameterNullability,
resultNullability
);
if ( jspecifyDecision != null ) {
ctx.getMessager().note( 2,
jspecifyDecision
? Message.PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK
: Message.PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK,
jspecifyDecision && parameterNullability == NullabilityResolver.Nullability.NON_NULL
? Message.PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK_NON_NULL_PARAM
: jspecifyDecision
? Message.PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK
: Message.PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK,
targetPropertyName,
sourceNullability,
targetNullability
Expand All @@ -636,6 +649,24 @@ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs, Type targetTyp
return false;
}

private NullabilityResolver.Nullability getAssignmentResultNullability(
Assignment assignment,
NullabilityResolver.Nullability sourceNullability
) {
NullabilityResolver.Nullability resultNullability = assignment.getResultNullability();
if ( resultNullability == null ) {
return sourceNullability;
}
if ( sourceNullability != NullabilityResolver.Nullability.NON_NULL
&& assignment.getSourceParameterNullability() == NullabilityResolver.Nullability.NON_NULL
&& resultNullability == NullabilityResolver.Nullability.NON_NULL ) {
// A guard before a non-null helper leaves the constructor argument nullable when the
// source is absent, even though the helper itself returns a non-null value.
return NullabilityResolver.Nullability.NULLABLE;
}
return resultNullability;
}

private NullabilityResolver.Nullability getSourceJSpecifyNullability() {
if ( sourceReference == null ) {
return NullabilityResolver.Nullability.UNKNOWN;
Expand Down Expand Up @@ -760,6 +791,9 @@ private Assignment assignToCollection(Type targetType, AccessorType targetAccess
.nullValueCheckStrategy( hasDefaultValueOrDefaultExpression() ? ALWAYS : nvcs )
.nullValuePropertyMappingStrategy( nvpms )
.sourceJSpecifyNullability( getSourceJSpecifyNullability() )
.targetJSpecifyNullability( ctx.getNullabilityResolver().getSetterNullability(
targetWriteAccessor.getElement(), this::targetDeclaringTypeIsNullMarked
) )
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.mapstruct.ap.internal.model.common.ModelElement;
import org.mapstruct.ap.internal.model.common.PresenceCheck;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.util.NullabilityResolver.Nullability;

/**
* An inline conversion from a source to an optional of the source.
Expand Down Expand Up @@ -69,6 +70,16 @@ public Type getSourceType() {
return conversionAssignment.getSourceType();
}

@Override
public Nullability getSourceParameterNullability() {
return conversionAssignment.getSourceParameterNullability();
}

@Override
public Nullability getResultNullability() {
return conversionAssignment.getResultNullability();
}

@Override
public String createUniqueVarName(String desiredName) {
return conversionAssignment.createUniqueVarName( desiredName );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.mapstruct.ap.internal.model.common.ModelElement;
import org.mapstruct.ap.internal.model.common.PresenceCheck;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.util.NullabilityResolver.Nullability;

/**
* An inline conversion between source and target type of a mapping.
Expand Down Expand Up @@ -89,6 +90,16 @@ public Type getSourceType() {
return assignment.getSourceType();
}

@Override
public Nullability getSourceParameterNullability() {
return assignment.getSourceParameterNullability();
}

@Override
public Nullability getResultNullability() {
return assignment.getResultNullability();
}

@Override
public String createUniqueVarName(String desiredName ) {
return assignment.createUniqueVarName( desiredName );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.mapstruct.ap.internal.model.common.ModelElement;
import org.mapstruct.ap.internal.model.common.PresenceCheck;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.util.NullabilityResolver.Nullability;

/**
* Base class for decorators (wrappers). Decorator pattern is used to decorate assignments.
Expand Down Expand Up @@ -97,6 +98,16 @@ public AssignmentType getType() {
return decoratedAssignment.getType();
}

@Override
public Nullability getSourceParameterNullability() {
return decoratedAssignment.getSourceParameterNullability();
}

@Override
public Nullability getResultNullability() {
return decoratedAssignment.getResultNullability();
}

@Override
public boolean isCallingUpdateMethod() {
return decoratedAssignment.isCallingUpdateMethod();
Expand Down
Loading