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