/*
* Copyright 2014 the original author or authors.
*
* Licensed 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.gradle.model.dsl.internal.transform;
import com.google.common.base.Joiner;
import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.gradle.api.Transformer;
import org.gradle.groovy.scripts.internal.AstUtils;
import org.gradle.groovy.scripts.internal.ExpressionReplacingVisitorSupport;
import org.gradle.internal.SystemProperties;
import org.gradle.model.dsl.internal.inputs.PotentialInputs;
import org.gradle.model.internal.core.ModelPath;
import org.gradle.util.CollectionUtils;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class RuleVisitor extends ExpressionReplacingVisitorSupport {
public static final String INVALID_ARGUMENT_LIST = "argument list must be exactly 1 literal non empty string";
public static final String SOURCE_URI_TOKEN = "@@sourceuri@@";
public static final String SOURCE_DESC_TOKEN = "@@sourcedesc@@";
private static final String AST_NODE_METADATA_INPUTS_KEY = RuleVisitor.class.getName() + ".inputs";
private static final String AST_NODE_METADATA_LOCATION_KEY = RuleVisitor.class.getName() + ".location";
private static final String DOLLAR = "$";
private static final String GET = "get";
private static final ClassNode POTENTIAL_INPUTS = new ClassNode(PotentialInputs.class);
private static final ClassNode TRANSFORMED_CLOSURE = new ClassNode(TransformedClosure.class);
private static final ClassNode INPUT_REFERENCES = new ClassNode(InputReferences.class);
private static final ClassNode SOURCE_LOCATION = new ClassNode(SourceLocation.class);
private static final ClassNode RULE_FACTORY = new ClassNode(ClosureBackedRuleFactory.class);
private static final String INPUTS_FIELD_NAME = "__inputs__";
private static final String RULE_FACTORY_FIELD_NAME = "__rule_factory__";
private static final Token ASSIGN = new Token(Types.ASSIGN, "=", -1, -1);
private final String scriptSourceDescription;
private final URI location;
private final SourceUnit sourceUnit;
private InputReferences inputs;
private VariableExpression inputsVariable;
private int nestingDepth;
private int counter;
public RuleVisitor(SourceUnit sourceUnit, String scriptSourceDescription, URI location) {
this.scriptSourceDescription = scriptSourceDescription;
this.location = location;
this.sourceUnit = sourceUnit;
}
// Not part of a normal visitor, see ClosureCreationInterceptingVerifier
public static void visitGeneratedClosure(ClassNode node) {
MethodNode closureCallMethod = AstUtils.getGeneratedClosureImplMethod(node);
Statement closureCode = closureCallMethod.getCode();
InputReferences inputs = closureCode.getNodeMetaData(AST_NODE_METADATA_INPUTS_KEY);
if (inputs != null) {
SourceLocation sourceLocation = closureCode.getNodeMetaData(AST_NODE_METADATA_LOCATION_KEY);
node.addInterface(TRANSFORMED_CLOSURE);
FieldNode inputsField = new FieldNode(INPUTS_FIELD_NAME, Modifier.PRIVATE, POTENTIAL_INPUTS, node, null);
FieldNode ruleFactoryField = new FieldNode(RULE_FACTORY_FIELD_NAME, Modifier.PRIVATE, RULE_FACTORY, node, null);
node.addField(inputsField);
node.addField(ruleFactoryField);
// Generate makeRule() method
List<Statement> statements = new ArrayList<Statement>();
statements.add(new ExpressionStatement(new BinaryExpression(new FieldExpression(inputsField), ASSIGN, new VariableExpression("inputs"))));
statements.add(new ExpressionStatement(new BinaryExpression(new FieldExpression(ruleFactoryField), ASSIGN, new VariableExpression("ruleFactory"))));
node.addMethod(new MethodNode("makeRule",
Modifier.PUBLIC,
ClassHelper.VOID_TYPE,
new Parameter[]{new Parameter(POTENTIAL_INPUTS, "inputs"), new Parameter(RULE_FACTORY, "ruleFactory")},
new ClassNode[0],
new BlockStatement(statements, new VariableScope())));
// Generate inputReferences() method
VariableExpression inputsVar = new VariableExpression("inputs", INPUT_REFERENCES);
VariableScope methodVarScope = new VariableScope();
methodVarScope.putDeclaredVariable(inputsVar);
statements = new ArrayList<Statement>();
statements.add(new ExpressionStatement(new DeclarationExpression(inputsVar, ASSIGN, new ConstructorCallExpression(INPUT_REFERENCES, new ArgumentListExpression()))));
for (InputReference inputReference : inputs.getOwnReferences()) {
statements.add(new ExpressionStatement(new MethodCallExpression(inputsVar,
"ownReference",
new ArgumentListExpression(
new ConstantExpression(inputReference.getPath()),
new ConstantExpression(inputReference.getLineNumber())))));
}
for (InputReference inputReference : inputs.getNestedReferences()) {
statements.add(new ExpressionStatement(new MethodCallExpression(inputsVar,
"nestedReference",
new ArgumentListExpression(
new ConstantExpression(inputReference.getPath()),
new ConstantExpression(inputReference.getLineNumber())))));
}
statements.add(new ReturnStatement(inputsVar));
node.addMethod(new MethodNode("inputReferences",
Modifier.PUBLIC,
INPUT_REFERENCES,
new Parameter[0],
new ClassNode[0],
new BlockStatement(statements, methodVarScope)));
// Generate sourceLocation() method
statements = new ArrayList<Statement>();
statements.add(new ReturnStatement(new ConstructorCallExpression(SOURCE_LOCATION,
new ArgumentListExpression(Arrays.<Expression>asList(
new ConstantExpression(SOURCE_URI_TOKEN),
new ConstantExpression(SOURCE_DESC_TOKEN),
new ConstantExpression(sourceLocation.getExpression()),
new ConstantExpression(sourceLocation.getLineNumber()),
new ConstantExpression(sourceLocation.getColumnNumber())
)))));
node.addMethod(new MethodNode("sourceLocation",
Modifier.PUBLIC,
SOURCE_LOCATION,
new Parameter[0],
new ClassNode[0],
new BlockStatement(statements, new VariableScope())));
}
}
public void visitRuleClosure(ClosureExpression expression, Expression invocation, String invocationDisplayName) {
InputReferences parentInputs = inputs;
VariableExpression parentInputsVariable = inputsVariable;
try {
inputs = new InputReferences();
inputsVariable = new VariableExpression("__rule_inputs_var_" + (counter++), POTENTIAL_INPUTS);
inputsVariable.setClosureSharedVariable(true);
super.visitClosureExpression(expression);
BlockStatement code = (BlockStatement) expression.getCode();
code.setNodeMetaData(AST_NODE_METADATA_LOCATION_KEY, new SourceLocation(location, scriptSourceDescription, invocationDisplayName, invocation.getLineNumber(), invocation.getColumnNumber()));
code.setNodeMetaData(AST_NODE_METADATA_INPUTS_KEY, inputs);
if (parentInputsVariable != null) {
expression.getVariableScope().putReferencedLocalVariable(parentInputsVariable);
}
code.getVariableScope().putDeclaredVariable(inputsVariable);
if (parentInputsVariable == null) {
// <inputs-lvar> = <inputs-field>
DeclarationExpression variableDeclaration = new DeclarationExpression(inputsVariable, ASSIGN, new VariableExpression(INPUTS_FIELD_NAME));
code.getStatements().add(0, new ExpressionStatement(variableDeclaration));
} else {
// <inputs-lvar> = <inputs-field> ?: <parent-inputs-lvar>
DeclarationExpression variableDeclaration = new DeclarationExpression(inputsVariable, ASSIGN,
new ElvisOperatorExpression(
new VariableExpression(INPUTS_FIELD_NAME),
parentInputsVariable));
code.getStatements().add(0, new ExpressionStatement(variableDeclaration));
}
// Move default values into body of closure, so they can use <inputs-lvar>
for (Parameter parameter : expression.getParameters()) {
if (parameter.hasInitialExpression()) {
code.getStatements().add(1, new ExpressionStatement(new BinaryExpression(new VariableExpression(parameter.getName()), ASSIGN, parameter.getInitialExpression())));
parameter.setInitialExpression(ConstantExpression.NULL);
}
}
} finally {
if (parentInputs != null) {
parentInputs.addNestedReferences(inputs);
}
inputs = parentInputs;
inputsVariable = parentInputsVariable;
}
}
@Override
public void visitClosureExpression(ClosureExpression expression) {
// Nested closure
nestingDepth++;
try {
expression.getVariableScope().putReferencedLocalVariable(inputsVariable);
super.visitClosureExpression(expression);
} finally {
nestingDepth--;
}
}
@Override
public void visitPropertyExpression(PropertyExpression expr) {
String modelPath = isDollarPathExpression(expr);
if (modelPath != null) {
inputs.ownReference(modelPath, expr.getLineNumber());
replaceVisitedExpressionWith(inputReferenceExpression(modelPath));
} else {
super.visitPropertyExpression(expr);
}
}
private String isDollarPathExpression(PropertyExpression expr) {
if (expr.isSafe() || expr.isSpreadSafe()) {
return null;
}
if (expr.getObjectExpression() instanceof VariableExpression) {
VariableExpression objectExpression = (VariableExpression) expr.getObjectExpression();
if (objectExpression.getName().equals(DOLLAR)) {
return expr.getPropertyAsString();
} else {
return null;
}
}
if (expr.getObjectExpression() instanceof PropertyExpression) {
PropertyExpression objectExpression = (PropertyExpression) expr.getObjectExpression();
String path = isDollarPathExpression(objectExpression);
if (path != null) {
return path + '.' + expr.getPropertyAsString();
} else {
return null;
}
}
return null;
}
@Override
public void visitExpressionStatement(ExpressionStatement stat) {
if (nestingDepth == 0 && stat.getExpression() instanceof MethodCallExpression) {
MethodCallExpression call = (MethodCallExpression) stat.getExpression();
if (call.isImplicitThis() && call.getArguments() instanceof ArgumentListExpression) {
ArgumentListExpression arguments = (ArgumentListExpression) call.getArguments();
if (!arguments.getExpressions().isEmpty()) {
Expression lastArg = arguments.getExpression(arguments.getExpressions().size() - 1);
if (lastArg instanceof ClosureExpression) {
// This is a potential nested rule.
// Visit method parameters
for (int i = 0; i < arguments.getExpressions().size() - 1; i++) {
arguments.getExpressions().set(i, replaceExpr(arguments.getExpression(i)));
}
// Transform closure
ClosureExpression closureExpression = (ClosureExpression) lastArg;
visitRuleClosure(closureExpression, call, displayName(call));
Expression replaced = new StaticMethodCallExpression(RULE_FACTORY, "decorate", new ArgumentListExpression(new VariableExpression(RULE_FACTORY_FIELD_NAME), closureExpression));
arguments.getExpressions().set(arguments.getExpressions().size() - 1, replaced);
return;
}
}
}
}
super.visitExpressionStatement(stat);
}
@Override
public void visitMethodCallExpression(MethodCallExpression call) {
String methodName = call.getMethodAsString();
if (call.isImplicitThis() && methodName != null && methodName.equals(DOLLAR)) {
visitInputMethod(call);
return;
}
// visit the method call, because one of the args may be an input method call
super.visitMethodCallExpression(call);
}
private void visitInputMethod(MethodCallExpression call) {
ConstantExpression argExpression = AstUtils.hasSingleConstantStringArg(call);
if (argExpression == null) { // not a valid signature
error(call, INVALID_ARGUMENT_LIST);
} else {
String modelPath = argExpression.getText();
if (modelPath.isEmpty()) {
error(argExpression, INVALID_ARGUMENT_LIST);
return;
}
try {
ModelPath.validatePath(modelPath);
} catch (ModelPath.InvalidPathException e) {
// TODO find a better way to present this information in the error message
// Attempt to mimic Gradle nested exception output
String message = "Invalid model path given as rule input." + SystemProperties.getInstance().getLineSeparator()
+ " > " + e.getMessage();
if (e.getCause() != null) {
// if there is a cause, it's an invalid name exception
message += SystemProperties.getInstance().getLineSeparator() + " > " + e.getCause().getMessage();
}
error(argExpression, message);
return;
}
inputs.ownReference(modelPath, call.getLineNumber());
replaceVisitedExpressionWith(inputReferenceExpression(modelPath));
}
}
private MethodCallExpression inputReferenceExpression(String modelPath) {
return new MethodCallExpression(new VariableExpression(inputsVariable), new ConstantExpression(GET), new ArgumentListExpression(new ConstantExpression(modelPath)));
}
private void error(ASTNode call, String message) {
SyntaxException syntaxException = new SyntaxException(message, call.getLineNumber(), call.getColumnNumber());
sourceUnit.getErrorCollector().addError(syntaxException, sourceUnit);
}
public static String displayName(MethodCallExpression expression) {
StringBuilder builder = new StringBuilder();
if (!expression.isImplicitThis()) {
builder.append(expression.getObjectExpression().getText());
builder.append('.');
}
builder.append(expression.getMethodAsString());
if (expression.getArguments() instanceof ArgumentListExpression) {
ArgumentListExpression arguments = (ArgumentListExpression) expression.getArguments();
boolean hasTrailingClosure = !arguments.getExpressions().isEmpty() && arguments.getExpression(arguments.getExpressions().size() - 1) instanceof ClosureExpression;
List<Expression> otherArgs = hasTrailingClosure ? arguments.getExpressions().subList(0, arguments.getExpressions().size() - 1) : arguments.getExpressions();
if (!otherArgs.isEmpty() || !hasTrailingClosure) {
builder.append("(");
builder.append(Joiner.on(", ").join(CollectionUtils.collect(otherArgs, new Transformer<Object, Expression>() {
@Override
public Object transform(Expression expression) {
return expression.getText();
}
})));
builder.append(")");
}
if (hasTrailingClosure) {
builder.append(" { ... }");
}
} else {
builder.append("()");
}
return builder.toString();
}
}