blob: 5468626ba42d54bbaeac15c7478ea29f1485d376 [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.annotations.NonNull;
import com.android.tools.idea.gradle.editor.entity.GradleEditorEntity;
import com.google.common.collect.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Utility class which stores intermediate information during building {@link GradleEditorEntity} from the <code>build.gradle</code>.
*/
public class GradleEditorModelParseContext {
/**
* Used as an {@link Assignment#value} for no-args methods like <code>'jcenter()'</code>.
*/
public static final String NO_ARGS_METHOD_ASSIGNMENT_VALUE = "()";
private final Multimap<Variable, Assignment> myAssignmentsByVariable = HashMultimap.create();
private final Multimap<List<String>, Assignment> myAssignmentsByCodeStructure = HashMultimap.create();
/**
* Holds information about nested named code blocks, e.g. it's empty initially, holds 'buildscript' when we start parsing that section,
* 'dependencies' (for the nested block) etc.
*/
private final List<String> myCodeStructure = Lists.newArrayList();
/**
* There is a possible case that a variable is defined like <code>'a.b.c = 1'</code>. Here variable name is 'c' and it's qualifier
* is <code>'a.b'</code>. Parsing processing is iterative and we first parse <code>'a'</code> qualifier part,
* then <code>'b'</code> qualifier part and finally <code>'c'</code> variable.
* <p/>
* Current list is assumed to be ['a', 'b'] (at that order) when <code>'c'</code> is being parsed.
*/
private final List<String> myCachedVariableQualifier = Lists.newArrayList();
private final Multimap<Variable, Location> myCachedVariables = HashMultimap.create();
private final List<Value> myCachedValues = Lists.newArrayList();
@NotNull private final VirtualFile myTargetFile;
@NotNull private final Project myProject;
@NotNull private VirtualFile myCurrentFile;
public GradleEditorModelParseContext(@NotNull VirtualFile targetFile, @NotNull Project project) {
myTargetFile = targetFile;
myCurrentFile = targetFile;
myProject = project;
}
/**
* @return virtual file for the target <code>build.gradle</code> being parsed
*/
@NotNull
public VirtualFile getTargetFile() {
return myTargetFile;
}
/**
* There is a possible case that we parse more than one <code>build.gradle</code> file - e.g. consider a multi-project. We parse
* either parent or child <code>build.gradle</code> files then (e.g. parent file might declare external dependencies at
* <code>'subprojects'</code> section which effectively modify child's configuration).
* <p/>
* Target file is exposed via {@link #getTargetFile()} and current file being parsed is exposed by this method.
*
* @return current gradle config file being parsed
*/
@NotNull
public VirtualFile getCurrentFile() {
return myCurrentFile;
}
@NotNull
public Project getProject() {
return myProject;
}
/**
* There is a notion of 'cached values' hold within the context - their values are accumulated during complex expressions
* parsing and are later flushed during {@link #registerAssignmentFromCachedData(Variable, Location, PsiElement)}.
* <p/>
* This property exposes values set previously via {@link #setCachedValues(List)} or {@link #addCachedValue(String, TextRange)}.
*
* @return cached values registered within the current context
*/
@NotNull
public List<Value> getCachedValues() {
return myCachedValues;
}
/**
* Registers a cached value within the current context.
*
* @param value a value's text to register within the current context
* @param range target value's range
* @see #getCachedValues()
*/
public void addCachedValue(@NotNull String value, @NotNull TextRange range) {
addCachedValue(new Value(value, new Location(myCurrentFile, range)));
}
/**
* @param value a value to register within the current context
* @see #getCachedValues()
*/
public void addCachedValue(@Nullable Value value) {
myCachedValues.add(value);
}
/**
* @param values values to use within the current context
* @see #getCachedValues()
*/
public void setCachedValues(@NotNull List<Value> values) {
myCachedValues.clear();
myCachedValues.addAll(values);
}
/**
* @return similar to {@link #getCachedValues()} but not for variables but values registered within the curent context
* @see #getCachedValues()
*/
@NotNull
public Multimap<Variable, Location> getCachedVariables() {
return myCachedVariables;
}
/**
* @param variableName name of the variable to register within the current context
* ({@link #myCachedVariableQualifier cached qualifier} is used for the variable)
* @param range target variable's range
*/
public void addCachedVariable(@NotNull String variableName, @NotNull TextRange range) {
addCachedVariable(new Variable(variableName, myCachedVariableQualifier), range);
}
/**
* @param variable target variable to register within the current context
* @param range target variable's range
*/
public void addCachedVariable(@Nullable Variable variable, @NotNull TextRange range) {
myCachedVariables.put(variable, new Location(myCurrentFile, range));
}
/**
* @param variable target variable
* @return assignments registered for the given variable within the current context
*/
@NotNull
public Collection<Assignment> getAssignments(@NotNull Variable variable) {
Collection<Assignment> result = myAssignmentsByVariable.get(variable);
return result == null ? Collections.<Assignment>emptyList() : result;
}
/**
* @param codeStructure target code structure (see {@link #myCodeStructure})
* @return assignments registered for the target code structure. E.g. one might call this method with ['dependencies']
* argument in order to get all assignments parsed from the <code>'dependencies {...}'</code> section of the
* target <code>build.gradle</code> file
*/
@NotNull
public Collection<Assignment> getAssignments(@NotNull List<String> codeStructure) {
Collection<Assignment> result = myAssignmentsByCodeStructure.get(codeStructure);
return result == null ? Collections.<Assignment>emptyList() : result;
}
/**
* Notifies current context that new file (given) is being parsed.
* <p/>
* Given argument is returned from subsequent {@link #getTargetFile()} calls.
*
* @param file new file being parsed
*/
public void onChangeFile(@NotNull VirtualFile file) {
myCurrentFile = file;
}
/**
* Notifies current context that new section is being parsed. E.g. when <code>'dependencies {...}'</code> is about to be parsed,
* this method is called with <code>'dependencies'</code> argument.
*
* @param methodName
*/
public void onMethodEnter(@NotNull String methodName) {
myCodeStructure.add(methodName);
}
public void onMethodExit() {
if (!myCodeStructure.isEmpty()) {
myCodeStructure.remove(myCodeStructure.size() - 1);
}
}
public boolean registerAssignmentFromCachedData(@NonNull String lValueVariableName,
@NotNull TextRange range,
@NotNull PsiElement valueElement) {
return registerAssignmentFromCachedData(new Variable(lValueVariableName, myCachedVariableQualifier), range, valueElement);
}
/**
* Delegates to the {@link #registerAssignmentFromCachedData(Variable, Location, PsiElement)} with location build from the given
* range and {@link #getCurrentFile() current file}.
*
* @param lValue lvalue to use for the target assignment
* @param lValueRange lvalue range
* @param valueElement PSI element for the rvalue of the whole assignment
* @return <code>true</code> if an assignment is registered for the given lvalue and cached rvalue;
* <code>false</code> otherwise
*/
public boolean registerAssignmentFromCachedData(@NonNull Variable lValue,
@NotNull TextRange lValueRange,
@NotNull PsiElement valueElement) {
return registerAssignmentFromCachedData(lValue, new Location(myCurrentFile, lValueRange), valueElement);
}
/**
* Registers assignment using given lvalue and cached {@link #getCachedVariables() variables}/{@link #getCachedValues() values} as
* a rvalue if possible.
*
* @param lValue lvalue to use for the target assignment
* @param lValueLocation lvalue location
* @param valueElement PSI element for the rvalue of the whole assignment
* @return <code>true</code> if an assignment is registered for the given lvalue and cached rvalue;
* <code>false</code> otherwise
*/
public boolean registerAssignmentFromCachedData(@NonNull Variable lValue,
@NotNull Location lValueLocation,
@NotNull PsiElement valueElement) {
if (myCachedValues.isEmpty() && myCachedVariables.isEmpty()) {
return false;
}
String wholeElementText = GradleEditorModelUtil.unquote(valueElement.getText());
TextRange wholeElementRange = GradleEditorModelUtil.interestedRange(valueElement);
// There is a possible case that we processed an expression like 'a = b' - i.e. 'a' here just references 'b', so, we don't want
// to register a value. However, we do want to register a value for an assignment like 'a = b + 1'.
final Value valueToUse;
if (myCachedVariables.isEmpty()) {
switch (myCachedValues.size()) {
case 0: valueToUse = null; break;
case 1: valueToUse = myCachedValues.get(0); break;
// a = 1 + 2 - we don't evaluate '1 + 2' but keep it only as a source binding instead.
default: valueToUse = new Value(wholeElementText, new Location(myCurrentFile, wholeElementRange));
}
}
else if (myCachedVariables.size() == 1
&& GradleEditorModelUtil.isVariable(valueElement.getText(), myCachedVariables.keys().iterator().next().name)) {
valueToUse = null;
}
else {
valueToUse = new Value("", new Location(myCurrentFile, wholeElementRange));
}
TextRange assignmentRange = TextRange.create(lValueLocation.range.getStartOffset(), valueElement.getTextRange().getEndOffset());
onAssignment(assignmentRange, lValue, lValueLocation, new Location(myCurrentFile, wholeElementRange), valueToUse, wholeElementText,
myCachedVariables);
resetCaches();
return true;
}
/**
* Delegates to {@link #onAssignment(Assignment)} with an assignment built from the given data.
*
* @param assignmentRange assignment's range
* @param lValue assignment's lvalue
* @param lValueLocation lvalue location
* @param rValueLocation rvalue location
* @param rValue assignment's rvalue
* @param rValueString rvalue string
* @param dependencies dependencies used at the given assignment, e.g. <code>'a = b + 1'</code> is an assignment with
* <code>'a'</code> as a lvalue, <code>null</code> as rvalue (because we don't evaluate it in runtime)
* and <code>'b'</code> as a dependency
*/
public void onAssignment(@NotNull TextRange assignmentRange,
@NonNull Variable lValue,
@NotNull Location lValueLocation,
@NotNull Location rValueLocation,
@Nullable Value rValue,
@Nullable String rValueString,
@NotNull Multimap<Variable, Location> dependencies) {
onAssignment(
new Assignment(assignmentRange, lValue, lValueLocation, rValueLocation, myCodeStructure, rValue, rValueString, dependencies)
);
}
/**
* Registers given assignment within the current context.
* <p/>
* It's exposed via {@link #getAssignments(List)} or {@link #getAssignments(Variable)} later.
*
* @param assignment an assignment to register within the current context
*/
public void onAssignment(@NotNull Assignment assignment) {
myAssignmentsByVariable.put(assignment.lValue, assignment);
myAssignmentsByCodeStructure.put(assignment.codeStructure, assignment);
}
/**
* Populates {@link #myCachedVariableQualifier cached variable qualifier} by the given value, e.g. considering expression like
* <code>'a.b.c = 1'</code> - this method is expected to be called with <code>'a'</code> argument, then <code>'b'</code>.
*
* @param qualifier a qualifier to register within the current context
*/
public void rememberVariableQualifier(@NotNull String qualifier) {
if (!qualifier.isEmpty()) {
myCachedVariableQualifier.add(qualifier);
}
}
/**
* Resets cached {@link #getCachedValues() values}, {@link #getCachedVariables() variables} and
* {@link #rememberVariableQualifier(String) variable qualifier} registered within the current context (if any).
*/
public void resetCaches() {
myCachedVariableQualifier.clear();
myCachedVariables.clear();
myCachedValues.clear();
}
/**
* Object representation of a a variable, might serve either as a lvalue or rvalue.
*/
public static final class Variable {
@NotNull public final String name;
/**
* Variable qualifier, e.g. it's <code>['a', 'b']</code> for definition like <code>'a.b.c = 1'</code>.
*/
@NotNull public final List<String> qualifier;
public Variable(@NotNull String name, @NotNull List<String> qualifier) {
this.name = name;
this.qualifier = ImmutableList.copyOf(qualifier);
}
@Override
public int hashCode() {
int result = name.hashCode();
return 31 * result + qualifier.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Variable variable = (Variable)o;
return name.equals(variable.name) && qualifier.equals(variable.qualifier);
}
@Override
public String toString() {
String q = qualifier.isEmpty() ? "" : StringUtil.join(qualifier, ".") + ".";
return q + name;
}
}
public static class Assignment {
@NotNull public final TextRange assignmentRange;
@NotNull public final Variable lValue;
@NotNull public final Location lValueLocation;
@NotNull public final Location rValueLocation;
@NotNull public final List<String> codeStructure;
@Nullable public final Value value;
@Nullable public final String rValueString;
@NotNull public final Multimap<Variable, Location> dependencies;
public Assignment(@NotNull TextRange assignmentRange,
@NotNull Variable lValue,
@NotNull Location lValueLocation,
@NotNull Location rValueLocation,
@NotNull List<String> codeStructure,
@Nullable Value value,
@Nullable String rValueString,
@NotNull Multimap<Variable, Location> dependencies) {
this.assignmentRange = assignmentRange;
this.lValue = lValue;
this.lValueLocation = lValueLocation;
this.rValueLocation = rValueLocation;
this.codeStructure = codeStructure.isEmpty() ? Collections.<String>emptyList() : ImmutableList.copyOf(codeStructure);
this.value = value;
this.rValueString = rValueString;
this.dependencies = dependencies.isEmpty() ? ImmutableMultimap.<Variable, Location>of() : ImmutableMultimap.copyOf(dependencies);
}
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
buffer.append(StringUtil.join(codeStructure, "->"));
if (buffer.length() > 0) {
buffer.append(": ");
}
buffer.append(lValue);
if (rValueString != null) {
buffer.append(" = ").append(rValueString);
}
else if (value != null) {
buffer.append(" = ").append(value);
}
if (!dependencies.isEmpty()) {
buffer.append(" depends on ").append(StringUtil.join(dependencies.keySet(), ","));
}
return buffer.toString();
}
}
public static class Value {
@NotNull public final String value;
@NotNull public final Location location;
public Value(@NotNull String value, @NotNull Location location) {
this.value = value;
this.location = location;
}
@NotNull
public Value shrink(@NotNull TextRange range) {
if (range.equals(location.range)) {
return this;
}
if (range.getStartOffset() < location.range.getStartOffset()
|| range.getStartOffset() >= location.range.getEndOffset()
|| range.getEndOffset() > location.range.getEndOffset()) {
throw new IllegalArgumentException(String.format("Can't shrink value '%s' to range %s. Reason - given range is not inside "
+ "the current one (%s)", this, range, location.range));
}
int newValueStartOffset = range.getStartOffset() - location.range.getStartOffset();
int newValueEndOffset = value.length() - (location.range.getEndOffset() - range.getEndOffset());
return new Value(value.substring(newValueStartOffset, newValueEndOffset), new Location(location.file, range));
}
@Override
public String toString() {
return String.format("'%s' at %s", value, location);
}
}
public static class Location {
@NotNull public final VirtualFile file;
@NotNull public final TextRange range;
public Location(@NotNull VirtualFile file, @NotNull TextRange range) {
this.file = file;
this.range = range;
}
@Override
public int hashCode() {
int result = file.hashCode();
result = 31 * result + range.hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Location location = (Location)o;
return file.equals(location.file) && range.equals(location.range);
}
@Override
public String toString() {
return String.format("%s [%d;%d)", file.getName(), range.getStartOffset(), range.getEndOffset());
}
}
}