| /* |
| * 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.service.resolve; |
| |
| import com.android.annotations.VisibleForTesting; |
| import com.android.tools.idea.gradle.parser.GradleBuildFile; |
| import com.android.tools.lint.checks.GradleDetector; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Lists; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.*; |
| import com.intellij.psi.scope.PsiScopeProcessor; |
| import com.intellij.psi.search.GlobalSearchScope; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.util.NotNullFunction; |
| import com.intellij.util.containers.ContainerUtil; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.plugins.gradle.service.GradleBuildClasspathManager; |
| import org.jetbrains.plugins.gradle.service.resolve.GradleMethodContextContributor; |
| import org.jetbrains.plugins.gradle.service.resolve.GradleResolverUtil; |
| import org.jetbrains.plugins.groovy.lang.psi.GroovyFile; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression; |
| import org.jetbrains.plugins.groovy.lang.psi.impl.GroovyPsiManager; |
| import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightMethodBuilder; |
| import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightParameter; |
| import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightVariable; |
| import org.jetbrains.plugins.groovy.lang.psi.util.GroovyPropertyUtils; |
| |
| import java.util.*; |
| |
| /** |
| * {@link AndroidDslContributor} provides symbol resolution for identifiers inside the android block |
| * in a Gradle build script. |
| */ |
| public class AndroidDslContributor implements GradleMethodContextContributor { |
| private static final Logger LOG = Logger.getInstance(AndroidDslContributor.class); |
| |
| @NonNls private static final String DSL_ANDROID = "android"; |
| @NonNls private static final String ANDROID_FQCN = "com.android.build.gradle.AppExtension"; |
| @NonNls private static final String ANDROID_LIB_FQCN = "com.android.build.gradle.LibraryExtension"; |
| |
| private static final Key<PsiElement> CONTRIBUTOR_KEY = Key.create("AndroidDslContributor.key"); |
| |
| @NonNls private List<VirtualFile> myLastClassPath = Collections.emptyList(); |
| |
| private static final Map<String, String> ourDslForClassMap = ImmutableMap.of( |
| "com.android.builder.DefaultProductFlavor", "com.android.build.gradle.internal.dsl.ProductFlavorDsl", |
| "com.android.builder.DefaultBuildType", "com.android.build.gradle.internal.dsl.BuildTypeDsl", |
| "com.android.builder.model.SigningConfig", "com.android.build.gradle.internal.dsl.SigningConfigDsl"); |
| |
| @Override |
| public void process(@NotNull List<String> callStack, |
| @NotNull PsiScopeProcessor processor, |
| @NotNull ResolveState state, |
| @NotNull PsiElement place) { |
| // The Android DSL within a Gradle build script looks something like this: |
| // android { |
| // compileSdkVersion 18 |
| // buildToolsVersion "19.0.0" |
| // |
| // defaultConfig { |
| // minSdkVersion 8 |
| // } |
| // |
| // buildTypes { |
| // release { |
| // runProguard false |
| // proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' |
| // } |
| // } |
| // |
| // lintOptions { |
| // quiet true |
| // } |
| // } |
| // This method receives a callstack leading to a particular symbol, e.g. android.compileSdkVersion, or |
| // android.buildTypes.release.runProguard. Based on the given call stack, we have to resolve either to a particular class or to a |
| // particular setter within the class. |
| // |
| // The blocks are processed top down i.e. android is resolved before any symbols within the android closure are resolved. |
| // When a particular symbol is resolved, the resolution is cached as user data of that PsiElement under CONTRIBUTOR_KEY. |
| // All symbols inside the android block first attempt to determine their parent method, and look at the parent contributor. |
| // Depending on the parent contributor (method or class), the symbols are resolved to be either method calls of a class or |
| // new domain objects. |
| |
| |
| // There are two issues that necessitate this custom processing: 1. Groovy doesn't know what the block corresponding to |
| // 'android' with a closure means i.e. it doesn't know that it is an extension provided by the android Gradle plugin. |
| // 2. Once it understands that 'android' is a closure of a certain type, it still stumbles over methods that take in |
| // either an Action<T> or a Action<NamedDomainObject<T>>. So most of the code simply tries to match the former to a method |
| // that takes a closure<T> and the latter to be a closure that defines objects with closure<T> |
| |
| // we only care about symbols within the android closure |
| String topLevel = ContainerUtil.getLastItem(callStack, null); |
| if (!DSL_ANDROID.equals(topLevel)) { |
| return; |
| } |
| |
| logClassPathOnce(place.getProject()); |
| |
| GroovyPsiManager psiManager = GroovyPsiManager.getInstance(place.getProject()); |
| |
| // top level android block |
| if (callStack.size() == 1) { |
| String fqcn = resolveAndroidExtension(place.getContainingFile()); |
| PsiClass contributorClass = fqcn == null ? null : findClassByName(psiManager, place.getResolveScope(), fqcn); |
| if (contributorClass != null) { |
| String qualifiedName = contributorClass.getQualifiedName(); |
| if (qualifiedName == null) { |
| qualifiedName = fqcn; |
| } |
| |
| // resolve 'android' as a method that takes a closure |
| resolveToMethodWithClosure(place, contributorClass, qualifiedName, processor, state, psiManager); |
| cacheContributorInfo(place, contributorClass); |
| } |
| return; |
| } |
| |
| // For all blocks within android, we first figure out who contributed the parent block. |
| PsiElement parentContributor = getParentContributor(place); |
| if (parentContributor == null) { |
| return; |
| } |
| |
| // if the parent object is a class, then process the current identifier as a method of the parent class |
| if (parentContributor instanceof PsiClass) { |
| PsiMethod method = |
| findAndProcessContributingMethod(callStack.get(0), processor, state, place, (PsiClass)parentContributor, psiManager); |
| cacheContributorInfo(place, method); |
| return; |
| } |
| |
| // if the parent object is a method, then the type of the current object depends on the arguments of the parent: |
| // (In the snippets below, ^ points to the current symbol being resolved). |
| // 1. lintOptions { ^quiet = true; } |
| // lintOptions is declared as: lintOptions(Action<LintOptionsImpl> action). |
| // So 'quiet' is simply a method on the LintOptionsImpl class. |
| // 2. buildTypes { debug^ { } } |
| // buildTypes is declared as: buildTypes(Action<NamedDomainObjectContainer<DefaultBuildType>> action) |
| // So debug is a named domain object of type DefaultBuildType |
| // 3. sourceSets { |
| // main {} |
| // debug.setRoot {} |
| // } |
| // This is similar to case 2, we just need to make sure that debug is resolved as a variable of type AndroidSourceSet |
| if (!(parentContributor instanceof PsiMethod)) { |
| return; |
| } |
| |
| // determine the type variable present in the parent method |
| ParametrizedTypeExtractor typeExtractor = getTypeExtractor((PsiMethod)parentContributor); |
| if (typeExtractor == null) { |
| LOG.info("inside the closure of a method, but unable to extract the closure parameter's type."); |
| return; |
| } |
| |
| if (typeExtractor.hasNamedDomainObjectContainer()) { |
| // this symbol must be a NamedDomainObject<T> |
| // so define a it as a method with the given name (place.getText()) with an argument Closure<T> |
| String namedDomainObject = typeExtractor.getNamedDomainObject(); |
| assert namedDomainObject != null : typeExtractor.getCanonicalType(); // because hasNamedDomainObjectContainer() |
| |
| PsiClass contributorClass = findClassByName(psiManager, place.getResolveScope(), namedDomainObject); |
| if (contributorClass != null) { |
| String qualifiedName = contributorClass.getQualifiedName(); |
| if (qualifiedName == null) { |
| qualifiedName = namedDomainObject; |
| } |
| resolveToMethodWithClosure(place, contributorClass, qualifiedName, processor, state, psiManager); |
| cacheContributorInfo(place, contributorClass); |
| } |
| return; |
| } |
| |
| if (typeExtractor.isClosure()) { |
| // the parent method was of type Action<T>, so this is simply a method of class T |
| String clz = typeExtractor.getClosureType(); |
| assert clz != null : typeExtractor.getCanonicalType(); // because typeExtractor.isClosure() |
| |
| PsiClass contributorClass = findClassByName(psiManager, place.getResolveScope(), clz); |
| if (contributorClass == null) { |
| return; |
| } |
| |
| PsiMethod method = findAndProcessContributingMethod(callStack.get(0), processor, state, place, contributorClass, psiManager); |
| cacheContributorInfo(place, method); |
| } |
| } |
| |
| private static void resolveToMethodWithClosure(PsiElement place, |
| PsiElement resolveToElement, |
| String closureTypeFqcn, |
| PsiScopeProcessor processor, |
| ResolveState state, |
| GroovyPsiManager psiManager) { |
| if (place.getParent() instanceof GrMethodCallExpression) { |
| GrLightMethodBuilder methodWithClosure = |
| GradleResolverUtil.createMethodWithClosure(place.getText(), closureTypeFqcn, null, place, psiManager); |
| if (methodWithClosure != null) { |
| processor.execute(methodWithClosure, state); |
| methodWithClosure.setNavigationElement(resolveToElement); |
| } |
| } else if (place.getParent() instanceof GrReferenceExpression) { |
| GrLightVariable variable = new GrLightVariable(place.getManager(), place.getText(), closureTypeFqcn, place); |
| processor.execute(variable, state); |
| } |
| } |
| |
| @Nullable |
| private static PsiMethod findAndProcessContributingMethod(String symbol, |
| PsiScopeProcessor processor, |
| ResolveState state, |
| PsiElement place, |
| PsiClass contributorClass, |
| GroovyPsiManager psiManager) { |
| PsiMethod method = getContributingMethod(place, contributorClass, symbol); |
| if (method == null) { |
| return null; |
| } |
| |
| ParametrizedTypeExtractor typeExtractor = getTypeExtractor(method); |
| if (typeExtractor != null && !typeExtractor.hasNamedDomainObjectContainer() && typeExtractor.isClosure()) { |
| // method takes a closure argument |
| String clz = typeExtractor.getClosureType(); |
| if (clz == null) { |
| clz = CommonClassNames.JAVA_LANG_OBJECT; |
| } |
| if (ourDslForClassMap.containsKey(clz)) { |
| clz = ourDslForClassMap.get(clz); |
| } |
| resolveToMethodWithClosure(place, method, clz, processor, state, psiManager); |
| } else { |
| GrLightMethodBuilder builder = new GrLightMethodBuilder(place.getManager(), method.getName()); |
| PsiElementFactory factory = JavaPsiFacade.getElementFactory(place.getManager().getProject()); |
| PsiType type = new PsiArrayType(factory.createTypeByFQClassName(CommonClassNames.JAVA_LANG_OBJECT, place.getResolveScope())); |
| builder.addParameter(new GrLightParameter("param", type, builder)); |
| PsiClassType retType = factory.createTypeByFQClassName(CommonClassNames.JAVA_LANG_OBJECT, place.getResolveScope()); |
| builder.setReturnType(retType); |
| processor.execute(builder, state); |
| |
| builder.setNavigationElement(method); |
| } |
| |
| return method; |
| } |
| |
| @Nullable |
| private static PsiMethod getContributingMethod(PsiElement place, |
| PsiClass contributorClass, |
| String methodName) { |
| GrMethodCall call = PsiTreeUtil.getParentOfType(place, GrMethodCall.class); |
| if (call == null) { |
| return null; |
| } |
| |
| GrArgumentList args = call.getArgumentList(); |
| int argsCount = GradleResolverUtil.getGrMethodArumentsCount(args); |
| |
| PsiMethod[] methodsByName = findMethodByName(contributorClass, methodName); |
| |
| // first check to see if we can narrow down by # of arguments |
| for (PsiMethod method : methodsByName) { |
| if (method.getParameterList().getParametersCount() == argsCount) { |
| return method; |
| } |
| } |
| |
| // if we couldn't narrow down by # of arguments, just use the first one |
| return methodsByName.length > 0 ? methodsByName[0] : null; |
| } |
| |
| @NotNull |
| private static PsiMethod[] findMethodByName(PsiClass contributorClass, String methodName) { |
| // Search for methods that match the given name, or a setter or getter. |
| List<String> possibleMethods = Arrays.asList(methodName, GroovyPropertyUtils.getSetterName(methodName), |
| GroovyPropertyUtils.getGetterNameNonBoolean(methodName), |
| GroovyPropertyUtils.getGetterNameBoolean(methodName)); |
| |
| for (String possibleMethod : possibleMethods) { |
| PsiMethod[] methods = contributorClass.findMethodsByName(possibleMethod, true); |
| if (methods.length > 0) { |
| return methods; |
| } |
| } |
| |
| return PsiMethod.EMPTY_ARRAY; |
| } |
| |
| @Nullable |
| private static ParametrizedTypeExtractor getTypeExtractor(PsiMethod parentContributor) { |
| PsiParameter[] parameters = parentContributor.getParameterList().getParameters(); |
| |
| // The method must have had at least 1 closure argument. |
| if (parameters.length < 1) { |
| return null; |
| } |
| |
| PsiParameter param = parameters[parameters.length-1]; |
| String parameterType = param.getType().getCanonicalText(); |
| |
| return new ParametrizedTypeExtractor(parameterType); |
| } |
| |
| /** |
| * Returns the contributor of the enclosing block. |
| * This is performed by first obtaining the closeable block that contains this element, and figuring out the method whose |
| * closure argument is the closeable block. We do this instead of directly looking for a parent element of type method call |
| * since this scheme allows us to handle both the following two cases: |
| * sourceSets { |
| * ^main {} |
| * ^debug.setRoot() |
| * } |
| * In the above example, parent(parent('main')) == parent('debug') == 'sourceSets'. |
| */ |
| @Nullable |
| private static PsiElement getParentContributor(PsiElement place) { |
| ApplicationManager.getApplication().assertReadAccessAllowed(); |
| |
| GrClosableBlock closeableBlock = PsiTreeUtil.getParentOfType(place, GrClosableBlock.class); |
| if (closeableBlock == null || !(closeableBlock.getParent() instanceof GrMethodCall)) { |
| return null; |
| } |
| PsiElement parentContributor = closeableBlock.getParent().getUserData(CONTRIBUTOR_KEY); |
| if (parentContributor == null) { |
| return null; |
| } |
| return parentContributor; |
| } |
| |
| @Nullable |
| public static PsiClass findClassByName(GroovyPsiManager psiManager, GlobalSearchScope resolveScope, @NotNull String fqcn) { |
| if (ourDslForClassMap.containsKey(fqcn)) { |
| fqcn = ourDslForClassMap.get(fqcn); |
| } |
| |
| return psiManager.findClassWithCache(fqcn, resolveScope); |
| } |
| |
| private static void cacheContributorInfo(@NotNull PsiElement place, @Nullable PsiElement contributor) { |
| if (contributor == null) { |
| return; |
| } |
| |
| // only cache info if this is a method call (and not a reference expression or something else), |
| // as only method calls can contain closure arguments where this might be needed |
| if (!(place.getParent() instanceof GrMethodCall)) { |
| return; |
| } |
| |
| // A method call of form "lintOptions { quiet = true }" has a PSI structure like: |
| // |- Method call |
| // |---- Reference Expression |
| // |--------PsiElement (identifier) (place usually points to this) |
| // |---- Arguments |
| // |---- Closeable block |
| // Rather than caching information at the method call identifier, we cache it at the |
| // root method call. |
| GrMethodCall method = PsiTreeUtil.getParentOfType(place, GrMethodCall.class); |
| if (method != null) { |
| method.putUserData(CONTRIBUTOR_KEY, contributor); |
| } |
| } |
| |
| private void logClassPathOnce(@NotNull Project project) { |
| List<VirtualFile> files = GradleBuildClasspathManager.getInstance(project).getAllClasspathEntries(); |
| if (ContainerUtil.equalsIdentity(files, myLastClassPath)) { |
| return; |
| } |
| myLastClassPath = files; |
| |
| List<String> paths = ContainerUtil.map(files, new NotNullFunction<VirtualFile, String>() { |
| @NotNull |
| @Override |
| public String fun(VirtualFile vf) { |
| return vf.getPath(); |
| } |
| }); |
| String classPath = Joiner.on(':').join(paths); |
| LOG.info(String.format("Android DSL resolver classpath (project %1$s): %2$s", project.getName(), classPath)); |
| } |
| |
| /** Returns the class corresponding to the android extension for given file. */ |
| @Nullable |
| private static String resolveAndroidExtension(PsiFile file) { |
| assert file instanceof GroovyFile; |
| List<String> plugins = GradleBuildFile.getPlugins((GroovyFile)file); |
| if (plugins.contains(GradleDetector.APP_PLUGIN_ID) || plugins.contains(GradleDetector.OLD_APP_PLUGIN_ID)) { |
| return ANDROID_FQCN; |
| } |
| else if (plugins.contains(GradleDetector.LIB_PLUGIN_ID) || plugins.contains(GradleDetector.OLD_LIB_PLUGIN_ID)) { |
| return ANDROID_LIB_FQCN; |
| } |
| else { |
| return null; |
| } |
| } |
| |
| /** |
| * {@link ParametrizedTypeExtractor} is a simple utility class that allows a few queries to be made on a parameterized type |
| * such as {@code Action<NamedDomainObject<X>>}. |
| */ |
| @VisibleForTesting |
| static class ParametrizedTypeExtractor { |
| private static final String GRADLE_ACTION_FQCN = "org.gradle.api.Action"; |
| private static final String GRADLE_NAMED_DOMAIN_OBJECT_CONTAINER_FQCN = "org.gradle.api.NamedDomainObjectContainer"; |
| |
| private static final Splitter SPLITTER = Splitter.onPattern("[<>]").trimResults().omitEmptyStrings(); |
| private final ArrayList<String> myParameterTypes; |
| private final String myCanonicalType; |
| |
| public ParametrizedTypeExtractor(String canonicalType) { |
| myCanonicalType = canonicalType; |
| myParameterTypes = Lists.newArrayList(SPLITTER.split(canonicalType)); |
| } |
| |
| public String getCanonicalType() { |
| return myCanonicalType; |
| } |
| |
| public boolean isClosure() { |
| return myParameterTypes.contains(GRADLE_ACTION_FQCN); |
| } |
| |
| @Nullable |
| public String getClosureType() { |
| if (!isClosure()) { |
| return null; |
| } |
| |
| StringBuilder sb = new StringBuilder(100); |
| for (int i = 1; i < myParameterTypes.size(); i++) { |
| String type = myParameterTypes.get(i); |
| type = type.replace("? extends ", ""); // remove wildcards |
| type = type.replace("? super ", ""); // remove wildcards |
| sb.append(type); |
| if (i != myParameterTypes.size() - 1) { |
| sb.append('<'); |
| } |
| } |
| for (int i = 1; i < myParameterTypes.size() - 1; i++) { |
| sb.append('>'); |
| } |
| |
| return sb.toString(); |
| } |
| |
| @Nullable |
| public String getNamedDomainObject() { |
| return hasNamedDomainObjectContainer() ? ContainerUtil.getLastItem(myParameterTypes) : null; |
| } |
| |
| public boolean hasNamedDomainObjectContainer() { |
| for (String type : myParameterTypes) { |
| if (type.contains(GRADLE_NAMED_DOMAIN_OBJECT_CONTAINER_FQCN)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| } |
| } |