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!")
+  }
+}