[FIR IDE] Extract common components of the Analysis API to separate modules

This commit is contained in:
Yan Zhulanow
2021-10-05 19:08:06 +09:00
committed by teamcityserver
parent 44a1fe668e
commit e2c9be0932
100 changed files with 966 additions and 424 deletions
@@ -0,0 +1,25 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
api(project(":compiler:psi"))
api(project(":analysis:analysis-api-providers"))
api(project(":analysis:project-structure"))
api(intellijCoreDep()) { includeJars("intellij-core", rootProject = rootProject) }
testApiJUnit5()
testApi(project(":kotlin-test:kotlin-test-junit"))
testApi(projectTests(":compiler:tests-common"))
testApi(projectTests(":compiler:test-infrastructure-utils"))
testApi(projectTests(":compiler:test-infrastructure"))
testApi(projectTests(":compiler:tests-common-new"))
}
sourceSets {
"main" { projectDefault() }
"test" { projectDefault() }
}
testsJar()
@@ -0,0 +1,12 @@
/*
* Copyright 2010-2020 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.impl.barebone.annotations
@RequiresOptIn
annotation class PrivateForInline
@RequiresOptIn
annotation class InternalForInline
@@ -0,0 +1,15 @@
/*
* 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.analysis.api.impl.barebone.annotations
@Target(AnnotationTarget.CLASS)
annotation class ThreadSafe
@Target(AnnotationTarget.CLASS)
annotation class NotThreadSafe
@Target(AnnotationTarget.CLASS)
annotation class Immutable
@@ -0,0 +1,22 @@
/*
* 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.analysis.api.impl.barebone
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
inline fun <reified T : PsiElement> PsiElement.parentOfType(withSelf: Boolean = false): T? {
return PsiTreeUtil.getParentOfType(this, T::class.java, !withSelf)
}
fun <T : PsiElement> PsiElement.parentsOfType(clazz: Class<out T>, withSelf: Boolean = true): Sequence<T> {
return (if (withSelf) parentsWithSelf else parents).filterIsInstance(clazz)
}
inline fun <reified T : PsiElement> PsiElement.parentsOfType(withSelf: Boolean = true): Sequence<T> =
parentsOfType(T::class.java, withSelf)
@@ -0,0 +1,41 @@
/*
* 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.analysis.api.impl.barebone.test
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.test.TestConfiguration
import org.jetbrains.kotlin.test.runners.AbstractKotlinCompilerTest
import org.jetbrains.kotlin.test.services.isKtFile
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInfo
abstract class AbstractCompilerBasedTest : AbstractKotlinCompilerTest() {
private var _disposable: Disposable? = null
protected val disposable: Disposable get() = _disposable!!
@BeforeEach
private fun intiDisposable(testInfo: TestInfo) {
_disposable = Disposer.newDisposable("disposable for ${testInfo.displayName}")
}
@AfterEach
private fun disposeDisposable() {
_disposable?.let { Disposer.dispose(it) }
_disposable = null
}
protected fun ignoreTest(filePath: String, configuration: TestConfiguration): Boolean {
val modules = configuration.moduleStructureExtractor.splitTestDataByModules(filePath, configuration.directives)
if (modules.modules.none { it.files.any { it.isKtFile } }) {
return true // nothing to highlight
}
return false
}
}
@@ -0,0 +1,182 @@
/*
* 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.analysis.api.impl.barebone.test
import com.intellij.mock.MockApplication
import com.intellij.mock.MockProject
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.TestDataFile
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.jetbrains.kotlin.test.TestInfrastructureInternals
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.builders.testConfiguration
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.model.DependencyKind
import org.jetbrains.kotlin.test.model.FrontendKinds
import org.jetbrains.kotlin.test.model.ResultingArtifact
import org.jetbrains.kotlin.test.runners.AbstractKotlinCompilerTest
import org.jetbrains.kotlin.test.services.*
import org.jetbrains.kotlin.test.services.configuration.CommonEnvironmentConfigurator
import org.jetbrains.kotlin.test.services.configuration.JvmEnvironmentConfigurator
import org.jetbrains.kotlin.test.services.impl.TemporaryDirectoryManagerImpl
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInfo
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.exists
import java.util.concurrent.ExecutionException
import kotlin.io.path.nameWithoutExtension
interface FrontendApiTestConfiguratorService {
fun TestConfigurationBuilder.configureTest(disposable: Disposable)
fun processTestFiles(files: List<KtFile>): List<KtFile> = files
fun getOriginalFile(file: KtFile): KtFile = file
fun registerProjectServices(project: MockProject)
fun registerApplicationServices(application: MockApplication)
}
abstract class AbstractFrontendApiTest : TestWithDisposable() {
protected open val enableTestInDependedMode: Boolean = true
protected lateinit var testInfo: KotlinTestInfo
private set
protected var useDependedAnalysisSession: Boolean = false
protected lateinit var testDataPath: Path
private set
protected abstract val configurator: FrontendApiTestConfiguratorService
protected open val testPrefix: String?
get() = null
protected open fun configureTest(builder: TestConfigurationBuilder) {
with(configurator) {
builder.configureTest(disposable)
}
}
protected abstract fun doTestByFileStructure(ktFiles: List<KtFile>, moduleStructure: TestModuleStructure, testServices: TestServices)
protected fun testDataFileSibling(extension: String): Path {
val extensionWithDot = "." + extension.removePrefix(".")
val baseName = testDataPath.nameWithoutExtension
val testPrefix = this.testPrefix
if (testPrefix != null) {
val prefixedFile = testDataPath.resolveSibling("$baseName.$testPrefix$extensionWithDot")
if (prefixedFile.exists()) {
return prefixedFile
}
}
return testDataPath.resolveSibling(baseName + extensionWithDot)
}
@OptIn(TestInfrastructureInternals::class)
private val configure: TestConfigurationBuilder.() -> Unit = {
globalDefaults {
frontend = FrontendKinds.FIR
targetPlatform = JvmPlatforms.defaultJvmPlatform
dependencyKind = DependencyKind.Source
}
useConfigurators(
::CommonEnvironmentConfigurator,
::JvmEnvironmentConfigurator,
)
assertions = JUnit5Assertions
useAdditionalService<TemporaryDirectoryManager>(::TemporaryDirectoryManagerImpl)
useDirectives(*AbstractKotlinCompilerTest.defaultDirectiveContainers.toTypedArray())
useDirectives(JvmEnvironmentConfigurationDirectives)
useSourcePreprocessor(::ExpressionMarkersSourceFilePreprocessor)
useAdditionalService { ExpressionMarkerProvider() }
useAdditionalService(::TestKtModuleProvider)
configureTest(this)
startingArtifactFactory = { ResultingArtifact.Source() }
this.testInfo = this@AbstractFrontendApiTest.testInfo
}
protected fun runTest(@TestDataFile path: String) {
testDataPath = Paths.get(path)
val testConfiguration = testConfiguration(path, configure)
Disposer.register(disposable, testConfiguration.rootDisposable)
val testServices = testConfiguration.testServices
val moduleStructure = testConfiguration.moduleStructureExtractor.splitTestDataByModules(
path,
testConfiguration.directives,
).also { testModuleStructure ->
testConfiguration.testServices.register(TestModuleStructure::class, testModuleStructure)
testConfiguration.preAnalysisHandlers.forEach { preprocessor ->
preprocessor.preprocessModuleStructure(testModuleStructure)
}
}
val singleModule = moduleStructure.modules.single()
val project = testServices.compilerConfigurationProvider.getProject(singleModule)
val moduleInfoProvider = testServices.projectModuleProvider
val moduleInfo = moduleInfoProvider.getModule(singleModule.name)
with(project as MockProject) {
configurator.registerProjectServices(this)
}
registerApplicationServices()
val ktFiles = moduleInfo.testFilesToKtFiles.filterKeys { testFile -> !testFile.isAdditional }.values.toList()
doTestByFileStructure(ktFiles, moduleStructure, testServices)
if (!enableTestInDependedMode || ktFiles.any {
InTextDirectivesUtils.isDirectiveDefined(it.text, DISABLE_DEPENDED_MODE_DIRECTIVE)
}) {
return
}
try {
useDependedAnalysisSession = true
doTestByFileStructure(configurator.processTestFiles(ktFiles), moduleStructure, testServices)
} catch (e: SkipDependedModeException) {
// Skip the test if needed
} catch (e: ExecutionException) {
if (e.cause !is SkipDependedModeException)
throw Exception("Test succeeded in normal analysis mode but failed in depended analysis mode.", e)
} catch (e: Exception) {
throw Exception("Test succeeded in normal analysis mode but failed in depended analysis mode.", e)
}
}
private fun registerApplicationServices() {
val application = ApplicationManager.getApplication() as MockApplication
KotlinCoreEnvironment.underApplicationLock {
configurator.registerApplicationServices(application)
}
}
@BeforeEach
fun initTestInfo(testInfo: TestInfo) {
this.testInfo = KotlinTestInfo(
className = testInfo.testClass.orElseGet(null)?.name ?: "_undefined_",
methodName = testInfo.testMethod.orElseGet(null)?.name ?: "_testUndefined_",
tags = testInfo.tags
)
}
protected class SkipDependedModeException : Exception()
companion object {
val DISABLE_DEPENDED_MODE_DIRECTIVE = "DISABLE_DEPENDED_MODE"
}
}
@@ -0,0 +1,104 @@
/*
* 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.analysis.api.impl.barebone.test
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.analysis.api.impl.barebone.parentOfType
import org.jetbrains.kotlin.fir.PrivateForInline
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.elementsInRange
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.services.SourceFilePreprocessor
import org.jetbrains.kotlin.test.services.TestService
import org.jetbrains.kotlin.test.services.TestServices
internal class ExpressionMarkersSourceFilePreprocessor(testServices: TestServices) : SourceFilePreprocessor(testServices) {
override fun process(file: TestFile, content: String): String {
val withSelectedProcessed = processSelectedExpression(file, content)
return processCaretExpression(file, withSelectedProcessed)
}
private fun processSelectedExpression(file: TestFile, content: String): String {
val startCaretPosition = content.indexOfOrNull(TAGS.OPENING_EXPRESSION_TAG) ?: return content
val endCaretPosition = content.indexOfOrNull(TAGS.CLOSING_EXPRESSION_TAG)
?: error("${TAGS.CLOSING_EXPRESSION_TAG} was not found in the file")
check(startCaretPosition < endCaretPosition)
testServices.expressionMarkerProvider.addSelectedExpression(
file,
TextRange.create(startCaretPosition, endCaretPosition - TAGS.OPENING_EXPRESSION_TAG.length)
)
return content
.replace(TAGS.OPENING_EXPRESSION_TAG, "")
.replace(TAGS.CLOSING_EXPRESSION_TAG, "")
}
private fun processCaretExpression(file: TestFile, content: String): String {
val startCaretPosition = content.indexOfOrNull(TAGS.CARET) ?: return content
testServices.expressionMarkerProvider.addCaret(file, startCaretPosition)
return content
.replace(TAGS.CARET, "")
}
object TAGS {
const val OPENING_EXPRESSION_TAG = "<expr>"
const val CLOSING_EXPRESSION_TAG = "</expr>"
const val CARET = "<caret>"
}
}
class ExpressionMarkerProvider : TestService {
private val selected = mutableMapOf<String, TextRange>()
@PrivateForInline
val atCaret = mutableMapOf<String, Int>()
fun addSelectedExpression(file: TestFile, range: TextRange) {
selected[file.relativePath] = range
}
@OptIn(PrivateForInline::class)
fun addCaret(file: TestFile, caret: Int) {
atCaret[file.relativePath] = caret
}
@OptIn(PrivateForInline::class)
fun getCaretPosition(file: KtFile): Int {
return atCaret[file.name]
?: error("No caret found in file")
}
inline fun <reified P : KtElement> getElementOfTypAtCaret(file: KtFile): P {
val offset = getCaretPosition(file)
return file.findElementAt(offset)
?.parentOfType<P>()
?: error("No expression found at caret")
}
fun getSelectedElement(file: KtFile): KtElement {
val range = selected[file.name]
?: error("No selected expression found in file")
val elements = file.elementsInRange(range).trimWhitespaces()
if (elements.size != 1) {
error("Expected one element at rage but found ${elements.size} [${elements.joinToString { it::class.simpleName + ": " + it.text }}]")
}
return elements.single() as KtElement
}
private fun List<PsiElement>.trimWhitespaces(): List<PsiElement> =
dropWhile { it is PsiWhiteSpace }
.dropLastWhile { it is PsiWhiteSpace }
}
val TestServices.expressionMarkerProvider: ExpressionMarkerProvider by TestServices.testServiceAccessor()
fun String.indexOfOrNull(substring: String) =
indexOf(substring).takeIf { it >= 0 }
@@ -0,0 +1,33 @@
/*
* 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.analysis.api.impl.barebone.test
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestService
import org.jetbrains.kotlin.test.services.TestServices
class TestKtModuleProvider(
private val testServices: TestServices
) : TestService {
private val cache = mutableMapOf<String, TestKtSourceModule>()
fun registerModuleInfo(project: Project, testModule: TestModule, ktFiles: Map<TestFile, KtFile>) {
cache[testModule.name] = TestKtSourceModule(project, testModule, ktFiles, testServices)
}
fun getModuleInfoByKtFile(ktFile: KtFile): TestKtSourceModule =
cache.values.first { moduleSourceInfo ->
(if (ktFile.isPhysical) ktFile else ktFile.originalFile) in moduleSourceInfo.ktFiles
}
fun getModule(moduleName: String): TestKtSourceModule =
cache.getValue(moduleName)
}
val TestServices.projectModuleProvider: TestKtModuleProvider by TestServices.testServiceAccessor()
@@ -0,0 +1,111 @@
/*
* 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.analysis.api.impl.barebone.test
import com.intellij.openapi.project.Project
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.ProjectScope
import org.jetbrains.kotlin.analysis.project.structure.KtLibraryModule
import org.jetbrains.kotlin.analysis.project.structure.KtLibrarySourceModule
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule
import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots
import org.jetbrains.kotlin.cli.jvm.config.jvmModularRoots
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
import org.jetbrains.kotlin.test.frontend.fir.getAnalyzerServices
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.compilerConfigurationProvider
import org.jetbrains.kotlin.utils.addIfNotNull
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
@OptIn(ExperimentalStdlibApi::class)
class TestKtSourceModule(
override val project: Project,
val testModule: TestModule,
val testFilesToKtFiles: Map<TestFile, KtFile>,
testServices: TestServices,
) : KtSourceModule {
private val moduleProvider = testServices.projectModuleProvider
private val compilerConfigurationProvider = testServices.compilerConfigurationProvider
private val configuration = compilerConfigurationProvider.getCompilerConfiguration(testModule)
val ktFiles = testFilesToKtFiles.values.toSet()
override val moduleName: String
get() = testModule.name
override val directRegularDependencies: List<KtModule> by lazy(LazyThreadSafetyMode.PUBLICATION) {
buildList {
testModule.allDependencies.mapTo(this) { moduleProvider.getModule(it.moduleName) }
addIfNotNull(
libraryByRoots(
(configuration.jvmModularRoots + configuration.jvmClasspathRoots).map(File::toPath)
)
)
}
}
override val directRefinementDependencies: List<KtModule> by lazy(LazyThreadSafetyMode.PUBLICATION) {
testModule.dependsOnDependencies
.map { moduleProvider.getModule(it.moduleName) }
}
override val directFriendDependencies: List<KtModule> by lazy(LazyThreadSafetyMode.PUBLICATION) {
buildList {
testModule.friendDependencies.mapTo(this) { moduleProvider.getModule(it.moduleName) }
addIfNotNull(
libraryByRoots(configuration[JVMConfigurationKeys.FRIEND_PATHS].orEmpty().map(Paths::get))
)
}
}
private fun libraryByRoots(roots: List<Path>): LibraryByRoots? {
if (roots.isEmpty()) return null
return LibraryByRoots(
roots,
this@TestKtSourceModule,
project,
)
}
override val contentScope: GlobalSearchScope =
TopDownAnalyzerFacadeForJVM.newModuleSearchScope(project, testFilesToKtFiles.values)
override val languageVersionSettings: LanguageVersionSettings
get() = testModule.languageVersionSettings
override val platform: TargetPlatform
get() = testModule.targetPlatform
override val analyzerServices: PlatformDependentAnalyzerServices
get() = testModule.targetPlatform.getAnalyzerServices()
}
private class LibraryByRoots(
private val roots: List<Path>,
private val sourceModule: KtSourceModule,
override val project: Project,
) : KtLibraryModule {
override val libraryName: String get() = "Test Library"
override val directRegularDependencies: List<KtModule> get() = emptyList()
override val directRefinementDependencies: List<KtModule> get() = emptyList()
override val directFriendDependencies: List<KtModule> get() = emptyList()
override val contentScope: GlobalSearchScope get() = ProjectScope.getLibrariesScope(project)
override val platform: TargetPlatform get() = sourceModule.platform
override val analyzerServices: PlatformDependentAnalyzerServices get() = sourceModule.analyzerServices
override fun getBinaryRoots(): Collection<Path> = roots
override val librarySources: KtLibrarySourceModule? get() = null
}
@@ -0,0 +1,28 @@
/*
* 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.analysis.api.impl.barebone.test
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInfo
abstract class TestWithDisposable {
private var _disposable: Disposable? = null
protected val disposable: Disposable get() = _disposable!!
@BeforeEach
private fun intiDisposable(testInfo: TestInfo) {
_disposable = Disposer.newDisposable("disposable for ${testInfo.displayName}")
}
@AfterEach
private fun disposeDisposable() {
_disposable?.let { Disposer.dispose(it) }
_disposable = null
}
}