/* * 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 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.CatchStatement; 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.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.codehaus.groovy.ast.stmt.ForStatement; import org.codehaus.groovy.syntax.SyntaxException; import org.objectweb.asm.Opcodes; import groovy.lang.Reference; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * 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> * * @author Alex Tkachman */ @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>(); names.addAll(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>(); names.addAll(varStack.getLast()); varStack.add(names); super.visitBlockStatement(block); varStack.remove(names); } @Override public void visitClosureExpression(ClosureExpression ce) { addVariablesToStack(ce.getParameters()); 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()); Parameter[] params = ce.getParameters(); if (params == null) { params = Parameter.EMPTY_ARRAY; } else if (params.length == 0) { params = new Parameter[]{ new Parameter(ClassHelper.OBJECT_TYPE, "it") }; } addVariablesToStack(params); 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 == null || !(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(); } } }