From cb05db4739c9251bc91e8750c2484c48e81c14db Mon Sep 17 00:00:00 2001 From: Bill Kim <199436087+BK202503@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:11:22 +0900 Subject: [PATCH] #4000 Fix property name resolution for fluent setters whose second character is upper case --- NEXT_RELEASE_CHANGELOG.md | 1 + .../ap/spi/DefaultAccessorNamingStrategy.java | 9 ++++- .../ap/test/bugs/_4000/Issue4000Mapper.java | 17 +++++++++ .../ap/test/bugs/_4000/Issue4000Test.java | 28 ++++++++++++++ .../mapstruct/ap/test/bugs/_4000/Source.java | 19 ++++++++++ .../mapstruct/ap/test/bugs/_4000/Target.java | 37 +++++++++++++++++++ 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Issue4000Mapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Issue4000Test.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Source.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Target.java diff --git a/NEXT_RELEASE_CHANGELOG.md b/NEXT_RELEASE_CHANGELOG.md index 4e67c55aba..4fea39e922 100644 --- a/NEXT_RELEASE_CHANGELOG.md +++ b/NEXT_RELEASE_CHANGELOG.md @@ -24,6 +24,7 @@ * 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 property name resolution for fluent setters whose second character is upper case (#4000) ### Documentation diff --git a/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java index 20cb1c76c1..3b972e3098 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java @@ -242,7 +242,14 @@ public String getPropertyName(ExecutableElement getterOrSetterMethod) { return IntrospectorUtils.decapitalize( methodName.substring( 3 ) ); } else { - return methodName; + // [#4000] Apply the same JavaBean-style decapitalization as for `getXyz` / + // `setXyz` accessors so that a fluent setter `xNameField(...)` resolves to + // the same property name as a JavaBean accessor `getXNameField()` / + // `setXNameField(...)`. Without this, the two are recognised as different + // properties when the second character is upper case (see Introspector spec). + return IntrospectorUtils.decapitalize( + Character.toUpperCase( methodName.charAt( 0 ) ) + methodName.substring( 1 ) + ); } } return IntrospectorUtils.decapitalize( methodName.substring( methodName.startsWith( "is" ) ? 2 : 3 ) ); diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Issue4000Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Issue4000Mapper.java new file mode 100644 index 0000000000..50310512ac --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Issue4000Mapper.java @@ -0,0 +1,17 @@ +/* + * 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._4000; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface Issue4000Mapper { + + Issue4000Mapper INSTANCE = Mappers.getMapper( Issue4000Mapper.class ); + + Target map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Issue4000Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Issue4000Test.java new file mode 100644 index 0000000000..de434a0697 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Issue4000Test.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.bugs._4000; + +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; + +@WithClasses({ + Issue4000Mapper.class, + Source.class, + Target.class, +}) +@IssueKey("4000") +public class Issue4000Test { + + @ProcessorTest + public void fluentSetterWithSecondCharUpperCaseShouldMatchJavaBeanAccessor() { + Target target = Issue4000Mapper.INSTANCE.map( new Source( "value" ) ); + + assertThat( target.getXNameField() ).isEqualTo( "value" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Source.java new file mode 100644 index 0000000000..c67f8f43bb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Source.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.bugs._4000; + +public class Source { + + private final String xNameField; + + public Source(String xNameField) { + this.xNameField = xNameField; + } + + public String getXNameField() { + return xNameField; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Target.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Target.java new file mode 100644 index 0000000000..bff4b4dcc0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4000/Target.java @@ -0,0 +1,37 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.ap.test.bugs._4000; + +public class Target { + + private final String xNameField; + + public Target(Builder builder) { + this.xNameField = builder.xNameField; + } + + public String getXNameField() { + return xNameField; + } + + public static Target.Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String xNameField; + + public Builder xNameField(String xNameField) { + this.xNameField = xNameField; + return this; + } + + public Target build() { + return new Target( this ); + } + } +}