| /* |
| * 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(); |
| } |
| } |
| } |