| /* |
| * 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.android.tools.idea.gradle.util.GradleUtil; |
| import com.android.tools.lint.checks.GradleDetector; |
| import com.google.common.base.Objects; |
| import com.google.common.collect.Lists; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiElement; |
| 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.api.statements.expressions.GrMethodCall; |
| import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner; |
| |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| |
| import static com.android.tools.idea.gradle.parser.ValueFactory.KeyFilter; |
| |
| /** |
| * GradleBuildFile uses PSI to parse build.gradle files and provides high-level methods to read and mutate the file. For many things in |
| * the file it uses a simple key/value interface to set and retrieve values. Since a user can potentially edit a build.gradle file by |
| * hand and make changes that we are unable to parse, there is also a |
| * {@link #canParseValue(BuildFileKey)} method that will query if the value can |
| * be edited by this class or not. |
| * |
| * 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}. |
| */ |
| public class GradleBuildFile extends GradleGroovyFile { |
| /** |
| * Used as a placeholder for a value in a build file we couldn't understand. We avoid overwriting these unless the explicit intent |
| * is to replace an unparseable value with a new, parseable one. |
| */ |
| public static final Object UNRECOGNIZED_VALUE = "Unrecognized value"; |
| |
| @Nullable |
| public static GradleBuildFile get(@NotNull Module module) { |
| VirtualFile file = GradleUtil.getGradleBuildFile(module); |
| return file != null ? new GradleBuildFile(file, module.getProject()) : null; |
| } |
| |
| public GradleBuildFile(@NotNull VirtualFile buildFile, @NotNull Project project) { |
| super(buildFile, project); |
| } |
| |
| @NotNull |
| public List<BuildFileStatement> getDependencies() { |
| Object dependencies = getValue(BuildFileKey.DEPENDENCIES); |
| if (dependencies == null) { |
| return Collections.emptyList(); |
| } |
| assert dependencies instanceof List; |
| //noinspection unchecked |
| return (List<BuildFileStatement>)dependencies; |
| } |
| |
| /** |
| * Returns the value in the file for the given key, or null if not present. |
| */ |
| public @Nullable Object getValue(@NotNull BuildFileKey key) { |
| checkInitialized(); |
| return getValue(myGroovyFile, key); |
| } |
| |
| /** |
| * Returns the value in the file for the given key, or null if not present. |
| */ |
| public @Nullable Object getValue(@Nullable GrStatementOwner root, @NotNull BuildFileKey key) { |
| checkInitialized(); |
| if (root == null) { |
| root = myGroovyFile; |
| } |
| return getValueStatic(root, key); |
| } |
| |
| /** |
| * Given a path to a method, returns the first argument of that method that is a closure, or null. |
| */ |
| public @Nullable GrStatementOwner getClosure(String path) { |
| checkInitialized(); |
| GrMethodCall method = getMethodCallByPath(myGroovyFile, path); |
| if (method == null) { |
| return null; |
| } |
| return getMethodClosureArgument(method); |
| } |
| |
| /** |
| * Modifies the value in the file. Must be run inside a write action. |
| */ |
| public void setValue(@NotNull BuildFileKey key, @NotNull Object value) { |
| checkInitialized(); |
| commitDocumentChanges(); |
| setValue(myGroovyFile, key, value, null); |
| } |
| |
| /** |
| * Modifies the value in the file. Must be run inside a write action. |
| */ |
| public void setValue(@Nullable GrStatementOwner root, @NotNull BuildFileKey key, @NotNull Object value) { |
| checkInitialized(); |
| commitDocumentChanges(); |
| setValue(root, key, value, null); |
| } |
| |
| /** |
| * Modifies the value in the file. Must be run inside a write action. The filter is intended for composite value types (e.g. |
| * {@link com.android.tools.idea.gradle.parser.NamedObject} and allows greater control over whether a sub-key gets written |
| * out. |
| */ |
| public void setValue(@NotNull BuildFileKey key, @NotNull Object value, @Nullable KeyFilter filter) { |
| checkInitialized(); |
| commitDocumentChanges(); |
| setValue(myGroovyFile, key, value, filter); |
| } |
| |
| /** |
| * Modifies the value in the file. Must be run inside a write action. The filter is intended for composite value types (e.g. |
| * {@link com.android.tools.idea.gradle.parser.NamedObject} and allows greater control over whether a sub-key gets written |
| * out. |
| */ |
| public void setValue(@Nullable GrStatementOwner root, @NotNull BuildFileKey key, @NotNull Object value, @Nullable KeyFilter filter) { |
| checkInitialized(); |
| commitDocumentChanges(); |
| if (root == null) { |
| root = myGroovyFile; |
| } |
| setValueStatic(root, key, value, true, filter); |
| } |
| |
| /** |
| * If the given key has a value at the given root, removes it and returns true. Returns false if there is no value for that key. |
| */ |
| public boolean removeValue(@Nullable GrStatementOwner root, @NotNull BuildFileKey key) { |
| checkInitialized(); |
| commitDocumentChanges(); |
| if (root == null) { |
| root = myGroovyFile; |
| } |
| GrMethodCall method = getMethodCallByPath(root, key.getPath()); |
| if (method != null) { |
| GrStatementOwner parent = (GrStatementOwner)method.getParent(); |
| parent.removeElements(new PsiElement[]{method}); |
| reformatClosure(parent); |
| return true; |
| } |
| return false; |
| } |
| |
| public boolean hasDependency(@NotNull BuildFileStatement statement) { |
| List<BuildFileStatement> currentDeps = (List<BuildFileStatement>)getValue(BuildFileKey.DEPENDENCIES); |
| if (currentDeps == null) { |
| return false; |
| } |
| return hasDependency(currentDeps, statement); |
| } |
| |
| public static boolean hasDependency(@NotNull List<BuildFileStatement> currentDeps, @NotNull BuildFileStatement statement) { |
| if (currentDeps.contains(statement)) { |
| return true; |
| } |
| if (!(statement instanceof Dependency)) { |
| return false; |
| } |
| for (BuildFileStatement currentStatement : currentDeps) { |
| if (currentStatement instanceof Dependency && ((Dependency)currentStatement).matches((Dependency)statement)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns a list of all the plugins used by the given build file. |
| */ |
| @NotNull |
| public static List<String> getPlugins(GroovyFile buildScript) { |
| List<String> plugins = Lists.newArrayListWithExpectedSize(1); |
| for (GrMethodCall methodCall : getMethodCalls(buildScript, "apply")) { |
| Map<String,Object> values = getNamedArgumentValues(methodCall); |
| Object plugin = values.get("plugin"); |
| if (plugin != null) { |
| plugins.add(plugin.toString()); |
| } |
| } |
| return plugins; |
| } |
| |
| /** |
| * Returns a list of all the plugins used by the build file. |
| */ |
| @NotNull |
| public List<String> getPlugins() { |
| return getPlugins(myGroovyFile); |
| } |
| |
| /** |
| * Returns true if the build file uses the android or android-library plugin. |
| */ |
| public boolean hasAndroidPlugin() { |
| List<String> plugins = getPlugins(); |
| return plugins.contains(GradleDetector.APP_PLUGIN_ID) || plugins.contains(GradleDetector.OLD_APP_PLUGIN_ID) || |
| plugins.contains(GradleDetector.LIB_PLUGIN_ID) || plugins.contains(GradleDetector.OLD_LIB_PLUGIN_ID); |
| } |
| |
| /** |
| * Returns true if the current and new values differ in a way that should cause us to write them out to the build file. This differs from |
| * simple object equality in that if the only differences between current and new are in unparseable objects, then we ignore those |
| * differences for the purpose of this check -- since we don't understand unparseable statements, we can't meaningfully perform object |
| * equality checks on them and we should endeavor to not write them back out to the file if we can avoid it. |
| */ |
| public static boolean shouldWriteValue(@Nullable Object currentValue, @Nullable Object newValue) { |
| if (Objects.equal(currentValue, newValue)) { |
| return false; |
| } |
| // If it's a list type, then iterate though the elements. If each element is equal or if both the current and new values at a given list |
| // position are both unparseable, then we don't need to write it out. |
| if (!(currentValue instanceof List && newValue instanceof List)) { |
| return true; |
| } |
| List currentList = (List)currentValue; |
| List newList = (List)newValue; |
| if (currentList.size() != newList.size()) { |
| return true; |
| } |
| for (int i = 0; i < currentList.size(); i++) { |
| Object currentObj = currentList.get(i); |
| Object newObj = newList.get(i); |
| if (!currentObj.equals(newObj) && !(currentObj instanceof UnparseableStatement && newObj instanceof UnparseableStatement)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |