| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.codehaus.groovy.transform; |
| |
| import groovy.lang.Reference; |
| import org.codehaus.groovy.ast.ASTNode; |
| import org.codehaus.groovy.ast.AnnotationNode; |
| import org.codehaus.groovy.ast.ClassCodeExpressionTransformer; |
| import org.codehaus.groovy.ast.ClassHelper; |
| import org.codehaus.groovy.ast.ClassNode; |
| 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.ast.expr.ClassExpression; |
| import org.codehaus.groovy.ast.expr.ClosureExpression; |
| import org.codehaus.groovy.ast.expr.DeclarationExpression; |
| import org.codehaus.groovy.ast.expr.Expression; |
| 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.BlockStatement; |
| import org.codehaus.groovy.ast.stmt.CatchStatement; |
| import org.codehaus.groovy.ast.stmt.ExpressionStatement; |
| import org.codehaus.groovy.ast.stmt.ForStatement; |
| import org.codehaus.groovy.classgen.VariableScopeVisitor; |
| import org.codehaus.groovy.control.CompilePhase; |
| import org.codehaus.groovy.control.SourceUnit; |
| import org.codehaus.groovy.control.messages.SyntaxErrorMessage; |
| import org.codehaus.groovy.syntax.SyntaxException; |
| import org.objectweb.asm.Opcodes; |
| |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Set; |
| |
| import static org.codehaus.groovy.ast.tools.ClosureUtils.getParametersSafe; |
| import static org.codehaus.groovy.ast.tools.ClosureUtils.hasImplicitParameter; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.param; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.params; |
| |
| /** |
| * Handles generation of code for the @Category annotation. |
| * <p> |
| * Transformation logic is as follows: |
| * <ul> |
| * <li>all non-static methods converted to static ones with an additional 'self' parameter</li> |
| * <li>references to 'this' changed to the additional 'self' parameter</li> |
| * </ul> |
| */ |
| @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) |
| public class CategoryASTTransformation implements ASTTransformation, Opcodes { |
| // should not use a static variable because of possible changes to node metadata |
| // which would be visible to other compilation units |
| private final VariableExpression thisExpression = createThisExpression(); |
| |
| private static VariableExpression createThisExpression() { |
| VariableExpression expr = new VariableExpression("$this"); |
| expr.setClosureSharedVariable(true); |
| return expr; |
| } |
| |
| /** |
| * Property invocations done on 'this' reference are transformed so that the invocations at runtime are |
| * done on the additional parameter 'self' |
| */ |
| public void visit(ASTNode[] nodes, final SourceUnit source) { |
| if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof ClassNode)) { |
| source.getErrorCollector().addError( |
| new SyntaxErrorMessage(new SyntaxException("@Category can only be added to a ClassNode but got: " + (nodes.length==2?nodes[1]:"nothing"), |
| nodes[0].getLineNumber(), nodes[0].getColumnNumber()), source)); |
| } |
| |
| AnnotationNode annotation = (AnnotationNode) nodes[0]; |
| ClassNode parent = (ClassNode) nodes[1]; |
| |
| ClassNode targetClass = getTargetClass(source, annotation); |
| thisExpression.setType(targetClass); |
| |
| final LinkedList<Set<String>> varStack = new LinkedList<Set<String>>(); |
| if (!ensureNoInstanceFieldOrProperty(source, parent)) return; |
| |
| Set<String> names = new HashSet<String>(); |
| for (FieldNode field : parent.getFields()) { |
| names.add(field.getName()); |
| } |
| for (PropertyNode field : parent.getProperties()) { |
| names.add(field.getName()); |
| } |
| varStack.add(names); |
| |
| final Reference parameter = new Reference(); |
| final ClassCodeExpressionTransformer expressionTransformer = new ClassCodeExpressionTransformer() { |
| protected SourceUnit getSourceUnit() { |
| return source; |
| } |
| |
| private void addVariablesToStack(Parameter[] params) { |
| Set<String> names = new HashSet<String>(varStack.getLast()); |
| for (Parameter param : params) { |
| names.add(param.getName()); |
| } |
| varStack.add(names); |
| } |
| |
| @Override |
| public void visitCatchStatement(CatchStatement statement) { |
| varStack.getLast().add(statement.getVariable().getName()); |
| super.visitCatchStatement(statement); |
| varStack.getLast().remove(statement.getVariable().getName()); |
| } |
| |
| @Override |
| public void visitMethod(MethodNode node) { |
| addVariablesToStack(node.getParameters()); |
| super.visitMethod(node); |
| varStack.removeLast(); |
| } |
| |
| @Override |
| public void visitBlockStatement(BlockStatement block) { |
| Set<String> names = new HashSet<String>(varStack.getLast()); |
| varStack.add(names); |
| super.visitBlockStatement(block); |
| varStack.remove(names); |
| } |
| |
| @Override |
| public void visitClosureExpression(ClosureExpression ce) { |
| addVariablesToStack(getParametersSafe(ce)); |
| super.visitClosureExpression(ce); |
| varStack.removeLast(); |
| } |
| |
| @Override |
| public void visitDeclarationExpression(DeclarationExpression expression) { |
| if (expression.isMultipleAssignmentDeclaration()) { |
| TupleExpression te = expression.getTupleExpression(); |
| List<Expression> list = te.getExpressions(); |
| for (Expression arg : list) { |
| VariableExpression ve = (VariableExpression) arg; |
| varStack.getLast().add(ve.getName()); |
| } |
| } else { |
| VariableExpression ve = expression.getVariableExpression(); |
| varStack.getLast().add(ve.getName()); |
| } |
| super.visitDeclarationExpression(expression); |
| } |
| |
| @Override |
| public void visitForLoop(ForStatement forLoop) { |
| Expression exp = forLoop.getCollectionExpression(); |
| exp.visit(this); |
| Parameter loopParam = forLoop.getVariable(); |
| if (loopParam != null) { |
| varStack.getLast().add(loopParam.getName()); |
| } |
| super.visitForLoop(forLoop); |
| } |
| |
| @Override |
| public void visitExpressionStatement(ExpressionStatement es) { |
| // GROOVY-3543: visit the declaration expressions so that declaration variables get added on the varStack |
| Expression exp = es.getExpression(); |
| if (exp instanceof DeclarationExpression) { |
| exp.visit(this); |
| } |
| super.visitExpressionStatement(es); |
| } |
| |
| @Override |
| public Expression transform(Expression exp) { |
| if (exp instanceof VariableExpression) { |
| VariableExpression ve = (VariableExpression) exp; |
| if (ve.getName().equals("this")) |
| return thisExpression; |
| else { |
| if (!varStack.getLast().contains(ve.getName())) { |
| return new PropertyExpression(thisExpression, ve.getName()); |
| } |
| } |
| } else if (exp instanceof PropertyExpression) { |
| PropertyExpression pe = (PropertyExpression) exp; |
| if (pe.getObjectExpression() instanceof VariableExpression) { |
| VariableExpression vex = (VariableExpression) pe.getObjectExpression(); |
| if (vex.isThisExpression()) { |
| pe.setObjectExpression(thisExpression); |
| return pe; |
| } |
| } |
| } else if (exp instanceof ClosureExpression) { |
| ClosureExpression ce = (ClosureExpression) exp; |
| ce.getVariableScope().putReferencedLocalVariable((Parameter) parameter.get()); |
| addVariablesToStack( |
| hasImplicitParameter(ce) |
| ? params(param(ClassHelper.OBJECT_TYPE, "it")) |
| : getParametersSafe(ce)); |
| ce.getCode().visit(this); |
| varStack.removeLast(); |
| } |
| return super.transform(exp); |
| } |
| }; |
| |
| for (MethodNode method : parent.getMethods()) { |
| if (!method.isStatic()) { |
| method.setModifiers(method.getModifiers() | Opcodes.ACC_STATIC); |
| final Parameter[] origParams = method.getParameters(); |
| final Parameter[] newParams = new Parameter[origParams.length + 1]; |
| Parameter p = new Parameter(targetClass, "$this"); |
| p.setClosureSharedVariable(true); |
| newParams[0] = p; |
| parameter.set(p); |
| System.arraycopy(origParams, 0, newParams, 1, origParams.length); |
| method.setParameters(newParams); |
| |
| expressionTransformer.visitMethod(method); |
| } |
| } |
| new VariableScopeVisitor(source, true).visitClass(parent); |
| } |
| |
| private static boolean ensureNoInstanceFieldOrProperty(final SourceUnit source, final ClassNode parent) { |
| boolean valid = true; |
| for (FieldNode fieldNode : parent.getFields()) { |
| if (!fieldNode.isStatic() && fieldNode.getLineNumber()>0) { |
| // if <0, probably an AST transform or internal code (like generated metaclass field, ...) |
| addUnsupportedError(fieldNode, source); |
| valid = false; |
| } |
| } |
| for (PropertyNode propertyNode : parent.getProperties()) { |
| if (!propertyNode.isStatic() && propertyNode.getLineNumber()>0) { |
| // if <0, probably an AST transform or internal code (like generated metaclass field, ...) |
| addUnsupportedError(propertyNode, source); |
| valid = false; |
| } |
| } |
| return valid; |
| } |
| |
| private static void addUnsupportedError(ASTNode node, SourceUnit unit) { |
| unit.getErrorCollector().addErrorAndContinue( |
| new SyntaxErrorMessage( |
| new SyntaxException("The @Category transformation does not support instance "+ |
| (node instanceof FieldNode?"fields":"properties") |
| + " but found ["+getName(node)+"]", |
| node.getLineNumber(), |
| node.getColumnNumber() |
| |
| ), unit |
| )); |
| } |
| |
| private static String getName(ASTNode node) { |
| if (node instanceof FieldNode) return ((FieldNode) node).getName(); |
| if (node instanceof PropertyNode) return ((PropertyNode) node).getName(); |
| return node.getText(); |
| } |
| |
| private static ClassNode getTargetClass(SourceUnit source, AnnotationNode annotation) { |
| Expression value = annotation.getMember("value"); |
| if (!(value instanceof ClassExpression)) { |
| //noinspection ThrowableInstanceNeverThrown |
| source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage( |
| new SyntaxException("@groovy.lang.Category must define 'value' which is the class to apply this category to", |
| annotation.getLineNumber(), annotation.getColumnNumber(), annotation.getLastLineNumber(), annotation.getLastColumnNumber()), |
| source)); |
| return null; |
| } else { |
| ClassExpression ce = (ClassExpression) value; |
| return ce.getType(); |
| } |
| } |
| } |