Repository navigation
GROOVY-12757: STC: a lambda result naming a type parameter of the calling method takes the receiver's type argument for that name - #3290
Conversation
…ling method takes the receiver's type argument for that name
inferReturnTypeGenerics resolved the method's type parameters from the
arguments and then applied the receiver's type arguments to the result.
A type the arguments supply can name a type parameter of the calling
method, and when the receiver's class has one of the same name, the
receiver's type argument replaced it:
static <T> Optional<List<T>> wrap(Optional<String> value, T item) {
Optional<List<T>> result = value.map((String s) -> [item] as List<T>)
result
}
was inferred as Optional<List<String>>, which the assignment check has
rejected since GROOVY-12714. The receiver's type arguments are now applied
to the declared return type first, before the method's type parameters
are resolved.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3290 +/- ##
=============================================
Coverage 74.4429% 74.4429%
Complexity 44774 44774
=============================================
Files 1702 1702
Lines 151891 151891
Branches 29091 29091
=============================================
Hits 113072 113072
+ Misses 28413 28411 -2
- Partials 10406 10408 +2
🚀 New features to boost your workflow:
|
✅ All tests passed ✅🏷️ Commit: 491ceeb Learn more about TestLens at testlens.app/docs. |
daniellansun
left a comment
There was a problem hiding this comment.
The reorder is the right one. The receiver's type arguments and the method's type parameters are different variables even when they share a name (JLS 18.5.2). Applying the receiver context to the declared return type first, and not again after the arguments have been substituted, stops Optional's T from replacing the caller's T. extractPlaceHoldersVisibleToDeclaration already drops the method's own type parameters (GROOVY-10322), so a method type parameter that shadows the class's is left alone.
I compared inferred types on the parent and on this tip. These were wrong and are now right:
static <T> Optional<List<T>> wrap(Optional<String> value, T item) {
def result = value.map((String s) -> [item] as List<T>) // Optional<List<T>>, was Optional<List<String>>
result
}
static <T> Optional<T> head(Optional<List<T>> value) {
Optional<T> result = value.map { list -> list.get(0) } // Optional<T>, was Optional<Optional<T>>
result
}The same correction shows up for a closure, for value.<List<T>>map(...), for Holder<String>.inst { s -> [item] as List<T> }, for thenApply, and for Box<List<T>>.map { list -> list.get(0) } (List<T>, was List<List<T>>). javac 21 accepts the Java forms. GenericsJavaCompatibilityTest compiles them with this JDK's javac and the static-Groovy translation, and the results match. JDK 25 is not what I ran.
The old head / Box.map form compiled even when the inferred type was wrong. checkTypeGenerics skips a right-hand side that still contains a placeholder, so only the case that had already been rewritten to String or List<Object> failed the assignment. The tests in the PR lock that case. Please also lock the unresolved one above. It failed open, not closed.
I did not see a change for anything that should not have changed: a raw receiver stays List<Object> and Map<Object, Integer>, a Box<?> stays List<?>, a static method stays List<T>, Box<String>.id(1) is still Integer when the method's T shadows the class's, Box<CharSequence>.bound('x') is still String, and Outer<String>.Inner<Integer>.make(item) is still Map<String, Map<Integer, T>>. GenericsSTCTest, GenericsStaticCompileTest and GenericsJavaCompatibilityTest pass. Both new tests fail without the change, on master and on GROOVY_6_0_X.
No COMPATIBILITY.md entry. The bad inference was not in a release, and the type now matches javac. The cherry-pick onto GROOVY_6_0_X is clean. With this and #3289 applied together, those suites pass there (1107 tests).
https://issues.apache.org/jira/browse/GROOVY-12757
When a generic method of a generic receiver is called with a lambda whose result names a type parameter of the calling method, and the receiver's class has a type parameter of the same name, static type checking replaced the caller's type parameter with the receiver's type argument:
map'sUis inferred from the lambda asList<T>, with the calling method'sT, and thenOptional's ownT, hereString, was applied to the result. Groovy before GROOVY-12714 inferred the same wrong type, but its assignment check accepted it. javac accepts the equivalent Java. Grails no longer compiles against GROOVY_6_0_X because of this:AwaitablePromiseFactorycallsgroovy.concurrent.Awaitable#thenthat way.inferReturnTypeGenericsresolved the method's type parameters from the arguments first, and then applied the receiver's type arguments to the result. It now applies the receiver's type arguments to the declared return type first, which keeps them away from the types the arguments supply.extractPlaceHoldersVisibleToDeclarationalready leaves out the method's own type parameters (GROOVY-10322), so a method type parameter that shadows the class's is unaffected.GenericsJavaCompatibilityTestgets a case checked against javac andGenericsSTCTest, and soGenericsStaticCompileTest, the Groovy form. Both fail without the change.Please also apply this to GROOVY_6_0_X, where it cherry-picks cleanly.