prevent mapper generation from type with generic super bound to type with generic extends bound#3994
Conversation
…with generic extends bound
There was a problem hiding this comment.
Pull request overview
Adjusts MapStruct’s container (Iterable/Stream) forging to avoid generating uncompilable code when mapping from ? super T sources to ? extends T targets, and adds regression tests around wildcard-bound assignments.
Changes:
- Add
Type.containsSuperBound()/Type.containsExtendsBound()helpers to detect wildcard bounds in type parameters. - Short-circuit iterable/stream element-mapping forging for
source: ? super→target: ? extendsto triggerPROPERTYMAPPING_MAPPING_NOT_FOUNDinstead of generating invalid code. - Add new wildcard-focused tests covering the rejected
super -> extendsscenarios (Collection + Stream) and the allowedextends -> superassignment (Collection).
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java | Adds helpers for detecting super/extends wildcard bounds in type parameters. |
| processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java | Prevents forging iterable/stream element mappings for super-bounded sources to extends-bounded targets by returning null. |
| processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/DistinguishBetweenSuperAndExtendTest.java | Adds compilation-failure assertions for super -> extends and a runtime assertion for extends -> super (Collection). |
| processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/ErroneousCollectionSuperToExtendMapper.java | Mapper used to validate compilation error for Collection super -> extends. |
| processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/ErroneousStreamSuperToExtendMapper.java | Mapper used to validate compilation error for Stream super -> extends. |
| processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionExtendToSuperMapper.java | Mapper used to validate extends -> super works for Collection properties. |
| processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionExtendTypes.java | Test bean with Collection<? extends SimpleObject> property. |
| processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/CollectionSuperTypes.java | Test bean with Collection<? super SimpleObject> property. |
| processor/src/test/java/org/mapstruct/ap/test/generics/wildcard/SimpleObject.java | Minimal element type for wildcard-bound container tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java
Outdated
Show resolved
Hide resolved
processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java
Outdated
Show resolved
Hide resolved
processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java
Outdated
Show resolved
Hide resolved
.../test/java/org/mapstruct/ap/test/generics/wildcard/DistinguishBetweenSuperAndExtendTest.java
Outdated
Show resolved
Hide resolved
.../test/java/org/mapstruct/ap/test/generics/wildcard/DistinguishBetweenSuperAndExtendTest.java
Outdated
Show resolved
Hide resolved
filiphr
left a comment
There was a problem hiding this comment.
Thanks for the fix @hduelme. As you can see from the previous review from Copilot, I've been exploring the agents a bit for helping with the reviews 😄.
I did a pass through Claude Code and I think that it found some interesting things that we should address:
Should fix
These are a bit style related and what Claude Code suggests based on our code base.
1. Typo in test method names + naming convention
unabaleToMapSuperToExtendforCollection and unabaleToMapSuperToExtendforStream have a typo ("unabale" → "unable") and don't follow the project's should* naming convention used in neighboring test classes (e.g. WildCardTest). Suggested names:
shouldFailOnSuperToExtendMappingForCollectionshouldFailOnSuperToExtendMappingForStreamallowAssignmentOfExtendBoundToSuperBound→shouldMapExtendBoundToSuperBound
2. Import ordering in DistinguishBetweenSuperAndExtendTest.java
java.util.ArrayList and java.util.List appear after the org.mapstruct.* imports. Project convention places java.* imports first, then javax.*, then org.*, then static imports.
3. Missing line and type in @Diagnostic annotations
The overwhelming convention in this project is to include line and type in @Diagnostic (293 occurrences across 75 files include type). Adding them makes the tests more precise — they verify the error is reported on the correct mapper and line, preventing false passes. See WildCardTest.java in the same test area for examples.
Worth discussing
These are quite interesting, as I didn't even think about them. However, I think that they are correct and we should address them. What do you think?
4. forgeMapMapping is not guarded
The fix guards forgeWithElementMapping() (called by forgeIterableMapping and forgeStreamMapping), but forgeMapMapping() is a completely separate code path that does not go through forgeWithElementMapping. Would a Map<? super K, ? super V> → Map<? extends K, ? extends V> mapping hit the same class of invalid code generation? If so, the same guard should be added there. If not (or if it's out of scope), it might be worth adding a comment or a follow-up issue.
5. containsSuperBound() / containsExtendsBound() semantics with multi-type-parameter types
These methods use anyMatch across all type parameters. For single-type-parameter containers (Collection, Stream) this works perfectly. But for a type like Map<? super K, ? extends V>, both methods would return true, which could incorrectly block a valid mapping if the guard were ever applied to Map types. This is fine today, but worth keeping in mind — a Javadoc note clarifying the intended usage scope would help prevent misuse.
Minor / optional
- Pre-existing Javadoc bug:
hasExtendsBound()atType.java:477— the@returnsays "wild card super bound" but means "extends bound" (copy-paste fromhasSuperBound()). Not introduced by this PR, but could be fixed while you're in the area. - Structural inconsistency:
ErroneousStreamSuperToExtendMapperuses inner classes for source/target types whileErroneousCollectionSuperToExtendMapperuses separate top-level classes. Using the same pattern for both would improve consistency. - Stream extends→super test gap: The PR description explains this is omitted due to a separate existing bug. A
@Disabledtest or code comment would help track this so it doesn't get forgotten.
What's good
- The fix is surgical and minimal — 3 lines in the right place
- The null return correctly propagates through the existing error reporting path (verified by tracing callers)
- The positive test verifies runtime behavior, not just compilation
- Error messages in
@ExpectedCompilationOutcomeare specific and fully qualified
|
@filiphr I addressed points 1 to 3 from your review, as well as the old Javadoc bug.
I discarded Copilot’s suggestion to simply ignore all super targets, although at the moment that would at least prevent the invalid code generation. The tests added with this PR are valid and can serve as guidance for future improvements. My solution tries to block some of the invalid code generation, but there are still many additional cases. The proper long-term solution would be full generic bound support, since currently the bounds are largely ignored. |
|
Thanks for addressing all the comments @hduelme.
I'm not sure that I am following your explanation here. You are talking about This is for "
That's completely fine. I've only wanted to test it out, to see what it would generate.
That's great and extremely appreciated.
I wouldn't say that they are largely ignored, we do have a lot of quirks around generics. Especially around method reusing etc. |
|
@filiphr Sorry my explanation was not really clear.
First of all the point highlighted by Claude Code are correct. In 4. the behavior how mapstruct handles
5.when mapping So yes, the same problem plus some new problems are in The 5. point is although correct, in my implementation the position is ignored, that could cause problems in the future.
Totally right, Many things are already working, I was referring to the current dropping of bounds by calling While investigating, I found, that my implementation is although missing a valide mapping, A super bounded type can be mapped to a I think the better solution for this issue would be replacing the sourceType |
filiphr
left a comment
There was a problem hiding this comment.
Thanks for the clarification @hduelme. Everything makes sense. I didn't realize hat we actually already handle the map properly.
I think the better solution for this issue would be replacing the sourceType
? super Twithjava.lang,Objectif source is not assignable to targetType.
That's indeed a good one. Everything looks good now.
Currently, MapStruct generates code that attempts to implicitly cast a
Collection<? super Type>toTypeinside theforloop. This is not valid, since the elements of the collection are not guaranteed to be instances of Type, which results in a compilation error.The same issue applies to Stream.
I changed the behavior so that mappings from
Collection<? super Type>toCollection<? extends Type>and fromStream<? super Type>toStream<? extends Type>now result inPROPERTYMAPPING_MAPPING_NOT_FOUND.Additionally, I added a test to verify that mapping from
Collection<? extends Type>toCollection<? super Type>works correctly. I did not add a corresponding test for Stream, since it currently generates invalid code (it tries to directly assignCollection<? extends Type>toCollection<Type>).I am unsure what the correct behavior should be in this case, Currently, MapStruct generates code that attempts to implicitly cast a
Collection<? super Type>toTypeinside theforloop. This is not valid, since the elements of the collection are not guaranteed to be instances of Type, which results in a compilation error.The same issue applies to Stream.
I changed the behavior so that mappings from
Collection<? super Type>toCollection<? extends Type>and fromStream<? super Type>toStream<? extends Type>now result inPROPERTYMAPPING_MAPPING_NOT_FOUND.Additionally, I added a test to verify that mapping from
Collection<? extends Type>toCollection<? super Type>works correctly. I did not add a corresponding test for Stream, since it currently generates invalid code (it tries to directly assignCollection<? extends Type>toCollection<Type>).I am unsure what the correct behavior should be in this case, as MapStruct currently doesn't fully supports type bonds: