mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
LSP-272 Support Kotlin compiler plugins in LSP
GitOrigin-RevId: 82f7d4342816f1d9ab5e7ac4573038c91f012842
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f2c2288fcf
commit
c0e373e852
+304
@@ -0,0 +1,304 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:Suppress("IO_FILE_USAGE")
|
||||
|
||||
package org.jetbrains.kotlin.idea.fir.extensions
|
||||
|
||||
import com.intellij.openapi.components.PathMacroManager
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.diagnostic.runAndLogException
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleManager
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.roots.CompilerModuleExtension
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.utils.Path
|
||||
import com.intellij.util.concurrency.SynchronizedClearableLazy
|
||||
import com.intellij.util.containers.ContainerUtil
|
||||
import com.intellij.util.containers.orNull
|
||||
import com.intellij.util.lang.UrlClassLoader
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
|
||||
import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinCompilerPluginsProvider
|
||||
import org.jetbrains.kotlin.analysis.api.platform.projectStructure.areCompilerPluginsSupported
|
||||
import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
|
||||
import org.jetbrains.kotlin.analysis.api.projectStructure.KaScriptModule
|
||||
import org.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule
|
||||
import org.jetbrains.kotlin.analysis.low.level.api.fir.LLFirInternals
|
||||
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
|
||||
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
|
||||
import org.jetbrains.kotlin.cli.plugins.processCompilerPluginsOptions
|
||||
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
|
||||
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
|
||||
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.config.JVMConfigurationKeys
|
||||
import org.jetbrains.kotlin.config.JvmTarget
|
||||
import org.jetbrains.kotlin.extensions.ProjectExtensionDescriptor
|
||||
import org.jetbrains.kotlin.fir.extensions.FirAssignExpressionAltererExtension
|
||||
import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar
|
||||
import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.KaSourceModuleKind
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.openapiModule
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.sourceModuleKind
|
||||
import org.jetbrains.kotlin.idea.compiler.configuration.KotlinCommonCompilerArgumentsHolder
|
||||
import org.jetbrains.kotlin.idea.facet.KotlinFacet
|
||||
import org.jetbrains.kotlin.idea.util.getOriginalOrDelegateFileOrSelf
|
||||
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.makeScriptCompilerArguments
|
||||
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
|
||||
import org.jetbrains.kotlin.util.ServiceLoaderLite
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
import kotlin.script.experimental.api.ScriptCompilationConfiguration
|
||||
import kotlin.script.experimental.api.compilerOptions
|
||||
|
||||
@OptIn(ExperimentalCompilerApi::class)
|
||||
@ApiStatus.Internal
|
||||
class KtCompilerPluginsCache private constructor(
|
||||
private val project: Project,
|
||||
private val pluginsClassLoader: UrlClassLoader,
|
||||
private val registrarForSourceModule: ConcurrentMap<KaSourceModule, Optional<CompilerPluginRegistrar.ExtensionStorage>>,
|
||||
/**
|
||||
* As scripts might be associated with injections,
|
||||
* it's better to have a more stable anchor such as a top-level file.
|
||||
*/
|
||||
private val registrarForScriptModule: SynchronizedClearableLazy<ConcurrentMap<VirtualFile, Optional<CompilerPluginRegistrar.ExtensionStorage>>>,
|
||||
private val onlyBundledPluginsEnabled: Boolean
|
||||
) {
|
||||
|
||||
fun isPluginOfTypeRegistered(
|
||||
module: KaModule,
|
||||
pluginType: KotlinCompilerPluginsProvider.CompilerPluginType,
|
||||
): Boolean {
|
||||
@Suppress("REDUNDANT_ELSE_IN_WHEN")
|
||||
val extension = when (pluginType) {
|
||||
KotlinCompilerPluginsProvider.CompilerPluginType.ASSIGNMENT -> FirAssignExpressionAltererExtension::class
|
||||
else -> return false
|
||||
}
|
||||
return getRegisteredExtensions(
|
||||
module = module,
|
||||
extensionType = FirExtensionRegistrarAdapter.Companion
|
||||
)
|
||||
.map { (it as FirExtensionRegistrar).configure() }
|
||||
.any { it.extensions[extension]?.isNotEmpty() == true }
|
||||
}
|
||||
|
||||
@OptIn(KaExperimentalApi::class)
|
||||
fun <T : Any> getRegisteredExtensions(
|
||||
module: KaModule,
|
||||
extensionType: ProjectExtensionDescriptor<T>
|
||||
): List<T> {
|
||||
if (!module.areCompilerPluginsSupported()) return emptyList()
|
||||
val classLoader = pluginsClassLoader
|
||||
return when (module) {
|
||||
is KaSourceModule -> {
|
||||
val registrarForModule = registrarForSourceModule
|
||||
module.getExtensionsForModule(classLoader, registrarForModule, module, extensionType)
|
||||
}
|
||||
|
||||
is KaScriptModule -> {
|
||||
val registrarForModule = registrarForScriptModule.value
|
||||
val cacheKey = module.file.virtualFile.getOriginalOrDelegateFileOrSelf()
|
||||
|
||||
module.getExtensionsForModule(classLoader, registrarForModule, cacheKey, extensionType)
|
||||
}
|
||||
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(KaExperimentalApi::class)
|
||||
private fun <T : Any, K : Any> KaModule.getExtensionsForModule(
|
||||
classLoader: ClassLoader,
|
||||
registrarForModule: ConcurrentMap<K, Optional<CompilerPluginRegistrar.ExtensionStorage>>,
|
||||
cacheKey: K,
|
||||
extensionType: ProjectExtensionDescriptor<T>
|
||||
): List<T> {
|
||||
val extensionStorage = registrarForModule.computeIfAbsent(cacheKey) {
|
||||
Optional.ofNullable(computeExtensionStorage(classLoader, this))
|
||||
}.orNull() ?: return emptyList()
|
||||
val registrars = extensionStorage.registeredExtensions[extensionType] ?: return emptyList()
|
||||
@Suppress("UNCHECKED_CAST") return registrars as List<T>
|
||||
}
|
||||
|
||||
@OptIn(KaExperimentalApi::class, LLFirInternals::class)
|
||||
private fun computeExtensionStorage(
|
||||
classLoader: ClassLoader,
|
||||
module: KaModule
|
||||
): CompilerPluginRegistrar.ExtensionStorage? {
|
||||
val compilerArguments = when (module) {
|
||||
is KaSourceModule -> module.openapiModule.getCompilerArguments()
|
||||
is KaScriptModule -> {
|
||||
val scriptDefinition = module.file.findScriptDefinition() ?: return null
|
||||
val scriptConfiguration = scriptDefinition.compilationConfiguration
|
||||
|
||||
val providedOptions = scriptConfiguration[ScriptCompilationConfiguration.compilerOptions] ?: return null
|
||||
makeScriptCompilerArguments(providedOptions)
|
||||
}
|
||||
|
||||
else -> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
val pluginClasspaths = collectSubstitutedPluginClasspaths(project, onlyBundledPluginsEnabled, listOf(compilerArguments)).map { it.toFile() }
|
||||
if (pluginClasspaths.isEmpty()) return null
|
||||
|
||||
val logger = logger<KtCompilerPluginsProviderIdeImpl>()
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val pluginRegistrars =
|
||||
logger.runAndLogException { ServiceLoaderLite.loadImplementations<CompilerPluginRegistrar>(pluginClasspaths, classLoader) }
|
||||
?.takeIf { it.isNotEmpty() } ?: return null
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val commandLineProcessors = logger.runAndLogException {
|
||||
ServiceLoaderLite.loadImplementations<CommandLineProcessor>(pluginClasspaths, classLoader)
|
||||
} ?: return null
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val compilerConfiguration =
|
||||
CompilerConfiguration().apply { // Temporary work-around for KTIJ-24320. Calls to 'setupCommonArguments()' and 'setupJvmSpecificArguments()'
|
||||
// (or even a platform-agnostic alternative) should be added.
|
||||
if (compilerArguments is K2JVMCompilerArguments && module is KaSourceModule) {
|
||||
val compilerExtension = CompilerModuleExtension.getInstance(module.openapiModule)
|
||||
val outputUrl = when (module.sourceModuleKind) {
|
||||
KaSourceModuleKind.TEST -> compilerExtension?.compilerOutputUrlForTests
|
||||
KaSourceModuleKind.PRODUCTION -> compilerExtension?.compilerOutputUrl
|
||||
}
|
||||
|
||||
putIfNotNull(JVMConfigurationKeys.JVM_TARGET, compilerArguments.jvmTarget?.let(JvmTarget::fromString))
|
||||
putIfNotNull(JVMConfigurationKeys.OUTPUT_DIRECTORY, outputUrl?.let { File(it) })
|
||||
}
|
||||
|
||||
processCompilerPluginsOptions(this, compilerArguments.pluginOptions?.toList(), commandLineProcessors)
|
||||
}
|
||||
|
||||
val storage = CompilerPluginRegistrar.ExtensionStorage()
|
||||
for (pluginRegistrar in pluginRegistrars) {
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
with(pluginRegistrar) {
|
||||
try {
|
||||
val configuration = KotlinFirCompilerPluginConfigurationForIdeProvider.getCompilerConfigurationWithCustomOptions(
|
||||
pluginRegistrar, compilerConfiguration
|
||||
) ?: compilerConfiguration
|
||||
storage.registerExtensions(configuration)
|
||||
} catch (e: ProcessCanceledException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
LOG.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws away only part of the cache related to the scripts, leaving other storage intact.
|
||||
*/
|
||||
fun resetScriptCache() {
|
||||
val scriptsCache = this.registrarForScriptModule.drop() ?: return
|
||||
|
||||
scriptsCache.values.mapNotNull { it.orNull() }.disposeAll()
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
registrarForSourceModule.values.mapNotNull { it.orNull() }.disposeAll()
|
||||
resetScriptCache()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val LOG = logger<KtCompilerPluginsCache>()
|
||||
|
||||
fun new(project: Project, onlyBundledPluginsEnabled: Boolean): KtCompilerPluginsCache {
|
||||
val pluginsClassLoader: UrlClassLoader = UrlClassLoader.build().apply {
|
||||
parent(KaModule::class.java.classLoader)
|
||||
val compilerArguments = ModuleManager.getInstance(project).modules.map { it.getCompilerArguments() }
|
||||
val pluginClasspaths = collectSubstitutedPluginClasspaths(project, onlyBundledPluginsEnabled, compilerArguments)
|
||||
files(pluginClasspaths)
|
||||
}.get()
|
||||
return KtCompilerPluginsCache(
|
||||
pluginsClassLoader = pluginsClassLoader,
|
||||
registrarForSourceModule = ContainerUtil.createConcurrentWeakMap(),
|
||||
registrarForScriptModule = SynchronizedClearableLazy { ContainerUtil.createConcurrentWeakMap() },
|
||||
onlyBundledPluginsEnabled = onlyBundledPluginsEnabled,
|
||||
project = project
|
||||
)
|
||||
}
|
||||
|
||||
private fun Collection<CompilerPluginRegistrar.ExtensionStorage>.disposeAll() {
|
||||
for (storage in this) {
|
||||
for (disposable in storage.disposables) {
|
||||
LOG.runAndLogException {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the paths defined in [org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments.pluginClasspaths]
|
||||
* in the absolute form with the expansion of the present path macros
|
||||
* (like 'KOTLIN_BUNDLED').
|
||||
*/
|
||||
private fun CommonCompilerArguments.getOriginalPluginClasspaths(project: Project): List<Path> {
|
||||
val pluginClassPaths = this.pluginClasspaths
|
||||
|
||||
if (pluginClassPaths.isNullOrEmpty()) return emptyList()
|
||||
|
||||
val pathMacroManager = PathMacroManager.getInstance(project)
|
||||
val expandedPluginClassPaths = pluginClassPaths.map { pathMacroManager.expandPath(it) }
|
||||
val eel = project.getEelDescriptor()
|
||||
return expandedPluginClassPaths.map { Path(it, eel).toAbsolutePath() }
|
||||
}
|
||||
|
||||
/**
|
||||
* We have the following logic for plugins' substitution:
|
||||
* 1. Always replace our own plugins (like "allopen", "noarg", etc.) with bundled ones to avoid binary incompatibility.
|
||||
* 2. Allow using other compiler plugins only if [onlyBundledPluginsEnabled] is set to false; otherwise, filter them.
|
||||
*/
|
||||
private fun substitutePluginJar(project: Project, onlyBundledPluginsEnabled: Boolean, userSuppliedPluginJar: Path): Path? {
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val bundledPlugin = KotlinBundledFirCompilerPluginProvider.provideBundledPluginJar(project, userSuppliedPluginJar)
|
||||
if (bundledPlugin != null) return bundledPlugin
|
||||
|
||||
return userSuppliedPluginJar.takeUnless { onlyBundledPluginsEnabled }
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the substituted plugin classpaths for the given [compilerArguments] list.
|
||||
*
|
||||
* We process the whole [compilerArguments] list at once, because it gives us opportunity
|
||||
* to not call [substitutePluginJar] multiple times if [compilerArguments] use the same
|
||||
* compiler plugins jars.
|
||||
*/
|
||||
private fun collectSubstitutedPluginClasspaths(
|
||||
project: Project,
|
||||
onlyBundledPluginsEnabled: Boolean,
|
||||
compilerArguments: List<CommonCompilerArguments>
|
||||
): List<Path> {
|
||||
val combinedOriginalClasspaths = compilerArguments.asSequence().flatMap { it.getOriginalPluginClasspaths(project) }.distinct()
|
||||
|
||||
val substitutedClasspaths = combinedOriginalClasspaths.mapNotNull { userSuppliedPluginJar ->
|
||||
substitutePluginJar(project, onlyBundledPluginsEnabled, userSuppliedPluginJar)
|
||||
}.distinct()
|
||||
|
||||
return substitutedClasspaths.toList()
|
||||
}
|
||||
|
||||
@Suppress("MISSING_DEPENDENCY_SUPERCLASS_IN_TYPE_ARGUMENT")
|
||||
private fun Module.getCompilerArguments(): CommonCompilerArguments {
|
||||
return KotlinFacet.get(this)?.configuration?.settings?.mergedCompilerArguments
|
||||
?: KotlinCommonCompilerArgumentsHolder.getInstance(project).settings
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-265
@@ -3,84 +3,45 @@ package org.jetbrains.kotlin.idea.fir.extensions
|
||||
|
||||
import com.intellij.ide.trustedProjects.TrustedProjects
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.PathMacroManager
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.diagnostic.runAndLogException
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleManager
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.roots.CompilerModuleExtension
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.util.registry.RegistryValue
|
||||
import com.intellij.openapi.util.registry.RegistryValueListener
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.platform.backend.workspace.workspaceModel
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.utils.Path
|
||||
import com.intellij.platform.workspace.jps.entities.FacetEntity
|
||||
import com.intellij.util.concurrency.SynchronizedClearableLazy
|
||||
import com.intellij.util.containers.ContainerUtil
|
||||
import com.intellij.util.containers.orNull
|
||||
import com.intellij.util.lang.UrlClassLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
|
||||
import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinCompilerPluginsProvider
|
||||
import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinCompilerPluginsProvider.CompilerPluginType
|
||||
import org.jetbrains.kotlin.analysis.api.platform.projectStructure.areCompilerPluginsSupported
|
||||
import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
|
||||
import org.jetbrains.kotlin.analysis.api.projectStructure.KaScriptModule
|
||||
import org.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule
|
||||
import org.jetbrains.kotlin.analysis.low.level.api.fir.LLFirInternals
|
||||
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
|
||||
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
|
||||
import org.jetbrains.kotlin.cli.plugins.processCompilerPluginsOptions
|
||||
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
|
||||
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
|
||||
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.config.JVMConfigurationKeys
|
||||
import org.jetbrains.kotlin.config.JvmTarget
|
||||
import org.jetbrains.kotlin.extensions.ProjectExtensionDescriptor
|
||||
import org.jetbrains.kotlin.fir.extensions.FirAssignExpressionAltererExtension
|
||||
import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar
|
||||
import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.KaSourceModuleKind
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.openapiModule
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.sourceModuleKind
|
||||
import org.jetbrains.kotlin.idea.base.util.caching.getChanges
|
||||
import org.jetbrains.kotlin.idea.compiler.configuration.KotlinCommonCompilerArgumentsHolder
|
||||
import org.jetbrains.kotlin.idea.compiler.configuration.KotlinCompilerPluginsScriptConfigurationListener
|
||||
import org.jetbrains.kotlin.idea.compiler.configuration.KotlinCompilerSettingsListener
|
||||
import org.jetbrains.kotlin.idea.facet.KotlinFacet
|
||||
import org.jetbrains.kotlin.idea.facet.isKotlinFacet
|
||||
import org.jetbrains.kotlin.idea.util.getOriginalOrDelegateFileOrSelf
|
||||
import org.jetbrains.kotlin.idea.workspaceModel.KotlinSettingsEntity
|
||||
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.makeScriptCompilerArguments
|
||||
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
|
||||
import org.jetbrains.kotlin.util.ServiceLoaderLite
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
import kotlin.script.experimental.api.ScriptCompilationConfiguration
|
||||
import kotlin.script.experimental.api.compilerOptions
|
||||
|
||||
@OptIn(ExperimentalCompilerApi::class)
|
||||
@ApiStatus.Internal
|
||||
internal class KtCompilerPluginsProviderIdeImpl(
|
||||
private val project: Project,
|
||||
cs: CoroutineScope,
|
||||
) : KotlinCompilerPluginsProvider, Disposable {
|
||||
private val pluginsCacheCachedValue: SynchronizedClearableLazy<PluginsCache?> = SynchronizedClearableLazy { createNewCache() }
|
||||
private val pluginsCache: PluginsCache?
|
||||
get() = pluginsCacheCachedValue.value
|
||||
|
||||
private val onlyBundledPluginsEnabledRegistryValue: RegistryValue = Registry.get("kotlin.k2.only.bundled.compiler.plugins.enabled")
|
||||
|
||||
private val onlyBundledPluginsEnabled: Boolean
|
||||
get() = onlyBundledPluginsEnabledRegistryValue.asBoolean()
|
||||
private val pluginsCacheCachedValue: SynchronizedClearableLazy<KtCompilerPluginsCache?> = SynchronizedClearableLazy {
|
||||
if (TrustedProjects.isProjectTrusted(project)) {
|
||||
KtCompilerPluginsCache.new(project, onlyBundledPluginsEnabledRegistryValue.asBoolean())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
cs.launch {
|
||||
@@ -106,10 +67,9 @@ internal class KtCompilerPluginsProviderIdeImpl(
|
||||
messageBusConnection.subscribe(
|
||||
KotlinCompilerPluginsScriptConfigurationListener.TOPIC, object : KotlinCompilerPluginsScriptConfigurationListener {
|
||||
override fun scriptConfigurationsChanged() {
|
||||
resetScriptCache(pluginsCacheCachedValue.valueIfInitialized ?: return)
|
||||
(pluginsCacheCachedValue.valueIfInitialized ?: return).resetScriptCache()
|
||||
}
|
||||
})
|
||||
|
||||
onlyBundledPluginsEnabledRegistryValue.addListener(
|
||||
object : RegistryValueListener {
|
||||
override fun afterValueChanged(value: RegistryValue) {
|
||||
@@ -119,200 +79,21 @@ internal class KtCompilerPluginsProviderIdeImpl(
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNewCache(): PluginsCache? {
|
||||
if (!TrustedProjects.isProjectTrusted(project)) return null
|
||||
val pluginsClassLoader: UrlClassLoader = UrlClassLoader.build().apply {
|
||||
parent(KaModule::class.java.classLoader)
|
||||
|
||||
val allModules = ModuleManager.getInstance(project).modules
|
||||
val pluginClasspaths = collectSubstitutedPluginClasspaths(allModules.map { it.getCompilerArguments() })
|
||||
|
||||
files(pluginClasspaths)
|
||||
}.get()
|
||||
return PluginsCache(
|
||||
pluginsClassLoader,
|
||||
ContainerUtil.createConcurrentWeakMap(),
|
||||
SynchronizedClearableLazy { ContainerUtil.createConcurrentWeakMap() })
|
||||
}
|
||||
|
||||
private class PluginsCache(
|
||||
val pluginsClassLoader: UrlClassLoader,
|
||||
val registrarForSourceModule: ConcurrentMap<KaSourceModule, Optional<CompilerPluginRegistrar.ExtensionStorage>>,
|
||||
/**
|
||||
* As scripts might be associated with injections,
|
||||
* it's better to have a more stable anchor such as a top-level file.
|
||||
*/
|
||||
val registrarForScriptModule: SynchronizedClearableLazy<ConcurrentMap<VirtualFile, Optional<CompilerPluginRegistrar.ExtensionStorage>>>
|
||||
)
|
||||
|
||||
@OptIn(KaExperimentalApi::class)
|
||||
override fun <T : Any> getRegisteredExtensions(module: KaModule, extensionType: ProjectExtensionDescriptor<T>): List<T> {
|
||||
if (!module.areCompilerPluginsSupported()) return emptyList()
|
||||
|
||||
return when (module) {
|
||||
is KaSourceModule -> {
|
||||
val registrarForModule = pluginsCache?.registrarForSourceModule ?: return emptyList()
|
||||
module.getExtensionsForModule(registrarForModule, module, extensionType)
|
||||
}
|
||||
|
||||
is KaScriptModule -> {
|
||||
val registrarForModule = pluginsCache?.registrarForScriptModule?.value ?: return emptyList()
|
||||
val cacheKey = module.file.virtualFile.getOriginalOrDelegateFileOrSelf()
|
||||
|
||||
module.getExtensionsForModule(registrarForModule, cacheKey, extensionType)
|
||||
}
|
||||
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(KaExperimentalApi::class)
|
||||
private fun <T : Any, K : Any> KaModule.getExtensionsForModule(
|
||||
registrarForModule: ConcurrentMap<K, Optional<CompilerPluginRegistrar.ExtensionStorage>>,
|
||||
cacheKey: K,
|
||||
extensionType: ProjectExtensionDescriptor<T>
|
||||
): List<T> {
|
||||
val extensionStorage = registrarForModule.computeIfAbsent(cacheKey) {
|
||||
Optional.ofNullable(computeExtensionStorage(this))
|
||||
}.orNull() ?: return emptyList()
|
||||
val registrars = extensionStorage.registeredExtensions[extensionType] ?: return emptyList()
|
||||
@Suppress("UNCHECKED_CAST") return registrars as List<T>
|
||||
val pluginsCache = pluginsCacheCachedValue.value ?: return emptyList()
|
||||
return pluginsCache.getRegisteredExtensions(
|
||||
module = module,
|
||||
extensionType = extensionType
|
||||
)
|
||||
}
|
||||
|
||||
override fun isPluginOfTypeRegistered(module: KaModule, pluginType: CompilerPluginType): Boolean {
|
||||
val extension = when (pluginType) {
|
||||
CompilerPluginType.ASSIGNMENT -> FirAssignExpressionAltererExtension::class
|
||||
else -> return false
|
||||
}
|
||||
|
||||
return getRegisteredExtensions(module, FirExtensionRegistrarAdapter).map { (it as FirExtensionRegistrar).configure() }
|
||||
.any { it.extensions[extension]?.isNotEmpty() == true }
|
||||
}
|
||||
|
||||
@OptIn(KaExperimentalApi::class, LLFirInternals::class)
|
||||
private fun computeExtensionStorage(module: KaModule): CompilerPluginRegistrar.ExtensionStorage? {
|
||||
val classLoader = pluginsCache?.pluginsClassLoader ?: return null
|
||||
|
||||
val compilerArguments = when (module) {
|
||||
is KaSourceModule -> module.openapiModule.getCompilerArguments()
|
||||
is KaScriptModule -> {
|
||||
val scriptDefinition = module.file.findScriptDefinition() ?: return null
|
||||
val scriptConfiguration = scriptDefinition.compilationConfiguration
|
||||
|
||||
val providedOptions = scriptConfiguration[ScriptCompilationConfiguration.compilerOptions] ?: return null
|
||||
makeScriptCompilerArguments(providedOptions)
|
||||
}
|
||||
|
||||
else -> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
val pluginClasspaths = collectSubstitutedPluginClasspaths(listOf(compilerArguments)).map { it.toFile() }
|
||||
if (pluginClasspaths.isEmpty()) return null
|
||||
|
||||
val logger = logger<KtCompilerPluginsProviderIdeImpl>()
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val pluginRegistrars =
|
||||
logger.runAndLogException { ServiceLoaderLite.loadImplementations<CompilerPluginRegistrar>(pluginClasspaths, classLoader) }
|
||||
?.takeIf { it.isNotEmpty() } ?: return null
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val commandLineProcessors = logger.runAndLogException {
|
||||
ServiceLoaderLite.loadImplementations<CommandLineProcessor>(pluginClasspaths, classLoader)
|
||||
} ?: return null
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val compilerConfiguration =
|
||||
CompilerConfiguration().apply { // Temporary work-around for KTIJ-24320. Calls to 'setupCommonArguments()' and 'setupJvmSpecificArguments()'
|
||||
// (or even a platform-agnostic alternative) should be added.
|
||||
if (compilerArguments is K2JVMCompilerArguments && module is KaSourceModule) {
|
||||
val compilerExtension = CompilerModuleExtension.getInstance(module.openapiModule)
|
||||
val outputUrl = when (module.sourceModuleKind) {
|
||||
KaSourceModuleKind.TEST -> compilerExtension?.compilerOutputUrlForTests
|
||||
KaSourceModuleKind.PRODUCTION, null -> compilerExtension?.compilerOutputUrl
|
||||
}
|
||||
|
||||
putIfNotNull(JVMConfigurationKeys.JVM_TARGET, compilerArguments.jvmTarget?.let(JvmTarget::fromString))
|
||||
putIfNotNull(JVMConfigurationKeys.OUTPUT_DIRECTORY, outputUrl?.let { File(it) })
|
||||
}
|
||||
|
||||
processCompilerPluginsOptions(this, compilerArguments.pluginOptions?.toList(), commandLineProcessors)
|
||||
}
|
||||
|
||||
val storage = CompilerPluginRegistrar.ExtensionStorage()
|
||||
for (pluginRegistrar in pluginRegistrars) {
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
with(pluginRegistrar) {
|
||||
try {
|
||||
val configuration = KotlinFirCompilerPluginConfigurationForIdeProvider.getCompilerConfigurationWithCustomOptions(
|
||||
pluginRegistrar, compilerConfiguration
|
||||
) ?: compilerConfiguration
|
||||
storage.registerExtensions(configuration)
|
||||
} catch (e: ProcessCanceledException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
LOG.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the paths defined in [CommonCompilerArguments.pluginClasspaths]
|
||||
* in the absolute form with the expansion of the present path macros
|
||||
* (like 'KOTLIN_BUNDLED').
|
||||
*/
|
||||
private fun CommonCompilerArguments.getOriginalPluginClasspaths(): List<Path> {
|
||||
val pluginClassPaths = this.pluginClasspaths
|
||||
|
||||
if (pluginClassPaths.isNullOrEmpty()) return emptyList()
|
||||
|
||||
val pathMacroManager = PathMacroManager.getInstance(project)
|
||||
val expandedPluginClassPaths = pluginClassPaths.map { pathMacroManager.expandPath(it) }
|
||||
val eel = project.getEelDescriptor()
|
||||
return expandedPluginClassPaths.map { Path(it, eel).toAbsolutePath() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the substituted plugin classpaths for the given [compilerArguments] list.
|
||||
*
|
||||
* We process the whole [compilerArguments] list at once, because it gives us opportunity
|
||||
* to not call [substitutePluginJar] multiple times if [compilerArguments] use the same
|
||||
* compiler plugins jars.
|
||||
*/
|
||||
private fun collectSubstitutedPluginClasspaths(compilerArguments: List<CommonCompilerArguments>): List<Path> {
|
||||
val combinedOriginalClasspaths = compilerArguments.asSequence().flatMap { it.getOriginalPluginClasspaths() }.distinct()
|
||||
|
||||
val substitutedClasspaths = combinedOriginalClasspaths.mapNotNull(::substitutePluginJar).distinct()
|
||||
|
||||
return substitutedClasspaths.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* We have the following logic for plugins' substitution:
|
||||
* 1. Always replace our own plugins (like "allopen", "noarg", etc.) with bundled ones to avoid binary incompatibility.
|
||||
* 2. Allow to use other compiler plugins only if [onlyBundledPluginsEnabled] is set to false; otherwise, filter them.
|
||||
*/
|
||||
private fun substitutePluginJar(userSuppliedPluginJar: Path): Path? {
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val bundledPlugin = KotlinBundledFirCompilerPluginProvider.provideBundledPluginJar(project, userSuppliedPluginJar)
|
||||
if (bundledPlugin != null) return bundledPlugin
|
||||
|
||||
return userSuppliedPluginJar.takeUnless { onlyBundledPluginsEnabled }
|
||||
}
|
||||
|
||||
|
||||
private fun Module.getCompilerArguments(): CommonCompilerArguments {
|
||||
return KotlinFacet.get(this)?.configuration?.settings?.mergedCompilerArguments ?: KotlinCommonCompilerArgumentsHolder.getInstance(
|
||||
project
|
||||
).settings
|
||||
val pluginsCache = pluginsCacheCachedValue.value ?: return false
|
||||
return pluginsCache.isPluginOfTypeRegistered(
|
||||
module = module,
|
||||
pluginType = pluginType
|
||||
)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
@@ -324,37 +105,13 @@ internal class KtCompilerPluginsProviderIdeImpl(
|
||||
* registered in the corresponding [CompilerPluginRegistrar.ExtensionStorage]s.
|
||||
*/
|
||||
private fun resetPluginsCache() {
|
||||
val droppedCache = pluginsCacheCachedValue.drop() ?: return
|
||||
|
||||
droppedCache.registrarForSourceModule.values.mapNotNull { it.orNull() }.disposeAll()
|
||||
|
||||
resetScriptCache(droppedCache)
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws away only part of the cache related to the scripts, leaving other storage intact.
|
||||
*/
|
||||
private fun resetScriptCache(pluginsCache: PluginsCache) {
|
||||
val scriptsCache = pluginsCache.registrarForScriptModule.drop() ?: return
|
||||
|
||||
scriptsCache.values.mapNotNull { it.orNull() }.disposeAll()
|
||||
}
|
||||
|
||||
private fun Collection<CompilerPluginRegistrar.ExtensionStorage>.disposeAll() {
|
||||
for (storage in this) {
|
||||
for (disposable in storage.disposables) {
|
||||
LOG.runAndLogException {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
pluginsCacheCachedValue.drop()?.dispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getInstance(project: Project): KtCompilerPluginsProviderIdeImpl {
|
||||
return KotlinCompilerPluginsProvider.getInstance(project) as KtCompilerPluginsProviderIdeImpl
|
||||
}
|
||||
|
||||
private val LOG = logger<KtCompilerPluginsProviderIdeImpl>()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user