From d68587de7750809e5f36c23928c377e3acbe8596 Mon Sep 17 00:00:00 2001 From: Ilya Kirillov Date: Mon, 17 Apr 2023 18:12:00 +0200 Subject: [PATCH] [Analysis API] implement API to extend Kotlin resolution by generated declarations ^KT-57930 fixed --- .../components/KtAnalysisScopeProviderImpl.kt | 41 ++- .../structure/KtStaticModuleProvider.kt | 4 + .../resolve/extensions/KtResolveExtension.kt | 56 ++++ .../extensions/KtResolveExtensionFile.kt | 87 ++++++ .../extensions/KtResolveExtensionProvider.kt | 37 +++ analysis/low-level-api-fir/build.gradle.kts | 1 + .../FileBasedKotlinDeclarationProvider.kt | 2 +- .../structure/sessionFactoryHelpers.kt | 15 +- .../api/fir/providers/LLFirProviderHelper.kt | 31 +- .../extensions/LLFirResolveExtensionTool.kt | 276 ++++++++++++++++++ .../level/api/fir/sessions/LLFirSession.kt | 15 +- .../structure/ktModuleInternalUtils.kt | 20 ++ 12 files changed, 571 insertions(+), 14 deletions(-) create mode 100644 analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtension.kt create mode 100644 analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtensionFile.kt create mode 100644 analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtensionProvider.kt create mode 100644 analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/extensions/LLFirResolveExtensionTool.kt create mode 100644 analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/ktModuleInternalUtils.kt diff --git a/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/components/KtAnalysisScopeProviderImpl.kt b/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/components/KtAnalysisScopeProviderImpl.kt index 78b9773d90d..24c41369e5f 100644 --- a/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/components/KtAnalysisScopeProviderImpl.kt +++ b/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/components/KtAnalysisScopeProviderImpl.kt @@ -5,12 +5,20 @@ package org.jetbrains.kotlin.analysis.api.impl.base.components +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiElement import com.intellij.psi.search.GlobalSearchScope import org.jetbrains.kotlin.analysis.api.KtAnalysisSession import org.jetbrains.kotlin.analysis.api.components.KtAnalysisScopeProvider import org.jetbrains.kotlin.analysis.api.lifetime.KtLifetimeToken +import org.jetbrains.kotlin.analysis.project.structure.KtModule +import org.jetbrains.kotlin.analysis.project.structure.KtModuleStructureInternals +import org.jetbrains.kotlin.analysis.project.structure.allDirectDependencies +import org.jetbrains.kotlin.analysis.project.structure.analysisExtensionFileContextModule import org.jetbrains.kotlin.analysis.providers.KotlinResolutionScopeProvider +import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.psiUtil.contains class KtAnalysisScopeProviderImpl( @@ -18,13 +26,40 @@ class KtAnalysisScopeProviderImpl( override val token: KtLifetimeToken ) : KtAnalysisScopeProvider() { - private val scope by lazy(LazyThreadSafetyMode.PUBLICATION) { + private val baseResolveScope by lazy(LazyThreadSafetyMode.PUBLICATION) { KotlinResolutionScopeProvider.getInstance(analysisSession.useSiteModule.project).getResolutionScope(analysisSession.useSiteModule) } - override fun getAnalysisScope(): GlobalSearchScope = scope + private val resolveScope by lazy(LazyThreadSafetyMode.PUBLICATION) { + KtAnalysisScopeProviderResolveScope(baseResolveScope, analysisSession.useSiteModule) + } + + override fun getAnalysisScope(): GlobalSearchScope = resolveScope override fun canBeAnalysed(psi: PsiElement): Boolean { - return scope.contains(psi) + return baseResolveScope.contains(psi) + || psi.isFromGeneratedModule() + } + + private fun PsiElement.isFromGeneratedModule(): Boolean { + val ktFile = containingFile as? KtFile ?: return false + return ktFile.virtualFile?.isFromGeneratedModule(analysisSession.useSiteModule) == true } } + +private class KtAnalysisScopeProviderResolveScope( + private val base: GlobalSearchScope, + private val useSiteModule: KtModule +) : GlobalSearchScope() { + override fun getProject(): Project? = base.project + override fun isSearchInModuleContent(aModule: Module): Boolean = base.isSearchInModuleContent(aModule) + override fun isSearchInLibraries(): Boolean = base.isSearchInLibraries + override fun contains(file: VirtualFile): Boolean = base.contains(file) || file.isFromGeneratedModule(useSiteModule) +} + +@OptIn(KtModuleStructureInternals::class) +private fun VirtualFile.isFromGeneratedModule(useSiteModule: KtModule): Boolean { + val analysisContextModule = analysisExtensionFileContextModule ?: return false + if (analysisContextModule == useSiteModule) return true + return analysisContextModule in useSiteModule.allDirectDependencies() +} diff --git a/analysis/analysis-api-standalone/analysis-api-standalone-base/src/org/jetbrains/kotlin/analysis/api/standalone/base/project/structure/KtStaticModuleProvider.kt b/analysis/analysis-api-standalone/analysis-api-standalone-base/src/org/jetbrains/kotlin/analysis/api/standalone/base/project/structure/KtStaticModuleProvider.kt index 79d42cbc242..6ad60476c8b 100644 --- a/analysis/analysis-api-standalone/analysis-api-standalone-base/src/org/jetbrains/kotlin/analysis/api/standalone/base/project/structure/KtStaticModuleProvider.kt +++ b/analysis/analysis-api-standalone/analysis-api-standalone-base/src/org/jetbrains/kotlin/analysis/api/standalone/base/project/structure/KtStaticModuleProvider.kt @@ -14,6 +14,8 @@ class KtStaticModuleProvider( private val builtinsModule: KtBuiltinsModule, val projectStructure: KtModuleProjectStructure, ) : ProjectStructureProvider() { + + @OptIn(KtModuleStructureInternals::class) override fun getKtModuleForKtElement(element: PsiElement): KtModule { val containingFileAsPsiFile = element.containingFile val containingFileAsVirtualFile = containingFileAsPsiFile.virtualFile @@ -21,6 +23,8 @@ class KtStaticModuleProvider( return builtinsModule } + containingFileAsVirtualFile.analysisExtensionFileContextModule?.let { return it } + return projectStructure.mainModules .first { module -> element in module.ktModule.contentScope diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtension.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtension.kt new file mode 100644 index 00000000000..3124a71be36 --- /dev/null +++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtension.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2010-2023 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.analysis.api.resolve.extensions + +import com.intellij.openapi.util.ModificationTracker +import org.jetbrains.kotlin.name.FqName + +/** + * Provides a list of Kotlin files which provides additional, generated declarations for resolution. + * + * Provided by the [KtResolveExtensionProvider]. + * + * All member implementations should: + * - consider caching the results for subsequent invocations. + * - be lightweight and not build the whole file structure inside. + * - not use the Kotlin resolve inside, as this function is called during session initialization, so Analysis API access is forbidden. + * + * @see KtResolveExtensionFile + * @see KtResolveExtensionProvider + */ +abstract class KtResolveExtension { + /** + * Get the list of files that should be generated for the module. + * + * The content of those files should remain valid until the tracker [getModificationTracker] is modified. + * + * Returned files should contain a valid Kotlin code. + * + * @see KtResolveExtensionFile + * @see KtResolveExtension + */ + abstract fun getKtFiles(): List + + /** + * Returns a [ModificationTracker], which controls the validity lifecycle of the files provided by [getKtFiles]. + * + * All files generated by [getKtFiles] should be valid if the [ModificationTracker] did not change. + * If files become invalid (e.g., the in-source declarations they were based on changed) the [ModificationTracker] should be incremented. + * + * @see KtResolveExtension + */ + abstract fun getModificationTracker(): ModificationTracker + + /** + * Returns the set of packages that are contained in the files provided by [getKtFiles]. + * + * The returned package set should be a strict set of all file packages, + * so `for-all pckg: pckg in getContainedPackages() <=> exists file: file in getKtFiles() && file.getFilePackageName() == pckg` + * + * @see KtResolveExtension + */ + abstract fun getContainedPackages(): Set +} \ No newline at end of file diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtensionFile.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtensionFile.kt new file mode 100644 index 00000000000..4b037fc5536 --- /dev/null +++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtensionFile.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2010-2023 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.analysis.api.resolve.extensions + +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +/** + * Represents the Kotlin file which provides additional, generated declarations for resolution. + * + * All member implementations should: + * - consider caching the results for subsequent invocations. + * - be lightweight and not build the whole file structure inside. + * - not use the Kotlin resolve inside, as this function is called during session initialization, so Analysis API access is forbidden. + * + * @see KtResolveExtension + */ +abstract class KtResolveExtensionFile { + /** + * The name a Kotlin file which will be generated. + * + * Should have the `.kt` extension. + * + * It will be used as a Java facade name, e.g., for the file name `myFile.kt`, the `MyFileKt` facade is generated if the file contains some properties or functions. + * + * @see KtResolveExtensionFile + */ + abstract fun getFileName(): String + + /** + * [FqName] of the package specified in the file + * + * The operation might be called regularly, so the [getFilePackageName] should work fast and avoid building the whole file text. + * + * It should be equal to the package name specified in the [buildFileText]. + * + * @see KtResolveExtensionFile + */ + abstract fun getFilePackageName(): FqName + + /** + * Returns the set of top-level classifier (classes, interfaces, objects, and type-aliases) names in the file. + * + * The result may have false-positive entries but cannot have false-negative entries. It should contain all the names in the package but may have some additional names that are not there. + * + * @see KtResolveExtensionFile + */ + abstract fun getTopLevelClassifierNames(): Set + + /** + * Returns the set of top-level callable (functions and properties) names in the file. + * + * The result may have false-positive entries but cannot have false-negative entries. It should contain all the names in the package but may have some additional names that are not there. + * + * @see KtResolveExtensionFile + */ + abstract fun getTopLevelCallableNames(): Set + + /** + * Creates the generated Kotlin source file text. + * + * The resulted String should be a valid Kotlin code. + * It should be consistent with other declarations which are present in the [KtResolveExtensionFile], more specifically: + * 1. [getFilePackageName] should be equal to the file's package name. + * 2. All classifier names should be contained in the [getTopLevelClassifierNames]. + * 3. All callable names should be contained in the [getTopLevelCallableNames]. + * + * Additional restrictions on the file text: + * 1. The File should not contain the `kotlin.jvm.JvmMultifileClass` and `kotlin.jvm.JvmName` annotations on the file level. + * 2. All declaration types should be specified explicitly. + * + * @see KtResolveExtensionFile + */ + abstract fun buildFileText(): String + + /** + * Creates a [KtResolveExtensionReferenceTargetsPsiProvider] for this [KtResolveExtensionFile]. + * + * @see KtResolveExtensionReferenceTargetsPsiProvider + * @see KtResolveExtensionFile + */ + abstract fun createTargetPsiProvider(): KtResolveExtensionReferenceTargetsPsiProvider + +} \ No newline at end of file diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtensionProvider.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtensionProvider.kt new file mode 100644 index 00000000000..7205292dc65 --- /dev/null +++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/resolve/extensions/KtResolveExtensionProvider.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2023 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.analysis.api.resolve.extensions + +import com.intellij.openapi.extensions.ExtensionPointName +import org.jetbrains.kotlin.analysis.project.structure.KtModule + +/** + * Allows extending Kotlin resolution by generating additional declarations. + * + * Those declarations will be analyzed the same way as they were just regular source files inside the project. + * + * All member implementations should consider caching the results for subsequent invocations. + */ +abstract class KtResolveExtensionProvider { + /** + * Provides a list of [KtResolveExtension]s for a given [KtModule]. + * + * Should not perform any heavy analysis and the generation of the actual files. All file generation should be performed only in [KtResolveExtensionFile.buildFileText]. + * + * Implementations should consider caching the results, so the subsequent invocations should be performed instantly. + * + * Implementation cannot use the Kotlin resolve inside, as this function is called during session initialization, so Analysis API access is forbidden. + */ + abstract fun provideExtensionsFor(module: KtModule): List + + companion object { + val EP_NAME = ExtensionPointName("org.jetbrains.kotlin.ktResolveExtensionProvider") + + fun provideExtensionsFor(module: KtModule): List { + return EP_NAME.getExtensionList(module.project).flatMap { it.provideExtensionsFor(module) } + } + } +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/build.gradle.kts b/analysis/low-level-api-fir/build.gradle.kts index 23beaf66664..e941c85cadd 100644 --- a/analysis/low-level-api-fir/build.gradle.kts +++ b/analysis/low-level-api-fir/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(project(":compiler:frontend.common")) implementation(project(":compiler:fir:entrypoint")) implementation(project(":analysis:analysis-api-providers")) + implementation(project(":analysis:analysis-api")) implementation(project(":analysis:analysis-internal-utils")) // We cannot use the latest version `3.1.5` because it doesn't support Java 8. diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/FileBasedKotlinDeclarationProvider.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/FileBasedKotlinDeclarationProvider.kt index 4a86a951284..240dc1bdfd8 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/FileBasedKotlinDeclarationProvider.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/FileBasedKotlinDeclarationProvider.kt @@ -15,7 +15,7 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.utils.addIfNotNull import org.jetbrains.kotlin.utils.yieldIfNotNull -internal class FileBasedKotlinDeclarationProvider(private val kotlinFile: KtFile) : KotlinDeclarationProvider() { +internal class FileBasedKotlinDeclarationProvider(val kotlinFile: KtFile) : KotlinDeclarationProvider() { private val topLevelDeclarations: Sequence get() { return sequence { diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/sessionFactoryHelpers.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/sessionFactoryHelpers.kt index fab1ceb8524..58b20561a3d 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/sessionFactoryHelpers.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/project/structure/sessionFactoryHelpers.kt @@ -7,11 +7,14 @@ package org.jetbrains.kotlin.analysis.low.level.api.fir.project.structure import com.intellij.openapi.project.Project import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionProvider import org.jetbrains.kotlin.analysis.low.level.api.fir.IdeSessionComponents import org.jetbrains.kotlin.analysis.low.level.api.fir.api.services.createSealedInheritorsProvider import org.jetbrains.kotlin.analysis.low.level.api.fir.fir.caches.FirThreadSafeCachesFactory import org.jetbrains.kotlin.analysis.low.level.api.fir.providers.LLFirIdePredicateBasedProvider import org.jetbrains.kotlin.analysis.low.level.api.fir.providers.LLFirIdeRegisteredPluginAnnotations +import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.extensions.LLFirNonEmptyResolveExtensionTool +import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.extensions.LLFirResolveExtensionTool import org.jetbrains.kotlin.analysis.low.level.api.fir.sessions.LLFirSession import org.jetbrains.kotlin.analysis.low.level.api.fir.sessions.LLFirSourcesSession import org.jetbrains.kotlin.analysis.low.level.api.fir.util.LLFirExceptionHandler @@ -42,8 +45,18 @@ internal fun LLFirSession.registerIdeComponents(project: Project) { register(FirCachesFactory::class, FirThreadSafeCachesFactory) register(SealedClassInheritorsProvider::class, project.createSealedInheritorsProvider()) register(FirExceptionHandler::class, LLFirExceptionHandler) + createResolveExtensionTool()?.let { + register(LLFirResolveExtensionTool::class, it) + } } +private fun LLFirSession.createResolveExtensionTool(): LLFirResolveExtensionTool? { + val extensions = KtResolveExtensionProvider.provideExtensionsFor(ktModule) + if (extensions.isEmpty()) return null + return LLFirNonEmptyResolveExtensionTool(this, extensions) +} + + internal inline fun createCompositeSymbolProvider( session: FirSession, createSubProviders: MutableList.() -> Unit @@ -52,7 +65,7 @@ internal inline fun createCompositeSymbolProvider( @SessionConfiguration internal fun FirSession.registerCompilerPluginExtensions(project: Project, module: KtSourceModule) { - val extensionProvider = project.getService(KtCompilerPluginsProvider::class.java) ?: return + val extensionProvider = project.getService(KtCompilerPluginsProvider::class.java) ?: return FirSessionConfigurator(this).apply { val registrars = FirExtensionRegistrarAdapter.getInstances(project) + extensionProvider.getRegisteredExtensions(module, FirExtensionRegistrarAdapter) diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirProviderHelper.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirProviderHelper.kt index 0aa64ae5ced..f950c13b4b1 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirProviderHelper.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirProviderHelper.kt @@ -6,7 +6,12 @@ package org.jetbrains.kotlin.analysis.low.level.api.fir.providers import org.jetbrains.kotlin.analysis.low.level.api.fir.file.builder.LLFirFileBuilder +import org.jetbrains.kotlin.analysis.low.level.api.fir.project.structure.CompositeKotlinDeclarationProvider +import org.jetbrains.kotlin.analysis.low.level.api.fir.project.structure.CompositeKotlinPackageProvider +import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.extensions.LLFirResolveExtensionTool +import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.extensions.llResolveExtensionTool import org.jetbrains.kotlin.analysis.low.level.api.fir.util.FirElementFinder +import org.jetbrains.kotlin.analysis.low.level.api.fir.util.LLFirCompositeSymbolProviderNameCache import org.jetbrains.kotlin.analysis.low.level.api.fir.util.LLFirKotlinSymbolProviderNameCache import org.jetbrains.kotlin.analysis.providers.KotlinDeclarationProvider import org.jetbrains.kotlin.analysis.providers.KotlinPackageProvider @@ -32,10 +37,25 @@ import org.jetbrains.kotlin.psi.KtFile internal class LLFirProviderHelper( firSession: FirSession, private val firFileBuilder: LLFirFileBuilder, - private val declarationProvider: KotlinDeclarationProvider, - private val packageProvider: KotlinPackageProvider, + mainDeclarationProvider: KotlinDeclarationProvider, + mainPackageProvider: KotlinPackageProvider, canContainKotlinPackage: Boolean, ) { + private val extensionTool: LLFirResolveExtensionTool? = firSession.llResolveExtensionTool + + private val declarationProvider = CompositeKotlinDeclarationProvider.create( + listOfNotNull( + mainDeclarationProvider, + extensionTool?.declarationProvider, + ) + ) + + private val packageProvider = CompositeKotlinPackageProvider.create( + listOfNotNull( + mainPackageProvider, + extensionTool?.packageProvider, + ) + ) private val allowKotlinPackage = canContainKotlinPackage || firSession.languageVersionSettings.getFlag(AnalysisFlags.allowKotlinPackage) @@ -60,7 +80,12 @@ internal class LLFirProviderHelper( } } - val symbolNameCache = LLFirKotlinSymbolProviderNameCache(firSession, declarationProvider) + val symbolNameCache = LLFirCompositeSymbolProviderNameCache.create( + listOfNotNull( + LLFirKotlinSymbolProviderNameCache(firSession, declarationProvider), + extensionTool?.symbolNameCache, + ) + ) fun getFirClassifierByFqNameAndDeclaration( classId: ClassId, diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/extensions/LLFirResolveExtensionTool.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/extensions/LLFirResolveExtensionTool.kt new file mode 100644 index 00000000000..6d8da2cae36 --- /dev/null +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/extensions/LLFirResolveExtensionTool.kt @@ -0,0 +1,276 @@ +/* + * Copyright 2010-2023 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.analysis.low.level.api.fir.resolve.extensions + +import com.intellij.openapi.util.ModificationTracker +import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtension +import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionFile +import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionReferenceTargetsPsiProvider +import org.jetbrains.kotlin.analysis.low.level.api.fir.project.structure.FileBasedKotlinDeclarationProvider +import org.jetbrains.kotlin.analysis.low.level.api.fir.sessions.LLFirSession +import org.jetbrains.kotlin.analysis.low.level.api.fir.util.LLFirSymbolProviderNameCache +import org.jetbrains.kotlin.analysis.project.structure.KtModule +import org.jetbrains.kotlin.analysis.project.structure.KtModuleStructureInternals +import org.jetbrains.kotlin.analysis.project.structure.analysisExtensionFileContextModule +import org.jetbrains.kotlin.analysis.providers.KotlinDeclarationProvider +import org.jetbrains.kotlin.analysis.providers.KotlinPackageProvider +import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.FirSessionComponent +import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.psi.* +import java.util.concurrent.ConcurrentHashMap + +/** + * Encapsulate all the work with the [KtResolveExtension] for the LL API. + * + * Caches generated [KtResolveExtensionFile]s, creates [KotlinDeclarationProvider], [KotlinPackageProvider], [LLFirSymbolProviderNameCache] needed for the [org.jetbrains.kotlin.fir.resolve.providers.FirSymbolProvider]. + */ +internal abstract class LLFirResolveExtensionTool : FirSessionComponent { + abstract val modificationTrackers: List + abstract val declarationProvider: KotlinDeclarationProvider + abstract val packageProvider: KotlinPackageProvider + abstract val symbolNameCache: LLFirSymbolProviderNameCache +} + +internal val FirSession.llResolveExtensionTool: LLFirResolveExtensionTool? by FirSession.nullableSessionComponentAccessor() + +internal class LLFirNonEmptyResolveExtensionTool( + session: LLFirSession, + extensions: List, +) : LLFirResolveExtensionTool() { + init { + require(extensions.isNotEmpty()) + } + + private val fileProvider = LLFirResolveExtensionsFileProvider(extensions) + + private val packageFilter = LLFirResolveExtensionToolPackageFilter(extensions) + + override val modificationTrackers by lazy { extensions.map { it.getModificationTracker() } } + + override val declarationProvider: KotlinDeclarationProvider = + LLFirResolveExtensionToolDeclarationProvider(fileProvider, session.ktModule) + + override val packageProvider: KotlinPackageProvider = LLFirResolveExtensionToolPackageProvider(packageFilter) + + override val symbolNameCache: LLFirSymbolProviderNameCache = LLFirResolveExtensionToolNameCache(packageFilter, fileProvider) +} + +private class LLFirResolveExtensionToolNameCache( + private val packageFilter: LLFirResolveExtensionToolPackageFilter, + private val fileProvider: LLFirResolveExtensionsFileProvider, +) : LLFirSymbolProviderNameCache() { + override fun getTopLevelClassifierNamesInPackage(packageFqName: FqName): Set? { + if (!packageFilter.packageExists(packageFqName)) return emptySet() + return fileProvider.getFilesByPackage(packageFqName) + .flatMap { it.getTopLevelClassifierNames() } + .mapTo(mutableSetOf()) { it.asString() } + } + + override fun getTopLevelCallableNamesInPackage(packageFqName: FqName): Set { + if (!packageFilter.packageExists(packageFqName)) return emptySet() + return fileProvider.getFilesByPackage(packageFqName) + .flatMapTo(mutableSetOf()) { it.getTopLevelCallableNames() } + } + + override fun mayHaveTopLevelClassifier(classId: ClassId, mayHaveFunctionClass: Boolean): Boolean { + if (!packageFilter.packageExists(classId.packageFqName)) return false + + return fileProvider.getFilesByPackage(classId.packageFqName) + .any { it.mayHaveTopLevelClassifier(classId.getTopLevelShortClassName()) } + } + + override fun mayHaveTopLevelCallable(packageFqName: FqName, name: Name): Boolean { + if (!packageFilter.packageExists(packageFqName)) return false + + return fileProvider.getFilesByPackage(packageFqName) + .any { it.mayHaveTopLevelCallable(name) } + } +} + +private class LLFirResolveExtensionToolPackageFilter( + private val extensions: List +) { + val allPackages: Set by lazy { + extensions.flatMapTo(mutableSetOf()) { it.getContainedPackages() } + } + + fun packageExists(packageFqName: FqName): Boolean { + return packageFqName in allPackages + } +} + +private class LLFirResolveExtensionToolDeclarationProvider( + private val extensionProvider: LLFirResolveExtensionsFileProvider, + private val ktModule: KtModule, +) : KotlinDeclarationProvider() { + + private val extensionFileToDeclarationProvider: ConcurrentHashMap = + ConcurrentHashMap() + + override fun getClassLikeDeclarationByClassId(classId: ClassId): KtClassLikeDeclaration? { + return getDeclarationProvidersByPackage(classId.packageFqName) { it.mayHaveTopLevelClassifier(classId.getTopLevelShortClassName()) } + .firstNotNullOfOrNull { it.getClassLikeDeclarationByClassId(classId) } + } + + override fun getAllClassesByClassId(classId: ClassId): Collection { + return getDeclarationProvidersByPackage(classId.packageFqName) { it.mayHaveTopLevelClassifier(classId.getTopLevelShortClassName()) } + .flatMapTo(mutableListOf()) { it.getAllClassesByClassId(classId) } + } + + override fun getAllTypeAliasesByClassId(classId: ClassId): Collection { + return getDeclarationProvidersByPackage(classId.packageFqName) { it.mayHaveTopLevelClassifier(classId.getTopLevelShortClassName()) } + .flatMapTo(mutableListOf()) { it.getAllTypeAliasesByClassId(classId) } + } + + override fun getTopLevelKotlinClassLikeDeclarationNamesInPackage(packageFqName: FqName): Set { + return getDeclarationProvidersByPackage(packageFqName) { true } + .flatMapTo(mutableSetOf()) { it.getTopLevelKotlinClassLikeDeclarationNamesInPackage(packageFqName) } + } + + override fun getTopLevelProperties(callableId: CallableId): Collection { + return getDeclarationProvidersByPackage(callableId.packageName) { it.mayHaveTopLevelCallable(callableId.callableName) } + .flatMapTo(mutableListOf()) { it.getTopLevelProperties(callableId) } + } + + override fun getTopLevelFunctions(callableId: CallableId): Collection { + return getDeclarationProvidersByPackage(callableId.packageName) { it.mayHaveTopLevelCallable(callableId.callableName) } + .flatMapTo(mutableListOf()) { it.getTopLevelFunctions(callableId) } + } + + override fun getTopLevelCallableFiles(callableId: CallableId): Collection { + return getDeclarationProvidersByPackage(callableId.packageName) { it.mayHaveTopLevelCallable(callableId.callableName) } + .mapTo(mutableListOf()) { it.kotlinFile } + } + + override fun getTopLevelCallableNamesInPackage(packageFqName: FqName): Set { + return extensionProvider.getFilesByPackage(packageFqName).flatMapTo(mutableSetOf()) { it.getTopLevelClassifierNames() } + } + + override fun findFilesForFacadeByPackage(packageFqName: FqName): Collection { + return getDeclarationProvidersByPackage(packageFqName) { file -> + file.getTopLevelCallableNames().isNotEmpty() + }.mapTo(mutableListOf()) { it.kotlinFile } + } + + override fun findFilesForFacade(facadeFqName: FqName): Collection { + if (facadeFqName.isRoot) return emptyList() + val packageFqName = facadeFqName.parent() + return getDeclarationProvidersByPackage(packageFqName) { file -> + facadeFqName.shortName().asString() == PackagePartClassUtils.getFilePartShortName(file.getFileName()) + } + .mapTo(mutableListOf()) { it.kotlinFile } + } + + override fun findInternalFilesForFacade(facadeFqName: FqName): Collection { + // no decompiled files here (see the `org.jetbrains.kotlin.analysis.providers.KotlinDeclarationProvider.findInternalFilesForFacade` KDoc) + return emptyList() + } + + private inline fun getDeclarationProvidersByPackage( + packageFqName: FqName, + crossinline filter: (KtResolveExtensionFile) -> Boolean + ): Sequence { + return extensionProvider.getFilesByPackage(packageFqName) + .filter { filter(it) } + .map { createDeclarationProviderByFile(it) } + } + + private fun createDeclarationProviderByFile(file: KtResolveExtensionFile): FileBasedKotlinDeclarationProvider { + return extensionFileToDeclarationProvider.getOrPut(file) { + val factory = KtPsiFactory( + ktModule.project, + markGenerated = true, + eventSystemEnabled = true // so every generated KtFile backed by some VirtualFile + ) + val text = file.buildFileText() + val ktFile = createKtFile(factory, file.getFileName(), text) + FileBasedKotlinDeclarationProvider(ktFile) + } + } + + + @OptIn(KtModuleStructureInternals::class) + private fun createKtFile(factory: KtPsiFactory, fileName: String, fileText: String): KtFile { + val ktFile = factory.createFile(fileName, fileText) + ktFile.virtualFile.analysisExtensionFileContextModule = ktModule + return ktFile + } +} + +private class LLFirResolveExtensionsFileProvider( + val extensions: List, +) { + fun getFilesByPackage(packageFqName: FqName): Sequence { + return extensions + .asSequence() + .filter { packageFqName in it.getContainedPackages() } + .flatMap { it.getKtFiles() } + .filter { it.getFilePackageName() == packageFqName } + } +} + +private class LLFirResolveExtensionToolPackageProvider( + private val packageFilter: LLFirResolveExtensionToolPackageFilter, +) : KotlinPackageProvider() { + + private val packageSubPackages: Map> by lazy { + createSubPackagesMapping(packageFilter.allPackages) + } + + override fun doesPackageExist(packageFqName: FqName, platform: TargetPlatform): Boolean = + doesKotlinOnlyPackageExist(packageFqName) + + override fun getSubPackageFqNames(packageFqName: FqName, platform: TargetPlatform, nameFilter: (Name) -> Boolean): Set = + getKotlinOnlySubPackagesFqNames(packageFqName, nameFilter) + + override fun doesPlatformSpecificPackageExist(packageFqName: FqName, platform: TargetPlatform): Boolean = false + + override fun getPlatformSpecificSubPackagesFqNames(packageFqName: FqName, platform: TargetPlatform, nameFilter: (Name) -> Boolean) = + emptySet() + + override fun doesKotlinOnlyPackageExist(packageFqName: FqName): Boolean = + packageFilter.packageExists(packageFqName) + + override fun getKotlinOnlySubPackagesFqNames(packageFqName: FqName, nameFilter: (Name) -> Boolean): Set { + val subPackageNames = packageSubPackages[packageFqName] ?: return emptySet() + if (subPackageNames.isEmpty()) return emptySet() + return subPackageNames.filterTo(mutableSetOf()) { nameFilter(it) } + } + + private fun createSubPackagesMapping(packages: Set): Map> { + return buildMap> { + for (packageName in packages) { + collectAllSubPackages(packageName) + } + } + } + + private fun MutableMap>.collectAllSubPackages(packageName: FqName) { + var currentPackage = FqName.ROOT + for (packagePart in packageName.pathSegments()) { + getOrPut(currentPackage) { mutableSetOf() }.add(packagePart) + currentPackage = currentPackage.child(packagePart) + } + } +} + +private fun ClassId.getTopLevelShortClassName(): Name { + return Name.guessByFirstCharacter(relativeClassName.asString().substringBefore(".")) +} + +private fun KtResolveExtensionFile.mayHaveTopLevelClassifier(name: Name): Boolean { + return name in getTopLevelClassifierNames() +} + +private fun KtResolveExtensionFile.mayHaveTopLevelCallable(name: Name): Boolean { + return name in getTopLevelCallableNames() +} diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSession.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSession.kt index cf50d971475..5472ba2b1b7 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSession.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSession.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.ModificationTracker import com.intellij.psi.PsiFile import com.intellij.psi.SmartPointerManager +import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.extensions.llResolveExtensionTool import org.jetbrains.kotlin.analysis.project.structure.* import org.jetbrains.kotlin.analysis.providers.KotlinModificationTrackerFactory import org.jetbrains.kotlin.analysis.providers.KtModuleStateTracker @@ -19,6 +20,7 @@ import org.jetbrains.kotlin.fir.FirSession import org.jetbrains.kotlin.fir.PrivateSessionConstructor import org.jetbrains.kotlin.fir.resolve.ScopeSession import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol +import org.jetbrains.kotlin.utils.addIfNotNull import java.util.concurrent.atomic.AtomicBoolean @OptIn(PrivateSessionConstructor::class) @@ -51,12 +53,13 @@ abstract class LLFirSession( } modificationTracker = CompositeModificationTracker.createFlattened( - listOfNotNull( - ExplicitInvalidationTracker(ktModule, isExplicitlyInvalidated), - ModuleStateModificationTracker(ktModule, validityTracker), - outOfBlockTracker, - dependencyTracker - ) + buildList { + add(ExplicitInvalidationTracker(ktModule, isExplicitlyInvalidated)) + add(ModuleStateModificationTracker(ktModule, validityTracker)) + addIfNotNull(outOfBlockTracker) + add(dependencyTracker) + llResolveExtensionTool?.modificationTrackers?.let(::addAll) + } ) initialModificationCount = modificationTracker.modificationCount diff --git a/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/ktModuleInternalUtils.kt b/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/ktModuleInternalUtils.kt new file mode 100644 index 00000000000..f2b909cd99d --- /dev/null +++ b/analysis/project-structure/src/org/jetbrains/kotlin/analysis/project/structure/ktModuleInternalUtils.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2010-2023 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.analysis.project.structure + +import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.psi.UserDataProperty + +/** + * Used by the [org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionProvider] implementations + * to store the references on the [KtModule] for which the [VirtualFile] was generated. + */ +@KtModuleStructureInternals +public var VirtualFile.analysisExtensionFileContextModule: KtModule? by UserDataProperty(Key.create("ANALYSIS_CONTEXT_MODULE")) + +@RequiresOptIn("Internal KtModule structure API component which should not be used outside the Analysis API implementation modules as it does not have any compatibility guarantees") +public annotation class KtModuleStructureInternals