[ 
https://issues.apache.org/jira/browse/GROOVY-12041?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18083467#comment-18083467
 ] 

Paul King commented on GROOVY-12041:
------------------------------------

h2. Root cause

Commit [1ea6927ad9|https://github.com/apache/groovy/commit/1ea6927ad9] 
(2023-08-16, Eric Milles) — _GROOVY-7024, GROOVY-10985: self property 
precedence_ — broadened the GROOVY-7996 dynamic-lookup heuristic in 
{{StaticTypeCheckingVisitor.existsProperty}} (current source ~lines 2306–2328):

# Removed the closure-only gate ({{typeCheckingContext.getEnclosingClosure() != 
null}}) — the heuristic now applies in plain method bodies too, not only inside 
closures.
# Added {{getProperty(String)}} and {{setProperty(String, Object)}} to the MOP 
methods that count as evidence of dynamic resolution. Previously only 
{{get(String)}}, {{set(String, Object)}} and {{propertyMissing}} were checked.

Consequence for an extension-aware compile: when 
{{tryVariableExpressionAsProperty(vexp, "g")}} synthesises {{this.g}} and calls 
{{existsProperty}}, the inherited {{getProperty(String)}} on the superclass is 
now found. {{existsProperty}} returns {{true}}, marks the property with 
{{DYNAMIC_RESOLUTION=true}}, and that metadata is copied onto {{vexp}}. STC 
then treats {{g}} as resolved and *skips* the 
{{handleUnresolvedVariableExpression(vexp)}} / {{handleUnresolvedProperty(pe)}} 
extension path.

h2. Behavioural contract change

{quote}
*Before Groovy 5:* in a non-closure method body, a bare identifier (or 
{{this.foo}}) resolvable only via the receiver's {{getProperty(String)}} was 
_unresolved_ from STC's perspective. {{unresolvedVariable}} / 
{{unresolvedProperty}} fired.

*Since Groovy 5:* STC pre-resolves it as a dynamic property lookup. The 
callbacks do *not* fire. Instead the receiver expression carries 
{{StaticTypesMarker.DYNAMIC_RESOLUTION = true}} metadata.
{quote}

This is the contract change real-world extensions (notably Grails' 
[GroovyPageTypeCheckingExtension|https://github.com/apache/grails-core/blob/grails8-groovy5-sb4/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageTypeCheckingExtension.groovy])
 have tripped over, and is the reason {{GspCompileStaticSpec}} cases that use 
the {{g.}} prefix are currently {{@IgnoreIf}}'d on Groovy 5 in 
[apache/grails-core PR #15557|https://github.com/apache/grails-core/pull/15557].

h2. Minor secondary observation

On Groovy 5, {{unresolvedVariable}} and {{unresolvedProperty}} are invoked 
*twice* for the same node where Groovy 4 invoked them once. Node identity is 
preserved so identity-based set bookkeeping is unaffected, but extensions 
performing side effects in those callbacks may behave differently.

> STC: unresolvedVariable/unresolvedProperty no longer fires when receiver 
> inherits getProperty(String)
> -----------------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12041
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12041
>             Project: Groovy
>          Issue Type: Bug
>            Reporter: Paul King
>            Priority: Major
>
> A {{TypeCheckingDSL}} extension that captures the receiver in 
> {{unresolvedVariable}} / {{unresolvedProperty}} and later matches it by node 
> identity in {{methodNotFound}} stops working in Groovy 5 when the enclosing 
> class inherits {{getProperty(String)}} (e.g. extends {{Script}} or a 
> GSP-style {{GroovyPage}} base). The {{unresolvedVariable}} / 
> {{unresolvedProperty}} callback never fires, so the extension has no chance 
> to record the receiver, and the downstream {{methodNotFound}} rejects the 
> chained call.
> h2. Observed behaviour
> Reproducer extension (abridged):
> {code:groovy}
> newScope { dynamicProperties = [] as Set }
> unresolvedVariable { VariableExpression ve ->
>     if (ve.name == 'g') { currentScope.dynamicProperties << ve; return 
> makeDynamic(ve) }
> }
> unresolvedProperty { PropertyExpression pe ->
>     if (pe.propertyAsString == 'g') { currentScope.dynamicProperties << pe; 
> return makeDynamic(pe) }
> }
> methodNotFound { receiver, name, argList, argTypes, call ->
>     if (call.objectExpression != null
>             && 
> currentScope.dynamicProperties.contains(call.objectExpression)) {
>         return makeDynamic(call)
>     }
> }
> {code}
> Results across versions (running {{g.message(code: 'World')}} inside the 
> {{Subject}}):
> ||Subject shape||4.0.27||5.0.5 / 5.0.6||
> |Plain class, {{g.message(...)}}|PASS ({{unresolvedVariable}} fires)|PASS 
> (fires twice, but works)|
> |Plain class, {{this.g.message(...)}}|PASS ({{unresolvedProperty}} 
> fires)|PASS|
> |{{extends Script}}, {{g.message(...)}}|*PASS*|*FAIL* — 
> {{unresolvedVariable}} never fires|
> |{{extends Base}} with {{Object getProperty(String)}}, 
> {{g.message(...)}}|*PASS*|*FAIL* — same|
> |{{extends Script}}, {{this.g.message(...)}}|PASS|PASS|
> The Groovy 5 failure is:
> {noformat}
> [Static type checking] - Cannot find matching method 
> java.lang.Object#message(LinkedHashMap<String,String>)
> {noformat}
> Diagnostic prints confirm that on the failing cases the extension is *not* 
> called for {{g}} at all; {{methodNotFound}} fires with an empty 
> {{dynamicProperties}} set. Where the callbacks do fire, AST node identity is 
> *preserved* across callbacks on Groovy 5 — i.e. the originally reported "node 
> identity changes between callbacks" hypothesis is not what is happening.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to