Scott Murphy Heiberg created GROOVY-12713:
---------------------------------------------
Summary: Static type checking rejects nested copyWith: dotted keys
are "unexpected named arg" and the block form's properties are undeclared
Key: GROOVY-12713
URL: https://issues.apache.org/jira/browse/GROOVY-12713
Project: Groovy
Issue Type: Bug
Components: Static Type Checker
Affects Versions: 6.0.0
Environment: Groovy 6.0.0 binary distribution and master 7379c5720d;
BellSoft JDK 21.0.12; macOS 15 arm64
Reporter: Scott Murphy Heiberg
GROOVY-12015 (6.0.0-alpha-2) gave the generated {{copyWith}} of records and
{{@Immutable}} classes dotted-path keys ({{copyWith('address.city': 'Paris')}})
and a block form ({{copyWith { address.city = 'Paris' }}}). Both work in
dynamic code; neither passes static type checking, while the single-level
named-argument form does. On 6.0.0 and master:
{code:groovy}
import groovy.transform.CompileStatic
import groovy.transform.RecordOptions
@RecordOptions(copyWith = true)
record Address(String street, String city) {}
@RecordOptions(copyWith = true)
record Customer(String name, Address address) {}
@CompileStatic
class Demo {
static void main(String[] args) {
Customer c = new Customer('Ada', new Address('1 Main St', 'Lyon'))
println c.copyWith(name: 'Bob') // accepted
println c.copyWith('address.city': 'Paris') // line 14
println c.copyWith { address.city = 'Rome' } // line 15
}
}
{code}
{noformat}
Copy.groovy: 14: [Static type checking] - unexpected named arg: address.city
Copy.groovy: 15: [Static type checking] - The variable [address] is undeclared.
{noformat}
Without {{@CompileStatic}} the same file prints the three copies.
Causes: the record and immutable transforms annotate the generated
{{copyWith(Map)}} parameter with one {{@NamedParam}} per component, and
{{StaticTypeCheckingVisitor.checkNamedParamsAnnotation}} reports any key that
is not one of them, so a dotted path is "unexpected" although the generated
method accepts it ({{NestedCopyWithSupport.flatten}} resolves it at run time).
The block form's closure runs with a {{CopyWithRecorder}} delegate that the
type checker knows nothing about, so the assignments' targets are undeclared
variables. The documentation of {{RecordOptions.copyWith}} describes both forms
without a static-typing caveat.
Suggested fix for the dotted keys: in the named-argument check, accept a dotted
key when each segment is a named parameter of the type reached so far (the
{{type}} member of the previous segment's {{@NamedParam}}, whose own
{{copyWith(Map)}} declares the next level), report an unknown segment as
before, and check the value against the last segment's declared type. Pull
request to follow. The block form needs the type checker to model the recorder
(the record type as the closure's delegate, with writes to its read-only
properties and {{old}} allowed inside that block); that is a follow-up.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)