[Test] Merge multiple abstract legacy test runners into a single one

There was only one real inheritor of AbstractFirBaseDiagnosticTest,
  so there is no need in the whole hierarchy of abstract test runners

Also AbstractFirOldFrontendLightClassesTest is not intended to check any
  diagnostics or CFG dumps, so this logic was removed (they still can be
  checked using non-legacy AbstractFirDiagnosticTest from the main test
  infrastructure)
This commit is contained in:
Dmitriy Novozhilov
2023-09-05 13:55:46 +03:00
committed by Space Team
parent fa4c6d55c1
commit 2d334c4e71
5 changed files with 210 additions and 776 deletions
@@ -1,24 +1,159 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.fir.java
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiElementFinder
import com.intellij.psi.impl.compiled.ClsClassImpl
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.fir.AbstractFirOldFrontendDiagnosticsTest
import org.jetbrains.kotlin.KtInMemoryTextSourceFile
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.asJava.finder.JavaElementFinder
import org.jetbrains.kotlin.checkers.BaseDiagnosticsTest
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.PsiBasedProjectFileSearchScope
import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
import org.jetbrains.kotlin.cli.jvm.compiler.VfsBasedProjectEnvironment
import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.builder.BodyBuildingMode
import org.jetbrains.kotlin.fir.builder.PsiRawFirBuilder
import org.jetbrains.kotlin.fir.declarations.FirFile
import org.jetbrains.kotlin.fir.doFirResolveTestBench
import org.jetbrains.kotlin.fir.lightTree.LightTree2Fir
import org.jetbrains.kotlin.fir.resolve.providers.firProvider
import org.jetbrains.kotlin.fir.resolve.providers.impl.FirProviderImpl
import org.jetbrains.kotlin.fir.resolve.transformers.createAllCompilerResolveProcessors
import org.jetbrains.kotlin.fir.session.FirSessionFactoryHelper
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatformAnalyzerServices
import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.jetbrains.kotlin.toSourceLinesMapping
import java.io.File
abstract class AbstractFirOldFrontendLightClassesTest : AbstractFirOldFrontendDiagnosticsTest() {
override fun checkResultingFirFiles(firFiles: List<FirFile>, testDataFile: File) {
super.checkResultingFirFiles(firFiles, testDataFile)
abstract class AbstractFirOldFrontendLightClassesTest : BaseDiagnosticsTest() {
override fun analyzeAndCheck(testDataFile: File, files: List<TestFile>) {
if (files.any { "FIR_IGNORE" in it.directives }) return
try {
analyzeAndCheckUnhandled(testDataFile, files, useLightTree)
} catch (t: AssertionError) {
throw t
} catch (t: Throwable) {
throw t
}
}
private val useLightTree: Boolean
get() = false
private val useLazyBodiesModeForRawFir: Boolean
get() = false
override fun setupEnvironment(environment: KotlinCoreEnvironment) {
PsiElementFinder.EP.getPoint(environment.project).unregisterExtension(JavaElementFinder::class.java)
}
private fun analyzeAndCheckUnhandled(testDataFile: File, files: List<TestFile>, useLightTree: Boolean = false) {
val groupedByModule = files.groupBy(TestFile::module)
val modules = createModules(groupedByModule)
val sessionProvider = FirProjectSessionProvider()
//For BuiltIns, registered in sessionProvider automatically
val allProjectScope = GlobalSearchScope.allScope(project)
val configToSession = modules.mapValues { (config, info) ->
val moduleFiles = groupedByModule.getValue(config)
val scope = TopDownAnalyzerFacadeForJVM.newModuleSearchScope(
project,
moduleFiles.mapNotNull { it.ktFile }
)
val projectEnvironment = VfsBasedProjectEnvironment(
project,
VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL)
) { environment.createPackagePartProvider(it) }
FirSessionFactoryHelper.createSessionWithDependencies(
Name.identifier(info.name.asString().removeSurrounding("<", ">")),
info.platform,
info.analyzerServices,
externalSessionProvider = sessionProvider,
projectEnvironment,
config?.languageVersionSettings ?: LanguageVersionSettingsImpl.DEFAULT,
javaSourcesScope = PsiBasedProjectFileSearchScope(scope),
librariesScope = PsiBasedProjectFileSearchScope(allProjectScope),
lookupTracker = null,
enumWhenTracker = null,
importTracker = null,
incrementalCompilationContext = null,
extensionRegistrars = emptyList(),
needRegisterJavaElementFinder = true
) {}
}
val firFilesPerSession = mutableMapOf<FirSession, List<FirFile>>()
// TODO: make module/session/transformer handling like in AbstractFirMultiModuleTest (IDE)
for ((testModule, testFilesInModule) in groupedByModule) {
val ktFiles = getKtFiles(testFilesInModule, true)
val session = configToSession.getValue(testModule)
val firFiles = mutableListOf<FirFile>()
mapKtFilesToFirFiles(session, ktFiles, firFiles, useLightTree)
firFilesPerSession[session] = firFiles
}
runAnalysis(testDataFile, firFilesPerSession)
}
private fun mapKtFilesToFirFiles(session: FirSession, ktFiles: List<KtFile>, firFiles: MutableList<FirFile>, useLightTree: Boolean) {
val firProvider = (session.firProvider as FirProviderImpl)
if (useLightTree) {
val lightTreeBuilder = LightTree2Fir(session, firProvider.kotlinScopeProvider)
ktFiles.mapTo(firFiles) {
val firFile =
lightTreeBuilder.buildFirFile(
it.text,
KtInMemoryTextSourceFile(it.name, it.virtualFilePath, it.text),
it.text.toSourceLinesMapping()
)
(session.firProvider as FirProviderImpl).recordFile(firFile)
firFile
}
} else {
val firBuilder = PsiRawFirBuilder(
session,
firProvider.kotlinScopeProvider,
bodyBuildingMode = BodyBuildingMode.lazyBodies(useLazyBodiesModeForRawFir)
)
ktFiles.mapTo(firFiles) {
val firFile = firBuilder.buildFirFile(it)
firProvider.recordFile(firFile)
firFile
}
}
}
private fun runAnalysis(testDataFile: File, firFilesPerSession: Map<FirSession, List<FirFile>>) {
for ((session, firFiles) in firFilesPerSession) {
doFirResolveTestBench(firFiles, createAllCompilerResolveProcessors(session), gc = false)
}
checkResultingFirFiles(testDataFile)
}
private fun checkResultingFirFiles(testDataFile: File) {
val ourFinders = PsiElementFinder.EP.getPoint(project).extensions.filterIsInstance<FirJavaElementFinder>()
assertNotEmpty(ourFinders)
@@ -52,4 +187,73 @@ abstract class AbstractFirOldFrontendLightClassesTest : AbstractFirOldFrontendDi
override fun createTestFileFromPath(filePath: String): File {
return File(filePath)
}
private fun createModules(groupedByModule: Map<TestModule?, List<TestFile>>): MutableMap<TestModule?, ModuleInfo> {
val modules =
HashMap<TestModule?, ModuleInfo>()
for (testModule in groupedByModule.keys) {
val module = if (testModule == null)
createSealedModule()
else
createModule(testModule.name)
modules[testModule] = module
}
for (testModule in groupedByModule.keys) {
if (testModule == null) continue
val module = modules[testModule]!!
val dependencies = ArrayList<ModuleInfo>()
dependencies.add(module)
for (dependency in testModule.dependencies) {
dependencies.add(modules[dependency as TestModule?]!!)
}
dependencies.add(builtInsModuleInfo)
(module as TestModuleInfo).dependencies.addAll(dependencies)
}
return modules
}
private val builtInsModuleInfo = BuiltInModuleInfo(Name.special("<built-ins>"))
private fun createModule(moduleName: String): TestModuleInfo {
parseModulePlatformByName(moduleName)
return TestModuleInfo(Name.special("<$moduleName>"))
}
private class BuiltInModuleInfo(override val name: Name) : ModuleInfo {
override val platform: TargetPlatform
get() = JvmPlatforms.unspecifiedJvmPlatform
override val analyzerServices: PlatformDependentAnalyzerServices
get() = JvmPlatformAnalyzerServices
override fun dependencies(): List<ModuleInfo> {
return listOf(this)
}
}
private class TestModuleInfo(override val name: Name) : ModuleInfo {
override val platform: TargetPlatform
get() = JvmPlatforms.unspecifiedJvmPlatform
override val analyzerServices: PlatformDependentAnalyzerServices
get() = JvmPlatformAnalyzerServices
val dependencies = mutableListOf<ModuleInfo>(this)
override fun dependencies(): List<ModuleInfo> {
return dependencies
}
}
private fun createSealedModule(): TestModuleInfo {
return createModule("test-module-jvm").apply {
dependencies += builtInsModuleInfo
}
}
}
@@ -1,355 +0,0 @@
/*
* Copyright 2010-2019 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.fir
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementFinder
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.KtInMemoryTextSourceFile
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.asJava.finder.JavaElementFinder
import org.jetbrains.kotlin.checkers.BaseDiagnosticsTest
import org.jetbrains.kotlin.checkers.DiagnosticDiffCallbacks
import org.jetbrains.kotlin.checkers.diagnostics.ActualDiagnostic
import org.jetbrains.kotlin.checkers.diagnostics.PositionalTextDiagnostic
import org.jetbrains.kotlin.checkers.diagnostics.SyntaxErrorDiagnostic
import org.jetbrains.kotlin.checkers.diagnostics.TextDiagnostic
import org.jetbrains.kotlin.checkers.utils.CheckerTestUtil
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.PsiBasedProjectFileSearchScope
import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
import org.jetbrains.kotlin.cli.jvm.compiler.VfsBasedProjectEnvironment
import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.KtDiagnostic
import org.jetbrains.kotlin.diagnostics.PsiDiagnosticUtils
import org.jetbrains.kotlin.fir.builder.BodyBuildingMode
import org.jetbrains.kotlin.fir.builder.PsiRawFirBuilder
import org.jetbrains.kotlin.fir.declarations.FirFile
import org.jetbrains.kotlin.fir.java.FirProjectSessionProvider
import org.jetbrains.kotlin.fir.lightTree.LightTree2Fir
import org.jetbrains.kotlin.fir.resolve.providers.firProvider
import org.jetbrains.kotlin.fir.resolve.providers.impl.FirProviderImpl
import org.jetbrains.kotlin.fir.session.FirSessionFactoryHelper
import org.jetbrains.kotlin.fir.session.FirSessionConfigurator
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.AnalyzingUtils
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatformAnalyzerServices
import org.jetbrains.kotlin.toSourceLinesMapping
import java.io.File
abstract class AbstractFirBaseDiagnosticsTest : BaseDiagnosticsTest() {
override fun analyzeAndCheck(testDataFile: File, files: List<TestFile>) {
try {
analyzeAndCheckUnhandled(testDataFile, files, useLightTree)
} catch (t: AssertionError) {
throw t
} catch (t: Throwable) {
throw t
}
}
protected open val useLightTree: Boolean
get() = false
protected open val useLazyBodiesModeForRawFir: Boolean
get() = false
override fun setupEnvironment(environment: KotlinCoreEnvironment) {
PsiElementFinder.EP.getPoint(environment.project).unregisterExtension(JavaElementFinder::class.java)
}
open fun analyzeAndCheckUnhandled(testDataFile: File, files: List<TestFile>, useLightTree: Boolean = false) {
val groupedByModule = files.groupBy(TestFile::module)
val modules = createModules(groupedByModule)
val sessionProvider = FirProjectSessionProvider()
//For BuiltIns, registered in sessionProvider automatically
val allProjectScope = GlobalSearchScope.allScope(project)
val configToSession = modules.mapValues { (config, info) ->
val moduleFiles = groupedByModule.getValue(config)
val scope = TopDownAnalyzerFacadeForJVM.newModuleSearchScope(
project,
moduleFiles.mapNotNull { it.ktFile })
FirSessionFactoryHelper.createSessionWithDependencies(
Name.identifier(info.name.asString().removeSurrounding("<", ">")),
info.platform,
info.analyzerServices,
externalSessionProvider = sessionProvider,
VfsBasedProjectEnvironment(
project, VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL),
{ environment.createPackagePartProvider(it) }
),
config?.languageVersionSettings ?: LanguageVersionSettingsImpl.DEFAULT,
javaSourcesScope = PsiBasedProjectFileSearchScope(scope),
librariesScope = PsiBasedProjectFileSearchScope(allProjectScope),
lookupTracker = null,
enumWhenTracker = null,
importTracker = null,
incrementalCompilationContext = null,
extensionRegistrars = emptyList(),
needRegisterJavaElementFinder = true
) {
configureSession()
}
}
val firFilesPerSession = mutableMapOf<FirSession, List<FirFile>>()
// TODO: make module/session/transformer handling like in AbstractFirMultiModuleTest (IDE)
for ((testModule, testFilesInModule) in groupedByModule) {
val ktFiles = getKtFiles(testFilesInModule, true)
val session = configToSession.getValue(testModule)
val firFiles = mutableListOf<FirFile>()
mapKtFilesToFirFiles(session, ktFiles, firFiles, useLightTree)
firFilesPerSession[session] = firFiles
}
runAnalysis(testDataFile, files, firFilesPerSession)
}
private fun mapKtFilesToFirFiles(session: FirSession, ktFiles: List<KtFile>, firFiles: MutableList<FirFile>, useLightTree: Boolean) {
val firProvider = (session.firProvider as FirProviderImpl)
if (useLightTree) {
val lightTreeBuilder = LightTree2Fir(session, firProvider.kotlinScopeProvider)
ktFiles.mapTo(firFiles) {
val firFile =
lightTreeBuilder.buildFirFile(
it.text,
KtInMemoryTextSourceFile(it.name, it.virtualFilePath, it.text),
it.text.toSourceLinesMapping()
)
(session.firProvider as FirProviderImpl).recordFile(firFile)
firFile
}
} else {
val firBuilder = PsiRawFirBuilder(
session,
firProvider.kotlinScopeProvider,
bodyBuildingMode = BodyBuildingMode.lazyBodies(useLazyBodiesModeForRawFir)
)
ktFiles.mapTo(firFiles) {
val firFile = firBuilder.buildFirFile(it)
firProvider.recordFile(firFile)
firFile
}
}
}
protected abstract fun runAnalysis(testDataFile: File, testFiles: List<TestFile>, firFilesPerSession: Map<FirSession, List<FirFile>>)
private fun createModules(
groupedByModule: Map<TestModule?, List<TestFile>>
): MutableMap<TestModule?, ModuleInfo> {
val modules =
HashMap<TestModule?, ModuleInfo>()
for (testModule in groupedByModule.keys) {
val module = if (testModule == null)
createSealedModule()
else
createModule(testModule.name)
modules[testModule] = module
}
for (testModule in groupedByModule.keys) {
if (testModule == null) continue
val module = modules[testModule]!!
val dependencies = ArrayList<ModuleInfo>()
dependencies.add(module)
for (dependency in testModule.dependencies) {
dependencies.add(modules[dependency as TestModule?]!!)
}
dependencies.add(builtInsModuleInfo)
//dependencies.addAll(getAdditionalDependencies(module))
(module as TestModuleInfo).dependencies.addAll(dependencies)
}
return modules
}
private val builtInsModuleInfo = BuiltInModuleInfo(Name.special("<built-ins>"))
protected open fun createModule(moduleName: String): TestModuleInfo {
parseModulePlatformByName(moduleName)
return TestModuleInfo(Name.special("<$moduleName>"))
}
class BuiltInModuleInfo(override val name: Name) :
ModuleInfo {
override val platform: TargetPlatform
get() = JvmPlatforms.unspecifiedJvmPlatform
override val analyzerServices: PlatformDependentAnalyzerServices
get() = JvmPlatformAnalyzerServices
override fun dependencies(): List<ModuleInfo> {
return listOf(this)
}
}
protected class TestModuleInfo(override val name: Name) :
ModuleInfo {
override val platform: TargetPlatform
get() = JvmPlatforms.unspecifiedJvmPlatform
override val analyzerServices: PlatformDependentAnalyzerServices
get() = JvmPlatformAnalyzerServices
val dependencies = mutableListOf<ModuleInfo>(this)
override fun dependencies(): List<ModuleInfo> {
return dependencies
}
}
protected open fun createSealedModule(): TestModuleInfo =
createModule("test-module-jvm").apply {
dependencies += builtInsModuleInfo
}
protected fun TestFile.getActualText(
ktDiagnostics: Iterable<KtDiagnostic>,
actualText: StringBuilder
): Boolean {
val ktFile = this.ktFile
if (ktFile == null) {
// TODO: check java files too
actualText.append(this.clearText)
return true
}
if (ktFile.name.endsWith("CoroutineUtil.kt") && ktFile.packageFqName == FqName("helpers")) return true
// TODO: report JVM signature diagnostics also for implementing modules
val ok = booleanArrayOf(true)
val diagnostics = ktDiagnostics.toActualDiagnostic(ktFile)
val filteredDiagnostics = diagnostics // TODO
actualDiagnostics.addAll(filteredDiagnostics)
val uncheckedDiagnostics = mutableListOf<PositionalTextDiagnostic>()
val diagnosticToExpectedDiagnostic =
CheckerTestUtil.diagnosticsDiff(
diagnosedRanges,
filteredDiagnostics,
object : DiagnosticDiffCallbacks {
override fun missingDiagnostic(
diagnostic: TextDiagnostic,
expectedStart: Int,
expectedEnd: Int
) {
val message =
"Missing " + diagnostic.description + PsiDiagnosticUtils.atLocation(
ktFile,
TextRange(
expectedStart,
expectedEnd
)
)
System.err.println(message)
ok[0] = false
}
override fun wrongParametersDiagnostic(
expectedDiagnostic: TextDiagnostic,
actualDiagnostic: TextDiagnostic,
start: Int,
end: Int
) {
val message = "Parameters of diagnostic not equal at position " +
PsiDiagnosticUtils.atLocation(
ktFile,
TextRange(
start,
end
)
) +
". Expected: ${expectedDiagnostic.asString()}, actual: $actualDiagnostic"
System.err.println(message)
ok[0] = false
}
override fun unexpectedDiagnostic(
diagnostic: TextDiagnostic,
actualStart: Int,
actualEnd: Int
) {
val message =
"Unexpected ${diagnostic.description}${PsiDiagnosticUtils.atLocation(
ktFile,
TextRange(
actualStart,
actualEnd
)
)}"
System.err.println(message)
ok[0] = false
}
fun updateUncheckedDiagnostics(
diagnostic: TextDiagnostic,
start: Int,
end: Int
) {
uncheckedDiagnostics.add(
PositionalTextDiagnostic(
diagnostic,
start,
end
)
)
}
})
actualText.append(
CheckerTestUtil.addDiagnosticMarkersToText(
ktFile,
filteredDiagnostics,
diagnosticToExpectedDiagnostic,
{ file -> file.text },
uncheckedDiagnostics,
false,
false
)
)
stripExtras(actualText)
return ok[0]
}
private fun Iterable<KtDiagnostic>.toActualDiagnostic(root: PsiElement): List<ActualDiagnostic> {
val result = mutableListOf<ActualDiagnostic>()
filterIsInstance<Diagnostic>().mapTo(result) {
ActualDiagnostic(it, null, true)
}
for (errorElement in AnalyzingUtils.getSyntaxErrorRanges(root)) {
result.add(ActualDiagnostic(SyntaxErrorDiagnostic(errorElement), null, true))
}
return result
}
protected open fun FirSessionConfigurator.configureSession() {}
}
@@ -1,339 +0,0 @@
/*
* Copyright 2010-2022 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.fir
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.*
import org.jetbrains.kotlin.checkers.diagnostics.factories.DebugInfoDiagnosticFactory1
import org.jetbrains.kotlin.checkers.utils.TypeOfCall
import org.jetbrains.kotlin.diagnostics.*
import org.jetbrains.kotlin.diagnostics.rendering.Renderers
import org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector
import org.jetbrains.kotlin.fir.analysis.collectors.FirDiagnosticsCollector
import org.jetbrains.kotlin.fir.declarations.FirCallableDeclaration
import org.jetbrains.kotlin.fir.declarations.FirFile
import org.jetbrains.kotlin.fir.declarations.FirFunction
import org.jetbrains.kotlin.fir.declarations.FirProperty
import org.jetbrains.kotlin.fir.expressions.*
import org.jetbrains.kotlin.fir.references.FirNamedReference
import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference
import org.jetbrains.kotlin.fir.renderer.FirRenderer
import org.jetbrains.kotlin.fir.resolve.ScopeSession
import org.jetbrains.kotlin.fir.resolve.dfa.cfg.FirControlFlowGraphRenderVisitor
import org.jetbrains.kotlin.fir.resolve.transformers.createAllCompilerResolveProcessors
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
import org.jetbrains.kotlin.fir.symbols.SymbolInternals
import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirFunctionSymbol
import org.jetbrains.kotlin.fir.types.ConeKotlinType
import org.jetbrains.kotlin.fir.types.coneTypeOrNull
import org.jetbrains.kotlin.fir.visitors.FirDefaultVisitorVoid
import org.jetbrains.kotlin.name.FqNameUnsafe
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.jetbrains.kotlin.test.util.JUnit4Assertions
import org.jetbrains.kotlin.util.OperatorNameConventions
import org.jetbrains.kotlin.utils.addIfNotNull
import java.io.File
/*
* For comfort viewing dumps of control flow graph you can setup external tool in IDEA that opens .dot files
*
* Example of config for `xdot` viewer:
*
* File -> Settings -> External tools -> Add
*
* Name: XDot
* Program: xdot
* Arguments: $FileNameWithoutExtension$.dot
* Working directory: $FileDir$
* Disable "Open console for tool output"
*
* After that you can run action `XDot` in editor with source of test (or with cfg dump)
* and it will opens xdot with dump for that test
*/
@OptIn(SymbolInternals::class)
abstract class AbstractKtDiagnosticsTest : AbstractFirBaseDiagnosticsTest() {
companion object {
const val DUMP_CFG_DIRECTIVE = "DUMP_CFG"
private val allowedKindsForDebugInfo = setOf(
KtRealSourceElementKind,
KtFakeSourceElementKind.DesugaredCompoundAssignment,
)
val TestFile.withDumpCfgDirective: Boolean
get() = DUMP_CFG_DIRECTIVE in directives
val File.cfgDumpFile: File
get() = File(absolutePath.replace(".kt", ".dot"))
}
override fun runAnalysis(testDataFile: File, testFiles: List<TestFile>, firFilesPerSession: Map<FirSession, List<FirFile>>) {
for ((session, firFiles) in firFilesPerSession) {
doFirResolveTestBench(
firFiles,
createAllCompilerResolveProcessors(session),
gc = false
)
}
val allFirFiles = firFilesPerSession.values.flatten()
checkDiagnostics(testDataFile, testFiles, allFirFiles)
checkFir(testDataFile, allFirFiles)
checkCfg(allFirFiles, testFiles, testDataFile)
}
protected fun checkCfg(
allFirFiles: List<FirFile>,
testFiles: List<TestFile>,
testDataFile: File
) {
checkCfgEdgeConsistency(allFirFiles)
if (testFiles.any { it.withDumpCfgDirective }) {
checkCfgDump(testDataFile, allFirFiles)
} else {
checkCfgDumpNotExists(testDataFile)
}
}
fun checkFir(testDataFile: File, firFiles: List<FirFile>) {
val renderer = FirRenderer()
firFiles.forEach { renderer.renderElementAsString(it) }
val firFileDump = renderer.toString()
val expectedPath = testDataFile.absolutePath.replace(".kt", ".txt")
KotlinTestUtils.assertEqualsToFile(
File(expectedPath),
firFileDump
)
}
protected open fun checkDiagnostics(file: File, testFiles: List<TestFile>, firFiles: List<FirFile>) {
val diagnostics = collectDiagnostics(firFiles)
val actualTextBuilder = StringBuilder()
for (testFile in testFiles) {
val firFile = firFiles.firstOrNull { it.psi == testFile.ktFile }
if (firFile != null) {
val debugInfoDiagnostics: List<KtDiagnostic> =
collectDebugInfoDiagnostics(firFile, testFile.diagnosedRangesToDiagnosticNames)
testFile.getActualText(
diagnostics.getValue(firFile) + debugInfoDiagnostics,
actualTextBuilder,
)
} else {
actualTextBuilder.append(testFile.expectedText)
}
}
val actualText = actualTextBuilder.toString()
KotlinTestUtils.assertEqualsToFile(file, actualText)
}
private fun collectDebugInfoDiagnostics(
firFile: FirFile,
diagnosedRangesToDiagnosticNames: MutableMap<IntRange, MutableSet<String>>
): List<KtDiagnostic> {
val result = mutableListOf<KtDiagnostic>()
object : FirDefaultVisitorVoid() {
override fun visitElement(element: FirElement) {
if (element is FirExpression) {
result.addIfNotNull(
createExpressionTypeDiagnosticIfExpected(
element, diagnosedRangesToDiagnosticNames
)
)
}
element.acceptChildren(this)
}
override fun visitFunctionCall(functionCall: FirFunctionCall) {
val reference = functionCall.calleeReference
result.addIfNotNull(createCallDiagnosticIfExpected(functionCall, reference, diagnosedRangesToDiagnosticNames))
result.addIfNotNull(createDerivedClassDiagnosticIfExpected(functionCall, reference, diagnosedRangesToDiagnosticNames))
super.visitFunctionCall(functionCall)
}
override fun visitDelegatedConstructorCall(delegatedConstructorCall: FirDelegatedConstructorCall) {
val reference = delegatedConstructorCall.calleeReference as FirNamedReference
result.addIfNotNull(
createDerivedClassDiagnosticIfExpected(delegatedConstructorCall, reference, diagnosedRangesToDiagnosticNames))
super.visitDelegatedConstructorCall(delegatedConstructorCall)
}
override fun visitPropertyAccessExpression(propertyAccessExpression: FirPropertyAccessExpression) {
val reference = propertyAccessExpression.calleeReference as FirNamedReference
result.addIfNotNull(
createDerivedClassDiagnosticIfExpected(propertyAccessExpression, reference, diagnosedRangesToDiagnosticNames))
}
}.let(firFile::accept)
return result
}
fun createExpressionTypeDiagnosticIfExpected(
element: FirExpression,
diagnosedRangesToDiagnosticNames: MutableMap<IntRange, MutableSet<String>>
): KtDiagnosticWithParameters1<String>? =
DebugInfoDiagnosticFactory1.EXPRESSION_TYPE.createDebugInfoDiagnostic(element, diagnosedRangesToDiagnosticNames) {
element.coneTypeOrNull.renderAsString((element as? FirSmartCastExpression)?.originalExpression?.coneTypeOrNull)
}
private fun ConeKotlinType?.renderAsString(originalType: ConeKotlinType?): String {
val type = this ?: return "Type is unknown"
val rendered = type.renderForDebugInfo()
val originalTypeRendered = originalType?.renderForDebugInfo() ?: return rendered
return "$rendered & $originalTypeRendered"
}
private fun createCallDiagnosticIfExpected(
element: FirElement,
reference: FirNamedReference,
diagnosedRangesToDiagnosticNames: MutableMap<IntRange, MutableSet<String>>
): KtDiagnostic? {
return DebugInfoDiagnosticFactory1.CALL.createDebugInfoDiagnostic(element, diagnosedRangesToDiagnosticNames) {
val resolvedSymbol = (reference as? FirResolvedNamedReference)?.resolvedSymbol
val fqName = resolvedSymbol?.fqNameUnsafe()
Renderers.renderCallInfo(fqName, getTypeOfCall(reference, resolvedSymbol))
}
}
private fun createDerivedClassDiagnosticIfExpected(
element: FirElement,
reference: FirNamedReference,
diagnosedRangesToDiagnosticNames: MutableMap<IntRange, MutableSet<String>>
): KtDiagnostic? {
return DebugInfoDiagnosticFactory1.CALLABLE_OWNER.createDebugInfoDiagnostic(element, diagnosedRangesToDiagnosticNames) {
val resolvedSymbol = (reference as? FirResolvedNamedReference)?.resolvedSymbol
val callable = resolvedSymbol?.fir as? FirCallableDeclaration ?: return@createDebugInfoDiagnostic ""
DebugInfoDiagnosticFactory1.renderCallableOwner(
callable.symbol.callableId,
callable.containingClassLookupTag()?.classId,
callable.containingClassForStaticMemberAttr == null
)
}
}
private fun DebugInfoDiagnosticFactory1.createDebugInfoDiagnostic(
element: FirElement,
diagnosedRangesToDiagnosticNames: MutableMap<IntRange, MutableSet<String>>,
argument: () -> String,
): KtDiagnosticWithParameters1<String>? {
val sourceElement = element.source ?: return null
val sourceKind = sourceElement.kind
if (sourceKind !in allowedKindsForDebugInfo) {
if (sourceKind !is KtFakeSourceElementKind.ImplicitReturn || sourceElement.elementType != KtNodeTypes.RETURN) {
return null
}
}
// Lambda argument is always (?) duplicated by function literal
// Block expression is always (?) duplicated by single block expression
if (sourceElement.elementType == KtNodeTypes.LAMBDA_ARGUMENT || sourceElement.elementType == KtNodeTypes.BLOCK) return null
val name = name
if (diagnosedRangesToDiagnosticNames[sourceElement.startOffset..sourceElement.endOffset]?.contains(name) != true) return null
val argumentText = argument()
return when (sourceElement) {
is KtPsiSourceElement -> KtPsiDiagnosticWithParameters1(
sourceElement,
argumentText,
severity,
KtDiagnosticFactory1(name, severity, AbstractSourceElementPositioningStrategy.DEFAULT, PsiElement::class),
AbstractSourceElementPositioningStrategy.DEFAULT
)
is KtLightSourceElement -> KtLightDiagnosticWithParameters1(
sourceElement,
argumentText,
severity,
KtDiagnosticFactory1(name, severity, AbstractSourceElementPositioningStrategy.DEFAULT, PsiElement::class),
AbstractSourceElementPositioningStrategy.DEFAULT
)
}
}
private fun FirBasedSymbol<*>.fqNameUnsafe(): FqNameUnsafe? = when (this) {
is FirClassLikeSymbol<*> -> classId.asSingleFqName().toUnsafe()
is FirCallableSymbol<*> -> callableId.asFqNameForDebugInfo().toUnsafe()
else -> null
}
private fun getTypeOfCall(
reference: FirNamedReference,
resolvedSymbol: FirBasedSymbol<*>?
): String {
if (resolvedSymbol == null) return TypeOfCall.UNRESOLVED.nameToRender
if ((resolvedSymbol as? FirFunctionSymbol)?.callableId?.callableName == OperatorNameConventions.INVOKE
&& reference.name != OperatorNameConventions.INVOKE
) {
return TypeOfCall.VARIABLE_THROUGH_INVOKE.nameToRender
}
return when (val fir = resolvedSymbol.fir) {
is FirProperty -> {
TypeOfCall.PROPERTY_GETTER.nameToRender
}
is FirFunction -> buildString {
if (fir.status.isInline) append("inline ")
if (fir.status.isInfix) append("infix ")
if (fir.status.isOperator) append("operator ")
if (fir.receiverParameter != null) append("extension ")
append(TypeOfCall.FUNCTION.nameToRender)
}
else -> TypeOfCall.OTHER.nameToRender
}
}
protected fun collectDiagnostics(firFiles: List<FirFile>): Map<FirFile, List<KtDiagnostic>> {
val collectors = mutableMapOf<FirSession, AbstractDiagnosticCollector>()
val result = mutableMapOf<FirFile, List<KtDiagnostic>>()
for (firFile in firFiles) {
val session = firFile.moduleData.session
val collector = collectors.computeIfAbsent(session) { createCollector(session) }
val reporter = DiagnosticReporterFactory.createPendingReporter()
collector.collectDiagnostics(firFile, reporter)
result[firFile] = reporter.diagnostics
}
return result
}
private fun createCollector(session: FirSession): AbstractDiagnosticCollector {
return FirDiagnosticsCollector.create(
session,
ScopeSession()
) // seems this class is obsolete, so do not care about correctness of the scope session here
}
private fun checkCfgDump(testDataFile: File, firFiles: List<FirFile>) {
val builder = StringBuilder()
firFiles.first().accept(FirControlFlowGraphRenderVisitor(builder), null)
val dotCfgDump = builder.toString()
KotlinTestUtils.assertEqualsToFile(testDataFile.cfgDumpFile, dotCfgDump)
}
private fun checkCfgEdgeConsistency(firFiles: List<FirFile>) {
firFiles.forEach { it.accept(FirCfgConsistencyChecker(JUnit4Assertions)) }
}
private fun checkCfgDumpNotExists(testDataFile: File) {
val cfgDumpFile = testDataFile.cfgDumpFile
if (cfgDumpFile.exists()) {
val message = """
Directive `!$DUMP_CFG_DIRECTIVE` is missing, but file with cfg dump is present.
Please remove ${cfgDumpFile.path} or add `!$DUMP_CFG_DIRECTIVE` to test
""".trimIndent()
kotlin.test.fail(message)
}
}
}
@@ -1,76 +0,0 @@
/*
* Copyright 2010-2019 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.fir
import org.jetbrains.kotlin.fir.declarations.FirFile
import org.jetbrains.kotlin.fir.resolve.transformers.createAllCompilerResolveProcessors
import org.jetbrains.kotlin.test.KotlinTestUtils
import java.io.File
abstract class AbstractFirOldFrontendDiagnosticsTest : AbstractKtDiagnosticsTest() {
override fun createTestFileFromPath(filePath: String): File {
val newPath = if (File(filePath).readText().contains("// FIR_IDENTICAL")) filePath else filePath.replace(".kt", ".fir.kt")
return File(newPath).also {
prepareTestDataFile(filePath, it)
}
}
private fun prepareTestDataFile(originalFilePath: String, firTestDataFile: File) {
if (!firTestDataFile.exists()) {
KotlinTestUtils.assertEqualsToFile(firTestDataFile, loadTestDataWithDiagnostics(File(originalFilePath)))
}
}
override fun analyzeAndCheck(testDataFile: File, files: List<TestFile>) {
if (files.any { "FIR_IGNORE" in it.directives }) return
super.analyzeAndCheck(testDataFile, files)
}
override fun runAnalysis(testDataFile: File, testFiles: List<TestFile>, firFilesPerSession: Map<FirSession, List<FirFile>>) {
val failure: FirRuntimeException? = try {
for ((session, firFiles) in firFilesPerSession) {
doFirResolveTestBench(firFiles, createAllCompilerResolveProcessors(session), gc = false)
}
null
} catch (e: FirRuntimeException) {
e
}
val failureFile = File(testDataFile.path.replace(".kt", ".fail"))
if (failure == null) {
val allFirFiles = firFilesPerSession.values.flatten()
checkResultingFirFiles(allFirFiles, testDataFile)
assertFalse("Test is good but there is expected exception", failureFile.exists())
checkDiagnostics(testDataFile, testFiles, allFirFiles)
if (testDataFile.absolutePath.endsWith(".fir.kt")) {
val oldFrontendTestDataFile = File(testDataFile.absolutePath.replace(".fir.kt", ".kt"))
compareAndMergeFirFileAndOldFrontendFile(oldFrontendTestDataFile, testDataFile)
}
val needDump = testFiles.any { "FIR_DUMP" in it.directives }
if (needDump) {
checkFir(testDataFile, allFirFiles)
}
checkCfg(allFirFiles, testFiles, testDataFile)
} else {
if (!failureFile.exists()) {
throw failure
}
checkFailureFile(failure, failureFile)
}
}
private fun checkFailureFile(failure: FirRuntimeException, failureFile: File) {
val failureMessage = buildString {
appendLine(failure.message)
append("Cause: ")
appendLine(failure.cause)
}
KotlinTestUtils.assertEqualsToFile(failureFile, failureMessage)
}
protected open fun checkResultingFirFiles(firFiles: List<FirFile>, testDataFile: File) {}
}
@@ -15,10 +15,10 @@ import org.jetbrains.kotlin.codegen.*
import org.jetbrains.kotlin.codegen.defaultConstructor.AbstractDefaultArgumentsReflectionTest
import org.jetbrains.kotlin.codegen.flags.AbstractWriteFlagsTest
import org.jetbrains.kotlin.codegen.ir.*
import org.jetbrains.kotlin.fir.java.AbstractFirOldFrontendLightClassesTest
import org.jetbrains.kotlin.fir.builder.AbstractRawFirBuilderLazyBodiesTestCase
import org.jetbrains.kotlin.fir.builder.AbstractRawFirBuilderSourceElementMappingTestCase
import org.jetbrains.kotlin.fir.builder.AbstractRawFirBuilderTestCase
import org.jetbrains.kotlin.fir.java.AbstractFirOldFrontendLightClassesTest
import org.jetbrains.kotlin.fir.java.AbstractFirTypeEnhancementTest
import org.jetbrains.kotlin.fir.java.AbstractOwnFirTypeEnhancementTest
import org.jetbrains.kotlin.fir.lightTree.AbstractLightTree2FirConverterTestCase