[ 
https://issues.apache.org/jira/browse/GROOVY-12225?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King reassigned GROOVY-12225:
----------------------------------

    Assignee: Paul King

> STC: property access to a record component fails for precompiled records 
> -------------------------------------------------------------------------
>
>                 Key: GROOVY-12225
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12225
>             Project: Groovy
>          Issue Type: Bug
>    Affects Versions: 4.0.33, 5.0.8, 6.0.0-beta-1
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> Property access to a record component ({{p.x}}) should resolve to the 
> component accessor {{x()}}. Under {{@CompileStatic}} it instead resolves to 
> the private backing field and fails at bytecode generation:
> {noformat}
> Access to demo.Point#x is forbidden
> {noformat}
> This only happens when the record is *precompiled* (read from a class file). 
> A record declared in the same compilation unit works.
> h2. Reproducing
> Compile these to a directory first, then compile the consumer against it:
> {code:java}
> // demo/Point.groovy  -- compiled separately
> package demo
> record Point(int x, int y) { }
> {code}
> {code:java}
> import groovy.transform.CompileStatic
> import groovy.transform.TypeChecked
> import demo.Point
> @TypeChecked
> static int typeChecked(Point p) { p.x }      // OK
> @CompileStatic
> static int accessorForm(Point p) { p.x() }   // OK
> @CompileStatic
> static int compileStatic(Point p) { p.x }    // FAILS: Access to demo.Point#x 
> is forbidden
> {code}
> h2. What works and what doesn't
> ||Form||Result||
> |{{p.x}} -- dynamic|works|
> |{{p.x}} -- {{@TypeChecked}}|works|
> |{{p.x}} -- {{@CompileStatic}}, record in the *same* compilation unit|works|
> |{{p.x}} -- {{@CompileStatic}}, record *precompiled* (Groovy)|*fails*|
> |{{p.x}} -- {{@CompileStatic}}, record *precompiled* (Java)|*fails*|
> |{{p.x()}} -- {{@CompileStatic}}|works (workaround)|
> Two discriminators worth noting:
> * {{@TypeChecked}} passes and {{@CompileStatic}} fails, so the type checker 
> resolves the property correctly and the failure is in the call-site writer.
> * Java records fail identically, so this is about how the record is _read_, 
> not how it was produced.
> h2. Root cause
> {{StaticTypesCallSiteWriter.makeGetPropertyWithGetter}} looks for a getter in 
> this order:
> {code:java}
> String getterName = "is" + capitalize(propertyName);
> MethodNode getterNode = receiverType.getGetterMethod(getterName);
> if (getterNode == null) {
>     getterName = "get" + capitalize(propertyName);
>     getterNode = receiverType.getGetterMethod(getterName);
> }
> // GROOVY-5561: same-source-unit fallback
> PropertyNode propertyNode = receiverType.getProperty(propertyName);
> {code}
> A record component accessor is {{x()}}, not {{getX()}}/{{isX()}}, so both 
> name-based lookups miss. The third lookup -- the GROOVY-5561 same-source-unit 
> fallback -- is what rescues the same-unit case, because the record transform 
> has already attached a {{PropertyNode}}. For a record read from a class file 
> there is no {{PropertyNode}}, so resolution falls through to field access and 
> {{addPropertyAccessError}} fires.
> Inspecting the decompiled {{ClassNode}} shows everything needed is already 
> present, it just isn't consulted:
> {noformat}
> precompiled Groovy record demo.Point      precompiled Java record demo.JPoint
>   isRecord()              : true            isRecord()              : true
>   recordComponents        : [x, y]          recordComponents        : [x, y]
>   getProperty('x')        : null            getProperty('x')        : null
>   getField('x')           : private int x   getField('x')           : private 
> int x
>   getDeclaredMethod x()   : int x()         getDeclaredMethod x()   : int x()
>   getGetterMethod('getX') : null            getGetterMethod('getX') : null
> {noformat}
> h2. Suggested fix
> Add a record-component branch to {{makeGetPropertyWithGetter}}, before the 
> field fallback: if {{receiverType.isRecord()}} and a record component matches 
> {{propertyName}}, use the {{propertyName()}} accessor as the getter. 
> {{ClassNode}} already exposes {{isRecord()}} and {{getRecordComponents()}} on 
> decompiled types.
> h2. Impact
> Any {{@CompileStatic}} code consuming records from a jar hits this. 
> Concretely it affects Groovy's own modules: {{groovy.http.HttpResult}} in 
> {{groovy-http-builder}} is a record, so {{result.status}} and {{result.body}} 
> cannot be used from statically compiled code without the {{()}} workaround.
> h2. Affects
> Reproduced on 6.0.0-beta-1 and a current master build, JDK 21.



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

Reply via email to