From commits-return-6361-archive-asf-public=cust-asf.ponee.io@groovy.apache.org Mon Apr 23 01:32:16 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id AFD7D180625 for ; Mon, 23 Apr 2018 01:32:14 +0200 (CEST) Received: (qmail 93302 invoked by uid 500); 22 Apr 2018 23:32:13 -0000 Mailing-List: contact commits-help@groovy.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@groovy.apache.org Delivered-To: mailing list commits@groovy.apache.org Received: (qmail 93293 invoked by uid 99); 22 Apr 2018 23:32:13 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Sun, 22 Apr 2018 23:32:13 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 45657F17B1; Sun, 22 Apr 2018 23:32:13 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: sunlan@apache.org To: commits@groovy.apache.org Message-Id: <16ae88d0243548b4a573044876fc1aa3@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: groovy git commit: Newify pattern support for pull(closes #689 #686) Date: Sun, 22 Apr 2018 23:32:13 +0000 (UTC) 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 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. *

- * 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. + *

+ * {@literal @Newify} allows you to write code snippets like this ("Python-style"): *

  * {@code @Newify([Tree,Leaf])} class MyTreeProcessor {
  *     def myTree = Tree(Tree(Leaf("A"), Leaf("B")), Leaf("C"))
  *     def process() { ... }
  * }
  * 
+ *
+ * {@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)
+ * }
+ * 
* or this ("Ruby-style"): *
  * {@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.
  * 

- * 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 Leaf("A") would be transformed to * new Leaf("A") but x.Leaf("A") 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 nameToGlobalClassesNodesMap; + private Map 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(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 seen = new HashSet(); - @SuppressWarnings("unchecked") - final List classes = (List)list.getExpressions(); + @SuppressWarnings("unchecked") final List 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 classes = (List)list.getExpressions(); + @SuppressWarnings("unchecked") final List 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 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 argExpList = argsListExp.getExpressions(); + final VariableExpression thisVarExp = new VariableExpression("this"); + + final List expressionsWithThis = new ArrayList(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 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 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 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 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 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 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 compileStaticFlags, final String errorMsgStartsWith) { + try { + mixedClassesNewifyWithNamePattern(newifyAnnotation, compileStaticFlags) + } + catch(Exception e) { + assert e.message.contains(errorMsgStartsWith) + } + } + + void assertMixedClassesNewifyWithNamePatternResult( + final String newifyAnnotation, + final List compileStaticFlags, final List classNameList, final List resultList) { + final List list = mixedClassesNewifyWithNamePattern(newifyAnnotation, compileStaticFlags) + assert list[0] == classNameList + assert list[1] == resultList + } + + List mixedClassesNewifyWithNamePattern(final String newifyAnnotation, final List compileStaticFlags) { + + int iCompileStaticOrDynamic = 0 + final Closure 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 lines) { code(lines, 1) } + + String scriptCode(final List lines) { code(lines, 0) } + + String code(final List lines, final int indent = 0) { + lines.collect { "${'\t' * indent}${it};" }.join('\n') + } + + String newifyTestScript( + final boolean hasAnnotation, + final Map annotationParameters, + final String classPart, final String scriptPart = '') { + assert !hasAnnotation || (annotationParameters != null); assert classPart + final String annotationParametersTerm = annotationParameters ? "(${annotationParameters.collect { final Entry 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!") + } + +}