Leonard Brünings created GROOVY-12045:
-----------------------------------------
Summary: Calling an enclosing-class instance method on a static
nested class instance throws IllegalArgumentException instead of
MissingMethodException
Key: GROOVY-12045
URL: https://issues.apache.org/jira/browse/GROOVY-12045
Project: Groovy
Issue Type: Bug
Components: class generator, groovy-runtime
Affects Versions: 6.0.0-alpha-1
Reporter: Leonard Brünings
This was discovered in the Spock Groovy 6 [integration
branch|https://github.com/spockframework/spock/pull/2356].
The analysis was done by Claude, but it seems reasonable to me.
I've verified that the reproducers work.
h2. Summary
Invoking an enclosing-class **instance** method on an instance of a **static
nested class**
throws `IllegalArgumentException: object is not an instance of declaring class`
instead of the
expected `MissingMethodException`. Regression introduced by the outer-method
resolution added in
GROOVY-11823 / GROOVY-11858 (the new `getNonClosureOuter` helper, `@since
6.0.0`).
h2. Environment
Groovy 6.0.0-alpha-1 and current master (`f6e2248d12`), JDK 17.
h2. Description
When a method is not found on a static nested class, Groovy now tries to
resolve it on the
*enclosing* class. For a static nested class there is no enclosing
**instance**, so the runtime
falls back to the enclosing **Class** object and then tries to invoke the
(instance) method on
that Class — which fails with a raw `IllegalArgumentException` from reflection
rather than a
clean `MissingMethodException`.
This also breaks delegate-based dispatch: a closure with {{resolveStrategy =
DELEGATE_FIRST}}
whose delegate is a static nested class no longer falls through to the owner,
because the
exception thrown while probing the delegate is not a {{MissingMethodException}}.
h3. Steps to reproduce
{code:groovy}
class Outer {
void foo() { println "foo() on ${this.class.simpleName}" }
static class StaticInner {}
void run() {
// (1) direct call of an outer *instance* method on a static-nested-class
instance
println "--- direct ---"
try { new StaticInner().foo() } catch (Throwable t) { println "
${t.class.simpleName}: ${t.message}" }
// (2) same thing via a DELEGATE_FIRST closure (delegate = static nested
class, owner = Outer)
println "--- closure DELEGATE_FIRST ---"
Closure c = { foo() }
c.delegate = new StaticInner()
c.resolveStrategy = Closure.DELEGATE_FIRST
try { c() } catch (Throwable t) { println " ${t.class.simpleName}:
${t.message}" }
}
}
new Outer().run()
{code}
h3. Expected (Groovy 5.0.x)
{noformat}
--- direct ---
MissingMethodException: No signature of method: foo for class:
Outer$StaticInner ...
--- closure DELEGATE_FIRST ---
foo() on Outer
{noformat}
The static nested class has no {{foo}}, so the direct call misses cleanly, and
the
DELEGATE_FIRST closure falls through from the delegate to the owner ({{Outer}}).
### Actual (Groovy 6.0.0-alpha-1 and master)
{noformat}
--- direct ---
IllegalArgumentException: object is not an instance of declaring class
--- closure DELEGATE_FIRST ---
IllegalArgumentException: object is not an instance of declaring class
{noformat}
h2. Root cause
In {{groovy.lang.MetaClassImpl}} (master `f6e2248d12`):
* {{invokeOuterMethod(...)}} (line ~1328) resolves a not-found method against
the enclosing class
and invokes it via {{omc.invokeMethod(outerClass, target, methodName, ...)}}
where
{{target = getOuterReference(sender, object)}}.
* {{getOuterReference(Class innerClass, Object object)}} (line ~1344): for a
**static** nested
class the {{this$0}} branch is skipped (line ~1347 guards on {{(modifiers &
ACC_STATIC) == 0}}),
so {{outer}} stays {{null}} and it falls back to
{{outer = getNonClosureOuter(innerClass)}} (line ~1361), which returns the
enclosing
**`Class`** object (line ~1369, {{@since 6.0.0}}).
* {{invokeOuterMethod}} then invokes the enclosing **instance** method with the
`Class` object as
the receiver, so reflection throws
{{IllegalArgumentException: object is not an instance of declaring class}}.
A static nested class has no enclosing instance, so an enclosing **instance**
method is simply not
applicable and should yield a {{MissingMethodException}} (allowing
DELEGATE_FIRST/owner fallback to
proceed). Only enclosing **static** methods are legitimately callable via the
`Class` target.
h2. Impact
Breaks frameworks that rely on closure delegate/owner fallback. Spock's
{{with}} / {{verifyAll}}
blocks dispatch implicit-`this` method conditions on the block closure
(DELEGATE_FIRST). When the
{{with}} target is a static nested class and the condition calls a method
declared on the spec
(the owner), Groovy 6 throws instead of resolving the owner method.
Spock reproducer: `org.spockframework.smoke.WithBlocks."with works with void
methods"`.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)