You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2017/10/08 11:11:40 UTC
groovy git commit: GROOVY-8347: @AutoFinal: Added support to
auto-finalize closure parameters (closes #613)
Repository: groovy
Updated Branches:
refs/heads/master 3e3b0f49b -> 8a7fe24f6
GROOVY-8347: @AutoFinal: Added support to auto-finalize closure parameters (closes #613)
Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/8a7fe24f
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/8a7fe24f
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/8a7fe24f
Branch: refs/heads/master
Commit: 8a7fe24f64e2e908d5923621cf48073091669175
Parents: 3e3b0f4
Author: mgroovy <31...@users.noreply.github.com>
Authored: Sat Sep 23 04:33:58 2017 +0200
Committer: paulk <pa...@asert.com.au>
Committed: Sun Oct 8 21:07:50 2017 +1000
----------------------------------------------------------------------
src/main/groovy/transform/AutoFinal.java | 29 ++--
.../transform/AutoFinalASTTransformation.java | 43 ++++--
.../AutoFinalTransformBlackBoxTest.groovy | 139 +++++++++++++++++++
3 files changed, 194 insertions(+), 17 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/groovy/blob/8a7fe24f/src/main/groovy/transform/AutoFinal.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/transform/AutoFinal.java b/src/main/groovy/transform/AutoFinal.java
index 5fb673f..8fb5310 100644
--- a/src/main/groovy/transform/AutoFinal.java
+++ b/src/main/groovy/transform/AutoFinal.java
@@ -18,6 +18,7 @@
*/
package groovy.transform;
+import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.ElementType;
@@ -26,12 +27,17 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Annotation to automatically add final to various syntactic structures,
- * saving you typing of some boilerplate code.
- * Initially, only method and constructor parameters are supported.
- * The annotation may be placed on any method or constructor.
- * It can also be placed at the class level in which case it applies to
- * all methods and constructors within the class.
+ * Annotation to automatically add the final qualifier to method, constructor,
+ * and closure parameters.
+ * <p>The annotation can be placed at the class level in which case it applies to
+ * all methods, constructors, and closures within the class, or on individual
+ * methods or constructors.
+ * <p>In general it will make the most sense to automatically apply the
+ * annotation to all classes of a project
+ * (groovyc --configscript; google "Customising The Groovy Compiler", or see {@link CompilerConfiguration}
+ * so that one can be sure that all arguments will automatically be final,
+ * completely eliminating the need to clutter the code with final keywords
+ * in any paramete list.
* <p>
* <em>Example usage:</em>
* <pre class="groovyTestCase">
@@ -43,11 +49,12 @@ import java.lang.annotation.Target;
* this.last = last
* }
* String fullName(boolean reversed = false, String separator = ' ') {
- * "${reversed ? last : first}$separator${reversed ? first : last}"
+ * final concatCls = { String n0, String n1 -> "$n0$separator$n1" }
+ * concatCls(reversed ? last : first, reversed ? first : last)
* }
* }
*
- * def js = new Person('John', 'Smith')
+ * final js = new Person('John', 'Smith')
* assert js.fullName() == 'John Smith'
* assert js.fullName(true, ', ') == 'Smith, John'
* </pre>
@@ -55,7 +62,7 @@ import java.lang.annotation.Target;
* equivalent to the following code:
* <pre>
* Person(final String first, final String last) {
- * //...
+ * ...
* }
* </pre>
* And after normal default parameter processing takes place, the following overloaded methods will exist:
@@ -64,6 +71,10 @@ import java.lang.annotation.Target;
* String fullName(final boolean reversed) { fullName(reversed, ' ') }
* String fullName() { fullName(false) }
* </pre>
+ * and the closure will have become:
+ * <pre>
+ * { final String n0, final String n1 -> "$n0$separator$n1" }
+ * </pre>
*
* @since 2.5.0
*/
http://git-wip-us.apache.org/repos/asf/groovy/blob/8a7fe24f/src/main/org/codehaus/groovy/transform/AutoFinalASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/transform/AutoFinalASTTransformation.java b/src/main/org/codehaus/groovy/transform/AutoFinalASTTransformation.java
index 36fc7b1..20e8d51 100644
--- a/src/main/org/codehaus/groovy/transform/AutoFinalASTTransformation.java
+++ b/src/main/org/codehaus/groovy/transform/AutoFinalASTTransformation.java
@@ -19,13 +19,8 @@
package org.codehaus.groovy.transform;
import groovy.transform.AutoFinal;
-import org.codehaus.groovy.ast.ASTNode;
-import org.codehaus.groovy.ast.AnnotatedNode;
-import org.codehaus.groovy.ast.AnnotationNode;
-import org.codehaus.groovy.ast.ClassNode;
-import org.codehaus.groovy.ast.ConstructorNode;
-import org.codehaus.groovy.ast.MethodNode;
-import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.*;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
@@ -34,7 +29,7 @@ import java.lang.reflect.Modifier;
import static org.codehaus.groovy.ast.ClassHelper.make;
/**
- * Handles generation of code for the {@code @}AutoFinal annotation.
+ * Handles generation of code for the {@link AutoFinal} annotation.
*/
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
public class AutoFinalASTTransformation extends AbstractASTTransformation {
@@ -45,6 +40,11 @@ public class AutoFinalASTTransformation extends AbstractASTTransformation {
public void visit(ASTNode[] nodes, SourceUnit source) {
init(nodes, source);
+ processClassesConstructorsMethods(nodes, source);
+ processClosures(nodes, source);
+ }
+
+ private void processClassesConstructorsMethods(ASTNode[] nodes, final SourceUnit unit) {
AnnotatedNode candidate = (AnnotatedNode) nodes[1];
AnnotationNode node = (AnnotationNode) nodes[0];
if (!MY_TYPE.equals(node.getClassNode())) return;
@@ -57,6 +57,33 @@ public class AutoFinalASTTransformation extends AbstractASTTransformation {
}
}
+ private void processClosures(ASTNode[] nodes, final SourceUnit source) {
+ final ASTNode node = nodes[1];
+ if(node instanceof ClassNode) {
+ ClassNode annotatedClass = (ClassNode) node;
+
+ final ClassCodeVisitorSupport visitor = new ClassCodeVisitorSupport() {
+ @Override
+ public void visitClosureExpression(ClosureExpression expression) {
+ if (expression.isSynthetic()) { return; }
+ Parameter[] origParams = expression.getParameters();
+ for (Parameter p : origParams) {
+ p.setModifiers(p.getModifiers() | Modifier.FINAL);
+ }
+ super.visitClosureExpression(expression);
+ }
+
+ protected SourceUnit getSourceUnit() {
+ return source;
+ }
+ };
+
+ visitor.visitClass(annotatedClass);
+ }
+ }
+
+
+
private void processClass(ClassNode cNode) {
if (cNode.isInterface()) {
addError("Error processing interface '" + cNode.getName() +
http://git-wip-us.apache.org/repos/asf/groovy/blob/8a7fe24f/src/test/org/codehaus/groovy/transform/AutoFinalTransformBlackBoxTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/AutoFinalTransformBlackBoxTest.groovy b/src/test/org/codehaus/groovy/transform/AutoFinalTransformBlackBoxTest.groovy
new file mode 100644
index 0000000..d9625ed
--- /dev/null
+++ b/src/test/org/codehaus/groovy/transform/AutoFinalTransformBlackBoxTest.groovy
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.transform
+
+import gls.CompilableTestSupport
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+
+/**
+ * Tests for the {@code @AutoFinal} AST transform.
+ */
+
+// Execute single test:
+// gradlew :test --build-cache --tests org.codehaus.groovy.transform.AutoFinalTransformTest
+@RunWith(JUnit4)
+class AutoFinalTransformBlackBoxTest extends CompilableTestSupport {
+
+ @Test
+ void testAutoFinal_Closure() {
+ assertAutoFinalClassTestScript("param0", "String foo() { final cls = { String param0 -> param0 = 'abc'; param0 }; cls() }")
+ }
+
+ @Test
+ void testAutoFinal_ClosureInClosure() {
+ assertAutoFinalClassTestScript("param1", "String foo() { final cls0 = { String param0 -> final cls1 = { String param1 -> param1 = 'xyz'; param1 }; cls1() }; cls0() }")
+ }
+
+ @Test
+ void testAutoFinal_ClassMethod_Param0() {
+ assertAutoFinalClassTestScript("param0", "String foo(String param0, param1) { param0 = 'abc'; param0 }")
+ }
+
+ @Test
+ void testAutoFinal_ClassMethod_Param1() {
+ assertAutoFinalClassTestScript("param1", "String foo(String param0, param1) { param1 = new Object(); param1 }")
+ }
+
+ // Check default parameters are not negatively impacted by @AutoFinal
+ @Test
+ void testAutoFinalClassMethodDefaultParameters() {
+ final String classPart = """
+ String foo(String param0 = 'XyZ', param1 = Closure.IDENTITY) {
+ assert param0.equals('XyZ')
+ assert param1.is(Closure.IDENTITY)
+ return param0
+ }
+ """
+ final script = autoFinalTestScript(true, classPart, "final foo = new $autoFinalTestClassName(); foo.foo()" )
+ assert script.contains('@AutoFinal')
+ assertScript(script)
+ }
+
+
+
+
+ void assertAutoFinalClassTestScript(final String paramName, final String classPart) {
+ assertAutoFinalTestScriptWithAnnotation(paramName, classPart)
+ assertAutoFinalTestScriptWithoutAnnotation(paramName, classPart)
+ }
+
+ // Checks Groovy compiler behavior when putting the passed classPart into an @AutoFinal annotated class
+ void assertAutoFinalTestScriptWithAnnotation(final String paramName, final String classPart) {
+ final script = autoFinalTestScript(true, classPart)
+ assert script.contains('@AutoFinal')
+ final result = shouldNotCompile(script)
+ println "\nassertAutoFinalTestScript result: |$result|\n\n"
+ assert result.contains("The parameter [$paramName] is declared final but is reassigned")
+ }
+
+ void assertAutoFinalTestScriptWithoutAnnotation(final String paramName, final String classPart) {
+ final script = autoFinalTestScript(false, classPart)
+ assert !script.contains('@AutoFinal')
+ shouldCompile(script)
+ }
+
+ String autoFinalTestScript(final boolean autoFinalAnnotationQ, final String classPart, final String scriptPart = '') {
+ final String script = """
+ import groovy.transform.AutoFinal
+ import groovy.transform.ASTTest
+ import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+ import static java.lang.reflect.Modifier.isFinal
+
+ ${autoFinalAnnotationQ ? '@AutoFinal' : ''}
+ class $autoFinalTestClassName {
+ $classPart
+ }
+
+ $scriptPart
+ """
+ println "script: |$script|"
+ return script
+ }
+
+ String getAutoFinalTestClassName() {
+ 'AutoFinalFoo'
+ }
+
+ /**
+ * Prints better readable, unabbreviated stack trace for passed Throwable
+ */
+ void printStackTrace(final Throwable throwable) {
+ println "${throwable.getClass().name}${throwable.message ? ": $throwable.message" : ""}"
+ throwable.stackTrace.each { println it }
+ final inner = throwable.cause
+ if(inner != null) {
+ println "Caused by........................................................................................."
+ printStackTrace(inner)
+ }
+ }
+
+ Throwable shouldThrow(final String script) {
+ try {
+ final GroovyClassLoader gcl = new GroovyClassLoader()
+ gcl.parseClass(script, getTestClassName())
+ }
+ catch(Throwable throwable) {
+ return throwable
+ }
+ throw new Exception("Script was expected to throw here!")
+ }
+}