From 03a7162f3736b26931d17998a60ac6fd8edc9dec Mon Sep 17 00:00:00 2001 From: Marco Pennekamp Date: Mon, 13 Nov 2023 15:17:59 +0100 Subject: [PATCH] [AA] Enable test-specific configuration of write access permissions - In general, Analysis API tests forbid write access because the Analysis API should not be used from write actions. However, in some cases we might want tests to e.g. modify PSI, which requires write access. This change allows `AnalysisApiTestConfigurator`s to enable write access for the specific test. --- .../structure/StandaloneProjectFactory.kt | 11 +-- .../StandaloneAnalysisAPISessionBuilder.kt | 3 +- .../base/registerBaseTestServices.kt | 2 +- .../services/AnalysisApiEnvironmentManager.kt | 5 +- .../AnalysisApiTestConfigurator.kt | 9 +++ .../KotlinCoreApplicationEnvironment.kt | 73 ++++++++++++++++--- .../cli/jvm/compiler/KotlinCoreEnvironment.kt | 37 +++++++--- 7 files changed, 108 insertions(+), 32 deletions(-) diff --git a/analysis/analysis-api-standalone/analysis-api-standalone-base/src/org/jetbrains/kotlin/analysis/api/standalone/base/project/structure/StandaloneProjectFactory.kt b/analysis/analysis-api-standalone/analysis-api-standalone-base/src/org/jetbrains/kotlin/analysis/api/standalone/base/project/structure/StandaloneProjectFactory.kt index f5f7e3e23de..96542a37b4b 100644 --- a/analysis/analysis-api-standalone/analysis-api-standalone-base/src/org/jetbrains/kotlin/analysis/api/standalone/base/project/structure/StandaloneProjectFactory.kt +++ b/analysis/analysis-api-standalone/analysis-api-standalone-base/src/org/jetbrains/kotlin/analysis/api/standalone/base/project/structure/StandaloneProjectFactory.kt @@ -69,14 +69,15 @@ object StandaloneProjectFactory { fun createProjectEnvironment( projectDisposable: Disposable, applicationDisposable: Disposable, - unitTestMode: Boolean = false, + applicationEnvironmentMode: KotlinCoreApplicationEnvironmentMode, compilerConfiguration: CompilerConfiguration = CompilerConfiguration(), classLoader: ClassLoader = MockProject::class.java.classLoader, ): KotlinCoreProjectEnvironment { - val applicationEnvironment = if (unitTestMode) - KotlinCoreEnvironment.getOrCreateApplicationEnvironmentForTests(applicationDisposable, compilerConfiguration) - else - KotlinCoreEnvironment.getOrCreateApplicationEnvironmentForProduction(applicationDisposable, compilerConfiguration) + val applicationEnvironment = KotlinCoreEnvironment.getOrCreateApplicationEnvironment( + applicationDisposable, + compilerConfiguration, + applicationEnvironmentMode, + ) registerApplicationExtensionPoints(applicationEnvironment, applicationDisposable) diff --git a/analysis/analysis-api-standalone/src/org/jetbrains/kotlin/analysis/api/standalone/StandaloneAnalysisAPISessionBuilder.kt b/analysis/analysis-api-standalone/src/org/jetbrains/kotlin/analysis/api/standalone/StandaloneAnalysisAPISessionBuilder.kt index 5d55307f05c..770a1029c6c 100644 --- a/analysis/analysis-api-standalone/src/org/jetbrains/kotlin/analysis/api/standalone/StandaloneAnalysisAPISessionBuilder.kt +++ b/analysis/analysis-api-standalone/src/org/jetbrains/kotlin/analysis/api/standalone/StandaloneAnalysisAPISessionBuilder.kt @@ -30,6 +30,7 @@ import org.jetbrains.kotlin.analysis.project.structure.impl.getPsiFilesFromPaths import org.jetbrains.kotlin.analysis.project.structure.impl.getSourceFilePaths import org.jetbrains.kotlin.analysis.providers.* import org.jetbrains.kotlin.analysis.providers.impl.* +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironmentMode import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.setupIdeaStandaloneExecution import org.jetbrains.kotlin.config.CompilerConfiguration @@ -61,7 +62,7 @@ public class StandaloneAnalysisAPISessionBuilder( StandaloneProjectFactory.createProjectEnvironment( projectDisposable, applicationDisposable, - unitTestMode, + KotlinCoreApplicationEnvironmentMode.fromUnitTestModeFlag(unitTestMode), classLoader = classLoader ) diff --git a/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/base/registerBaseTestServices.kt b/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/base/registerBaseTestServices.kt index 396617a8692..6b6ce295b7a 100644 --- a/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/base/registerBaseTestServices.kt +++ b/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/base/registerBaseTestServices.kt @@ -26,7 +26,7 @@ fun TestConfigurationBuilder.registerAnalysisApiBaseTestServices( ) { useAdditionalService(::TestDisposableProviderImpl) useAdditionalService(::AnalysisApiKtModuleProviderImpl) - useAdditionalService(::AnalysisApiEnvironmentManagerImpl.bind(testDisposable)) + useAdditionalService(::AnalysisApiEnvironmentManagerImpl.bind(testDisposable, configurator)) useAdditionalService { ExecutionListenerBasedDisposableProvider() } useAdditionalService { StandardLibrariesPathProviderForKotlinProject } diff --git a/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/services/AnalysisApiEnvironmentManager.kt b/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/services/AnalysisApiEnvironmentManager.kt index 1790116f73a..7de356619b9 100644 --- a/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/services/AnalysisApiEnvironmentManager.kt +++ b/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/services/AnalysisApiEnvironmentManager.kt @@ -12,7 +12,9 @@ import org.jetbrains.kotlin.analysis.api.standalone.base.project.structure.KtSta import org.jetbrains.kotlin.analysis.api.standalone.base.project.structure.StandaloneProjectFactory import org.jetbrains.kotlin.analysis.project.structure.KtBuiltinsModule import org.jetbrains.kotlin.analysis.test.framework.project.structure.ktModuleProvider +import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestConfigurator import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironmentMode import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment import org.jetbrains.kotlin.config.JVMConfigurationKeys import org.jetbrains.kotlin.config.languageVersionSettings @@ -38,12 +40,13 @@ abstract class AnalysisApiEnvironmentManager : TestService { class AnalysisApiEnvironmentManagerImpl( override val testServices: TestServices, override val testRootDisposable: Disposable, + private val configurator: AnalysisApiTestConfigurator, ) : AnalysisApiEnvironmentManager() { private val _projectEnvironment: KotlinCoreProjectEnvironment by lazy { StandaloneProjectFactory.createProjectEnvironment( testRootDisposable, testServices.applicationDisposableProvider.getApplicationRootDisposable(), - unitTestMode = true + KotlinCoreApplicationEnvironmentMode.UnitTest(configurator.isWriteAccessAllowed), ) } diff --git a/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/test/configurators/AnalysisApiTestConfigurator.kt b/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/test/configurators/AnalysisApiTestConfigurator.kt index 95a8960610a..dc426ab25cd 100644 --- a/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/test/configurators/AnalysisApiTestConfigurator.kt +++ b/analysis/analysis-test-framework/tests/org/jetbrains/kotlin/analysis/test/framework/test/configurators/AnalysisApiTestConfigurator.kt @@ -36,6 +36,15 @@ abstract class AnalysisApiTestConfigurator { abstract val serviceRegistrars: List + /** + * Whether the associated [core application environment][org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironment] should + * permit write access. + * + * Write access is disabled by default because the Analysis API should not be used during write actions. Nevertheless, some tests may + * need write access, for example to modify PSI. + */ + open val isWriteAccessAllowed get() = false + open fun prepareFilesInModule(files: List, module: TestModule, testServices: TestServices) {} open fun doGlobalModuleStateModification(project: Project) { diff --git a/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreApplicationEnvironment.kt b/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreApplicationEnvironment.kt index 8bcb3c1fceb..ce2f5da4690 100644 --- a/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreApplicationEnvironment.kt +++ b/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreApplicationEnvironment.kt @@ -24,13 +24,31 @@ import org.jetbrains.kotlin.cli.jvm.compiler.IdeaExtensionPoints.registerVersion import org.jetbrains.kotlin.cli.jvm.compiler.jarfs.FastJarFileSystem import org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem +sealed interface KotlinCoreApplicationEnvironmentMode { + val isWriteAccessAllowed: Boolean + + object Production : KotlinCoreApplicationEnvironmentMode { + override val isWriteAccessAllowed: Boolean get() = true + } + + class UnitTest(override val isWriteAccessAllowed: Boolean) : KotlinCoreApplicationEnvironmentMode + + companion object { + fun fromUnitTestModeFlag(isUnitTestMode: Boolean): KotlinCoreApplicationEnvironmentMode = + if (isUnitTestMode) UnitTest(isWriteAccessAllowed = false) else Production + } +} + class KotlinCoreApplicationEnvironment private constructor( - parentDisposable: Disposable, unitTestMode: Boolean -) : JavaCoreApplicationEnvironment(parentDisposable, unitTestMode) { + parentDisposable: Disposable, + private val environmentMode: KotlinCoreApplicationEnvironmentMode, +) : JavaCoreApplicationEnvironment(parentDisposable, environmentMode is KotlinCoreApplicationEnvironmentMode.UnitTest) { init { + application.initializeEnvironmentMode(environmentMode) + registerApplicationService(JavaFileCodeStyleFacadeFactory::class.java, DummyJavaFileCodeStyleFacadeFactory()) - registerFileType(JavaClassFileType.INSTANCE, "sig"); + registerFileType(JavaClassFileType.INSTANCE, "sig") } override fun createJrtFileSystem(): VirtualFileSystem { @@ -41,15 +59,11 @@ class KotlinCoreApplicationEnvironment private constructor( val mock = super.createApplication(parentDisposable) /** - * We can't use just [unitTestMode] from constructor, because at this moment - * the corresponding property is not yet initialized, so [unitTestMode] is effectively always false, - * because this function called from super class constructor + * We can't use [environmentMode] from the constructor to decide whether we're in unit test mode, because the corresponding property + * is not yet initialized when this function is called from the superclass constructor. */ return if (mock.isUnitTestMode) { - object : MockApplication(parentDisposable) { - override fun isUnitTestMode(): Boolean = true - override fun isWriteAccessAllowed(): Boolean = false - } + MockUnitTestApplication(parentDisposable) } else { mock } @@ -80,10 +94,22 @@ class KotlinCoreApplicationEnvironment private constructor( } companion object { + @Deprecated( + message = "The `unitTestMode` flag is deprecated in favor of `KotlinCoreApplicationEnvironmentMode` configuration.", + replaceWith = ReplaceWith("create(parentDisposable, KotlinCoreApplicationEnvironmentMode.fromUnitTestModeFlag(unitTestMode))"), + ) fun create( - parentDisposable: Disposable, unitTestMode: Boolean + parentDisposable: Disposable, + unitTestMode: Boolean, ): KotlinCoreApplicationEnvironment { - val environment = KotlinCoreApplicationEnvironment(parentDisposable, unitTestMode) + return create(parentDisposable, KotlinCoreApplicationEnvironmentMode.fromUnitTestModeFlag(unitTestMode)) + } + + fun create( + parentDisposable: Disposable, + environmentMode: KotlinCoreApplicationEnvironmentMode, + ): KotlinCoreApplicationEnvironment { + val environment = KotlinCoreApplicationEnvironment(parentDisposable, environmentMode) registerExtensionPoints() return environment } @@ -102,3 +128,26 @@ class KotlinCoreApplicationEnvironment private constructor( } } } + +private class MockUnitTestApplication(parentDisposable: Disposable) : MockApplication(parentDisposable) { + /** + * We can't use [KotlinCoreApplicationEnvironment.environmentMode] in [KotlinCoreApplicationEnvironment.createApplication], because the + * corresponding property is not yet initialized when `createApplication` is called from the superclass constructor. So we have to + * initialize it later. + */ + lateinit var environmentMode: KotlinCoreApplicationEnvironmentMode + + override fun isUnitTestMode(): Boolean = true + + override fun isWriteAccessAllowed(): Boolean = environmentMode.isWriteAccessAllowed +} + +private fun MockApplication.initializeEnvironmentMode(environmentMode: KotlinCoreApplicationEnvironmentMode) { + if (this !is MockUnitTestApplication) return + + this.environmentMode = environmentMode + + require(isWriteAccessAllowed == environmentMode.isWriteAccessAllowed) { + "The mock application's `isWriteAccessAllowed` should correspond to the environment mode configuration." + } +} diff --git a/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt b/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt index b145729f4ce..a420afcb5dd 100644 --- a/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt +++ b/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt @@ -469,7 +469,9 @@ class KotlinCoreEnvironment private constructor( // Tests are supposed to create a single project and dispose it right after use val appEnv = createApplicationEnvironment( - parentDisposable, configuration, unitTestMode = true + parentDisposable, + configuration, + KotlinCoreApplicationEnvironmentMode.UnitTest(isWriteAccessAllowed = false), ) val projectEnv = ProjectEnvironment(parentDisposable, appEnv, configuration) return KotlinCoreEnvironment(projectEnv, configuration, extensionConfigs) @@ -499,7 +501,7 @@ class KotlinCoreEnvironment private constructor( val appEnv = createApplicationEnvironment( parentDisposable, configuration, - unitTestMode = true + KotlinCoreApplicationEnvironmentMode.UnitTest(isWriteAccessAllowed = false), ) return ProjectEnvironment(parentDisposable, appEnv, configuration) } @@ -509,14 +511,25 @@ class KotlinCoreEnvironment private constructor( fun getOrCreateApplicationEnvironmentForProduction( parentDisposable: Disposable, configuration: CompilerConfiguration - ): KotlinCoreApplicationEnvironment = getOrCreateApplicationEnvironment(parentDisposable, configuration, unitTestMode = false) + ): KotlinCoreApplicationEnvironment = getOrCreateApplicationEnvironment( + parentDisposable, + configuration, + KotlinCoreApplicationEnvironmentMode.Production, + ) fun getOrCreateApplicationEnvironmentForTests( - parentDisposable: Disposable, configuration: CompilerConfiguration - ): KotlinCoreApplicationEnvironment = getOrCreateApplicationEnvironment(parentDisposable, configuration, unitTestMode = true) + parentDisposable: Disposable, + configuration: CompilerConfiguration, + ): KotlinCoreApplicationEnvironment = getOrCreateApplicationEnvironment( + parentDisposable, + configuration, + KotlinCoreApplicationEnvironmentMode.UnitTest(isWriteAccessAllowed = false), + ) - private fun getOrCreateApplicationEnvironment( - parentDisposable: Disposable, configuration: CompilerConfiguration, unitTestMode: Boolean + fun getOrCreateApplicationEnvironment( + parentDisposable: Disposable, + configuration: CompilerConfiguration, + environmentMode: KotlinCoreApplicationEnvironmentMode, ): KotlinCoreApplicationEnvironment { synchronized(APPLICATION_LOCK) { if (ourApplicationEnvironment == null) { @@ -525,7 +538,7 @@ class KotlinCoreEnvironment private constructor( createApplicationEnvironment( disposable, configuration, - unitTestMode + environmentMode, ) ourProjectCount = 0 Disposer.register(disposable, Disposable { @@ -606,11 +619,11 @@ class KotlinCoreEnvironment private constructor( } private fun createApplicationEnvironment( - parentDisposable: Disposable, configuration: CompilerConfiguration, unitTestMode: Boolean + parentDisposable: Disposable, + configuration: CompilerConfiguration, + environmentMode: KotlinCoreApplicationEnvironmentMode, ): KotlinCoreApplicationEnvironment { - val applicationEnvironment = KotlinCoreApplicationEnvironment.create( - parentDisposable, unitTestMode - ) + val applicationEnvironment = KotlinCoreApplicationEnvironment.create(parentDisposable, environmentMode) registerApplicationExtensionPointsAndExtensionsFrom(configuration, "extensions/compiler.xml")