blob: 8b75e7e294452a27c70d23f1da53dbbf376b8227 [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.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;
}
}