groovy-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From sun...@apache.org
Subject groovy git commit: Newify pattern support for pull(closes #689 #686)
Date Sun, 22 Apr 2018 23:32:13 GMT
Repository: groovy
Updated Branches:
  refs/heads/master b5ff40d87 -> e91e73651


Newify pattern support for pull(closes #689 #686)


Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/e91e7365
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/e91e7365
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/e91e7365

Branch: refs/heads/master
Commit: e91e73651e0c6f48d54ecda28a8cb492b427d648
Parents: b5ff40d
Author: mgroovy <31455466+mgroovy@users.noreply.github.com>
Authored: Mon Apr 23 00:31:20 2018 +0200
Committer: sunlan <sunlan@apache.org>
Committed: Mon Apr 23 07:30:56 2018 +0800

----------------------------------------------------------------------
 src/main/groovy/groovy/lang/Newify.java         |  31 +-
 .../transform/NewifyASTTransformation.java      | 349 ++++++++---
 .../NewifyTransformBlackBoxTest.groovy          | 593 +++++++++++++++++++
 3 files changed, 884 insertions(+), 89 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/groovy/blob/e91e7365/src/main/groovy/groovy/lang/Newify.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/lang/Newify.java b/src/main/groovy/groovy/lang/Newify.java
index 525cecb..023a0b3 100644
--- a/src/main/groovy/groovy/lang/Newify.java
+++ b/src/main/groovy/groovy/lang/Newify.java
@@ -30,13 +30,30 @@ import java.lang.annotation.Target;
  * keyword. Instead they can be written "Ruby-style" as a method call to a 'new'
  * method or "Python-style" by just omitting the 'new' keyword.
  * <p>
- * It allows you to write code snippets like this ("Python-style"):
+ * WARNING: For the Python style with class-name-matching pattern, the pattern should be
chosen as to avoid matching
+ * method names if possible. If following Java/Groovy naming convention, class names (contrary
to method names) start
+ * with an uppercase letter. In this case {@code pattern="[A-Z].*"} (see {@link java.util.regex.Pattern}
for supported
+ * Java pattern syntax) is the recommended pattern to allow all classes to be created without
requiring a new keyword.
+ * Using a pattern that also matches method names (e.g. ".+", ".*" or "[a-zA-Z].*") might
negatively impact build
+ * performance, since the Groovy compiler will have to match every class in context against
any potential constructor
+ * call.
+ * <p>
+ * {@literal @Newify} allows you to write code snippets like this ("Python-style"):
  * <pre>
  * {@code @Newify([Tree,Leaf])} class MyTreeProcessor {
  *     def myTree = Tree(Tree(Leaf("A"), Leaf("B")), Leaf("C"))
  *     def process() { ... }
  * }
  * </pre>
+ * <pre>
+ * {@code // Any class whose name matches pattern can be created without new}
+ * {@code @Newify(pattern="[A-Z].*")} class MyTreeProcessor {
+ *     final myTree = Tree(Tree(Leaf("A"), Leaf("B")), Leaf("C"))
+ *     final sb = StringBuilder("...")
+ *     def dir = File('.')
+ *     def root = XmlSlurper().parseText(File(dir, sb.toString()).text)
+ * }
+ * </pre>
  * or this ("Ruby-style"):
  * <pre>
  * {@code @Newify} class MyTreeProcessor {
@@ -59,8 +76,9 @@ import java.lang.annotation.Target;
  * flag is given when using the annotation. You might do this if you create a new method
  * using meta programming.
  * <p>
- * The "Python-style" conversions require you to specify each class on which you want them
- * to apply. The transformation then works by matching the basename of the provided classes
to any
+ * For the "Python-style" conversions you can either specify each class name on which you
want them
+ * to apply, or supply a pattern to match class names against. The transformation then works
by matching the basename
+ * of the provided classes to any
  * similarly named instance method calls not specifically bound to an object, i.e. associated
  * with the 'this' object. In other words <code>Leaf("A")</code> would be transformed
to
  * <code>new Leaf("A")</code> but <code>x.Leaf("A")</code> would
not be touched.
@@ -73,9 +91,9 @@ import java.lang.annotation.Target;
  *     def field1 = java.math.BigInteger.new(42)
  *     def field2, field3, field4
  *
- *     {@code @Newify(Bar)}
+ *     {@code @Newify(pattern="[A-z][A-Za-z0-9_]*")} // Any class name that starts with an
uppercase letter
  *     def process() {
- *         field2 = Bar("my bar")
+ *         field2 = A(Bb(Ccc("my bar")))
  *     }
  *
  *     {@code @Newify(Baz)}
@@ -94,6 +112,7 @@ import java.lang.annotation.Target;
  * field level if already turned on at the class level.
  *
  * @author Paul King
+ * @author mgroovy
  */
 @java.lang.annotation.Documented
 @Retention(RetentionPolicy.SOURCE)
@@ -106,4 +125,6 @@ public @interface Newify {
      * @return if automatic conversion of "Ruby-style" new method calls should occur
      */
     boolean auto() default true;
+
+    String pattern() default "";
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/e91e7365/src/main/java/org/codehaus/groovy/transform/NewifyASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/codehaus/groovy/transform/NewifyASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/NewifyASTTransformation.java
index 9cd5c3f..aade78c 100644
--- a/src/main/java/org/codehaus/groovy/transform/NewifyASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/NewifyASTTransformation.java
@@ -18,31 +18,20 @@
  */
 package org.codehaus.groovy.transform;
 
-import groovy.lang.Newify;
+import groovy.lang.*;
 import org.codehaus.groovy.GroovyBugError;
-import org.codehaus.groovy.ast.ASTNode;
-import org.codehaus.groovy.ast.AnnotatedNode;
-import org.codehaus.groovy.ast.AnnotationNode;
-import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
-import org.codehaus.groovy.ast.ClassNode;
-import org.codehaus.groovy.ast.FieldNode;
-import org.codehaus.groovy.ast.MethodNode;
-import org.codehaus.groovy.ast.expr.ClassExpression;
-import org.codehaus.groovy.ast.expr.ClosureExpression;
-import org.codehaus.groovy.ast.expr.ConstantExpression;
-import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
-import org.codehaus.groovy.ast.expr.DeclarationExpression;
-import org.codehaus.groovy.ast.expr.Expression;
-import org.codehaus.groovy.ast.expr.ListExpression;
-import org.codehaus.groovy.ast.expr.MethodCallExpression;
-import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.*;
+import org.codehaus.groovy.ast.expr.*;
 import org.codehaus.groovy.control.CompilePhase;
 import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
 
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 import static org.codehaus.groovy.ast.ClassHelper.make;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
@@ -61,6 +50,65 @@ public class NewifyASTTransformation extends ClassCodeExpressionTransformer
impl
     private ListExpression classesToNewify;
     private DeclarationExpression candidate;
     private boolean auto;
+    private Pattern classNamePattern;
+
+    private static Map<String, ClassNode> nameToGlobalClassesNodesMap;
+    private Map<String, NewifyClassData> nameToInnerClassesNodesMap;
+
+    // ClassHelper.classes minus interfaces, abstract classes, and classes with private ctors
+    private static final Class[] globalClasses = new Class[]{
+            Object.class,
+            Boolean.TYPE,
+            Character.TYPE,
+            Byte.TYPE,
+            Short.TYPE,
+            Integer.TYPE,
+            Long.TYPE,
+            Double.TYPE,
+            Float.TYPE,
+            // Void.TYPE,
+            // Closure.class,
+            // GString.class,
+            // List.class,
+            // Map.class,
+            // Range.class,
+            //Pattern.class,
+            // Script.class,
+            String.class,
+            Boolean.class,  // Shall we allow this ? Using Boolean ctors is usually not what
user wants...
+            Character.class,
+            Byte.class,
+            Short.class,
+            Integer.class,
+            Long.class,
+            Double.class,
+            Float.class,
+            BigDecimal.class,
+            BigInteger.class,
+            //Number.class,
+            //Void.class,
+            Reference.class,
+            //Class.class,
+            //MetaClass.class,
+            //Iterator.class,
+            //GeneratedClosure.class,
+            //GeneratedLambda.class,
+            //GroovyObjectSupport.class
+    };
+
+    static {
+        nameToGlobalClassesNodesMap = new ConcurrentHashMap<String, ClassNode>(16,
0.9f, 1);
+        for (Class globalClass : globalClasses) {
+            nameToGlobalClassesNodesMap.put(globalClass.getSimpleName(), ClassHelper.makeCached(globalClass));
+        }
+    }
+
+
+    private static final Pattern extractNamePattern = Pattern.compile("^(?:.*\\$|)(.*)$");
+
+    public static String extractName(final String s) {
+        return extractNamePattern.matcher(s).replaceFirst("$1");
+    }
 
     public void visit(ASTNode[] nodes, SourceUnit source) {
         this.source = source;
@@ -74,35 +122,106 @@ public class NewifyASTTransformation extends ClassCodeExpressionTransformer
impl
             internalError("Transformation called from wrong annotation: " + node.getClassNode().getName());
         }
 
-        boolean autoFlag = determineAutoFlag(node.getMember("auto"));
-        Expression value = node.getMember("value");
+        final boolean autoFlag = determineAutoFlag(node.getMember("auto"));
+        final Expression classNames = node.getMember("value");
+        final Pattern cnPattern = determineClassNamePattern(node.getMember("pattern"));
 
         if (parent instanceof ClassNode) {
-            newifyClass((ClassNode) parent, autoFlag, determineClasses(value, false));
+            newifyClass((ClassNode) parent, autoFlag, determineClasses(classNames, false),
cnPattern);
         } else if (parent instanceof MethodNode || parent instanceof FieldNode) {
-            newifyMethodOrField(parent, autoFlag, determineClasses(value, false));
+            newifyMethodOrField(parent, autoFlag, determineClasses(classNames, false), cnPattern);
         } else if (parent instanceof DeclarationExpression) {
-            newifyDeclaration((DeclarationExpression) parent, autoFlag, determineClasses(value,
true));
+            newifyDeclaration((DeclarationExpression) parent, autoFlag, determineClasses(classNames,
true), cnPattern);
+        }
+    }
+
+
+    private void newifyClass(ClassNode cNode, boolean autoFlag, ListExpression list, final
Pattern cnPattern) {
+        String cName = cNode.getName();
+        if (cNode.isInterface()) {
+            addError("Error processing interface '" + cName + "'. @"
+                    + MY_NAME + " not allowed for interfaces.", cNode);
+        }
+
+        final ListExpression oldClassesToNewify = classesToNewify;
+        final boolean oldAuto = auto;
+        final Pattern oldCnPattern = classNamePattern;
+
+        classesToNewify = list;
+        auto = autoFlag;
+        classNamePattern = cnPattern;
+
+        super.visitClass(cNode);
+
+        classesToNewify = oldClassesToNewify;
+        auto = oldAuto;
+        classNamePattern = oldCnPattern;
+    }
+
+    private void newifyMethodOrField(AnnotatedNode parent, boolean autoFlag, ListExpression
list, final Pattern cnPattern) {
+
+        final ListExpression oldClassesToNewify = classesToNewify;
+        final boolean oldAuto = auto;
+        final Pattern oldCnPattern = classNamePattern;
+
+        checkClassLevelClashes(list);
+        checkAutoClash(autoFlag, parent);
+
+        classesToNewify = list;
+        auto = autoFlag;
+        classNamePattern = cnPattern;
+
+        if (parent instanceof FieldNode) {
+            super.visitField((FieldNode) parent);
+        } else {
+            super.visitMethod((MethodNode) parent);
         }
+
+        classesToNewify = oldClassesToNewify;
+        auto = oldAuto;
+        classNamePattern = oldCnPattern;
     }
 
-    private void newifyDeclaration(DeclarationExpression de, boolean autoFlag, ListExpression
list) {
+
+    private void newifyDeclaration(DeclarationExpression de, boolean autoFlag, ListExpression
list, final Pattern cnPattern) {
         ClassNode cNode = de.getDeclaringClass();
         candidate = de;
         final ListExpression oldClassesToNewify = classesToNewify;
         final boolean oldAuto = auto;
+        final Pattern oldCnPattern = classNamePattern;
+
         classesToNewify = list;
         auto = autoFlag;
+        classNamePattern = cnPattern;
+
         super.visitClass(cNode);
+
         classesToNewify = oldClassesToNewify;
         auto = oldAuto;
+        classNamePattern = oldCnPattern;
     }
 
     private static boolean determineAutoFlag(Expression autoExpr) {
         return !(autoExpr instanceof ConstantExpression && ((ConstantExpression)
autoExpr).getValue().equals(false));
     }
 
-    /** allow non-strict mode in scripts because parsing not complete at that point */
+    private Pattern determineClassNamePattern(Expression expr) {
+        if (!(expr instanceof ConstantExpression)) { return null; }
+        final ConstantExpression constExpr = (ConstantExpression) expr;
+        final String text = constExpr.getText();
+        if (constExpr.getValue() == null || text.equals("")) { return null; }
+        try {
+            final Pattern pattern = Pattern.compile(text);
+            return pattern;
+        } catch (PatternSyntaxException e) {
+            addError("Invalid class name pattern: " + e.getMessage(), expr);
+            return null;
+        }
+    }
+
+    /**
+     * allow non-strict mode in scripts because parsing not complete at that point
+     */
     private ListExpression determineClasses(Expression expr, boolean searchSourceUnit) {
         ListExpression list = new ListExpression();
         if (expr instanceof ClassExpression) {
@@ -196,44 +315,13 @@ public class NewifyASTTransformation extends ClassCodeExpressionTransformer
impl
     }
 
     private boolean hasClassesToNewify() {
-        return classesToNewify != null && !classesToNewify.getExpressions().isEmpty();
+        return (classesToNewify != null && !classesToNewify.getExpressions().isEmpty())
|| (classNamePattern != null);
     }
 
-    private void newifyClass(ClassNode cNode, boolean autoFlag, ListExpression list) {
-        String cName = cNode.getName();
-        if (cNode.isInterface()) {
-            addError("Error processing interface '" + cName + "'. @"
-                    + MY_NAME + " not allowed for interfaces.", cNode);
-        }
-        final ListExpression oldClassesToNewify = classesToNewify;
-        final boolean oldAuto = auto;
-        classesToNewify = list;
-        auto = autoFlag;
-        super.visitClass(cNode);
-        classesToNewify = oldClassesToNewify;
-        auto = oldAuto;
-    }
-
-    private void newifyMethodOrField(AnnotatedNode parent, boolean autoFlag, ListExpression
list) {
-        final ListExpression oldClassesToNewify = classesToNewify;
-        final boolean oldAuto = auto;
-        checkClassLevelClashes(list);
-        checkAutoClash(autoFlag, parent);
-        classesToNewify = list;
-        auto = autoFlag;
-        if (parent instanceof FieldNode) {
-            super.visitField((FieldNode) parent);
-        } else {
-            super.visitMethod((MethodNode) parent);
-        }
-        classesToNewify = oldClassesToNewify;
-        auto = oldAuto;
-    }
 
     private void checkDuplicateNameClashes(ListExpression list) {
         final Set<String> seen = new HashSet<String>();
-        @SuppressWarnings("unchecked")
-        final List<ClassExpression> classes = (List)list.getExpressions();
+        @SuppressWarnings("unchecked") final List<ClassExpression> classes = (List)
list.getExpressions();
         for (ClassExpression ce : classes) {
             final String name = ce.getType().getNameWithoutPackage();
             if (seen.contains(name)) {
@@ -251,8 +339,7 @@ public class NewifyASTTransformation extends ClassCodeExpressionTransformer
impl
     }
 
     private void checkClassLevelClashes(ListExpression list) {
-        @SuppressWarnings("unchecked")
-        final List<ClassExpression> classes = (List)list.getExpressions();
+        @SuppressWarnings("unchecked") final List<ClassExpression> classes = (List)
list.getExpressions();
         for (ClassExpression ce : classes) {
             final String name = ce.getType().getNameWithoutPackage();
             if (findClassWithMatchingBasename(name)) {
@@ -262,17 +349,6 @@ public class NewifyASTTransformation extends ClassCodeExpressionTransformer
impl
         }
     }
 
-    private boolean findClassWithMatchingBasename(String nameWithoutPackage) {
-        if (classesToNewify == null) return false;
-        @SuppressWarnings("unchecked")
-        final List<ClassExpression> classes = (List)classesToNewify.getExpressions();
-        for (ClassExpression ce : classes) {
-            if (ce.getType().getNameWithoutPackage().equals(nameWithoutPackage)) {
-                return true;
-            }
-        }
-        return false;
-    }
 
     private boolean isNewifyCandidate(MethodCallExpression mce) {
         return mce.getObjectExpression() == VariableExpression.THIS_EXPRESSION
@@ -286,31 +362,114 @@ public class NewifyASTTransformation extends ClassCodeExpressionTransformer
impl
                 && ((ConstantExpression) meth).getValue().equals("new"));
     }
 
-    private Expression transformMethodCall(MethodCallExpression mce, Expression args) {
+    private Expression transformMethodCall(MethodCallExpression mce, Expression argsExp)
{
         ClassNode classType;
+
         if (isNewMethodStyle(mce)) {
             classType = mce.getObjectExpression().getType();
         } else {
             classType = findMatchingCandidateClass(mce);
         }
+
         if (classType != null) {
-            return new ConstructorCallExpression(classType, args);
+            Expression argsToUse = argsExp;
+            if (classType.getOuterClass() != null && ((classType.getModifiers() &
org.objectweb.asm.Opcodes.ACC_STATIC) == 0)) {
+                if (!(argsExp instanceof ArgumentListExpression)) {
+                    addError("Non-static inner constructor arguments must be an argument
list expression; pass 'this' pointer explicitely as first constructor argument otherwise.",
mce);
+                    return mce;
+                }
+                final ArgumentListExpression argsListExp = (ArgumentListExpression) argsExp;
+                final List<Expression> argExpList = argsListExp.getExpressions();
+                final VariableExpression thisVarExp = new VariableExpression("this");
+
+                final List<Expression> expressionsWithThis = new ArrayList<Expression>(argExpList.size()
+ 1);
+                expressionsWithThis.add(thisVarExp);
+                expressionsWithThis.addAll(argExpList);
+
+                argsToUse = new ArgumentListExpression(expressionsWithThis);
+            }
+            return new ConstructorCallExpression(classType, argsToUse);
         }
+
         // set the args as they might have gotten Newify transformed GROOVY-3491
-        mce.setArguments(args);
+        mce.setArguments(argsExp);
         return mce;
     }
 
+
+    private boolean findClassWithMatchingBasename(String nameWithoutPackage) {
+        // For performance reasons test against classNamePattern first
+        if (classNamePattern != null && classNamePattern.matcher(nameWithoutPackage).matches())
{
+            return true;
+        }
+
+        if (classesToNewify != null) {
+            @SuppressWarnings("unchecked") final List<ClassExpression> classes = (List)
classesToNewify.getExpressions();
+            for (ClassExpression ce : classes) {
+                if (ce.getType().getNameWithoutPackage().equals(nameWithoutPackage)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
     private ClassNode findMatchingCandidateClass(MethodCallExpression mce) {
-        if (classesToNewify == null) return null;
-        @SuppressWarnings("unchecked")
-        List<ClassExpression> classes = (List)classesToNewify.getExpressions();
-        for (ClassExpression ce : classes) {
-            final ClassNode type = ce.getType();
-            if (type.getNameWithoutPackage().equals(mce.getMethodAsString())) {
-                return type;
+        final String methodName = mce.getMethodAsString();
+
+        if (classesToNewify != null) {
+            @SuppressWarnings("unchecked")
+            List<ClassExpression> classes = (List) classesToNewify.getExpressions();
+            for (ClassExpression ce : classes) {
+                final ClassNode type = ce.getType();
+                if (type.getNameWithoutPackage().equals(methodName)) {
+                    return type;
+                }
             }
         }
+
+        if (classNamePattern != null && classNamePattern.matcher(methodName).matches())
{
+
+            // One-time-fill inner classes lookup map
+            if (nameToInnerClassesNodesMap == null) {
+                final List<ClassNode> innerClassNodes = source.getAST().getClasses();
+                nameToInnerClassesNodesMap = new HashMap<>(innerClassNodes.size());
+                for (ClassNode type : innerClassNodes) {
+                    final String pureClassName = extractName(type.getNameWithoutPackage());
+                    final NewifyClassData classData = nameToInnerClassesNodesMap.get(pureClassName);
+                    if (classData == null) {
+                        nameToInnerClassesNodesMap.put(pureClassName, new NewifyClassData(pureClassName,
type));
+                    } else {
+                        // If class name is looked up below, additional types will be used
in error message
+                        classData.addAdditionalType(type);
+                    }
+                }
+            }
+
+            // Inner classes
+            final NewifyClassData innerTypeClassData = nameToInnerClassesNodesMap.get(methodName);
+            if (innerTypeClassData != null) {
+                if (innerTypeClassData.types != null) {
+                    addError("Inner class name lookup is ambiguous between the following
classes: " + DefaultGroovyMethods.join(innerTypeClassData.types, ", ") + ". Use new keyword
and qualify name to break ambiguity.", mce);
+                    return null;
+                }
+                return innerTypeClassData.type;
+            }
+
+            // Imported classes
+            final ClassNode importedType = source.getAST().getImportType(methodName);
+            if (importedType != null) {
+                return importedType;
+            }
+
+            // Global classes
+            final ClassNode globalType = nameToGlobalClassesNodesMap.get(methodName);
+            if (globalType != null) {
+                return globalType;
+            }
+        }
+
         return null;
     }
 
@@ -321,4 +480,26 @@ public class NewifyASTTransformation extends ClassCodeExpressionTransformer
impl
     protected SourceUnit getSourceUnit() {
         return source;
     }
+
+
+    private static class NewifyClassData {
+        final String name;
+        final ClassNode type;
+        List<ClassNode> types = null;
+
+        public NewifyClassData(final String name, final ClassNode type) {
+            this.name = name;
+            this.type = type;
+        }
+
+        public void addAdditionalType(final ClassNode additionalType) {
+            if (types == null) {
+                types = new LinkedList<>();
+                types.add(type);
+            }
+            types.add(additionalType);
+        }
+    }
+
+
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/e91e7365/src/test/org/codehaus/groovy/transform/NewifyTransformBlackBoxTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/NewifyTransformBlackBoxTest.groovy b/src/test/org/codehaus/groovy/transform/NewifyTransformBlackBoxTest.groovy
new file mode 100644
index 0000000..cecd483
--- /dev/null
+++ b/src/test/org/codehaus/groovy/transform/NewifyTransformBlackBoxTest.groovy
@@ -0,0 +1,593 @@
+/*
+ *  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
+import java.util.Map.Entry
+
+/**
+ * Tests for the {@code @Newify} AST transform.
+ */
+@RunWith(JUnit4)
+class NewifyTransformBlackBoxTest extends CompilableTestSupport {
+
+  @Test
+  void testNewifyWithoutNamePattern() {
+    final String classPart = """  
+            final a = A('XyZ')
+            String foo(final x = null) { x?.toString() }
+        """
+    final script = newifyTestScript(true, [value: "[A]"], classPart, "final foo = new $newifyTestClassName();
foo.foo()")
+    println script
+    assert script.contains('@Newify')
+    assertScript(script)
+  }
+
+  @Test
+  void testNewifyWithoutNamePatternFails() {
+    final String classPart = classCode([
+        "final a = A('XyZ')",
+        "final ab0 = new AB('XyZ')",
+        "final ab1 = AB('XyZ')",
+        "String foo(final x = null) { x?.toString() }"
+    ])
+
+    final script0 = newifyTestScript(true, [value: "[A,AB]"], classPart, "final foo = new
$newifyTestClassName(); foo.foo()")
+    final script1 = newifyTestScript(true, [value: "[A]"], classPart, "final foo = new $newifyTestClassName();
foo.foo()")
+
+    assertScript(script0)
+
+    final result = shouldNotCompile(script1)
+    assert result.contains("Cannot find matching method NewifyFoo#AB(java.lang.String)")
+  }
+
+
+  @Test
+  void testRegularClassNewifyWithNamePattern() {
+    final String script = """
+              import groovy.transform.Canonical
+              import groovy.transform.CompileStatic
+              import groovy.lang.Newify
+              import groovy.transform.ASTTest
+              import java.lang.StringBuilder
+              import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+  
+              @Canonical class TheClass { String classField }            
+   
+              @Newify(pattern=/[A-Z][A-Za-z0-9_]+/)
+              @CompileStatic
+              def newTheClassField() {
+                final sb = StringBuilder(13)
+                sb.append("abc"); sb.append("_")
+                sb.append("123"); sb.append("_")
+                sb.append(sb.capacity())
+                return sb
+              }
+              
+               newTheClassField()
+          """
+
+    println "script=|$script|"
+    final result = evalScript(script)
+    println "result=$result"
+    assert result instanceof java.lang.StringBuilder
+    assert result.toString() == 'abc_123_13'
+  }
+
+
+  @Test
+  void testInnerScriptClassNewifyWithNamePattern() {
+    final String script = """
+              import groovy.transform.Canonical
+              import groovy.transform.CompileStatic
+              import groovy.lang.Newify
+              import groovy.transform.ASTTest
+              import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+  
+              @Canonical class A { String a }            
+              @Canonical class AB { String a; String b }            
+              @Canonical class ABC { String a; String b; String c }            
+               
+              @Newify(pattern=/[A-Z].*/)
+              @CompileStatic
+              def createClassList() {
+                final l = [ A('2018-04-08'), AB("I am", "class AB"), ABC("A","B","C") ]
+                [ l.collect { it.getClass().getCanonicalName() }, l.collect { it.toString()
} ]
+              }
+              
+              createClassList()
+          """
+
+    println "script=|$script|"
+    final List resultList = (List) evalScript(script)
+    println "result=$resultList"
+
+    assert resultList[0] == ['A', 'AB', 'ABC']
+    assert resultList[1] == ['A(2018-04-08)', 'AB(I am, class AB)', 'ABC(A, B, C)']
+  }
+
+
+  @Test
+  void testInnerClassesNewifyWithNamePattern() {
+    final String script = """           
+        import groovy.transform.Canonical
+        import groovy.transform.CompileStatic
+        import groovy.lang.Newify
+        import groovy.transform.ASTTest
+        import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+  
+        @Newify(pattern=/[A-Z].*/)
+        class Foo {
+          @Canonical class A { String a }            
+          @Canonical class AB { String a; String b }            
+          @Canonical class ABC { String a; String b; String c }            
+           
+          List createClassList() {
+            final l = [ A('2018-04-08'), AB("I am", "class AB"), ABC("A","B","C") ]
+            //final l = [ A(this, '2018-04-08'), AB(this, "I am", "class AB"), ABC(this,
"A","B","C") ]
+            [ l.collect { it.getClass().getCanonicalName() }, l.collect { it.toString() }
]
+          }
+        }
+        
+        final Foo foo = new Foo()
+        foo.createClassList()
+      """
+
+    println "script=|$script|"
+    final List resultList = (List) evalScript(script)
+    println "result=$resultList"
+
+    assert resultList[0] == ['Foo.A', 'Foo.AB', 'Foo.ABC']
+    assert resultList[1] == ['Foo$A(2018-04-08)', 'Foo$AB(I am, class AB)', 'Foo$ABC(A, B,
C)']
+  }
+
+
+  @Test
+  void testInnerStaticClassesNewifyWithNamePattern() {
+    final String script = """
+          import groovy.transform.Canonical
+          import groovy.transform.CompileStatic
+          import groovy.lang.Newify
+          import groovy.transform.ASTTest
+          import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+  
+          @Newify(pattern=/[A-Z].*/)
+          class Foo {
+            @Canonical static class A { String a }            
+            @Canonical static class AB { String a; String b }            
+            @Canonical static class ABC { String a; String b; String c }            
+             
+            List createClassList() {
+              final l = [ A('2018-04-08'), AB("I am", "class AB"), ABC("A","B","C") ]
+              [ l.collect { it.getClass().getCanonicalName() }, l.collect { it.toString()
} ]
+            }
+          }
+          
+          final Foo foo = new Foo()
+          foo.createClassList()
+      """
+
+    println "script=|$script|"
+    final List resultList = (List) evalScript(script)
+    println "result=$resultList"
+
+    assert resultList[0] == ['Foo.A', 'Foo.AB', 'Foo.ABC']
+    assert resultList[1] == ['Foo$A(2018-04-08)', 'Foo$AB(I am, class AB)', 'Foo$ABC(A, B,
C)']
+  }
+
+
+  @Test
+  void testAmbiguousInnerStaticClassesNewifyWithNamePatternFails() {
+    final String script = """
+          import groovy.transform.CompileStatic
+          import groovy.lang.Newify
+          import groovy.transform.ASTTest
+          import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+  
+          @Newify(pattern=/[A-Z].*/)
+          class Foo {
+            static class Foo {
+              static class Foo { }
+            }
+            List createClassList() {
+              final l = [ new Foo(), new Foo.Foo.Foo(), Foo() ]
+              [ l.collect { it.getClass().getCanonicalName() }, l.collect { it.toString()
} ]
+            }
+          }
+          
+          final Foo foo = new Foo()
+          foo.createClassList()
+      """
+
+    println "script=|$script|"
+
+    final String result = shouldNotCompile(script)
+    assert result ==~ '(?s).*Inner class name lookup is ambiguous between the following classes:
Foo, Foo\\$Foo, Foo\\$Foo\\$Foo\\..*'
+  }
+
+
+  @Test
+  void testImportedClassesNewifyWithNamePattern() {
+    final String script = """
+        import groovy.transform.Canonical
+        import groovy.transform.CompileStatic
+        import groovy.lang.Newify
+        import groovy.transform.ASTTest
+        import java.lang.StringBuilder
+        import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+
+        @Canonical class A { String a }            
+        @Canonical class AB { String a; String b }            
+        @Canonical class ABC { String a; String b; String c }            
+         
+        @Newify(pattern=/[A-Z][A-Za-z0-9_]*/)
+        @CompileStatic
+        def createClassList() {
+          final l = [ A('2018-04-08'), StringBuilder('*lol*'), AB("I am", "class AB"), ABC("A","B","C")
]
+          [ l.collect { it.getClass().getCanonicalName() }, l.collect { it.toString() } ]
+        }
+        
+        createClassList()
+      """
+
+    println "script=|$script|"
+    final List resultList = (List) evalScript(script)
+    println "result=$resultList"
+
+    assert resultList[0] == ['A', 'java.lang.StringBuilder', 'AB', 'ABC']
+    assert resultList[1] == ['A(2018-04-08)', '*lol*', 'AB(I am, class AB)', 'ABC(A, B, C)']
+  }
+
+
+  @Test
+  void testAlwaysExistingClassesNewifyWithNamePattern() {
+    final String script = """
+              import groovy.transform.Canonical
+              import groovy.transform.CompileStatic
+              import groovy.lang.Newify
+              import groovy.transform.ASTTest
+              import java.lang.StringBuilder
+              import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+  
+              @Canonical class A { String a }            
+              @Canonical class AB { String a; String b }            
+              @Canonical class ABC { String a; String b; String c }            
+               
+              @Newify(pattern=/[A-Z][A-Za-z0-9_]*/)
+              @CompileStatic
+              def createClassList() {
+                final l = [ A('2018-04-08'), StringBuilder('*lol*'), AB("I am", "class AB"),
ABC("A","B","C"), Object() ]
+                [ l.collect { it.getClass().getName() }, l.collect { it.toString().replaceAll(/@[a-f0-9]+\\b/,'')
} ]
+              }
+              
+              createClassList()
+          """
+
+    println "script=|$script|"
+    final List resultList = (List) evalScript(script)
+    println "result=$resultList"
+
+    assert resultList[0] == ['A', 'java.lang.StringBuilder', 'AB', 'ABC', 'java.lang.Object']
+    assert resultList[1] == ['A(2018-04-08)', '*lol*', 'AB(I am, class AB)', 'ABC(A, B, C)',
'java.lang.Object']
+  }
+
+
+  @Test
+  void testNewifyWithNamePatternMixed() {
+    final String script = """
+              import groovy.transform.Canonical
+              import groovy.transform.CompileStatic
+              import groovy.lang.Newify
+              import groovy.transform.ASTTest
+              import java.lang.StringBuilder
+              import groovy.lang.Binding
+              import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+  
+              @Canonical class A { String a }            
+              @Canonical class AB { String a; String b }            
+              @Canonical class ABC { String a; String b; String c }            
+               
+              @Newify(pattern=/[A-Z][A-Za-z0-9_]*/)
+              @CompileStatic
+              def createClassList() {
+                final l = [ 
+                  A('2018-04-08'), StringBuilder('*lol*'), AB("I am", "class AB"), ABC("A","B","C"),
Object(),
+                  Reference(), Binding(), Double(123.456d), Integer(987), BigInteger('987654321',10),
+                  BigDecimal('1234.5678')
+                ]
+                [ l.collect { it.getClass().getName() }, l.collect { it.toString().replaceAll(/@[a-f0-9]+\\b/,'')
} ]
+              }
+              
+              createClassList()
+          """
+
+    println "script=|$script|"
+    final List resultList = (List) evalScript(script)
+    println "result=$resultList"
+
+    assert resultList[0] == [
+        'A', 'java.lang.StringBuilder', 'AB', 'ABC', 'java.lang.Object',
+        'groovy.lang.Reference', 'groovy.lang.Binding', 'java.lang.Double', 'java.lang.Integer',
'java.math.BigInteger',
+        'java.math.BigDecimal'
+    ]
+    assert resultList[1] == [
+        'A(2018-04-08)', '*lol*', 'AB(I am, class AB)', 'ABC(A, B, C)', 'java.lang.Object',
+        'groovy.lang.Reference', 'groovy.lang.Binding', '123.456', '987', '987654321',
+        '1234.5678'
+    ]
+  }
+
+
+  @Test
+  void testAliasImportedClassesNewifyWithNamePattern() {
+    final String script = """
+        import groovy.lang.Newify
+        import groovy.transform.ASTTest
+        import java.lang.StringBuilder as WobblyOneDimensionalObjectBuilda
+        import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+
+        @Newify(pattern=/[A-Z][A-Za-z0-9_]*/)
+        def createClassList() {
+          final l = [ WobblyOneDimensionalObjectBuilda('Discrete Reality') ]
+          [ l.collect { it.getClass().getCanonicalName() }, l.collect { it.toString() } ]
+        }
+        
+        createClassList()
+      """
+
+    println "script=|$script|"
+    final List resultList = (List) evalScript(script)
+    println "result=$resultList"
+
+    assert resultList[0] == ['java.lang.StringBuilder']
+    assert resultList[1] == ['Discrete Reality']
+  }
+
+
+  @Test
+  void testAliasShadowededImportedClassesNewifyWithNamePatternFails() {
+    final String script = """   
+        import groovy.transform.CompileStatic
+        import groovy.lang.Newify
+        import groovy.transform.ASTTest
+        import java.lang.StringBuilder as WobblyOneDimensionalObjectBuilda
+        import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+
+        @CompileStatic
+        @Newify(pattern=/[A-Z][A-Za-z0-9_]*/)
+        def createClassList() {
+          final l = [ WobblyOneDimensionalObjectBuilda('Discrete Reality'), StringBuilder('Quantum
Loops') ]
+          [ l.collect { it.getClass().getCanonicalName() }, l.collect { it.toString() } ]
+        }
+        
+        createClassList()
+      """
+
+    println "script=|$script|"
+
+    final String result = shouldNotCompile(script)
+    assert result ==~ /(?s).*\[Static type checking] - Cannot find matching method TestScript[A-Za-z0-9]*#StringBuilder\(java\.lang\.String\).*/
+  }
+
+
+  @Test
+  void testInvalidNamePatternNewifyWithNamePatternFails() {
+    final String script = """   
+        import groovy.transform.CompileStatic
+        import groovy.lang.Newify
+        import groovy.transform.ASTTest
+        import java.lang.StringBuilder as WobblyOneDimensionalObjectBuilda
+        import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+
+        @CompileStatic
+        @Newify(pattern=/[A-/)
+        def createClassList() {
+          final l = [ WobblyOneDimensionalObjectBuilda('Discrete Reality'), StringBuilder('Quantum
Loops') ]
+          [ l.collect { it.getClass().getCanonicalName() }, l.collect { it.toString() } ]
+        }
+        
+        createClassList()
+      """
+
+    println "script=|$script|"
+
+    final String result = shouldNotCompile(script)
+    assert result ==~ /(?s).*Invalid class name pattern: Illegal character range near index
3.*/
+  }
+
+
+  @Test
+  void testStaticallyAndDynamicallyCompiledMixedClassesNewifyWithNamePattern() {
+    final List<Boolean> compileStaticFlags = [true]
+    assertMixedClassesNewifyWithNamePatternResult("@Newify(pattern=/[A-Z].*/)", compileStaticFlags,
+        ['Foo.A', 'Foo.AB', 'Foo.ABC'], ['Foo$A(2018-04-08)', 'Foo$AB(I am, class AB)', 'Foo$ABC(A,
B, C)']
+    )
+  }
+
+  @Test
+  void testStaticallyCompiledMixedClassesNoNewify() {
+    assertMixedClassesNewifyWithNamePatternFails("", [true], standardCompileStaticErrorMsg)
+  }
+
+  @Test
+  void testStaticallyCompiledMixedClassesNewifyWithNamePattern() {
+    assertMixedClassesNewifyWithNamePatternFails("@Newify(pattern=/XXX/)", [true], standardCompileStaticErrorMsg)
+  }
+
+  @Test
+  void testDynmaicallyCompiledMixedClassesNoNewify() {
+    assertMixedClassesNewifyWithNamePatternFails("", [false], standardCompileDynamiccErrorMsg)
+  }
+
+  @Test
+  void testDynmaicallyCompiledMixedClassesNewifyWithNamePattern() {
+    assertMixedClassesNewifyWithNamePatternFails("@Newify(pattern=/XXX/)", [false], standardCompileDynamiccErrorMsg)
+  }
+
+
+  @Test
+  void testExtractName() {
+    ['', 'A', 'Bc', 'DEF'].each { String s ->
+      assertExtractName(s, s)
+      assertExtractName("\$$s", s)
+      assertExtractName("A\$$s", s)
+      assertExtractName("Foo\$$s", s)
+      assertExtractName("Foo\$Foo\$$s", s)
+      assertExtractName("A\$AB\$ABC\$$s", s)
+    }
+  }
+
+
+  String getStandardCompileDynamiccErrorMsg() {
+    "No signature of method: Foo.A() is applicable for argument types: (String) values: [2018-04-08]"
+  }
+
+  String getStandardCompileStaticErrorMsg() {
+    "[Static type checking] - Cannot find matching method Foo#A(java.lang.String)."
+  }
+
+  void assertMixedClassesNewifyWithNamePatternFails(
+      final String newifyAnnotation, final List<Boolean> compileStaticFlags, final
String errorMsgStartsWith) {
+    try {
+      mixedClassesNewifyWithNamePattern(newifyAnnotation, compileStaticFlags)
+    }
+    catch(Exception e) {
+      assert e.message.contains(errorMsgStartsWith)
+    }
+  }
+
+  void assertMixedClassesNewifyWithNamePatternResult(
+      final String newifyAnnotation,
+      final List<Boolean> compileStaticFlags, final List<String> classNameList,
final List<String> resultList) {
+    final List list = mixedClassesNewifyWithNamePattern(newifyAnnotation, compileStaticFlags)
+    assert list[0] == classNameList
+    assert list[1] == resultList
+  }
+
+  List mixedClassesNewifyWithNamePattern(final String newifyAnnotation, final List<Boolean>
compileStaticFlags) {
+
+    int iCompileStaticOrDynamic = 0
+    final Closure<String> compileStaticOrDynamicCls = {
+      compileStaticFlags[iCompileStaticOrDynamic++] ? "@CompileStatic" : "@CompileDynamic"
+    }
+
+    final String script = """
+            import groovy.transform.Canonical
+            import groovy.transform.CompileStatic
+            import groovy.transform.CompileDynamic
+            import groovy.lang.Newify
+            import java.lang.StringBuilder
+            import groovy.lang.Binding
+            import groovy.transform.ASTTest
+            import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+    
+            $newifyAnnotation
+            ${compileStaticOrDynamicCls()}
+            class Foo {
+              @Canonical static class A { String a }            
+              @Canonical static class AB { String a; String b }            
+              @Canonical static class ABC { String a; String b; String c }            
+               
+              List createClassList() {
+                final l = [ A('2018-04-08'), AB("I am", "class AB"), ABC("A","B","C") ]
+                [ l.collect { it.getClass().getCanonicalName() }, l.collect { it.toString()
} ]
+              }
+            }
+            
+            final Foo foo = new Foo()
+            foo.createClassList()
+        """
+
+    println "script=|$script|"
+    final List resultList = (List) evalScript(script)
+    println "result=$resultList"
+
+    return resultList
+  }
+
+
+  void assertExtractName(final String s, final String expected) {
+    final String result = NewifyASTTransformation.extractName(s)
+    println "|$s| -> |$result|"
+    assert result == expected
+  }
+
+
+  String classCode(final List<String> lines) { code(lines, 1) }
+
+  String scriptCode(final List<String> lines) { code(lines, 0) }
+
+  String code(final List<String> lines, final int indent = 0) {
+    lines.collect { "${'\t' * indent}${it};" }.join('\n')
+  }
+
+  String newifyTestScript(
+      final boolean hasAnnotation,
+      final Map<String, Object> annotationParameters,
+      final String classPart, final String scriptPart = '') {
+    assert !hasAnnotation || (annotationParameters != null); assert classPart
+    final String annotationParametersTerm = annotationParameters ? "(${annotationParameters.collect
{ final Entry<String, Object> e -> "$e.key=$e.value" }.join(', ')})" : ''
+    final String script = """
+            import groovy.transform.Canonical
+            import groovy.transform.CompileStatic
+            import groovy.lang.Newify
+            import groovy.transform.ASTTest
+            import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+
+            @Canonical class A { String a }            
+            @Canonical class AB { String a; String b }            
+            @Canonical class ABC { String a; String b; String c }            
+
+            @CompileStatic
+            ${hasAnnotation ? "@Newify${annotationParametersTerm}" : ''}
+            class $newifyTestClassName {
+                $classPart
+            } 
+
+            $scriptPart
+        """
+    return script
+  }
+
+  String getNewifyTestClassName() {
+    'NewifyFoo'
+  }
+
+
+  static def evalScript(final String script) throws Exception {
+    GroovyShell shell = new GroovyShell();
+    shell.evaluate(script);
+  }
+
+
+  static Throwable compileShouldThrow(final String script, final String testClassName) {
+    try {
+      final GroovyClassLoader gcl = new GroovyClassLoader()
+      gcl.parseClass(script, testClassName)
+    }
+    catch(Throwable throwable) {
+      return throwable
+    }
+    throw new Exception("Script was expected to throw here!")
+  }
+
+}


Mime
View raw message