HL API: implement tests for reference resolve

This commit is contained in:
Ilya Kirillov
2021-07-21 17:06:18 +02:00
committed by teamcityserver
parent 55489b6249
commit 970d7b5a78
8 changed files with 242 additions and 12 deletions
@@ -462,10 +462,15 @@ class KotlinCoreEnvironment private constructor(
companion object {
private val LOG = Logger.getInstance(KotlinCoreEnvironment::class.java)
private val APPLICATION_LOCK = Object()
@PublishedApi
internal val APPLICATION_LOCK = Object()
private var ourApplicationEnvironment: KotlinCoreApplicationEnvironment? = null
private var ourProjectCount = 0
inline fun <R> underApplicationLock(action: () -> R): R =
synchronized(APPLICATION_LOCK) { action() }
@JvmStatic
fun createForProduction(
parentDisposable: Disposable, configuration: CompilerConfiguration, configFiles: EnvironmentConfigFiles
@@ -108,6 +108,8 @@ public enum class RendererModifier(public val includeByDefault: Boolean) {
public companion object {
public val ALL: Set<RendererModifier> = values().toSet()
public val DEFAULT: Set<RendererModifier> = values().filterTo(mutableSetOf()) { it.includeByDefault }
public val NONE: Set<RendererModifier> = emptySet()
}
}
@@ -5,17 +5,19 @@
package org.jetbrains.kotlin.idea.fir.low.level.api.test.base
import com.intellij.DynamicBundle
import com.intellij.core.CoreApplicationEnvironment
import com.intellij.mock.MockApplication
import com.intellij.mock.MockProject
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementFinder
import org.jetbrains.kotlin.asJava.KotlinAsJavaSupport
import org.jetbrains.kotlin.asJava.finder.JavaElementFinder
import org.jetbrains.kotlin.fir.session.FirModuleInfoBasedModuleData
import org.jetbrains.kotlin.idea.fir.low.level.api.compiler.based.FirModuleResolveStateConfiguratorForSingleModuleTestImpl
import org.jetbrains.kotlin.idea.fir.low.level.api.compiler.based.registerTestServices
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.Disposer
import com.intellij.psi.impl.source.tree.TreeCopyHandler
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.idea.fir.low.level.api.compiler.based.*
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.TestInfrastructureInternals
import org.jetbrains.kotlin.test.bind
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.builders.testConfiguration
@@ -36,6 +38,7 @@ import kotlin.io.path.nameWithoutExtension
abstract class AbstractLowLevelApiTest : TestWithDisposable() {
private lateinit var testInfo: KotlinTestInfo
@OptIn(TestInfrastructureInternals::class)
private val configure: TestConfigurationBuilder.() -> Unit = {
globalDefaults {
frontend = FrontendKinds.FIR
@@ -95,11 +98,26 @@ abstract class AbstractLowLevelApiTest : TestWithDisposable() {
registerServicesForProject(this)
}
doTestByFileStructure(moduleInfo.ktFiles.toList(), moduleStructure, testServices)
registerApplicationServices()
doTestByFileStructure(
moduleInfo.testFilesToKtFiles.filter { (testFile, _) -> !testFile.isAdditional }.values.toList(),
moduleStructure,
testServices
)
}
private fun registerApplicationServices() {
val application = ApplicationManager.getApplication() as MockApplication
KotlinCoreEnvironment.underApplicationLock {
registerApplicationServices(application)
}
}
protected open fun registerServicesForProject(project: MockProject) {}
protected open fun registerApplicationServices(application: MockApplication) {}
protected abstract fun doTestByFileStructure(ktFiles: List<KtFile>, moduleStructure: TestModuleStructure, testServices: TestServices)
@BeforeEach
@@ -71,9 +71,13 @@ class ExpressionMarkerProvider(testServices: TestServices) : TestService {
}
@OptIn(PrivateForInline::class)
inline fun <reified P : KtElement> getElementOfTypAtCaret(file: KtFile): P {
val offset = atCaret[file.name]
fun getCaretPosition(file: KtFile): Int {
return atCaret[file.name]
?: error("No caret found in file")
}
inline fun <reified P : KtElement> getElementOfTypAtCaret(file: KtFile): P {
val offset = getCaretPosition(file)
return file.findElementAt(offset)
?.parentOfType<P>()
?: error("No expression found at caret")
@@ -0,0 +1,70 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.references
import com.intellij.psi.ContributedReferenceHost
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.psi.PsiReferenceService
import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.util.containers.ConcurrentFactoryMap
import com.intellij.util.containers.MultiMap
import org.jetbrains.kotlin.idea.references.*
import org.jetbrains.kotlin.psi.KotlinReferenceProvidersService
import org.jetbrains.kotlin.utils.SmartList
class HLApiReferenceProviderService : KotlinReferenceProvidersService() {
private val originalProvidersBinding: MultiMap<Class<out PsiElement>, KotlinPsiReferenceProvider>
private val providersBindingCache: Map<Class<out PsiElement>, List<KotlinPsiReferenceProvider>>
init {
val registrar = KotlinPsiReferenceRegistrar()
KotlinReferenceProviderContributor.getInstance().registerReferenceProviders(registrar)
originalProvidersBinding = registrar.providers
providersBindingCache = ConcurrentFactoryMap.createMap<Class<out PsiElement>, List<KotlinPsiReferenceProvider>> { klass ->
val result = SmartList<KotlinPsiReferenceProvider>()
for (bindingClass in originalProvidersBinding.keySet()) {
if (bindingClass.isAssignableFrom(klass)) {
result.addAll(originalProvidersBinding.get(bindingClass))
}
}
result
}
}
private fun doGetKotlinReferencesFromProviders(context: PsiElement): Array<PsiReference> {
val providers: List<KotlinPsiReferenceProvider>? = providersBindingCache[context.javaClass]
if (providers.isNullOrEmpty()) return PsiReference.EMPTY_ARRAY
val result = SmartList<PsiReference>()
for (provider in providers) {
result.addAll(provider.getReferencesByElement(context))
}
if (result.isEmpty()) {
return PsiReference.EMPTY_ARRAY
}
return result.toTypedArray()
}
override fun getReferences(psiElement: PsiElement): Array<PsiReference> {
if (psiElement is ContributedReferenceHost) {
return ReferenceProvidersRegistry.getReferencesFromProviders(psiElement, PsiReferenceService.Hints.NO_HINTS)
}
return CachedValuesManager.getCachedValue(psiElement) {
CachedValueProvider.Result.create(
doGetKotlinReferencesFromProviders(psiElement),
PsiModificationTracker.MODIFICATION_COUNT
)
}
}
}
@@ -0,0 +1,119 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.fir.frontend.api
import com.intellij.psi.PsiReference
import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference
import org.jetbrains.kotlin.idea.fir.frontend.api.test.framework.AbstractHLApiSingleModuleTest
import org.jetbrains.kotlin.idea.fir.low.level.api.test.base.expressionMarkerProvider
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.analyse
import org.jetbrains.kotlin.idea.frontend.api.components.KtDeclarationRendererOptions
import org.jetbrains.kotlin.idea.frontend.api.components.RendererModifier
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtCallableSymbol
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassLikeSymbol
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtConstructorSymbol
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtSymbol
import org.jetbrains.kotlin.idea.references.KtReference
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.assertions
abstract class AbstractReferenceResolveTest : AbstractHLApiSingleModuleTest() {
override fun configureTest(builder: TestConfigurationBuilder) {
super.configureTest(builder)
with(builder) {
defaultDirectives {
+JvmEnvironmentConfigurationDirectives.WITH_STDLIB
}
useDirectives(Directives)
}
}
override fun doTestByFileStructure(ktFiles: List<KtFile>, module: TestModule, testServices: TestServices) {
val mainKtFile = ktFiles.singleOrNull() ?: ktFiles.first { it.name == "main.kt" }
val caretPosition = testServices.expressionMarkerProvider.getCaretPosition(mainKtFile)
val ktReferences = findReferencesAtCaret(mainKtFile, caretPosition)
if (ktReferences.isEmpty()) {
testServices.assertions.fail { "No references at caret found" }
}
val resolvedTo = analyse(mainKtFile) {
val symbols = ktReferences.flatMap { it.resolveToSymbols() }
checkReferenceResultForValidity(module, testServices, symbols)
renderResolvedTo(symbols)
}
if (Directives.UNRESOLVED_REFERENCE in module.directives) {
return
}
val actual = "Resolved to:\n$resolvedTo"
testServices.assertions.assertEqualsToFile(testDataFileSibling(".txt"), actual)
}
private fun findReferencesAtCaret(mainKtFile: KtFile, caretPosition: Int): List<KtReference> =
mainKtFile.findReferenceAt(caretPosition)?.unwrapMultiReferences().orEmpty().filterIsInstance<KtReference>()
private fun KtAnalysisSession.checkReferenceResultForValidity(
module: TestModule,
testServices: TestServices,
resolvedTo: List<KtSymbol>
) {
if (Directives.UNRESOLVED_REFERENCE in module.directives) {
testServices.assertions.assertTrue(resolvedTo.isEmpty()) {
"Reference should be unresolved, but was resolved to ${renderResolvedTo(resolvedTo)}"
}
} else {
testServices.assertions.assertTrue(resolvedTo.isNotEmpty()) { "Unresolved reference" }
}
}
private fun KtAnalysisSession.renderResolvedTo(symbols: List<KtSymbol>) =
symbols.map { renderResolveResult(it) }
.sorted()
.withIndex()
.joinToString(separator = "\n") { "${it.index}: ${it.value}" }
private fun KtAnalysisSession.renderResolveResult(symbol: KtSymbol): String {
return buildString {
symbolFqName(symbol)?.let { fqName ->
append("(in $fqName) ")
}
append(symbol.render(renderingOptions))
}
}
@Suppress("unused")// KtAnalysisSession receiver
private fun KtAnalysisSession.symbolFqName(symbol: KtSymbol): String? = when (symbol) {
is KtCallableSymbol -> symbol.callableIdIfNonLocal?.asSingleFqName()?.parent()
is KtClassLikeSymbol -> symbol.classIdIfNonLocal?.asSingleFqName()?.parent()
is KtConstructorSymbol -> symbol.containingClassIdIfNonLocal?.asSingleFqName()
else -> null
}?.let { if (it == FqName.ROOT) "ROOT" else it.asString()}
private object Directives : SimpleDirectivesContainer() {
val UNRESOLVED_REFERENCE by directive(
"Reference should be unresolved",
)
}
private val renderingOptions = KtDeclarationRendererOptions.DEFAULT.copy(
modifiers = RendererModifier.DEFAULT - RendererModifier.ANNOTATIONS
)
private fun PsiReference.unwrapMultiReferences(): List<PsiReference> = when (this) {
is KtReference -> listOf(this)
is PsiMultiReference -> references.flatMap { it.unwrapMultiReferences() }
else -> error("Unexpected reference $this")
}
}
@@ -12,7 +12,7 @@ import org.jetbrains.kotlin.test.services.TestServices
abstract class AbstractHLApiSingleModuleTest : AbstractHLApiTest() {
final override fun doTestByFileStructure(ktFiles: List<KtFile>, moduleStructure: TestModuleStructure, testServices: TestServices) {
val singleModule = moduleStructure.modules.single()
val singleModule = moduleStructure.modules.single()
doTestByFileStructure(ktFiles, singleModule, testServices)
}
@@ -5,11 +5,16 @@
package org.jetbrains.kotlin.idea.fir.frontend.api.test.framework
import com.intellij.mock.MockApplication
import com.intellij.mock.MockProject
import org.jetbrains.kotlin.idea.fir.low.level.api.test.base.AbstractLowLevelApiTest
import org.jetbrains.kotlin.idea.frontend.api.InvalidWayOfUsingAnalysisSession
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSessionProvider
import org.jetbrains.kotlin.idea.frontend.api.fir.KtFirAnalysisSessionProvider
import org.jetbrains.kotlin.idea.references.HLApiReferenceProviderService
import org.jetbrains.kotlin.idea.references.KotlinFirReferenceContributor
import org.jetbrains.kotlin.idea.references.KotlinReferenceProviderContributor
import org.jetbrains.kotlin.psi.KotlinReferenceProvidersService
abstract class AbstractHLApiTest : AbstractLowLevelApiTest() {
@OptIn(InvalidWayOfUsingAnalysisSession::class)
@@ -17,4 +22,11 @@ abstract class AbstractHLApiTest : AbstractLowLevelApiTest() {
super.registerServicesForProject(project)
project.registerService(KtAnalysisSessionProvider::class.java, KtFirAnalysisSessionProvider::class.java)
}
override fun registerApplicationServices(application: MockApplication) {
super.registerApplicationServices(application)
if (application.getServiceIfCreated(KotlinReferenceProvidersService::class.java) != null) return
application.registerService(KotlinReferenceProvidersService::class.java, HLApiReferenceProviderService::class.java)
application.registerService(KotlinReferenceProviderContributor::class.java, KotlinFirReferenceContributor::class.java)
}
}