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)

Reply via email to