From c0b0f8e59e430c5a8605a646391d0b7d44c566e0 Mon Sep 17 00:00:00 2001 From: renechoi Date: Tue, 28 Jul 2026 02:01:47 +0900 Subject: [PATCH 1/2] #3861 Do not warn about unused Map source parameter used in an expression --- .../ap/internal/model/BeanMappingMethod.java | 25 +++++++++- .../ap/test/frommap/FromMapMappingTest.java | 17 +++++++ ...eanNonStringMapUsedInExpressionMapper.java | 50 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInExpressionMapper.java diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index 080eab1e76..ac09b32bc3 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 @@ -21,6 +21,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.Supplier; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -2071,7 +2072,12 @@ private void reportErrorForUnusedSourceParameters() { Type parameterType = sourceParameter.getType(); if ( parameterType.isMapType() ) { // We are only going to output a warning for the source parameter if it was unused - // i.e. the intention of the user was most likely to use it as a mapping from Bean to Map + // i.e. the intention of the user was most likely to use it as a mapping from Bean to Map. + // A parameter that is referenced from a Java expression is used, even though it does not + // take part in the property mapping itself. + if ( isReferencedFromJavaExpression( sourceParameter ) ) { + continue; + } List typeParameters = parameterType.getTypeParameters(); if ( typeParameters.size() != 2 || !typeParameters.get( 0 ).isString() ) { Message message = typeParameters.isEmpty() ? @@ -2092,6 +2098,23 @@ private void reportErrorForUnusedSourceParameters() { } } } + + private boolean isReferencedFromJavaExpression(Parameter sourceParameter) { + Pattern reference = Pattern.compile( "\\b" + Pattern.quote( sourceParameter.getName() ) + "\\b" ); + for ( MappingOptions mapping : method.getOptions().getMappings() ) { + if ( isReferencedBy( reference, mapping.getJavaExpression() ) + || isReferencedBy( reference, mapping.getDefaultJavaExpression() ) + || isReferencedBy( reference, mapping.getConditionJavaExpression() ) ) { + return true; + } + } + + return false; + } + + private boolean isReferencedBy(Pattern reference, String javaExpression) { + return javaExpression != null && reference.matcher( javaExpression ).find(); + } } private static class ConstructorAccessor { diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java index bcb78d8dcc..4842e86dbb 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java @@ -369,6 +369,23 @@ void shouldNotWarnIfMappedIsUsedAsSourceParameter() { .containsOnly( entry( "10", "value" ) ); } + @ProcessorTest + @IssueKey("3861") + @WithClasses({ + MapToBeanNonStringMapUsedInExpressionMapper.class + }) + void shouldNotWarnIfMapParameterIsUsedInExpression() { + Map values = new HashMap<>(); + values.put( 1, 10 ); + values.put( 2, 20 ); + + MapToBeanNonStringMapUsedInExpressionMapper.Target target = + MapToBeanNonStringMapUsedInExpressionMapper.INSTANCE.toTarget( "test", values ); + + assertThat( target.getValue() ).isEqualTo( "test" ); + assertThat( target.getSum() ).isEqualTo( 30 ); + } + @ProcessorTest @WithClasses(MapToBeanImplicitUnmappedSourcePolicyMapper.class) void shouldNotReportUnmappedSourcePropertiesWithMap() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInExpressionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInExpressionMapper.java new file mode 100644 index 0000000000..53de9d741e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInExpressionMapper.java @@ -0,0 +1,50 @@ +/* + * 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.frommap; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface MapToBeanNonStringMapUsedInExpressionMapper { + + MapToBeanNonStringMapUsedInExpressionMapper INSTANCE = + Mappers.getMapper( MapToBeanNonStringMapUsedInExpressionMapper.class ); + + @Mapping(target = "sum", expression = "java(calculateSum( values ))") + Target toTarget(String value, Map values); + + default int calculateSum(Map values) { + int sum = 0; + for ( Integer value : values.values() ) { + sum += value; + } + return sum; + } + + class Target { + + private final String value; + private final int sum; + + public Target(String value, int sum) { + this.value = value; + this.sum = sum; + } + + public String getValue() { + return value; + } + + public int getSum() { + return sum; + } + } + +} From 8e1d51bf939e3678cdfcb10a3e58d6ceff3cf0d3 Mon Sep 17 00:00:00 2001 From: renechoi Date: Wed, 29 Jul 2026 11:51:01 +0900 Subject: [PATCH 2/2] #3861 Add tests for the defaultExpression and conditionExpression cases --- .../ap/internal/model/BeanMappingMethod.java | 3 + .../ap/test/frommap/FromMapMappingTest.java | 46 +++++++++++++++ ...ingMapUsedInConditionExpressionMapper.java | 49 ++++++++++++++++ ...tringMapUsedInDefaultExpressionMapper.java | 57 +++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInConditionExpressionMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInDefaultExpressionMapper.java 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 ac09b32bc3..7d0f2dd3c8 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 @@ -2113,6 +2113,9 @@ private boolean isReferencedFromJavaExpression(Parameter sourceParameter) { } private boolean isReferencedBy(Pattern reference, String javaExpression) { + // The parameter name is looked up in the raw expression text, so a name that occurs in a string + // literal or as a member name counts as a reference as well. That can only suppress the warning, + // never raise a new one, which is the safe direction for a heuristic hint like this one. return javaExpression != null && reference.matcher( javaExpression ).find(); } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java index 4842e86dbb..32dcb19e22 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/FromMapMappingTest.java @@ -386,6 +386,52 @@ void shouldNotWarnIfMapParameterIsUsedInExpression() { assertThat( target.getSum() ).isEqualTo( 30 ); } + @ProcessorTest + @IssueKey("3861") + @WithClasses({ + MapToBeanNonStringMapUsedInDefaultExpressionMapper.class + }) + void shouldNotWarnIfMapParameterIsUsedInDefaultExpression() { + Map values = new HashMap<>(); + values.put( 1, 10 ); + values.put( 2, 20 ); + + MapToBeanNonStringMapUsedInDefaultExpressionMapper.Target withoutSum = + MapToBeanNonStringMapUsedInDefaultExpressionMapper.INSTANCE + .toTarget( new MapToBeanNonStringMapUsedInDefaultExpressionMapper.Source( null ), values ); + + assertThat( withoutSum.getSum() ).isEqualTo( 30 ); + + MapToBeanNonStringMapUsedInDefaultExpressionMapper.Target withSum = + MapToBeanNonStringMapUsedInDefaultExpressionMapper.INSTANCE + .toTarget( new MapToBeanNonStringMapUsedInDefaultExpressionMapper.Source( 5 ), values ); + + assertThat( withSum.getSum() ).isEqualTo( 5 ); + } + + @ProcessorTest + @IssueKey("3861") + @WithClasses({ + MapToBeanNonStringMapUsedInConditionExpressionMapper.class + }) + void shouldNotWarnIfMapParameterIsUsedInConditionExpression() { + MapToBeanNonStringMapUsedInConditionExpressionMapper.Source source = + new MapToBeanNonStringMapUsedInConditionExpressionMapper.Source( 5 ); + + MapToBeanNonStringMapUsedInConditionExpressionMapper.Target notMapped = + MapToBeanNonStringMapUsedInConditionExpressionMapper.INSTANCE.toTarget( source, new HashMap<>() ); + + assertThat( notMapped.getSum() ).isNull(); + + Map values = new HashMap<>(); + values.put( 1, 10 ); + + MapToBeanNonStringMapUsedInConditionExpressionMapper.Target mapped = + MapToBeanNonStringMapUsedInConditionExpressionMapper.INSTANCE.toTarget( source, values ); + + assertThat( mapped.getSum() ).isEqualTo( 5 ); + } + @ProcessorTest @WithClasses(MapToBeanImplicitUnmappedSourcePolicyMapper.class) void shouldNotReportUnmappedSourcePropertiesWithMap() { diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInConditionExpressionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInConditionExpressionMapper.java new file mode 100644 index 0000000000..5fe97e3009 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInConditionExpressionMapper.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.frommap; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface MapToBeanNonStringMapUsedInConditionExpressionMapper { + + MapToBeanNonStringMapUsedInConditionExpressionMapper INSTANCE = + Mappers.getMapper( MapToBeanNonStringMapUsedInConditionExpressionMapper.class ); + + @Mapping(target = "sum", source = "source.sum", conditionExpression = "java(!values.isEmpty())") + Target toTarget(Source source, Map values); + + class Source { + + private final Integer sum; + + public Source(Integer sum) { + this.sum = sum; + } + + public Integer getSum() { + return sum; + } + } + + class Target { + + private Integer sum; + + public Integer getSum() { + return sum; + } + + public void setSum(Integer sum) { + this.sum = sum; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInDefaultExpressionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInDefaultExpressionMapper.java new file mode 100644 index 0000000000..5cfa305097 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/frommap/MapToBeanNonStringMapUsedInDefaultExpressionMapper.java @@ -0,0 +1,57 @@ +/* + * 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.frommap; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface MapToBeanNonStringMapUsedInDefaultExpressionMapper { + + MapToBeanNonStringMapUsedInDefaultExpressionMapper INSTANCE = + Mappers.getMapper( MapToBeanNonStringMapUsedInDefaultExpressionMapper.class ); + + @Mapping(target = "sum", source = "source.sum", defaultExpression = "java(calculateSum( values ))") + Target toTarget(Source source, Map values); + + default int calculateSum(Map values) { + int sum = 0; + for ( Integer value : values.values() ) { + sum += value; + } + return sum; + } + + class Source { + + private final Integer sum; + + public Source(Integer sum) { + this.sum = sum; + } + + public Integer getSum() { + return sum; + } + } + + class Target { + + private Integer sum; + + public Integer getSum() { + return sum; + } + + public void setSum(Integer sum) { + this.sum = sum; + } + } + +}