#3997 Fix InheritInverseConfiguration ignore for nested sources with parameter prefix - #4091
Conversation
…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
left a comment
There was a problem hiding this comment.
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
containsKeybranch 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>
|
@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). |
Summary
Fixes #3997
When a forward mapping uses a source path that includes the source parameter name (e.g.
source.client.id),@InheritInverseConfigurationpreviously 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
copyForInverseInheritanceswapped source/target naively, sosource = "source.client.id"became inversetarget = "source.client.id".TargetReference, so nested mapping still worked.source), not the property (client).Fix
When building the inverse target name, strip the source parameter name prefix (e.g.
source.client.id→client.id), mirroringSourceReference/TargetReferenceresolution. If the first segment is also a property of the source type, the full path is kept (property path takes precedence).Tests
ignore = true→ nested properties stay nullChecklist
NEXT_RELEASE_CHANGELOG.md