Repository navigation
GROOVY-12713: type check the block form of nested copyWith - #3245
codeconsole wants to merge 9 commits into
Conversation
daniellansun
left a comment
There was a problem hiding this comment.
Build-and-test is red with the two ParserNegativeSyntaxTest failures that were broken on master after GROOVY-12641 (GEP-19). Those fixtures have been fixed on current master (see #3247).
Please rebase onto the latest master and push so CI can re-run against that tip — that should clear this noise. If anything is still red after the rebase, we can dig into it then.
The copyWith(Map) that @RecordType and @immutable generate accepts dotted keys ('address.city') since GROOVY-12015, but the named-argument check of the static type checker reported every key that is not one of the parameter's @NamedParams, so the nested form failed with "unexpected named arg" under @TypeChecked and @CompileStatic while it worked dynamically. checkNamedParamsAnnotation now walks a dotted key: each segment must be a named parameter of the type reached so far (the @NamedParam type of the previous segment, whose own copyWith(Map) declares the next level), an unknown segment is reported as before, and the value is checked against the last segment's declared type. The walk stops, accepting the key, at a type whose copyWith(Map) declares no named parameters; the runtime checks the rest. Keys without a dot are checked exactly as before. Tests in RecordNestedCopyWithTest and ImmutableNestedCopyWithTest: a statically compiled method using two nested keys, an unknown nested segment ("unexpected named arg: address.state") and a wrongly typed value for a nested key. The block form (copyWith { address.city = 'x' }) is still dynamic only; the issue tracks it.
copyWith { name = old.name.toUpperCase(); address.city = 'x' } runs its
closure against a CopyWithRecorder that turns the assignments into the map
form, so under @TypeChecked and @CompileStatic every name in it was an
undeclared variable. The type checker now recognises the generated
copyWith(Closure) of a type whose copyWith(Map) declares named parameters,
makes that type the closure's delegate, and inside the block types a bare
component name, a component path and old as the declared component types and
the copied type; assignments to them are checked against those types,
path.modify { it ... } gets the component's type as its closure parameter, and
all of them are marked for dynamic resolution so static compilation dispatches
them through the closure at run time, where the recorder answers. Reads
through old stay static: old is the real object. An unknown component is still
"No such property".
Tests: RecordNestedCopyWithTest (the block under @CompileStatic with nested
writes, old and modify; a wrongly typed value; an unknown component) and
ImmutableNestedCopyWithTest (the block under @TypeChecked).
0f1b5be to
2706945
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3245 +/- ##
==================================================
+ Coverage 74.3428% 74.4186% +0.0758%
- Complexity 44310 44424 +114
==================================================
Files 1699 1699
Lines 150862 151176 +314
Branches 28756 28829 +73
==================================================
+ Hits 112155 112503 +348
+ Misses 28435 28381 -54
- Partials 10272 10292 +20
🚀 New features to boost your workflow:
|
daniellansun
left a comment
There was a problem hiding this comment.
Thanks for rebasing and stacking cleanly on the rebased #3244. Tip 2706945846a9 is green.
The block-form STC work for nested copyWith is carefully scoped: isCopyWithBlock only engages when the receiver’s map copyWith declares @NamedParams; bare names and dotted paths inside the closure are typed from those components and marked DYNAMIC_RESOLUTION so the recorder stays the dispatch target; old is typed as the copied type; and modify { … } is modelled as a synthetic method so the nested closure is checked. The new record / immutable tests (happy path, bad assignment types, unknown components) exercise the important edges.
Please merge #3244 first so this tip’s history stays linear. Aside from that ordering note, I am happy to approve.
…2713-copywith-block-stc
…enerated copyWith(Map)
# Conflicts: # src/test/groovy/org/codehaus/groovy/transform/RecordNestedCopyWithTest.groovy
…gate A component read in the block is the recorder, not the value; modify on a component path is always the recorder's; method calls go to the owner; and a bare name that is not a component is an error rather than an owner member.
…ns against the component type
✅ All tests passed ✅🏷️ Commit: 3caa0ab Learn more about TestLens at testlens.app/docs. |
Second part of GROOVY-12713, on top of #3244 (whose commit this branch contains; the diff against master shows both until that one is merged).
copyWith { name = old.name.toUpperCase(); address.city = 'x' }runs its closure against aCopyWithRecorder(NestedCopyWithSupport.applyBlock) that turns the assignments into the map form, so under@TypeCheckedand@CompileStaticevery name in the block was "The variable [address] is undeclared.", while the same block worked dynamically.The type checker now recognises the generated
copyWith(Closure)of a type whosecopyWith(Map)declares its components as@NamedParams (the same test as for the dotted keys), types the closure's delegate as what it is at run time, aCopyWithRecorder(DELEGATE_FIRST), and inside the block:name) or a component path (company.address.zip) that is the target of an assignment has the declared component type, so the assignment is checked against it (Cannot assign value of type java.lang.String to variable of type int), and an unknown component is stillNo such property: state for class: Address;name = name.toUpperCase()is a compile error (Cannot find matching method ...CopyWithRecorder#toUpperCase()); read the current value throughold, which is typed as the copied type:name = old.name.toUpperCase();path.modify { it ... }always resolves to the recorder'smodify, even when the component type has amodifymethod of its own, with the component's type as the closure's parameter and result type;... a qualifier is required to reference a member of the enclosing class);this.xreaches it. Closures inside the block without a delegate of their own (each, amodifyclosure) resolve names the same way;oldstay static, sinceoldis the real object.Nothing changes outside such a block: the marker lives on the block's closure for the duration of its visit, and the lookup of the type's named parameters is the helper #3244 added.
Tests:
RecordNestedCopyWithTestgains the block under@CompileStaticwith writes at three depths,oldandmodify(and runs the result), a wrongly typed value and an unknown component;ImmutableNestedCopyWithTestthe block under@TypeChecked. On JDK 21 those two classes,groovy.transform.stc.*,org.codehaus.groovy.classgen.asm.sc.*,NamedParameterTest,ImmutableTransformTestandRecordTestpass (4,845 tests).Candidate for
GROOVY_6_0_Xtogether with #3244.