From 19c830d2d9a3f611f6d19b531aa3821272ac9c1f Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 15 May 2026 11:26:07 +0200 Subject: [PATCH 1/3] #4056 Extend JSpecify support for container mapping and @NonNull return types Follow-up to #1243 closing the gaps reported in #4056: * `@NonNull` source on collection-typed property mappings now skips the wrapping null guard around the collection construction (mirrors the existing non-collection rule in PropertyMapping). * Container mapping methods (Iterable, Map, Stream, arrays) now honor JSpecify on their source parameter: ContainerMappingMethod and MapMappingMethod route the source-parameter presence check through the JSpecify-aware PresenceCheckMethodResolver (same helper BeanMappingMethod already uses), and the corresponding FTL templates wrap the early-return block in <#if sourceParameterPresenceCheck??>. * `@NonNull` mapping-method return type now implies NullValueMappingStrategy.RETURN_DEFAULT semantics across bean, iterable, map and stream mapping methods, so generated code never violates a @NonNull return contract by emitting `return null`. All new behaviour is gated on JSpecify annotations being present and is fully suppressed by the existing `mapstruct.disableJSpecify` option. --- NEXT_RELEASE_CHANGELOG.md | 3 ++ ...apter-10-advanced-mapping-options.asciidoc | 5 ++ .../ap/internal/model/BeanMappingMethod.java | 20 +++++++- .../model/CollectionAssignmentBuilder.java | 19 +++++++ .../model/ContainerMappingMethod.java | 7 +-- .../model/ContainerMappingMethodBuilder.java | 28 +++++++++-- .../internal/model/IterableMappingMethod.java | 14 ++++-- .../ap/internal/model/MapMappingMethod.java | 31 ++++++++++-- .../ap/internal/model/PropertyMapping.java | 1 + .../internal/model/StreamMappingMethod.java | 12 +++-- .../mapstruct/ap/internal/util/Message.java | 1 + .../internal/model/IterableMappingMethod.ftl | 2 + .../ap/internal/model/MapMappingMethod.ftl | 2 + .../ap/internal/model/StreamMappingMethod.ftl | 2 + .../JSpecifyCollectionPropertyMapper.java | 19 +++++++ .../JSpecifyCollectionPropertyTest.java | 43 ++++++++++++++++ .../JSpecifyIterableMethodMapper.java | 23 +++++++++ .../jspecify/JSpecifyIterableMethodTest.java | 44 +++++++++++++++++ .../jspecify/JSpecifyMapMethodMapper.java | 23 +++++++++ .../jspecify/JSpecifyMapMethodTest.java | 45 +++++++++++++++++ .../JSpecifyNonNullReturnBeanMapper.java | 24 +++++++++ .../JSpecifyNonNullReturnBeanSourceBean.java | 22 +++++++++ .../JSpecifyNonNullReturnBeanTest.java | 46 +++++++++++++++++ .../JSpecifyNonNullReturnIterableMapper.java | 24 +++++++++ .../JSpecifyNonNullReturnIterableTest.java | 49 +++++++++++++++++++ .../JSpecifyNonNullReturnMapMapper.java | 24 +++++++++ .../JSpecifyNonNullReturnMapTest.java | 49 +++++++++++++++++++ .../NullMarkedCollectionSourceBean.java | 27 ++++++++++ .../NullMarkedCollectionTargetBean.java | 24 +++++++++ .../jspecify/JSpecifyMapMethodMapperImpl.java | 43 ++++++++++++++++ .../JSpecifyNonNullReturnMapMapperImpl.java | 46 +++++++++++++++++ .../JSpecifyCollectionPropertyMapperImpl.java | 26 ++++++++++ .../JSpecifyIterableMethodMapperImpl.java | 40 +++++++++++++++ .../jspecify/JSpecifyMapMethodMapperImpl.java | 43 ++++++++++++++++ .../JSpecifyNonNullReturnBeanMapperImpl.java | 28 +++++++++++ ...pecifyNonNullReturnIterableMapperImpl.java | 43 ++++++++++++++++ .../JSpecifyNonNullReturnMapMapperImpl.java | 46 +++++++++++++++++ 37 files changed, 928 insertions(+), 20 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java diff --git a/NEXT_RELEASE_CHANGELOG.md b/NEXT_RELEASE_CHANGELOG.md index 8392b63c60..4e67c55aba 100644 --- a/NEXT_RELEASE_CHANGELOG.md +++ b/NEXT_RELEASE_CHANGELOG.md @@ -3,6 +3,9 @@ * Add support for JSpecify nullness annotations (#1243) - MapStruct now detects `@NonNull`, `@Nullable`, `@NullMarked` and `@NullUnmarked` from `org.jspecify.annotations` to control null check generation: - Source `@NonNull` skips null checks; target `@NonNull` always adds them - `@NonNull` source parameters skip the method-level null guard + - `@NonNull` source on collection-typed property mappings skips the wrapping null guard + - Container mapping methods (`Iterable`, `Map`, `Stream`, arrays) honor JSpecify on their source parameter + - `@NonNull` mapping-method return type implies `NullValueMappingStrategy.RETURN_DEFAULT` semantics across bean, iterable, map and stream mapping methods - `@NullMarked` / `@NullUnmarked` scope is resolved by walking method → class → outer class → package - Compile error when mapping a potentially nullable source to a `@NonNull` constructor parameter without a `defaultValue` - Can be disabled with the `mapstruct.disableJSpecify` compiler option diff --git a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc index 4c8be52503..306169193d 100644 --- a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc +++ b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc @@ -328,6 +328,11 @@ The existing safety guards (`defaultValue` / `defaultExpression`, primitive unbo ==== Method-level source parameter When the source parameter of a mapping method is annotated `@NonNull` (directly or via a `@NullMarked` scope), MapStruct skips the method-level null guard, since the caller is contractually obliged to pass a non-null value. +This rule applies uniformly to bean, iterable, map, and stream mapping methods. + +==== Method-level return type + +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. ==== Constructor parameter constraint diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index eaf89fc589..8fcb866d06 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java @@ -64,6 +64,7 @@ import org.mapstruct.ap.internal.model.source.selector.SelectedMethod; 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 org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.internal.util.accessor.AccessorType; @@ -371,7 +372,7 @@ else if ( !method.isUpdateMethod() ) { reportErrorForUnusedSourceParameters(); reportErrorForRedundantIgnoredSourceProperties(); - // mapNullToDefault + // mapNullToDefault — JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. boolean mapNullToDefault = method.getOptions() .getBeanMapping() .getNullValueMappingStrategy() @@ -507,6 +508,23 @@ else if ( !method.isUpdateMethod() ) { } } + // JSpecify: @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. + // Only applies when there are nullable source parameters (presence checks exist), since without them + // the template never generates a `return null` block in the first place. + if ( !mapNullToDefault + && !method.isUpdateMethod() + && !method.getReturnType().isVoid() + && !presenceChecksByParameter.isEmpty() ) { + NullabilityResolver.Nullability returnNullability = ctx.getNullabilityResolver().getNullability( + method.getExecutable(), + () -> ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked() ); + if ( returnNullability == NullabilityResolver.Nullability.NON_NULL ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; + } + } return new BeanMappingMethod( method, diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java index db9e012d78..5c99b85588 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java @@ -27,6 +27,7 @@ import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.NullabilityResolver; import org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.internal.util.accessor.AccessorType; @@ -73,6 +74,7 @@ public class CollectionAssignmentBuilder { private SourceRHS sourceRHS; private NullValueCheckStrategyGem nvcs; private NullValuePropertyMappingStrategyGem nvpms; + private NullabilityResolver.Nullability sourceJSpecifyNullability = NullabilityResolver.Nullability.UNKNOWN; public CollectionAssignmentBuilder mappingBuilderContext(MappingBuilderContext ctx) { this.ctx = ctx; @@ -134,6 +136,15 @@ public CollectionAssignmentBuilder nullValuePropertyMappingStrategy( NullValuePr return this; } + public CollectionAssignmentBuilder sourceJSpecifyNullability( + NullabilityResolver.Nullability sourceJSpecifyNullability + ) { + this.sourceJSpecifyNullability = sourceJSpecifyNullability != null + ? sourceJSpecifyNullability + : NullabilityResolver.Nullability.UNKNOWN; + return this; + } + public Assignment build() { Assignment result = assignment; @@ -262,6 +273,14 @@ private boolean canBeMappedOrDirectlyAssigned(Assignment result) { * @return whether to include a null / presence check or not */ private boolean setterWrapperNeedsSourceNullCheck(Assignment rhs) { + // JSpecify: source @NonNull means the value is guaranteed non-null, skip the wrapper + if ( sourceJSpecifyNullability == NullabilityResolver.Nullability.NON_NULL ) { + ctx.getMessager().note( 2, + Message.PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK_NON_NULL_SOURCE, + targetPropertyName ); + return false; + } + if ( rhs.getSourcePresenceCheckerReference() != null ) { // If there is a source presence check then we should do a null check return true; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethod.java index 9e7e64fefa..1a807725d3 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethod.java @@ -14,7 +14,6 @@ import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.Type; -import org.mapstruct.ap.internal.model.presence.NullPresenceCheck; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; import org.mapstruct.ap.internal.util.Strings; @@ -36,12 +35,14 @@ public abstract class ContainerMappingMethod extends NormalTypeMappingMethod { private final PresenceCheck sourceParameterPresenceCheck; private IterableCreation iterableCreation; + //CHECKSTYLE:OFF ContainerMappingMethod(Method method, List annotations, Collection existingVariables, Assignment parameterAssignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingReferences, List afterMappingReferences, - SelectionParameters selectionParameters) { + SelectionParameters selectionParameters, PresenceCheck sourceParameterPresenceCheck) { + //CHECKSTYLE:ON super( method, annotations, existingVariables, factoryMethod, mapNullToDefault, beforeMappingReferences, afterMappingReferences ); this.elementAssignment = parameterAssignment; @@ -64,7 +65,7 @@ public abstract class ContainerMappingMethod extends NormalTypeMappingMethod { } this.sourceParameter = sourceParameter; - this.sourceParameterPresenceCheck = new NullPresenceCheck( this.sourceParameter.getName() ); + this.sourceParameterPresenceCheck = sourceParameterPresenceCheck; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java index 6edaf5407f..c4da5b8b17 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java @@ -13,12 +13,15 @@ import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.FormattingParameters; +import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; 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; @@ -124,11 +127,24 @@ public final M build() { } assignment = getWrapper( assignment, method ); - // mapNullToDefault + // mapNullToDefault — JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. boolean mapNullToDefault = method.getOptions() .getIterableMapping() .getNullValueMappingStrategy() .isReturnDefault(); + if ( !mapNullToDefault + && !method.isUpdateMethod() + && !method.getReturnType().isVoid() ) { + NullabilityResolver.Nullability returnNullability = ctx.getNullabilityResolver().getNullability( + method.getExecutable(), + () -> ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked() ); + if ( returnNullability == NullabilityResolver.Nullability.NON_NULL ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; + } + } MethodReference factoryMethod = null; if ( !method.isUpdateMethod() ) { @@ -151,6 +167,11 @@ public final M build() { existingVariables ); + // Resolve presence check via JSpecify-aware resolver — returns null when source is @NonNull. + Parameter sourceParam = first( method.getSourceParameters() ); + PresenceCheck sourceParameterPresenceCheck = + PresenceCheckMethodResolver.getPresenceCheckForSourceParameter( method, null, sourceParam, ctx ); + return instantiateMappingMethod( method, existingVariables, @@ -160,7 +181,8 @@ public final M build() { loopVariableName, beforeMappingMethods, afterMappingMethods, - selectionParameters + selectionParameters, + sourceParameterPresenceCheck ); } @@ -177,7 +199,7 @@ protected abstract M instantiateMappingMethod(Method method, Collection boolean mapNullToDefault, String loopVariableName, List beforeMappingMethods, List afterMappingMethods, - SelectionParameters selectionParameters); + SelectionParameters selectionParameters, PresenceCheck sourceParameterPresenceCheck); protected abstract Type getElementType(Type parameterType); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java index 402eaa546d..220466f802 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java @@ -12,6 +12,7 @@ import org.mapstruct.ap.internal.model.assignment.LocalVarWrapper; import org.mapstruct.ap.internal.model.assignment.SetterWrapper; import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; @@ -54,7 +55,8 @@ protected Assignment getWrapper(Assignment assignment, Method method) { protected IterableMappingMethod instantiateMappingMethod(Method method, Collection existingVariables, Assignment assignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingMethods, - List afterMappingMethods, SelectionParameters selectionParameters) { + List afterMappingMethods, SelectionParameters selectionParameters, + PresenceCheck sourceParameterPresenceCheck) { return new IterableMappingMethod( method, getMethodAnnotations(), @@ -65,17 +67,20 @@ protected IterableMappingMethod instantiateMappingMethod(Method method, Collecti loopVariableName, beforeMappingMethods, afterMappingMethods, - selectionParameters + selectionParameters, + sourceParameterPresenceCheck ); } } + //CHECKSTYLE:OFF private IterableMappingMethod(Method method, List annotations, Collection existingVariables, Assignment parameterAssignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingReferences, List afterMappingReferences, - SelectionParameters selectionParameters) { + SelectionParameters selectionParameters, PresenceCheck sourceParameterPresenceCheck) { + //CHECKSTYLE:ON super( method, annotations, @@ -86,7 +91,8 @@ private IterableMappingMethod(Method method, List annotations, loopVariableName, beforeMappingReferences, afterMappingReferences, - selectionParameters + selectionParameters, + sourceParameterPresenceCheck ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java index 42dbf826d0..5051c5634c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java @@ -18,11 +18,11 @@ import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.SourceRHS; import org.mapstruct.ap.internal.model.common.Type; -import org.mapstruct.ap.internal.model.presence.NullPresenceCheck; import org.mapstruct.ap.internal.model.source.Method; 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; @@ -182,9 +182,22 @@ public MapMappingMethod build() { ctx.getMessager().note( 2, Message.MAPMAPPING_SELECT_VALUE_NOTE, valueAssignment ); } - // mapNullToDefault + // mapNullToDefault — JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. boolean mapNullToDefault = method.getOptions().getMapMapping().getNullValueMappingStrategy().isReturnDefault(); + if ( !mapNullToDefault + && !method.isUpdateMethod() + && !method.getReturnType().isVoid() ) { + NullabilityResolver.Nullability returnNullability = ctx.getNullabilityResolver().getNullability( + method.getExecutable(), + () -> ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked() ); + if ( returnNullability == NullabilityResolver.Nullability.NON_NULL ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; + } + } MethodReference factoryMethod = null; if ( !method.isUpdateMethod() ) { @@ -201,6 +214,10 @@ public MapMappingMethod build() { List afterMappingMethods = LifecycleMethodResolver.afterMappingMethods( method, null, ctx, existingVariables ); + Parameter sourceParam = first( method.getSourceParameters() ); + PresenceCheck sourceParameterPresenceCheck = + PresenceCheckMethodResolver.getPresenceCheckForSourceParameter( method, null, sourceParam, ctx ); + return new MapMappingMethod( method, getMethodAnnotations(), @@ -210,7 +227,8 @@ public MapMappingMethod build() { factoryMethod, mapNullToDefault, beforeMappingMethods, - afterMappingMethods + afterMappingMethods, + sourceParameterPresenceCheck ); } @@ -229,11 +247,14 @@ protected boolean shouldUsePropertyNamesInHistory() { } + //CHECKSTYLE:OFF private MapMappingMethod(Method method, List annotations, Collection existingVariableNames, Assignment keyAssignment, Assignment valueAssignment, MethodReference factoryMethod, boolean mapNullToDefault, List beforeMappingReferences, - List afterMappingReferences) { + List afterMappingReferences, + PresenceCheck sourceParameterPresenceCheck) { + //CHECKSTYLE:ON super( method, annotations, existingVariableNames, factoryMethod, mapNullToDefault, beforeMappingReferences, afterMappingReferences ); @@ -252,7 +273,7 @@ private MapMappingMethod(Method method, List annotations, } this.sourceParameter = sourceParameter; - this.sourceParameterPresenceCheck = new NullPresenceCheck( this.sourceParameter.getName() ); + this.sourceParameterPresenceCheck = sourceParameterPresenceCheck; } public Parameter getSourceParameter() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java index 1f3fbf6ed2..5746be9287 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java @@ -717,6 +717,7 @@ private Assignment assignToCollection(Type targetType, AccessorType targetAccess .assignment( rhs ) .nullValueCheckStrategy( hasDefaultValueOrDefaultExpression() ? ALWAYS : nvcs ) .nullValuePropertyMappingStrategy( nvpms ) + .sourceJSpecifyNullability( getSourceJSpecifyNullability() ) .build(); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/StreamMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/StreamMappingMethod.java index a774d73065..3955bc47ce 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/StreamMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/StreamMappingMethod.java @@ -15,6 +15,7 @@ import org.mapstruct.ap.internal.model.assignment.Java8FunctionWrapper; import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.PresenceCheck; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; @@ -51,7 +52,8 @@ protected Assignment getWrapper(Assignment assignment, Method method) { protected StreamMappingMethod instantiateMappingMethod(Method method, Collection existingVariables, Assignment assignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingMethods, - List afterMappingMethods, SelectionParameters selectionParameters) { + List afterMappingMethods, SelectionParameters selectionParameters, + PresenceCheck sourceParameterPresenceCheck) { Set helperImports = new HashSet<>(); if ( method.getResultType().isIterableType() ) { @@ -74,7 +76,8 @@ protected StreamMappingMethod instantiateMappingMethod(Method method, Collection beforeMappingMethods, afterMappingMethods, selectionParameters, - helperImports + helperImports, + sourceParameterPresenceCheck ); } } @@ -84,7 +87,7 @@ private StreamMappingMethod(Method method, List annotations, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingReferences, List afterMappingReferences, - SelectionParameters selectionParameters, Set helperImports) { + SelectionParameters selectionParameters, Set helperImports, PresenceCheck sourceParameterPresenceCheck) { super( method, annotations, @@ -95,7 +98,8 @@ private StreamMappingMethod(Method method, List annotations, loopVariableName, beforeMappingReferences, afterMappingReferences, - selectionParameters + selectionParameters, + sourceParameterPresenceCheck ); //CHECKSTYLE:ON this.helperImports = helperImports; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java index 8b45f2b52f..707f3c273f 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java @@ -101,6 +101,7 @@ public enum Message { PROPERTYMAPPING_JSPECIFY_ADD_NULL_CHECK( "JSpecify adding null check for property \"%s\": source=%s, target=%s.", Diagnostic.Kind.NOTE ), PROPERTYMAPPING_JSPECIFY_SKIP_NULL_CHECK( "JSpecify skipping null check for property \"%s\": source=%s, target=%s.", Diagnostic.Kind.NOTE ), PROPERTYMAPPING_JSPECIFY_SKIP_METHOD_GUARD_NON_NULL_PARAM( "JSpecify skipping method-level null guard for property \"%s\": parameter is @NonNull.", Diagnostic.Kind.NOTE ), + MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT( "JSpecify forcing NullValueMappingStrategy.RETURN_DEFAULT for method \"%s\": return type is @NonNull.", Diagnostic.Kind.NOTE ), CONVERSION_LOSSY_WARNING( "%s has a possibly lossy conversion from %s to %s.", Diagnostic.Kind.WARNING ), CONVERSION_LOSSY_ERROR( "Can't map %s. It has a possibly lossy conversion from %s to %s." ), diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl index 0e88d09972..6268366f7b 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl @@ -17,6 +17,7 @@ + <#if sourceParameterPresenceCheck??> if ( <@includeModel object=sourceParameterPresenceCheck.negate() /> ) { <#if !mapNullToDefault> return<#if returnType.name != "void"> <#if existingInstanceMapping>${resultName}<#else>null; @@ -41,6 +42,7 @@ } + <#if resultType.arrayType> <#if !existingInstanceMapping> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/MapMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/MapMappingMethod.ftl index 957dc712ee..e04d1b9475 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/MapMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/MapMappingMethod.ftl @@ -16,6 +16,7 @@ + <#if sourceParameterPresenceCheck??> if ( <@includeModel object=sourceParameterPresenceCheck.negate() /> ) { <#if !mapNullToDefault> return<#if returnType.name != "void"> <#if existingInstanceMapping>${resultName}<#else>null; @@ -28,6 +29,7 @@ } + <#if existingInstanceMapping> ${resultName}.clear(); diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/StreamMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/StreamMappingMethod.ftl index 3cd97ef9ee..0f335f9c54 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/StreamMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/StreamMappingMethod.ftl @@ -18,6 +18,7 @@ + <#if sourceParameterPresenceCheck??> if ( <@includeModel object=sourceParameterPresenceCheck.negate() /> ) { <#if !mapNullToDefault> return<#if returnType.name != "void"> <#if existingInstanceMapping>${resultName}<#else>null; @@ -49,6 +50,7 @@ } + <#-- A variable needs to be defined if there are before or after mappings and this is not exisitingInstanceMapping --> <#assign needVarDefine = (beforeMappingReferencesWithMappingTarget?has_content || afterMappingReferences?has_content) && !existingInstanceMapping /> diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java new file mode 100644 index 0000000000..d705652832 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java @@ -0,0 +1,19 @@ +/* + * 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 org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyCollectionPropertyMapper { + + JSpecifyCollectionPropertyMapper INSTANCE = Mappers.getMapper( JSpecifyCollectionPropertyMapper.class ); + + NullMarkedCollectionTargetBean map(NullMarkedCollectionSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java new file mode 100644 index 0000000000..bf194fc08f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java @@ -0,0 +1,43 @@ +/* + * 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.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyCollectionPropertyTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedCollectionSourceBean.class, + NullMarkedCollectionTargetBean.class, + JSpecifyCollectionPropertyMapper.class + }) + void collectionPropertyWithNonNullSourceSkipsNullCheck() { + generatedSource.addComparisonToFixtureFor( JSpecifyCollectionPropertyMapper.class ); + + NullMarkedCollectionSourceBean source = new NullMarkedCollectionSourceBean(); + source.setValues( Arrays.asList( "a", "b", "c" ) ); + + NullMarkedCollectionTargetBean target = JSpecifyCollectionPropertyMapper.INSTANCE.map( source ); + + assertThat( target ).isNotNull(); + assertThat( target.getValues() ).containsExactly( "a", "b", "c" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java new file mode 100644 index 0000000000..5a1d0b0d68 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java @@ -0,0 +1,23 @@ +/* + * 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.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyIterableMethodMapper { + + JSpecifyIterableMethodMapper INSTANCE = Mappers.getMapper( JSpecifyIterableMethodMapper.class ); + + List mapAll(List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java new file mode 100644 index 0000000000..ceed2236f6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java @@ -0,0 +1,44 @@ +/* + * 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.Arrays; +import java.util.List; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyIterableMethodTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyIterableMethodMapper.class + }) + void iterableMethodWithNonNullSourceSkipsMethodGuard() { + generatedSource.addComparisonToFixtureFor( JSpecifyIterableMethodMapper.class ); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List targets = JSpecifyIterableMethodMapper.INSTANCE.mapAll( Arrays.asList( source ) ); + + assertThat( targets ).hasSize( 1 ); + assertThat( targets.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java new file mode 100644 index 0000000000..cdb844795b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java @@ -0,0 +1,23 @@ +/* + * 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.Map; + +import org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyMapMethodMapper { + + JSpecifyMapMethodMapper INSTANCE = Mappers.getMapper( JSpecifyMapMethodMapper.class ); + + Map mapAll(Map sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java new file mode 100644 index 0000000000..4673841873 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java @@ -0,0 +1,45 @@ +/* + * 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.Collections; +import java.util.Map; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyMapMethodTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyMapMethodMapper.class + }) + void mapMethodWithNonNullSourceSkipsMethodGuard() { + generatedSource.addComparisonToFixtureFor( JSpecifyMapMethodMapper.class ); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + Map targets = + JSpecifyMapMethodMapper.INSTANCE.mapAll( Collections.singletonMap( "k", source ) ); + + assertThat( targets ).hasSize( 1 ); + assertThat( targets.get( "k" ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java new file mode 100644 index 0000000000..8401e56682 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java @@ -0,0 +1,24 @@ +/* + * 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 org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNonNullReturnBeanMapper { + + JSpecifyNonNullReturnBeanMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnBeanMapper.class ); + + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "nonNullByDefault", source = "value") + NullMarkedTargetBean map(@Nullable JSpecifyNonNullReturnBeanSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java new file mode 100644 index 0000000000..d4bdb926c5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java @@ -0,0 +1,22 @@ +/* + * 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 org.jspecify.annotations.NullMarked; + +@NullMarked +public class JSpecifyNonNullReturnBeanSourceBean { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java new file mode 100644 index 0000000000..ccac0e0653 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java @@ -0,0 +1,46 @@ +/* + * 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 org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnBeanTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + JSpecifyNonNullReturnBeanSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnBeanMapper.class + }) + void nonNullReturnForcesMapNullToDefault() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnBeanMapper.class ); + + NullMarkedTargetBean fromNull = JSpecifyNonNullReturnBeanMapper.INSTANCE.map( null ); + + assertThat( fromNull ).isNotNull(); + assertThat( fromNull.getNonNullByDefault() ).isNull(); + + JSpecifyNonNullReturnBeanSourceBean source = new JSpecifyNonNullReturnBeanSourceBean(); + source.setValue( "value" ); + + NullMarkedTargetBean fromSource = JSpecifyNonNullReturnBeanMapper.INSTANCE.map( source ); + + assertThat( fromSource ).isNotNull(); + assertThat( fromSource.getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java new file mode 100644 index 0000000000..4f3fd1c0e2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java @@ -0,0 +1,24 @@ +/* + * 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 JSpecifyNonNullReturnIterableMapper { + + JSpecifyNonNullReturnIterableMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnIterableMapper.class ); + + List mapAll(@Nullable List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java new file mode 100644 index 0000000000..f4ba29bb92 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java @@ -0,0 +1,49 @@ +/* + * 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.Arrays; +import java.util.List; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnIterableTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnIterableMapper.class + }) + void nonNullReturnIterableForcesEmptyDefault() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnIterableMapper.class ); + + List fromNull = JSpecifyNonNullReturnIterableMapper.INSTANCE.mapAll( null ); + + assertThat( fromNull ).isEmpty(); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List fromSource = + JSpecifyNonNullReturnIterableMapper.INSTANCE.mapAll( Arrays.asList( source ) ); + + assertThat( fromSource ).hasSize( 1 ); + assertThat( fromSource.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java new file mode 100644 index 0000000000..8e6a0cb885 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java @@ -0,0 +1,24 @@ +/* + * 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.Map; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNonNullReturnMapMapper { + + JSpecifyNonNullReturnMapMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnMapMapper.class ); + + Map mapAll(@Nullable Map sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java new file mode 100644 index 0000000000..b96d3f3fc0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java @@ -0,0 +1,49 @@ +/* + * 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.Collections; +import java.util.Map; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnMapTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnMapMapper.class + }) + void nonNullReturnMapForcesEmptyDefault() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnMapMapper.class ); + + Map fromNull = JSpecifyNonNullReturnMapMapper.INSTANCE.mapAll( null ); + + assertThat( fromNull ).isEmpty(); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + Map fromSource = + JSpecifyNonNullReturnMapMapper.INSTANCE.mapAll( Collections.singletonMap( "k", source ) ); + + assertThat( fromSource ).hasSize( 1 ); + assertThat( fromSource.get( "k" ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java new file mode 100644 index 0000000000..3c67457637 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * Source bean with a {@code @NonNull} collection getter (via @NullMarked scope). + */ +@NullMarked +public class NullMarkedCollectionSourceBean { + + private List values; + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java new file mode 100644 index 0000000000..eae3adc1a9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java @@ -0,0 +1,24 @@ +/* + * 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; + +@NullMarked +public class NullMarkedCollectionTargetBean { + + private List values; + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java new file mode 100644 index 0000000000..a655a662dc --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java @@ -0,0 +1,43 @@ +/* + * 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.LinkedHashMap; +import java.util.Map; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-05-14T23:31:18+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyMapMethodMapperImpl implements JSpecifyMapMethodMapper { + + @Override + public Map mapAll(Map sources) { + + Map map = LinkedHashMap.newLinkedHashMap( sources.size() ); + + for ( java.util.Map.Entry entry : sources.entrySet() ) { + String key = entry.getKey(); + NullMarkedTargetBean value = map( entry.getValue() ); + map.put( key, value ); + } + + return map; + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java new file mode 100644 index 0000000000..eefeafd349 --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java @@ -0,0 +1,46 @@ +/* + * 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.LinkedHashMap; +import java.util.Map; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-05-15T00:03:32+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyNonNullReturnMapMapperImpl implements JSpecifyNonNullReturnMapMapper { + + @Override + public Map mapAll(Map sources) { + if ( sources == null ) { + return new LinkedHashMap<>(); + } + + Map map = LinkedHashMap.newLinkedHashMap( sources.size() ); + + for ( java.util.Map.Entry entry : sources.entrySet() ) { + String key = entry.getKey(); + NullMarkedTargetBean value = map( entry.getValue() ); + map.put( key, value ); + } + + return map; + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapperImpl.java new file mode 100644 index 0000000000..4004bc60fe --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapperImpl.java @@ -0,0 +1,26 @@ +/* + * 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 javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-05-14T23:05:10+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyCollectionPropertyMapperImpl implements JSpecifyCollectionPropertyMapper { + + @Override + public NullMarkedCollectionTargetBean map(NullMarkedCollectionSourceBean source) { + + NullMarkedCollectionTargetBean nullMarkedCollectionTargetBean = new NullMarkedCollectionTargetBean(); + + nullMarkedCollectionTargetBean.setValues( source.getValues() ); + + return nullMarkedCollectionTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapperImpl.java new file mode 100644 index 0000000000..5336db031a --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapperImpl.java @@ -0,0 +1,40 @@ +/* + * 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.ArrayList; +import java.util.List; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-05-14T23:18:22+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyIterableMethodMapperImpl implements JSpecifyIterableMethodMapper { + + @Override + public List mapAll(List sources) { + + List list = new ArrayList<>( sources.size() ); + for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) { + list.add( map( nullMarkedSourceBean ) ); + } + + return list; + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java new file mode 100644 index 0000000000..2a74b75c6e --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java @@ -0,0 +1,43 @@ +/* + * 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.LinkedHashMap; +import java.util.Map; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-05-14T23:30:08+0200", + comments = "version: , compiler: Eclipse JDT (Batch) 3.20.0.v20191203-2131, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyMapMethodMapperImpl implements JSpecifyMapMethodMapper { + + @Override + public Map mapAll(Map sources) { + + Map map = new LinkedHashMap<>( Math.max( (int) ( sources.size() / .75f ) + 1, 16 ) ); + + for ( java.util.Map.Entry entry : sources.entrySet() ) { + String key = entry.getKey(); + NullMarkedTargetBean value = map( entry.getValue() ); + map.put( key, value ); + } + + return map; + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapperImpl.java new file mode 100644 index 0000000000..c1efd39d0d --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapperImpl.java @@ -0,0 +1,28 @@ +/* + * 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 javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-05-14T23:41:40+0200", + comments = "version: , compiler: Eclipse JDT (Batch) 3.20.0.v20191203-2131, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyNonNullReturnBeanMapperImpl implements JSpecifyNonNullReturnBeanMapper { + + @Override + public NullMarkedTargetBean map(JSpecifyNonNullReturnBeanSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + if ( source != null ) { + nullMarkedTargetBean.setNonNullByDefault( source.getValue() ); + } + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapperImpl.java new file mode 100644 index 0000000000..8c6de34e37 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapperImpl.java @@ -0,0 +1,43 @@ +/* + * 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.ArrayList; +import java.util.List; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-05-14T23:55:43+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyNonNullReturnIterableMapperImpl implements JSpecifyNonNullReturnIterableMapper { + + @Override + public List mapAll(List sources) { + if ( sources == null ) { + return new ArrayList<>(); + } + + List list = new ArrayList<>( sources.size() ); + for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) { + list.add( map( nullMarkedSourceBean ) ); + } + + return list; + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java new file mode 100644 index 0000000000..ffd45d3855 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java @@ -0,0 +1,46 @@ +/* + * 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.LinkedHashMap; +import java.util.Map; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-05-15T00:03:33+0200", + comments = "version: , compiler: Eclipse JDT (Batch) 3.20.0.v20191203-2131, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyNonNullReturnMapMapperImpl implements JSpecifyNonNullReturnMapMapper { + + @Override + public Map mapAll(Map sources) { + if ( sources == null ) { + return new LinkedHashMap<>(); + } + + Map map = new LinkedHashMap<>( Math.max( (int) ( sources.size() / .75f ) + 1, 16 ) ); + + for ( java.util.Map.Entry entry : sources.entrySet() ) { + String key = entry.getKey(); + NullMarkedTargetBean value = map( entry.getValue() ); + map.put( key, value ); + } + + return map; + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} From f43b3cb2f6498ad829bc0f4a0a1226f0ce2c0daa Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 5 Jun 2026 15:03:29 +0200 Subject: [PATCH 2/3] #4056 Address review: extract @NonNull-return helper, add Stream + negative tests - Extract the duplicated 'JSpecify @NonNull return forces RETURN_DEFAULT' logic into MappingBuilderContext#isJSpecifyNonNullReturn, used by BeanMappingMethod, ContainerMappingMethodBuilder and MapMappingMethod. - Clarify the comments explaining why the bean path keeps the extra presence-check guard while container/map/stream force unconditionally. - Add Stream coverage (was entirely untested): JSpecifyStreamMethodTest and JSpecifyNonNullReturnStreamTest with fixtures. - Add negative/control coverage: nullable source keeps the method guard, @NonNull return overrides an explicit RETURN_NULL, update methods are not forced to RETURN_DEFAULT, and mapstruct.disableJSpecify suppresses the new container-source and @NonNull-return behaviour. --- .../ap/internal/model/BeanMappingMethod.java | 29 +++++------ .../model/ContainerMappingMethodBuilder.java | 22 +++----- .../ap/internal/model/MapMappingMethod.java | 22 +++----- .../internal/model/MappingBuilderContext.java | 22 ++++++++ ...JSpecifyDisabledContainerSourceMapper.java | 28 ++++++++++ .../JSpecifyDisabledNonNullReturnMapper.java | 29 +++++++++++ .../jspecify/JSpecifyDisabledTest.java | 26 ++++++++++ .../JSpecifyNonNullReturnStreamMapper.java | 24 +++++++++ .../JSpecifyNonNullReturnStreamTest.java | 52 +++++++++++++++++++ ...cifyNonNullReturnUpdateIterableMapper.java | 27 ++++++++++ ...pecifyNonNullReturnUpdateIterableTest.java | 46 ++++++++++++++++ .../JSpecifyNullableSourceIterableMapper.java | 25 +++++++++ .../JSpecifyNullableSourceIterableTest.java | 52 +++++++++++++++++++ ...ecifyReturnNullOverrideIterableMapper.java | 29 +++++++++++ ...SpecifyReturnNullOverrideIterableTest.java | 40 ++++++++++++++ .../jspecify/JSpecifyStreamMethodMapper.java | 23 ++++++++ .../jspecify/JSpecifyStreamMethodTest.java | 47 +++++++++++++++++ ...JSpecifyNonNullReturnStreamMapperImpl.java | 37 +++++++++++++ ...NonNullReturnUpdateIterableMapperImpl.java | 42 +++++++++++++++ ...ecifyNullableSourceIterableMapperImpl.java | 43 +++++++++++++++ ...yReturnNullOverrideIterableMapperImpl.java | 43 +++++++++++++++ .../JSpecifyStreamMethodMapperImpl.java | 34 ++++++++++++ 22 files changed, 698 insertions(+), 44 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapperImpl.java diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index 8fcb866d06..080eab1e76 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java @@ -64,7 +64,6 @@ import org.mapstruct.ap.internal.model.source.selector.SelectedMethod; 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 org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.internal.util.accessor.AccessorType; @@ -508,22 +507,20 @@ else if ( !method.isUpdateMethod() ) { } } - // JSpecify: @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. - // Only applies when there are nullable source parameters (presence checks exist), since without them - // the template never generates a `return null` block in the first place. + // JSpecify: a @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. + // The extra presence-check guard is bean-specific and required for correctness, not just an + // optimization: the bean template only emits a `return null` (and the presence-check wrapping that + // forcing mapNullToDefault would alter) when there are nullable source parameters. With none, + // getPresenceCheckByParameter would resolve to null in the single-source template branches. + // Container/Map/Stream methods instead gate this in their templates via `sourceParameterPresenceCheck??`, + // so they force unconditionally. if ( !mapNullToDefault - && !method.isUpdateMethod() - && !method.getReturnType().isVoid() - && !presenceChecksByParameter.isEmpty() ) { - NullabilityResolver.Nullability returnNullability = ctx.getNullabilityResolver().getNullability( - method.getExecutable(), - () -> ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked() ); - if ( returnNullability == NullabilityResolver.Nullability.NON_NULL ) { - ctx.getMessager().note( 2, - Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, - method.getName() ); - mapNullToDefault = true; - } + && !presenceChecksByParameter.isEmpty() + && ctx.isJSpecifyNonNullReturn( method ) ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; } return new BeanMappingMethod( diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java index c4da5b8b17..598db971e2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ContainerMappingMethodBuilder.java @@ -21,7 +21,6 @@ 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; @@ -127,23 +126,18 @@ public final M build() { } assignment = getWrapper( assignment, method ); - // mapNullToDefault — JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. + // 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 + // the whole guard block via `sourceParameterPresenceCheck??`, so the forced value is simply unused there. boolean mapNullToDefault = method.getOptions() .getIterableMapping() .getNullValueMappingStrategy() .isReturnDefault(); - if ( !mapNullToDefault - && !method.isUpdateMethod() - && !method.getReturnType().isVoid() ) { - NullabilityResolver.Nullability returnNullability = ctx.getNullabilityResolver().getNullability( - method.getExecutable(), - () -> ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked() ); - if ( returnNullability == NullabilityResolver.Nullability.NON_NULL ) { - ctx.getMessager().note( 2, - Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, - method.getName() ); - mapNullToDefault = true; - } + if ( !mapNullToDefault && ctx.isJSpecifyNonNullReturn( method ) ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; } MethodReference factoryMethod = null; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java index 5051c5634c..5a49e3a726 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MapMappingMethod.java @@ -22,7 +22,6 @@ 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; @@ -182,21 +181,16 @@ public MapMappingMethod build() { ctx.getMessager().note( 2, Message.MAPMAPPING_SELECT_VALUE_NOTE, valueAssignment ); } - // mapNullToDefault — JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. + // 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 + // the whole guard block via `sourceParameterPresenceCheck??`, so the forced value is simply unused there. boolean mapNullToDefault = method.getOptions().getMapMapping().getNullValueMappingStrategy().isReturnDefault(); - if ( !mapNullToDefault - && !method.isUpdateMethod() - && !method.getReturnType().isVoid() ) { - NullabilityResolver.Nullability returnNullability = ctx.getNullabilityResolver().getNullability( - method.getExecutable(), - () -> ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked() ); - if ( returnNullability == NullabilityResolver.Nullability.NON_NULL ) { - ctx.getMessager().note( 2, - Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, - method.getName() ); - mapNullToDefault = true; - } + if ( !mapNullToDefault && ctx.isJSpecifyNonNullReturn( method ) ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; } MethodReference factoryMethod = null; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java index a933cc4b8f..5d9d077740 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java @@ -209,6 +209,28 @@ public NullabilityResolver getNullabilityResolver() { return nullabilityResolver; } + /** + * 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 + * force {@link org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem#RETURN_DEFAULT} semantics. + *

+ * Update methods and {@code void}-returning methods never generate a {@code return null} and are excluded. + * + * @param method the mapping method to inspect + * + * @return {@code true} if the return type is {@code @NonNull}, {@code false} otherwise + */ + public boolean isJSpecifyNonNullReturn(Method method) { + if ( method.isUpdateMethod() || method.getReturnType().isVoid() ) { + return false; + } + + NullabilityResolver.Nullability returnNullability = nullabilityResolver.getNullability( + method.getExecutable(), + () -> typeFactory.getType( mapperTypeElement.asType() ).isNullMarked() ); + return returnNullability == NullabilityResolver.Nullability.NON_NULL; + } + public EnumMappingStrategy getEnumMappingStrategy() { return enumMappingStrategy; } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java new file mode 100644 index 0000000000..2de32c4235 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java @@ -0,0 +1,28 @@ +/* + * 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.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * Dedicated to {@link JSpecifyDisabledTest} so its generated implementation is not shared (and pre-loaded) by an + * enabled test in the same JVM. With JSpecify enabled the {@code @NonNull} source would skip the method-level guard. + */ +@NullMarked +@Mapper +public interface JSpecifyDisabledContainerSourceMapper { + + JSpecifyDisabledContainerSourceMapper INSTANCE = + Mappers.getMapper( JSpecifyDisabledContainerSourceMapper.class ); + + List mapAll(List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java new file mode 100644 index 0000000000..bcdb0bc0ca --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * Dedicated to {@link JSpecifyDisabledTest} so its generated implementation is not shared (and pre-loaded) by an + * enabled test in the same JVM. With JSpecify enabled the {@code @NonNull} return would force RETURN_DEFAULT. + */ +@NullMarked +@Mapper +public interface JSpecifyDisabledNonNullReturnMapper { + + JSpecifyDisabledNonNullReturnMapper INSTANCE = + Mappers.getMapper( JSpecifyDisabledNonNullReturnMapper.class ); + + List mapAll(@Nullable List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java index 547365467f..d669952737 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java @@ -54,4 +54,30 @@ public void disabledFlagSkipsConstructorNonNullHardError() { // The JSpecify-driven hard error for a @Nullable source mapped to a @NonNull constructor // parameter is purely a JSpecify signal; with the flag disabled it must not be raised. } + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyDisabledContainerSourceMapper.class + }) + public void disabledFlagKeepsContainerMethodSourceGuard() { + // With JSpecify enabled the @NonNull source parameter skips the method-level null guard. When disabled, + // the resolver reports UNKNOWN, so the unconditional "if (sources == null) return null;" guard is kept and + // mapping a null source returns null rather than throwing an NPE. + assertThat( JSpecifyDisabledContainerSourceMapper.INSTANCE.mapAll( null ) ).isNull(); + } + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyDisabledNonNullReturnMapper.class + }) + public void disabledFlagSkipsNonNullReturnForcing() { + // With JSpecify enabled the @NonNull return forces RETURN_DEFAULT (empty list for a null source). When + // disabled, that forcing is suppressed and the default RETURN_NULL strategy applies, so a null source + // maps to null. + assertThat( JSpecifyDisabledNonNullReturnMapper.INSTANCE.mapAll( null ) ).isNull(); + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java new file mode 100644 index 0000000000..62d061a001 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java @@ -0,0 +1,24 @@ +/* + * 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.stream.Stream; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNonNullReturnStreamMapper { + + JSpecifyNonNullReturnStreamMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnStreamMapper.class ); + + Stream mapAll(@Nullable Stream sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java new file mode 100644 index 0000000000..fa62973ebf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java @@ -0,0 +1,52 @@ +/* + * 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 java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnStreamTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnStreamMapper.class + }) + void nonNullReturnStreamForcesEmptyDefault() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnStreamMapper.class ); + + Stream fromNull = JSpecifyNonNullReturnStreamMapper.INSTANCE.mapAll( null ); + + assertThat( fromNull ).isNotNull(); + assertThat( fromNull.collect( Collectors.toList() ) ).isEmpty(); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List fromSource = JSpecifyNonNullReturnStreamMapper.INSTANCE + .mapAll( Stream.of( source ) ) + .collect( Collectors.toList() ); + + assertThat( fromSource ).hasSize( 1 ); + assertThat( fromSource.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java new file mode 100644 index 0000000000..b79763d712 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java @@ -0,0 +1,27 @@ +/* + * 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.MappingTarget; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyNonNullReturnUpdateIterableMapper { + + JSpecifyNonNullReturnUpdateIterableMapper INSTANCE = + Mappers.getMapper( JSpecifyNonNullReturnUpdateIterableMapper.class ); + + List mapAll(@Nullable List sources, + @MappingTarget List target); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java new file mode 100644 index 0000000000..56e71e6995 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java @@ -0,0 +1,46 @@ +/* + * 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.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The @NonNull-return forcing is gated on {@code !isUpdateMethod()}. An update (existing-instance) method must not + * be forced to RETURN_DEFAULT: a null source returns the supplied target instance, not a fresh empty collection. + */ +@IssueKey("1243") +@WithJSpecify +class JSpecifyNonNullReturnUpdateIterableTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNonNullReturnUpdateIterableMapper.class + }) + void updateMethodWithNonNullReturnIsNotForced() { + generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnUpdateIterableMapper.class ); + + List target = new ArrayList<>(); + + List result = JSpecifyNonNullReturnUpdateIterableMapper.INSTANCE.mapAll( null, target ); + + assertThat( result ).isSameAs( target ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java new file mode 100644 index 0000000000..026890dee8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java @@ -0,0 +1,25 @@ +/* + * 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 JSpecifyNullableSourceIterableMapper { + + JSpecifyNullableSourceIterableMapper INSTANCE = Mappers.getMapper( JSpecifyNullableSourceIterableMapper.class ); + + @Nullable + List mapAll(@Nullable List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java new file mode 100644 index 0000000000..da77fe71e1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java @@ -0,0 +1,52 @@ +/* + * 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.Arrays; +import java.util.List; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Negative counterpart to {@link JSpecifyIterableMethodTest}: a {@code @Nullable} source parameter (and a + * {@code @Nullable} return, so the @NonNull-return forcing does not kick in) must keep the method-level + * {@code if ( sources == null ) return null;} guard. + */ +@IssueKey("1243") +@WithJSpecify +class JSpecifyNullableSourceIterableTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyNullableSourceIterableMapper.class + }) + void nullableSourceKeepsMethodGuard() { + generatedSource.addComparisonToFixtureFor( JSpecifyNullableSourceIterableMapper.class ); + + assertThat( JSpecifyNullableSourceIterableMapper.INSTANCE.mapAll( null ) ).isNull(); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List targets = + JSpecifyNullableSourceIterableMapper.INSTANCE.mapAll( Arrays.asList( source ) ); + + assertThat( targets ).hasSize( 1 ); + assertThat( targets.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java new file mode 100644 index 0000000000..387103bae5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java @@ -0,0 +1,29 @@ +/* + * 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.IterableMapping; +import org.mapstruct.Mapper; +import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyReturnNullOverrideIterableMapper { + + JSpecifyReturnNullOverrideIterableMapper INSTANCE = + Mappers.getMapper( JSpecifyReturnNullOverrideIterableMapper.class ); + + // Explicit RETURN_NULL, but the @NonNull return type (NullMarked scope) wins and forces RETURN_DEFAULT. + @IterableMapping(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL) + List mapAll(@Nullable List sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java new file mode 100644 index 0000000000..eaea578903 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java @@ -0,0 +1,40 @@ +/* + * 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 org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that a JSpecify {@code @NonNull} return type wins over an explicitly configured + * {@code NullValueMappingStrategy.RETURN_NULL}: the generated method returns an empty collection rather + * than {@code null}. + */ +@IssueKey("1243") +@WithJSpecify +class JSpecifyReturnNullOverrideIterableTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyReturnNullOverrideIterableMapper.class + }) + void nonNullReturnOverridesExplicitReturnNull() { + generatedSource.addComparisonToFixtureFor( JSpecifyReturnNullOverrideIterableMapper.class ); + + assertThat( JSpecifyReturnNullOverrideIterableMapper.INSTANCE.mapAll( null ) ).isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java new file mode 100644 index 0000000000..1b069af5b8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java @@ -0,0 +1,23 @@ +/* + * 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.stream.Stream; + +import org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyStreamMethodMapper { + + JSpecifyStreamMethodMapper INSTANCE = Mappers.getMapper( JSpecifyStreamMethodMapper.class ); + + Stream mapAll(Stream sources); + + NullMarkedTargetBean map(NullMarkedSourceBean source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java new file mode 100644 index 0000000000..76c7af8792 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java @@ -0,0 +1,47 @@ +/* + * 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 java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithJSpecify; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("1243") +@WithJSpecify +class JSpecifyStreamMethodTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses({ + NullMarkedSourceBean.class, + NullMarkedTargetBean.class, + JSpecifyStreamMethodMapper.class + }) + void streamMethodWithNonNullSourceSkipsMethodGuard() { + generatedSource.addComparisonToFixtureFor( JSpecifyStreamMethodMapper.class ); + + NullMarkedSourceBean source = new NullMarkedSourceBean(); + source.setNonNullByDefault( "value" ); + + List targets = JSpecifyStreamMethodMapper.INSTANCE + .mapAll( Stream.of( source ) ) + .collect( Collectors.toList() ); + + assertThat( targets ).hasSize( 1 ); + assertThat( targets.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapperImpl.java new file mode 100644 index 0000000000..01c2981ab1 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapperImpl.java @@ -0,0 +1,37 @@ +/* + * 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.stream.Stream; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-05T14:55:49+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyNonNullReturnStreamMapperImpl implements JSpecifyNonNullReturnStreamMapper { + + @Override + public Stream mapAll(Stream sources) { + if ( sources == null ) { + return Stream.empty(); + } + + return sources.map( nullMarkedSourceBean -> map( nullMarkedSourceBean ) ); + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapperImpl.java new file mode 100644 index 0000000000..70b8fbea1d --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapperImpl.java @@ -0,0 +1,42 @@ +/* + * 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 javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-05T15:01:46+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyNonNullReturnUpdateIterableMapperImpl implements JSpecifyNonNullReturnUpdateIterableMapper { + + @Override + public List mapAll(List sources, List target) { + if ( sources == null ) { + return target; + } + + target.clear(); + for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) { + target.add( map( nullMarkedSourceBean ) ); + } + + return target; + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapperImpl.java new file mode 100644 index 0000000000..2a6950f71d --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapperImpl.java @@ -0,0 +1,43 @@ +/* + * 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.ArrayList; +import java.util.List; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-05T14:55:49+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyNullableSourceIterableMapperImpl implements JSpecifyNullableSourceIterableMapper { + + @Override + public List mapAll(List sources) { + if ( sources == null ) { + return null; + } + + List list = new ArrayList<>( sources.size() ); + for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) { + list.add( map( nullMarkedSourceBean ) ); + } + + return list; + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapperImpl.java new file mode 100644 index 0000000000..52313a0327 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapperImpl.java @@ -0,0 +1,43 @@ +/* + * 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.ArrayList; +import java.util.List; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-05T14:55:49+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyReturnNullOverrideIterableMapperImpl implements JSpecifyReturnNullOverrideIterableMapper { + + @Override + public List mapAll(List sources) { + if ( sources == null ) { + return new ArrayList<>(); + } + + List list = new ArrayList<>( sources.size() ); + for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) { + list.add( map( nullMarkedSourceBean ) ); + } + + return list; + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapperImpl.java new file mode 100644 index 0000000000..4f276edc03 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapperImpl.java @@ -0,0 +1,34 @@ +/* + * 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.stream.Stream; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-05T14:54:43+0200", + comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)" +) +public class JSpecifyStreamMethodMapperImpl implements JSpecifyStreamMethodMapper { + + @Override + public Stream mapAll(Stream sources) { + + return sources.map( nullMarkedSourceBean -> map( nullMarkedSourceBean ) ); + } + + @Override + public NullMarkedTargetBean map(NullMarkedSourceBean source) { + + NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean(); + + nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() ); + nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() ); + + return nullMarkedTargetBean; + } +} From 3a4ace47fff7a68056ec7f5936f4af83544c698d Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 5 Jun 2026 21:22:06 +0200 Subject: [PATCH 3/3] #4056 Centralize JSpecify mapper-scope nullability resolution The supplier resolving the mapper type's @NullMarked scope (() -> typeFactory.getType(mapperTypeElement.asType()).isNullMarked()) was duplicated across MappingBuilderContext, PresenceCheckMethodResolver and PropertyMapping for elements declared directly on the mapper (return types, source parameters). Extract it into MappingBuilderContext#getNullabilityInMapperScope and route all three sites through it. --- .../internal/model/MappingBuilderContext.java | 21 +++++++++++++++---- .../model/PresenceCheckMethodResolver.java | 7 ++----- .../ap/internal/model/PropertyMapping.java | 5 +---- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java index 5d9d077740..39e8558e5e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java @@ -13,6 +13,7 @@ import java.util.Set; import java.util.function.Supplier; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import org.mapstruct.ap.internal.model.common.Assignment; @@ -209,6 +210,21 @@ public NullabilityResolver getNullabilityResolver() { return nullabilityResolver; } + /** + * Resolves the JSpecify nullability of an element declared directly on the mapper (e.g. a mapping method's + * return type or one of its source parameters), using the mapper type's {@code @NullMarked} scope as the + * enclosing scope for unannotated elements. + * + * @param element the element declared on the mapper to inspect + * + * @return the resolved nullability ({@link NullabilityResolver.Nullability#UNKNOWN} when JSpecify is disabled) + */ + public NullabilityResolver.Nullability getNullabilityInMapperScope(Element element) { + return nullabilityResolver.getNullability( + element, + () -> 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 @@ -225,10 +241,7 @@ public boolean isJSpecifyNonNullReturn(Method method) { return false; } - NullabilityResolver.Nullability returnNullability = nullabilityResolver.getNullability( - method.getExecutable(), - () -> typeFactory.getType( mapperTypeElement.asType() ).isNullMarked() ); - return returnNullability == NullabilityResolver.Nullability.NON_NULL; + return getNullabilityInMapperScope( method.getExecutable() ) == NullabilityResolver.Nullability.NON_NULL; } public EnumMappingStrategy getEnumMappingStrategy() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java index 8500b28d49..3163614871 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PresenceCheckMethodResolver.java @@ -96,11 +96,8 @@ public static PresenceCheck getPresenceCheckForSourceParameter( } else if ( !sourceParameter.getType().isPrimitive() ) { // If the source parameter is @NonNull (JSpecify), skip the null guard entirely. - // Use the mapper type for @NullMarked scope resolution since the parameter - // is declared in the mapper interface. - if ( ctx.getNullabilityResolver().getNullability( - sourceParameter.getElement(), - () -> ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked() ) + // Resolved in the mapper's @NullMarked scope since the parameter is declared in the mapper interface. + if ( ctx.getNullabilityInMapperScope( sourceParameter.getElement() ) == NullabilityResolver.Nullability.NON_NULL ) { ctx.getMessager().note( 2, Message.PROPERTYMAPPING_JSPECIFY_SKIP_METHOD_GUARD_NON_NULL_PARAM, diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java index 5746be9287..e7a7ab7420 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java @@ -626,10 +626,7 @@ private NullabilityResolver.Nullability getSourceJSpecifyNullability() { // Use the mapper type for @NullMarked scope resolution since the parameter is declared there. Parameter parameter = sourceReference.getParameter(); if ( parameter != null && parameter.getElement() != null ) { - return ctx.getNullabilityResolver().getNullability( - parameter.getElement(), - () -> ctx.getTypeFactory().getType( ctx.getMapperTypeElement().asType() ).isNullMarked() - ); + return ctx.getNullabilityInMapperScope( parameter.getElement() ); } return NullabilityResolver.Nullability.UNKNOWN; }