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 2020/05/30 15:18:38 UTC

[groovy] 09/11: return unmodifiable collections and other minor refactoring

This is an automated email from the ASF dual-hosted git repository.

sunlan pushed a commit to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 3f2f3d4b0680a03a833215429cea7e52196a27e1
Author: Eric Milles <er...@thomsonreuters.com>
AuthorDate: Fri May 29 10:02:06 2020 -0500

    return unmodifiable collections and other minor refactoring
    
    (cherry picked from commit 435280d495242cbbbd7148a2b0d72cdc6ac4d35f)
---
 .../java/org/codehaus/groovy/ast/ImportNode.java   |  65 +++---
 .../java/org/codehaus/groovy/ast/ModuleNode.java   | 231 ++++++++++-----------
 .../codehaus/groovy/control/CompilationUnit.java   |  10 +-
 .../{Groovy4193Bug.groovy => Groovy4193.groovy}    |  19 +-
 4 files changed, 161 insertions(+), 164 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/ast/ImportNode.java b/src/main/java/org/codehaus/groovy/ast/ImportNode.java
index df249bb..d5d3f50 100644
--- a/src/main/java/org/codehaus/groovy/ast/ImportNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ImportNode.java
@@ -18,12 +18,12 @@
  */
 package org.codehaus.groovy.ast;
 
-import org.objectweb.asm.Opcodes;
+import static java.util.Objects.requireNonNull;
 
 /**
- * Represents an import statement of a single class
+ * Represents an import statement.
  */
-public class ImportNode extends AnnotatedNode implements Opcodes {
+public class ImportNode extends AnnotatedNode {
 
     private final ClassNode type;
     private final String alias;
@@ -33,13 +33,13 @@ public class ImportNode extends AnnotatedNode implements Opcodes {
     private final boolean isStatic;
 
     /**
-     * Represent an import of an entire package, i.e.&#160;import package.Classname
+     * An import of a single type, i.e.&#160;{@code import pack.Type} or {@code import pack.Type as Alias}
      *
-     * @param type  the referenced class
+     * @param type  the type reference
      * @param alias optional alias
      */
-    public ImportNode(ClassNode type, String alias) {
-        this.type = type;
+    public ImportNode(final ClassNode type, final String alias) {
+        this.type = requireNonNull(type);
         this.alias = alias;
         this.isStar = false;
         this.isStatic = false;
@@ -48,26 +48,26 @@ public class ImportNode extends AnnotatedNode implements Opcodes {
     }
 
     /**
-     * Represent an import of an entire package, i.e.&#160;import package.*
+     * An import of all types in a package, i.e.&#160;{@code import pack.*}
      *
      * @param packageName the name of the package
      */
-    public ImportNode(String packageName) {
+    public ImportNode(final String packageName) {
         this.type = null;
         this.alias = null;
         this.isStar = true;
         this.isStatic = false;
-        this.packageName = packageName;
+        this.packageName = requireNonNull(packageName);
         this.fieldName = null;
     }
 
     /**
-     * Represent a static import of a Class, i.e.&#160;import static package.Classname.*
+     * An import of all static members of a type, i.e.&#160;{@code import static pack.Type.*}
      *
-     * @param type the referenced class
+     * @param type the type reference
      */
-    public ImportNode(ClassNode type) {
-        this.type = type;
+    public ImportNode(final ClassNode type) {
+        this.type = requireNonNull(type);
         this.alias = null;
         this.isStar = true;
         this.isStatic = true;
@@ -76,24 +76,25 @@ public class ImportNode extends AnnotatedNode implements Opcodes {
     }
 
     /**
-     * Represent a static import of a field or method, i.e.&#160;import static package.Classname.name
+     * An import of a static field or method of a type, i.e.&#160;{@code import static pack.Type.name} or {@code import static pack.Type.name as alias}
      *
-     * @param type      the referenced class
+     * @param type      the type reference
      * @param fieldName the field name
      * @param alias     optional alias
      */
-    public ImportNode(ClassNode type, String fieldName, String alias) {
-        this.type = type;
+    public ImportNode(final ClassNode type, final String fieldName, final String alias) {
+        this.type = requireNonNull(type);
         this.alias = alias;
         this.isStar = false;
         this.isStatic = true;
         this.packageName = null;
-        this.fieldName = fieldName;
+        this.fieldName = requireNonNull(fieldName);
     }
 
     /**
      * @return the text display of this import
      */
+    @Override
     public String getText() {
         String typeName = getClassName();
         if (isStar && !isStatic) {
@@ -114,14 +115,6 @@ public class ImportNode extends AnnotatedNode implements Opcodes {
         return "import " + typeName + " as " + alias;
     }
 
-    public String getPackageName() {
-        return packageName;
-    }
-
-    public String getFieldName() {
-        return fieldName;
-    }
-
     public boolean isStar() {
         return isStar;
     }
@@ -134,15 +127,23 @@ public class ImportNode extends AnnotatedNode implements Opcodes {
         return alias;
     }
 
-    public ClassNode getType() {
-        return type;
+    public String getClassName() {
+        return (type == null ? null : type.getName());
     }
 
-    public String getClassName() {
-        return type == null ? null : type.getName();
+    public String getFieldName() {
+        return fieldName;
+    }
+
+    public String getPackageName() {
+        return packageName;
     }
 
-    public void visit(GroovyCodeVisitor visitor) {
+    public ClassNode getType() {
+        return type;
     }
 
+    @Override
+    public void visit(final GroovyCodeVisitor visitor) {
+    }
 }
diff --git a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
index f305aea..7fcdfc6 100644
--- a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
@@ -19,13 +19,7 @@
 package org.codehaus.groovy.ast;
 
 import groovy.lang.Binding;
-import org.codehaus.groovy.ast.expr.ArgumentListExpression;
-import org.codehaus.groovy.ast.expr.ClassExpression;
-import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
-import org.codehaus.groovy.ast.expr.MethodCallExpression;
-import org.codehaus.groovy.ast.expr.VariableExpression;
 import org.codehaus.groovy.ast.stmt.BlockStatement;
-import org.codehaus.groovy.ast.stmt.ExpressionStatement;
 import org.codehaus.groovy.ast.stmt.Statement;
 import org.codehaus.groovy.classgen.GeneratorContext;
 import org.codehaus.groovy.control.SourceUnit;
@@ -45,6 +39,15 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+
 /**
  * Represents a module, which consists typically of a class declaration
  * but could include some imports, some statements and multiple classes
@@ -67,24 +70,15 @@ public class ModuleNode extends ASTNode implements Opcodes {
     private ClassNode scriptDummy;
     private String mainClassName;
     private final BlockStatement statementBlock = new BlockStatement();
-    private final Parameter[] SCRIPT_CONTEXT_CTOR = {new Parameter(ClassHelper.BINDING_TYPE, "context")};
 
-    public ModuleNode (SourceUnit context ) {
+    public ModuleNode(final SourceUnit context) {
         this.context = context;
     }
 
-    public ModuleNode (CompileUnit unit) {
+    public ModuleNode(final CompileUnit unit) {
         this.unit = unit;
     }
 
-    public BlockStatement getStatementBlock() {
-        return statementBlock;
-    }
-
-    public List<MethodNode> getMethods() {
-        return methods;
-    }
-
     public List<ClassNode> getClasses() {
         if (createClassForStatements && (!statementBlock.isEmpty() || !methods.isEmpty() || isPackageInfo())) {
             ClassNode mainClass = createStatementsClass();
@@ -94,11 +88,11 @@ public class ModuleNode extends ASTNode implements Opcodes {
             mainClass.setModule(this);
             addToCompileUnit(mainClass);
         }
-        return classes;
+        return /*Collections.unmodifiableList(*/classes/*)*/; // modified by MacroClassTransform
     }
 
-    private boolean isPackageInfo() {
-        return context != null && context.getName() != null && context.getName().endsWith("package-info.groovy");
+    public List<MethodNode> getMethods() {
+        return Collections.unmodifiableList(methods);
     }
 
     public List<ImportNode> getImports() {
@@ -106,53 +100,63 @@ public class ModuleNode extends ASTNode implements Opcodes {
     }
 
     public List<ImportNode> getStarImports() {
-        return starImports;
+        return Collections.unmodifiableList(starImports);
+    }
+
+    public Map<String, ImportNode> getStaticImports() {
+        return Collections.unmodifiableMap(staticImports);
+    }
+
+    public Map<String, ImportNode> getStaticStarImports() {
+        return Collections.unmodifiableMap(staticStarImports);
     }
 
     /**
      * @param alias the name of interest
-     * @return the class node for the given alias or null if none is available
+     * @return the import type for the given alias or null if none is available
      */
-    public ClassNode getImportType(String alias) {
-        ImportNode importNode = imports.get(alias);
-        return importNode == null ? null : importNode.getType();
+    public ClassNode getImportType(final String alias) {
+        ImportNode node = getImport(alias);
+        return (node != null ? node.getType() : null);
     }
 
     /**
      * @param alias the name of interest
      * @return the import node for the given alias or null if none is available
      */
-    public ImportNode getImport(String alias) {
+    public ImportNode getImport(final String alias) {
         return imports.get(alias);
     }
 
-    public void addImport(String alias, ClassNode type) {
+    public void addImport(final String alias, final ClassNode type) {
         addImport(alias, type, Collections.emptyList());
     }
 
-    public void addImport(String alias, ClassNode type, List<AnnotationNode> annotations) {
+    public void addImport(final String alias, final ClassNode type, final List<AnnotationNode> annotations) {
         ImportNode importNode = new ImportNode(type, alias);
-        imports.put(alias, importNode);
         importNode.addAnnotations(annotations);
+        imports.put(alias, importNode);
+
         storeLastAddedImportNode(importNode);
     }
 
-    public void addStarImport(String packageName) {
+    public void addStarImport(final String packageName) {
         addStarImport(packageName, Collections.emptyList());
     }
 
-    public void addStarImport(String packageName, List<AnnotationNode> annotations) {
+    public void addStarImport(final String packageName, final List<AnnotationNode> annotations) {
         ImportNode importNode = new ImportNode(packageName);
         importNode.addAnnotations(annotations);
         starImports.add(importNode);
+
         storeLastAddedImportNode(importNode);
     }
 
-    public void addStaticImport(ClassNode type, String fieldName, String alias) {
+    public void addStaticImport(final ClassNode type, final String fieldName, final String alias) {
         addStaticImport(type, fieldName, alias, Collections.emptyList());
     }
 
-    public void addStaticImport(ClassNode type, String fieldName, String alias, List<AnnotationNode> annotations) {
+    public void addStaticImport(final ClassNode type, final String fieldName, final String alias, final List<AnnotationNode> annotations) {
         ImportNode node = new ImportNode(type, fieldName, alias);
         node.addAnnotations(annotations);
         ImportNode prev = staticImports.put(alias, node);
@@ -160,43 +164,47 @@ public class ModuleNode extends ASTNode implements Opcodes {
             staticImports.put(prev.toString(), prev);
             staticImports.put(alias, staticImports.remove(alias));
         }
+
         storeLastAddedImportNode(node);
     }
 
-    public void addStaticStarImport(String name, ClassNode type) {
+    public void addStaticStarImport(final String name, final ClassNode type) {
         addStaticStarImport(name, type, Collections.emptyList());
     }
 
-    public void addStaticStarImport(String name, ClassNode type, List<AnnotationNode> annotations) {
+    public void addStaticStarImport(final String name, final ClassNode type, final List<AnnotationNode> annotations) {
         ImportNode node = new ImportNode(type);
         node.addAnnotations(annotations);
         staticStarImports.put(name, node);
+
         storeLastAddedImportNode(node);
     }
 
-    public void addStatement(Statement node) {
+    public void addStatement(final Statement node) {
         statementBlock.addStatement(node);
     }
 
-    public void addClass(ClassNode node) {
-        if(classes.isEmpty()) mainClassName = node.getName();
+    public void addClass(final ClassNode node) {
+        if (classes.isEmpty())
+            mainClassName = node.getName();
         classes.add(node);
         node.setModule(this);
         addToCompileUnit(node);
     }
 
-    private void addToCompileUnit(ClassNode node) {
+    private void addToCompileUnit(final ClassNode node) {
         // register the new class with the compile unit
         if (unit != null) {
             unit.addClass(node);
         }
     }
 
-    public void addMethod(MethodNode node) {
+    public void addMethod(final MethodNode node) {
         methods.add(node);
     }
 
-    public void visit(GroovyCodeVisitor visitor) {
+    @Override
+    public void visit(final GroovyCodeVisitor visitor) {
     }
 
     public String getPackageName() {
@@ -207,26 +215,20 @@ public class ModuleNode extends ASTNode implements Opcodes {
         return packageNode;
     }
 
-    // TODO don't allow override?
-    public void setPackage(PackageNode packageNode) {
-        this.packageNode = packageNode;
-    }
-
-    // TODO don't allow override?
-    public void setPackageName(String packageName) {
-        this.packageNode = new PackageNode(packageName);
+    public boolean hasPackage() {
+        return (packageNode != null);
     }
 
-    public boolean hasPackageName(){
-        return packageNode != null && packageNode.getName() != null;
+    public void setPackage(final PackageNode packageNode) {
+        this.packageNode = packageNode;
     }
 
-    public boolean hasPackage(){
-        return this.packageNode != null;
+    public boolean hasPackageName() {
+        return (packageNode != null && packageNode.getName() != null);
     }
 
-    public SourceUnit getContext() {
-        return context;
+    public void setPackageName(final String packageName) {
+        setPackage(new PackageNode(packageName));
     }
 
     /**
@@ -236,11 +238,11 @@ public class ModuleNode extends ASTNode implements Opcodes {
         if (context != null) {
             return context.getName();
         } else {
-            return this.description;
+            return description;
         }
     }
 
-    public void setDescription(String description) {
+    public void setDescription(final String description) {
         this.description = description;
     }
 
@@ -248,12 +250,20 @@ public class ModuleNode extends ASTNode implements Opcodes {
         return unit;
     }
 
-    void setUnit(CompileUnit unit) {
+    void setUnit(final CompileUnit unit) {
         this.unit = unit;
     }
 
+    public SourceUnit getContext() {
+        return context;
+    }
+
+    private boolean isPackageInfo() {
+        return context != null && context.getName() != null && context.getName().endsWith("package-info.groovy");
+    }
+
     public ClassNode getScriptClassDummy() {
-        if (scriptDummy!=null) {
+        if (scriptDummy != null) {
             setScriptBaseClassFromConfig(scriptDummy);
             return scriptDummy;
         }
@@ -282,7 +292,7 @@ public class ModuleNode extends ASTNode implements Opcodes {
         return classNode;
     }
 
-    private void setScriptBaseClassFromConfig(ClassNode cn) {
+    private void setScriptBaseClassFromConfig(final ClassNode cn) {
         String baseClassName = null;
         if (unit != null) {
             baseClassName = unit.getConfig().getScriptBaseClass();
@@ -312,15 +322,17 @@ public class ModuleNode extends ASTNode implements Opcodes {
                 "main",
                 ACC_PUBLIC | ACC_STATIC,
                 ClassHelper.VOID_TYPE,
-                new Parameter[] { new Parameter(ClassHelper.STRING_TYPE.makeArray(), "args")},
+                params(param(ClassHelper.STRING_TYPE.makeArray(), "args")),
                 ClassNode.EMPTY_ARRAY,
-                new ExpressionStatement(
-                    new MethodCallExpression(
-                        new ClassExpression(ClassHelper.make(InvokerHelper.class)),
+                stmt(
+                    callX(
+                        classX(ClassHelper.make(InvokerHelper.class)),
                         "runScript",
-                        new ArgumentListExpression(
-                                new ClassExpression(classNode),
-                                new VariableExpression("args"))))));
+                        args(classX(classNode), varX("args"))
+                    )
+                )
+            )
+        );
 
         MethodNode methodNode = new MethodNode("run", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, statementBlock);
         methodNode.setIsScriptBody();
@@ -332,39 +344,25 @@ public class ModuleNode extends ASTNode implements Opcodes {
         // A script's contextual constructor should call it's super class' contextual constructor, if it has one.
         // In practice this will always be true because currently this visitor is run before the AST transformations
         // (like @BaseScript) that could change this.  But this is cautious and anticipates possible compiler changes.
-        if (classNode.getSuperClass().getDeclaredConstructor(SCRIPT_CONTEXT_CTOR) != null) {
-            stmt = new ExpressionStatement(
-                    new ConstructorCallExpression(ClassNode.SUPER,
-                            new ArgumentListExpression(
-                                    new VariableExpression("context"))));
+        if (classNode.getSuperClass().getDeclaredConstructor(params(param(ClassHelper.BINDING_TYPE, "context"))) != null) {
+            stmt = stmt(ctorX(ClassNode.SUPER, args(varX("context"))));
         } else {
             // Fallback for non-standard base "script" classes with no context (Binding) constructor.
-            stmt = new ExpressionStatement(
-                    new MethodCallExpression(
-                            new VariableExpression("super"),
-                            "setBinding",
-                            new ArgumentListExpression(
-                                    new VariableExpression("context"))));
+            stmt = stmt(callX(varX("super"), "setBinding", args(varX("context"))));
         }
 
         classNode.addConstructor(
             ACC_PUBLIC,
-            new Parameter[] { new Parameter(ClassHelper.make(Binding.class), "context")},
+            params(param(ClassHelper.make(Binding.class), "context")),
             ClassNode.EMPTY_ARRAY,
             stmt);
 
-        for (MethodNode node : methods) {
-            int modifiers = node.getModifiers();
-            if ((modifiers & ACC_ABSTRACT) != 0) {
-                throw new RuntimeException(
-                    "Cannot use abstract methods in a script, they are only available inside classes. Method: "
-                        + node.getName());
+        for (MethodNode method : methods) {
+            if (method.isAbstract()) {
+                throw new RuntimeException("Cannot use abstract methods in a script" +
+                    ", they are only available inside classes. Method: " + method.getName());
             }
-            // br: the old logic seems to add static to all def f().... in a script, which makes enclosing
-            // inner classes (including closures) in a def function difficult. Comment it out.
-            node.setModifiers(modifiers /*| ACC_STATIC*/);
-
-            classNode.addMethod(node);
+            classNode.addMethod(method);
         }
         return classNode;
     }
@@ -372,11 +370,11 @@ public class ModuleNode extends ASTNode implements Opcodes {
     /*
      * If a main method is provided by user, account for it under run() as scripts generate their own 'main' so they can run.
      */
-    private void handleMainMethodIfPresent(List methods) {
+    private void handleMainMethodIfPresent(final List<MethodNode> methods) {
         boolean found = false;
-        for (Iterator iter = methods.iterator(); iter.hasNext();) {
-            MethodNode node = (MethodNode) iter.next();
-            if(node.getName().equals("main")) {
+        for (Iterator<MethodNode> iter = methods.iterator(); iter.hasNext();) {
+            MethodNode node = iter.next();
+            if (node.getName().equals("main")) {
                 if (node.isStatic() && node.getParameters().length == 1) {
                     boolean retTypeMatches, argTypeMatches;
                     ClassNode argType = node.getParameters()[0].getType();
@@ -385,14 +383,14 @@ public class ModuleNode extends ASTNode implements Opcodes {
                     argTypeMatches = (argType.equals(ClassHelper.OBJECT_TYPE) || argType.getName().contains("String[]"));
                     retTypeMatches = (retType == ClassHelper.VOID_TYPE || retType == ClassHelper.OBJECT_TYPE);
 
-                    if(retTypeMatches && argTypeMatches) {
-                        if(found) {
+                    if (retTypeMatches && argTypeMatches) {
+                        if (found) {
                             throw new RuntimeException("Repetitive main method found.");
                         } else {
                             found = true;
                         }
                         // if script has both loose statements as well as main(), then main() is ignored
-                        if(statementBlock.isEmpty()) {
+                        if (statementBlock.isEmpty()) {
                             addStatement(node.getCode());
                         }
                         iter.remove();
@@ -437,21 +435,20 @@ public class ModuleNode extends ASTNode implements Opcodes {
         return classes.isEmpty() && statementBlock.getStatements().isEmpty();
     }
 
-    public void sortClasses(){
+    public void sortClasses() {
         if (isEmpty()) return;
-        List<ClassNode> classes = getClasses();
-        LinkedList<ClassNode> sorted = new LinkedList<>();
-        int level=1;
-        while (!classes.isEmpty()) {
-            for (Iterator<ClassNode> cni = classes.iterator(); cni.hasNext();) {
-                ClassNode cn = cni.next();
-                ClassNode sn = cn;
-                for (int i=0; sn!=null && i<level; i++) sn = sn.getSuperClass();
-                if (sn!=null && sn.isPrimaryClassNode()) continue;
-                cni.remove();
-                sorted.addLast(cn);
+        List<ClassNode> sorted = new LinkedList<>(), todo = getClasses();
+        int level = 1;
+        while (!todo.isEmpty()) {
+            for (Iterator<ClassNode> it = todo.iterator(); it.hasNext(); ) {
+                ClassNode cn = it.next(), sc = cn;
+
+                for (int i = 0; sc != null && i < level; i += 1) sc = sc.getSuperClass();
+                if (sc != null && sc.isPrimaryClassNode()) continue;
+                sorted.add(cn);
+                it.remove();
             }
-            level++;
+            level += 1;
         }
         this.classes = sorted;
     }
@@ -460,18 +457,10 @@ public class ModuleNode extends ASTNode implements Opcodes {
         return importsResolved;
     }
 
-    public void setImportsResolved(boolean importsResolved) {
+    public void setImportsResolved(final boolean importsResolved) {
         this.importsResolved = importsResolved;
     }
 
-    public Map<String, ImportNode> getStaticImports() {
-        return staticImports;
-    }
-
-    public Map<String, ImportNode> getStaticStarImports() {
-        return staticStarImports;
-    }
-
     // This method only exists as a workaround for GROOVY-6094
     // In order to keep binary compatibility
     private void storeLastAddedImportNode(final ImportNode node) {
@@ -483,4 +472,8 @@ public class ModuleNode extends ASTNode implements Opcodes {
     public String getMainClassName() {
         return mainClassName;
     }
+
+    public BlockStatement getStatementBlock() {
+        return statementBlock;
+    }
 }
diff --git a/src/main/java/org/codehaus/groovy/control/CompilationUnit.java b/src/main/java/org/codehaus/groovy/control/CompilationUnit.java
index 02e4f1c..24d52e1 100644
--- a/src/main/java/org/codehaus/groovy/control/CompilationUnit.java
+++ b/src/main/java/org/codehaus/groovy/control/CompilationUnit.java
@@ -112,12 +112,12 @@ public class CompilationUnit extends ProcessingUnit {
         }
     }
 
-    /** Controls behavior of {@link #classgen} and other routines. */
+    /** Controls behavior of {@link #classgen()} and other routines. */
     protected boolean debug;
     /** True after the first {@link #configure(CompilerConfiguration)} operation. */
     protected boolean configured;
 
-    /** A callback for use during {@link #classgen} */
+    /** A callback for use during {@link #classgen()} */
     protected ClassgenCallback classgenCallback;
     /** A callback for use during {@link #compile()} */
     protected ProgressCallback progressCallback;
@@ -219,7 +219,9 @@ public class CompilationUnit extends ProcessingUnit {
             }
         }, Phases.SEMANTIC_ANALYSIS);
 
-        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> TraitComposer.doExtendTraits(classNode, source, this), Phases.CANONICALIZATION);
+        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
+            TraitComposer.doExtendTraits(classNode, source, this);
+        }, Phases.CANONICALIZATION);
 
         addPhaseOperation(source -> {
             List<ClassNode> classes = source.getAST().getClasses();
@@ -447,7 +449,7 @@ public class CompilationUnit extends ProcessingUnit {
     }
 
     /**
-     * @return the class loader for loading AST transformations
+     * Returns the class loader for loading AST transformations.
      */
     public GroovyClassLoader getTransformLoader() {
         return Optional.ofNullable(getASTTransformationsContext().getTransformLoader()).orElseGet(this::getClassLoader);
diff --git a/src/test/groovy/bugs/Groovy4193Bug.groovy b/src/test/groovy/bugs/Groovy4193.groovy
similarity index 74%
rename from src/test/groovy/bugs/Groovy4193Bug.groovy
rename to src/test/groovy/bugs/Groovy4193.groovy
index 6db3096..63195a6 100644
--- a/src/test/groovy/bugs/Groovy4193Bug.groovy
+++ b/src/test/groovy/bugs/Groovy4193.groovy
@@ -18,15 +18,16 @@
  */
 package groovy.bugs
 
-import groovy.test.GroovyTestCase
+import groovy.transform.CompileStatic
 import org.codehaus.groovy.ast.ImportNode
+import org.junit.Test
 
-class Groovy4193Bug extends GroovyTestCase {
-    void testNPEOnImportNode() {
-        def imp = new ImportNode(null, "GROOVY-4193")
-        
-        assertNull imp.className // no NPE
-        
-        assertNotNull imp.text // no NPE
+@CompileStatic
+final class Groovy4193 {
+    @Test
+    void testImportNodeGetClassNameForNullType() {
+        def imp = new ImportNode('GROOVY-4193')
+        assert imp.className == null // no NPE
+        assert imp.text != null // no NPE
     }
-}
\ No newline at end of file
+}