Paul King created GROOVY-12630:
----------------------------------
Summary: static-compilation gaps (`++`/`--` and real setter with
extension getter)
Key: GROOVY-12630
URL: https://issues.apache.org/jira/browse/GROOVY-12630
Project: Groovy
Issue Type: Bug
Components: Static compilation
Affects Versions: 6.0.0
Reporter: Paul King
h2. Description
Under {{@CompileStatic}}, GROOVY-12524 made compound assignment ({{+=}},
{{-=}}, {{?=}}, ...) work on a property whose accessors are extension methods.
Two related forms still fail to compile. Both work in dynamic and
{{@TypeChecked}} code, and both failed the same way in 6.0.0.
# Prefix and postfix increment and decrement ({{++}}, {{--}}) on an extension
property.
# Compound assignment to a property with a real setter and an extension getter.
h2. Example
An extension module declares:
{code:java}
public class Box { public int v; }
public class Mix {
public String v = "a";
public void setRs(String x) { v = "RS:" + x; } // real setter
}
public class BoxExtension {
public static int getCount(Box b) { return b.v; }
public static void setCount(Box b, int x) { b.v = x; }
public static String getRs(Mix m) { return m.v + "E"; } // extension
getter
}
{code}
Then:
{code:groovy}
@groovy.transform.CompileStatic
void test(Box b, Mix m) {
b.count += 1 // compiles since GROOVY-12524
b.count++ // Access to Box#count is forbidden
++b.count // Access to Box#count is forbidden
b.count-- // Access to Box#count is forbidden
m.rs += 'x' // Access to Mix#rs is forbidden
}
{code}
The same failure occurs for a static extension property ({{Box.sx++}}) and for
extension properties supplied by Groovy itself.
The long forms compile and are the workaround:
{code:groovy}
b.count = b.count + 1
m.rs = m.rs + 'x'
{code}
A real getter with an extension setter ({{m.rg += 'x'}}) already works.
h2. Cause
* *Increment and decrement.* Static compilation has no rewrite for
{{PrefixExpression}} or {{PostfixExpression}}. Codegen loads the operand as a
property, and that load does not consider extension getters. A plain read works
only because {{PropertyExpressionTransformer}} first turns it into a getter
call.
* *Mixed accessors.* The GROOVY-12524 rewrite in
{{BinaryExpressionTransformer}} applies only when the setter is an
{{ExtensionMethodNode}}. When the setter is a real method, the compound
assignment takes the old path, which fails on the extension getter for the read.
h2. Suggested fix
* Let the compound-assignment rewrite apply when *either* accessor is an
extension method. The type checker already records the extension getter for the
read side.
* Rewrite {{++p}} / {{--p}} on such a property to {{setP(getP().next())}} /
{{setP(getP().previous())}}, whose value is the new value. Rewrite {{p++}} /
{{p--}} the same way, keeping the old value in a temporary as the expression's
value. Reuse the getter lookup and setter-call construction from GROOVY-12524.
* The receiver can be evaluated twice, once for the read and once for the
write. Dynamic, {{@TypeChecked}} and {{@CompileStatic}} code all do this today,
for real and extension properties alike.
h2. Test clean-up in Groovy12524
Three {{shouldFail}} tests pass without reaching the guard they are named
after, and none of them checks a message:
* {{testAttributeCompoundAssignOfExtensionPropertyIsRejected}}: {{file.@text +=
'b'}} fails with an internal compiler error
({{ArrayIndexOutOfBoundsException}}). Plain {{file.@text = 'b'}} fails the same
way, which is a separate pre-existing bug.
* {{testSpreadSafeCompoundAssignIsNotRewrittenAsExtensionProperty}}: fails on a
generics error ({{Cannot assign
java.util.List<java.util.List<java.lang.String>> to:
java.util.List<java.lang.String>}}) before the spread-safe guard is reached.
* {{testPrimitiveExtensionSetterWithoutGetterIsRejected}}: fails with {{Cannot
find matching method java.lang.Object#plus(int)}}.
Each test should assert the error it expects. If no spread-safe form gets past
type checking, that guard is defensive only and its test can be dropped.
The Javadoc of the private method
{{transformExtensionPropertyCompoundAssignment}} also opens with the JIRA key,
which CONTRIBUTING asks to keep out of doc comments.
h2. Not in scope
* {{b.count **= 2}} is rejected under {{@TypeChecked}} and {{@CompileStatic}},
with the same error as for a real {{int}} property ({{int ** int}} is a
{{Number}}).
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)