G
YouTube TikTok Games
Skip to content

GROOVY-12757: STC: a lambda result naming a type parameter of the calling method takes the receiver's type argument for that name - #3290

Open
codeconsole wants to merge 1 commit into
apache:masterfrom
codeconsole:stc-receiver-context-before-arguments
Open

codeconsole wants to merge 1 commit into
apache:masterfrom
codeconsole:stc-receiver-context-before-arguments

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

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:

static <T> Optional<List<T>> wrap(Optional<String> value, T item) {
    Optional<List<T>> result = value.map((String s) -> [item] as List<T>)
    result
}
[Static type checking] - Incompatible generic argument types. Cannot assign java.util.Optional<java.util.List<java.lang.String>> to: java.util.Optional<java.util.List<T>>

map's U is inferred from the lambda as List<T>, with the calling method's T, and then Optional's own T, here String, 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: AwaitablePromiseFactory calls groovy.concurrent.Awaitable#then that way.

inferReturnTypeGenerics resolved 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. extractPlaceHoldersVisibleToDeclaration already leaves out the method's own type parameters (GROOVY-10322), so a method type parameter that shadows the class's is unaffected.

GenericsJavaCompatibilityTest gets a case checked against javac and GenericsSTCTest, and so GenericsStaticCompileTest, the Groovy form. Both fail without the change.

Please also apply this to GROOVY_6_0_X, where it cherry-picks cleanly.

…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-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.4429%. Comparing base (856b28a) to head (491ceeb).

Additional details and impacted files

Impacted file tree graph

@@              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     
Files with missing lines Coverage Δ
...roovy/transform/stc/StaticTypeCheckingVisitor.java 87.9704% <100.0000%> (ø)

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@testlens-app

testlens-app Bot commented Nov 10, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 491ceeb
▶️ Tests: 131375 executed
⚪️ Checks: 26/26 completed


Learn more about TestLens at testlens.app/docs.

@daniellansun daniellansun left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

This branch has not been deployed

No deployments
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.

3 participants