You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2018/04/22 23:32:13 UTC
groovy git commit: Newify pattern support for pull(closes #689 #686)
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 <31...@users.noreply.github.com>
Authored: Mon Apr 23 00:31:20 2018 +0200
Committer: sunlan <su...@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!")
+ }
+
+}