Skip to content

#3861 Do not warn about unused Map source parameter used in an expression - #4102

Open
renechoi wants to merge 2 commits into
mapstruct:mainfrom
renechoi:fix/issue-3861-map-parameter-used-in-expression
Open

#3861 Do not warn about unused Map source parameter used in an expression#4102
renechoi wants to merge 2 commits into
mapstruct:mainfrom
renechoi:fix/issue-3861-map-parameter-used-in-expression

Conversation

@renechoi

Copy link
Copy Markdown

A Map source parameter that is only referenced from a Java expression is currently reported as unused.

BeanMappingMethod#reportErrorForUnusedSourceParameters warns for every unprocessed Map source parameter whose key type is not String, on the assumption that the user intended a bean-from-map mapping and got the type wrong. A parameter that is passed to a method from @Mapping(expression = "java(...)") never becomes a processed source parameter, so it hits that branch even though it is used.

For the mapper from the issue:

@Mapper
public interface MyMapper {

    @Mapping(target = "sum", expression = "java(calculateSum( values ))")
    Target map(String foo, Map<Integer, Integer> values);

    default int calculateSum(Map<Integer, Integer> values) {
        ...
    }
}

MapStruct emits:

warning: The Map parameter "values" cannot be used for property mapping.
It must be typed with Map<String, ???> but it was typed with Map<Integer,Integer>.

The generated code is correct; only the warning is wrong.

Change

Before emitting the warning, check whether the parameter name appears as an identifier in any of the method's Java expressions (expression, defaultExpression, conditionExpression). If it does, the parameter is in use and the warning is skipped. Everything else is untouched: the parameter stays in unprocessedSourceParameters, so property mapping, unmapped-source reporting and parameter-name based mapping behave exactly as before.

The check is intentionally a word-boundary match on the raw expression text rather than a parse. It can only ever suppress a warning, never introduce one.

Verification

Run from the repository root with JDK 21.

  • Added MapToBeanNonStringMapUsedInExpressionMapper plus FromMapMappingTest#shouldNotWarnIfMapParameterIsUsedInExpression. @ProcessorTest fails on any unexpected diagnostic, so the new test reproduces the issue.
  • On unmodified main (with only the test added) that test fails for both the javac and the eclipse compiler with exactly the warning quoted above: Tests run: 48, Failures: 2.
  • With the change: ./mvnw -pl processor test -Dtest=FromMapMappingTest -> Tests run: 48, Failures: 0, Errors: 0.
  • Full processor module suite: ./mvnw -pl processor test -> Tests run: 3645, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS.

Fixes #3861

@kdelay kdelay left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unprocessedSourceParameters analysis matches what I see in reportErrorForUnusedSourceParameters, and skipping the warning rather than removing the parameter from that set looks like the right minimal move.

One gap while checking the change: of the three expression kinds isReferencedFromJavaExpression guards, only getJavaExpression() is covered by a test. I removed the getDefaultJavaExpression() and getConditionJavaExpression() clauses (keeping the first) and ran the whole module:

./mvnw -pl processor -am test
Tests run: 3645, Failures: 0, Errors: 0, Skipped: 0

So neither clause currently has any regression protection. They are not dead code though. With those two clauses removed, both of these mappers emit MAPTOBEANMAPPING_WRONG_KEY_TYPE for values on the JDK and the Eclipse compiler, and with your branch as-is they compile clean:

@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<Integer, Integer> values);

    default int calculateSum(Map<Integer, Integer> values) {
        int sum = 0;
        for ( Integer value : values.values() ) {
            sum += value;
        }
        return sum;
    }

    // Source and Target are plain beans with a nullable Integer sum
}
@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<Integer, Integer> values);

    // Source and Target are plain beans with a nullable Integer sum
}

And the two test methods, dropped in next to shouldNotWarnIfMapParameterIsUsedInExpression:

    @ProcessorTest
    @IssueKey("3861")
    @WithClasses({
        MapToBeanNonStringMapUsedInDefaultExpressionMapper.class
    })
    void shouldNotWarnIfMapParameterIsUsedInDefaultExpression() {
        Map<Integer, Integer> values = new HashMap<>();
        values.put( 1, 10 );
        values.put( 2, 20 );

        MapToBeanNonStringMapUsedInDefaultExpressionMapper.Target target =
            MapToBeanNonStringMapUsedInDefaultExpressionMapper.INSTANCE
                .toTarget( new MapToBeanNonStringMapUsedInDefaultExpressionMapper.Source(), values );

        assertThat( target.getSum() ).isEqualTo( 30 );
    }

    @ProcessorTest
    @IssueKey("3861")
    @WithClasses({
        MapToBeanNonStringMapUsedInConditionExpressionMapper.class
    })
    void shouldNotWarnIfMapParameterIsUsedInConditionExpression() {
        MapToBeanNonStringMapUsedInConditionExpressionMapper.Source source =
            new MapToBeanNonStringMapUsedInConditionExpressionMapper.Source();
        source.setSum( 5 );

        MapToBeanNonStringMapUsedInConditionExpressionMapper.Target target =
            MapToBeanNonStringMapUsedInConditionExpressionMapper.INSTANCE
                .toTarget( source, new HashMap<>() );

        assertThat( target.getSum() ).isNull();
    }

Measured on FromMapMappingTest: 52 run / 0 failures on your branch, 52 run / 4 failures with the two clauses removed (both compilers, both tests). JDK 26 locally. Take them as-is if useful.

One thing I am unsure about rather than objecting to: the match is on raw expression text, so a parameter named values also counts as referenced in java(load("values")) or java(other.values). You call the trade-off out in the description, and the direction is safe, but would it be worth a short code comment at isReferencedBy so it does not read as an oversight later?

@renechoi

Copy link
Copy Markdown
Author

Thanks, the gap is real and I reproduced it before touching anything: with both extra clauses dropped and no new tests, ./mvnw -pl processor -am test is still 3645 run / 0 failures, so neither defaultExpression nor conditionExpression had any regression protection.

Added both cases in 8e1d51b. I wrote my own fixtures rather than taking yours as-is, so that each test also pins what the expression does instead of only that it compiles:

  • shouldNotWarnIfMapParameterIsUsedInDefaultExpression asserts 30 when source.sum is null (default expression taken) and 5 when it is not (default expression skipped).
  • shouldNotWarnIfMapParameterIsUsedInConditionExpression asserts null for an empty map and 5 for a non-empty one.

Negative control per clause, FromMapMappingTest on JDK 21:

processor state result
branch as-is 52 run / 0 failures
getDefaultJavaExpression() clause removed 52 run / 2 failures, both ...UsedInDefaultExpression (javac + Eclipse)
getConditionJavaExpression() clause removed 52 run / 2 failures, both ...UsedInConditionExpression (javac + Eclipse)

Each failure is the warning from the issue: The Map parameter "values" cannot be used for property mapping. It must be typed with Map<String, ???> but it was typed with Map<Integer,Integer>.

Full module on the new head is 3649 run / 0 failures, and ./mvnw -pl processor -am verify -DskipTests (license, checkstyle, forbiddenapis, japicmp) is clean.

On the raw text match: you are right that java(load( "values" )) counts as a reference. I added a comment at isReferencedBy saying so and why that direction is the safe one, namely that the check can only suppress the warning and never raise one that was not there before. Resolving the parameter properly would mean parsing the expression, which seems heavy for a hint, but I am happy to go that way if a maintainer prefers it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Do not emit warning for unused source parameters of Map type

2 participants