Skip to content

#3997 Fix InheritInverseConfiguration ignore for nested sources with parameter prefix - #4091

Open
kamilkrzywanski wants to merge 2 commits into
mapstruct:mainfrom
kamilkrzywanski:issue-3997-inherit-inverse-ignore-parameter-prefix
Open

#3997 Fix InheritInverseConfiguration ignore for nested sources with parameter prefix#4091
kamilkrzywanski wants to merge 2 commits into
mapstruct:mainfrom
kamilkrzywanski:issue-3997-inherit-inverse-ignore-parameter-prefix

Conversation

@kamilkrzywanski

Copy link
Copy Markdown

Summary

Fixes #3997

When a forward mapping uses a source path that includes the source parameter name (e.g. source.client.id), @InheritInverseConfiguration previously kept that prefix in the inverse target name (source.client.id). As a result, @Mapping(target = "client", ignore = true) on the inverse method did not match the nested inherited mapping, and MapStruct still created intermediate objects for ignored properties.

Root cause

  1. copyForInverseInheritance swapped source/target naively, so source = "source.client.id" became inverse target = "source.client.id".
  2. Code generation later strips the parameter prefix via TargetReference, so nested mapping still worked.
  3. Ignore / redefinition matching runs earlier on the raw target name, where the first segment is the parameter name (source), not the property (client).

Fix

When building the inverse target name, strip the source parameter name prefix (e.g. source.client.idclient.id), mirroring SourceReference / TargetReference resolution. If the first segment is also a property of the source type, the full path is kept (property path takes precedence).

Tests

  • Reproducer from the issue: nested sources with parameter prefix + inverse ignore = true → nested properties stay null
  • Guard: same nested sources without ignores still reverse-map correctly

Checklist

  • Provide test cases
  • One pull request per feature/bug
  • Meaningful history (single focused commit)
  • Changelog entry in NEXT_RELEASE_CHANGELOG.md

…ces with parameter prefix

When a forward mapping uses a source path that includes the source parameter
name (e.g. source.client.id), inverse inheritance previously kept that prefix
in the inverse target name. Ignore and redefinition matching then failed
because the first path segment was the parameter name rather than the property.

Strip the source parameter name prefix when building the inverse target name,
mirroring SourceReference / TargetReference resolution.

@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.

I ran this locally on 75ad6b9 against 211b2be (JDK 21.0.4, both javac and ecj through @ProcessorTest). The diagnosis and the fix hold for the reported shape. One thing about the guard clause looks worth resolving before it lands.

Verification

Control first, to check the test actually defends the change: the four new test files on top of main, with MappingOptions.java reverted to main and nothing else changed.

Issue3997Test.inverseIgnoreShouldRespectNestedSourcesWithParameterPrefix:35
expected: null
 but was: org.mapstruct.ap.test.bugs._3997.Issue3997Mapper$Client@58168099
Tests run: 4, Failures: 2, Errors: 0, Skipped: 0

Two failures are the same assertion under the two compilers. On the PR head: Tests run: 4, Failures: 0. Full processor module on the PR head: Tests run: 3638, Failures: 0, Errors: 0, Skipped: 0.

I also checked that first( templateMethod.getSourceParameters() ) cannot fail here: SourceMethod#inverses (SourceMethod.java:352) requires exactly one source parameter on both methods, so the template always has one.

The guard uses a different rule than the two resolvers it mirrors

stripSourceParameterPrefix keeps the full path when the first segment is a read accessor of the source type. Neither resolver decides it that way.

  • SourceReference.BuilderFromMapping#buildFromSingleSourceParameters (SourceReference.java:152-188) matches the whole path against the source accessors first and strips the parameter name only when the entire path fails to resolve. A first segment that happens to be a property is not enough to keep the path.
  • TargetReference (TargetReference.java:205-206) strips when the first segment is not among the target properties and matches the parameter name.

So when the parameter name collides with a property that does not carry the rest of the path, the forward mapping resolves through the strip path while this PR keeps the prefix, and the fix does not apply. Reproducer: parameter named client, Order has both getClient() and getId(), Client has no id.

@Mapper
public interface M {
    @Mapping(target = "dtoId", source = "client.id")
    OrderDto toDto(Order client);

    @InheritInverseConfiguration
    @Mapping(target = "client", ignore = true)
    Order toEntity(OrderDto dto);
}

The forward side resolves by stripping, so the inverse target should be id:

-- MapStruct: mapping property: client.getId() to: setDtoId(java.lang.Long).

Compiled with the processor built from this branch:

warning: Unmapped target property: "id".

    public Probe.Order toEntity(Probe.OrderDto dto) {
        if ( dto == null ) {
            return null;
        }

        Probe.Order order = new Probe.Order();

        return order;
    }

dtoId -> id is dropped: the inverse target name stays client.id, and filterNestedTargetIgnores then removes it under @Mapping(target = "client", ignore = true). That is the same failure #3997 reports.

Two controls around it. The generated file is identical (apart from the date attribute) when compiled with a processor built from main, so this is not a regression introduced here, it is a case the fix declines to cover. And renaming the parameter to order (source = "order.id", everything else unchanged) makes both main and this branch emit order.setId( dto.getDtoId() ), which isolates the guard as the deciding factor rather than anything else in the mapper.

Direction rather than a patch: have the guard ask the question buildFromSingleSourceParameters asks, i.e. whether the full path resolves against the source type's accessors, and strip only when it does not. That would also make the javadoc claim on the new method true of the code that runs.

Smaller notes:

  • The containsKey branch is untested. Whichever rule you settle on, a mapper whose parameter name collides with a source property would pin it.
  • The javadoc says the method mirrors SourceReference / TargetReference; going by the two line references above it currently matches neither.
  • Agreed on the #4078 interaction noted in the description, nothing to add there.

When building the inverse target name, try the full source path as
property accessors first and strip the parameter name only if that path
does not resolve. Fixes the case where the parameter name collides with
a property but the rest of the path is not on that property.

Signed-off-by: Kamil Krzywanski <kamilkrzywanski01@gmail.com>
@kamilkrzywanski

Copy link
Copy Markdown
Author

@kdelay thanks for the detailed review.

Updated the guard to match SourceReference: try the full path as property accessors first, strip the parameter name only when that fails. Added tests for both the strip and keep cases (including the client/id collision you described).

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.

@InheritInverseConfiguration ignores "ignore = true" for nested source fields

2 participants