[gradle] process conventions' members as additional members of Project receiver

This fixes `project.someProperty` cases where `someProperty` is a property of some convention.
Also conventions doesn't support setter invocation via method call, so they are ignored.
This commit is contained in:
Daniil Ovchinnikov
2019-03-22 17:33:02 +03:00
parent 27e1980ef0
commit 9a187b0213
7 changed files with 133 additions and 22 deletions
@@ -62,6 +62,7 @@
<membersContributor implementation="org.jetbrains.plugins.gradle.service.resolve.GradleNonCodeMembersContributor"/>
<membersContributor implementation="org.jetbrains.plugins.gradle.service.resolve.GradleScriptContributor"/>
<membersContributor implementation="org.jetbrains.plugins.gradle.service.resolve.GradleSetterAsMethodContributor"/>
<membersContributor implementation="org.jetbrains.plugins.gradle.service.resolve.GradleConventionsContributor"/>
<delegatesToProvider implementation="org.jetbrains.plugins.gradle.service.resolve.GradleDelegatesToProvider"/>
<referenceTypeEnhancer implementation="org.jetbrains.plugins.gradle.service.resolve.NamedDomainObjectCollectionTypeEnhancer"/>
<unresolvedHighlightFilter implementation="org.jetbrains.plugins.gradle.service.resolve.GradleUnresolvedReferenceFilter"/>
@@ -0,0 +1,31 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.gradle.service.resolve
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiType
import com.intellij.psi.ResolveState
import com.intellij.psi.scope.PsiScopeProcessor
import org.jetbrains.plugins.gradle.service.resolve.GradleCommonClassNames.*
import org.jetbrains.plugins.groovy.lang.resolve.NonCodeMembersContributor
class GradleConventionsContributor : NonCodeMembersContributor() {
companion object {
val conventions = arrayOf(
GRADLE_API_BASE_PLUGIN_CONVENTION,
GRADLE_API_JAVA_PLUGIN_CONVENTION,
GRADLE_API_APPLICATION_PLUGIN_CONVENTION,
GRADLE_API_WAR_CONVENTION
)
}
/**
* Plugin conventions are available on the project instance, be it top level reference or `project.something`.
*/
override fun getParentClassName(): String = GRADLE_API_PROJECT
override fun processDynamicElements(qualifierType: PsiType, processor: PsiScopeProcessor, place: PsiElement, state: ResolveState) {
// TODO process conventions from Gradle import instead of hardcoded ones
processDelegatedDeclarations(processor, state, place, *conventions)
}
}
@@ -1,12 +1,10 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.gradle.service.resolve
import com.intellij.patterns.PsiJavaElementPattern
import com.intellij.patterns.PsiJavaPatterns.psiElement
import com.intellij.patterns.StandardPatterns.or
import com.intellij.psi.PsiElement
import com.intellij.psi.ResolveState
import com.intellij.psi.scope.PsiScopeProcessor
import groovy.lang.Closure
import org.jetbrains.plugins.gradle.service.resolve.GradleCommonClassNames.*
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock
@@ -20,6 +18,7 @@ import org.jetbrains.plugins.groovy.lang.resolve.delegatesTo.DelegatesToInfo
* @author Vladislav.Soroka
*/
class GradleJavaContributor : GradleMethodContextContributor {
companion object {
val sourceSetsClosure: GroovyClosurePattern = groovyClosure().inMethod(psiMethod(GRADLE_API_JAVA_PLUGIN_CONVENTION, "sourceSets"))
val sourceDirectorySetClosure: PsiJavaElementPattern.Capture<PsiElement> = psiElement().andOr(
@@ -36,16 +35,4 @@ class GradleJavaContributor : GradleMethodContextContributor {
}
return null
}
override fun process(methodCallInfo: List<String>, processor: PsiScopeProcessor, state: ResolveState, place: PsiElement): Boolean {
if (!GradleResolverUtil.processDeclarations(processor, state, place,
GRADLE_API_BASE_PLUGIN_CONVENTION,
GRADLE_API_JAVA_PLUGIN_CONVENTION,
GRADLE_API_APPLICATION_PLUGIN_CONVENTION,
GRADLE_API_WAR_CONVENTION)) {
return false
}
return true
}
}
}
@@ -96,7 +96,9 @@ class GradleNonCodeMembersContributor : NonCodeMembersContributor() {
extensionsData.findProperty(propCandidate)?.let(processVariable)
}
else {
processDeclarations(aClass, processor, state, place)
if (aClass.qualifiedName !in GradleConventionsContributor.conventions) {
processDeclarations(aClass, processor, state, place)
}
val propCandidate = place.references.singleOrNull()?.canonicalText ?: return
val domainObjectType = (qualifierType.superTypes.firstOrNull { it is PsiClassType } as? PsiClassType)?.parameters?.singleOrNull()
?: return
@@ -37,11 +37,16 @@ internal fun PsiFile?.isGradleScript() = this?.originalFile?.virtualFile?.extens
val RESOLVED_CODE: Key<Boolean?> = Key.create("gradle.resolved")
// TODO extract API for delegation
fun processDelegatedDeclarations(processor: PsiScopeProcessor, state: ResolveState, place: PsiElement, fqn: String): Boolean {
fun processDelegatedDeclarations(processor: PsiScopeProcessor, state: ResolveState, place: PsiElement, vararg fqns: String): Boolean {
val javaPsiFacade = JavaPsiFacade.getInstance(place.project)
val clazz = javaPsiFacade.findClass(fqn, place.resolveScope) ?: return true
val type = javaPsiFacade.elementFactory.createType(clazz)
return type.processReceiverType(processor, state, place)
for (fqn in fqns) {
val clazz = javaPsiFacade.findClass(fqn, place.resolveScope) ?: continue
val type = javaPsiFacade.elementFactory.createType(clazz)
if (!type.processReceiverType(processor, state, place)) {
return false
}
}
return true
}
fun processDeclarations(aClass: PsiClass,
@@ -0,0 +1,61 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.gradle.dsl
import com.intellij.psi.PsiMethod
import com.intellij.testFramework.RunAll
import groovy.transform.CompileStatic
import org.jetbrains.plugins.gradle.highlighting.GradleHighlightingBaseTest
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall
import org.jetbrains.plugins.groovy.util.ResolveTest
import org.junit.Test
import static org.jetbrains.plugins.gradle.service.resolve.GradleCommonClassNames.GRADLE_API_JAVA_PLUGIN_CONVENTION
@CompileStatic
class GradleConventionsTest extends GradleHighlightingBaseTest implements ResolveTest {
// The test should fail when conventions would be taken from Gradle import
// instead of being processed in org.jetbrains.plugins.gradle.service.resolve.GradleConventionsContributor unconditionally.
// In such case `apply plugin: 'java'` is required to fix the test.
@Test
void test() {
importProject("")
new RunAll().append {
'property read'()
} append {
'property read via project'()
} append {
'property write'()
} append {
'setter method'()
} run()
}
void 'property read'() {
doTest('<caret>docsDir') {
methodTest(resolveTest(PsiMethod), 'getDocsDir', GRADLE_API_JAVA_PLUGIN_CONVENTION)
}
}
void 'property read via project'() {
doTest('project.<caret>docsDir') {
methodTest(resolveTest(PsiMethod), 'getDocsDir', GRADLE_API_JAVA_PLUGIN_CONVENTION)
}
}
void 'property write'() {
doTest('<caret>sourceCompatibility = 42') {
methodTest(resolveTest(PsiMethod), 'setSourceCompatibility', GRADLE_API_JAVA_PLUGIN_CONVENTION)
}
}
void 'setter method'() {
doTest('<caret>targetCompatibility("1.8")') {
def call = elementUnderCaret(GrMethodCall)
def result = call.advancedResolve()
assert result.invokedOnProperty
// getTargetCompatibility() is resolved, just because it exists, but later it's highlighted with warning
methodTest(assertInstanceOf(result.element, PsiMethod), 'getTargetCompatibility', GRADLE_API_JAVA_PLUGIN_CONVENTION)
}
}
}
@@ -1,7 +1,9 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.groovy.util;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiReference;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -20,7 +22,13 @@ public interface ResolveTest extends BaseTest {
@NotNull
default <T extends PsiReference> T referenceByText(@NotNull String text, @NotNull Class<T> refType) {
PsiReference ref = configureByText(text).findReferenceAt(getFixture().getCaretOffset());
configureByText(text);
return referenceUnderCaret(refType);
}
@NotNull
default <T extends PsiReference> T referenceUnderCaret(@NotNull Class<T> refType) {
PsiReference ref = getGroovyFile().findReferenceAt(getFixture().getCaretOffset());
return assertInstanceOf(ref, refType);
}
@@ -43,6 +51,10 @@ public interface ResolveTest extends BaseTest {
return resolveTest(referenceByText(text), clazz);
}
default <T extends PsiElement> T resolveTest(@Nullable Class<T> clazz) {
return resolveTest(referenceUnderCaret(GroovyReference.class), clazz);
}
default <T extends PsiElement> T resolveTest(@NotNull GroovyReference reference, @Nullable Class<T> clazz) {
Collection<? extends GroovyResolveResult> results = reference.resolve(false);
if (clazz == null) {
@@ -62,4 +74,16 @@ public interface ResolveTest extends BaseTest {
assertType(fqn, delegatesToInfo.getTypeToDelegate());
assertEquals(strategy, delegatesToInfo.getStrategy());
}
default void methodTest(@NotNull PsiMethod method, String name, String fqn) {
assertEquals(name, method.getName());
PsiClass containingClass = method.getContainingClass();
if (fqn == null) {
assertNull(containingClass);
}
else {
assertNotNull(containingClass);
assertEquals(fqn, containingClass.getQualifiedName());
}
}
}