From 428fd278bef5e02388de7b9b96ad480ef33a44d1 Mon Sep 17 00:00:00 2001 From: Takch02 Date: Sun, 24 May 2026 19:10:48 +0900 Subject: [PATCH 01/24] #4027 Fix mapping ZonedDateTime or OffsetDateTime to LocalDateTime or Instant (#4047) --- .../chapter-5-data-type-conversions.asciidoc | 8 ++ .../ap/internal/conversion/Conversions.java | 5 + ...JavaOffsetDateTimeToInstantConversion.java | 34 ++++++ ...fsetDateTimeToLocalDateTimeConversion.java | 35 ++++++ .../JavaZonedDateTimeToInstantConversion.java | 34 ++++++ ...onedDateTimeToLocalDateTimeConversion.java | 36 ++++++ ...DateTimeToLocalDateTimeConversionTest.java | 113 ++++++++++++++++++ .../Source.java | 48 ++++++++ .../SourceTargetMapper.java | 18 +++ .../Target.java | 48 ++++++++ .../SourceTargetMapperImpl.java | 65 ++++++++++ 11 files changed, 444 insertions(+) create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToInstantConversion.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToLocalDateTimeConversion.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToInstantConversion.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToLocalDateTimeConversion.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java create mode 100644 processor/src/test/resources/fixtures/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapperImpl.java diff --git a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc index d95e6c7a62..10c41506de 100644 --- a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc +++ b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc @@ -109,6 +109,14 @@ public interface CarMapper { * Between `java.time.LocalDateTime` from Java 8 Date-Time package and `java.time.LocalDate` from the same package. +* Between `java.time.ZonedDateTime` from Java 8 Date-Time package and `java.time.LocalDateTime` from the same package. + +* Between `java.time.OffsetDateTime` from Java 8 Date-Time package and `java.time.LocalDateTime` from the same package. + +* Between `java.time.ZonedDateTime` from Java 8 Date-Time package and `java.time.Instant` from the same package. + +* Between `java.time.OffsetDateTime` from Java 8 Date-Time package and `java.time.Instant` from the same package. + * Between `java.time.ZonedDateTime` from Java 8 Date-Time package and `java.util.Calendar`. * Between `java.sql.Date` and `java.util.Date` diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/Conversions.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/Conversions.java index c29ad5141e..6e1f2c03aa 100755 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/Conversions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/Conversions.java @@ -16,6 +16,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.time.Period; import java.time.ZonedDateTime; import java.util.Calendar; @@ -244,7 +245,11 @@ private void registerJava8TimeConversions() { // Java 8 time register( LocalDateTime.class, LocalDate.class, new JavaLocalDateTimeToLocalDateConversion() ); + register( ZonedDateTime.class, LocalDateTime.class, new JavaZonedDateTimeToLocalDateTimeConversion() ); + register( OffsetDateTime.class, LocalDateTime.class, new JavaOffsetDateTimeToLocalDateTimeConversion() ); + register( ZonedDateTime.class, Instant.class, new JavaZonedDateTimeToInstantConversion() ); + register( OffsetDateTime.class, Instant.class, new JavaOffsetDateTimeToInstantConversion() ); } private void registerJavaTimeSqlConversions() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToInstantConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToInstantConversion.java new file mode 100644 index 0000000000..9c5cb63990 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToInstantConversion.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.internal.conversion; + +import java.time.ZoneOffset; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Collections; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.zoneOffset; + +public class JavaOffsetDateTimeToInstantConversion extends SimpleConversion { + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toInstant()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return ".atOffset( " + zoneOffset( conversionContext ) + ".UTC )"; + } + + @Override + protected Set getFromConversionImportTypes(ConversionContext conversionContext) { + return Collections.asSet( + conversionContext.getTypeFactory().getType( ZoneOffset.class ) + ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToLocalDateTimeConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToLocalDateTimeConversion.java new file mode 100644 index 0000000000..987c4cc625 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaOffsetDateTimeToLocalDateTimeConversion.java @@ -0,0 +1,35 @@ +/* + * 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.internal.conversion; + +import java.time.ZoneOffset; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Collections; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.zoneOffset; + +public class JavaOffsetDateTimeToLocalDateTimeConversion extends SimpleConversion { + + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toLocalDateTime()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return ".atOffset( " + zoneOffset( conversionContext ) + ".UTC )"; + } + + @Override + protected Set getFromConversionImportTypes(ConversionContext conversionContext) { + return Collections.asSet( + conversionContext.getTypeFactory().getType( ZoneOffset.class ) + ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToInstantConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToInstantConversion.java new file mode 100644 index 0000000000..44cc68c2a7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToInstantConversion.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.internal.conversion; + +import java.time.ZoneOffset; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Collections; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.zoneOffset; + +public class JavaZonedDateTimeToInstantConversion extends SimpleConversion { + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toInstant()"; + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return ".atZone( " + zoneOffset( conversionContext ) + ".UTC )"; + } + + @Override + protected Set getFromConversionImportTypes(ConversionContext conversionContext) { + return Collections.asSet( + conversionContext.getTypeFactory().getType( ZoneOffset.class ) + ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToLocalDateTimeConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToLocalDateTimeConversion.java new file mode 100644 index 0000000000..9a9275cede --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/JavaZonedDateTimeToLocalDateTimeConversion.java @@ -0,0 +1,36 @@ +/* + * 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.internal.conversion; + +import java.time.ZoneOffset; +import java.util.Set; + +import org.mapstruct.ap.internal.model.common.ConversionContext; +import org.mapstruct.ap.internal.model.common.Type; +import org.mapstruct.ap.internal.util.Collections; + +import static org.mapstruct.ap.internal.conversion.ConversionUtils.zoneOffset; + +public class JavaZonedDateTimeToLocalDateTimeConversion extends SimpleConversion { + + @Override + protected String getToExpression(ConversionContext conversionContext) { + return ".toLocalDateTime()"; + + } + + @Override + protected String getFromExpression(ConversionContext conversionContext) { + return ".atZone( " + zoneOffset( conversionContext ) + ".UTC )"; + } + + @Override + protected Set getFromConversionImportTypes(ConversionContext conversionContext) { + return Collections.asSet( + conversionContext.getTypeFactory().getType( ZoneOffset.class ) + ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java new file mode 100644 index 0000000000..ab17980e96 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java @@ -0,0 +1,113 @@ +/* + * 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.conversion.java8time; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion.Source; +import org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion.SourceTargetMapper; +import org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion.Target; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ Source.class, Target.class, SourceTargetMapper.class }) +@IssueKey("4027") +public class ZonedOffsetDateTimeToLocalDateTimeConversionTest { + + @RegisterExtension + GeneratedSource generatedSource = new GeneratedSource() + .addComparisonToFixtureFor( SourceTargetMapper.class ); + + @ProcessorTest + public void testZonedDateTimeToLocalDateTimeMapping() { + ZonedDateTime zonedDateTime = ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneId.of( "UTC" ) ); + Source source = new Source(); + source.setZonedDateTime( zonedDateTime ); + Target target = SourceTargetMapper.INSTANCE.toTarget( source ); + assertThat( target.getZonedDateTime() ) + .isEqualTo( LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ) ); + } + + @ProcessorTest + public void testOffsetDateTimeToLocalDateTimeMapping() { + OffsetDateTime offsetDateTime = OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ); + Source source = new Source(); + source.setOffsetDateTime( offsetDateTime ); + Target target = SourceTargetMapper.INSTANCE.toTarget( source ); + assertThat( target.getOffsetDateTime() ) + .isEqualTo( LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ) ); + } + + @ProcessorTest + public void testLocalDateTimeToZonedDateTimeMapping() { + LocalDateTime localDateTime = LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ); + Target target = new Target(); + target.setZonedDateTime( localDateTime ); + Source source = SourceTargetMapper.INSTANCE.toSource( target ); + assertThat( source.getZonedDateTime() ) + .isEqualTo( ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) ); + } + + @ProcessorTest + public void testLocalDateTimeToOffsetDateTimeMapping() { + LocalDateTime localDateTime = LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ); + Target target = new Target(); + target.setOffsetDateTime( localDateTime ); + Source source = SourceTargetMapper.INSTANCE.toSource( target ); + assertThat( source.getOffsetDateTime() ) + .isEqualTo( OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) ); + } + + @ProcessorTest + public void testZonedDateTimeToInstantMapping() { + ZonedDateTime zonedDateTime = ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ); + Source source = new Source(); + source.setZonedDateTimeAsInstant( zonedDateTime ); + Target target = SourceTargetMapper.INSTANCE.toTarget( source ); + assertThat( target.getZonedDateTimeAsInstant() ) + .isEqualTo( Instant.parse( "2024-01-01T12:30:00Z" ) ); + } + + @ProcessorTest + public void testOffsetDateTimeToInstantMapping() { + OffsetDateTime offsetDateTime = OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ); + Source source = new Source(); + source.setOffsetDateTimeAsInstant( offsetDateTime ); + Target target = SourceTargetMapper.INSTANCE.toTarget( source ); + assertThat( target.getOffsetDateTimeAsInstant() ) + .isEqualTo( Instant.parse( "2024-01-01T12:30:00Z" ) ); + } + + @ProcessorTest + public void testInstantToZonedDateTimeMapping() { + Instant instant = Instant.parse( "2024-01-01T12:30:00Z" ); + Target target = new Target(); + target.setZonedDateTimeAsInstant( instant ); + Source source = SourceTargetMapper.INSTANCE.toSource( target ); + assertThat( source.getZonedDateTimeAsInstant() ) + .isEqualTo( ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) ); + } + + @ProcessorTest + public void testInstantToOffsetDateTimeMapping() { + Instant instant = Instant.parse( "2024-01-01T12:30:00Z" ); + Target target = new Target(); + target.setOffsetDateTimeAsInstant( instant ); + Source source = SourceTargetMapper.INSTANCE.toSource( target ); + assertThat( source.getOffsetDateTimeAsInstant() ) + .isEqualTo( OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java new file mode 100644 index 0000000000..18bd99825c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java @@ -0,0 +1,48 @@ +/* + * 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.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion; + +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; + +public class Source { + private ZonedDateTime zonedDateTime; + private OffsetDateTime offsetDateTime; + private ZonedDateTime zonedDateTimeAsInstant; + private OffsetDateTime offsetDateTimeAsInstant; + + public ZonedDateTime getZonedDateTime() { + return zonedDateTime; + } + + public void setZonedDateTime(ZonedDateTime zonedDateTime) { + this.zonedDateTime = zonedDateTime; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + public ZonedDateTime getZonedDateTimeAsInstant() { + return zonedDateTimeAsInstant; + } + + public void setZonedDateTimeAsInstant(ZonedDateTime zonedDateTimeAsInstant) { + this.zonedDateTimeAsInstant = zonedDateTimeAsInstant; + } + + public OffsetDateTime getOffsetDateTimeAsInstant() { + return offsetDateTimeAsInstant; + } + + public void setOffsetDateTimeAsInstant(OffsetDateTime offsetDateTimeAsInstant) { + this.offsetDateTimeAsInstant = offsetDateTimeAsInstant; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java new file mode 100644 index 0000000000..43bee5f77f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java @@ -0,0 +1,18 @@ +/* + * 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.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SourceTargetMapper { + SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class ); + + Target toTarget(Source source); + + Source toSource(Target target); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java new file mode 100644 index 0000000000..b43b9f6843 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java @@ -0,0 +1,48 @@ +/* + * 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.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion; + +import java.time.Instant; +import java.time.LocalDateTime; + +public class Target { + private LocalDateTime zonedDateTime; + private LocalDateTime offsetDateTime; + private Instant zonedDateTimeAsInstant; + private Instant offsetDateTimeAsInstant; + + public LocalDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(LocalDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + public LocalDateTime getZonedDateTime() { + return zonedDateTime; + } + + public void setZonedDateTime(LocalDateTime zonedDateTime) { + this.zonedDateTime = zonedDateTime; + } + + public Instant getZonedDateTimeAsInstant() { + return zonedDateTimeAsInstant; + } + + public void setZonedDateTimeAsInstant(Instant zonedDateTimeAsInstant) { + this.zonedDateTimeAsInstant = zonedDateTimeAsInstant; + } + + public Instant getOffsetDateTimeAsInstant() { + return offsetDateTimeAsInstant; + } + + public void setOffsetDateTimeAsInstant(Instant offsetDateTimeAsInstant) { + this.offsetDateTimeAsInstant = offsetDateTimeAsInstant; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapperImpl.java new file mode 100644 index 0000000000..71f9998364 --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapperImpl.java @@ -0,0 +1,65 @@ +/* + * 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.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion; + +import java.time.ZoneOffset; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-05-23T21:55:42+0900", + comments = "version: , compiler: javac, environment: Java 25.0.2 (Homebrew)" +) +public class SourceTargetMapperImpl implements SourceTargetMapper { + + @Override + public Target toTarget(Source source) { + if ( source == null ) { + return null; + } + + Target target = new Target(); + + if ( source.getOffsetDateTime() != null ) { + target.setOffsetDateTime( source.getOffsetDateTime().toLocalDateTime() ); + } + if ( source.getZonedDateTime() != null ) { + target.setZonedDateTime( source.getZonedDateTime().toLocalDateTime() ); + } + if ( source.getZonedDateTimeAsInstant() != null ) { + target.setZonedDateTimeAsInstant( source.getZonedDateTimeAsInstant().toInstant() ); + } + if ( source.getOffsetDateTimeAsInstant() != null ) { + target.setOffsetDateTimeAsInstant( source.getOffsetDateTimeAsInstant().toInstant() ); + } + + return target; + } + + @Override + public Source toSource(Target target) { + if ( target == null ) { + return null; + } + + Source source = new Source(); + + if ( target.getZonedDateTime() != null ) { + source.setZonedDateTime( target.getZonedDateTime().atZone( ZoneOffset.UTC ) ); + } + if ( target.getOffsetDateTime() != null ) { + source.setOffsetDateTime( target.getOffsetDateTime().atOffset( ZoneOffset.UTC ) ); + } + if ( target.getZonedDateTimeAsInstant() != null ) { + source.setZonedDateTimeAsInstant( target.getZonedDateTimeAsInstant().atZone( ZoneOffset.UTC ) ); + } + if ( target.getOffsetDateTimeAsInstant() != null ) { + source.setOffsetDateTimeAsInstant( target.getOffsetDateTimeAsInstant().atOffset( ZoneOffset.UTC ) ); + } + + return source; + } +} \ No newline at end of file From 453602e3fa1de5e1f5717c3d9e59a23aa76aa7f5 Mon Sep 17 00:00:00 2001 From: Yevhen Vasyliev Date: Sun, 31 May 2026 19:06:45 +0300 Subject: [PATCH 02/24] #4041 Make `URLToStringConversion` generate `URI.create(String).toURL()` instead of `new URL(String)` (#4044) --- .../ap/internal/conversion/ConversionUtils.java | 12 ------------ .../internal/conversion/URLToStringConversion.java | 8 ++++---- .../ap/test/conversion/url/URLConversionTest.java | 10 +++++----- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionUtils.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionUtils.java index ee052b5405..e014e30f27 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionUtils.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/ConversionUtils.java @@ -8,7 +8,6 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.net.URI; -import java.net.URL; import java.sql.Time; import java.sql.Timestamp; import java.text.DecimalFormat; @@ -281,17 +280,6 @@ public static String uri(ConversionContext conversionContext) { return typeReferenceName( conversionContext, URI.class ); } - /** - * Name for {@link java.net.URL}. - * - * @param conversionContext Conversion context - * - * @return Name or fully-qualified name. - */ - public static String url(ConversionContext conversionContext) { - return typeReferenceName( conversionContext, URL.class ); - } - /** * Name for {@link java.text.DecimalFormatSymbols}. * diff --git a/processor/src/main/java/org/mapstruct/ap/internal/conversion/URLToStringConversion.java b/processor/src/main/java/org/mapstruct/ap/internal/conversion/URLToStringConversion.java index 34c1fec772..b1fd5f599a 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/conversion/URLToStringConversion.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/conversion/URLToStringConversion.java @@ -6,14 +6,14 @@ package org.mapstruct.ap.internal.conversion; import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; import java.util.List; import java.util.Set; import org.mapstruct.ap.internal.model.common.ConversionContext; import org.mapstruct.ap.internal.model.common.Type; -import static org.mapstruct.ap.internal.conversion.ConversionUtils.url; +import static org.mapstruct.ap.internal.conversion.ConversionUtils.uri; import static org.mapstruct.ap.internal.util.Collections.asSet; /** @@ -29,12 +29,12 @@ protected String getToExpression(ConversionContext conversionContext) { @Override protected String getFromExpression(ConversionContext conversionContext) { - return "new " + url( conversionContext ) + "( )"; + return uri( conversionContext ) + ".create( ).toURL()"; } @Override protected Set getFromConversionImportTypes(final ConversionContext conversionContext) { - return asSet( conversionContext.getTypeFactory().getType( URL.class ) ); + return asSet( conversionContext.getTypeFactory().getType( URI.class ) ); } @Override diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java index 1184134cf3..f5a3b7f7e8 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java @@ -6,7 +6,7 @@ package org.mapstruct.ap.test.conversion.url; import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.WithClasses; @@ -25,7 +25,7 @@ public class URLConversionTest { @ProcessorTest public void shouldApplyURLConversion() throws MalformedURLException { Source source = new Source(); - source.setURL( new URL("https://mapstruct.org/") ); + source.setURL( URI.create( "https://mapstruct.org/" ).toURL() ); Target target = URLMapper.INSTANCE.sourceToTarget( source ); @@ -41,13 +41,13 @@ public void shouldApplyReverseURLConversion() throws MalformedURLException { Source source = URLMapper.INSTANCE.targetToSource( target ); assertThat( source ).isNotNull(); - assertThat( source.getURL() ).isEqualTo( new URL( target.getURL() ) ); + assertThat( source.getURL() ).isEqualTo( URI.create( target.getURL() ).toURL() ); } @ProcessorTest public void shouldHandleInvalidURLString() { Target target = new Target(); - target.setInvalidURL( "XXXXXXXXX" ); + target.setInvalidURL( "xxxxxx://mapstruct.org/" ); assertThatThrownBy( () -> URLMapper.INSTANCE.targetToSource( target ) ) .isInstanceOf( RuntimeException.class ) @@ -57,7 +57,7 @@ public void shouldHandleInvalidURLString() { @ProcessorTest public void shouldHandleInvalidURLStringWithMalformedURLException() { Target target = new Target(); - target.setInvalidURL( "XXXXXXXXX" ); + target.setInvalidURL( "xxxxxx://mapstruct.org/" ); assertThatThrownBy( () -> URLMapper.INSTANCE.targetToSourceWithMalformedURLException( target ) ) .isInstanceOf( MalformedURLException.class ); From 95122187978f451c9fc54e6c67a2027dfb2840fd Mon Sep 17 00:00:00 2001 From: hduelme <46139144+hduelme@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:53:57 +0200 Subject: [PATCH 03/24] #3948 Generic types of declared type must match exactly (#4059) Declared generics must match exactly (e.g. `Comparable` cannot match `Comparable`). Recursive resolution of mismatched generics can cause infinite loops. --- .../ap/internal/model/common/Type.java | 7 ++ .../ap/test/bugs/_3948/Issue3948Mapper.java | 93 +++++++++++++++++++ .../ap/test/bugs/_3948/Issue3948Test.java | 23 +++++ 3 files changed, 123 insertions(+) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.java diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java index 5a45cb33cf..9f68ccab0e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java @@ -1842,6 +1842,13 @@ else if ( types.isSameType( types.erasure( parameterized ), types.erasure( decla for ( int i = 0; i < parameterized.getTypeArguments().size(); i++ ) { TypeMirror parameterizedTypeArg = parameterized.getTypeArguments().get( i ); Type declaredTypeArg = declared.getTypeParameters().get( i ); + if ( parameterizedTypeArg.getKind() == TypeKind.DECLARED && + !types.isSameType( types.erasure( parameterizedTypeArg ), + types.erasure( declaredTypeArg.getTypeMirror() ) ) ) { + // Comparable can never be assigned Comparable + // Generics enforce exact matches for declared types + return DEFAULT_VALUE; + } ResolvedPair result = visit( parameterizedTypeArg, declaredTypeArg ); if ( result != super.DEFAULT_VALUE ) { results.add( result ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java new file mode 100644 index 0000000000..ab053bc44e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java @@ -0,0 +1,93 @@ +/* + * 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._3948; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface Issue3948Mapper { + + class ComparableField implements Comparable { + + private final String code; + + public ComparableField(String code) { + this.code = code; + } + + @Override + public int compareTo(ComparableField o) { + return code.compareTo( o.code ); + } + } + + class Id implements Comparable> { + private final long value; + + public Id(long value) { + this.value = value; + } + + @Override + public int compareTo(Id o) { + return Long.compare( value, o.value ); + } + } + + class From { + private long id; + private String code; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + } + + class To { + private Id id; + private ComparableField comparableField; + + public Id getId() { + return id; + } + + public void setId(Id id) { + this.id = id; + } + + public ComparableField getComparableField() { + return comparableField; + } + + public void setComparableField(ComparableField comparableField) { + this.comparableField = comparableField; + } + } + + @Mapping(source = "code", target = "comparableField") + To convert(From from); + + default Id longToId(Long id) { + return id == null ? null : new Id<>(id); + } + + default ComparableField convert(String code) { + return code == null ? null : new ComparableField(code); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.java new file mode 100644 index 0000000000..8c80b2e802 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.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.bugs._3948; + +import org.junitpioneer.jupiter.Issue; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +/** + * @author hduelme + */ +@Issue("3948") +public class Issue3948Test { + + @ProcessorTest + @WithClasses(Issue3948Mapper.class) + public void shouldRequireExactGenericMatchForDeclaredSameTypes() { + + } +} From cbd5aa82715d718d0e412035c7cf6a1eb7225c78 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 5 Jun 2026 21:28:46 +0200 Subject: [PATCH 04/24] Improve JavaFileAssert#hasSameMapperContent to include File Information for the diff (#4063) The `FileInfo` from Open Test Alliance for the JVM allows the IDE to display a diff between 2 files nicely in the UI. This makes it easier to compare failing fixtures and immediately copy what you want to the fixture. Also improve the loading of the expected fixture from the resource folder instead of the compilation folder --- .../testutil/assertions/JavaFileAssert.java | 18 ++++++++++++++- .../testutil/runner/CompilingExtension.java | 2 +- .../ap/testutil/runner/GeneratedSource.java | 23 ++++++++----------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java b/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java index 2f8b13ba25..277b552f72 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java @@ -10,6 +10,7 @@ import java.io.UncheckedIOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; @@ -18,6 +19,7 @@ import org.assertj.core.internal.Diff; import org.assertj.core.internal.Failures; import org.assertj.core.util.diff.Delta; +import org.opentest4j.FileInfo; import static java.lang.String.format; @@ -81,8 +83,22 @@ public void hasSameMapperContent(File expected) { ) ); diffs.removeIf( this::ignoreDelta ); if ( !diffs.isEmpty() ) { + FileInfo actualInfo; + FileInfo expectedInfo; + try { + actualInfo = new FileInfo( actual.getAbsolutePath(), Files.readAllBytes( actual.toPath() ) ); + expectedInfo = new FileInfo( expected.getAbsolutePath(), Files.readAllBytes( expected.toPath() ) ); + } + catch ( IOException e ) { + throw new RuntimeException( e ); + } throw Failures.instance() - .failure( info, ShouldHaveSameContent.shouldHaveSameContent( actual, expected, diffs ) ); + .failure( + info, + ShouldHaveSameContent.shouldHaveSameContent( actual, expected, diffs ), + actualInfo, + expectedInfo + ); } } catch ( IOException e ) { diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java index 468e2908a1..eb18615b8b 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java @@ -674,7 +674,7 @@ boolean needsRecompilation(CompilationRequest compilationRequest, CompilationCac return !compilationRequest.equals( compilationCache.getLastRequest() ); } - private static String getBasePath() { + static String getBasePath() { try { return new File( "." ).getCanonicalPath(); } diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java index 85b6ec80fd..7c6c447d77 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java @@ -6,9 +6,6 @@ package org.mapstruct.ap.testutil.runner; import java.io.File; -import java.net.URL; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -19,6 +16,7 @@ import static org.assertj.core.api.Assertions.fail; import static org.mapstruct.ap.testutil.runner.CompilingExtension.NAMESPACE; +import static org.mapstruct.ap.testutil.runner.CompilingExtension.getBasePath; /** * A {@link org.junit.jupiter.api.extension.RegisterExtension RegisterExtension} to perform assertions on generated @@ -36,6 +34,7 @@ public class GeneratedSource implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final String FIXTURES_ROOT = "fixtures/"; + private static final String FIXTURES_DIR = getBasePath() + "/src/test/resources/fixtures/"; /** * ThreadLocal, as the source dir must be injected for this extension to gain access @@ -139,8 +138,8 @@ public JavaFileAssert forJavaFile(String path) { private void handleFixtureComparison() { for ( FixtureComparison fixture : fixturesFor ) { String fixtureName = getFixtureName( fixture.mapperClass, fixture.variant ); - URL expectedFile = getExpectedResource( fixtureName ); - if ( expectedFile == null ) { + File expectedFile = getExpectedResource( fixtureName ); + if ( !expectedFile.exists() ) { fail( String.format( "No reference file could be found for Mapper %s. You should create a file %s", fixture.mapperClass.getName(), @@ -148,8 +147,7 @@ private void handleFixtureComparison() { ) ); } else { - File expectedResource = new File( URLDecoder.decode( expectedFile.getFile(), StandardCharsets.UTF_8 ) ); - forMapper( fixture.mapperClass ).hasSameMapperContent( expectedResource ); + forMapper( fixture.mapperClass ).hasSameMapperContent( expectedFile ); } } } @@ -159,16 +157,15 @@ private String getFixtureName(Class mapperClass, String variant) { return mapperClass.getName().replace( '.', '/' ).concat( suffix ); } - private URL getExpectedResource(String fixtureName) { - ClassLoader classLoader = getClass().getClassLoader(); + private File getExpectedResource(String fixtureName) { for ( int version = Runtime.version().feature(); version >= 11 && compiler != Compiler.ECLIPSE; version-- ) { - URL resource = classLoader.getResource( FIXTURES_ROOT + "/" + version + "/" + fixtureName ); - if ( resource != null ) { - return resource; + File fixtureFile = new File( FIXTURES_DIR + version + File.separator + fixtureName ); + if ( fixtureFile.exists() ) { + return fixtureFile; } } - return classLoader.getResource( FIXTURES_ROOT + fixtureName ); + return new File( FIXTURES_DIR + fixtureName ); } private static final class FixtureComparison { From 35334e5388ebb4c464998f45d3f9bd2862393b4d Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 5 Jun 2026 21:46:38 +0200 Subject: [PATCH 05/24] #4056 Extend JSpecify support for container mapping and @NonNull return types (#4057) 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 | 17 +++++- .../model/CollectionAssignmentBuilder.java | 19 +++++++ .../model/ContainerMappingMethod.java | 7 +-- .../model/ContainerMappingMethodBuilder.java | 22 ++++++-- .../internal/model/IterableMappingMethod.java | 14 +++-- .../ap/internal/model/MapMappingMethod.java | 25 +++++++-- .../internal/model/MappingBuilderContext.java | 35 +++++++++++++ .../model/PresenceCheckMethodResolver.java | 7 +-- .../ap/internal/model/PropertyMapping.java | 6 +-- .../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 +++++++++++++++ ...JSpecifyDisabledContainerSourceMapper.java | 28 ++++++++++ .../JSpecifyDisabledNonNullReturnMapper.java | 29 +++++++++++ .../jspecify/JSpecifyDisabledTest.java | 26 ++++++++++ .../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 +++++++++++++++++ .../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 +++++++++++++++++ .../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 ++++++++++++++++ ...JSpecifyNonNullReturnStreamMapperImpl.java | 37 +++++++++++++ ...NonNullReturnUpdateIterableMapperImpl.java | 42 +++++++++++++++ ...ecifyNullableSourceIterableMapperImpl.java | 43 +++++++++++++++ ...yReturnNullOverrideIterableMapperImpl.java | 43 +++++++++++++++ .../JSpecifyStreamMethodMapperImpl.java | 34 ++++++++++++ 57 files changed, 1598 insertions(+), 29 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/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/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/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/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 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/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..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 @@ -371,7 +371,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 +507,21 @@ else if ( !method.isUpdateMethod() ) { } } + // 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 + && !presenceChecksByParameter.isEmpty() + && ctx.isJSpecifyNonNullReturn( method ) ) { + 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..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 @@ -13,6 +13,8 @@ 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; @@ -124,11 +126,19 @@ public final M build() { } assignment = getWrapper( assignment, method ); - // mapNullToDefault + // 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 && ctx.isJSpecifyNonNullReturn( method ) ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; + } MethodReference factoryMethod = null; if ( !method.isUpdateMethod() ) { @@ -151,6 +161,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 +175,8 @@ public final M build() { loopVariableName, beforeMappingMethods, afterMappingMethods, - selectionParameters + selectionParameters, + sourceParameterPresenceCheck ); } @@ -177,7 +193,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..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 @@ -18,7 +18,6 @@ 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; @@ -182,9 +181,17 @@ public MapMappingMethod build() { ctx.getMessager().note( 2, Message.MAPMAPPING_SELECT_VALUE_NOTE, valueAssignment ); } - // mapNullToDefault + // 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 && ctx.isJSpecifyNonNullReturn( method ) ) { + ctx.getMessager().note( 2, + Message.MAPPING_METHOD_JSPECIFY_FORCE_RETURN_DEFAULT, + method.getName() ); + mapNullToDefault = true; + } MethodReference factoryMethod = null; if ( !method.isUpdateMethod() ) { @@ -201,6 +208,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 +221,8 @@ public MapMappingMethod build() { factoryMethod, mapNullToDefault, beforeMappingMethods, - afterMappingMethods + afterMappingMethods, + sourceParameterPresenceCheck ); } @@ -229,11 +241,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 +267,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/MappingBuilderContext.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MappingBuilderContext.java index a933cc4b8f..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,40 @@ 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 + * 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; + } + + return getNullabilityInMapperScope( method.getExecutable() ) == NullabilityResolver.Nullability.NON_NULL; + } + public EnumMappingStrategy getEnumMappingStrategy() { return enumMappingStrategy; } 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 1f3fbf6ed2..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; } @@ -717,6 +714,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/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/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/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/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; + } +} 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 cabe266e33e3c43ed038386042deb17094ddeb7a Mon Sep 17 00:00:00 2001 From: jmwbRyDWLeNsvtzrihGoY <105581318+jmwbRyDWLeNsvtzrihGoY@users.noreply.github.com> Date: Sat, 6 Jun 2026 05:45:03 +0900 Subject: [PATCH 06/24] #4046 Fix Optional target not using static builder factory method (#4058) --- .../model/ObjectFactoryMethodResolver.java | 6 ++- .../CrossPackageOptionalBuilderMapper.java | 20 ++++++++++ .../CrossPackageOptionalBuilderTest.java | 29 ++++++++++++++ .../builder/crosspackage/PersonSource.java | 18 +++++++++ .../crosspackage/dto/PersonTarget.java | 39 +++++++++++++++++++ 5 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java index 88965dedca..2d7da03abb 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java @@ -135,7 +135,11 @@ public static List> getMatchingFactoryMethods( Meth } public static MethodReference getBuilderFactoryMethod(Method method, BuilderType builder ) { - return getBuilderFactoryMethod( method.getReturnType(), builder ); + Type returnType = method.getReturnType(); + if ( returnType.isOptionalType() ) { + returnType = returnType.getOptionalBaseType(); + } + return getBuilderFactoryMethod( returnType, builder ); } public static MethodReference getBuilderFactoryMethod(Type typeToBuild, BuilderType builder ) { diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java new file mode 100644 index 0000000000..bdff608ddc --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java @@ -0,0 +1,20 @@ +/* + * 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.optional.builder.crosspackage; + +import java.util.Optional; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.optional.builder.crosspackage.dto.PersonTarget; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface CrossPackageOptionalBuilderMapper { + + CrossPackageOptionalBuilderMapper INSTANCE = Mappers.getMapper( CrossPackageOptionalBuilderMapper.class ); + + Optional toTarget(PersonSource source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.java new file mode 100644 index 0000000000..8919787dfb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.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.optional.builder.crosspackage; + +import java.util.Optional; + +import org.mapstruct.ap.test.optional.builder.crosspackage.dto.PersonTarget; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey( "4046" ) +@WithClasses({ CrossPackageOptionalBuilderMapper.class, PersonSource.class, PersonTarget.class }) +class CrossPackageOptionalBuilderTest { + + @ProcessorTest + void shouldUseStaticBuilderMethodForOptionalTargetAcrossPackages() { + PersonSource source = new PersonSource( "John" ); + Optional result = CrossPackageOptionalBuilderMapper.INSTANCE.toTarget( source ); + + assertThat( result ).isPresent(); + assertThat( result.get().getName() ).isEqualTo( "John" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java new file mode 100644 index 0000000000..dea0c2f2b8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java @@ -0,0 +1,18 @@ +/* + * 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.optional.builder.crosspackage; + +public class PersonSource { + private final String name; + + public PersonSource(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java new file mode 100644 index 0000000000..6fb5ab770b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java @@ -0,0 +1,39 @@ +/* + * 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.optional.builder.crosspackage.dto; + +public class PersonTarget { + + private final String name; + + private PersonTarget(PersonTargetBuilder builder) { + this.name = builder.name; + } + + public String getName() { + return name; + } + + public static PersonTargetBuilder builder() { + return new PersonTargetBuilder(); + } + + public static class PersonTargetBuilder { + + private String name; + + PersonTargetBuilder() { } + + public PersonTargetBuilder name(String name) { + this.name = name; + return this; + } + + public PersonTarget build() { + return new PersonTarget( this ); + } + } +} From 750ff533802a7bf52365bc18a20ea5baacd1aafa Mon Sep 17 00:00:00 2001 From: seonwoojung Date: Mon, 8 Jun 2026 06:04:52 +0900 Subject: [PATCH 07/24] #4060 Report compilation error for SET_TO_DEFAULT without accessible no-args constructor (#4061) * #4060 Report compilation error for SET_TO_DEFAULT without accessible no-args constructor When NullValuePropertyMappingStrategy.SET_TO_DEFAULT resets a null source property, MapStruct generates new Target() to create the default instance. For target types without an accessible parameterless constructor (e.g. LocalDate, BigDecimal, Comparable) this produced uncompilable code such as target.setLocalDate( new LocalDate() ). Detect this during annotation processing and raise GENERAL_NO_SUITABLE_CONSTRUCTOR instead. --- .../ap/internal/model/PropertyMapping.java | 37 +++++++ .../ap/internal/model/common/Type.java | 24 +++++ .../mapstruct/ap/internal/util/Message.java | 1 + .../_4060/ErroneousSetToDefaultMapper.java | 100 ++++++++++++++++++ .../ap/test/bugs/_4060/Issue4060Test.java | 77 ++++++++++++++ .../test/bugs/_4060/SetToDefaultMapper.java | 75 +++++++++++++ 6 files changed, 314 insertions(+) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java 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 e7a7ab7420..163c390b66 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 @@ -485,6 +485,7 @@ private Assignment assignToPlainViaSetter(Type targetType, Assignment rhs) { includeSourceNullCheck = false; } } + reportErrorWhenSetToDefaultCannotConstructTarget( targetType, factory ); return new UpdateWrapper( rhs, method.getThrownTypes(), @@ -505,6 +506,7 @@ private Assignment assignToPlainViaSetter(Type targetType, Assignment rhs) { // however, a local var is not needed if there's no need to check for null. rhs.setSourceLocalVarName( null ); } + reportErrorWhenSetToDefaultCannotConstructTarget( targetType, null ); return new SetterWrapper( rhs, method.getThrownTypes(), @@ -518,6 +520,41 @@ private Assignment assignToPlainViaSetter(Type targetType, Assignment rhs) { } } + /** + * For {@code NullValuePropertyMappingStrategy.SET_TO_DEFAULT} the target property is reset to a new + * default instance when the source is {@code null}. When that default instance is constructed via a + * parameterless constructor ({@code new Target()}) and the target type does not provide one, MapStruct + * would generate uncompilable code such as {@code target.setLocalDate( new LocalDate() )}. Report a + * compilation error in that case instead. + * + * @param targetType the type of the target property + * @param factory the factory used to construct the default instance, or {@code null} if none is used + */ + private void reportErrorWhenSetToDefaultCannotConstructTarget(Type targetType, Assignment factory) { + if ( nvpms != SET_TO_DEFAULT || factory != null ) { + // the default instance is either not created or created through a factory / builder method + return; + } + + Type typeToConstruct = targetType.isOptionalType() ? targetType.getOptionalBaseType() : targetType; + if ( typeToConstruct.getImplementationType() != null + || typeToConstruct.isArrayType() + || typeToConstruct.isOptionalType() + || typeToConstruct.getSensibleDefault() != null ) { + // these are initialized without invoking a parameterless constructor on the target type + return; + } + + if ( !typeToConstruct.hasAccessibleParameterlessConstructor() ) { + ctx.getMessager().printMessage( + method.getExecutable(), + positionHint, + Message.PROPERTYMAPPING_NO_ACCESSIBLE_PARAMETERLESS_CONSTRUCTOR, + typeToConstruct.describe() + ); + } + } + /** * Checks whether the setter wrapper should include a null / presence check or not * diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java index 9f68ccab0e..0c02870352 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java @@ -1560,6 +1560,30 @@ public boolean hasAccessibleConstructor() { return hasAccessibleConstructor; } + /** + * Whether this type can be instantiated through an accessible parameterless constructor, i.e. via + * {@code new Type()}. Unlike {@link #hasAccessibleConstructor()} this returns {@code false} for types that + * only expose constructors with parameters (e.g. {@code BigDecimal}). + * + * @return {@code true} if a {@code new Type()} call would compile, {@code false} otherwise + */ + public boolean hasAccessibleParameterlessConstructor() { + if ( typeElement == null || isInterface() || isAbstract() || isEnumType() ) { + return false; + } + List constructors = ElementFilter.constructorsIn( typeElement.getEnclosedElements() ); + if ( constructors.isEmpty() ) { + // no declared constructors means the implicit, accessible default constructor is available + return true; + } + for ( ExecutableElement constructor : constructors ) { + if ( !constructor.getModifiers().contains( Modifier.PRIVATE ) && constructor.getParameters().isEmpty() ) { + return true; + } + } + return false; + } + public KotlinMetadata getKotlinMetadata() { if ( !kotlinMetadataInitialized ) { kotlinMetadataInitialized = true; 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 707f3c273f..acac07b4b3 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 @@ -94,6 +94,7 @@ public enum Message { PROPERTYMAPPING_CANNOT_DETERMINE_SOURCE_PROPERTY_FROM_TARGET("The type of parameter \"%s\" has no property named \"%s\". Please define the source property explicitly."), PROPERTYMAPPING_CANNOT_DETERMINE_SOURCE_PARAMETER_FROM_TARGET("No property named \"%s\" exists in source parameter(s). Please define the source explicitly."), PROPERTYMAPPING_NO_SUITABLE_COLLECTION_OR_MAP_CONSTRUCTOR( "%s does not have an accessible copy or no-args constructor." ), + PROPERTYMAPPING_NO_ACCESSIBLE_PARAMETERLESS_CONSTRUCTOR( "%s does not have an accessible parameterless constructor. Either change the nullValuePropertyMappingStrategy or define a defaultValue or a defaultExpression." ), PROPERTYMAPPING_EXPRESSION_AND_CONDITION_QUALIFIED_BY_NAME_BOTH_DEFINED( "Expression and condition qualified by name are both defined in @Mapping, either define an expression or a condition qualified by name." ), PROPERTYMAPPING_TARGET_HAS_NO_TARGET_PROPERTIES( "No target property found for target \"%s\".", Diagnostic.Kind.WARNING ), PROPERTYMAPPING_NULLABLE_SOURCE_TO_NON_NULL_CONSTRUCTOR_PARAM( "Can't map potentially nullable source property \"%s\" to @NonNull constructor parameter \"%s\". Consider adding a defaultValue or defaultExpression." ), diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java new file mode 100644 index 0000000000..4ede382781 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java @@ -0,0 +1,100 @@ +/* + * 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._4060; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; + +@Mapper +public interface ErroneousSetToDefaultMapper { + + class WithLocalDate { + private LocalDate value; + + public LocalDate getValue() { + return value; + } + + public void setValue(LocalDate value) { + this.value = value; + } + } + + class WithBigDecimal { + private BigDecimal value; + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + } + + class WithComparable { + private Comparable value; + + public Comparable getValue() { + return value; + } + + public void setValue(Comparable value) { + this.value = value; + } + } + + abstract class AbstractValue { + } + + class WithAbstractValue { + private AbstractValue value; + + public AbstractValue getValue() { + return value; + } + + public void setValue(AbstractValue value) { + this.value = value; + } + } + + enum EnumValue { + FIRST + } + + class WithEnumValue { + private EnumValue value; + + public EnumValue getValue() { + return value; + } + + public void setValue(EnumValue value) { + this.value = value; + } + } + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateLocalDate(@MappingTarget WithLocalDate target, WithLocalDate source); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateBigDecimal(@MappingTarget WithBigDecimal target, WithBigDecimal source); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateComparable(@MappingTarget WithComparable target, WithComparable source); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateAbstractValue(@MappingTarget WithAbstractValue target, WithAbstractValue source); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void updateEnumValue(@MappingTarget WithEnumValue target, WithEnumValue source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java new file mode 100644 index 0000000000..e0e51dac32 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java @@ -0,0 +1,77 @@ +/* + * 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._4060; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; +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 static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("4060") +public class Issue4060Test { + + @ProcessorTest + @WithClasses(ErroneousSetToDefaultMapper.class) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 87, + message = "LocalDate does not have an accessible parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression."), + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 90, + message = "BigDecimal does not have an accessible parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression."), + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 93, + message = "Comparable does not have an accessible parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression."), + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 96, + message = "ErroneousSetToDefaultMapper.AbstractValue does not have an accessible " + + "parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression."), + @Diagnostic(type = ErroneousSetToDefaultMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 99, + message = "ErroneousSetToDefaultMapper.EnumValue does not have an accessible " + + "parameterless constructor. " + + "Either change the nullValuePropertyMappingStrategy or define a defaultValue " + + "or a defaultExpression.") + }) + void setToDefaultWithoutParameterlessConstructorFails() { + } + + @ProcessorTest + @WithClasses(SetToDefaultMapper.class) + void setToDefaultWithParameterlessConstructorResetsToNewInstance() { + SetToDefaultMapper.Target target = new SetToDefaultMapper.Target(); + SetToDefaultMapper.Nested existing = new SetToDefaultMapper.Nested(); + existing.setName( "existing" ); + target.setNested( existing ); + target.setText( "existing" ); + + SetToDefaultMapper.INSTANCE.update( target, new SetToDefaultMapper.Source() ); + + // source properties are null, SET_TO_DEFAULT resets them to a fresh default + assertThat( target.getNested() ).isNotNull(); + assertThat( target.getNested() ).isNotSameAs( existing ); + assertThat( target.getNested().getName() ).isNull(); + assertThat( target.getText() ).isEmpty(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java new file mode 100644 index 0000000000..edfe7e9b3b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java @@ -0,0 +1,75 @@ +/* + * 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._4060; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SetToDefaultMapper { + + SetToDefaultMapper INSTANCE = Mappers.getMapper( SetToDefaultMapper.class ); + + class Nested { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Target { + private Nested nested; + private String text; + + public Nested getNested() { + return nested; + } + + public void setNested(Nested nested) { + this.nested = nested; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + + class Source { + private Nested nested; + private String text; + + public Nested getNested() { + return nested; + } + + public void setNested(Nested nested) { + this.nested = nested; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) + void update(@MappingTarget Target target, Source source); +} From 3245c3d5bd8418d7d9d5a1cd659ae564d104905a Mon Sep 17 00:00:00 2001 From: hduelme <46139144+hduelme@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:51:41 +0200 Subject: [PATCH 08/24] Upgrade codecov-action to v7 (#4067) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 168cb9c7f0..7b25fc299b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,7 +33,7 @@ jobs: run: ./mvnw jacoco:report - name: 'Upload coverage to Codecov' if: matrix.java == 21 && github.repository == 'mapstruct/mapstruct' - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} From 9376d42561c503ceab5a18a14ca317a32f46d733 Mon Sep 17 00:00:00 2001 From: Burak Yildirim <22540269+bydrim@users.noreply.github.com> Date: Sun, 14 Jun 2026 22:35:32 +0200 Subject: [PATCH 09/24] #4062 Added the new switch expression usage for jdk equal or greater than 14 (#4065) --- .../ap/internal/model/ValueMappingMethod.java | 55 +++-- .../processor/DefaultVersionInformation.java | 9 +- .../internal/version/VersionInformation.java | 2 + .../ap/internal/model/ValueMappingMethod.ftl | 36 +++- .../nestedbeans/UserDtoMapperClassicImpl.java | 121 +++++++++++ .../nestedbeans/UserDtoMapperSmartImpl.java | 196 ++++++++++++++++++ .../UserDtoUpdateMapperSmartImpl.java | 163 +++++++++++++++ .../enum2enum/DefaultOrderMapperImpl.java | 42 ++++ .../test/value/enum2enum/OrderMapperImpl.java | 64 ++++++ .../enum2enum/SpecialOrderMapperImpl.java | 79 +++++++ ...tionDefinedInMapperAndEnumMappingImpl.java | 77 +++++++ ...ingExceptionDefinedInMapperConfigImpl.java | 77 +++++++ ...ueMappingExceptionDefinedInMapperImpl.java | 77 +++++++ ...pectedValueMappingExceptionMapperImpl.java | 77 +++++++ .../value/spi/CustomCheeseMapperImpl.java | 106 ++++++++++ 15 files changed, 1159 insertions(+), 22 deletions(-) create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassicImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperSmartImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoUpdateMapperSmartImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/DefaultOrderMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/OrderMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/SpecialOrderMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMappingImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperConfigImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionMapperImpl.java create mode 100644 processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java index 46af8f0efd..82346fa8ac 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java @@ -26,6 +26,7 @@ import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.util.TypeUtils; +import org.mapstruct.ap.internal.version.VersionInformation; import org.mapstruct.ap.spi.EnumTransformationStrategy; import static org.mapstruct.ap.internal.gem.MappingConstantsGem.ANY_REMAINING; @@ -46,6 +47,7 @@ public class ValueMappingMethod extends MappingMethod { private final List valueMappings; private final MappingEntry defaultTarget; private final MappingEntry nullTarget; + private final VersionInformation versionInformation; private final Type unexpectedValueMappingException; @@ -128,19 +130,22 @@ else if ( sourceType.isString() && targetType.isEnumType() ) { new AdditionalAnnotationsBuilder( ctx.getElementUtils(), ctx.getTypeFactory(), - ctx.getMessager() ); + ctx.getMessager() + ); annotations.addAll( additionalAnnotationsBuilder.getProcessedAnnotations( method.getExecutable() ) ); } // finally return a mapping - return new ValueMappingMethod( method, + return new ValueMappingMethod( + method, annotations, mappingEntries, valueMappings.nullValueTarget, valueMappings.defaultTargetValue, determineUnexpectedValueMappingException(), beforeMappingMethods, - afterMappingMethods + afterMappingMethods, + ctx.getVersionInformation() ); } @@ -154,8 +159,10 @@ private void initializeEnumTransformationStrategy() { String nameTransformationStrategy = enumMapping.getNameTransformationStrategy(); if ( enumTransformationStrategies.containsKey( nameTransformationStrategy ) ) { - enumTransformationInvoker = new EnumTransformationStrategyInvoker( enumTransformationStrategies.get( - nameTransformationStrategy ), enumMapping.getNameTransformationConfiguration() ); + enumTransformationInvoker = new EnumTransformationStrategyInvoker( + enumTransformationStrategies.get( + nameTransformationStrategy ), enumMapping.getNameTransformationConfiguration() + ); } } @@ -180,7 +187,7 @@ private String transform(String source) { } } - private List enumToEnumMapping(Method method, Type sourceType, Type targetType ) { + private List enumToEnumMapping(Method method, Type sourceType, Type targetType) { List mappings = new ArrayList<>(); List unmappedSourceConstants = new ArrayList<>( sourceType.getEnumConstants() ); @@ -252,7 +259,8 @@ else if ( NULL.equals( targetConstant ) ) { } // all sources should now be matched, there's no default to fall back to, so if sources remain, // we have an issue. - ctx.getMessager().printMessage( method.getExecutable(), + ctx.getMessager().printMessage( + method.getExecutable(), Message.VALUEMAPPING_UNMAPPED_SOURCES, sourceErrorMessage, targetErrorMessage, @@ -264,7 +272,7 @@ else if ( NULL.equals( targetConstant ) ) { return mappings; } - private List enumToStringMapping(Method method, Type sourceType ) { + private List enumToStringMapping(Method method, Type sourceType) { List mappings = new ArrayList<>(); List unmappedSourceConstants = new ArrayList<>( sourceType.getEnumConstants() ); @@ -294,7 +302,7 @@ private List enumToStringMapping(Method method, Type sourceType ) return mappings; } - private List stringToEnumMapping(Method method, Type targetType ) { + private List stringToEnumMapping(Method method, Type targetType) { List mappings = new ArrayList<>(); List unmappedSourceConstants = new ArrayList<>( targetType.getEnumConstants() ); @@ -439,7 +447,8 @@ private boolean reportErrorIfMappedTargetEnumConstantsDontExist(Method method, T if ( valueMappings.nullTarget != null && NULL.equals( valueMappings.nullTarget.getTarget() ) && !targetEnumConstants.contains( valueMappings.nullTarget.getTarget() ) ) { - ctx.getMessager().printMessage( method.getExecutable(), + ctx.getMessager().printMessage( + method.getExecutable(), valueMappings.nullTarget.getMirror(), valueMappings.nullTarget.getTargetAnnotationValue(), Message.VALUEMAPPING_NON_EXISTING_CONSTANT, @@ -554,14 +563,29 @@ private ValueMappingMethod(Method method, String defaultTarget, Type unexpectedValueMappingException, List beforeMappingMethods, - List afterMappingMethods) { + List afterMappingMethods, + VersionInformation versionInformation) { super( method, beforeMappingMethods, afterMappingMethods ); this.valueMappings = enumMappings; this.nullTarget = new MappingEntry( null, nullTarget ); - this.defaultTarget = new MappingEntry( null, defaultTarget != null ? defaultTarget : THROW_EXCEPTION); + this.defaultTarget = new MappingEntry( null, defaultTarget != null ? defaultTarget : THROW_EXCEPTION ); this.unexpectedValueMappingException = unexpectedValueMappingException; this.overridden = method.overridesMethod(); this.annotations = annotations; + this.versionInformation = versionInformation; + } + + public boolean isDefaultTargetRequired() { + if ( !versionInformation.isSourceVersionAtLeast14() ) { + return true; + } + + Type sourceType = getSourceParameter().getType(); + if ( !sourceType.isEnumType() ) { + return true; + } + + return sourceType.getEnumConstants().size() != getValueMappings().size(); } @Override @@ -569,7 +593,8 @@ public Set getImportTypes() { Set importTypes = super.getImportTypes(); if ( unexpectedValueMappingException != null && !unexpectedValueMappingException.isJavaLangType() ) { - if ( defaultTarget.isTargetAsException() || nullTarget.isTargetAsException() || + if ( ( isDefaultTargetRequired() && defaultTarget.isTargetAsException() ) || + nullTarget.isTargetAsException() || hasMappingWithTargetAsException() ) { importTypes.addAll( unexpectedValueMappingException.getImportTypes() ); } @@ -598,6 +623,10 @@ public MappingEntry getNullTarget() { return nullTarget; } + public VersionInformation getVersionInformation() { + return versionInformation; + } + public Type getUnexpectedValueMappingException() { return unexpectedValueMappingException; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultVersionInformation.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultVersionInformation.java index f67f116745..f8053c92ab 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultVersionInformation.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultVersionInformation.java @@ -41,12 +41,13 @@ public class DefaultVersionInformation implements VersionInformation { private final String compiler; private final boolean sourceVersionAtLeast9; private final boolean sourceVersionAtLeast11; + private final boolean sourceVersionAtLeast14; private final boolean sourceVersionAtLeast19; private final boolean eclipseJDT; private final boolean javac; DefaultVersionInformation(String runtimeVersion, String runtimeVendor, String compiler, - SourceVersion sourceVersion) { + SourceVersion sourceVersion) { this.runtimeVersion = runtimeVersion; this.runtimeVendor = runtimeVendor; this.compiler = compiler; @@ -55,6 +56,7 @@ public class DefaultVersionInformation implements VersionInformation { // If the difference between the source version and RELEASE_6 is more that 2 than we are at least on 9 this.sourceVersionAtLeast9 = sourceVersion.compareTo( SourceVersion.RELEASE_6 ) > 2; this.sourceVersionAtLeast11 = sourceVersion.compareTo( SourceVersion.RELEASE_6 ) > 4; + this.sourceVersionAtLeast14 = sourceVersion.compareTo( SourceVersion.RELEASE_6 ) > 7; this.sourceVersionAtLeast19 = sourceVersion.compareTo( SourceVersion.RELEASE_6 ) > 12; } @@ -88,6 +90,11 @@ public boolean isSourceVersionAtLeast11() { return sourceVersionAtLeast11; } + @Override + public boolean isSourceVersionAtLeast14() { + return sourceVersionAtLeast14; + } + @Override public boolean isSourceVersionAtLeast19() { return sourceVersionAtLeast19; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/version/VersionInformation.java b/processor/src/main/java/org/mapstruct/ap/internal/version/VersionInformation.java index 411e11bf9e..a30a383c0c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/version/VersionInformation.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/version/VersionInformation.java @@ -23,6 +23,8 @@ public interface VersionInformation { boolean isSourceVersionAtLeast11(); + boolean isSourceVersionAtLeast14(); + boolean isSourceVersionAtLeast19(); boolean isEclipseJDTCompiler(); diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl index 104fd98437..224b6c3dfa 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl @@ -21,15 +21,35 @@ <#if nullTarget.targetAsException>throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else>return <@writeTarget target=nullTarget.target/>; } - <@includeModel object=resultType/> ${resultName}; + <#if versionInformation.isSourceVersionAtLeast14()> + <#if valueMappings.empty> + <#if defaultTarget.targetAsException> + <@includeModel object=resultType/> ${resultName}; + throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} ); + <#else> + <@includeModel object=resultType/> ${resultName} = <@writeTarget target=defaultTarget.target/>; + + <#else> + <@includeModel object=resultType/> ${resultName} = switch ( ${sourceParameter.name} ) { + <#list valueMappings as valueMapping> + case <@writeSource source=valueMapping.source/> -> <#if valueMapping.targetAsException > throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else><@writeTarget target=valueMapping.target/>; + + <#if isDefaultTargetRequired()> + default -> <#if defaultTarget.targetAsException>throw new <@includeModel object=unexpectedValueMappingException/>( "Unexpected enum constant: " + ${sourceParameter.name} )<#else><@writeTarget target=defaultTarget.target/>; + + }; + + <#else> + <@includeModel object=resultType/> ${resultName}; - switch ( ${sourceParameter.name} ) { - <#list valueMappings as valueMapping> - case <@writeSource source=valueMapping.source/>: <#if valueMapping.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else>${resultName} = <@writeTarget target=valueMapping.target/>; - break; - - default: <#if defaultTarget.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} )<#else>${resultName} = <@writeTarget target=defaultTarget.target/>; - } + switch ( ${sourceParameter.name} ) { + <#list valueMappings as valueMapping> + case <@writeSource source=valueMapping.source/>: <#if valueMapping.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else>${resultName} = <@writeTarget target=valueMapping.target/>; + break; + + default: <#if defaultTarget.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} )<#else>${resultName} = <@writeTarget target=defaultTarget.target/>; + } + <#list beforeMappingReferencesWithMappingTarget as callback> <#if callback_index = 0> diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassicImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassicImpl.java new file mode 100644 index 0000000000..408a83c145 --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassicImpl.java @@ -0,0 +1,121 @@ +/* + * 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.nestedbeans; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T22:15:18+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class UserDtoMapperClassicImpl implements UserDtoMapperClassic { + + @Override + public UserDto userToUserDto(User user) { + if ( user == null ) { + return null; + } + + UserDto userDto = new UserDto(); + + userDto.setName( user.getName() ); + userDto.setCar( carToCarDto( user.getCar() ) ); + userDto.setSecondCar( carToCarDto( user.getSecondCar() ) ); + userDto.setHouse( houseToHouseDto( user.getHouse() ) ); + + return userDto; + } + + @Override + public CarDto carToCarDto(Car car) { + if ( car == null ) { + return null; + } + + CarDto carDto = new CarDto(); + + carDto.setName( car.getName() ); + carDto.setYear( car.getYear() ); + carDto.setWheels( mapWheels( car.getWheels() ) ); + + return carDto; + } + + @Override + public HouseDto houseToHouseDto(House house) { + if ( house == null ) { + return null; + } + + HouseDto houseDto = new HouseDto(); + + houseDto.setName( house.getName() ); + houseDto.setYear( house.getYear() ); + houseDto.setRoof( roofToRoofDto( house.getRoof() ) ); + + return houseDto; + } + + @Override + public RoofDto roofToRoofDto(Roof roof) { + if ( roof == null ) { + return null; + } + + RoofDto roofDto = new RoofDto(); + + roofDto.setColor( String.valueOf( roof.getColor() ) ); + roofDto.setType( mapRoofType( roof.getType() ) ); + + return roofDto; + } + + @Override + public List mapWheels(List wheels) { + if ( wheels == null ) { + return null; + } + + List list = new ArrayList<>( wheels.size() ); + for ( Wheel wheel : wheels ) { + list.add( mapWheel( wheel ) ); + } + + return list; + } + + @Override + public WheelDto mapWheel(Wheel wheels) { + if ( wheels == null ) { + return null; + } + + WheelDto wheelDto = new WheelDto(); + + wheelDto.setFront( wheels.isFront() ); + wheelDto.setRight( wheels.isRight() ); + + return wheelDto; + } + + @Override + public ExternalRoofType mapRoofType(RoofType roofType) { + if ( roofType == null ) { + return null; + } + + ExternalRoofType externalRoofType = switch ( roofType ) { + case OPEN -> ExternalRoofType.OPEN; + case BOX -> ExternalRoofType.BOX; + case GAMBREL -> ExternalRoofType.GAMBREL; + }; + + return externalRoofType; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperSmartImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperSmartImpl.java new file mode 100644 index 0000000000..f20d3be4ef --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperSmartImpl.java @@ -0,0 +1,196 @@ +/* + * 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.nestedbeans; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T22:22:12+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class UserDtoMapperSmartImpl implements UserDtoMapperSmart { + + @Override + public UserDto userToUserDto(User user) { + if ( user == null ) { + return null; + } + + UserDto userDto = new UserDto(); + + userDto.setName( user.getName() ); + userDto.setCar( carToCarDto( user.getCar() ) ); + userDto.setSecondCar( carToCarDto( user.getSecondCar() ) ); + userDto.setHouse( houseToHouseDto( user.getHouse() ) ); + + return userDto; + } + + @Override + public org.mapstruct.ap.test.nestedbeans.other.UserDto userToUserDto2(User user) { + if ( user == null ) { + return null; + } + + org.mapstruct.ap.test.nestedbeans.other.UserDto userDto = new org.mapstruct.ap.test.nestedbeans.other.UserDto(); + + userDto.setName( user.getName() ); + userDto.setCar( carToCarDto1( user.getCar() ) ); + userDto.setHouse( houseToHouseDto1( user.getHouse() ) ); + + return userDto; + } + + protected WheelDto wheelToWheelDto(Wheel wheel) { + if ( wheel == null ) { + return null; + } + + WheelDto wheelDto = new WheelDto(); + + wheelDto.setFront( wheel.isFront() ); + wheelDto.setRight( wheel.isRight() ); + + return wheelDto; + } + + protected List wheelListToWheelDtoList(List list) { + if ( list == null ) { + return null; + } + + List list1 = new ArrayList<>( list.size() ); + for ( Wheel wheel : list ) { + list1.add( wheelToWheelDto( wheel ) ); + } + + return list1; + } + + protected CarDto carToCarDto(Car car) { + if ( car == null ) { + return null; + } + + CarDto carDto = new CarDto(); + + carDto.setName( car.getName() ); + carDto.setYear( car.getYear() ); + carDto.setWheels( wheelListToWheelDtoList( car.getWheels() ) ); + + return carDto; + } + + protected ExternalRoofType roofTypeToExternalRoofType(RoofType roofType) { + if ( roofType == null ) { + return null; + } + + ExternalRoofType externalRoofType = switch ( roofType ) { + case OPEN -> ExternalRoofType.OPEN; + case BOX -> ExternalRoofType.BOX; + case GAMBREL -> ExternalRoofType.GAMBREL; + }; + + return externalRoofType; + } + + protected RoofDto roofToRoofDto(Roof roof) { + if ( roof == null ) { + return null; + } + + RoofDto roofDto = new RoofDto(); + + roofDto.setColor( String.valueOf( roof.getColor() ) ); + roofDto.setType( roofTypeToExternalRoofType( roof.getType() ) ); + + return roofDto; + } + + protected HouseDto houseToHouseDto(House house) { + if ( house == null ) { + return null; + } + + HouseDto houseDto = new HouseDto(); + + houseDto.setName( house.getName() ); + houseDto.setYear( house.getYear() ); + houseDto.setRoof( roofToRoofDto( house.getRoof() ) ); + + return houseDto; + } + + protected org.mapstruct.ap.test.nestedbeans.other.WheelDto wheelToWheelDto1(Wheel wheel) { + if ( wheel == null ) { + return null; + } + + org.mapstruct.ap.test.nestedbeans.other.WheelDto wheelDto = new org.mapstruct.ap.test.nestedbeans.other.WheelDto(); + + wheelDto.setFront( wheel.isFront() ); + wheelDto.setRight( wheel.isRight() ); + + return wheelDto; + } + + protected List wheelListToWheelDtoList1(List list) { + if ( list == null ) { + return null; + } + + List list1 = new ArrayList<>( list.size() ); + for ( Wheel wheel : list ) { + list1.add( wheelToWheelDto1( wheel ) ); + } + + return list1; + } + + protected org.mapstruct.ap.test.nestedbeans.other.CarDto carToCarDto1(Car car) { + if ( car == null ) { + return null; + } + + org.mapstruct.ap.test.nestedbeans.other.CarDto carDto = new org.mapstruct.ap.test.nestedbeans.other.CarDto(); + + carDto.setName( car.getName() ); + carDto.setYear( car.getYear() ); + carDto.setWheels( wheelListToWheelDtoList1( car.getWheels() ) ); + + return carDto; + } + + protected org.mapstruct.ap.test.nestedbeans.other.RoofDto roofToRoofDto1(Roof roof) { + if ( roof == null ) { + return null; + } + + org.mapstruct.ap.test.nestedbeans.other.RoofDto roofDto = new org.mapstruct.ap.test.nestedbeans.other.RoofDto(); + + roofDto.setColor( String.valueOf( roof.getColor() ) ); + + return roofDto; + } + + protected org.mapstruct.ap.test.nestedbeans.other.HouseDto houseToHouseDto1(House house) { + if ( house == null ) { + return null; + } + + org.mapstruct.ap.test.nestedbeans.other.HouseDto houseDto = new org.mapstruct.ap.test.nestedbeans.other.HouseDto(); + + houseDto.setName( house.getName() ); + houseDto.setYear( house.getYear() ); + houseDto.setRoof( roofToRoofDto1( house.getRoof() ) ); + + return houseDto; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoUpdateMapperSmartImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoUpdateMapperSmartImpl.java new file mode 100644 index 0000000000..1b82ddc4ee --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoUpdateMapperSmartImpl.java @@ -0,0 +1,163 @@ +/* + * 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.nestedbeans; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T22:22:13+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class UserDtoUpdateMapperSmartImpl implements UserDtoUpdateMapperSmart { + + @Override + public void userToUserDto(UserDto userDto, User user) { + if ( user == null ) { + return; + } + + if ( user.getName() != null ) { + userDto.setName( user.getName() ); + } + else { + userDto.setName( null ); + } + if ( user.getCar() != null ) { + if ( userDto.getCar() == null ) { + userDto.setCar( new CarDto() ); + } + carToCarDto( user.getCar(), userDto.getCar() ); + } + else { + userDto.setCar( null ); + } + if ( user.getSecondCar() != null ) { + if ( userDto.getSecondCar() == null ) { + userDto.setSecondCar( new CarDto() ); + } + carToCarDto( user.getSecondCar(), userDto.getSecondCar() ); + } + else { + userDto.setSecondCar( null ); + } + if ( user.getHouse() != null ) { + if ( userDto.getHouse() == null ) { + userDto.setHouse( new HouseDto() ); + } + houseToHouseDto( user.getHouse(), userDto.getHouse() ); + } + else { + userDto.setHouse( null ); + } + } + + protected WheelDto wheelToWheelDto(Wheel wheel) { + if ( wheel == null ) { + return null; + } + + WheelDto wheelDto = new WheelDto(); + + wheelDto.setFront( wheel.isFront() ); + wheelDto.setRight( wheel.isRight() ); + + return wheelDto; + } + + protected List wheelListToWheelDtoList(List list) { + if ( list == null ) { + return null; + } + + List list1 = new ArrayList<>( list.size() ); + for ( Wheel wheel : list ) { + list1.add( wheelToWheelDto( wheel ) ); + } + + return list1; + } + + protected void carToCarDto(Car car, CarDto mappingTarget) { + if ( car == null ) { + return; + } + + if ( car.getName() != null ) { + mappingTarget.setName( car.getName() ); + } + else { + mappingTarget.setName( null ); + } + mappingTarget.setYear( car.getYear() ); + if ( mappingTarget.getWheels() != null ) { + List list = wheelListToWheelDtoList( car.getWheels() ); + if ( list != null ) { + mappingTarget.getWheels().clear(); + mappingTarget.getWheels().addAll( list ); + } + } + else { + List list = wheelListToWheelDtoList( car.getWheels() ); + if ( list != null ) { + mappingTarget.setWheels( list ); + } + } + } + + protected ExternalRoofType roofTypeToExternalRoofType(RoofType roofType) { + if ( roofType == null ) { + return null; + } + + ExternalRoofType externalRoofType = switch ( roofType ) { + case OPEN -> ExternalRoofType.OPEN; + case BOX -> ExternalRoofType.BOX; + case GAMBREL -> ExternalRoofType.GAMBREL; + }; + + return externalRoofType; + } + + protected void roofToRoofDto(Roof roof, RoofDto mappingTarget) { + if ( roof == null ) { + return; + } + + mappingTarget.setColor( String.valueOf( roof.getColor() ) ); + if ( roof.getType() != null ) { + mappingTarget.setType( roofTypeToExternalRoofType( roof.getType() ) ); + } + else { + mappingTarget.setType( null ); + } + } + + protected void houseToHouseDto(House house, HouseDto mappingTarget) { + if ( house == null ) { + return; + } + + if ( house.getName() != null ) { + mappingTarget.setName( house.getName() ); + } + else { + mappingTarget.setName( null ); + } + mappingTarget.setYear( house.getYear() ); + if ( house.getRoof() != null ) { + if ( mappingTarget.getRoof() == null ) { + mappingTarget.setRoof( new RoofDto() ); + } + roofToRoofDto( house.getRoof(), mappingTarget.getRoof() ); + } + else { + mappingTarget.setRoof( null ); + } + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/DefaultOrderMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/DefaultOrderMapperImpl.java new file mode 100644 index 0000000000..864accaa9e --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/DefaultOrderMapperImpl.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.value.enum2enum; + +import javax.annotation.processing.Generated; +import org.mapstruct.ap.test.value.ExternalOrderType; +import org.mapstruct.ap.test.value.OrderType; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-05T22:27:58+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class DefaultOrderMapperImpl implements DefaultOrderMapper { + + @Override + public OrderDto orderEntityToDto(OrderEntity order) { + if ( order == null ) { + return null; + } + + OrderDto orderDto = new OrderDto(); + + orderDto.setOrderType( orderTypeToExternalOrderType( order.getOrderType() ) ); + + return orderDto; + } + + @Override + public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT; + + return externalOrderType; + } +} \ No newline at end of file diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/OrderMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/OrderMapperImpl.java new file mode 100644 index 0000000000..61e6bb3f03 --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/OrderMapperImpl.java @@ -0,0 +1,64 @@ +/* + * 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.value.enum2enum; + +import javax.annotation.processing.Generated; +import org.mapstruct.ap.test.value.ExternalOrderType; +import org.mapstruct.ap.test.value.OrderType; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T14:19:13+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class OrderMapperImpl implements OrderMapper { + + @Override + public OrderDto orderEntityToDto(OrderEntity order) { + if ( order == null ) { + return null; + } + + OrderDto orderDto = new OrderDto(); + + orderDto.setOrderType( orderTypeToExternalOrderType( order.getOrderType() ) ); + + return orderDto; + } + + @Override + public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case EXTRA -> ExternalOrderType.SPECIAL; + case STANDARD -> ExternalOrderType.DEFAULT; + case NORMAL -> ExternalOrderType.DEFAULT; + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + }; + + return externalOrderType; + } + + @Override + public OrderType externalOrderTypeToOrderType(ExternalOrderType orderType) { + if ( orderType == null ) { + return null; + } + + OrderType orderType1 = switch ( orderType ) { + case SPECIAL -> OrderType.EXTRA; + case DEFAULT -> OrderType.STANDARD; + case RETAIL -> OrderType.RETAIL; + case B2B -> OrderType.B2B; + }; + + return orderType1; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/SpecialOrderMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/SpecialOrderMapperImpl.java new file mode 100644 index 0000000000..223485c327 --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/SpecialOrderMapperImpl.java @@ -0,0 +1,79 @@ +/* + * 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.value.enum2enum; + +import javax.annotation.processing.Generated; +import org.mapstruct.ap.test.value.ExternalOrderType; +import org.mapstruct.ap.test.value.OrderType; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T16:23:42+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class SpecialOrderMapperImpl implements SpecialOrderMapper { + + @Override + public OrderDto orderEntityToDto(OrderEntity order) { + if ( order == null ) { + return null; + } + + OrderDto orderDto = new OrderDto(); + + orderDto.setOrderType( orderTypeToExternalOrderType( order.getOrderType() ) ); + + return orderDto; + } + + @Override + public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) { + if ( orderType == null ) { + return ExternalOrderType.DEFAULT; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case STANDARD -> null; + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + default -> ExternalOrderType.SPECIAL; + }; + + return externalOrderType; + } + + @Override + public OrderType externalOrderTypeToOrderType(ExternalOrderType orderType) { + if ( orderType == null ) { + return OrderType.STANDARD; + } + + OrderType orderType1 = switch ( orderType ) { + case SPECIAL -> OrderType.EXTRA; + case DEFAULT -> null; + case RETAIL -> OrderType.RETAIL; + case B2B -> OrderType.B2B; + }; + + return orderType1; + } + + @Override + public ExternalOrderType anyRemainingToNull(OrderType orderType) { + if ( orderType == null ) { + return ExternalOrderType.DEFAULT; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case STANDARD -> null; + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + default -> null; + }; + + return externalOrderType; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMappingImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMappingImpl.java new file mode 100644 index 0000000000..bfa7fc1fdc --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMappingImpl.java @@ -0,0 +1,77 @@ +/* + * 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.value.exception; + +import javax.annotation.processing.Generated; +import org.mapstruct.ap.test.value.ExternalOrderType; +import org.mapstruct.ap.test.value.OrderType; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T16:23:39+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMappingImpl implements CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMapping { + + @Override + public ExternalOrderType withAnyUnmapped(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT; + + return externalOrderType; + } + + @Override + public ExternalOrderType withAnyRemaining(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + default -> ExternalOrderType.DEFAULT; + }; + + return externalOrderType; + } + + @Override + public ExternalOrderType onlyWithMappings(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case EXTRA -> ExternalOrderType.SPECIAL; + case STANDARD -> ExternalOrderType.DEFAULT; + case NORMAL -> ExternalOrderType.DEFAULT; + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + }; + + return externalOrderType; + } + + @Override + public OrderType inverseOnlyWithMappings(ExternalOrderType orderType) { + if ( orderType == null ) { + return null; + } + + OrderType orderType1 = switch ( orderType ) { + case SPECIAL -> OrderType.EXTRA; + case DEFAULT -> OrderType.STANDARD; + case RETAIL -> OrderType.RETAIL; + case B2B -> OrderType.B2B; + }; + + return orderType1; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperConfigImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperConfigImpl.java new file mode 100644 index 0000000000..641ff95b17 --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperConfigImpl.java @@ -0,0 +1,77 @@ +/* + * 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.value.exception; + +import javax.annotation.processing.Generated; +import org.mapstruct.ap.test.value.ExternalOrderType; +import org.mapstruct.ap.test.value.OrderType; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T22:01:43+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class CustomUnexpectedValueMappingExceptionDefinedInMapperConfigImpl implements CustomUnexpectedValueMappingExceptionDefinedInMapperConfig { + + @Override + public ExternalOrderType withAnyUnmapped(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT; + + return externalOrderType; + } + + @Override + public ExternalOrderType withAnyRemaining(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + default -> ExternalOrderType.DEFAULT; + }; + + return externalOrderType; + } + + @Override + public ExternalOrderType onlyWithMappings(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case EXTRA -> ExternalOrderType.SPECIAL; + case STANDARD -> ExternalOrderType.DEFAULT; + case NORMAL -> ExternalOrderType.DEFAULT; + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + }; + + return externalOrderType; + } + + @Override + public OrderType inverseOnlyWithMappings(ExternalOrderType orderType) { + if ( orderType == null ) { + return null; + } + + OrderType orderType1 = switch ( orderType ) { + case SPECIAL -> OrderType.EXTRA; + case DEFAULT -> OrderType.STANDARD; + case RETAIL -> OrderType.RETAIL; + case B2B -> OrderType.B2B; + }; + + return orderType1; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperImpl.java new file mode 100644 index 0000000000..2f47b74ae4 --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperImpl.java @@ -0,0 +1,77 @@ +/* + * 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.value.exception; + +import javax.annotation.processing.Generated; +import org.mapstruct.ap.test.value.ExternalOrderType; +import org.mapstruct.ap.test.value.OrderType; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T14:19:13+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class CustomUnexpectedValueMappingExceptionDefinedInMapperImpl implements CustomUnexpectedValueMappingExceptionDefinedInMapper { + + @Override + public ExternalOrderType withAnyUnmapped(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT; + + return externalOrderType; + } + + @Override + public ExternalOrderType withAnyRemaining(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + default -> ExternalOrderType.DEFAULT; + }; + + return externalOrderType; + } + + @Override + public ExternalOrderType onlyWithMappings(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case EXTRA -> ExternalOrderType.SPECIAL; + case STANDARD -> ExternalOrderType.DEFAULT; + case NORMAL -> ExternalOrderType.DEFAULT; + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + }; + + return externalOrderType; + } + + @Override + public OrderType inverseOnlyWithMappings(ExternalOrderType orderType) { + if ( orderType == null ) { + return null; + } + + OrderType orderType1 = switch ( orderType ) { + case SPECIAL -> OrderType.EXTRA; + case DEFAULT -> OrderType.STANDARD; + case RETAIL -> OrderType.RETAIL; + case B2B -> OrderType.B2B; + }; + + return orderType1; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionMapperImpl.java new file mode 100644 index 0000000000..ef5c3f9bfa --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionMapperImpl.java @@ -0,0 +1,77 @@ +/* + * 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.value.exception; + +import javax.annotation.processing.Generated; +import org.mapstruct.ap.test.value.ExternalOrderType; +import org.mapstruct.ap.test.value.OrderType; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T22:04:26+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class CustomUnexpectedValueMappingExceptionMapperImpl implements CustomUnexpectedValueMappingExceptionMapper { + + @Override + public ExternalOrderType withAnyUnmapped(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT; + + return externalOrderType; + } + + @Override + public ExternalOrderType withAnyRemaining(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + default -> ExternalOrderType.DEFAULT; + }; + + return externalOrderType; + } + + @Override + public ExternalOrderType onlyWithMappings(OrderType orderType) { + if ( orderType == null ) { + return null; + } + + ExternalOrderType externalOrderType = switch ( orderType ) { + case EXTRA -> ExternalOrderType.SPECIAL; + case STANDARD -> ExternalOrderType.DEFAULT; + case NORMAL -> ExternalOrderType.DEFAULT; + case RETAIL -> ExternalOrderType.RETAIL; + case B2B -> ExternalOrderType.B2B; + }; + + return externalOrderType; + } + + @Override + public OrderType inverseOnlyWithMappings(ExternalOrderType orderType) { + if ( orderType == null ) { + return null; + } + + OrderType orderType1 = switch ( orderType ) { + case SPECIAL -> OrderType.EXTRA; + case DEFAULT -> OrderType.STANDARD; + case RETAIL -> OrderType.RETAIL; + case B2B -> OrderType.B2B; + }; + + return orderType1; + } +} diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java new file mode 100644 index 0000000000..94ac662859 --- /dev/null +++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java @@ -0,0 +1,106 @@ +/* + * 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.value.spi; + +import javax.annotation.processing.Generated; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2026-06-12T14:19:17+0200", + comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)" +) +public class CustomCheeseMapperImpl implements CustomCheeseMapper { + + @Override + public CheeseType map(CustomCheeseType cheese) { + if ( cheese == null ) { + return null; + } + + CheeseType cheeseType = switch ( cheese ) { + case UNSPECIFIED -> null; + case CUSTOM_BRIE -> CheeseType.BRIE; + case CUSTOM_ROQUEFORT -> CheeseType.ROQUEFORT; + case UNRECOGNIZED -> null; + }; + + return cheeseType; + } + + @Override + public CustomCheeseType map(CheeseType cheese) { + if ( cheese == null ) { + return CustomCheeseType.UNSPECIFIED; + } + + CustomCheeseType customCheeseType = switch ( cheese ) { + case BRIE -> CustomCheeseType.CUSTOM_BRIE; + case ROQUEFORT -> CustomCheeseType.CUSTOM_ROQUEFORT; + }; + + return customCheeseType; + } + + @Override + public String mapToString(CustomCheeseType cheeseType) { + if ( cheeseType == null ) { + return null; + } + + String string = switch ( cheeseType ) { + case UNSPECIFIED -> null; + case CUSTOM_BRIE -> "BRIE"; + case CUSTOM_ROQUEFORT -> "ROQUEFORT"; + case UNRECOGNIZED -> null; + }; + + return string; + } + + @Override + public String mapToString(CheeseType cheeseType) { + if ( cheeseType == null ) { + return null; + } + + String string = switch ( cheeseType ) { + case BRIE -> "BRIE"; + case ROQUEFORT -> "ROQUEFORT"; + }; + + return string; + } + + @Override + public CustomCheeseType mapStringToCustom(String cheese) { + if ( cheese == null ) { + return CustomCheeseType.UNSPECIFIED; + } + + CustomCheeseType customCheeseType = switch ( cheese ) { + case "BRIE" -> CustomCheeseType.CUSTOM_BRIE; + case "ROQUEFORT" -> CustomCheeseType.CUSTOM_ROQUEFORT; + default -> CustomCheeseType.CUSTOM_BRIE; + }; + + return customCheeseType; + } + + @Override + public CheeseType mapStringToCheese(String cheese) { + if ( cheese == null ) { + return null; + } + + CheeseType cheeseType = switch ( cheese ) { + case "BRIE" -> CheeseType.BRIE; + case "ROQUEFORT" -> CheeseType.ROQUEFORT; + default -> CheeseType.BRIE; + }; + + return cheeseType; + } +} From f5253e82492ae693b9407017992edbc8de30765d Mon Sep 17 00:00:00 2001 From: Raimund Klein <770876+Chessray@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:05:57 +0100 Subject: [PATCH 10/24] #2513 Define new enum ClassAccessibility (#4071) * Define new enum ClassAccessibility for controlling generated Mappers' declared accessibility. --- .../org/mapstruct/ClassAccessibility.java | 28 +++++++++++ core/src/main/java/org/mapstruct/Mapper.java | 10 ++++ .../main/java/org/mapstruct/MapperConfig.java | 10 ++++ .../chapter-4-retrieving-a-mapper.asciidoc | 22 ++++++++- .../internal/gem/ClassAccessibilityGem.java | 15 ++++++ .../ap/internal/model/GeneratedType.java | 6 +++ .../mapstruct/ap/internal/model/Mapper.java | 8 ++- .../internal/model/common/Accessibility.java | 4 +- .../internal/model/source/DefaultOptions.java | 6 +++ .../model/source/DelegatingOptions.java | 5 ++ .../model/source/MapperConfigOptions.java | 8 +++ .../internal/model/source/MapperOptions.java | 8 +++ .../processor/MapperCreationProcessor.java | 1 + .../ClassAccessibilityTest.java | 49 +++++++++++++++++++ .../ForcedPackagePrivateMapper.java | 15 ++++++ .../ForcedPublicMapper.java | 15 ++++++ .../PackageAbstractionMapper.java | 14 ++++++ .../PublicAbstractionMapper.java | 14 ++++++ .../ap/test/classaccessibility/Source.java | 21 ++++++++ .../ap/test/classaccessibility/Target.java | 21 ++++++++ .../mapstruct/ap/test/gem/EnumGemsTest.java | 8 +++ .../ap/test/mapperconfig/CentralConfig.java | 5 +- .../ap/test/mapperconfig/ConfigTest.java | 18 +++++++ 23 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/org/mapstruct/ClassAccessibility.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/gem/ClassAccessibilityGem.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java diff --git a/core/src/main/java/org/mapstruct/ClassAccessibility.java b/core/src/main/java/org/mapstruct/ClassAccessibility.java new file mode 100644 index 0000000000..c533b1ea32 --- /dev/null +++ b/core/src/main/java/org/mapstruct/ClassAccessibility.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; + +/** + * Determines whether a generated Mapper implementation class will be declared {@code public} or not. + * + * @author Raimund Klein + * + * @since 1.7.0 + */ +public enum ClassAccessibility { + /** + * The generated Mapper will have the same modifier ({@code public} or none) as the annotated class or interface. + */ + DEFAULT, + /** + * The generated Mapper will be declared {@code public}. + */ + PUBLIC, + /** + * The generated Mapper will have no visibility modifier ("package-private"). + */ + PACKAGE_PRIVATE +} diff --git a/core/src/main/java/org/mapstruct/Mapper.java b/core/src/main/java/org/mapstruct/Mapper.java index 398dc1870a..c502dfe8a9 100644 --- a/core/src/main/java/org/mapstruct/Mapper.java +++ b/core/src/main/java/org/mapstruct/Mapper.java @@ -14,6 +14,7 @@ import org.mapstruct.control.MappingControl; import org.mapstruct.factory.Mappers; +import static org.mapstruct.ClassAccessibility.DEFAULT; import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR; @@ -388,4 +389,13 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default * @since 1.5 */ boolean suppressTimestampInGenerated() default false; + + /** + * Determines the {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper + * implementation. Default is to mirror the interface or abstract class annotated by this {@code Mapper}. + * + * @return The {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper + * implementation + */ + ClassAccessibility accessibility() default DEFAULT; } diff --git a/core/src/main/java/org/mapstruct/MapperConfig.java b/core/src/main/java/org/mapstruct/MapperConfig.java index 8631562a56..a81e3e8d4f 100644 --- a/core/src/main/java/org/mapstruct/MapperConfig.java +++ b/core/src/main/java/org/mapstruct/MapperConfig.java @@ -14,6 +14,7 @@ import org.mapstruct.control.MappingControl; import org.mapstruct.factory.Mappers; +import static org.mapstruct.ClassAccessibility.DEFAULT; import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR; @@ -357,5 +358,14 @@ MappingInheritanceStrategy mappingInheritanceStrategy() * @since 1.5 */ boolean suppressTimestampInGenerated() default false; + + /** + * Determines the {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper + * implementation. Default is to mirror the interface or abstract class annotated by {@code Mapper}. + * + * @return The {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper + * implementation + */ + ClassAccessibility accessibility() default DEFAULT; } diff --git a/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc b/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc index 35afc93a51..ec817e0cdf 100644 --- a/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc +++ b/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc @@ -130,4 +130,24 @@ In that case utilize the `InjectionStrategy#SETTER` strategy. [TIP] ==== For abstract classes or decorators setter injection should be used. -==== \ No newline at end of file +==== + +[[class-accessibility]] +=== Class accessibility + +By default, the generated Mapper implementation will have the same accessibility (`public` or none) as the interface or abstract class carrying the `@Mapper` annotation. Especially in conjunction with a DI framework, this might not be desired. If you wish to reduce the visibility of the generated class, you can make use of the `accessibility` attribute like this: + +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, accessibility = ClassAccessibility.PACKAGE_PRIVATE) +public interface CarMapper { + + CarDto carToCarDto(Car car); +} + +---- +==== + +In this case, the generated Mapper will have no modifier. Possible values are `ClassAccessibility.DEFAULT` (the behavior described above), `ClassAccessibility.PUBLIC`, and `ClassAccessibility.PACKAGE_PRIVATE` (no modifier). diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/ClassAccessibilityGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/ClassAccessibilityGem.java new file mode 100644 index 0000000000..609c6d04e7 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/ClassAccessibilityGem.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.internal.gem; + +/** + * Gem for the enum {@link org.mapstruct.ClassAccessibility} + * + * @author Raimund Klein + */ +public enum ClassAccessibilityGem { + DEFAULT, PUBLIC, PACKAGE_PRIVATE +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedType.java b/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedType.java index 4b39e92273..6eb8bbfef8 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedType.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/GeneratedType.java @@ -12,6 +12,7 @@ import java.util.TreeSet; import javax.lang.model.type.TypeKind; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.model.common.Accessibility; import org.mapstruct.ap.internal.model.common.ModelElement; import org.mapstruct.ap.internal.model.common.Type; @@ -40,6 +41,7 @@ protected abstract static class GeneratedTypeBuilder extraImportedTypes; protected List methods; + protected ClassAccessibilityGem classAccessibility; GeneratedTypeBuilder(Class selfType) { myself = selfType.cast( this ); @@ -75,6 +77,10 @@ public T methods(List methods) { return myself; } + public T classAccessibility(ClassAccessibilityGem classAccessibility) { + this.classAccessibility = classAccessibility; + return myself; + } } private final String packageName; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/Mapper.java b/processor/src/main/java/org/mapstruct/ap/internal/model/Mapper.java index bcf7e840e5..c261358455 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/Mapper.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/Mapper.java @@ -17,6 +17,8 @@ import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.version.VersionInformation; +import static org.mapstruct.ap.internal.gem.ClassAccessibilityGem.DEFAULT; + /** * Represents a type implementing a mapper interface (annotated with {@code @Mapper}). This is the root object of the * mapper model. @@ -109,6 +111,10 @@ public Mapper build() { Type definitionType = typeFactory.getType( element ); + Accessibility accessibility = + classAccessibility == DEFAULT ? Accessibility.fromModifiers( element.getModifiers() ) : + Accessibility.valueOf( classAccessibility.name() ); + return new Mapper( typeFactory, packageName, @@ -121,7 +127,7 @@ public Mapper build() { options, versionInformation, suppressGeneratorTimestamp, - Accessibility.fromModifiers( element.getModifiers() ), + accessibility, fields, constructor, decorator, diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Accessibility.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Accessibility.java index ef5be01285..1988e4f90b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Accessibility.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Accessibility.java @@ -14,7 +14,7 @@ * @author Andreas Gudian */ public enum Accessibility { - PRIVATE( "private" ), DEFAULT( "" ), PROTECTED( "protected" ), PUBLIC( "public" ); + PRIVATE( "private" ), PACKAGE_PRIVATE( "" ), PROTECTED( "protected" ), PUBLIC( "public" ); private final String keyword; @@ -37,6 +37,6 @@ else if ( modifiers.contains( Modifier.PRIVATE ) ) { return PRIVATE; } - return DEFAULT; + return PACKAGE_PRIVATE; } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java index 8d76e65c75..d958272f9b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java @@ -11,6 +11,7 @@ import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.gem.MapperGem; @@ -155,6 +156,11 @@ public NullValueMappingStrategyGem getNullValueMapMappingStrategy() { return NullValueMappingStrategyGem.valueOf( mapper.nullValueMapMappingStrategy().getDefaultValue() ); } + @Override + public ClassAccessibilityGem accessibility() { + return ClassAccessibilityGem.valueOf( mapper.accessibility().getDefaultValue() ); + } + public BuilderGem getBuilder() { // TODO: I realized this is not correct, however it needs to be null in order to keep downward compatibility // but assuming a default @Builder will make testcases fail. Not having a default means that you need to diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java index 34478969fa..e97f527745 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java @@ -12,6 +12,7 @@ import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.gem.MappingInheritanceStrategyGem; @@ -84,6 +85,10 @@ public Boolean isDisableSubMappingMethodsGeneration() { return next.isDisableSubMappingMethodsGeneration(); } + public ClassAccessibilityGem accessibility() { + return next.accessibility(); + } + // BeanMapping and Mapping public CollectionMappingStrategyGem getCollectionMappingStrategy() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java index b656e2b5ea..b51008b713 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java @@ -10,6 +10,7 @@ import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.gem.MapperConfigGem; @@ -169,6 +170,13 @@ public NullValueMappingStrategyGem getNullValueMapMappingStrategy() { return next().getNullValueMapMappingStrategy(); } + @Override + public ClassAccessibilityGem accessibility() { + return mapperConfig.accessibility().hasValue() ? + ClassAccessibilityGem.valueOf( mapperConfig.accessibility().get() ) : + next().accessibility(); + } + @Override public BuilderGem getBuilder() { return mapperConfig.builder().hasValue() ? mapperConfig.builder().get() : next().getBuilder(); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java index fa3d527a30..b30b8c95fa 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java @@ -14,6 +14,7 @@ import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; import org.mapstruct.ap.internal.gem.MapperConfigGem; @@ -170,6 +171,13 @@ public SubclassExhaustiveStrategyGem getSubclassExhaustiveStrategy() { next().getSubclassExhaustiveStrategy(); } + @Override + public ClassAccessibilityGem accessibility() { + return mapper.accessibility().hasValue() ? + ClassAccessibilityGem.valueOf( mapper.accessibility().get() ) : + next().accessibility(); + } + @Override public TypeMirror getSubclassExhaustiveException() { return mapper.subclassExhaustiveException().hasValue() ? diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java index 8bae8ee4c1..db16d52106 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java @@ -217,6 +217,7 @@ private Mapper getMapper(TypeElement element, MapperOptions mapperOptions, List< .suppressGeneratorTimestamp( mapperOptions.suppressTimestampInGenerated() ) .additionalAnnotations( additionalAnnotationsBuilder.getProcessedAnnotations( element ) ) .javadoc( getJavadoc( element ) ) + .classAccessibility( mapperOptions.accessibility() ) .build(); if ( !mappingContext.getForgedMethodsUnderCreation().isEmpty() ) { diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.java new file mode 100644 index 0000000000..8bac5841f1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.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.classaccessibility; + +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static java.lang.reflect.Modifier.isPrivate; +import static java.lang.reflect.Modifier.isProtected; +import static java.lang.reflect.Modifier.isPublic; +import static org.assertj.core.api.Assertions.assertThat; + +@WithClasses({ + Source.class, + Target.class, + PublicAbstractionMapper.class, + PackageAbstractionMapper.class, + ForcedPublicMapper.class, + ForcedPackagePrivateMapper.class +}) +public class ClassAccessibilityTest { + + @ProcessorTest + public void shouldCreateModifierAccordingToAnnotation() throws Exception { + Class publicLike = loadForMapper( PublicAbstractionMapper.class ); + assertThat( isPublic( publicLike.getModifiers() ) ).isTrue(); + + Class packageLike = loadForMapper( PackageAbstractionMapper.class ); + assertThat( isDefault( packageLike.getModifiers() ) ).isTrue(); + + Class forcedPublic = loadForMapper( ForcedPublicMapper.class ); + assertThat( isPublic( forcedPublic.getModifiers() ) ).isTrue(); + + Class forcedDefault = loadForMapper( ForcedPackagePrivateMapper.class ); + assertThat( isDefault( forcedDefault.getModifiers() ) ).isTrue(); + } + + private static Class loadForMapper(Class mapper) throws ClassNotFoundException { + return Thread.currentThread().getContextClassLoader().loadClass( mapper.getName() + "Impl" ); + } + + private static boolean isDefault(int modifiers) { + return !isPublic( modifiers ) && !isProtected( modifiers ) && !isPrivate( modifiers ); + } +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.java new file mode 100644 index 0000000000..191aadb2a0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.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.classaccessibility; + +import org.mapstruct.ClassAccessibility; +import org.mapstruct.Mapper; + +@Mapper(accessibility = ClassAccessibility.PACKAGE_PRIVATE) +public interface ForcedPackagePrivateMapper { + Target map(Source value); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.java new file mode 100644 index 0000000000..fd6ced910a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.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.classaccessibility; + +import org.mapstruct.ClassAccessibility; +import org.mapstruct.Mapper; + +@Mapper(accessibility = ClassAccessibility.PUBLIC) +interface ForcedPublicMapper { + Target map(Source value); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java new file mode 100644 index 0000000000..d9f426b382 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java @@ -0,0 +1,14 @@ +/* + * 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.classaccessibility; + +import org.mapstruct.Mapper; + +@Mapper +interface PackageAbstractionMapper { + Target map(Source value); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java new file mode 100644 index 0000000000..7c1b5de10f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java @@ -0,0 +1,14 @@ +/* + * 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.classaccessibility; + +import org.mapstruct.Mapper; + +@Mapper +public interface PublicAbstractionMapper { + Target map(Source value); +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java new file mode 100644 index 0000000000..feff965719 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java @@ -0,0 +1,21 @@ +/* + * 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.classaccessibility; + +/** + * @author Raimund Klein + **/ +public class Source { + private String foo; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java new file mode 100644 index 0000000000..e8c50a2531 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java @@ -0,0 +1,21 @@ +/* + * 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.classaccessibility; + +/** + * @author Raimund Klein + **/ +public class Target { + private String foo; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java b/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java index f460180651..57c2099b8b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java @@ -10,6 +10,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.mapstruct.ClassAccessibility; import org.mapstruct.CollectionMappingStrategy; import org.mapstruct.ConditionStrategy; import org.mapstruct.InjectionStrategy; @@ -17,6 +18,7 @@ import org.mapstruct.NullValueCheckStrategy; import org.mapstruct.NullValueMappingStrategy; import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.internal.gem.ClassAccessibilityGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.ConditionStrategyGem; import org.mapstruct.ap.internal.gem.InjectionStrategyGem; @@ -75,6 +77,12 @@ public void conditionStrategyGemIsCorrect() { namesOf( ConditionStrategyGem.values() ) ); } + @Test + public void classAccesibilityGemIsCorrect() { + assertThat( namesOf( ClassAccessibility.values() ) ).isEqualTo( + namesOf( ClassAccessibilityGem.values() ) ); + } + private static List namesOf(Enum[] values) { return Stream.of( values ) .map( Enum::name ) diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java index 81dc9a452a..3caeb8cbba 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java +++ b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java @@ -5,6 +5,7 @@ */ package org.mapstruct.ap.test.mapperconfig; +import org.mapstruct.ClassAccessibility; import org.mapstruct.MapperConfig; import org.mapstruct.ReportingPolicy; @@ -12,7 +13,9 @@ * * @author Sjaak Derksen */ -@MapperConfig(uses = { CustomMapperViaMapperConfig.class }, unmappedTargetPolicy = ReportingPolicy.ERROR ) +@MapperConfig(uses = {CustomMapperViaMapperConfig.class}, + unmappedTargetPolicy = ReportingPolicy.ERROR, + accessibility = ClassAccessibility.PACKAGE_PRIVATE) public class CentralConfig { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java index 1f7fe423f5..a13db4ab0b 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java @@ -12,6 +12,9 @@ import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic; import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome; +import static java.lang.reflect.Modifier.isPrivate; +import static java.lang.reflect.Modifier.isProtected; +import static java.lang.reflect.Modifier.isPublic; import static org.assertj.core.api.Assertions.assertThat; /** @@ -49,6 +52,13 @@ public void shouldUseCustomMapperViaMapperConfigForFooToDto() { assertThat( source.getFoo().getCreatedBy() ).isEqualTo( CustomMapperViaMapperConfig.class.getSimpleName() ); } + @ProcessorTest + @WithClasses( { Target.class, SourceTargetMapper.class } ) + public void shouldDeclareMapperImplementationAsPackagePrivate() throws ClassNotFoundException { + Class implementation = loadForMapper( SourceTargetMapper.class ); + assertThat( isDefault( implementation.getModifiers() ) ).isTrue(); + } + @ProcessorTest @WithClasses( { TargetNoFoo.class, SourceTargetMapperWarn.class } ) @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, @@ -70,4 +80,12 @@ public void shouldUseWARNViaMapper() { }) public void shouldUseERRORViaMapperConfig() { } + + private static Class loadForMapper(Class mapper) throws ClassNotFoundException { + return Thread.currentThread().getContextClassLoader().loadClass( mapper.getName() + "Impl" ); + } + + private static boolean isDefault(int modifiers) { + return !isPublic( modifiers ) && !isProtected( modifiers ) && !isPrivate( modifiers ); + } } From bb73200b5ad5bd2e4a444b5ad93448df1bd0c825 Mon Sep 17 00:00:00 2001 From: hduelme <46139144+hduelme@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:10:30 +0200 Subject: [PATCH 11/24] Enforce ParenPad for ENUM_CONSTANT_DEF (#4075) --- build-config/src/main/resources/build-config/checkstyle.xml | 2 +- .../ap/test/source/nullvaluecheckstrategy/Stage.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build-config/src/main/resources/build-config/checkstyle.xml b/build-config/src/main/resources/build-config/checkstyle.xml index fa83d1d709..b15e108c5c 100644 --- a/build-config/src/main/resources/build-config/checkstyle.xml +++ b/build-config/src/main/resources/build-config/checkstyle.xml @@ -149,7 +149,7 @@ - + diff --git a/processor/src/test/java/org/mapstruct/ap/test/source/nullvaluecheckstrategy/Stage.java b/processor/src/test/java/org/mapstruct/ap/test/source/nullvaluecheckstrategy/Stage.java index 3a13f0aa1c..be70aa9754 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/source/nullvaluecheckstrategy/Stage.java +++ b/processor/src/test/java/org/mapstruct/ap/test/source/nullvaluecheckstrategy/Stage.java @@ -14,9 +14,9 @@ */ public enum Stage { - MAIN("Paul McCartney", "Ellie Goulding", "Disclosure", "Kaiser Chiefs", "Rammstein"), - KLUB_C("James Blake", "Lost Frequencies"), - THE_BARN("New Order", "Year and Years"); + MAIN( "Paul McCartney", "Ellie Goulding", "Disclosure", "Kaiser Chiefs", "Rammstein" ), + KLUB_C( "James Blake", "Lost Frequencies" ), + THE_BARN( "New Order", "Year and Years" ); private final List artists; From f57a13fb8a5f6491f28de7efb9d56a80dff6f7b2 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 26 Jun 2026 21:46:46 +0200 Subject: [PATCH 12/24] Update copyright.txt and changelog for changes since 893c30e --- NEXT_RELEASE_CHANGELOG.md | 10 ++++++++++ copyright.txt | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/NEXT_RELEASE_CHANGELOG.md b/NEXT_RELEASE_CHANGELOG.md index 4e67c55aba..89d3f8925b 100644 --- a/NEXT_RELEASE_CHANGELOG.md +++ b/NEXT_RELEASE_CHANGELOG.md @@ -10,10 +10,14 @@ - 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 * Add `URI` to `String` built-in conversions (#4018) +* Add `ClassAccessibility` enum to control the declared accessibility of generated mappers (#2513) ### Enhancements * Support `SET_TO_NULL` for overloaded target methods, requiring a cast (#3949) +* Use switch expressions in generated code when targeting JDK 14 or later (#4062) +* Make `URLToStringConversion` generate `URI.create(String).toURL()` instead of the deprecated `new URL(String)` (#4041) +* Report a compilation error for `SET_TO_DEFAULT` when the target has no accessible no-args constructor (#4060) * Use multi-catch in generated code (#4021) * Use diamond operator for `new` expressions in generated code (#4045) * Always use factory methods for `LinkedHashMap` and `LinkedHashSet` when targeting `SequencedSet` and `SequencedMap` (#3990) @@ -24,6 +28,9 @@ * Prevent mapper generation from a type with a generic super bound to a type with a generic extends bound (#3994) * Fix location for Javadoc when generating the distribution zip +* Fix `Optional` target not using the static builder factory method (#4046) +* Match generic types of a declared type exactly to prevent incorrect mappings and infinite resolution loops (#3948) +* Fix mapping `ZonedDateTime` or `OffsetDateTime` to `LocalDateTime` or `Instant` (#4027) ### Documentation @@ -48,6 +55,9 @@ * Let GitHub determine whether or not the released version is the latest * Simplify parallel-capable registration in `ModifiableURLClassLoader` test util (#4052) * Improve class loading for tests by replacing `LauncherDiscoveryListener` with `CompilerLauncherInterceptor` (#4051) +* Enforce `ParenPad` for `ENUM_CONSTANT_DEF` via Checkstyle (#4075) +* Upgrade codecov-action to v7 (#4067) +* Improve `JavaFileAssert#hasSameMapperContent` to include file information in the diff (#4063) ### Internal diff --git a/copyright.txt b/copyright.txt index 39a863e531..74f18d89b1 100644 --- a/copyright.txt +++ b/copyright.txt @@ -8,6 +8,7 @@ Andreas Gudian - https://github.com/agudian Andrei Arlou - https://github.com/Captain1653 Andres Jose Sebastian Rincon Gonzalez - https://github.com/stianrincon Arne Seime - https://github.com/seime +Burak Yildirim - https://github.com/bydrim Cause Chung - https://github.com/cuzfrog Christian Bandowski - https://github.com/chris922 Chris DeLashmutt - https://github.com/cdelashmutt-pivotal @@ -36,6 +37,7 @@ Ivo Smid - https://github.com/bedla Jason Bodnar - https://github.com/Blackbaud-JasonBodnar Jeroen van Wilgenburg - https://github.com/jvwilge Jeff Smyth - https://github.com/smythie86 +jmwbRyDWLeNsvtzrihGoY - https://github.com/jmwbRyDWLeNsvtzrihGoY João Paulo Bassinello - https://github.com/jpbassinello Jonathan Kraska - https://github.com/jakraska Joshua Spoerri - https://github.com/spoerri @@ -57,6 +59,7 @@ Paul Strugnell - https://github.com/ps-powa Pascal Grün - https://github.com/pascalgn Pavel Makhov - https://github.com/streetturtle Peter Larson - https://github.com/pjlarson +Raimund Klein - https://github.com/Chessray Remko Plantenga - https://github.com/sonata82 Remo Meier - https://github.com/remmeier Roel Mangelschots - https://github.com/rmschots @@ -69,8 +72,10 @@ Samuel Wright - https://github.com/samwright Sebastian Haberey - https://github.com/sebastianhaberey Sebastian Hasait - https://github.com/shasait Sean Huang - https://github.com/seanjob +seonwoojung - https://github.com/seonwooj0810 Sjaak Derksen - https://github.com/sjaakd Stefan May - https://github.com/osthus-sm +Takch02 - https://github.com/Takch02 Taras Mychaskiw - https://github.com/twentylemon Thibault Duperron - https://github.com/Zomzog Tomáš Poledný - https://github.com/Saljack @@ -84,6 +89,7 @@ Vincent Alexander Beelte - https://github.com/grandmasterpixel Winter Andreas - https://github.dev/wandi34 Xiu Hong Kooi - https://github.com/kooixh Yang Tang - https://github.com/tangyang9464 +Yevhen Vasyliev - https://github.com/yvasyliev Zegveld - https://github.com/Zegveld znight1020 - https://github.com/znight1020 zral - https://github.com/zyberzebra From 72f741dc136b440d1ad0e402a965a70d23a86496 Mon Sep 17 00:00:00 2001 From: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:54:21 +0000 Subject: [PATCH 13/24] Releasing version 1.7.0.Beta2 --- build-config/pom.xml | 2 +- core-jdk8/pom.xml | 2 +- core/pom.xml | 2 +- distribution/pom.xml | 2 +- documentation/pom.xml | 2 +- integrationtest/pom.xml | 2 +- parent/pom.xml | 4 ++-- pom.xml | 2 +- processor/pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build-config/pom.xml b/build-config/pom.xml index 0b3ca1c113..9ce2dc6c0c 100644 --- a/build-config/pom.xml +++ b/build-config/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/core-jdk8/pom.xml b/core-jdk8/pom.xml index c73676192b..7549118f50 100644 --- a/core-jdk8/pom.xml +++ b/core-jdk8/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/core/pom.xml b/core/pom.xml index e62d5e4682..fa1a5e8467 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/distribution/pom.xml b/distribution/pom.xml index b480e24a06..0e6db09026 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/documentation/pom.xml b/documentation/pom.xml index 21a2b151bd..4754aae091 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/integrationtest/pom.xml b/integrationtest/pom.xml index 71a61fc2fe..82df1f7287 100644 --- a/integrationtest/pom.xml +++ b/integrationtest/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/parent/pom.xml b/parent/pom.xml index 1f340ba2ab..be2d1adce6 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -11,7 +11,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 pom MapStruct Parent @@ -28,7 +28,7 @@ ${java.version} ${java.version} - ${git.commit.author.time} + 2026-06-26T20:54:20Z 1.0.0.Alpha3 3.6.2 diff --git a/pom.xml b/pom.xml index c753e3c1e4..7304c3d1ba 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 parent/pom.xml diff --git a/processor/pom.xml b/processor/pom.xml index e317365aec..a2f8201694 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml From 1e746985f364fb05161be6e01c9f1b513567267d Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 26 Jun 2026 23:55:24 +0200 Subject: [PATCH 14/24] Upgrade JReleaser to 1.24.0 --- parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parent/pom.xml b/parent/pom.xml index be2d1adce6..052739bf69 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -39,7 +39,7 @@ 13.0.0 5.14.1 2.2.0 - 1.12.0 + 1.24.0 1 3.27.7 From df8d557ec1acc3f7b9232d2618f8e8a70ebe1ab0 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Sat, 27 Jun 2026 00:32:28 +0200 Subject: [PATCH 15/24] Move to Snapshot version and disable Lombok tests on Java 27 and 28 --- build-config/pom.xml | 2 +- core-jdk8/pom.xml | 2 +- core/pom.xml | 2 +- distribution/pom.xml | 2 +- documentation/pom.xml | 2 +- integrationtest/pom.xml | 2 +- .../java/org/mapstruct/itest/tests/MavenIntegrationTest.java | 4 ++-- parent/pom.xml | 4 ++-- pom.xml | 2 +- processor/pom.xml | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build-config/pom.xml b/build-config/pom.xml index 9ce2dc6c0c..0b3ca1c113 100644 --- a/build-config/pom.xml +++ b/build-config/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/core-jdk8/pom.xml b/core-jdk8/pom.xml index 7549118f50..c73676192b 100644 --- a/core-jdk8/pom.xml +++ b/core-jdk8/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/core/pom.xml b/core/pom.xml index fa1a5e8467..e62d5e4682 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/distribution/pom.xml b/distribution/pom.xml index 0e6db09026..b480e24a06 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/documentation/pom.xml b/documentation/pom.xml index 4754aae091..21a2b151bd 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/integrationtest/pom.xml b/integrationtest/pom.xml index 82df1f7287..71a61fc2fe 100644 --- a/integrationtest/pom.xml +++ b/integrationtest/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java index 60784d206a..042d6ec1da 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java @@ -89,7 +89,7 @@ void jsr330Test() { @ProcessorTest(baseDir = "lombokBuilderTest", processorTypes = { ProcessorTest.ProcessorType.JAVAC }) - @DisabledOnJre(versions = 27) + @DisabledOnJre(versions = { 27, 28 }) void lombokBuilderTest() { } @@ -98,7 +98,7 @@ void lombokBuilderTest() { ProcessorTest.ProcessorType.JAVAC_WITH_PATHS }) @EnabledForJreRange(min = JRE.JAVA_11) - @DisabledOnJre(versions = 27) + @DisabledOnJre(versions = { 27, 28 }) void lombokModuleTest() { } diff --git a/parent/pom.xml b/parent/pom.xml index 052739bf69..074c507ea1 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -11,7 +11,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT pom MapStruct Parent @@ -28,7 +28,7 @@ ${java.version} ${java.version} - 2026-06-26T20:54:20Z + ${git.commit.author.time} 1.0.0.Alpha3 3.6.2 diff --git a/pom.xml b/pom.xml index 7304c3d1ba..c753e3c1e4 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT parent/pom.xml diff --git a/processor/pom.xml b/processor/pom.xml index a2f8201694..e317365aec 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml From 6a521651f1c3ffe2de8b9b3d9ebd363c36a0b661 Mon Sep 17 00:00:00 2001 From: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:34:46 +0000 Subject: [PATCH 16/24] Releasing version 1.7.0.Beta2 --- build-config/pom.xml | 2 +- core-jdk8/pom.xml | 2 +- core/pom.xml | 2 +- distribution/pom.xml | 2 +- documentation/pom.xml | 2 +- integrationtest/pom.xml | 2 +- parent/pom.xml | 4 ++-- pom.xml | 2 +- processor/pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build-config/pom.xml b/build-config/pom.xml index 0b3ca1c113..9ce2dc6c0c 100644 --- a/build-config/pom.xml +++ b/build-config/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/core-jdk8/pom.xml b/core-jdk8/pom.xml index c73676192b..7549118f50 100644 --- a/core-jdk8/pom.xml +++ b/core-jdk8/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/core/pom.xml b/core/pom.xml index e62d5e4682..fa1a5e8467 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/distribution/pom.xml b/distribution/pom.xml index b480e24a06..0e6db09026 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/documentation/pom.xml b/documentation/pom.xml index 21a2b151bd..4754aae091 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/integrationtest/pom.xml b/integrationtest/pom.xml index 71a61fc2fe..82df1f7287 100644 --- a/integrationtest/pom.xml +++ b/integrationtest/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/parent/pom.xml b/parent/pom.xml index 074c507ea1..ae9f87cfaf 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -11,7 +11,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 pom MapStruct Parent @@ -28,7 +28,7 @@ ${java.version} ${java.version} - ${git.commit.author.time} + 2026-06-26T22:34:46Z 1.0.0.Alpha3 3.6.2 diff --git a/pom.xml b/pom.xml index c753e3c1e4..7304c3d1ba 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 parent/pom.xml diff --git a/processor/pom.xml b/processor/pom.xml index e317365aec..a2f8201694 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml From 29f0ebe71cee18ad37ddd954c4d35b2a2454856c Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Sat, 27 Jun 2026 07:48:41 +0200 Subject: [PATCH 17/24] Use Maven 3.9.16 --- .mvn/wrapper/maven-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 8dea6c227c..216df05897 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,3 @@ wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip From 947463544be202b69e3625e3d923582053457d24 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Sat, 27 Jun 2026 08:08:43 +0200 Subject: [PATCH 18/24] Update jreleaser signing.armored to signing.pgp.armored --- parent/pom.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/parent/pom.xml b/parent/pom.xml index ae9f87cfaf..53431bb52f 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -879,7 +879,10 @@ ALWAYS - true + + ALWAYS + true + false From c49ce621cd7027e3938e0d351111cf7b4cc1b17c Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Sat, 27 Jun 2026 00:32:28 +0200 Subject: [PATCH 19/24] Move to Snapshot version --- build-config/pom.xml | 2 +- core-jdk8/pom.xml | 2 +- core/pom.xml | 2 +- distribution/pom.xml | 2 +- documentation/pom.xml | 2 +- integrationtest/pom.xml | 2 +- parent/pom.xml | 4 ++-- pom.xml | 2 +- processor/pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build-config/pom.xml b/build-config/pom.xml index 9ce2dc6c0c..0b3ca1c113 100644 --- a/build-config/pom.xml +++ b/build-config/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/core-jdk8/pom.xml b/core-jdk8/pom.xml index 7549118f50..c73676192b 100644 --- a/core-jdk8/pom.xml +++ b/core-jdk8/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/core/pom.xml b/core/pom.xml index fa1a5e8467..e62d5e4682 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/distribution/pom.xml b/distribution/pom.xml index 0e6db09026..b480e24a06 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/documentation/pom.xml b/documentation/pom.xml index 4754aae091..21a2b151bd 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/integrationtest/pom.xml b/integrationtest/pom.xml index 82df1f7287..71a61fc2fe 100644 --- a/integrationtest/pom.xml +++ b/integrationtest/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/parent/pom.xml b/parent/pom.xml index 53431bb52f..b3f3670d6b 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -11,7 +11,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT pom MapStruct Parent @@ -28,7 +28,7 @@ ${java.version} ${java.version} - 2026-06-26T22:34:46Z + ${git.commit.author.time} 1.0.0.Alpha3 3.6.2 diff --git a/pom.xml b/pom.xml index 7304c3d1ba..c753e3c1e4 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT parent/pom.xml diff --git a/processor/pom.xml b/processor/pom.xml index a2f8201694..e317365aec 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml From 98da5b852443c34fcec257c622216269d834a52a Mon Sep 17 00:00:00 2001 From: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 06:55:00 +0000 Subject: [PATCH 20/24] Releasing version 1.7.0.Beta2 --- build-config/pom.xml | 2 +- core-jdk8/pom.xml | 2 +- core/pom.xml | 2 +- distribution/pom.xml | 2 +- documentation/pom.xml | 2 +- integrationtest/pom.xml | 2 +- parent/pom.xml | 4 ++-- pom.xml | 2 +- processor/pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build-config/pom.xml b/build-config/pom.xml index 0b3ca1c113..9ce2dc6c0c 100644 --- a/build-config/pom.xml +++ b/build-config/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/core-jdk8/pom.xml b/core-jdk8/pom.xml index c73676192b..7549118f50 100644 --- a/core-jdk8/pom.xml +++ b/core-jdk8/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/core/pom.xml b/core/pom.xml index e62d5e4682..fa1a5e8467 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/distribution/pom.xml b/distribution/pom.xml index b480e24a06..0e6db09026 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/documentation/pom.xml b/documentation/pom.xml index 21a2b151bd..4754aae091 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/integrationtest/pom.xml b/integrationtest/pom.xml index 71a61fc2fe..82df1f7287 100644 --- a/integrationtest/pom.xml +++ b/integrationtest/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml diff --git a/parent/pom.xml b/parent/pom.xml index b3f3670d6b..434a9dbc1f 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -11,7 +11,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 pom MapStruct Parent @@ -28,7 +28,7 @@ ${java.version} ${java.version} - ${git.commit.author.time} + 2026-06-27T06:54:59Z 1.0.0.Alpha3 3.6.2 diff --git a/pom.xml b/pom.xml index c753e3c1e4..7304c3d1ba 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 parent/pom.xml diff --git a/processor/pom.xml b/processor/pom.xml index e317365aec..a2f8201694 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0-SNAPSHOT + 1.7.0.Beta2 ../parent/pom.xml From a4d1f42ba505fcb9be7264249b2d5b883ed1ca16 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Sat, 27 Jun 2026 09:35:43 +0200 Subject: [PATCH 21/24] Next version 1.7.0-SNAPSHOT --- NEXT_RELEASE_CHANGELOG.md | 61 --------------------------------------- build-config/pom.xml | 2 +- core-jdk8/pom.xml | 2 +- core/pom.xml | 2 +- distribution/pom.xml | 2 +- documentation/pom.xml | 2 +- integrationtest/pom.xml | 2 +- parent/pom.xml | 4 +-- pom.xml | 2 +- processor/pom.xml | 2 +- 10 files changed, 10 insertions(+), 71 deletions(-) diff --git a/NEXT_RELEASE_CHANGELOG.md b/NEXT_RELEASE_CHANGELOG.md index 89d3f8925b..e0f4cd31f0 100644 --- a/NEXT_RELEASE_CHANGELOG.md +++ b/NEXT_RELEASE_CHANGELOG.md @@ -1,71 +1,10 @@ ### Features -* 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 -* Add `URI` to `String` built-in conversions (#4018) -* Add `ClassAccessibility` enum to control the declared accessibility of generated mappers (#2513) - ### Enhancements -* Support `SET_TO_NULL` for overloaded target methods, requiring a cast (#3949) -* Use switch expressions in generated code when targeting JDK 14 or later (#4062) -* Make `URLToStringConversion` generate `URI.create(String).toURL()` instead of the deprecated `new URL(String)` (#4041) -* Report a compilation error for `SET_TO_DEFAULT` when the target has no accessible no-args constructor (#4060) -* Use multi-catch in generated code (#4021) -* Use diamond operator for `new` expressions in generated code (#4045) -* Always use factory methods for `LinkedHashMap` and `LinkedHashSet` when targeting `SequencedSet` and `SequencedMap` (#3990) -* Improve annotation processing performance by removing regex matching from `Type.describe()` (#3991) -* Add support for generics in arrays (#4050) - ### Bugs -* Prevent mapper generation from a type with a generic super bound to a type with a generic extends bound (#3994) -* Fix location for Javadoc when generating the distribution zip -* Fix `Optional` target not using the static builder factory method (#4046) -* Match generic types of a declared type exactly to prevent incorrect mappings and infinite resolution loops (#3948) -* Fix mapping `ZonedDateTime` or `OffsetDateTime` to `LocalDateTime` or `Instant` (#4027) - ### Documentation -* Add `SECURITY.md` and `.github/INCIDENT_RESPONSE.md` - ### Build -* Test on JDK 25 and 26, drop integration test on JDK 11 (#4039) -* Specify OpenJDK 21 for the Jitpack build (#4042) -* Add CodeQL custom workflow and set build mode for `java-kotlin` to `none` -* Use diamond operator in test code (#4048) -* Upgrade integration tests to JUnit 5 (#4023) -* Update Maven compiler plugin (#3972) -* Upgrade Freemarker to 2.3.34 -* Update license plugin -* Enforce import order via Checkstyle `CustomImportOrder` (#4024) -* Enforce spaces inside parentheses for control flow statements via Checkstyle -* Simplify `fail` in `assertCheckstyleRules` -* Remove deprecated `Number` API usage from tests -* Use `StandardCharsets.UTF_8` in tests -* Remove obsolete override of AssertJ version in integration tests -* Let GitHub determine whether or not the released version is the latest -* Simplify parallel-capable registration in `ModifiableURLClassLoader` test util (#4052) -* Improve class loading for tests by replacing `LauncherDiscoveryListener` with `CompilerLauncherInterceptor` (#4051) -* Enforce `ParenPad` for `ENUM_CONSTANT_DEF` via Checkstyle (#4075) -* Upgrade codecov-action to v7 (#4067) -* Improve `JavaFileAssert#hasSameMapperContent` to include file information in the diff (#4063) - -### Internal - -* Upgrade internal `Visitor6` usages to `Visitor8` -* Refactor `TypeFactory.getTypeParameters` (#4020) -* Simplify boolean logic in `ValueMappingMethod` by removing inversion (#4007) -* Remove unnecessary `keySet()` invocation (#3989) -* Remove unused methods in `Fields` (#4010) -* Add missing self reference in `GeneratedTypeBuilder` (#4009) -* Fix self check in `equals` of `Type` (#3995) -* Remove reflection from `isDefaultMethod` (#4053) diff --git a/build-config/pom.xml b/build-config/pom.xml index 9ce2dc6c0c..0b3ca1c113 100644 --- a/build-config/pom.xml +++ b/build-config/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/core-jdk8/pom.xml b/core-jdk8/pom.xml index 7549118f50..c73676192b 100644 --- a/core-jdk8/pom.xml +++ b/core-jdk8/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/core/pom.xml b/core/pom.xml index fa1a5e8467..e62d5e4682 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/distribution/pom.xml b/distribution/pom.xml index 0e6db09026..b480e24a06 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/documentation/pom.xml b/documentation/pom.xml index 4754aae091..21a2b151bd 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/integrationtest/pom.xml b/integrationtest/pom.xml index 82df1f7287..71a61fc2fe 100644 --- a/integrationtest/pom.xml +++ b/integrationtest/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml diff --git a/parent/pom.xml b/parent/pom.xml index 434a9dbc1f..b3f3670d6b 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -11,7 +11,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT pom MapStruct Parent @@ -28,7 +28,7 @@ ${java.version} ${java.version} - 2026-06-27T06:54:59Z + ${git.commit.author.time} 1.0.0.Alpha3 3.6.2 diff --git a/pom.xml b/pom.xml index 7304c3d1ba..c753e3c1e4 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT parent/pom.xml diff --git a/processor/pom.xml b/processor/pom.xml index a2f8201694..e317365aec 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -12,7 +12,7 @@ org.mapstruct mapstruct-parent - 1.7.0.Beta2 + 1.7.0-SNAPSHOT ../parent/pom.xml From 2134c0c102c20117a62e0bb1c711d26481fc992c Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Sat, 27 Jun 2026 09:53:42 +0200 Subject: [PATCH 22/24] Add correct permissions for JReleaser (closing milestone) + add JReleaser GitHub pre-release pattern + add Claude related gitignore --- .github/workflows/release.yml | 1 + .gitignore | 4 ++++ parent/pom.xml | 3 +++ 3 files changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aafb79a534..61049e9f53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + issues: write steps: - uses: actions/checkout@v4 with: diff --git a/.gitignore b/.gitignore index a33f0014ee..3cfac4e9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ test-output .DS_Store checkstyle.cache .flattened-pom.xml + +.claude/settings.local.json +.claude/worktrees +.mcp.json diff --git a/parent/pom.xml b/parent/pom.xml index b3f3670d6b..b444c1847a 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -913,6 +913,9 @@ ${maven.multiModuleProjectDirectory}/NEXT_RELEASE_CHANGELOG.md + + \d+\.\d+\.\d+\.(Alpha|Beta|M|RC)\d* +