Paul King created GROOVY-12466:
----------------------------------

             Summary: GEP-15: compound assignment that resolves to a *Assign 
method is refused for final parameters and for final fields used inside their 
own class
                 Key: GROOVY-12466
                 URL: https://issues.apache.org/jira/browse/GROOVY-12466
             Project: Groovy
          Issue Type: Bug
          Components: Compiler, Static Type Checker
    Affects Versions: 6.0.0
            Reporter: Paul King
            Assignee: Paul King


h2. Problem

GEP-15 lets a class define {{plusAssign}}, {{minusAssign}}, {{leftShiftAssign}} 
and the other {{*Assign}} methods. The 6.0 documentation (core-operators, 
"compound assignment") says that when the compiler resolves such a method 
statically, the receiver "is mutated in place, no reassignment occurs (so 
{{final}} fields and variables can use compound assignment in that case)". That 
holds for final local variables and for final fields reached from outside their 
class. It does not hold for final parameters, or for final fields used inside 
their own class:

{code:groovy}
@CompileStatic
class Acc {
    int total
    void plusAssign(int v) { total += v }
}

@CompileStatic
class H {
    final Acc acc = new Acc()

    int viaField() { acc += 1; acc.total }          // error: cannot modify 
final field 'acc' outside of constructor.
    int viaThis() { this.acc += 1; acc.total }      // error: cannot modify 
final field 'acc' outside of constructor.
    static int viaParam(final Acc a) { a += 1; a.total }  // error: Cannot 
assign a value to final variable 'a'
    static int viaLocal(Acc x) { final Acc a = x; a += 1; a.total }  // 
compiles and runs: 1
}

@CompileStatic
int outside() { def h = new H(); h.acc += 1; h.acc.total }  // compiles and 
runs: 1
{code}

Results on 6.0.0:

||Target of {{+= 1}}||@CompileStatic||@TypeChecked||dynamic||
|final local variable|compiles, runs|compiles, runs|refused (expected)|
|final field from outside ({{h.acc}})|compiles, runs|compiles, runs|n/a|
|final field inside its class ({{acc}} or 
{{this.acc}})|*refused*|*refused*|refused (expected)|
|final parameter|*refused*|*refused*|refused (expected)|

The dynamic rows are expected. The documentation says the final check cannot be 
relaxed there, because the compiler doesn't know whether a {{*Assign}} method 
will respond at runtime. The {{CompoundAssignmentTest}} cases cover the final 
local and the outside-field forms, but not these two.

h2. Cause

Two checks reject the assignment without considering the {{*Assign}} target 
that static type checking records in 
{{StaticTypesMarker.COMPOUND_ASSIGN_TARGET}}:

* *Final parameters:* {{VariableScopeVisitor.checkFinalFieldAccess}} reports 
"Cannot assign a value to final variable" for any assignment operator whose 
target is a final {{Parameter}}. It runs during semantic analysis, before type 
checking, so it cannot know whether a {{*Assign}} method will be found. The 
later check in {{Verifier}} (the {{FinalVariableAnalyzer}} callback) already 
skips expressions carrying {{COMPOUND_ASSIGN_TARGET}}, for parameters as well 
as locals, but is never reached.
* *Final fields:* {{ClassCompletionVerifier.checkFinalFieldAccess}} reports 
"cannot modify final field ... outside of constructor" for any assignment 
operator whose target is a final field named directly or through {{this}}. It 
runs after type checking, so the marker is available, but it is not consulted. 
A field reached through another object ({{h.acc}}) passes only because this 
check ignores properties on anything other than {{this}}.

h2. Proposed fix

* In {{VariableScopeVisitor}}, report final parameters only for plain 
assignment ({{=}}). Leave compound operators on final parameters to the 
{{FinalVariableAnalyzer}} check in {{Verifier}}, which already reports "The 
parameter \[a\] is declared final but is reassigned" when no {{*Assign}} target 
was resolved, in dynamic code or when the type has no such method. The error is 
still reported in those cases, with that message instead of the current one.
* In {{ClassCompletionVerifier}}, skip the final-field error when the 
assignment's {{BinaryExpression}} carries {{COMPOUND_ASSIGN_TARGET}}. The check 
currently receives only the left-hand expression, so it needs the binary 
expression passed in as well.
* Confirm that code generation emits no field store or local store for these 
cases, in both static code generation and the classic code generation used by 
{{@TypeChecked}}. A store to a final field outside a constructor would fail 
bytecode verification. The final-local and outside-field cases show the 
call-only path already works.

h2. Tests

Extend {{CompoundAssignmentTest}}:
* Final parameter with {{plusAssign}}, under {{@CompileStatic}} and 
{{@TypeChecked}}: compiles, mutates the argument in place, and does not rebind 
the parameter.
* Final instance field and final static field, used by simple name and through 
{{this}} inside their own class: compiles and mutates in place.
* {{leftShiftAssign}} and an {{@OperatorRename}}-renamed {{*Assign}} method, 
for the same targets.
* Still refused: the same forms in dynamic code; a type with only {{plus}} and 
no {{plusAssign}}; plain {{=}} to a final parameter or field outside a 
constructor.
* Still allowed: assignment to a final field inside a constructor.

h2. Related

Found while checking PurityChecker against GEP-15 operators. That is a separate 
ticket: PurityChecker does not treat {{a += v}} that resolves to {{plusAssign}} 
as a mutation of {{a}}.




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

Reply via email to