http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/classgen/BytecodeExpression.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/classgen/BytecodeExpression.java b/src/main/java/org/codehaus/groovy/classgen/BytecodeExpression.java new file mode 100644 index 0000000..ca8030f --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/BytecodeExpression.java @@ -0,0 +1,55 @@ +/* + * 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.classgen; + +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.GroovyCodeVisitor; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.ExpressionTransformer; +import org.objectweb.asm.MethodVisitor; + +/** + * Represents some custom bytecode generation by the compiler + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public abstract class BytecodeExpression extends Expression { + public static final BytecodeExpression NOP = new BytecodeExpression() { + public void visit(MethodVisitor visitor) { + //do nothing + } + }; + + public BytecodeExpression() { + } + + public BytecodeExpression(ClassNode type) { + super.setType(type); + } + + public void visit(GroovyCodeVisitor visitor) { + visitor.visitBytecodeExpression(this); + } + + public abstract void visit(MethodVisitor mv); + + public Expression transformExpression(ExpressionTransformer transformer) { + return this; + } +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/classgen/BytecodeInstruction.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/classgen/BytecodeInstruction.java b/src/main/java/org/codehaus/groovy/classgen/BytecodeInstruction.java new file mode 100644 index 0000000..e390654 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/BytecodeInstruction.java @@ -0,0 +1,32 @@ +/* + * 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.classgen; + +import org.objectweb.asm.MethodVisitor; + +/** + * Helper class used by the class generator. Usually + * an inner class is produced, that contains bytecode + * creation code in the visit method. + * + * @author Jochen Theodorou + */ +public abstract class BytecodeInstruction { + public abstract void visit(MethodVisitor mv); +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/classgen/BytecodeSequence.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/classgen/BytecodeSequence.java b/src/main/java/org/codehaus/groovy/classgen/BytecodeSequence.java new file mode 100644 index 0000000..c5a6645 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/BytecodeSequence.java @@ -0,0 +1,81 @@ +/* + * 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.classgen; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.GroovyCodeVisitor; +import org.codehaus.groovy.ast.stmt.Statement; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * This class represents a sequence of BytecodeInstructions + * or ASTNodes. The evaluation is depending on the type of + * the visitor. + * + * @see BytecodeInstruction + * @see ASTNode + */ + +public class BytecodeSequence extends Statement { + private final List<BytecodeInstruction> instructions; + + public BytecodeSequence(List instructions) { + this.instructions = instructions; + } + + public BytecodeSequence(BytecodeInstruction instruction) { + this.instructions = new ArrayList(1); + this.instructions.add(instruction); + } + + /** + * Delegates to the visit method used for this class. + * If the visitor is a ClassGenerator, then + * {@link ClassGenerator#visitBytecodeSequence(BytecodeSequence)} + * is called with this instance. If the visitor is no + * ClassGenerator, then this method will call visit on + * each ASTNode element sorted by this class. If one + * element is a BytecodeInstruction, then it will be skipped + * as it is no ASTNode. + * + * @param visitor the visitor + * @see ClassGenerator + */ + public void visit(GroovyCodeVisitor visitor) { + if (visitor instanceof ClassGenerator) { + ClassGenerator gen = (ClassGenerator) visitor; + gen.visitBytecodeSequence(this); + return; + } + for (Iterator iterator = instructions.iterator(); iterator.hasNext();) { + Object part = (Object) iterator.next(); + if (part instanceof ASTNode) { + ((ASTNode)part).visit(visitor); + } + } + } + + public List getInstructions() { + return instructions; + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/classgen/ClassCompletionVerifier.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/classgen/ClassCompletionVerifier.java b/src/main/java/org/codehaus/groovy/classgen/ClassCompletionVerifier.java new file mode 100644 index 0000000..1446877 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/ClassCompletionVerifier.java @@ -0,0 +1,763 @@ +/* + * 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.classgen; + +import org.apache.groovy.ast.tools.ClassNodeUtils; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassCodeVisitorSupport; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.InnerClassNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.PropertyNode; +import org.codehaus.groovy.ast.Variable; +import org.codehaus.groovy.ast.expr.BinaryExpression; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.DeclarationExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.GStringExpression; +import org.codehaus.groovy.ast.expr.MapEntryExpression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.PropertyExpression; +import org.codehaus.groovy.ast.expr.TupleExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.ast.stmt.CatchStatement; +import org.codehaus.groovy.ast.tools.GeneralUtils; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.runtime.MetaClassHelper; +import org.codehaus.groovy.syntax.Types; +import org.codehaus.groovy.transform.trait.Traits; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +import static java.lang.reflect.Modifier.isAbstract; +import static java.lang.reflect.Modifier.isFinal; +import static java.lang.reflect.Modifier.isInterface; +import static java.lang.reflect.Modifier.isNative; +import static java.lang.reflect.Modifier.isPrivate; +import static java.lang.reflect.Modifier.isStatic; +import static java.lang.reflect.Modifier.isStrict; +import static java.lang.reflect.Modifier.isSynchronized; +import static java.lang.reflect.Modifier.isTransient; +import static java.lang.reflect.Modifier.isVolatile; +import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE; +import static org.objectweb.asm.Opcodes.ACC_ABSTRACT; +import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_INTERFACE; +import static org.objectweb.asm.Opcodes.ACC_NATIVE; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PROTECTED; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ACC_STRICT; +import static org.objectweb.asm.Opcodes.ACC_SYNCHRONIZED; +import static org.objectweb.asm.Opcodes.ACC_TRANSIENT; +import static org.objectweb.asm.Opcodes.ACC_VOLATILE; +/** + * Checks that a class satisfies various conditions including: + * <ul> + * <li>Incorrect class or method access modifiers</li> + * <li>No abstract methods appear in a non-abstract class</li> + * <li>Existence and correct visibility for inherited members</li> + * <li>Invalid attempts to override final members</li> + * </ul> + */ +public class ClassCompletionVerifier extends ClassCodeVisitorSupport { + private static final String[] INVALID_NAME_CHARS = {".", ":", "/", ";", "[", "<", ">"}; + // the groovy.compiler.strictNames system property is experimental and may change default value or be removed in a future version of Groovy + private final boolean strictNames = Boolean.parseBoolean(System.getProperty("groovy.compiler.strictNames", "false")); + private ClassNode currentClass; + private final SourceUnit source; + private boolean inConstructor = false; + private boolean inStaticConstructor = false; + + public ClassCompletionVerifier(SourceUnit source) { + this.source = source; + } + + public ClassNode getClassNode() { + return currentClass; + } + + public void visitClass(ClassNode node) { + ClassNode oldClass = currentClass; + currentClass = node; + checkImplementsAndExtends(node); + if (source != null && !source.getErrorCollector().hasErrors()) { + checkClassForIncorrectModifiers(node); + checkInterfaceMethodVisibility(node); + checkAbstractMethodVisibility(node); + checkClassForOverwritingFinal(node); + checkMethodsForIncorrectModifiers(node); + checkMethodsForIncorrectName(node); + checkMethodsForWeakerAccess(node); + checkMethodsForOverridingFinal(node); + checkNoAbstractMethodsNonabstractClass(node); + checkClassExtendsAllSelfTypes(node); + checkNoStaticMethodWithSameSignatureAsNonStatic(node); + checkGenericsUsage(node, node.getUnresolvedInterfaces()); + checkGenericsUsage(node, node.getUnresolvedSuperClass()); + } + super.visitClass(node); + currentClass = oldClass; + } + + private void checkNoStaticMethodWithSameSignatureAsNonStatic(final ClassNode node) { + ClassNode parent = node.getSuperClass(); + Map<String, MethodNode> result; + // start with methods from the parent if any + if (parent != null) { + result = parent.getDeclaredMethodsMap(); + } else { + result = new HashMap<String, MethodNode>(); + } + // add in unimplemented abstract methods from the interfaces + ClassNodeUtils.addDeclaredMethodsFromInterfaces(node, result); + for (MethodNode methodNode : node.getMethods()) { + MethodNode mn = result.get(methodNode.getTypeDescriptor()); + if (mn != null && (mn.isStatic() ^ methodNode.isStatic()) && !methodNode.isStaticConstructor()) { + if (!mn.isAbstract()) continue; + ClassNode declaringClass = mn.getDeclaringClass(); + ClassNode cn = declaringClass.getOuterClass(); + if (cn == null && declaringClass.isResolved()) { + // in case of a precompiled class, the outerclass is unknown + Class typeClass = declaringClass.getTypeClass(); + typeClass = typeClass.getEnclosingClass(); + if (typeClass != null) { + cn = ClassHelper.make(typeClass); + } + } + if (cn == null || !Traits.isTrait(cn)) { + ASTNode errorNode = methodNode; + String name = mn.getName(); + if (errorNode.getLineNumber() == -1) { + // try to get a better error message location based on the property + for (PropertyNode propertyNode : node.getProperties()) { + if (name.startsWith("set") || name.startsWith("get") || name.startsWith("is")) { + String propName = Verifier.capitalize(propertyNode.getField().getName()); + String shortName = name.substring(name.startsWith("is") ? 2 : 3); + if (propName.equals(shortName)) { + errorNode = propertyNode; + break; + } + } + } + } + addError("The " + getDescription(methodNode) + " is already defined in " + getDescription(node) + + ". You cannot have both a static and an instance method with the same signature", errorNode); + } + } + result.put(methodNode.getTypeDescriptor(), methodNode); + } + } + + private void checkInterfaceMethodVisibility(ClassNode node) { + if (!node.isInterface()) return; + for (MethodNode method : node.getMethods()) { + if (method.isPrivate()) { + addError("Method '" + method.getName() + "' is private but should be public in " + getDescription(currentClass) + ".", method); + } else if (method.isProtected()) { + addError("Method '" + method.getName() + "' is protected but should be public in " + getDescription(currentClass) + ".", method); + } + } + } + + private void checkAbstractMethodVisibility(ClassNode node) { + // we only do check abstract classes (including enums), no interfaces or non-abstract classes + if (!isAbstract(node.getModifiers()) || isInterface(node.getModifiers())) return; + + List<MethodNode> abstractMethods = node.getAbstractMethods(); + if (abstractMethods == null || abstractMethods.isEmpty()) return; + + for (MethodNode method : abstractMethods) { + if (method.isPrivate()) { + addError("Method '" + method.getName() + "' from " + getDescription(node) + + " must not be private as it is declared as an abstract method.", method); + } + } + } + + private void checkNoAbstractMethodsNonabstractClass(ClassNode node) { + if (isAbstract(node.getModifiers())) return; + List<MethodNode> abstractMethods = node.getAbstractMethods(); + if (abstractMethods == null) return; + for (MethodNode method : abstractMethods) { + MethodNode sameArgsMethod = node.getMethod(method.getName(), method.getParameters()); + if (sameArgsMethod==null || method.getReturnType().equals(sameArgsMethod.getReturnType())) { + addError("Can't have an abstract method in a non-abstract class." + + " The " + getDescription(node) + " must be declared abstract or" + + " the " + getDescription(method) + " must be implemented.", node); + } else { + addError("Abstract "+getDescription(method)+" is not implemented but a " + + "method of the same name but different return type is defined: "+ + (sameArgsMethod.isStatic()?"static ":"")+ + getDescription(sameArgsMethod), method + ); + } + } + } + + private void checkClassExtendsAllSelfTypes(ClassNode node) { + int modifiers = node.getModifiers(); + if (!isInterface(modifiers)) { + for (ClassNode anInterface : GeneralUtils.getInterfacesAndSuperInterfaces(node)) { + if (Traits.isTrait(anInterface)) { + LinkedHashSet<ClassNode> selfTypes = new LinkedHashSet<ClassNode>(); + for (ClassNode type : Traits.collectSelfTypes(anInterface, selfTypes, true, false)) { + if (type.isInterface() && !node.implementsInterface(type)) { + addError(getDescription(node) + + " implements " + getDescription(anInterface) + + " but does not implement self type " + getDescription(type), + anInterface); + } else if (!type.isInterface() && !node.isDerivedFrom(type)) { + addError(getDescription(node) + + " implements " + getDescription(anInterface) + + " but does not extend self type " + getDescription(type), + anInterface); + } + } + } + } + } + } + + private void checkClassForIncorrectModifiers(ClassNode node) { + checkClassForAbstractAndFinal(node); + checkClassForOtherModifiers(node); + } + + private void checkClassForAbstractAndFinal(ClassNode node) { + if (!isAbstract(node.getModifiers())) return; + if (!isFinal(node.getModifiers())) return; + if (node.isInterface()) { + addError("The " + getDescription(node) + " must not be final. It is by definition abstract.", node); + } else { + addError("The " + getDescription(node) + " must not be both final and abstract.", node); + } + } + + private void checkClassForOtherModifiers(ClassNode node) { + checkClassForModifier(node, isTransient(node.getModifiers()), "transient"); + checkClassForModifier(node, isVolatile(node.getModifiers()), "volatile"); + checkClassForModifier(node, isNative(node.getModifiers()), "native"); + if (!(node instanceof InnerClassNode)) { + checkClassForModifier(node, isStatic(node.getModifiers()), "static"); + checkClassForModifier(node, isPrivate(node.getModifiers()), "private"); + } + // don't check synchronized here as it overlaps with ACC_SUPER + } + + private void checkMethodForModifier(MethodNode node, boolean condition, String modifierName) { + if (!condition) return; + addError("The " + getDescription(node) + " has an incorrect modifier " + modifierName + ".", node); + } + + private void checkClassForModifier(ClassNode node, boolean condition, String modifierName) { + if (!condition) return; + addError("The " + getDescription(node) + " has an incorrect modifier " + modifierName + ".", node); + } + + private static String getDescription(ClassNode node) { + return (node.isInterface() ? (Traits.isTrait(node)?"trait":"interface") : "class") + " '" + node.getName() + "'"; + } + + private static String getDescription(MethodNode node) { + return "method '" + node.getTypeDescriptor() + "'"; + } + + private static String getDescription(FieldNode node) { + return "field '" + node.getName() + "'"; + } + + private static String getDescription(Parameter node) { + return "parameter '" + node.getName() + "'"; + } + + private void checkAbstractDeclaration(MethodNode methodNode) { + if (!methodNode.isAbstract()) return; + if (isAbstract(currentClass.getModifiers())) return; + addError("Can't have an abstract method in a non-abstract class." + + " The " + getDescription(currentClass) + " must be declared abstract or the method '" + + methodNode.getTypeDescriptor() + "' must not be abstract.", methodNode); + } + + private void checkClassForOverwritingFinal(ClassNode cn) { + ClassNode superCN = cn.getSuperClass(); + if (superCN == null) return; + if (!isFinal(superCN.getModifiers())) return; + String msg = "You are not allowed to overwrite the final " + getDescription(superCN) + "."; + addError(msg, cn); + } + + private void checkImplementsAndExtends(ClassNode node) { + ClassNode cn = node.getSuperClass(); + if (cn.isInterface() && !node.isInterface()) { + addError("You are not allowed to extend the " + getDescription(cn) + ", use implements instead.", node); + } + for (ClassNode anInterface : node.getInterfaces()) { + cn = anInterface; + if (!cn.isInterface()) { + addError("You are not allowed to implement the " + getDescription(cn) + ", use extends instead.", node); + } + } + } + + private void checkMethodsForIncorrectName(ClassNode cn) { + if (!strictNames) return; + List<MethodNode> methods = cn.getAllDeclaredMethods(); + for (MethodNode mNode : methods) { + String name = mNode.getName(); + if (name.equals("<init>") || name.equals("<clinit>")) continue; + // Groovy allows more characters than Character.isValidJavaIdentifier() would allow + // if we find a good way to encode special chars we could remove (some of) these checks + for (String ch : INVALID_NAME_CHARS) { + if (name.contains(ch)) { + addError("You are not allowed to have '" + ch + "' in a method name", mNode); + } + } + } + } + + private void checkMethodsForIncorrectModifiers(ClassNode cn) { + if (!cn.isInterface()) return; + for (MethodNode method : cn.getMethods()) { + if (method.isFinal()) { + addError("The " + getDescription(method) + " from " + getDescription(cn) + + " must not be final. It is by definition abstract.", method); + } + if (method.isStatic() && !isConstructor(method)) { + addError("The " + getDescription(method) + " from " + getDescription(cn) + + " must not be static. Only fields may be static in an interface.", method); + } + } + } + + private void checkMethodsForWeakerAccess(ClassNode cn) { + for (MethodNode method : cn.getMethods()) { + checkMethodForWeakerAccessPrivileges(method, cn); + } + } + + private static boolean isConstructor(MethodNode method) { + return method.getName().equals("<clinit>"); + } + + private void checkMethodsForOverridingFinal(ClassNode cn) { + for (MethodNode method : cn.getMethods()) { + Parameter[] params = method.getParameters(); + for (MethodNode superMethod : cn.getSuperClass().getMethods(method.getName())) { + Parameter[] superParams = superMethod.getParameters(); + if (!hasEqualParameterTypes(params, superParams)) continue; + if (!superMethod.isFinal()) break; + addInvalidUseOfFinalError(method, params, superMethod.getDeclaringClass()); + return; + } + } + } + + private void addInvalidUseOfFinalError(MethodNode method, Parameter[] parameters, ClassNode superCN) { + StringBuilder msg = new StringBuilder(); + msg.append("You are not allowed to override the final method ").append(method.getName()); + appendParamsDescription(parameters, msg); + msg.append(" from ").append(getDescription(superCN)); + msg.append("."); + addError(msg.toString(), method); + } + + private void appendParamsDescription(Parameter[] parameters, StringBuilder msg) { + msg.append("("); + boolean needsComma = false; + for (Parameter parameter : parameters) { + if (needsComma) { + msg.append(","); + } else { + needsComma = true; + } + msg.append(parameter.getType()); + } + msg.append(")"); + } + + private void addWeakerAccessError(ClassNode cn, MethodNode method, Parameter[] parameters, MethodNode superMethod) { + StringBuilder msg = new StringBuilder(); + msg.append(method.getName()); + appendParamsDescription(parameters, msg); + msg.append(" in "); + msg.append(cn.getName()); + msg.append(" cannot override "); + msg.append(superMethod.getName()); + msg.append(" in "); + msg.append(superMethod.getDeclaringClass().getName()); + msg.append("; attempting to assign weaker access privileges; was "); + msg.append(superMethod.isPublic() ? "public" : "protected"); + addError(msg.toString(), method); + } + + private static boolean hasEqualParameterTypes(Parameter[] first, Parameter[] second) { + if (first.length != second.length) return false; + for (int i = 0; i < first.length; i++) { + String ft = first[i].getType().getName(); + String st = second[i].getType().getName(); + if (ft.equals(st)) continue; + return false; + } + return true; + } + + protected SourceUnit getSourceUnit() { + return source; + } + + public void visitMethod(MethodNode node) { + inConstructor = false; + inStaticConstructor = node.isStaticConstructor(); + checkAbstractDeclaration(node); + checkRepetitiveMethod(node); + checkOverloadingPrivateAndPublic(node); + checkMethodModifiers(node); + checkGenericsUsage(node, node.getParameters()); + checkGenericsUsage(node, node.getReturnType()); + for (Parameter param : node.getParameters()) { + if (param.getType().equals(VOID_TYPE)) { + addError("The " + getDescription(param) + " in " + getDescription(node) + " has invalid type void", param); + } + } + super.visitMethod(node); + } + + private void checkMethodModifiers(MethodNode node) { + // don't check volatile here as it overlaps with ACC_BRIDGE + // additional modifiers not allowed for interfaces + if ((this.currentClass.getModifiers() & ACC_INTERFACE) != 0) { + checkMethodForModifier(node, isStrict(node.getModifiers()), "strictfp"); + checkMethodForModifier(node, isSynchronized(node.getModifiers()), "synchronized"); + checkMethodForModifier(node, isNative(node.getModifiers()), "native"); + } + } + + private void checkMethodForWeakerAccessPrivileges(MethodNode mn, ClassNode cn) { + if (mn.isPublic()) return; + Parameter[] params = mn.getParameters(); + for (MethodNode superMethod : cn.getSuperClass().getMethods(mn.getName())) { + Parameter[] superParams = superMethod.getParameters(); + if (!hasEqualParameterTypes(params, superParams)) continue; + if ((mn.isPrivate() && !superMethod.isPrivate()) || + (mn.isProtected() && superMethod.isPublic())) { + addWeakerAccessError(cn, mn, params, superMethod); + return; + } + } + } + + private void checkOverloadingPrivateAndPublic(MethodNode node) { + if (isConstructor(node)) return; + boolean hasPrivate = node.isPrivate(); + boolean hasPublic = node.isPublic(); + for (MethodNode method : currentClass.getMethods(node.getName())) { + if (method == node) continue; + if (!method.getDeclaringClass().equals(node.getDeclaringClass())) continue; + if (method.isPublic() || method.isProtected()) { + hasPublic = true; + } else { + hasPrivate = true; + } + if (hasPrivate && hasPublic) break; + } + if (hasPrivate && hasPublic) { + addError("Mixing private and public/protected methods of the same name causes multimethods to be disabled and is forbidden to avoid surprising behaviour. Renaming the private methods will solve the problem.", node); + } + } + + private void checkRepetitiveMethod(MethodNode node) { + if (isConstructor(node)) return; + for (MethodNode method : currentClass.getMethods(node.getName())) { + if (method == node) continue; + if (!method.getDeclaringClass().equals(node.getDeclaringClass())) continue; + Parameter[] p1 = node.getParameters(); + Parameter[] p2 = method.getParameters(); + if (p1.length != p2.length) continue; + addErrorIfParamsAndReturnTypeEqual(p2, p1, node, method); + } + } + + private void addErrorIfParamsAndReturnTypeEqual(Parameter[] p2, Parameter[] p1, + MethodNode node, MethodNode element) { + boolean isEqual = true; + for (int i = 0; i < p2.length; i++) { + isEqual &= p1[i].getType().equals(p2[i].getType()); + if (!isEqual) break; + } + isEqual &= node.getReturnType().equals(element.getReturnType()); + if (isEqual) { + addError("Repetitive method name/signature for " + getDescription(node) + + " in " + getDescription(currentClass) + ".", node); + } + } + + public void visitField(FieldNode node) { + if (currentClass.getDeclaredField(node.getName()) != node) { + addError("The " + getDescription(node) + " is declared multiple times.", node); + } + checkInterfaceFieldModifiers(node); + checkGenericsUsage(node, node.getType()); + if (node.getType().equals(VOID_TYPE)) { + addError("The " + getDescription(node) + " has invalid type void", node); + } + super.visitField(node); + } + + public void visitProperty(PropertyNode node) { + checkDuplicateProperties(node); + checkGenericsUsage(node, node.getType()); + super.visitProperty(node); + } + + private void checkDuplicateProperties(PropertyNode node) { + ClassNode cn = node.getDeclaringClass(); + String name = node.getName(); + String getterName = "get" + MetaClassHelper.capitalize(name); + if (Character.isUpperCase(name.charAt(0))) { + for (PropertyNode propNode : cn.getProperties()) { + String otherName = propNode.getField().getName(); + String otherGetterName = "get" + MetaClassHelper.capitalize(otherName); + if (node != propNode && getterName.equals(otherGetterName)) { + String msg = "The field " + name + " and " + otherName + " on the class " + + cn.getName() + " will result in duplicate JavaBean properties, which is not allowed"; + addError(msg, node); + } + } + } + } + + private void checkInterfaceFieldModifiers(FieldNode node) { + if (!currentClass.isInterface()) return; + if ((node.getModifiers() & (ACC_PUBLIC | ACC_STATIC | ACC_FINAL)) == 0 || + (node.getModifiers() & (ACC_PRIVATE | ACC_PROTECTED)) != 0) { + addError("The " + getDescription(node) + " is not 'public static final' but is defined in " + + getDescription(currentClass) + ".", node); + } + } + + public void visitBinaryExpression(BinaryExpression expression) { + if (expression.getOperation().getType() == Types.LEFT_SQUARE_BRACKET && + expression.getRightExpression() instanceof MapEntryExpression) { + addError("You tried to use a map entry for an index operation, this is not allowed. " + + "Maybe something should be set in parentheses or a comma is missing?", + expression.getRightExpression()); + } + super.visitBinaryExpression(expression); + + switch (expression.getOperation().getType()) { + case Types.EQUAL: // = assignment + case Types.BITWISE_AND_EQUAL: + case Types.BITWISE_OR_EQUAL: + case Types.BITWISE_XOR_EQUAL: + case Types.PLUS_EQUAL: + case Types.MINUS_EQUAL: + case Types.MULTIPLY_EQUAL: + case Types.DIVIDE_EQUAL: + case Types.INTDIV_EQUAL: + case Types.MOD_EQUAL: + case Types.POWER_EQUAL: + case Types.LEFT_SHIFT_EQUAL: + case Types.RIGHT_SHIFT_EQUAL: + case Types.RIGHT_SHIFT_UNSIGNED_EQUAL: + checkFinalFieldAccess(expression.getLeftExpression()); + checkSuperOrThisOnLHS(expression.getLeftExpression()); + break; + default: + break; + } + } + + private void checkSuperOrThisOnLHS(Expression expression) { + if (!(expression instanceof VariableExpression)) return; + VariableExpression ve = (VariableExpression) expression; + if (ve.isThisExpression()) { + addError("cannot have 'this' as LHS of an assignment", expression); + } else if (ve.isSuperExpression()) { + addError("cannot have 'super' as LHS of an assignment", expression); + } + } + + private void checkFinalFieldAccess(Expression expression) { + if (!(expression instanceof VariableExpression) && !(expression instanceof PropertyExpression)) return; + Variable v = null; + if (expression instanceof VariableExpression) { + VariableExpression ve = (VariableExpression) expression; + v = ve.getAccessedVariable(); + } else { + PropertyExpression propExp = ((PropertyExpression) expression); + Expression objectExpression = propExp.getObjectExpression(); + if (objectExpression instanceof VariableExpression) { + VariableExpression varExp = (VariableExpression) objectExpression; + if (varExp.isThisExpression()) { + v = currentClass.getDeclaredField(propExp.getPropertyAsString()); + } + } + } + if (v instanceof FieldNode) { + FieldNode fn = (FieldNode) v; + + /* + * if it is static final but not accessed inside a static constructor, or, + * if it is an instance final but not accessed inside a instance constructor, it is an error + */ + boolean isFinal = fn.isFinal(); + boolean isStatic = fn.isStatic(); + boolean error = isFinal && ((isStatic && !inStaticConstructor) || (!isStatic && !inConstructor)); + + if (error) addError("cannot modify" + (isStatic ? " static" : "") + " final field '" + fn.getName() + + "' outside of " + (isStatic ? "static initialization block." : "constructor."), expression); + } + } + + public void visitConstructor(ConstructorNode node) { + inConstructor = true; + inStaticConstructor = node.isStaticConstructor(); + checkGenericsUsage(node, node.getParameters()); + super.visitConstructor(node); + } + + public void visitCatchStatement(CatchStatement cs) { + if (!(cs.getExceptionType().isDerivedFrom(ClassHelper.make(Throwable.class)))) { + addError("Catch statement parameter type is not a subclass of Throwable.", cs); + } + super.visitCatchStatement(cs); + } + + public void visitMethodCallExpression(MethodCallExpression mce) { + super.visitMethodCallExpression(mce); + Expression aexp = mce.getArguments(); + if (aexp instanceof TupleExpression) { + TupleExpression arguments = (TupleExpression) aexp; + for (Expression e : arguments.getExpressions()) { + checkForInvalidDeclaration(e); + } + } else { + checkForInvalidDeclaration(aexp); + } + } + + @Override + public void visitDeclarationExpression(DeclarationExpression expression) { + super.visitDeclarationExpression(expression); + if (expression.isMultipleAssignmentDeclaration()) return; + checkInvalidDeclarationModifier(expression, ACC_ABSTRACT, "abstract"); + checkInvalidDeclarationModifier(expression, ACC_NATIVE, "native"); + checkInvalidDeclarationModifier(expression, ACC_PRIVATE, "private"); + checkInvalidDeclarationModifier(expression, ACC_PROTECTED, "protected"); + checkInvalidDeclarationModifier(expression, ACC_PUBLIC, "public"); + checkInvalidDeclarationModifier(expression, ACC_STATIC, "static"); + checkInvalidDeclarationModifier(expression, ACC_STRICT, "strictfp"); + checkInvalidDeclarationModifier(expression, ACC_SYNCHRONIZED, "synchronized"); + checkInvalidDeclarationModifier(expression, ACC_TRANSIENT, "transient"); + checkInvalidDeclarationModifier(expression, ACC_VOLATILE, "volatile"); + if (expression.getVariableExpression().getOriginType().equals(VOID_TYPE)) { + addError("The variable '" + expression.getVariableExpression().getName() + "' has invalid type void", expression); + } + } + + private void checkInvalidDeclarationModifier(DeclarationExpression expression, int modifier, String modName) { + if ((expression.getVariableExpression().getModifiers() & modifier) != 0) { + addError("Modifier '" + modName + "' not allowed here.", expression); + } + } + + private void checkForInvalidDeclaration(Expression exp) { + if (!(exp instanceof DeclarationExpression)) return; + addError("Invalid use of declaration inside method call.", exp); + } + + public void visitConstantExpression(ConstantExpression expression) { + super.visitConstantExpression(expression); + checkStringExceedingMaximumLength(expression); + } + + public void visitGStringExpression(GStringExpression expression) { + super.visitGStringExpression(expression); + for (ConstantExpression ce : expression.getStrings()) { + checkStringExceedingMaximumLength(ce); + } + } + + private void checkStringExceedingMaximumLength(ConstantExpression expression) { + Object value = expression.getValue(); + if (value instanceof String) { + String s = (String) value; + if (s.length() > 65535) { + addError("String too long. The given string is " + s.length() + " Unicode code units long, but only a maximum of 65535 is allowed.", expression); + } + } + } + + private void checkGenericsUsage(ASTNode ref, ClassNode[] nodes) { + for (ClassNode node : nodes) { + checkGenericsUsage(ref, node); + } + } + + private void checkGenericsUsage(ASTNode ref, Parameter[] params) { + for (Parameter p : params) { + checkGenericsUsage(ref, p.getType()); + } + } + + private void checkGenericsUsage(ASTNode ref, ClassNode node) { + if (node.isArray()) { + checkGenericsUsage(ref, node.getComponentType()); + } else if (!node.isRedirectNode() && node.isUsingGenerics()) { + addError( + "A transform used a generics containing ClassNode "+ node + " " + + "for "+getRefDescriptor(ref) + + "directly. You are not supposed to do this. " + + "Please create a new ClassNode referring to the old ClassNode " + + "and use the new ClassNode instead of the old one. Otherwise " + + "the compiler will create wrong descriptors and a potential " + + "NullPointerException in TypeResolver in the OpenJDK. If this is " + + "not your own doing, please report this bug to the writer of the " + + "transform.", + ref); + } + } + + private static String getRefDescriptor(ASTNode ref) { + if (ref instanceof FieldNode) { + FieldNode f = (FieldNode) ref; + return "the field "+f.getName()+" "; + } else if (ref instanceof PropertyNode) { + PropertyNode p = (PropertyNode) ref; + return "the property "+p.getName()+" "; + } else if (ref instanceof ConstructorNode) { + return "the constructor "+ref.getText()+" "; + } else if (ref instanceof MethodNode) { + return "the method "+ref.getText()+" "; + } else if (ref instanceof ClassNode) { + return "the super class "+ref+" "; + } + return "<unknown with class "+ref.getClass()+"> "; + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/classgen/ClassGenerator.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/classgen/ClassGenerator.java b/src/main/java/org/codehaus/groovy/classgen/ClassGenerator.java new file mode 100644 index 0000000..2ed8661 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/ClassGenerator.java @@ -0,0 +1,48 @@ +/* + * 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.classgen; + +import org.codehaus.groovy.ast.ClassCodeVisitorSupport; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.control.SourceUnit; +import org.objectweb.asm.Opcodes; + +import java.util.LinkedList; + +/** + * Abstract base class for generator of Java class versions of Groovy AST classes + * + * @author <a href="mailto:[email protected]">James Strachan</a> + * @author Russel Winder + */ +public abstract class ClassGenerator extends ClassCodeVisitorSupport implements Opcodes { + // inner classes created while generating bytecode + protected LinkedList<ClassNode> innerClasses = new LinkedList<ClassNode>(); + + public LinkedList<ClassNode> getInnerClasses() { + return innerClasses; + } + + protected SourceUnit getSourceUnit() { + return null; + } + + public void visitBytecodeSequence(BytecodeSequence bytecodeSequence) { + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/classgen/ClassGeneratorException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/classgen/ClassGeneratorException.java b/src/main/java/org/codehaus/groovy/classgen/ClassGeneratorException.java new file mode 100644 index 0000000..bce3448 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/ClassGeneratorException.java @@ -0,0 +1,36 @@ +/* + * 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.classgen; + +/** + * An exception thrown by the class generator + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public class ClassGeneratorException extends RuntimeException { + + public ClassGeneratorException(String message) { + super(message); + } + + public ClassGeneratorException(String message, Throwable cause) { + super(message, cause); + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/classgen/DummyClassGenerator.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/classgen/DummyClassGenerator.java b/src/main/java/org/codehaus/groovy/classgen/DummyClassGenerator.java new file mode 100644 index 0000000..15ade40 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/DummyClassGenerator.java @@ -0,0 +1,180 @@ +/* + * 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.classgen; + +import groovy.lang.GroovyRuntimeException; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.CompileUnit; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.PropertyNode; +import org.codehaus.groovy.classgen.asm.BytecodeHelper; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.Iterator; + +/** + * To generate a class that has all the fields and methods, except that fields are not initialized + * and methods are empty. It's intended for being used as a place holder during code generation + * of reference to the "this" class itself. + * + * @author <a href="mailto:[email protected]">James Strachan</a> + * @author <a href="mailto:[email protected]">Bing Ran</a> + */ +public class DummyClassGenerator extends ClassGenerator { + + private final ClassVisitor cv; + private MethodVisitor mv; + private final GeneratorContext context; + + // current class details + private ClassNode classNode; + private String internalClassName; + private String internalBaseClassName; + + + public DummyClassGenerator( + GeneratorContext context, + ClassVisitor classVisitor, + ClassLoader classLoader, + String sourceFile) { + this.context = context; + this.cv = classVisitor; + } + + // GroovyClassVisitor interface + //------------------------------------------------------------------------- + public void visitClass(ClassNode classNode) { + try { + this.classNode = classNode; + this.internalClassName = BytecodeHelper.getClassInternalName(classNode); + + //System.out.println("Generating class: " + classNode.getName()); + + this.internalBaseClassName = BytecodeHelper.getClassInternalName(classNode.getSuperClass()); + + cv.visit( + Opcodes.V1_3, + classNode.getModifiers(), + internalClassName, + (String) null, + internalBaseClassName, + BytecodeHelper.getClassInternalNames(classNode.getInterfaces()) + ); + + classNode.visitContents(this); + + for (Iterator iter = innerClasses.iterator(); iter.hasNext();) { + ClassNode innerClass = (ClassNode) iter.next(); + ClassNode innerClassType = innerClass; + String innerClassInternalName = BytecodeHelper.getClassInternalName(innerClassType); + String outerClassName = internalClassName; // default for inner classes + MethodNode enclosingMethod = innerClass.getEnclosingMethod(); + if (enclosingMethod != null) { + // local inner classes do not specify the outer class name + outerClassName = null; + } + cv.visitInnerClass( + innerClassInternalName, + outerClassName, + innerClassType.getName(), + innerClass.getModifiers()); + } + cv.visitEnd(); + } + catch (GroovyRuntimeException e) { + e.setModule(classNode.getModule()); + throw e; + } + } + + public void visitConstructor(ConstructorNode node) { + + visitParameters(node, node.getParameters()); + + String methodType = BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, node.getParameters()); + mv = cv.visitMethod(node.getModifiers(), "<init>", methodType, null, null); + mv.visitTypeInsn(NEW, "java/lang/RuntimeException"); + mv.visitInsn(DUP); + mv.visitLdcInsn("not intended for execution"); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V", false); + mv.visitInsn(ATHROW); + mv.visitMaxs(0, 0); + } + + public void visitMethod(MethodNode node) { + + visitParameters(node, node.getParameters()); + + String methodType = BytecodeHelper.getMethodDescriptor(node.getReturnType(), node.getParameters()); + mv = cv.visitMethod(node.getModifiers(), node.getName(), methodType, null, null); + + mv.visitTypeInsn(NEW, "java/lang/RuntimeException"); + mv.visitInsn(DUP); + mv.visitLdcInsn("not intended for execution"); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V", false); + mv.visitInsn(ATHROW); + + mv.visitMaxs(0, 0); + } + + public void visitField(FieldNode fieldNode) { + + cv.visitField( + fieldNode.getModifiers(), + fieldNode.getName(), + BytecodeHelper.getTypeDescription(fieldNode.getType()), + null, //fieldValue, //br all the sudden that one cannot init the field here. init is done in static initializer and instance initializer. + null); + } + + /** + * Creates a getter, setter and field + */ + public void visitProperty(PropertyNode statement) { + } + + protected CompileUnit getCompileUnit() { + CompileUnit answer = classNode.getCompileUnit(); + if (answer == null) { + answer = context.getCompileUnit(); + } + return answer; + } + + protected void visitParameters(ASTNode node, Parameter[] parameters) { + for (int i = 0, size = parameters.length; i < size; i++) { + visitParameter(node, parameters[i]); + } + } + + protected void visitParameter(ASTNode node, Parameter parameter) { + } + + + public void visitAnnotations(AnnotatedNode node) { + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/classgen/EnumCompletionVisitor.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/classgen/EnumCompletionVisitor.java b/src/main/java/org/codehaus/groovy/classgen/EnumCompletionVisitor.java new file mode 100644 index 0000000..30c6fab --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/EnumCompletionVisitor.java @@ -0,0 +1,169 @@ +/* + * 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.classgen; + +import org.codehaus.groovy.ast.ClassCodeVisitorSupport; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.CodeVisitorSupport; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.EnumConstantClassNode; +import org.codehaus.groovy.ast.InnerClassNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ArgumentListExpression; +import org.codehaus.groovy.ast.expr.ConstructorCallExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.TupleExpression; +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.control.CompilationUnit; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.transform.TupleConstructorASTTransformation; +import org.objectweb.asm.Opcodes; + +import java.util.ArrayList; +import java.util.List; + +/** + * Enums have a parent constructor with two arguments from java.lang.Enum. + * This visitor adds those two arguments into manually created constructors + * and performs the necessary super call. + */ +public class EnumCompletionVisitor extends ClassCodeVisitorSupport { + private final SourceUnit sourceUnit; + + public EnumCompletionVisitor(CompilationUnit cu, SourceUnit su) { + sourceUnit = su; + } + + public void visitClass(ClassNode node) { + if (!node.isEnum()) return; + completeEnum(node); + } + + protected SourceUnit getSourceUnit() { + return sourceUnit; + } + + private void completeEnum(ClassNode enumClass) { + boolean isAic = isAnonymousInnerClass(enumClass); + if (enumClass.getDeclaredConstructors().isEmpty()) { + addImplicitConstructors(enumClass, isAic); + } + + for (ConstructorNode ctor : enumClass.getDeclaredConstructors()) { + transformConstructor(ctor, isAic); + } + } + + /** + * Add map and no-arg constructor or mirror those of the superclass (i.e. base enum). + */ + private static void addImplicitConstructors(ClassNode enumClass, boolean aic) { + if (aic) { + ClassNode sn = enumClass.getSuperClass(); + List<ConstructorNode> sctors = new ArrayList<ConstructorNode>(sn.getDeclaredConstructors()); + if (sctors.isEmpty()) { + addMapConstructors(enumClass, false); + } else { + for (ConstructorNode constructorNode : sctors) { + ConstructorNode init = new ConstructorNode(Opcodes.ACC_PUBLIC, constructorNode.getParameters(), ClassNode.EMPTY_ARRAY, new BlockStatement()); + enumClass.addConstructor(init); + } + } + } else { + addMapConstructors(enumClass, false); + } + } + + /** + * If constructor does not define a call to super, then transform constructor + * to get String,int parameters at beginning and add call super(String,int). + */ + private void transformConstructor(ConstructorNode ctor, boolean isAic) { + boolean chainedThisConstructorCall = false; + ConstructorCallExpression cce = null; + if (ctor.firstStatementIsSpecialConstructorCall()) { + Statement code = ctor.getFirstStatement(); + cce = (ConstructorCallExpression) ((ExpressionStatement) code).getExpression(); + if (cce.isSuperCall()) return; + // must be call to this(...) + chainedThisConstructorCall = true; + } + // we need to add parameters + Parameter[] oldP = ctor.getParameters(); + Parameter[] newP = new Parameter[oldP.length + 2]; + String stringParameterName = getUniqueVariableName("__str", ctor.getCode()); + newP[0] = new Parameter(ClassHelper.STRING_TYPE, stringParameterName); + String intParameterName = getUniqueVariableName("__int", ctor.getCode()); + newP[1] = new Parameter(ClassHelper.int_TYPE, intParameterName); + System.arraycopy(oldP, 0, newP, 2, oldP.length); + ctor.setParameters(newP); + VariableExpression stringVariable = new VariableExpression(newP[0]); + VariableExpression intVariable = new VariableExpression(newP[1]); + if (chainedThisConstructorCall) { + TupleExpression args = (TupleExpression) cce.getArguments(); + List<Expression> argsExprs = args.getExpressions(); + argsExprs.add(0, stringVariable); + argsExprs.add(1, intVariable); + } else { + // add a super call + List<Expression> args = new ArrayList<Expression>(); + args.add(stringVariable); + args.add(intVariable); + if (isAic) { + for (Parameter parameter : oldP) { + args.add(new VariableExpression(parameter.getName())); + } + } + cce = new ConstructorCallExpression(ClassNode.SUPER, new ArgumentListExpression(args)); + BlockStatement code = new BlockStatement(); + code.addStatement(new ExpressionStatement(cce)); + Statement oldCode = ctor.getCode(); + if (oldCode != null) code.addStatement(oldCode); + ctor.setCode(code); + } + } + + public static void addMapConstructors(ClassNode enumClass, boolean hasNoArg) { + TupleConstructorASTTransformation.addMapConstructors(enumClass, hasNoArg, "One of the enum constants for enum " + enumClass.getName() + + " was initialized with null. Please use a non-null value or define your own constructor."); + } + + private String getUniqueVariableName(final String name, Statement code) { + if (code == null) return name; + final Object[] found = new Object[1]; + CodeVisitorSupport cv = new CodeVisitorSupport() { + public void visitVariableExpression(VariableExpression expression) { + if (expression.getName().equals(name)) found[0] = Boolean.TRUE; + } + }; + code.visit(cv); + if (found[0] != null) return getUniqueVariableName("_" + name, code); + return name; + } + + private static boolean isAnonymousInnerClass(ClassNode enumClass) { + if (!(enumClass instanceof EnumConstantClassNode)) return false; + InnerClassNode ic = (InnerClassNode) enumClass; + return ic.getVariableScope() == null; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/classgen/EnumVisitor.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/classgen/EnumVisitor.java b/src/main/java/org/codehaus/groovy/classgen/EnumVisitor.java new file mode 100644 index 0000000..743801f --- /dev/null +++ b/src/main/java/org/codehaus/groovy/classgen/EnumVisitor.java @@ -0,0 +1,444 @@ +/* + * 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.classgen; + +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.ClassCodeVisitorSupport; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.EnumConstantClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.InnerClassNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ArgumentListExpression; +import org.codehaus.groovy.ast.expr.ArrayExpression; +import org.codehaus.groovy.ast.expr.BinaryExpression; +import org.codehaus.groovy.ast.expr.BooleanExpression; +import org.codehaus.groovy.ast.expr.ClassExpression; +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.FieldExpression; +import org.codehaus.groovy.ast.expr.ListExpression; +import org.codehaus.groovy.ast.expr.MapEntryExpression; +import org.codehaus.groovy.ast.expr.MapExpression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.SpreadExpression; +import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.EmptyStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.IfStatement; +import org.codehaus.groovy.ast.stmt.ReturnStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.messages.SyntaxErrorMessage; +import org.codehaus.groovy.syntax.SyntaxException; +import org.codehaus.groovy.syntax.Token; +import org.codehaus.groovy.syntax.Types; +import org.objectweb.asm.Opcodes; + +import java.util.ArrayList; +import java.util.List; + +public class EnumVisitor extends ClassCodeVisitorSupport { + // some constants for modifiers + private static final int FS = Opcodes.ACC_FINAL | Opcodes.ACC_STATIC; + private static final int PS = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC; + private static final int PUBLIC_FS = Opcodes.ACC_PUBLIC | FS; + private static final int PRIVATE_FS = Opcodes.ACC_PRIVATE | FS; + + private final SourceUnit sourceUnit; + + + public EnumVisitor(CompilationUnit cu, SourceUnit su) { + sourceUnit = su; + } + + public void visitClass(ClassNode node) { + if (!node.isEnum()) return; + completeEnum(node); + } + + protected SourceUnit getSourceUnit() { + return sourceUnit; + } + + private void completeEnum(ClassNode enumClass) { + boolean isAic = isAnonymousInnerClass(enumClass); + // create MIN_VALUE and MAX_VALUE fields + FieldNode minValue = null, maxValue = null, values = null; + + if (!isAic) { + ClassNode enumRef = enumClass.getPlainNodeReference(); + + // create values field + values = new FieldNode("$VALUES", PRIVATE_FS | Opcodes.ACC_SYNTHETIC, enumRef.makeArray(), enumClass, null); + values.setSynthetic(true); + + addMethods(enumClass, values); + checkForAbstractMethods(enumClass); + + // create MIN_VALUE and MAX_VALUE fields + minValue = new FieldNode("MIN_VALUE", PUBLIC_FS, enumRef, enumClass, null); + maxValue = new FieldNode("MAX_VALUE", PUBLIC_FS, enumRef, enumClass, null); + } + addInit(enumClass, minValue, maxValue, values, isAic); + } + + private static void checkForAbstractMethods(ClassNode enumClass) { + List<MethodNode> methods = enumClass.getMethods(); + for (MethodNode m : methods) { + if (m.isAbstract()) { + // make the class abstract also see Effective Java p.152 + enumClass.setModifiers(enumClass.getModifiers() | Opcodes.ACC_ABSTRACT); + break; + } + } + } + + private static void addMethods(ClassNode enumClass, FieldNode values) { + List<MethodNode> methods = enumClass.getMethods(); + + boolean hasNext = false; + boolean hasPrevious = false; + for (MethodNode m : methods) { + if (m.getName().equals("next") && m.getParameters().length == 0) hasNext = true; + if (m.getName().equals("previous") && m.getParameters().length == 0) hasPrevious = true; + if (hasNext && hasPrevious) break; + } + + ClassNode enumRef = enumClass.getPlainNodeReference(); + + { + // create values() method + MethodNode valuesMethod = new MethodNode("values", PUBLIC_FS, enumRef.makeArray(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null); + valuesMethod.setSynthetic(true); + BlockStatement code = new BlockStatement(); + MethodCallExpression cloneCall = new MethodCallExpression(new FieldExpression(values), "clone", MethodCallExpression.NO_ARGUMENTS); + cloneCall.setMethodTarget(values.getType().getMethod("clone", Parameter.EMPTY_ARRAY)); + code.addStatement(new ReturnStatement(cloneCall)); + valuesMethod.setCode(code); + enumClass.addMethod(valuesMethod); + } + + if (!hasNext) { + // create next() method, code: + // Day next() { + // int ordinal = ordinal().next() + // if (ordinal >= values().size()) ordinal = 0 + // return values()[ordinal] + // } + Token assign = Token.newSymbol(Types.ASSIGN, -1, -1); + Token ge = Token.newSymbol(Types.COMPARE_GREATER_THAN_EQUAL, -1, -1); + MethodNode nextMethod = new MethodNode("next", Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, enumRef, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null); + nextMethod.setSynthetic(true); + BlockStatement code = new BlockStatement(); + BlockStatement ifStatement = new BlockStatement(); + ifStatement.addStatement( + new ExpressionStatement( + new BinaryExpression(new VariableExpression("ordinal"), assign, new ConstantExpression(0)) + ) + ); + + code.addStatement( + new ExpressionStatement( + new DeclarationExpression( + new VariableExpression("ordinal"), + assign, + new MethodCallExpression( + new MethodCallExpression( + VariableExpression.THIS_EXPRESSION, + "ordinal", + MethodCallExpression.NO_ARGUMENTS), + "next", + MethodCallExpression.NO_ARGUMENTS + ) + ) + ) + ); + code.addStatement( + new IfStatement( + new BooleanExpression(new BinaryExpression( + new VariableExpression("ordinal"), + ge, + new MethodCallExpression( + new FieldExpression(values), + "size", + MethodCallExpression.NO_ARGUMENTS + ) + )), + ifStatement, + EmptyStatement.INSTANCE + ) + ); + code.addStatement( + new ReturnStatement( + new MethodCallExpression(new FieldExpression(values), "getAt", new VariableExpression("ordinal")) + ) + ); + nextMethod.setCode(code); + enumClass.addMethod(nextMethod); + } + + if (!hasPrevious) { + // create previous() method, code: + // Day previous() { + // int ordinal = ordinal().previous() + // if (ordinal < 0) ordinal = values().size() - 1 + // return values()[ordinal] + // } + Token assign = Token.newSymbol(Types.ASSIGN, -1, -1); + Token lt = Token.newSymbol(Types.COMPARE_LESS_THAN, -1, -1); + MethodNode nextMethod = new MethodNode("previous", Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, enumRef, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, null); + nextMethod.setSynthetic(true); + BlockStatement code = new BlockStatement(); + BlockStatement ifStatement = new BlockStatement(); + ifStatement.addStatement( + new ExpressionStatement( + new BinaryExpression(new VariableExpression("ordinal"), assign, + new MethodCallExpression( + new MethodCallExpression( + new FieldExpression(values), + "size", + MethodCallExpression.NO_ARGUMENTS + ), + "minus", + new ConstantExpression(1) + ) + ) + ) + ); + + code.addStatement( + new ExpressionStatement( + new DeclarationExpression( + new VariableExpression("ordinal"), + assign, + new MethodCallExpression( + new MethodCallExpression( + VariableExpression.THIS_EXPRESSION, + "ordinal", + MethodCallExpression.NO_ARGUMENTS), + "previous", + MethodCallExpression.NO_ARGUMENTS + ) + ) + ) + ); + code.addStatement( + new IfStatement( + new BooleanExpression(new BinaryExpression( + new VariableExpression("ordinal"), + lt, + new ConstantExpression(0) + )), + ifStatement, + EmptyStatement.INSTANCE + ) + ); + code.addStatement( + new ReturnStatement( + new MethodCallExpression(new FieldExpression(values), "getAt", new VariableExpression("ordinal")) + ) + ); + nextMethod.setCode(code); + enumClass.addMethod(nextMethod); + } + + { + // create valueOf + Parameter stringParameter = new Parameter(ClassHelper.STRING_TYPE, "name"); + MethodNode valueOfMethod = new MethodNode("valueOf", PS, enumRef, new Parameter[]{stringParameter}, ClassNode.EMPTY_ARRAY, null); + ArgumentListExpression callArguments = new ArgumentListExpression(); + callArguments.addExpression(new ClassExpression(enumClass)); + callArguments.addExpression(new VariableExpression("name")); + + BlockStatement code = new BlockStatement(); + code.addStatement( + new ReturnStatement( + new MethodCallExpression(new ClassExpression(ClassHelper.Enum_Type), "valueOf", callArguments) + ) + ); + valueOfMethod.setCode(code); + valueOfMethod.setSynthetic(true); + enumClass.addMethod(valueOfMethod); + } + } + + private void addInit(ClassNode enumClass, FieldNode minValue, + FieldNode maxValue, FieldNode values, + boolean isAic) { + // constructor helper + // This method is used instead of calling the constructor as + // calling the constructor may require a table with MetaClass + // selecting the constructor for each enum value. So instead we + // use this method to have a central point for constructor selection + // and only one table. The whole construction is needed because + // Reflection forbids access to the enum constructor. + // code: + // def $INIT(Object[] para) { + // return this(*para) + // } + ClassNode enumRef = enumClass.getPlainNodeReference(); + Parameter[] parameter = new Parameter[]{new Parameter(ClassHelper.OBJECT_TYPE.makeArray(), "para")}; + MethodNode initMethod = new MethodNode("$INIT", PUBLIC_FS | Opcodes.ACC_SYNTHETIC, enumRef, parameter, ClassNode.EMPTY_ARRAY, null); + initMethod.setSynthetic(true); + ConstructorCallExpression cce = new ConstructorCallExpression( + ClassNode.THIS, + new ArgumentListExpression( + new SpreadExpression(new VariableExpression("para")) + ) + ); + BlockStatement code = new BlockStatement(); + code.addStatement(new ReturnStatement(cce)); + initMethod.setCode(code); + enumClass.addMethod(initMethod); + + // static init + List<FieldNode> fields = enumClass.getFields(); + List<Expression> arrayInit = new ArrayList<Expression>(); + int value = -1; + Token assign = Token.newSymbol(Types.ASSIGN, -1, -1); + List<Statement> block = new ArrayList<Statement>(); + FieldNode tempMin = null; + FieldNode tempMax = null; + for (FieldNode field : fields) { + if ((field.getModifiers() & Opcodes.ACC_ENUM) == 0) continue; + value++; + if (tempMin == null) tempMin = field; + tempMax = field; + + ClassNode enumBase = enumClass; + ArgumentListExpression args = new ArgumentListExpression(); + args.addExpression(new ConstantExpression(field.getName())); + args.addExpression(new ConstantExpression(value)); + if (field.getInitialExpression() == null) { + if ((enumClass.getModifiers() & Opcodes.ACC_ABSTRACT) != 0) { + addError(field, "The enum constant " + field.getName() + " must override abstract methods from " + enumBase.getName() + "."); + continue; + } + } else { + ListExpression oldArgs = (ListExpression) field.getInitialExpression(); + List<MapEntryExpression> savedMapEntries = new ArrayList<MapEntryExpression>(); + for (Expression exp : oldArgs.getExpressions()) { + if (exp instanceof MapEntryExpression) { + savedMapEntries.add((MapEntryExpression) exp); + continue; + } + + InnerClassNode inner = null; + if (exp instanceof ClassExpression) { + ClassExpression clazzExp = (ClassExpression) exp; + ClassNode ref = clazzExp.getType(); + if (ref instanceof EnumConstantClassNode) { + inner = (InnerClassNode) ref; + } + } + if (inner != null) { + List<MethodNode> baseMethods = enumBase.getMethods(); + for (MethodNode methodNode : baseMethods) { + if (!methodNode.isAbstract()) continue; + MethodNode enumConstMethod = inner.getMethod(methodNode.getName(), methodNode.getParameters()); + if (enumConstMethod == null || (enumConstMethod.getModifiers() & Opcodes.ACC_ABSTRACT) != 0) { + addError(field, "Can't have an abstract method in enum constant " + field.getName() + ". Implement method '" + methodNode.getTypeDescriptor() + "'."); + } + } + if (inner.getVariableScope() == null) { + enumBase = inner; + /* + * GROOVY-3985: Remove the final modifier from $INIT method in this case + * so that subclasses of enum generated for enum constants (aic) can provide + * their own $INIT method + */ + initMethod.setModifiers(initMethod.getModifiers() & ~Opcodes.ACC_FINAL); + continue; + } + } + args.addExpression(exp); + } + if (!savedMapEntries.isEmpty()) { + args.getExpressions().add(2, new MapExpression(savedMapEntries)); + } + } + field.setInitialValueExpression(null); + block.add( + new ExpressionStatement( + new BinaryExpression( + new FieldExpression(field), + assign, + new StaticMethodCallExpression(enumBase, "$INIT", args) + ) + ) + ); + arrayInit.add(new FieldExpression(field)); + } + + if (!isAic) { + if (tempMin != null) { + block.add( + new ExpressionStatement( + new BinaryExpression( + new FieldExpression(minValue), + assign, + new FieldExpression(tempMin) + ) + ) + ); + block.add( + new ExpressionStatement( + new BinaryExpression( + new FieldExpression(maxValue), + assign, + new FieldExpression(tempMax) + ) + ) + ); + enumClass.addField(minValue); + enumClass.addField(maxValue); + } + + block.add( + new ExpressionStatement( + new BinaryExpression(new FieldExpression(values), assign, new ArrayExpression(enumClass, arrayInit)) + ) + ); + enumClass.addField(values); + } + enumClass.addStaticInitializerStatements(block, true); + } + + private void addError(AnnotatedNode exp, String msg) { + sourceUnit.getErrorCollector().addErrorAndContinue( + new SyntaxErrorMessage( + new SyntaxException(msg + '\n', exp.getLineNumber(), exp.getColumnNumber(), exp.getLastLineNumber(), exp.getLastColumnNumber()), sourceUnit) + ); + } + + private static boolean isAnonymousInnerClass(ClassNode enumClass) { + if (!(enumClass instanceof EnumConstantClassNode)) return false; + InnerClassNode ic = (InnerClassNode) enumClass; + return ic.getVariableScope() == null; + } + +}
