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)

Reply via email to