From 909795fce99daf46dcd8d5b9431ff0a544bfc8f0 Mon Sep 17 00:00:00 2001 From: hej090224 Date: Tue, 25 Aug 2026 22:50:12 +0900 Subject: [PATCH] Handle JSpecify type-use nullness for container elements --- .../model/ContainerMappingMethodBuilder.java | 74 +++++++++++++- .../internal/model/IterableMappingMethod.java | 22 ++++- .../internal/model/MappingBuilderContext.java | 13 +++ .../internal/model/StreamMappingMethod.java | 3 +- .../model/assignment/LocalVarWrapper.java | 21 ++++ .../model/assignment/SetterWrapper.java | 11 +++ .../ap/internal/util/NullabilityResolver.java | 22 +++++ .../model/assignment/LocalVarWrapper.ftl | 28 ++++-- .../jspecify/JSpecifyTypeUseMapper.java | 32 +++++++ .../jspecify/JSpecifyTypeUseSourceBean.java | 63 ++++++++++++ .../JSpecifyTypeUseSourceElement.java | 19 ++++ .../jspecify/JSpecifyTypeUseTargetBean.java | 63 ++++++++++++ .../JSpecifyTypeUseTargetElement.java | 19 ++++ .../jspecify/JSpecifyTypeUseTest.java | 59 ++++++++++++ .../jspecify/JSpecifyTypeUseMapperImpl.java | 96 +++++++++++++++++++ 15 files changed, 530 insertions(+), 15 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseSourceBean.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseSourceElement.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTargetBean.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTargetElement.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTest.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseMapperImpl.java 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..827414ba59 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 @@ -10,6 +10,11 @@ import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.FormattingParameters; @@ -21,6 +26,7 @@ import org.mapstruct.ap.internal.model.source.SelectionParameters; import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria; import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.NullabilityResolver; import org.mapstruct.ap.internal.util.Strings; import static org.mapstruct.ap.internal.util.Collections.first; @@ -75,6 +81,33 @@ public final M build() { Type sourceElementType = getElementType( sourceParameterType ); Type targetElementType = getElementType( resultType ); + TypeMirror sourceElementTypeMirror = getElementTypeMirror( sourceParameterType.getTypeMirror() ); + Element sourceParameterElement = first( method.getSourceParameters() ).getElement(); + if ( sourceParameterElement != null + && containsJSpecifyTypeUseAnnotation( sourceParameterElement.asType() ) ) { + sourceElementTypeMirror = getElementTypeMirror( sourceParameterElement.asType() ); + } + TypeMirror targetElementTypeMirror = getElementTypeMirror( resultType.getTypeMirror() ); + if ( method.getExecutable() != null + && containsJSpecifyTypeUseAnnotation( method.getExecutable().getReturnType() ) ) { + targetElementTypeMirror = getElementTypeMirror( method.getExecutable().getReturnType() ); + } + NullabilityResolver.Nullability sourceElementNullability = ctx.getNullabilityInMapperScope( + sourceElementTypeMirror + ); + NullabilityResolver.Nullability targetElementNullability = ctx.getNullabilityInMapperScope( + targetElementTypeMirror + ); + Boolean elementNullCheck = ctx.getNullabilityResolver().requiresNullCheck( + sourceElementNullability, + targetElementNullability + ); + boolean includeElementNullCheck = elementNullCheck != null + ? elementNullCheck + : sourceElementNullability == NullabilityResolver.Nullability.NULLABLE; + boolean setElementExplicitlyToNull = sourceElementNullability == NullabilityResolver.Nullability.NULLABLE + && targetElementNullability != NullabilityResolver.Nullability.NON_NULL; + String loopVariableName = Strings.getSafeVariableName( sourceElementType.getName(), method.getParameterNames() ); @@ -124,7 +157,7 @@ public final M build() { forgedMethod.addThrownTypes( assignment.getThrownTypes() ); } } - assignment = getWrapper( assignment, method ); + assignment = getWrapper( assignment, method, includeElementNullCheck, setElementExplicitlyToNull ); // mapNullToDefault — a JSpecify @NonNull return forces RETURN_DEFAULT to avoid generating `return null`. // Forcing is unconditional here (unlike BeanMappingMethod): when the source is @NonNull the template skips @@ -188,6 +221,42 @@ private Assignment forge(SourceRHS sourceRHS, Type sourceType, Type targetType) return assignment; } + private static TypeMirror getElementTypeMirror(TypeMirror typeMirror) { + if ( typeMirror.getKind() == TypeKind.ARRAY ) { + return ( (ArrayType) typeMirror ).getComponentType(); + } + if ( typeMirror.getKind() == TypeKind.DECLARED ) { + List typeArguments = ( (DeclaredType) typeMirror ).getTypeArguments(); + if ( !typeArguments.isEmpty() ) { + return typeArguments.get( 0 ); + } + } + return typeMirror; + } + + private static boolean containsJSpecifyTypeUseAnnotation(TypeMirror typeMirror) { + if ( !typeMirror.getAnnotationMirrors().isEmpty() ) { + for ( AnnotationMirror annotationMirror : typeMirror.getAnnotationMirrors() ) { + String annotationName = annotationMirror.getAnnotationType().toString(); + if ( "org.jspecify.annotations.Nullable".equals( annotationName ) + || "org.jspecify.annotations.NonNull".equals( annotationName ) ) { + return true; + } + } + } + if ( typeMirror.getKind() == TypeKind.ARRAY ) { + return containsJSpecifyTypeUseAnnotation( ( (ArrayType) typeMirror ).getComponentType() ); + } + if ( typeMirror.getKind() == TypeKind.DECLARED ) { + for ( TypeMirror typeArgument : ( (DeclaredType) typeMirror ).getTypeArguments() ) { + if ( containsJSpecifyTypeUseAnnotation( typeArgument ) ) { + return true; + } + } + } + return false; + } + protected abstract M instantiateMappingMethod(Method method, Collection existingVariables, Assignment assignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, @@ -197,7 +266,8 @@ protected abstract M instantiateMappingMethod(Method method, Collection protected abstract Type getElementType(Type parameterType); - protected abstract Assignment getWrapper(Assignment assignment, Method method); + protected abstract Assignment getWrapper(Assignment assignment, Method method, boolean includeSourceNullCheck, + boolean setExplicitlyToNull); @Override protected boolean shouldUsePropertyNamesInHistory() { 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 220466f802..b6c6eb6b15 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 @@ -40,15 +40,27 @@ protected Type getElementType(Type parameterType) { } @Override - protected Assignment getWrapper(Assignment assignment, Method method) { + protected Assignment getWrapper(Assignment assignment, Method method, boolean includeSourceNullCheck, + boolean setExplicitlyToNull) { Type resultType = method.getResultType(); // target accessor is setter, so decorate assignment as setter if ( resultType.isArrayType() ) { - return new LocalVarWrapper( assignment, method.getThrownTypes(), resultType, false ); - } - else { - return new SetterWrapper( assignment, method.getThrownTypes(), false ); + return new LocalVarWrapper( + assignment, + method.getThrownTypes(), + resultType, + false, + includeSourceNullCheck, + setExplicitlyToNull + ); } + return new SetterWrapper( + assignment, + method.getThrownTypes(), + false, + includeSourceNullCheck, + setExplicitlyToNull + ); } @Override 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 39e8558e5e..cc03e6d254 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 @@ -15,6 +15,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.model.common.Assignment; import org.mapstruct.ap.internal.model.common.FormattingParameters; @@ -225,6 +226,18 @@ public NullabilityResolver.Nullability getNullabilityInMapperScope(Element eleme () -> typeFactory.getType( mapperTypeElement.asType() ).isNullMarked() ); } + /** + * Resolves the JSpecify nullability of a nested type-use position in the mapper's scope. + * + * @param typeMirror the type-use position to inspect + * @return the resolved nullability + */ + public NullabilityResolver.Nullability getNullabilityInMapperScope(TypeMirror typeMirror) { + return nullabilityResolver.getNullability( + typeMirror, + () -> typeFactory.getType( mapperTypeElement.asType() ).isNullMarked() ); + } + /** * Whether the return type of the given mapping method is JSpecify {@code @NonNull} (directly or via a * {@code @NullMarked} scope). When it is, a mapping method must not generate {@code return null}, so callers 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 3955bc47ce..34048d91e3 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 @@ -44,7 +44,8 @@ protected Type getElementType(Type parameterType) { } @Override - protected Assignment getWrapper(Assignment assignment, Method method) { + protected Assignment getWrapper(Assignment assignment, Method method, boolean includeSourceNullCheck, + boolean setExplicitlyToNull) { return new Java8FunctionWrapper( assignment ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.java index 1287ab6cd6..1d52c10aef 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.java @@ -22,12 +22,25 @@ public class LocalVarWrapper extends AssignmentWrapper { private final List thrownTypesToExclude; private final Type targetType; + private final boolean includeSourceNullCheck; + private final boolean setExplicitlyToNull; public LocalVarWrapper(Assignment decoratedAssignment, List thrownTypesToExclude, Type targetType, boolean fieldAssignment) { super( decoratedAssignment, fieldAssignment ); this.thrownTypesToExclude = thrownTypesToExclude; this.targetType = targetType; + this.includeSourceNullCheck = false; + this.setExplicitlyToNull = false; + } + + public LocalVarWrapper(Assignment decoratedAssignment, List thrownTypesToExclude, Type targetType, + boolean fieldAssignment, boolean includeSourceNullCheck, boolean setExplicitlyToNull) { + super( decoratedAssignment, fieldAssignment ); + this.thrownTypesToExclude = thrownTypesToExclude; + this.targetType = targetType; + this.includeSourceNullCheck = includeSourceNullCheck; + this.setExplicitlyToNull = setExplicitlyToNull; } @Override @@ -52,4 +65,12 @@ public Set getImportTypes() { return imported; } + public boolean isIncludeSourceNullCheck() { + return includeSourceNullCheck; + } + + public boolean isSetExplicitlyToNull() { + return setExplicitlyToNull; + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapper.java index 94b031c622..f3d4b11522 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapper.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/assignment/SetterWrapper.java @@ -55,6 +55,17 @@ public SetterWrapper(Assignment rhs, List thrownTypesToExclude, boolean fi this.nullCastType = null; } + public SetterWrapper(Assignment rhs, List thrownTypesToExclude, boolean fieldAssignment, + boolean includeSourceNullCheck, boolean setExplicitlyToNull) { + super( rhs, fieldAssignment ); + this.thrownTypesToExclude = thrownTypesToExclude; + this.includeSourceNullCheck = includeSourceNullCheck; + this.setExplicitlyToNull = setExplicitlyToNull; + this.setExplicitlyToDefault = false; + this.mustCastForNull = false; + this.nullCastType = null; + } + @Override public List getThrownTypes() { List parentThrownTypes = super.getThrownTypes(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/NullabilityResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/util/NullabilityResolver.java index 768886cb95..057eea293e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/NullabilityResolver.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/NullabilityResolver.java @@ -126,6 +126,28 @@ else if ( element instanceof VariableElement ) { return Nullability.UNKNOWN; } + /** + * Determines the nullability of a type-use position. Unlike an accessor element, a nested type-use position + * has no declaration element to inspect, so only annotations on the supplied type mirror and the enclosing + * JSpecify scope are considered. + * + * @param typeMirror the type-use position to inspect; may be {@code null} + * @param enclosingTypeNullMarked supplier for the enclosing JSpecify scope; must be non-{@code null} + * @return the nullability state + */ + public Nullability getNullability(TypeMirror typeMirror, BooleanSupplier enclosingTypeNullMarked) { + if ( !enabled || typeMirror == null ) { + return Nullability.UNKNOWN; + } + + Nullability result = getNullabilityFromTypeMirror( typeMirror ); + if ( result != Nullability.UNKNOWN ) { + return result; + } + + return enclosingTypeNullMarked.getAsBoolean() ? Nullability.NON_NULL : Nullability.UNKNOWN; + } + /** * Determines the nullability of a write-accessor element — either a setter method or a field. *

diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl index 163a52896a..6ef2df132b 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/LocalVarWrapper.ftl @@ -7,14 +7,28 @@ --> <#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.LocalVarWrapper" --> <#import "../macro/CommonMacros.ftl" as lib> -<#if (thrownTypes?size == 0) > - <#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/> ${ext.targetWriteAccessorName} = <@_assignment/>; +<#if includeSourceNullCheck> + if ( <#if sourceLocalVarName??>${sourceLocalVarName}<#else>${sourceReference} != null ) { + <@_writeAssignment/> + } + <#if setExplicitlyToNull> + else { + ${ext.targetWriteAccessorName} = ${ext.targetType.null}; + } + <#else> - <#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/> ${ext.targetWriteAccessorName}; - <@lib.handleExceptions> - ${ext.targetWriteAccessorName} = <@_assignment/>; - + <@_writeAssignment/> +<#macro _writeAssignment> + <#if (thrownTypes?size == 0) > + <#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/> ${ext.targetWriteAccessorName} = <@_assignment/>; + <#else> + <#if !ext.isTargetDefined?? ><@includeModel object=ext.targetType/> ${ext.targetWriteAccessorName}; + <@lib.handleExceptions> + ${ext.targetWriteAccessorName} = <@_assignment/>; + + + <#macro _assignment> <@includeModel object=assignment targetBeanName=ext.targetBeanName @@ -23,4 +37,4 @@ targetWriteAccessorName=ext.targetWriteAccessorName targetPropertyName=ext.targetPropertyName targetType=ext.targetType/> - \ No newline at end of file + diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseMapper.java new file mode 100644 index 0000000000..0825ba078c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseMapper.java @@ -0,0 +1,32 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.nullcheck.jspecify; + +import java.util.List; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@NullMarked +@Mapper +public interface JSpecifyTypeUseMapper { + + JSpecifyTypeUseMapper INSTANCE = Mappers.getMapper( JSpecifyTypeUseMapper.class ); + + JSpecifyTypeUseTargetBean map(JSpecifyTypeUseSourceBean source); + + JSpecifyTypeUseTargetElement map(JSpecifyTypeUseSourceElement source); + + List<@Nullable JSpecifyTypeUseTargetElement> mapElements( + List<@Nullable JSpecifyTypeUseSourceElement> source + ); + + List> mapNested( + List> source + ); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseSourceBean.java new file mode 100644 index 0000000000..51cb16cc17 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseSourceBean.java @@ -0,0 +1,63 @@ +/* + * 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; + +@NullMarked +public class JSpecifyTypeUseSourceBean { + + private String @Nullable[] nullableArray; + private @Nullable String[] nullableArrayElements; + private @Nullable JSpecifyTypeUseSourceElement[] nullableElementArray; + private List<@Nullable JSpecifyTypeUseSourceElement> nullableListElements; + private List> nestedNullableListElements; + + public String @Nullable[] getNullableArray() { + return nullableArray; + } + + public void setNullableArray(String @Nullable[] nullableArray) { + this.nullableArray = nullableArray; + } + + public @Nullable String[] getNullableArrayElements() { + return nullableArrayElements; + } + + public void setNullableArrayElements(@Nullable String[] nullableArrayElements) { + this.nullableArrayElements = nullableArrayElements; + } + + public @Nullable JSpecifyTypeUseSourceElement[] getNullableElementArray() { + return nullableElementArray; + } + + public void setNullableElementArray(@Nullable JSpecifyTypeUseSourceElement[] nullableElementArray) { + this.nullableElementArray = nullableElementArray; + } + + public List<@Nullable JSpecifyTypeUseSourceElement> getNullableListElements() { + return nullableListElements; + } + + public void setNullableListElements(List<@Nullable JSpecifyTypeUseSourceElement> nullableListElements) { + this.nullableListElements = nullableListElements; + } + + public List> getNestedNullableListElements() { + return nestedNullableListElements; + } + + public void setNestedNullableListElements( + List> nestedNullableListElements + ) { + this.nestedNullableListElements = nestedNullableListElements; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseSourceElement.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseSourceElement.java new file mode 100644 index 0000000000..13efe99f0e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseSourceElement.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; + +public class JSpecifyTypeUseSourceElement { + + 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/JSpecifyTypeUseTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTargetBean.java new file mode 100644 index 0000000000..b9b201e25b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTargetBean.java @@ -0,0 +1,63 @@ +/* + * 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; + +@NullMarked +public class JSpecifyTypeUseTargetBean { + + private String @Nullable[] nullableArray; + private @Nullable String[] nullableArrayElements; + private @Nullable JSpecifyTypeUseTargetElement[] nullableElementArray; + private List<@Nullable JSpecifyTypeUseTargetElement> nullableListElements; + private List> nestedNullableListElements; + + public String @Nullable[] getNullableArray() { + return nullableArray; + } + + public void setNullableArray(String @Nullable[] nullableArray) { + this.nullableArray = nullableArray; + } + + public @Nullable String[] getNullableArrayElements() { + return nullableArrayElements; + } + + public void setNullableArrayElements(String[] nullableArrayElements) { + this.nullableArrayElements = nullableArrayElements; + } + + public List<@Nullable JSpecifyTypeUseTargetElement> getNullableListElements() { + return nullableListElements; + } + + public void setNullableListElements(List<@Nullable JSpecifyTypeUseTargetElement> nullableListElements) { + this.nullableListElements = nullableListElements; + } + + public @Nullable JSpecifyTypeUseTargetElement[] getNullableElementArray() { + return nullableElementArray; + } + + public void setNullableElementArray(@Nullable JSpecifyTypeUseTargetElement[] nullableElementArray) { + this.nullableElementArray = nullableElementArray; + } + + public List> getNestedNullableListElements() { + return nestedNullableListElements; + } + + public void setNestedNullableListElements( + List> nestedNullableListElements + ) { + this.nestedNullableListElements = nestedNullableListElements; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTargetElement.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTargetElement.java new file mode 100644 index 0000000000..c322548dc9 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTargetElement.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; + +public class JSpecifyTypeUseTargetElement { + + 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/JSpecifyTypeUseTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTest.java new file mode 100644 index 0000000000..39fa855f4e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseTest.java @@ -0,0 +1,59 @@ +/* + * 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("4035") +@WithJSpecify +class JSpecifyTypeUseTest { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource().addComparisonToFixtureFor( + JSpecifyTypeUseMapper.class + ); + + @ProcessorTest + @WithClasses({ + JSpecifyTypeUseSourceBean.class, + JSpecifyTypeUseTargetBean.class, + JSpecifyTypeUseSourceElement.class, + JSpecifyTypeUseTargetElement.class, + JSpecifyTypeUseMapper.class + }) + void typeUseNullnessIsPreservedForArraysAndListElements() { + JSpecifyTypeUseSourceBean source = new JSpecifyTypeUseSourceBean(); + source.setNullableArray( new String[] { "array" } ); + source.setNullableArrayElements( new String[] { null } ); + JSpecifyTypeUseSourceElement element = new JSpecifyTypeUseSourceElement(); + element.setValue( "list" ); + source.setNullableElementArray( new JSpecifyTypeUseSourceElement[] { null, element } ); + source.setNullableListElements( Arrays.asList( null, element ) ); + source.setNestedNullableListElements( Arrays.asList( Arrays.asList( null, element ) ) ); + + JSpecifyTypeUseTargetBean target = JSpecifyTypeUseMapper.INSTANCE.map( source ); + + assertThat( target.getNullableArray() ).containsExactly( "array" ); + assertThat( target.getNullableArrayElements() ).containsExactly( (String) null ); + assertThat( target.getNullableElementArray()[0] ).isNull(); + assertThat( target.getNullableElementArray()[1].getValue() ).isEqualTo( "list" ); + assertThat( target.getNullableListElements().get( 0 ) ).isNull(); + assertThat( target.getNullableListElements().get( 1 ).getValue() ).isEqualTo( "list" ); + assertThat( target.getNestedNullableListElements() ).hasSize( 1 ); + assertThat( target.getNestedNullableListElements().get( 0 ).get( 0 ) ).isNull(); + assertThat( target.getNestedNullableListElements().get( 0 ).get( 1 ).getValue() ) + .isEqualTo( "list" ); + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseMapperImpl.java new file mode 100644 index 0000000000..83baff0976 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyTypeUseMapperImpl.java @@ -0,0 +1,96 @@ +/* + * 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.Arrays; +import java.util.List; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-08-25T22:21:39+0900", + comments = "version: , compiler: Eclipse JDT (Batch) 3.20.0.v20191203-2131, environment: Java 25.0.2 (Oracle Corporation)" +) +public class JSpecifyTypeUseMapperImpl implements JSpecifyTypeUseMapper { + + @Override + public JSpecifyTypeUseTargetBean map(JSpecifyTypeUseSourceBean source) { + + JSpecifyTypeUseTargetBean jSpecifyTypeUseTargetBean = new JSpecifyTypeUseTargetBean(); + + String[] nullableArray = source.getNullableArray(); + if ( nullableArray != null ) { + jSpecifyTypeUseTargetBean.setNullableArray( Arrays.copyOf( nullableArray, nullableArray.length ) ); + } + String[] nullableArrayElements = source.getNullableArrayElements(); + if ( nullableArrayElements != null ) { + jSpecifyTypeUseTargetBean.setNullableArrayElements( Arrays.copyOf( nullableArrayElements, nullableArrayElements.length ) ); + } + jSpecifyTypeUseTargetBean.setNullableListElements( mapElements( source.getNullableListElements() ) ); + jSpecifyTypeUseTargetBean.setNullableElementArray( jSpecifyTypeUseSourceElementArrayToJSpecifyTypeUseTargetElementArray( source.getNullableElementArray() ) ); + jSpecifyTypeUseTargetBean.setNestedNullableListElements( mapNested( source.getNestedNullableListElements() ) ); + + return jSpecifyTypeUseTargetBean; + } + + @Override + public JSpecifyTypeUseTargetElement map(JSpecifyTypeUseSourceElement source) { + + JSpecifyTypeUseTargetElement jSpecifyTypeUseTargetElement = new JSpecifyTypeUseTargetElement(); + + jSpecifyTypeUseTargetElement.setValue( source.getValue() ); + + return jSpecifyTypeUseTargetElement; + } + + @Override + public List mapElements(List source) { + + List list = new ArrayList<>( source.size() ); + for ( JSpecifyTypeUseSourceElement jSpecifyTypeUseSourceElement : source ) { + if ( jSpecifyTypeUseSourceElement != null ) { + list.add( map( jSpecifyTypeUseSourceElement ) ); + } + else { + list.add( null ); + } + } + + return list; + } + + @Override + public List> mapNested(List> source) { + + List> list1 = new ArrayList<>( source.size() ); + for ( List list : source ) { + list1.add( mapElements( list ) ); + } + + return list1; + } + + protected JSpecifyTypeUseTargetElement[] jSpecifyTypeUseSourceElementArrayToJSpecifyTypeUseTargetElementArray(JSpecifyTypeUseSourceElement[] jSpecifyTypeUseSourceElementArray) { + if ( jSpecifyTypeUseSourceElementArray == null ) { + return new JSpecifyTypeUseTargetElement[0]; + } + + JSpecifyTypeUseTargetElement[] jSpecifyTypeUseTargetElementTmp = new JSpecifyTypeUseTargetElement[jSpecifyTypeUseSourceElementArray.length]; + int i = 0; + for ( JSpecifyTypeUseSourceElement jSpecifyTypeUseSourceElement : jSpecifyTypeUseSourceElementArray ) { + if ( jSpecifyTypeUseSourceElement != null ) { + jSpecifyTypeUseTargetElementTmp[i] = map( jSpecifyTypeUseSourceElement ); + } + else { + jSpecifyTypeUseTargetElementTmp[i] = null; + } + i++; + } + + return jSpecifyTypeUseTargetElementTmp; + } +}