From 9e41476aac4846c35157f630a55d9741cc21ff4d Mon Sep 17 00:00:00 2001 From: agarg6 Date: Fri, 14 Aug 2026 08:04:39 -0700 Subject: [PATCH] #4076 Propagate JSpecify nullability annotations to the mapper implementation Generated mapper implementations dropped the JSpecify `@Nullable` annotations declared on the mapper method they implement, so IDEs reported "Overriding method parameters are not annotated" on the generated `@Override` methods. The nullability information was already resolved for null-check decisions (NullabilityResolver), but was never emitted into the generated signature. This propagates it: * `Parameter` and `MappingMethod` now carry the type of the `@Nullable` annotation to emit, and include it in their import types so the annotation is imported rather than fully qualified. * `AbstractMappingMethodBuilder#propagateNullability` resolves the nullability of the overridden method's return type and parameters via the existing `MappingBuilderContext#getNullabilityInMapperScope`, and is called from `BeanMappingMethod.Builder` and `ContainerMappingMethodBuilder` (covering bean, iterable, map and stream mapping methods). * The corresponding FTL templates emit the annotation inline, directly in front of the return type / parameter type. Since JSpecify's `@Nullable` and `@NonNull` are `TYPE_USE`-only annotations, they are not valid in method-declaration position and therefore cannot go through the existing declaration-level `annotations` list. Only an explicit `@Nullable` is reproduced. `@NonNull` is deliberately left out: inside a `@NullMarked` scope it is the implied default, so emitting it would add noise to every generated signature without changing the contract. Nothing is propagated for forged methods, which do not override a mapper method. All of this stays gated behind the existing `mapstruct.disableJSpecify` option through the resolver. --- NEXT_RELEASE_CHANGELOG.md | 2 + ...apter-10-advanced-mapping-options.asciidoc | 37 ++++++++++ .../model/AbstractMappingMethodBuilder.java | 47 ++++++++++++ .../ap/internal/model/BeanMappingMethod.java | 6 +- .../model/ContainerMappingMethodBuilder.java | 6 +- .../ap/internal/model/MappingMethod.java | 26 ++++++- .../ap/internal/model/common/Parameter.java | 29 +++++++- .../ap/internal/model/BeanMappingMethod.ftl | 2 +- .../internal/model/IterableMappingMethod.ftl | 2 +- .../ap/internal/model/MapMappingMethod.ftl | 2 +- .../ap/internal/model/StreamMappingMethod.ftl | 2 +- .../ap/internal/model/common/Parameter.ftl | 2 +- .../bugs/_4076/Issue4076IterableMapper.java | 51 +++++++++++++ .../ap/test/bugs/_4076/Issue4076Mapper.java | 71 +++++++++++++++++++ .../ap/test/bugs/_4076/Issue4076Test.java | 61 ++++++++++++++++ .../ap/test/bugs/_4076/package-info.java | 9 +++ .../JSpecifyNonNullReturnBeanMapperImpl.java | 3 +- ...pecifyNonNullReturnIterableMapperImpl.java | 3 +- ...JSpecifyNonNullReturnStreamMapperImpl.java | 3 +- ...NonNullReturnUpdateIterableMapperImpl.java | 3 +- ...ecifyNullableSourceIterableMapperImpl.java | 3 +- ...yReturnNullOverrideIterableMapperImpl.java | 3 +- 22 files changed, 357 insertions(+), 16 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076IterableMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076Mapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076Test.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/package-info.java diff --git a/NEXT_RELEASE_CHANGELOG.md b/NEXT_RELEASE_CHANGELOG.md index e0f4cd31f0..50240040cf 100644 --- a/NEXT_RELEASE_CHANGELOG.md +++ b/NEXT_RELEASE_CHANGELOG.md @@ -4,6 +4,8 @@ ### Bugs +* Propagate JSpecify `@Nullable` from the mapper method to the generated implementation's return type and parameters (#4076) + ### Documentation ### Build 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 306169193d..54e17ee9de 100644 --- a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc +++ b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc @@ -334,6 +334,43 @@ This rule applies uniformly to bean, iterable, map, and stream mapping methods. When the return type of a mapping method is `@NonNull` (directly or via a `@NullMarked` scope), MapStruct forces `NullValueMappingStrategy.RETURN_DEFAULT` semantics. The generated method returns a default-constructed target (bean methods), an empty collection (`Iterable` / array mappings), an empty map (`Map` mappings), or `Stream.empty()` (stream mappings) rather than `null`, so the return contract is never violated. This rule applies regardless of the explicit `NullValueMappingStrategy` setting. +==== Nullability in the generated implementation + +The generated implementation reproduces the `@Nullable` annotations of the mapping method it implements, so the implementation keeps the nullability contract of its interface and IDEs do not report the overriding method as missing nullness information: + +.Mapping method with JSpecify nullness annotations +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface CarMapper { + + @Nullable + CarDto carToCarDto(Car car, @Nullable Options options); +} +---- +==== + +The following implementation is generated: + +.Generated implementation keeping the nullability contract +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public class CarMapperImpl implements CarMapper { + + @Override + public @Nullable CarDto carToCarDto(Car car, @Nullable Options options) { + // ... + } +} +---- +==== + +Only an explicit `@Nullable` is reproduced. A `@NonNull` nullability is deliberately not emitted: within a `@NullMarked` scope it is the implied default, so writing it out would add noise to every generated signature without changing the contract. Methods that MapStruct generates as internal helpers (rather than as an implementation of a mapper method) are not annotated, since they do not have to match an inherited signature. + ==== Constructor parameter constraint If a property mapping would assign a potentially `null` source value to a `@NonNull` constructor parameter, MapStruct raises a *compilation error*. Neither inserting a null check (which would leave the variable at `null` and violate the contract) nor passing the value through is safe. Provide a `defaultValue` or `defaultExpression` on the `@Mapping` to satisfy the parameter when the source is absent. diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractMappingMethodBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractMappingMethodBuilder.java index c124306105..ecf06a7793 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractMappingMethodBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/AbstractMappingMethodBuilder.java @@ -8,12 +8,16 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import javax.lang.model.element.Element; import org.mapstruct.ap.internal.model.beanmapping.MappingReferences; import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.Parameter; 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.util.JSpecifyConstants; +import org.mapstruct.ap.internal.util.NullabilityResolver; import org.mapstruct.ap.internal.util.Strings; /** @@ -137,4 +141,47 @@ public List getMethodAnnotations() { return annotations; } + /** + * Propagates the JSpecify {@code @Nullable} annotations of the overridden mapper method onto the generated + * implementation's return type and source parameters. + *

+ * Only explicit {@code @Nullable} annotations are reproduced. A {@code @NonNull} nullability is left out on + * purpose: within a {@code @NullMarked} scope it is the implied default, so emitting it would add noise to every + * generated signature without changing the contract. + *

+ * Nothing is propagated for methods that do not override a mapper method (e.g. forged methods), since those are + * generated helpers that do not have to match an inherited signature. + * + * @param mappingMethod the mapping method to annotate + */ + protected void propagateNullability(MappingMethod mappingMethod) { + if ( method instanceof ForgedMethod || !method.overridesMethod() ) { + return; + } + + Type nullableType = null; + + if ( isJSpecifyNullable( method.getExecutable() ) ) { + nullableType = getNullableAnnotationType(); + mappingMethod.setNullableReturnTypeAnnotationType( nullableType ); + } + + for ( Parameter parameter : mappingMethod.getParameters() ) { + if ( parameter.getElement() != null && isJSpecifyNullable( parameter.getElement() ) ) { + if ( nullableType == null ) { + nullableType = getNullableAnnotationType(); + } + parameter.setNullableAnnotationType( nullableType ); + } + } + } + + private boolean isJSpecifyNullable(Element element) { + return ctx.getNullabilityInMapperScope( element ) == NullabilityResolver.Nullability.NULLABLE; + } + + private Type getNullableAnnotationType() { + return ctx.getTypeFactory().getType( JSpecifyConstants.NULLABLE_FQN ); + } + } 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 080eab1e76..24004c87f6 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 @@ -523,7 +523,7 @@ else if ( !method.isUpdateMethod() ) { mapNullToDefault = true; } - return new BeanMappingMethod( + BeanMappingMethod beanMappingMethod = new BeanMappingMethod( method, getMethodAnnotations(), existingVariableNames, @@ -544,6 +544,10 @@ else if ( !method.isUpdateMethod() ) { subclassExhaustiveExceptionType, sourceParametersReassignments ); + + propagateNullability( beanMappingMethod ); + + return beanMappingMethod; } private void keepMappingReferencesUsingTarget(List references, Type type) { 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 598db971e2..a44ee581d7 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 @@ -166,7 +166,7 @@ public final M build() { PresenceCheck sourceParameterPresenceCheck = PresenceCheckMethodResolver.getPresenceCheckForSourceParameter( method, null, sourceParam, ctx ); - return instantiateMappingMethod( + M mappingMethod = instantiateMappingMethod( method, existingVariables, assignment, @@ -178,6 +178,10 @@ public final M build() { selectionParameters, sourceParameterPresenceCheck ); + + propagateNullability( mappingMethod ); + + return mappingMethod; } private Assignment forge(SourceRHS sourceRHS, Type sourceType, Type targetType) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingMethod.java index 0d04f5b6d5..fe1678953d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingMethod.java @@ -41,6 +41,13 @@ public abstract class MappingMethod extends GeneratedTypeMethod { private final List beforeMappingReferencesWithoutMappingTarget; private final List afterMappingReferences; + /** + * The type of the JSpecify {@code @Nullable} annotation to emit in front of this method's return type, or + * {@code null} when nothing should be emitted. Only set for methods that override a mapper method, so that the + * generated implementation keeps the nullability contract of the interface it implements. + */ + private Type nullableReturnTypeAnnotationType; + /** * constructor to be overloaded when local variable names are required prior to calling this constructor. (e.g. for * property mappings). It is supposed to be initialized with at least the parameter names. @@ -131,6 +138,19 @@ public Type getReturnType() { return returnType; } + /** + * @return the type of the JSpecify {@code @Nullable} annotation to emit in front of this method's return type, + * or {@code null} when the return type's nullability must not be reproduced in the generated + * implementation + */ + public Type getNullableReturnTypeAnnotationType() { + return nullableReturnTypeAnnotationType; + } + + public void setNullableReturnTypeAnnotationType(Type nullableReturnTypeAnnotationType) { + this.nullableReturnTypeAnnotationType = nullableReturnTypeAnnotationType; + } + public Accessibility getAccessibility() { return accessibility; } @@ -148,11 +168,15 @@ public Set getImportTypes() { Set types = new HashSet<>(); for ( Parameter param : parameters ) { - types.addAll( param.getType().getImportTypes() ); + types.addAll( param.getImportTypes() ); } types.addAll( getReturnType().getImportTypes() ); + if ( nullableReturnTypeAnnotationType != null ) { + types.add( nullableReturnTypeAnnotationType ); + } + for ( Type type : thrownTypes ) { types.addAll( type.getImportTypes() ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java index c5091869a6..98ef4c7ebd 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java @@ -38,6 +38,13 @@ public class Parameter extends ModelElement { private final boolean varArgs; + /** + * The type of the JSpecify {@code @Nullable} annotation to emit in front of this parameter's type, or + * {@code null} when nothing should be emitted. Only set for parameters of methods that override a mapper + * method, so that the generated implementation keeps the nullability contract of the interface it implements. + */ + private Type nullableAnnotationType; + private Parameter(Element element, Type type, boolean varArgs) { this.element = element; this.name = element.getSimpleName().toString(); @@ -113,9 +120,25 @@ private String format() { + "%s " + name; } + /** + * @return the type of the JSpecify {@code @Nullable} annotation to emit in front of this parameter's type, or + * {@code null} when the parameter's nullability must not be reproduced in the generated implementation + */ + public Type getNullableAnnotationType() { + return nullableAnnotationType; + } + + public void setNullableAnnotationType(Type nullableAnnotationType) { + this.nullableAnnotationType = nullableAnnotationType; + } + @Override public Set getImportTypes() { - return Collections.asSet( type ); + if ( nullableAnnotationType == null ) { + return Collections.asSet( type ); + } + + return Collections.asSet( type, nullableAnnotationType ); } public boolean isTargetType() { @@ -147,7 +170,7 @@ public boolean isSourceParameter() { } public Parameter withName(String name) { - return new Parameter( + Parameter parameter = new Parameter( name, this.name, type, @@ -158,6 +181,8 @@ public Parameter withName(String name) { targetPropertyName, varArgs ); + parameter.nullableAnnotationType = nullableAnnotationType; + return parameter; } @Override diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl index fd101f0d95..dd9ecb6c1c 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl @@ -9,7 +9,7 @@ <#list annotations as annotation> <#nt><@includeModel object=annotation/> -<#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { +<#lt>${accessibility.keyword} <#if nullableReturnTypeAnnotationType??>@<@includeModel object=nullableReturnTypeAnnotationType/> <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { <#assign targetType = resultType /> <#if !existingInstanceMapping> <#assign targetType = returnTypeToConstruct /> 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 6268366f7b..a87ce7c12f 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 @@ -10,7 +10,7 @@ <#list annotations as annotation> <#nt><@includeModel object=annotation/> -<#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { +<#lt>${accessibility.keyword} <#if nullableReturnTypeAnnotationType??>@<@includeModel object=nullableReturnTypeAnnotationType/> <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { <#list beforeMappingReferencesWithoutMappingTarget as callback> <@includeModel object=callback targetBeanName=resultName targetType=resultType/> <#if !callback_has_next> 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 e04d1b9475..9dd00d7f3d 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 @@ -9,7 +9,7 @@ <#list annotations as annotation> <#nt><@includeModel object=annotation/> -<#lt>${accessibility.keyword} <@includeModel object=returnType /> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { +<#lt>${accessibility.keyword} <#if nullableReturnTypeAnnotationType??>@<@includeModel object=nullableReturnTypeAnnotationType/> <@includeModel object=returnType /> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { <#list beforeMappingReferencesWithoutMappingTarget as callback> <@includeModel object=callback targetBeanName=resultName targetType=resultType/> <#if !callback_has_next> 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 0f335f9c54..00dc003d0a 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 @@ -10,7 +10,7 @@ <#list annotations as annotation> <#nt><@includeModel object=annotation/> -<#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { +<#lt>${accessibility.keyword} <#if nullableReturnTypeAnnotationType??>@<@includeModel object=nullableReturnTypeAnnotationType/> <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { <#--TODO does it even make sense to do a callback if the result is a Stream, as they are immutable--> <#list beforeMappingReferencesWithoutMappingTarget as callback> <@includeModel object=callback targetBeanName=resultName targetType=resultType/> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/common/Parameter.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/Parameter.ftl index 41afd0a42e..1485f98daa 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/common/Parameter.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/common/Parameter.ftl @@ -6,4 +6,4 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.common.Parameter" --> -<@includeModel object=type asVarArgs=varArgs/> ${name} \ No newline at end of file +<#if nullableAnnotationType??>@<@includeModel object=nullableAnnotationType/> <@includeModel object=type asVarArgs=varArgs/> ${name} \ No newline at end of file diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076IterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076IterableMapper.java new file mode 100644 index 0000000000..a70250bcd5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076IterableMapper.java @@ -0,0 +1,51 @@ +/* + * 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.bugs._4076; + +import java.util.List; + +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +/** + * Covers the container mapping methods (iterable / map / stream) and update methods, which are built through + * {@code ContainerMappingMethodBuilder} rather than {@code BeanMappingMethod.Builder}. + */ +@Mapper +public interface Issue4076IterableMapper { + + @Nullable + List mapList(@Nullable List source); + + void update(@Nullable Source source, @MappingTarget Target target); + + class Source { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Target { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076Mapper.java new file mode 100644 index 0000000000..e1e80e01a9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076Mapper.java @@ -0,0 +1,71 @@ +/* + * 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.bugs._4076; + +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface Issue4076Mapper { + + @Mapping( target = "name", source = "source1.name" ) + @Mapping( target = "description", source = "source2.description" ) + Target map(Source1 source1, @Nullable Source2 source2); + + @Mapping( target = "description", ignore = true ) + @Nullable + Target mapNullableReturn(Source1 source1); + + class Source1 { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Source2 { + + private @Nullable String description; + + public @Nullable String getDescription() { + return description; + } + + public void setDescription(@Nullable String description) { + this.description = description; + } + } + + class Target { + + private String name; + + private @Nullable String description; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public @Nullable String getDescription() { + return description; + } + + public void setDescription(@Nullable String description) { + this.description = description; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076Test.java new file mode 100644 index 0000000000..bebb05404c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/Issue4076Test.java @@ -0,0 +1,61 @@ +/* + * 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.bugs._4076; + +import org.jspecify.annotations.Nullable; +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; + +@IssueKey("4076") +@WithJSpecify +class Issue4076Test { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses(Issue4076Mapper.class) + void shouldPropagateNullableOnParameter() { + generatedSource.forMapper( Issue4076Mapper.class ) + .content() + .contains( "public Target map(Source1 source1, @Nullable Source2 source2)" ); + } + + @ProcessorTest + @WithClasses(Issue4076Mapper.class) + void shouldPropagateNullableOnReturnType() { + generatedSource.forMapper( Issue4076Mapper.class ) + .content() + .contains( "public @Nullable Target mapNullableReturn(Source1 source1)" ); + } + + @ProcessorTest + @WithClasses(Issue4076Mapper.class) + void shouldImportNullableAnnotation() { + generatedSource.forMapper( Issue4076Mapper.class ) + .containsImportFor( Nullable.class ); + } + + @ProcessorTest + @WithClasses(Issue4076IterableMapper.class) + void shouldPropagateNullableForIterableMethod() { + generatedSource.forMapper( Issue4076IterableMapper.class ) + .content() + .contains( "public @Nullable List mapList(@Nullable List source)" ); + } + + @ProcessorTest + @WithClasses(Issue4076IterableMapper.class) + void shouldPropagateNullableForUpdateMethod() { + generatedSource.forMapper( Issue4076IterableMapper.class ) + .content() + .contains( "public void update(@Nullable Source source, Target target)" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/package-info.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/package-info.java new file mode 100644 index 0000000000..5f6482cf31 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4076/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +@NullMarked +package org.mapstruct.ap.test.bugs._4076; + +import org.jspecify.annotations.NullMarked; 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 index c1efd39d0d..35f6ade067 100644 --- 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 @@ -6,6 +6,7 @@ package org.mapstruct.ap.test.nullcheck.jspecify; import javax.annotation.processing.Generated; +import org.jspecify.annotations.Nullable; @Generated( value = "org.mapstruct.ap.MappingProcessor", @@ -15,7 +16,7 @@ public class JSpecifyNonNullReturnBeanMapperImpl implements JSpecifyNonNullReturnBeanMapper { @Override - public NullMarkedTargetBean map(JSpecifyNonNullReturnBeanSourceBean source) { + public NullMarkedTargetBean map(@Nullable JSpecifyNonNullReturnBeanSourceBean source) { NullMarkedTargetBean nullMarkedTargetBean = new 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 index 8c6de34e37..bb8e1ebbf2 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import javax.annotation.processing.Generated; +import org.jspecify.annotations.Nullable; @Generated( value = "org.mapstruct.ap.MappingProcessor", @@ -17,7 +18,7 @@ public class JSpecifyNonNullReturnIterableMapperImpl implements JSpecifyNonNullReturnIterableMapper { @Override - public List mapAll(List sources) { + public List mapAll(@Nullable List sources) { if ( sources == null ) { return new ArrayList<>(); } 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 index 01c2981ab1..bbc45be3b7 100644 --- 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 @@ -7,6 +7,7 @@ import java.util.stream.Stream; import javax.annotation.processing.Generated; +import org.jspecify.annotations.Nullable; @Generated( value = "org.mapstruct.ap.MappingProcessor", @@ -16,7 +17,7 @@ public class JSpecifyNonNullReturnStreamMapperImpl implements JSpecifyNonNullReturnStreamMapper { @Override - public Stream mapAll(Stream sources) { + public Stream mapAll(@Nullable Stream sources) { if ( sources == null ) { return Stream.empty(); } 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 index 70b8fbea1d..6a5e190f18 100644 --- 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 @@ -7,6 +7,7 @@ import java.util.List; import javax.annotation.processing.Generated; +import org.jspecify.annotations.Nullable; @Generated( value = "org.mapstruct.ap.MappingProcessor", @@ -16,7 +17,7 @@ public class JSpecifyNonNullReturnUpdateIterableMapperImpl implements JSpecifyNonNullReturnUpdateIterableMapper { @Override - public List mapAll(List sources, List target) { + public List mapAll(@Nullable List sources, List target) { if ( sources == null ) { return target; } 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 index 2a6950f71d..1f0ad93802 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import javax.annotation.processing.Generated; +import org.jspecify.annotations.Nullable; @Generated( value = "org.mapstruct.ap.MappingProcessor", @@ -17,7 +18,7 @@ public class JSpecifyNullableSourceIterableMapperImpl implements JSpecifyNullableSourceIterableMapper { @Override - public List mapAll(List sources) { + public @Nullable List mapAll(@Nullable List sources) { if ( sources == null ) { return null; } 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 index 52313a0327..b26bdbcc3c 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import javax.annotation.processing.Generated; +import org.jspecify.annotations.Nullable; @Generated( value = "org.mapstruct.ap.MappingProcessor", @@ -17,7 +18,7 @@ public class JSpecifyReturnNullOverrideIterableMapperImpl implements JSpecifyReturnNullOverrideIterableMapper { @Override - public List mapAll(List sources) { + public List mapAll(@Nullable List sources) { if ( sources == null ) { return new ArrayList<>(); }