From 1a673a34fbf678b48ac7d7316c3f346e7f620248 Mon Sep 17 00:00:00 2001 From: Agustin Ranieri Date: Sun, 28 Jun 2026 12:11:04 -0300 Subject: [PATCH 1/3] test: reproduce #4079 --- .../ap/test/bugs/_4079/Issue4079Mapper.java | 15 ++++++++ .../ap/test/bugs/_4079/Issue4079Test.java | 37 +++++++++++++++++++ .../mapstruct/ap/test/bugs/_4079/Source.java | 34 +++++++++++++++++ .../mapstruct/ap/test/bugs/_4079/Target.java | 33 +++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Mapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Test.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Source.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Target.java diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Mapper.java new file mode 100644 index 0000000000..32649062c8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Mapper.java @@ -0,0 +1,15 @@ +/* + * 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._4079; + +import org.jspecify.annotations.NullMarked; +import org.mapstruct.Mapper; + +@Mapper +@NullMarked +public interface Issue4079Mapper { + Target map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Test.java new file mode 100644 index 0000000000..ef1e55db83 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Test.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.bugs._4079; + +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.compilation.annotation.CompilationResult; +import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; +import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +@IssueKey( "4079" ) +@WithJSpecify +public class Issue4079Test { + + @RegisterExtension + final GeneratedSource generatedSource = new GeneratedSource(); + + @ProcessorTest + @WithClasses( { Issue4079Mapper.class, Source.class, Target.class } ) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = Issue4079Mapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + message = "Can't map potentially nullable source property \"nested\" to @NonNull " + + "constructor parameter \"nested\". Consider adding a defaultValue or " + + "defaultExpression.") + }) + public void nullableSourceToNonNullConstructorParamUnderNullMarkedShouldFail() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Source.java new file mode 100644 index 0000000000..f02d11f764 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Source.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.bugs._4079; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class Source { + private final @Nullable Nested nested; + + public Source(@Nullable Nested nested) { + this.nested = nested; + } + + public @Nullable Nested getNested() { + return nested; + } + + public static class Nested { + private final String foo; + + public Nested(String foo) { + this.foo = foo; + } + + public String getFoo() { + return foo; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Target.java new file mode 100644 index 0000000000..612d25c896 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Target.java @@ -0,0 +1,33 @@ +/* + * 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._4079; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class Target { + private final Nested nested; + + public Target(Nested nested) { + this.nested = nested; + } + + public Nested getNested() { + return nested; + } + + public static class Nested { + private final String foo; + + public Nested(String foo) { + this.foo = foo; + } + + public String getFoo() { + return foo; + } + } +} From 79713c014d0b10510407022576b430f631df9864 Mon Sep 17 00:00:00 2001 From: Agustin Ranieri Date: Sun, 28 Jun 2026 12:26:55 -0300 Subject: [PATCH 2/3] chore: Use Erroneous convention for expected compilation errors --- .../{Issue4079Mapper.java => ErroneousIssue4079Mapper.java} | 2 +- .../java/org/mapstruct/ap/test/bugs/_4079/Issue4079Test.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/{Issue4079Mapper.java => ErroneousIssue4079Mapper.java} (87%) diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/ErroneousIssue4079Mapper.java similarity index 87% rename from processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Mapper.java rename to processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/ErroneousIssue4079Mapper.java index 32649062c8..763fd99683 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Mapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/ErroneousIssue4079Mapper.java @@ -10,6 +10,6 @@ @Mapper @NullMarked -public interface Issue4079Mapper { +public interface ErroneousIssue4079Mapper { Target map(Source source); } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Test.java index ef1e55db83..52da98ada8 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4079/Issue4079Test.java @@ -23,10 +23,10 @@ public class Issue4079Test { final GeneratedSource generatedSource = new GeneratedSource(); @ProcessorTest - @WithClasses( { Issue4079Mapper.class, Source.class, Target.class } ) + @WithClasses( { ErroneousIssue4079Mapper.class, Source.class, Target.class } ) @ExpectedCompilationOutcome(value = CompilationResult.FAILED, diagnostics = { - @Diagnostic(type = Issue4079Mapper.class, + @Diagnostic(type = ErroneousIssue4079Mapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, message = "Can't map potentially nullable source property \"nested\" to @NonNull " + "constructor parameter \"nested\". Consider adding a defaultValue or " + From 630dc62e314cb1ad0f33f36f640980d9015a11f4 Mon Sep 17 00:00:00 2001 From: Agustin Ranieri Date: Sun, 28 Jun 2026 12:39:34 -0300 Subject: [PATCH 3/3] fix `@NullMarked` scope not detected for constructor/setter parameters --- .../org/mapstruct/ap/internal/model/PropertyMapping.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 163c390b66..358b08f8ec 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 @@ -681,7 +681,10 @@ private boolean targetDeclaringTypeIsNullMarked() { return false; } Element declaring = targetElement.getEnclosingElement(); - if ( !( declaring instanceof TypeElement ) ) { + while ( declaring != null && !( declaring instanceof TypeElement ) ) { + declaring = declaring.getEnclosingElement(); + } + if ( declaring == null ) { return false; } return ctx.getTypeFactory().getType( declaring.asType() ).isNullMarked();