blob: f00757a075dc747af3f2c0f274401f967419a053 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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 com.android.tools.idea.gradle.parser;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.codeInsight.actions.ReformatCodeProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.GrListOrMap;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrNamedArgument;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrCall;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static com.android.tools.idea.gradle.parser.BuildFileKey.escapeLiteralString;
/**
* Base class for classes that parse Gradle Groovy files (e.g. settings.gradle, build.gradle). It provides a number of convenience
* methods for its subclasses to extract interesting pieces from Gradle files.
*
* Note that if you do any mutations on the PSI structure you must be inside a write action. See
* {@link com.intellij.util.ActionRunner#runInsideWriteAction}.
*/
class GradleGroovyFile {
private static final Logger LOG = Logger.getInstance(GradleGroovyFile.class);
protected final Project myProject;
protected final VirtualFile myFile;
protected GroovyFile myGroovyFile = null;
public GradleGroovyFile(@NotNull VirtualFile file, @NotNull Project project) {
myProject = project;
myFile = file;
reload();
}
public Project getProject() {
return myProject;
}
/**
* Automatically reformats all the Groovy code inside the given closure.
*/
static void reformatClosure(@NotNull GrStatementOwner closure) {
new ReformatCodeProcessor(closure.getProject(), closure.getContainingFile(), closure.getParent().getTextRange(), false)
.runWithoutProgress();
// Now strip out any blank lines. They tend to accumulate otherwise. To do this, we iterate through our elements and find those that
// consist only of whitespace, and eliminate all double-newline occurrences.
for (PsiElement psiElement : closure.getChildren()) {
if (psiElement instanceof LeafPsiElement) {
String text = psiElement.getText();
if (StringUtil.isEmptyOrSpaces(text)) {
String newText = text;
while (newText.contains("\n\n")) {
newText = newText.replaceAll("\n\n", "\n");
}
if (!newText.equals(text)) {
((LeafPsiElement)psiElement).replaceWithText(newText);
}
}
}
}
}
/**
* Creates a new, blank-valued property at the given path.
*/
@Nullable
static GrMethodCall createNewValue(@NotNull GrStatementOwner root,
@NotNull BuildFileKey key,
@Nullable Object value,
boolean reformatClosure) {
// First iterate through the components of the path and make sure all of the nested closures are in place.
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(root.getProject());
String path = key.getPath();
String[] parts = path.split("/");
GrStatementOwner parent = root;
for (int i = 0; i < parts.length - 1; i++) {
String part = parts[i];
GrStatementOwner closure = getMethodClosureArgument(parent, part);
if (closure == null) {
parent.addStatementBefore(factory.createStatementFromText(part + " {}"), null);
closure = getMethodClosureArgument(parent, part);
if (closure == null) {
return null;
}
}
parent = closure;
}
String name = parts[parts.length - 1];
String text = name + " " + key.getType().convertValueToExpression(value);
GrStatement statementBefore = null;
if (key.shouldInsertAtBeginning()) {
GrStatement[] parentStatements = parent.getStatements();
if (parentStatements.length > 0) {
statementBefore = parentStatements[0];
}
}
parent.addStatementBefore(factory.createStatementFromText(text), statementBefore);
if (reformatClosure) {
reformatClosure(parent);
}
return getMethodCall(parent, name);
}
/**
* Refreshes its state by rereading the contents from the underlying build file.
*/
public void reload() {
StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() {
@Override
public void run() {
if (!myFile.exists()) {
LOG.warn("File " + myFile.getPath() + " no longer exists");
return;
}
PsiFile psiFile = PsiManager.getInstance(myProject).findFile(myFile);
if (psiFile == null) {
LOG.warn("Could not find PsiFile for " + myFile.getPath());
return;
}
if (!(psiFile instanceof GroovyFile)) {
LOG.warn("PsiFile " + psiFile.getName() + " is not a Groovy file");
return;
}
myGroovyFile = (GroovyFile)psiFile;
onPsiFileAvailable();
}
});
}
public VirtualFile getFile() {
return myFile;
}
public PsiFile getPsiFile() {
return myGroovyFile;
}
/**
* @throws IllegalStateException if the instance has not parsed its PSI file yet in a
* {@link StartupManager#runWhenProjectIsInitialized(Runnable)} callback. To resolve this, wait until {@link #onPsiFileAvailable()}
* is called before invoking methods.
*/
protected void checkInitialized() {
if (myGroovyFile == null) {
throw new IllegalStateException("PsiFile not parsed for file " + myFile.getPath() +". Wait until onPsiFileAvailable() is called.");
}
}
/**
* Commits any {@link Document} changes outstanding to the document that underlies the PSI representation. Call this before manipulating
* PSI to ensure the PSI model of the document is in sync with what's on disk. Must be run inside a write action.
*/
protected void commitDocumentChanges() {
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
Document document = documentManager.getDocument(myGroovyFile);
if (document != null) {
documentManager.commitDocument(document);
}
}
/**
* Finds a method identified by the given path. It descends into nested closure arguments of method calls to find the leaf method.
* For example, if you have this code in Groovy:
*
* method_a {
* method_b {
* method_c 'literal'
* }
* }
*
* you can find method_c using the path "method_a/method_b/method_c"
*
* It returns the first eligible result. If you have code like this:
*
* method_a {
* method_b 'literal1'
* method_b 'literal2'
* }
*
* a search for "method_a/method_b" will only return the first invocation of method_b.
*
* It continues searching until it has exhausted all possibilities of finding the path. If you have code like this:
*
* method_a {
* method_b 'literal1'
* }
*
* method_a {
* method_c 'literal2'
* }
*
* a search for "method_a/method_c" will succeed: it will not give up just because it doesn't find it in the first method_a block.
*
* @param root the block to use as the root of the path
* @param path the slash-delimited chain of methods with closure arguments to navigate to find the final leaf.
* @return the resultant method, or null if it could not be found.
*/
protected static @Nullable GrMethodCall getMethodCallByPath(@NotNull GrStatementOwner root, @NotNull String path) {
if (path.isEmpty() || path.endsWith("/")) {
return null;
}
int slash = path.indexOf('/');
String pathElement = slash == -1 ? path : path.substring(0, slash);
for (GrMethodCall gmc : getMethodCalls(root, pathElement)) {
if (slash == -1) {
return gmc;
}
if (gmc == null) {
return null;
}
GrClosableBlock[] blocks = gmc.getClosureArguments();
if (blocks.length != 1) {
return null;
}
GrMethodCall subresult = getMethodCallByPath(blocks[0], path.substring(slash + 1));
if (subresult != null) {
return subresult;
}
}
return null;
}
/**
* Returns an array of arguments for the given method call. Note that it returns only regular arguments (via the
* {@link org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall#getArgumentList()} call), not closure arguments
* (via the {@link org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall#getClosureArguments()} call).
*
* @return the array of arguments. This method never returns null; even in the case of a nonexistent argument list or error, it will
* return an empty array.
*/
protected static @NotNull GroovyPsiElement[] getArguments(@NotNull GrCall gmc) {
GrArgumentList argList = gmc.getArgumentList();
if (argList == null) {
return GroovyPsiElement.EMPTY_ARRAY;
}
return argList.getAllArguments();
}
/**
* Returns the first argument for the given method call (which can be a literal, expression, or closure), or null if the method has
* no arguments.
*/
protected static @Nullable GroovyPsiElement getFirstArgument(@NotNull GrCall gmc) {
GroovyPsiElement[] arguments = getArguments(gmc);
return arguments.length > 0 ? arguments[0] : null;
}
/**
* Ensures that argument list has only one argument and that the argument value is a string constant.
*
* @return a string value of the argument or <code>null</code> if such a value cannot be deduced
*/
@Nullable
protected static String getSingleStringArgumentValue(@NotNull GrCall methodCall) {
GroovyPsiElement argument = getFirstArgument(methodCall);
if (argument instanceof GrLiteral) {
Object value = ((GrLiteral)argument).getValue();
return value instanceof String ? (String)value : null;
}
else {
return null;
}
}
/**
* Returns the first method call of the given method name in the given parent statement block, or null if one could not be found.
*/
protected static @Nullable GrMethodCall getMethodCall(@NotNull GrStatementOwner parent, @NotNull String methodName) {
return Iterables.getFirst(getMethodCalls(parent, methodName), null);
}
/**
* Returns all statements in the parent statement block that are method calls.
*/
protected static @NotNull Iterable<GrMethodCall> getMethodCalls(@NotNull GrStatementOwner parent) {
return Iterables.filter(Arrays.asList(parent.getStatements()), GrMethodCall.class);
}
/**
* Returns all statements in the parent statement block that are method calls with the given method name
*/
protected static @NotNull Iterable<GrMethodCall> getMethodCalls(@NotNull GrStatementOwner parent, @NotNull final String methodName) {
return Iterables.filter(getMethodCalls(parent), new Predicate<GrMethodCall>() {
@Override
public boolean apply(@Nullable GrMethodCall input) {
return input != null && methodName.equals(getMethodCallName(input));
}
});
}
/**
* Returns the name of the given method call
*/
protected static @NotNull String getMethodCallName(@NotNull GrMethodCall gmc) {
GrExpression expression = gmc.getInvokedExpression();
return expression.getText() != null ? expression.getText() : "";
}
/**
* Returns all arguments in the given argument list that are of the given type.
*/
protected static @NotNull <E> Iterable<E> getTypedArguments(@NotNull GrArgumentList args, @NotNull Class<E> clazz) {
return Iterables.filter(Arrays.asList(args.getAllArguments()), clazz);
}
/**
* Returns all arguments of the given method call that are literals.
*/
protected static @NotNull Iterable<GrLiteral> getLiteralArguments(@NotNull GrMethodCall gmc) {
GrArgumentList argumentList = gmc.getArgumentList();
return getTypedArguments(argumentList, GrLiteral.class);
}
/**
* Returns values of all literal-typed arguments of the given method call.
*/
protected static @NotNull Iterable<Object> getLiteralArgumentValues(@NotNull GrMethodCall gmc) {
return Iterables.filter(Iterables.transform(getLiteralArguments(gmc), new Function<GrLiteral, Object>() {
@Override
public Object apply(@Nullable GrLiteral input) {
return (input != null) ? input.getValue() : null;
}
}), Predicates.notNull());
}
/**
* If the given method takes named arguments, returns those arguments as a name:value map. Returns an empty map otherwise.
*/
protected static @NotNull Map<String, Object> getNamedArgumentValues(@NotNull GrMethodCall gmc) {
GrArgumentList argumentList = gmc.getArgumentList();
Map<String, Object> values = Maps.newHashMap();
for (GrNamedArgument grNamedArgument : getTypedArguments(argumentList, GrNamedArgument.class)) {
values.put(grNamedArgument.getLabelName(), parseValueExpression(grNamedArgument.getExpression()));
}
return values;
}
/**
* Given a Groovy expression, parses it as if it's literal or list type, and returns the corresponding literal value or List
* type. Returns null if the expression cannot be evaluated as a literal or list type.
*/
protected static @Nullable Object parseValueExpression(@Nullable GrExpression gre) {
if (gre instanceof GrLiteral) {
return ((GrLiteral)gre).getValue();
} else if (gre instanceof GrListOrMap) {
GrListOrMap grLom = (GrListOrMap)gre;
if (grLom.isMap()) {
return null;
}
List<Object> values = Lists.newArrayList();
for (GrExpression subexpression : grLom.getInitializers()) {
Object subValue = parseValueExpression(subexpression);
if (subValue != null) {
values.add(subValue);
}
}
return values;
} else {
return null;
}
}
/**
* Returns a text string with the Groovy expression that will represent the given map as a named argument list suitable for use in
* a method call.
*/
protected static @NotNull String convertMapToGroovySource(@NotNull Map<String, Object> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(entry.getKey());
sb.append(": ");
sb.append(convertValueToGroovySource(entry.getValue()));
}
return sb.toString();
}
/**
* Returns a text string with the Groovy expression that will represent the given object. It can be a literal type or a list of
* literals or sub-lists.
*/
protected static @NotNull String convertValueToGroovySource(@NotNull Object value) {
if (value instanceof List) {
StringBuilder sb = new StringBuilder();
sb.append('[');
for (Object v : ((List)value)) {
if (sb.length() > 1) {
sb.append(", ");
}
sb.append(convertValueToGroovySource(v));
}
sb.append(']');
return sb.toString();
} else if (value instanceof Number || value instanceof Boolean) {
return value.toString();
} else {
return "'" + escapeLiteralString(value.toString()) + "'";
}
}
/**
* Returns the value of the first literal argument in the given method call's argument list.
*/
protected static @Nullable Object getFirstLiteralArgumentValue(@NotNull GrMethodCall gmc) {
GrLiteral lit = getFirstLiteralArgument(gmc);
return lit != null ? lit.getValue() : null;
}
/**
* Returns the first literal argument in the given method call's argument list.
*/
protected static @Nullable GrLiteral getFirstLiteralArgument(@NotNull GrMethodCall gmc) {
return Iterables.getFirst(getLiteralArguments(gmc), null);
}
/**
* Returns the first argument of the method call with the given name in the given parent that is a closure, or null if the method
* or its closure could not be found.
*/
protected static @Nullable GrClosableBlock getMethodClosureArgument(@NotNull GrStatementOwner parent, @NotNull String methodName) {
GrMethodCall methodCall = getMethodCall(parent, methodName);
if (methodCall == null) {
return null;
}
return getMethodClosureArgument(methodCall);
}
/**
* Returns the first argument of the given method call that is a closure, or null if the closure could not be found.
*/
public static @Nullable GrClosableBlock getMethodClosureArgument(@NotNull GrMethodCall methodCall) {
return Iterables.getFirst(Arrays.asList(methodCall.getClosureArguments()), null);
}
/**
* Returns the value in the file for the given key, or null if not present.
*/
static @Nullable Object getValueStatic(@NotNull GrStatementOwner root, @NotNull BuildFileKey key) {
GrMethodCall method = getMethodCallByPath(root, key.getPath());
if (method == null) {
return null;
}
GroovyPsiElement arg = key.getType() == BuildFileKeyType.CLOSURE ? getMethodClosureArgument(method) : getFirstArgument(method);
if (arg == null) {
return null;
}
return key.getValue(arg);
}
/**
* Sets the value for the given key
*/
static void setValueStatic(@NotNull GrStatementOwner root, @NotNull BuildFileKey key, @NotNull Object value, boolean reformatClosure,
@Nullable ValueFactory.KeyFilter filter) {
if (value == GradleBuildFile.UNRECOGNIZED_VALUE) {
return;
}
GrMethodCall method = getMethodCallByPath(root, key.getPath());
if (method == null) {
method = createNewValue(root, key, value, reformatClosure);
if (key.getType() != BuildFileKeyType.CLOSURE) {
return;
}
}
if (method != null) {
GroovyPsiElement arg = key.getType() == BuildFileKeyType.CLOSURE ? getMethodClosureArgument(method) : getFirstArgument(method);
if (arg == null) {
return;
}
key.setValue(arg, value, filter);
}
}
/**
* Removes the build file value identified by the given key
*/
static void removeValueStatic(@NotNull GrStatementOwner root, @NotNull BuildFileKey key) {
GrMethodCall method = getMethodCallByPath(root, key.getPath());
if (method != null) {
method.delete();
}
}
/**
* Override this method if you wish to be called when the underlying PSI file is available and you would like to parse it.
*/
protected void onPsiFileAvailable() {
}
@Override
public @NotNull String toString() {
if (myGroovyFile == null) {
return "<uninitialized>";
} else {
ToStringPsiVisitor visitor = new ToStringPsiVisitor();
myGroovyFile.accept(visitor);
return myFile.getPath() + ":\n" + visitor.toString();
}
}
private static class ToStringPsiVisitor extends PsiRecursiveElementVisitor {
private StringBuilder myString = new StringBuilder();
@Override
public void visitElement(final @NotNull PsiElement element) {
PsiElement e = element;
while (e.getParent() != null) {
myString.append(" ");
e = e.getParent();
}
myString.append(element.getClass().getName());
myString.append(": ");
myString.append(element.toString());
if (element instanceof LeafPsiElement) {
myString.append(" ");
myString.append(element.getText());
}
myString.append("\n");
super.visitElement(element);
}
public @NotNull String toString() {
return myString.toString();
}
}
}