Improvements to the Gradle DSL resolver

The existing DSL resolver for the android extension treated
resolved a method of form: method(Action<T>) directly to its
definition in the plugin. However, the IDE infrastructure
doesn't really understand that an Action<T> is similar to
a Closure<T>.

This CL looks at the type signature of the method being
resolved to, and if it is of the Action<T> form, then it
resolves to a method that takes a closure.

As a bonus, this allows completions to work within all
the closures. This CL adds a few tests that checks that
the appropriate completions are returned.

Change-Id: Ifbc9f2fe17c59660224071a4a06e559057b30593
diff --git a/android/src/com/android/tools/idea/gradle/service/resolve/AndroidDslContributor.java b/android/src/com/android/tools/idea/gradle/service/resolve/AndroidDslContributor.java
index db2ce82..237dd54 100644
--- a/android/src/com/android/tools/idea/gradle/service/resolve/AndroidDslContributor.java
+++ b/android/src/com/android/tools/idea/gradle/service/resolve/AndroidDslContributor.java
@@ -21,6 +21,7 @@
 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;
@@ -51,6 +52,10 @@
 
 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);
 
@@ -88,18 +93,6 @@
     //           }
     //         }
     //
-    //         productFlavors {
-    //           flavor1 {
-    //             packageName "com.example.flavor1"
-    //           }
-    //         }
-    //
-    //         signingConfigs {
-    //           myConfig {
-    //             storeFile file("other.keystore")
-    //           }
-    //         }
-    //
     //         lintOptions {
     //           quiet true
     //         }
@@ -114,6 +107,13 @@
     // 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)) {
@@ -126,44 +126,31 @@
 
     // top level android block
     if (callStack.size() == 1) {
-      String clz = resolveAndroidExtension(place.getContainingFile());
-      if (clz == null) {
-        return;
-      }
-      PsiClass contributorClass = psiManager.findClassWithCache(clz, place.getResolveScope());
-      if (contributorClass == null) {
-        return;
-      }
+      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;
+        }
 
-      GrLightMethodBuilder methodWithClosure = GradleResolverUtil.createMethodWithClosure("android", ANDROID_FQCN, null, place, psiManager);
-      if (methodWithClosure != null) {
-        processor.execute(methodWithClosure, state);
+        // resolve 'android' as a method that takes a closure
+        resolveToMethodWithClosure(place, contributorClass, qualifiedName, processor, state, psiManager);
+        cacheContributorInfo(place, contributorClass);
       }
-
-      cacheContributorInfo(place, contributorClass);
       return;
     }
 
-    // For all blocks within android, we first figure out who contributed the parent block. We do this 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'.
-    GrClosableBlock closeableBlock = PsiTreeUtil.getParentOfType(place, GrClosableBlock.class);
-    if (closeableBlock == null || !(closeableBlock.getParent() instanceof GrMethodCall)) {
-      return;
-    }
-    PsiElement parentContributor = closeableBlock.getParent().getUserData(CONTRIBUTOR_KEY);
+    // 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);
+      PsiMethod method =
+        findAndProcessContributingMethod(callStack.get(0), processor, state, place, (PsiClass)parentContributor, psiManager);
       cacheContributorInfo(place, method);
       return;
     }
@@ -180,96 +167,67 @@
     //             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
+    //        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;
     }
 
-    PsiParameter[] parameters = ((PsiMethod)parentContributor).getParameterList().getParameters();
-
-    // The method must have had atleast 1 closure argument.
-    if (parameters.length < 1) {
-      LOG.info("inside the closure of a method, but method has " + parameters.length + " arguments (expected atleast 1).");
+    // 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;
     }
 
-    PsiParameter param = parameters[parameters.length-1];
-    String parameterType = param.getType().getCanonicalText();
-    ParametrizedTypeExtractor typeExtractor = new ParametrizedTypeExtractor(parameterType);
-
     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 : parameterType; // because hasNamedDomainObjectContainer()
+      assert namedDomainObject != null : typeExtractor.getCanonicalType(); // because hasNamedDomainObjectContainer()
 
       PsiClass contributorClass = findClassByName(psiManager, place.getResolveScope(), namedDomainObject);
-      if (contributorClass == null) {
-        return;
-      }
-
-      if (place.getParent() instanceof GrMethodCallExpression) {
-        // define a new named domain object as a method that takes a closure
-        GrLightMethodBuilder methodWithClosure = GradleResolverUtil
-          .createMethodWithClosure(place.getText(), namedDomainObject, null, place, GroovyPsiManager.getInstance(place.getProject()));
-        if (methodWithClosure != null) {
-          processor.execute(methodWithClosure, state);
+      if (contributorClass != null) {
+        String qualifiedName = contributorClass.getQualifiedName();
+        if (qualifiedName == null) {
+          qualifiedName = namedDomainObject;
         }
+        resolveToMethodWithClosure(place, contributorClass, qualifiedName, processor, state, psiManager);
         cacheContributorInfo(place, contributorClass);
       }
-      else if (place.getParent() instanceof GrReferenceExpression) {
-        // resolve the symbol as a variable of type namedDomainObject
-        GrLightVariable variable = new GrLightVariable(place.getManager(), place.getText(), namedDomainObject, place);
-        processor.execute(variable, state);
-      }
-
       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 : parameterType; // because typeExtractor.isClosure()
+      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);
+      PsiMethod method = findAndProcessContributingMethod(callStack.get(0), processor, state, place, contributorClass, psiManager);
       cacheContributorInfo(place, method);
     }
   }
 
-  @Nullable
-  private 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 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);
     }
   }
 
@@ -278,21 +236,36 @@
                                                             PsiScopeProcessor processor,
                                                             ResolveState state,
                                                             PsiElement place,
-                                                            PsiClass contributorClass) {
+                                                            PsiClass contributorClass,
+                                                            GroovyPsiManager psiManager) {
     PsiMethod method = getContributingMethod(place, contributorClass, symbol);
     if (method == null) {
       return null;
     }
 
-    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);
+    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);
+      builder.setNavigationElement(method);
+    }
+
     return method;
   }
 
@@ -324,8 +297,7 @@
   @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),
+    List<String> possibleMethods = Arrays.asList(methodName, GroovyPropertyUtils.getSetterName(methodName),
                                                  GroovyPropertyUtils.getGetterNameNonBoolean(methodName),
                                                  GroovyPropertyUtils.getGetterNameBoolean(methodName));
 
@@ -339,6 +311,81 @@
     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)) {
@@ -384,11 +431,17 @@
 
     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);
     }
diff --git a/android/testData/projects/resolve/simple/completion/comp.gradle b/android/testData/projects/resolve/simple/completion/comp.gradle
new file mode 100644
index 0000000..af39201
--- /dev/null
+++ b/android/testData/projects/resolve/simple/completion/comp.gradle
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+buildscript {
+  repositories {
+    mavenCentral()
+  }
+  dependencies {
+    classpath 'com.android.tools.build:gradle:0.9.+'
+  }
+}
+apply plugin: 'android'
+
+android {
+  comp<caret>
+}
diff --git a/android/testData/projects/resolve/simple/completion/ndk.gradle b/android/testData/projects/resolve/simple/completion/ndk.gradle
new file mode 100644
index 0000000..925beb6
--- /dev/null
+++ b/android/testData/projects/resolve/simple/completion/ndk.gradle
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+buildscript {
+  repositories {
+    mavenCentral()
+  }
+  dependencies {
+    classpath 'com.android.tools.build:gradle:0.9.+'
+  }
+}
+apply plugin: 'android'
+
+android {
+  defaultConfig {
+    ndk<caret>
+  }
+}
diff --git a/android/testData/projects/resolve/simple/completion/suffix.gradle b/android/testData/projects/resolve/simple/completion/suffix.gradle
new file mode 100644
index 0000000..8106635
--- /dev/null
+++ b/android/testData/projects/resolve/simple/completion/suffix.gradle
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+buildscript {
+  repositories {
+    mavenCentral()
+  }
+  dependencies {
+    classpath 'com.android.tools.build:gradle:0.9.+'
+  }
+}
+apply plugin: 'android'
+
+android {
+  buildTypes {
+    release {
+      suf<caret>
+    }
+  }
+}
diff --git a/android/testSrc/com/android/tools/idea/gradle/service/resolve/AndroidDslContributorTest.java b/android/testSrc/com/android/tools/idea/gradle/service/resolve/AndroidDslContributorTest.java
index b2dd1a2..53fd87e 100644
--- a/android/testSrc/com/android/tools/idea/gradle/service/resolve/AndroidDslContributorTest.java
+++ b/android/testSrc/com/android/tools/idea/gradle/service/resolve/AndroidDslContributorTest.java
@@ -16,13 +16,24 @@
 package com.android.tools.idea.gradle.service.resolve;
 
 import com.android.tools.idea.templates.AndroidGradleTestCase;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.intellij.codeInsight.completion.CompletionType;
+import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.*;
+import com.intellij.testFramework.fixtures.IdeaProjectTestFixture;
+import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.JavaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.TestFixtureBuilder;
 import org.jetbrains.annotations.Nullable;
 import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
 import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
 import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightMethodBuilder;
 
+import java.util.Set;
+
 /**
  * {@link AndroidDslContributorTest} tests that various elements in the Gradle build script are
  * being resolved to their appropriate elements in the gradle model. We have this as a
@@ -72,7 +83,7 @@
     validateResolution(psiFile, "publishNonDefault", "com.android.build.gradle.LibraryExtension", "publishNonDefault");
   }
 
-  private void validateNoResolution(PsiFile psiFile, String symbol) {
+  private static void validateNoResolution(PsiFile psiFile, String symbol) {
     PsiReference ref = getPsiReference(psiFile, symbol);
     assert ref instanceof GrReferenceExpression : symbol;
 
@@ -104,6 +115,34 @@
     assertEquals("Method names don't match while resolving " + symbol, methodName, psiMethod.getName());
   }
 
+  public void testCompletions() throws Exception {
+    loadProject("projects/resolve/simple");
+
+    assertHasCompletions("completion/comp.gradle", "compileSdkVersion", "compileOptions");
+    assertHasCompletions("completion/suffix.gradle", "packageNameSuffix", "versionNameSuffix");
+    assertHasCompletions("completion/ndk.gradle", "ndk", "ndkConfig", "renderscriptNdkMode");
+  }
+
+  private void assertHasCompletions(String path, String... expectedCompletions) throws Exception {
+    VirtualFile file = getProject().getBaseDir().findFileByRelativePath(path);
+    assertNotNull(file);
+
+    myFixture.configureFromExistingVirtualFile(file);
+
+    LookupElement[] elements = myFixture.complete(CompletionType.BASIC);
+    assertNotNull(elements);
+
+    Set<String> suggestions = Sets.newHashSetWithExpectedSize(elements.length);
+    for (LookupElement element : elements) {
+      suggestions.add(element.getLookupString());
+    }
+
+    for (String expected : expectedCompletions) {
+      String msg = String.format("%1$s not in available completions: {%2$s}", expected, Joiner.on(',').join(suggestions));
+      assertTrue(msg, suggestions.contains(expected));
+    }
+  }
+
   @Nullable
   private GroovyFile getPsiFile(String path) throws Exception {
     VirtualFile buildFile = getProject().getBaseDir().findChild(path);
@@ -123,46 +162,4 @@
     }
     return psiFile.findReferenceAt(offset);
   }
-
-  public void testParametersWildcardNdo() {
-    AndroidDslContributor.ParametrizedTypeExtractor extractor = new AndroidDslContributor.ParametrizedTypeExtractor(
-      "org.gradle.api.Action<? super org.gradle.api.NamedDomainObjectContainer<BuildType>>>");
-
-    assertTrue(extractor.isClosure());
-    assertEquals("org.gradle.api.NamedDomainObjectContainer<BuildType>", extractor.getClosureType());
-
-    assertTrue(extractor.hasNamedDomainObjectContainer());
-    assertEquals("BuildType", extractor.getNamedDomainObject());
-  }
-
-  public void testParametersNdo() {
-    AndroidDslContributor.ParametrizedTypeExtractor extractor =
-      new AndroidDslContributor.ParametrizedTypeExtractor("org.gradle.api.Action<org.gradle.api.NamedDomainObjectContainer<Flavor>>>");
-
-    assertTrue(extractor.isClosure());
-    assertEquals("org.gradle.api.NamedDomainObjectContainer<Flavor>", extractor.getClosureType());
-
-    assertTrue(extractor.hasNamedDomainObjectContainer());
-    assertEquals("Flavor", extractor.getNamedDomainObject());
-  }
-
-  public void testParametersPrimitiveClosure() {
-    AndroidDslContributor.ParametrizedTypeExtractor extractor =
-      new AndroidDslContributor.ParametrizedTypeExtractor("org.gradle.api.Action<String>");
-
-    assertTrue(extractor.isClosure());
-    assertEquals("String", extractor.getClosureType());
-
-    assertFalse(extractor.hasNamedDomainObjectContainer());
-    assertNull(extractor.getNamedDomainObject());
-  }
-
-  public void testParametersNoClosure() {
-    AndroidDslContributor.ParametrizedTypeExtractor extractor =
-      new AndroidDslContributor.ParametrizedTypeExtractor("String");
-    assertFalse(extractor.isClosure());
-
-    assertFalse(extractor.hasNamedDomainObjectContainer());
-    assertNull(extractor.getNamedDomainObject());
-  }
 }
diff --git a/android/testSrc/com/android/tools/idea/gradle/service/resolve/ParametrizedTypeExtractorTest.java b/android/testSrc/com/android/tools/idea/gradle/service/resolve/ParametrizedTypeExtractorTest.java
new file mode 100644
index 0000000..e394b6b
--- /dev/null
+++ b/android/testSrc/com/android/tools/idea/gradle/service/resolve/ParametrizedTypeExtractorTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014 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 junit.framework.TestCase;
+
+public class ParametrizedTypeExtractorTest extends TestCase {
+  public void testParametersWildcardNdo() {
+    AndroidDslContributor.ParametrizedTypeExtractor extractor = new AndroidDslContributor.ParametrizedTypeExtractor(
+      "org.gradle.api.Action<? super org.gradle.api.NamedDomainObjectContainer<BuildType>>>");
+
+    assertTrue(extractor.isClosure());
+    assertEquals("org.gradle.api.NamedDomainObjectContainer<BuildType>", extractor.getClosureType());
+
+    assertTrue(extractor.hasNamedDomainObjectContainer());
+    assertEquals("BuildType", extractor.getNamedDomainObject());
+  }
+
+  public void testParametersNdo() {
+    AndroidDslContributor.ParametrizedTypeExtractor extractor =
+      new AndroidDslContributor.ParametrizedTypeExtractor("org.gradle.api.Action<org.gradle.api.NamedDomainObjectContainer<Flavor>>>");
+
+    assertTrue(extractor.isClosure());
+    assertEquals("org.gradle.api.NamedDomainObjectContainer<Flavor>", extractor.getClosureType());
+
+    assertTrue(extractor.hasNamedDomainObjectContainer());
+    assertEquals("Flavor", extractor.getNamedDomainObject());
+  }
+
+  public void testParametersPrimitiveClosure() {
+    AndroidDslContributor.ParametrizedTypeExtractor extractor =
+      new AndroidDslContributor.ParametrizedTypeExtractor("org.gradle.api.Action<String>");
+
+    assertTrue(extractor.isClosure());
+    assertEquals("String", extractor.getClosureType());
+
+    assertFalse(extractor.hasNamedDomainObjectContainer());
+    assertNull(extractor.getNamedDomainObject());
+  }
+
+  public void testParametersNoClosure() {
+    AndroidDslContributor.ParametrizedTypeExtractor extractor =
+      new AndroidDslContributor.ParametrizedTypeExtractor("String");
+    assertFalse(extractor.isClosure());
+
+    assertFalse(extractor.hasNamedDomainObjectContainer());
+    assertNull(extractor.getNamedDomainObject());
+  }
+}