blob: 5665657241f77093c30b6be8ea4042425809932e [file] [log] [blame]
/*
* Copyright (C) 2015 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.editor.parser;
import com.android.ide.common.repository.GradleCoordinate;
import com.android.tools.idea.gradle.editor.entity.GradleEditorEntity;
import com.android.tools.idea.gradle.editor.entity.GradleEditorEntityGroup;
import com.android.tools.idea.gradle.editor.entity.VersionGradleEditorEntity;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.io.Files;
import com.intellij.lang.ASTNode;
import com.intellij.lang.LanguageParserDefinitions;
import com.intellij.lang.ParserDefinition;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import org.jetbrains.plugins.groovy.GroovyLanguage;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementVisitor;
import org.jetbrains.plugins.groovy.lang.psi.GroovyRecursiveElementVisitor;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrApplicationStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrAssignmentExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import static com.android.tools.idea.gradle.editor.parser.GradleEditorModelParseContext.*;
/**
* Entry point for functionality of {@link #parse(VirtualFile, Project) building} {@link GradleEditorEntity} objects from
* the target <code>build.gradle</code> file.
*/
public class GradleEditorModelParserFacade {
private static final List<GradleEditorModelParser> ourParsers = Lists.<GradleEditorModelParser>newArrayList(
new GradleEditorModelParserV1()
);
private static final Logger LOG = Logger.getInstance(GradleEditorModelParserFacade.class);
@NotNull
public List<GradleEditorEntityGroup> parse(@NotNull VirtualFile virtualFile, @NotNull Project project) {
PsiManager psiManager = PsiManager.getInstance(project);
PsiFile psiFile = psiManager.findFile(virtualFile);
if (psiFile == null) {
LOG.warn(String.format("Can't build PSI for the gradle config file '%s'", virtualFile.getCanonicalPath()));
return Collections.emptyList();
}
GradleEditorModelParseContext context = new GradleEditorModelParseContext(virtualFile, project);
// This a two-steps process:
// 1. Gradle config's PSI is parsed and the context is filled by assignments data;
// 2. That data is given for further processing and actual building of model entities;
fillContext(context, psiFile);
for (VirtualFile dir = virtualFile.getParent(); dir != null; dir = dir.getParent()) {
File settingsIoFile = new File(dir.getCanonicalPath(), GradleConstants.SETTINGS_FILE_NAME);
if (!settingsIoFile.isFile()) {
// Go up if there is no settings.gradle file in the current dir
continue;
}
if (isParentProject(settingsIoFile, virtualFile)) {
File parentIoFile = new File(dir.getCanonicalPath(), GradleConstants.DEFAULT_SCRIPT_NAME);
VirtualFile parentVFile = LocalFileSystem.getInstance().findFileByIoFile(parentIoFile);
if (parentVFile != null) {
parentVFile.refresh(false, false);
PsiFile parentPsiFile = psiManager.findFile(parentVFile);
if (parentPsiFile != null) {
context.onChangeFile(parentVFile);
fillContext(context, parentPsiFile);
}
}
}
break;
}
return buildEntities(context);
}
private static boolean isParentProject(@NotNull File settingsFile, @NotNull VirtualFile targetConfigFile) {
try {
ImmutableList<String> lines = Files.asCharSource(settingsFile, Charset.forName("UTF-8")).readLines();
String startLineMarker = "include ";
for (String line : lines) {
if (!line.startsWith(startLineMarker)) {
continue;
}
List<String> subProjects = Lists.newArrayList();
for (String s : Splitter.on(",").trimResults().omitEmptyStrings().split(line.substring(startLineMarker.length()))) {
// Sub-projects are defined as strings with leading colon, e.g. include ':app'.
s = GradleEditorModelUtil.unquote(s);
if (s.startsWith(":")) {
s = s.substring(1);
}
subProjects.add(s);
}
List<String> dirs = Lists.newArrayList();
LocalFileSystem fileSystem = LocalFileSystem.getInstance();
VirtualFile rootDir = fileSystem.refreshAndFindFileByIoFile(settingsFile.getParentFile());
if (rootDir == null) {
return false;
}
for (VirtualFile dir = targetConfigFile.getParent(); dir != null; dir = dir.getParent()) {
if (rootDir.equals(dir)) {
break;
}
dirs.add(dir.getName());
}
Collections.reverse(dirs);
int i = 0;
for (String subProject : subProjects) {
if (i >= dirs.size() || !subProject.equals(dirs.get(i++))) {
return false;
}
}
return true;
}
}
catch (IOException e) {
LOG.warn("Unexpected exception occurred on attempt to read contents of file " + settingsFile.getAbsolutePath());
}
return false;
}
/**
* Processes given PSI file and fills given context
* by {@link GradleEditorModelParseContext#getAssignments(Variable) corresponding assignments}.
*
* @param context context to fill
* @param psiFile psi file to parse
*/
private static void fillContext(@NotNull final GradleEditorModelParseContext context, @NotNull PsiFile psiFile) {
psiFile.acceptChildren(new GroovyPsiElementVisitor(new GroovyRecursiveElementVisitor() {
@Override
public void visitMethodCallExpression(GrMethodCallExpression methodCallExpression) {
Pair<String,TextRange> pair = GradleEditorValueExtractor.extractMethodName(methodCallExpression);
GrClosableBlock[] closureArguments = methodCallExpression.getClosureArguments();
if (pair == null || closureArguments.length > 1) {
super.visitMethodCallExpression(methodCallExpression);
return;
}
if (closureArguments.length == 0) {
if (methodCallExpression.getArgumentList().getAllArguments().length == 0) {
// This is a no-args method, so, we just register it for cases like 'mavenCentral()' or 'jcenter()'.
context.addCachedValue(NO_ARGS_METHOD_ASSIGNMENT_VALUE,
TextRange.create(pair.second.getEndOffset(), methodCallExpression.getTextRange().getEndOffset()));
context.registerAssignmentFromCachedData(pair.first, pair.second, methodCallExpression);
}
return;
}
context.onMethodEnter(pair.getFirst());
try {
super.visitClosure(closureArguments[0]);
}
finally {
context.onMethodExit();
}
}
@Override
public void visitApplicationStatement(GrApplicationStatement applicationStatement) {
Pair<String,TextRange> methodName = GradleEditorValueExtractor.extractMethodName(applicationStatement);
if (methodName == null) {
return;
}
GroovyPsiElement[] allArguments = applicationStatement.getArgumentList().getAllArguments();
if (allArguments.length == 1) {
context.resetCaches();
extractValueOrVariable(allArguments[0], context);
context.registerAssignmentFromCachedData(methodName.getFirst(), methodName.getSecond(), applicationStatement.getArgumentList());
}
}
@Override
public void visitAssignmentExpression(GrAssignmentExpression expression) {
// General idea is to try to extract variable from the given expression and, in case of success, try to extract rvalue and
// register corresponding assignment with them.
context.resetCaches();
extractValueOrVariable(expression.getLValue(), context);
Multimap<Variable, Location> vars = context.getCachedVariables();
if (vars.size() != 1) {
context.resetCaches();
return;
}
Map.Entry<Variable, Location> entry = vars.entries().iterator().next();
Variable lVariable = entry.getKey();
Location lVariableLocation = entry.getValue();
context.resetCaches();
GrExpression rValue = expression.getRValue();
if (rValue == null) {
return;
}
extractValueOrVariable(rValue, context);
if (context.getCachedValues().size() > 1) {
Value value = new Value("", new Location(context.getCurrentFile(), GradleEditorModelUtil.interestedRange(rValue)));
context.setCachedValues(Collections.singletonList(value));
}
context.registerAssignmentFromCachedData(lVariable, lVariableLocation, rValue);
context.resetCaches();
}
@Override
public void visitVariable(GrVariable variable) {
TextRange nameRange = null;
boolean lookForInitializer = false;
ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.findSingle(GroovyLanguage.INSTANCE);
for (PsiElement e = variable.getFirstChild(); e != null; e = e.getNextSibling()) {
ASTNode node = e.getNode();
if (node == null) {
continue;
}
if (!lookForInitializer) {
if (node.getElementType() == GroovyTokenTypes.mIDENT) {
nameRange = e.getTextRange();
}
else if (node.getElementType() == GroovyTokenTypes.mASSIGN) {
if (nameRange == null) {
return;
}
lookForInitializer = true;
}
continue;
}
if (node.getElementType() == GroovyTokenTypes.mNLS || node.getElementType() == GroovyTokenTypes.mSEMI) {
break;
}
if (parserDefinition.getWhitespaceTokens().contains(node.getElementType())) {
continue;
}
extractValueOrVariable(e, context);
if (context.getCachedValues().size() > 1) {
Value value = new Value("", new Location(context.getCurrentFile(), GradleEditorModelUtil.interestedRange(e)));
context.setCachedValues(Collections.singletonList(value));
}
if (context.registerAssignmentFromCachedData(variable.getName(), nameRange, e)) {
return;
}
}
}
}));
}
/**
* @see GradleEditorValueExtractor#extractValueOrVariable(PsiElement)
*/
private static void extractValueOrVariable(@NotNull PsiElement element, @NotNull final GradleEditorModelParseContext context) {
new GradleEditorValueExtractor(context).extractValueOrVariable(element);
}
@NotNull
private static List<GradleEditorEntityGroup> buildEntities(@NotNull GradleEditorModelParseContext context) {
VersionGradleEditorEntity entity = GradleEditorModelParserV1.buildGradlePluginVersion(context);
GradleCoordinate androidGradlePluginVersion = null;
if (entity != null) {
String currentVersion = entity.getCurrentValue();
if (!currentVersion.isEmpty()) {
androidGradlePluginVersion = GradleCoordinate.parseVersionOnly(currentVersion);
}
}
if (androidGradlePluginVersion == null) {
androidGradlePluginVersion = GradleCoordinate.parseVersionOnly("0");
}
Comparator<GradleCoordinate> c = GradleCoordinate.COMPARE_PLUS_HIGHER;
for (GradleEditorModelParser parser : ourParsers) {
if (c.compare(androidGradlePluginVersion, parser.getMinSupportedAndroidGradlePluginVersion()) >= 0
&& c.compare(androidGradlePluginVersion, parser.getMaxSupportedAndroidGradlePluginVersion()) < 0) {
return parser.buildEntities(context);
}
}
return Collections.emptyList();
}
}