From 970d7b5a78aeb07eb39e994fa8d3f2af842509e7 Mon Sep 17 00:00:00 2001 From: Ilya Kirillov Date: Wed, 21 Jul 2021 17:06:18 +0200 Subject: [PATCH] HL API: implement tests for reference resolve --- .../cli/jvm/compiler/KotlinCoreEnvironment.kt | 7 +- .../KtSymbolDeclarationRendererProvider.kt | 2 + .../api/test/base/AbstractLowLevelApiTest.kt | 34 +++-- ...ExpressionMarkersSourceFilePreprocessor.kt | 8 +- .../HLApiReferenceProviderService.kt | 70 +++++++++++ .../api/AbstractReferenceResolveTest.kt | 119 ++++++++++++++++++ .../AbstractHLApiSingleModuleTest.kt | 2 +- .../api/test/framework/AbstractHLApiTest.kt | 12 ++ 8 files changed, 242 insertions(+), 12 deletions(-) create mode 100644 idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/references/HLApiReferenceProviderService.kt create mode 100644 idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/AbstractReferenceResolveTest.kt diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt index 37c0d58af94..fbdd47b56ab 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt @@ -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 underApplicationLock(action: () -> R): R = + synchronized(APPLICATION_LOCK) { action() } + @JvmStatic fun createForProduction( parentDisposable: Disposable, configuration: CompilerConfiguration, configFiles: EnvironmentConfigFiles diff --git a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtSymbolDeclarationRendererProvider.kt b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtSymbolDeclarationRendererProvider.kt index ab7e3a74cf7..863ff31b9df 100644 --- a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtSymbolDeclarationRendererProvider.kt +++ b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtSymbolDeclarationRendererProvider.kt @@ -108,6 +108,8 @@ public enum class RendererModifier(public val includeByDefault: Boolean) { public companion object { public val ALL: Set = values().toSet() + public val DEFAULT: Set = values().filterTo(mutableSetOf()) { it.includeByDefault } + public val NONE: Set = emptySet() } } diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/test/base/AbstractLowLevelApiTest.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/test/base/AbstractLowLevelApiTest.kt index a9d28d726c9..aa8f7fe64c2 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/test/base/AbstractLowLevelApiTest.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/test/base/AbstractLowLevelApiTest.kt @@ -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, moduleStructure: TestModuleStructure, testServices: TestServices) @BeforeEach diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/test/base/ExpressionMarkersSourceFilePreprocessor.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/test/base/ExpressionMarkersSourceFilePreprocessor.kt index 7c5246f66ea..71a0b5faf7b 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/test/base/ExpressionMarkersSourceFilePreprocessor.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/test/base/ExpressionMarkersSourceFilePreprocessor.kt @@ -71,9 +71,13 @@ class ExpressionMarkerProvider(testServices: TestServices) : TestService { } @OptIn(PrivateForInline::class) - inline fun 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 getElementOfTypAtCaret(file: KtFile): P { + val offset = getCaretPosition(file) return file.findElementAt(offset) ?.parentOfType

() ?: error("No expression found at caret") diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/references/HLApiReferenceProviderService.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/references/HLApiReferenceProviderService.kt new file mode 100644 index 00000000000..1e043e8db9a --- /dev/null +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/references/HLApiReferenceProviderService.kt @@ -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, KotlinPsiReferenceProvider> + private val providersBindingCache: Map, List> + + init { + val registrar = KotlinPsiReferenceRegistrar() + KotlinReferenceProviderContributor.getInstance().registerReferenceProviders(registrar) + originalProvidersBinding = registrar.providers + + providersBindingCache = ConcurrentFactoryMap.createMap, List> { klass -> + val result = SmartList() + for (bindingClass in originalProvidersBinding.keySet()) { + if (bindingClass.isAssignableFrom(klass)) { + result.addAll(originalProvidersBinding.get(bindingClass)) + } + } + result + } + } + + private fun doGetKotlinReferencesFromProviders(context: PsiElement): Array { + val providers: List? = providersBindingCache[context.javaClass] + if (providers.isNullOrEmpty()) return PsiReference.EMPTY_ARRAY + + val result = SmartList() + 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 { + if (psiElement is ContributedReferenceHost) { + return ReferenceProvidersRegistry.getReferencesFromProviders(psiElement, PsiReferenceService.Hints.NO_HINTS) + } + + return CachedValuesManager.getCachedValue(psiElement) { + CachedValueProvider.Result.create( + doGetKotlinReferencesFromProviders(psiElement), + PsiModificationTracker.MODIFICATION_COUNT + ) + } + } +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/AbstractReferenceResolveTest.kt b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/AbstractReferenceResolveTest.kt new file mode 100644 index 00000000000..581b04e3374 --- /dev/null +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/AbstractReferenceResolveTest.kt @@ -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, 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 = + mainKtFile.findReferenceAt(caretPosition)?.unwrapMultiReferences().orEmpty().filterIsInstance() + + private fun KtAnalysisSession.checkReferenceResultForValidity( + module: TestModule, + testServices: TestServices, + resolvedTo: List + ) { + 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) = + 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 = when (this) { + is KtReference -> listOf(this) + is PsiMultiReference -> references.flatMap { it.unwrapMultiReferences() } + else -> error("Unexpected reference $this") + } +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/test/framework/AbstractHLApiSingleModuleTest.kt b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/test/framework/AbstractHLApiSingleModuleTest.kt index e6fa634dace..783cd50dda5 100644 --- a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/test/framework/AbstractHLApiSingleModuleTest.kt +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/test/framework/AbstractHLApiSingleModuleTest.kt @@ -12,7 +12,7 @@ import org.jetbrains.kotlin.test.services.TestServices abstract class AbstractHLApiSingleModuleTest : AbstractHLApiTest() { final override fun doTestByFileStructure(ktFiles: List, moduleStructure: TestModuleStructure, testServices: TestServices) { - val singleModule = moduleStructure.modules.single() + val singleModule = moduleStructure.modules.single() doTestByFileStructure(ktFiles, singleModule, testServices) } diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/test/framework/AbstractHLApiTest.kt b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/test/framework/AbstractHLApiTest.kt index c08a5a00d50..85466512cc7 100644 --- a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/test/framework/AbstractHLApiTest.kt +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/fir/frontend/api/test/framework/AbstractHLApiTest.kt @@ -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) + } } \ No newline at end of file