FIR IDE: handle reference to package and outer classes

For a qualified name like `foo.bar.Outer.Inner`, FIR represents it as
one atomic FIR element. Hence, to properly resolve these names to the
corresponding package and class, we need some additional work.
This commit is contained in:
Tianyu Geng
2021-06-23 18:29:39 -07:00
committed by Ilya Kirillov
parent a537074e1e
commit 84f8d4d315
23 changed files with 332 additions and 58 deletions
@@ -116,6 +116,12 @@ fun FirRegularClassBuilder.addDeclarations(declarations: Collection<FirDeclarati
val FirTypeAlias.expandedConeType: ConeClassLikeType? get() = expandedTypeRef.coneTypeSafe()
val FirClassLikeDeclaration<*>.classId
get() = when (this) {
is FirClass<*> -> symbol.classId
is FirTypeAlias -> symbol.classId
}
val FirClass<*>.classId get() = symbol.classId
val FirClassSymbol<*>.superConeTypes
@@ -5,13 +5,11 @@
package org.jetbrains.kotlin.idea.fir.resolve
import org.jetbrains.kotlin.idea.completion.test.configureWithExtraFile
import org.jetbrains.kotlin.idea.fir.invalidateCaches
import org.jetbrains.kotlin.idea.resolve.AbstractReferenceResolveTest
import org.jetbrains.kotlin.idea.test.KotlinLightProjectDescriptor
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.utils.IgnoreTests
abstract class AbstractFirReferenceResolveTest : AbstractReferenceResolveTest() {
override fun isFirPlugin(): Boolean = true
@@ -23,13 +21,4 @@ abstract class AbstractFirReferenceResolveTest : AbstractReferenceResolveTest()
project.invalidateCaches(myFixture.file as? KtFile)
super.tearDown()
}
override fun doTest(path: String) {
assert(path.endsWith(".kt")) { path }
myFixture.configureWithExtraFile(path, ".Data")
IgnoreTests.runTestIfNotDisabledByFileDirective(testDataFile().toPath(), IgnoreTests.DIRECTIVES.IGNORE_FIR) {
performChecks()
}
}
}
@@ -957,4 +957,52 @@ public class FirReferenceResolveTestGenerated extends AbstractFirReferenceResolv
runTest("idea/testData/resolve/references/packageReference/kotlinPackageSecondQualifier.kt");
}
}
@TestMetadata("idea/testData/resolve/references/qualifiedAccess")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class QualifiedAccess extends AbstractFirReferenceResolveTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
public void testAllFilesPresentInQualifiedAccess() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/resolve/references/qualifiedAccess"), Pattern.compile("^([^.]+)\\.kt$"), null, true);
}
@TestMetadata("callableReference1.kt")
public void testCallableReference1() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/callableReference1.kt");
}
@TestMetadata("callableReference2.kt")
public void testCallableReference2() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/callableReference2.kt");
}
@TestMetadata("callableReference3.kt")
public void testCallableReference3() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/callableReference3.kt");
}
@TestMetadata("ResolveFirstPackageOfFullyQualifiedReference.kt")
public void testResolveFirstPackageOfFullyQualifiedReference() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/ResolveFirstPackageOfFullyQualifiedReference.kt");
}
@TestMetadata("ResolveFullyQualifiedCompanionObject.kt")
public void testResolveFullyQualifiedCompanionObject() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/ResolveFullyQualifiedCompanionObject.kt");
}
@TestMetadata("ResolveOuterClassOfFullyQualifiedReference.kt")
public void testResolveOuterClassOfFullyQualifiedReference() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/ResolveOuterClassOfFullyQualifiedReference.kt");
}
@TestMetadata("ResolvePackageOfFullyQualifiedReference.kt")
public void testResolvePackageOfFullyQualifiedReference() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/ResolvePackageOfFullyQualifiedReference.kt");
}
}
}
@@ -69,7 +69,7 @@ internal object FirReferenceResolveHelper {
private fun ClassId.toTargetPsi(
session: FirSession,
symbolBuilder: KtSymbolByFirBuilder,
calleeReference: FirReference? = null
calleeReference: FirReference? = null,
): KtSymbol? {
val classLikeDeclaration = ConeClassLikeLookupTagImpl(this).toSymbol(session)?.fir
if (classLikeDeclaration is FirRegularClass) {
@@ -178,7 +178,7 @@ internal object FirReferenceResolveHelper {
return when (fir) {
is FirResolvedTypeRef -> getSymbolsForResolvedTypeRef(fir, expression, session, symbolBuilder)
is FirResolvedQualifier ->
getSymbolsForResolvedQualifier(fir, expression, session, symbolBuilder, analysisSession)
getSymbolsForResolvedQualifier(fir, expression, session, symbolBuilder)
is FirAnnotationCall -> getSymbolsForAnnotationCall(fir, session, symbolBuilder)
is FirResolvedImport -> getSymbolsByResolvedImport(expression, symbolBuilder, fir, session)
is FirFile -> getSymbolsByFirFile(expression, symbolBuilder, fir)
@@ -374,42 +374,126 @@ internal object FirReferenceResolveHelper {
fir: FirResolvedQualifier,
expression: KtSimpleNameExpression,
session: FirSession,
symbolBuilder: KtSymbolByFirBuilder,
analysisSession: KtFirAnalysisSession
symbolBuilder: KtSymbolByFirBuilder
): Collection<KtSymbol> {
// TODO refactor that block
val classId = fir.classId ?: return listOfNotNull(getPackageSymbolFor(expression, symbolBuilder, forQualifiedType = false))
var parent = expression.parent as? KtDotQualifiedExpression
// Distinguish A.foo() from A(.Companion).foo()
// Make expression.parent as? KtDotQualifiedExpression local function
while (parent != null) {
val selectorExpression = parent.selectorExpression ?: break
if (selectorExpression === expression) {
parent = parent.parent as? KtDotQualifiedExpression
continue
}
val receiverClassId = if (parent.receiverExpression == expression) {
/*
* <caret>A.Named.i -> class A
*/
val name = fir.relativeClassFqName?.pathSegments()?.firstOrNull()
name?.let { ClassId(fir.packageFqName, it) }
} else null
val parentFir = selectorExpression.getOrBuildFir(analysisSession.firResolveState)
when {
parentFir is FirQualifiedAccess -> {
return listOfNotNull(
(receiverClassId ?: classId).toTargetPsi(session, symbolBuilder, parentFir.calleeReference)
)
}
receiverClassId != null -> {
return listOfNotNull(receiverClassId.toTargetPsi(session, symbolBuilder))
}
else -> parent = parent.parent as? KtDotQualifiedExpression
}
val referencedSymbol = if (fir.resolvedToCompanionObject) {
(fir.symbol?.fir as? FirRegularClass)?.companionObject?.symbol
} else {
fir.symbol
}
return listOfNotNull(classId.toTargetPsi(session, symbolBuilder))
if (referencedSymbol == null) {
// If referencedSymbol is null, it means the reference goes to a package.
val parent = expression.parent as? KtDotQualifiedExpression ?: return emptyList()
val fqNameSegments =
when (expression) {
parent.selectorExpression -> parent.fqNameSegments() ?: return emptyList()
parent.receiverExpression -> listOf(expression.getReferencedName())
else -> return emptyList()
}
return listOfNotNull(symbolBuilder.createPackageSymbolIfOneExists(FqName.fromSegments(fqNameSegments)))
}
val referencedClass = referencedSymbol.fir
val referencedSymbolsByFir = listOfNotNull(symbolBuilder.buildSymbol(referencedClass))
val firSourcePsi = fir.source.psi ?: referencedSymbolsByFir
// The source of an `FirResolvedQualifier` is either a KtNamedReferenceExpression or a KtDotQualifiedExpression. In the former case,
// it implies the qualifier is an atomic reference and therefore, it should be identical with the `expression`. In the latter case,
// we need to manually break up the qualified access and resolve individual parts of it because in FIR, the entire qualified access
// is one element.
if (firSourcePsi === expression) return referencedSymbolsByFir
require(firSourcePsi is KtDotQualifiedExpression)
if (referencedClass.isLocal) {
// TODO: handle local classes after KT-47135 is fixed
return referencedSymbolsByFir
} else {
var qualifiedAccess: KtDotQualifiedExpression = firSourcePsi
val referencedClassId =
if ((referencedClass as? FirRegularClass)?.isCompanion == true &&
(qualifiedAccess.selectorExpression as? KtNameReferenceExpression)?.getReferencedName() != "Companion"
) {
// Remove the last "Companion" part if the qualified access does not contain it. This is needed because the "Companion"
// part is optional.
referencedClass.classId.outerClassId ?: return referencedSymbolsByFir
} else {
referencedClass.classId
}
val qualifiedAccessSegments = qualifiedAccess.fqNameSegments() ?: return referencedSymbolsByFir
assert(referencedClassId.asSingleFqName().pathSegments().takeLast(qualifiedAccessSegments.size)
.map { it.identifierOrNullIfSpecial } == qualifiedAccessSegments) {
"Referenced classId $referencedClassId should end with qualifiedAccess expression ${qualifiedAccess.text} "
}
// In the code below, we always maintain the contract that `classId` and `qualifiedAccess` should stay "in-sync", i.e. they
// refer to the same class and classId should be null if `qualifiedAccess` references to a package.
var classId: ClassId? = referencedClassId
// Handle nested classes.
while (classId != null) {
if (expression === qualifiedAccess.selectorExpression) {
return listOfNotNull(classId.toTargetPsi(session, symbolBuilder))
}
val outerClassId = classId.outerClassId
val receiverExpression = qualifiedAccess.receiverExpression
if (receiverExpression !is KtDotQualifiedExpression) {
// If the receiver is not a KtDotQualifiedExpression, it means we are hitting the end of nested receivers. In other
// words, this receiver expression should be pointing at an unqualified name of a class, whose class ID is
// `outerClassId`.
if (receiverExpression == expression) {
// If there is still an outer class, then return symbol of that class
outerClassId?.let { return listOfNotNull(it.toTargetPsi(session, symbolBuilder)) }
// Otherwise, it should be a package, so we return that
return listOfNotNull(symbolBuilder.createPackageSymbolIfOneExists(classId.packageFqName))
} else {
// This is unexpected. The code probably contains some weird structures. In this case, we just fail the resolution
// with zero results.
return emptyList()
}
}
qualifiedAccess = receiverExpression
classId = outerClassId
}
// Handle package names
var packageFqName = referencedClassId.packageFqName
while (!packageFqName.isRoot) {
if (expression === qualifiedAccess.selectorExpression) {
return listOfNotNull(symbolBuilder.createPackageSymbolIfOneExists(packageFqName))
}
val parentPackageFqName = packageFqName.parent()
val receiverExpression = qualifiedAccess.receiverExpression
if (receiverExpression !is KtDotQualifiedExpression) {
// If the receiver is not a KtDotQualifiedExpression, it means we are hitting the end of nested receivers. In other
// words, this receiver expression should be pointing at a top-level package now.
if (receiverExpression == expression) {
return listOfNotNull(symbolBuilder.createPackageSymbolIfOneExists(parentPackageFqName))
} else {
// This is unexpected. The code probably contains some weird structures. In this case, we just fail the resolution
// with zero results.
return emptyList()
}
}
qualifiedAccess = receiverExpression
packageFqName = parentPackageFqName
}
return referencedSymbolsByFir
}
}
/**
* Returns the segments of a qualified access PSI. For example, given `foo.bar.OuterClass.InnerClass`, this returns `["foo", "bar",
* "OuterClass", "InnerClass"]`.
*/
private fun KtDotQualifiedExpression.fqNameSegments(): List<String>? {
val result: MutableList<String> = mutableListOf()
var current: KtExpression = this
while (current is KtDotQualifiedExpression) {
result += (current.selectorExpression as? KtNameReferenceExpression)?.getReferencedName() ?: return null
current = current.receiverExpression
}
result += (current as? KtNameReferenceExpression)?.getReferencedName() ?: return null
result.reverse()
return result
}
private fun getSymbolsForAnnotationCall(
@@ -37,13 +37,9 @@ import org.jetbrains.kotlin.idea.search.KotlinSearchUsagesSupport.Companion.scri
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.types.expressions.OperatorConventions
import java.util.*
infix fun SearchScope.and(otherScope: SearchScope): SearchScope = intersectWith(otherScope)
infix fun SearchScope.or(otherScope: SearchScope): SearchScope = union(otherScope)
@@ -218,6 +218,7 @@ object IgnoreTests {
const val FIR_IDENTICAL = "// FIR_IDENTICAL"
const val IGNORE_FE10_BINDING_BY_FIR = "// IGNORE_FE10_BINDING_BY_FIR"
const val IGNORE_FE10 = "// IGNORE_FE10"
}
enum class DirectivePosition {
@@ -1,3 +1,4 @@
// FIR_COMPARISON
// PSI_ELEMENT: org.jetbrains.kotlin.psi.KtObjectDeclaration
// OPTIONS: usages
class Foo {
@@ -1,3 +1,4 @@
// FIR_COMPARISON
// PSI_ELEMENT: org.jetbrains.kotlin.psi.KtObjectDeclaration
// OPTIONS: usages
class Foo {
@@ -1,3 +1,4 @@
// FIR_COMPARISON
// PSI_ELEMENT: org.jetbrains.kotlin.psi.KtObjectDeclaration
// OPTIONS: usages
fun foo(): Any {
@@ -1 +1 @@
Value read 6 return Bar
Value read 7 return Bar
@@ -1,3 +1,4 @@
// FIR_COMPARISON
// PSI_ELEMENT: org.jetbrains.kotlin.psi.KtObjectDeclaration
// OPTIONS: usages
fun foo(): Any {
@@ -1 +1 @@
Value read 7 return Bar
Value read 8 return Bar
@@ -1,5 +1,3 @@
// IGNORE_FIR
package test
class Conflict
@@ -1,3 +1,4 @@
// IGNORE_FIR
package test
object Conflict
@@ -0,0 +1,13 @@
package foo.bar.baz
class AA {
class BB {
companion object
}
}
fun test() {
val b = f<caret>oo.bar.baz.AA.BB
}
// REF: foo
@@ -0,0 +1,14 @@
// IGNORE_FIR
package foo.bar.baz
class AA {
class BB {
companion object
}
}
fun test() {
val b = foo.bar.baz.AA.B<caret>B
}
// REF: companion object of (in foo.bar.baz.AA).BB
@@ -0,0 +1,13 @@
package foo.bar.baz
class AA {
class BB {
companion object
}
}
fun test() {
val b = foo.bar.baz.A<caret>A.BB
}
// REF: (foo.bar.baz).AA
@@ -0,0 +1,13 @@
package foo.bar.baz
class AA {
class BB {
companion object
}
}
fun test() {
val b = foo.bar.b<caret>az.AA.BB
}
// REF: baz
@@ -0,0 +1,11 @@
package foo.bar.baz
class AA {
fun foo() {}
}
fun test() {
A<caret>A::foo
}
// REF: (foo.bar.baz).AA
@@ -0,0 +1,14 @@
// IGNORE_FE10
package foo.bar.baz
class AA {
companion object {
fun foo() {}
}
}
fun test() {
A<caret>A::foo // FE1.0 won't resolve this
}
// REF: companion object of (foo.bar.baz).AA
@@ -0,0 +1,14 @@
// IGNORE_FE10
package foo.bar.baz
class AA {
companion object
}
fun AA.Companion.foo() {}
fun test() {
A<caret>A::foo // FE1.0 won't resolve this
}
// REF: companion object of (foo.bar.baz).AA
@@ -20,8 +20,8 @@ import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.jetbrains.kotlin.test.util.renderAsGotoImplementation
import org.jetbrains.kotlin.test.utils.IgnoreTests
import org.junit.Assert
import java.util.concurrent.Callable
import kotlin.test.assertTrue
abstract class AbstractReferenceResolveTest : KotlinLightPlatformCodeInsightFixtureTestCase() {
@@ -35,7 +35,14 @@ abstract class AbstractReferenceResolveTest : KotlinLightPlatformCodeInsightFixt
protected open fun doTest(path: String) {
assert(path.endsWith(".kt")) { path }
myFixture.configureWithExtraFile(path, ".Data")
performChecks()
val controlDirective = if (isFirPlugin()) {
IgnoreTests.DIRECTIVES.IGNORE_FIR
} else {
IgnoreTests.DIRECTIVES.IGNORE_FE10
}
IgnoreTests.runTestIfNotDisabledByFileDirective(testDataFile().toPath(), controlDirective) {
performChecks()
}
}
protected fun performChecks() {
@@ -957,4 +957,52 @@ public class ReferenceResolveTestGenerated extends AbstractReferenceResolveTest
runTest("idea/testData/resolve/references/packageReference/kotlinPackageSecondQualifier.kt");
}
}
@TestMetadata("idea/testData/resolve/references/qualifiedAccess")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class QualifiedAccess extends AbstractReferenceResolveTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
public void testAllFilesPresentInQualifiedAccess() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/resolve/references/qualifiedAccess"), Pattern.compile("^([^.]+)\\.kt$"), null, true);
}
@TestMetadata("callableReference1.kt")
public void testCallableReference1() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/callableReference1.kt");
}
@TestMetadata("callableReference2.kt")
public void testCallableReference2() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/callableReference2.kt");
}
@TestMetadata("callableReference3.kt")
public void testCallableReference3() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/callableReference3.kt");
}
@TestMetadata("ResolveFirstPackageOfFullyQualifiedReference.kt")
public void testResolveFirstPackageOfFullyQualifiedReference() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/ResolveFirstPackageOfFullyQualifiedReference.kt");
}
@TestMetadata("ResolveFullyQualifiedCompanionObject.kt")
public void testResolveFullyQualifiedCompanionObject() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/ResolveFullyQualifiedCompanionObject.kt");
}
@TestMetadata("ResolveOuterClassOfFullyQualifiedReference.kt")
public void testResolveOuterClassOfFullyQualifiedReference() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/ResolveOuterClassOfFullyQualifiedReference.kt");
}
@TestMetadata("ResolvePackageOfFullyQualifiedReference.kt")
public void testResolvePackageOfFullyQualifiedReference() throws Exception {
runTest("idea/testData/resolve/references/qualifiedAccess/ResolvePackageOfFullyQualifiedReference.kt");
}
}
}