/* | |
* 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.Lazy; | |
import groovy.transform.Field; | |
import org.codehaus.groovy.GroovyBugError; | |
import org.codehaus.groovy.ast.ASTNode; | |
import org.codehaus.groovy.ast.AnnotatedNode; | |
import org.codehaus.groovy.ast.AnnotationNode; | |
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer; | |
import org.codehaus.groovy.ast.ClassHelper; | |
import org.codehaus.groovy.ast.ClassNode; | |
import org.codehaus.groovy.ast.FieldNode; | |
import org.codehaus.groovy.ast.MethodNode; | |
import org.codehaus.groovy.ast.Variable; | |
import org.codehaus.groovy.ast.VariableScope; | |
import org.codehaus.groovy.ast.expr.BinaryExpression; | |
import org.codehaus.groovy.ast.expr.ClosureExpression; | |
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.VariableExpression; | |
import org.codehaus.groovy.ast.stmt.ExpressionStatement; | |
import org.codehaus.groovy.classgen.VariableScopeVisitor; | |
import org.codehaus.groovy.control.CompilePhase; | |
import org.codehaus.groovy.control.SourceUnit; | |
import org.codehaus.groovy.runtime.MetaClassHelper; | |
import org.objectweb.asm.Opcodes; | |
import java.util.Arrays; | |
import java.util.Iterator; | |
import java.util.List; | |
import static org.codehaus.groovy.ast.ClassHelper.make; | |
import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX; | |
import static org.codehaus.groovy.ast.tools.GeneralUtils.block; | |
import static org.codehaus.groovy.ast.tools.GeneralUtils.param; | |
import static org.codehaus.groovy.ast.tools.GeneralUtils.params; | |
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX; | |
import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; | |
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; | |
/** | |
* Handles transformation for the @Field annotation. | |
* | |
* @author Paul King | |
* @author Cedric Champeau | |
*/ | |
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) | |
public class FieldASTTransformation extends ClassCodeExpressionTransformer implements ASTTransformation, Opcodes { | |
private static final Class MY_CLASS = Field.class; | |
private static final ClassNode MY_TYPE = make(MY_CLASS); | |
private static final ClassNode LAZY_TYPE = make(Lazy.class); | |
private static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); | |
private static final ClassNode ASTTRANSFORMCLASS_TYPE = make(GroovyASTTransformationClass.class); | |
private SourceUnit sourceUnit; | |
private DeclarationExpression candidate; | |
private boolean insideScriptBody; | |
private String variableName; | |
private FieldNode fieldNode; | |
private ClosureExpression currentClosure; | |
public void visit(ASTNode[] nodes, SourceUnit source) { | |
sourceUnit = source; | |
if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) { | |
throw new GroovyBugError("Internal error: expecting [AnnotationNode, AnnotatedNode] but got: " + Arrays.asList(nodes)); | |
} | |
AnnotatedNode parent = (AnnotatedNode) nodes[1]; | |
AnnotationNode node = (AnnotationNode) nodes[0]; | |
if (!MY_TYPE.equals(node.getClassNode())) return; | |
if (parent instanceof DeclarationExpression) { | |
DeclarationExpression de = (DeclarationExpression) parent; | |
ClassNode cNode = de.getDeclaringClass(); | |
if (!cNode.isScript()) { | |
addError("Annotation " + MY_TYPE_NAME + " can only be used within a Script.", parent); | |
return; | |
} | |
candidate = de; | |
// GROOVY-4548: temp fix to stop CCE until proper support is added | |
if (de.isMultipleAssignmentDeclaration()) { | |
addError("Annotation " + MY_TYPE_NAME + " not supported with multiple assignment notation.", parent); | |
return; | |
} | |
VariableExpression ve = de.getVariableExpression(); | |
variableName = ve.getName(); | |
// set owner null here, it will be updated by addField | |
fieldNode = new FieldNode(variableName, ve.getModifiers(), ve.getType(), null, de.getRightExpression()); | |
fieldNode.setSourcePosition(de); | |
cNode.addField(fieldNode); | |
String setterName = "set" + MetaClassHelper.capitalize(variableName); | |
cNode.addMethod(setterName, ACC_PUBLIC | ACC_SYNTHETIC, ClassHelper.VOID_TYPE, params(param(ve.getType(), variableName)), ClassNode.EMPTY_ARRAY, block( | |
stmt(assignX(propX(varX("this"), variableName), varX(variableName))) | |
)); | |
// GROOVY-4833 : annotations that are not Groovy transforms should be transferred to the generated field | |
// GROOVY-6112 : also copy acceptable Groovy transforms | |
final List<AnnotationNode> annotations = de.getAnnotations(); | |
for (AnnotationNode annotation : annotations) { | |
// GROOVY-6337 HACK: in case newly created field is @Lazy | |
if (annotation.getClassNode().equals(LAZY_TYPE)) { | |
LazyASTTransformation.visitField(this, annotation, fieldNode); | |
} | |
final ClassNode annotationClassNode = annotation.getClassNode(); | |
if (notTransform(annotationClassNode) || acceptableTransform(annotation)) { | |
fieldNode.addAnnotation(annotation); | |
} | |
} | |
super.visitClass(cNode); | |
// GROOVY-5207 So that Closures can see newly added fields | |
// (not super efficient for a very large class with many @Fields but we chose simplicity | |
// and understandability of this solution over more complex but efficient alternatives) | |
VariableScopeVisitor scopeVisitor = new VariableScopeVisitor(source); | |
scopeVisitor.visitClass(cNode); | |
} | |
} | |
private static boolean acceptableTransform(AnnotationNode annotation) { | |
// TODO also check for phase after sourceUnit.getPhase()? but will be ignored anyway? | |
// TODO we should only copy those annotations with FIELD_TARGET but haven't visited annotations | |
// and gathered target info at this phase, so we can't do this: | |
// return annotation.isTargetAllowed(AnnotationNode.FIELD_TARGET); | |
// instead just don't copy ourselves for now | |
return !annotation.getClassNode().equals(MY_TYPE); | |
} | |
private static boolean notTransform(ClassNode annotationClassNode) { | |
return annotationClassNode.getAnnotations(ASTTRANSFORMCLASS_TYPE).isEmpty(); | |
} | |
@Override | |
public Expression transform(Expression expr) { | |
if (expr == null) return null; | |
if (expr instanceof DeclarationExpression) { | |
DeclarationExpression de = (DeclarationExpression) expr; | |
if (de.getLeftExpression() == candidate.getLeftExpression()) { | |
if (insideScriptBody) { | |
// TODO make EmptyExpression work | |
// partially works but not if only thing in script | |
// return EmptyExpression.INSTANCE; | |
return new ConstantExpression(null); | |
} | |
addError("Annotation " + MY_TYPE_NAME + " can only be used within a Script body.", expr); | |
return expr; | |
} | |
} else if (insideScriptBody && expr instanceof VariableExpression && currentClosure != null) { | |
VariableExpression ve = (VariableExpression) expr; | |
if (ve.getName().equals(variableName)) { | |
// we may only check the variable name because the Groovy compiler | |
// already fails if a variable with the same name already exists in the scope. | |
// this means that a closure cannot shadow a class variable | |
ve.setAccessedVariable(fieldNode); | |
final VariableScope variableScope = currentClosure.getVariableScope(); | |
final Iterator<Variable> iterator = variableScope.getReferencedLocalVariablesIterator(); | |
while (iterator.hasNext()) { | |
Variable next = iterator.next(); | |
if (next.getName().equals(variableName)) iterator.remove(); | |
} | |
variableScope.putReferencedClassVariable(fieldNode); | |
return ve; | |
} | |
} | |
return expr.transformExpression(this); | |
} | |
@Override | |
public void visitClosureExpression(final ClosureExpression expression) { | |
ClosureExpression old = currentClosure; | |
currentClosure = expression; | |
super.visitClosureExpression(expression); | |
currentClosure = old; | |
} | |
@Override | |
public void visitMethod(MethodNode node) { | |
Boolean oldInsideScriptBody = insideScriptBody; | |
if (node.isScriptBody()) insideScriptBody = true; | |
super.visitMethod(node); | |
insideScriptBody = oldInsideScriptBody; | |
} | |
@Override | |
public void visitExpressionStatement(ExpressionStatement es) { | |
Expression exp = es.getExpression(); | |
if (exp instanceof BinaryExpression) { | |
exp.visit(this); | |
} | |
super.visitExpressionStatement(es); | |
} | |
protected SourceUnit getSourceUnit() { | |
return sourceUnit; | |
} | |
} |