FIR IDE: add test which check that every declaration is visited single time during diagnostic collection

This commit is contained in:
Ilya Kirillov
2021-02-26 17:51:13 +01:00
parent 0b921ed9d8
commit 70c98d34d2
22 changed files with 353 additions and 10 deletions
@@ -68,13 +68,15 @@ abstract class AbstractDiagnosticCollector(
componentsInitialized = true
}
protected open fun beforeCollecting() {}
protected open fun beforeRunningAllComponentsOnElement(element: FirElement) {}
protected open fun beforeRunningSingleComponentOnElement(element: FirElement) {}
private inner class Visitor : FirDefaultVisitor<Unit, Nothing?>() {
private fun <T : FirElement> T.runComponents() {
if (currentAction.checkInCurrentDeclaration) {
beforeRunningAllComponentsOnElement(this)
components.forEach {
beforeCollecting()
beforeRunningSingleComponentOnElement(this)
this.accept(it, context)
}
}
@@ -83,6 +83,7 @@ import org.jetbrains.kotlin.idea.fir.low.level.api.AbstractFirLazyDeclarationRes
import org.jetbrains.kotlin.idea.fir.low.level.api.AbstractFirLazyResolveTest
import org.jetbrains.kotlin.idea.fir.low.level.api.AbstractFirMultiModuleLazyResolveTest
import org.jetbrains.kotlin.idea.fir.low.level.api.AbstractFirMultiModuleResolveTest
import org.jetbrains.kotlin.idea.fir.low.level.api.diagnostic.AbstractDiagnosticTraversalCounterTest
import org.jetbrains.kotlin.idea.fir.low.level.api.file.structure.AbstractFileStructureAndOutOfBlockModificationTrackerConsistencyTest
import org.jetbrains.kotlin.idea.fir.low.level.api.file.structure.AbstractFileStructureTest
import org.jetbrains.kotlin.idea.fir.low.level.api.sessions.AbstractSessionsInvalidationTest
@@ -1073,6 +1074,9 @@ fun main(args: Array<String>) {
testClass<AbstractFileStructureTest> {
model("fileStructure")
}
testClass<AbstractDiagnosticTraversalCounterTest> {
model("diagnosticTraversalCounter")
}
testClass<AbstractSessionsInvalidationTest> {
model("sessionInvalidation", recursive = false, extension = null)
}
@@ -13,8 +13,8 @@ import org.jetbrains.kotlin.idea.caches.project.IdeaModuleInfo
import org.jetbrains.kotlin.idea.caches.project.ModuleSourceInfo
import org.jetbrains.kotlin.idea.fir.low.level.api.api.FirModuleResolveState
import org.jetbrains.kotlin.idea.fir.low.level.api.lazy.resolve.FirLazyDeclarationResolver
import org.jetbrains.kotlin.idea.fir.low.level.api.sessions.FirIdeSession
import org.jetbrains.kotlin.idea.fir.low.level.api.sessions.FirIdeSessionProviderStorage
import org.jetbrains.kotlin.idea.fir.low.level.api.trackers.KotlinFirOutOfBlockModificationTrackerFactory
import org.jetbrains.kotlin.idea.util.cachedValue
import org.jetbrains.kotlin.idea.util.getValue
import org.jetbrains.kotlin.trackers.createProjectWideOutOfBlockModificationTracker
@@ -39,12 +39,13 @@ internal class FirIdeResolveStateService(project: Project) {
internal fun createResolveStateFor(
moduleInfo: IdeaModuleInfo,
sessionProviderStorage: FirIdeSessionProviderStorage
sessionProviderStorage: FirIdeSessionProviderStorage,
configureSession: (FirIdeSession.() -> Unit)? = null,
): FirModuleResolveStateImpl {
if (moduleInfo !is ModuleSourceInfo) {
error("Creating FirModuleResolveState is not yet supported for $moduleInfo")
}
val sessionProvider = sessionProviderStorage.getSessionProvider(moduleInfo)
val sessionProvider = sessionProviderStorage.getSessionProvider(moduleInfo, configureSession)
val firFileBuilder = sessionProvider.rootModuleSession.firFileBuilder
return FirModuleResolveStateImpl(
moduleInfo.project,
@@ -60,6 +61,12 @@ internal class FirIdeResolveStateService(project: Project) {
@TestOnly
fun createResolveStateForNoCaching(
moduleInfo: IdeaModuleInfo,
): FirModuleResolveState = FirIdeResolveStateService.createResolveStateFor(moduleInfo, FirIdeSessionProviderStorage(moduleInfo.project!!))
configureSession: (FirIdeSession.() -> Unit)? = null,
): FirModuleResolveState =
FirIdeResolveStateService.createResolveStateFor(
moduleInfo = moduleInfo,
sessionProviderStorage = FirIdeSessionProviderStorage(moduleInfo.project!!),
configureSession = configureSession
)
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.idea.fir.low.level.api.diagnostics
import org.jetbrains.kotlin.fir.FirElement
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.SessionConfiguration
import org.jetbrains.kotlin.fir.analysis.CheckersComponent
@@ -41,6 +42,9 @@ internal abstract class AbstractFirIdeDiagnosticsCollector(
::FirIdeDesignatedBodyResolveTransformerForReturnTypeCalculator
)
) {
private val beforeElementDiagnosticCollectionHandler: BeforeElementDiagnosticCollectionHandler? =
session.beforeElementDiagnosticCollectionHandler
init {
val declarationCheckers = CheckersFactory.createDeclarationCheckers(useExtendedCheckers)
val expressionCheckers = CheckersFactory.createExpressionCheckers(useExtendedCheckers)
@@ -70,10 +74,14 @@ internal abstract class AbstractFirIdeDiagnosticsCollector(
reporter = Reporter()
}
override fun beforeCollecting() {
override fun beforeRunningSingleComponentOnElement(element: FirElement) {
checkCanceled()
}
override fun beforeRunningAllComponentsOnElement(element: FirElement) {
beforeElementDiagnosticCollectionHandler?.beforeCollectingForElement(element)
}
override fun getCollectedDiagnostics(): List<FirDiagnostic<*>> {
// Not necessary in IDE
return emptyList()
@@ -0,0 +1,16 @@
/*
* 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.idea.fir.low.level.api.diagnostics
import org.jetbrains.kotlin.fir.FirElement
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.FirSessionComponent
abstract class BeforeElementDiagnosticCollectionHandler: FirSessionComponent {
abstract fun beforeCollectingForElement(element: FirElement)
}
val FirSession.beforeElementDiagnosticCollectionHandler: BeforeElementDiagnosticCollectionHandler? by FirSession.nullableSessionComponentAccessor()
@@ -58,6 +58,7 @@ internal object FirIdeSessionFactory {
isRootModule: Boolean,
librariesCache: LibrariesCache,
languageVersionSettings: LanguageVersionSettings = LanguageVersionSettingsImpl.DEFAULT,
configureSession: (FirIdeSession.() -> Unit)? = null
): FirIdeSourcesSession {
sessionsCache[moduleInfo]?.let { return it }
val scopeProvider = KotlinScopeProvider(::wrapScopeWithJvmMapped)
@@ -108,7 +109,8 @@ internal object FirIdeSessionFactory {
project,
builtinsAndCloneableSession,
builtinTypes,
librariesCache
librariesCache,
configureSession = configureSession,
).symbolProvider
)
dependentModules
@@ -123,6 +125,7 @@ internal object FirIdeSessionFactory {
sessionsCache,
isRootModule = false,
librariesCache,
configureSession = configureSession,
).symbolProvider
}
}
@@ -137,6 +140,8 @@ internal object FirIdeSessionFactory {
registerExtendedCommonCheckers()
}
}.configure()
configureSession?.invoke(this)
}
}
@@ -147,6 +152,7 @@ internal object FirIdeSessionFactory {
builtinTypes: BuiltinTypes,
librariesCache: LibrariesCache,
languageVersionSettings: LanguageVersionSettings = LanguageVersionSettingsImpl.DEFAULT,
configureSession: (FirIdeSession.() -> Unit)?,
): FirIdeLibrariesSession = librariesCache.cached(moduleInfo) {
checkCanceled()
val searchScope = ModuleLibrariesSearchScope(moduleInfo.module)
@@ -191,13 +197,15 @@ internal object FirIdeSessionFactory {
)
)
register(FirJvmTypeMapper::class, FirJvmTypeMapper(this))
configureSession?.invoke(this)
}
}
fun createBuiltinsAndCloneableSession(
project: Project,
builtinTypes: BuiltinTypes,
languageVersionSettings: LanguageVersionSettings = LanguageVersionSettingsImpl.DEFAULT
languageVersionSettings: LanguageVersionSettings = LanguageVersionSettingsImpl.DEFAULT,
configureSession: (FirIdeSession.() -> Unit)? = null,
): FirIdeBuiltinsAndCloneableSession {
return FirIdeBuiltinsAndCloneableSession(project, builtinTypes).apply {
registerIdeComponents()
@@ -215,6 +223,7 @@ internal object FirIdeSessionFactory {
)
)
register(FirJvmTypeMapper::class, FirJvmTypeMapper(this))
configureSession?.invoke(this)
}
}
@@ -26,7 +26,10 @@ internal class FirIdeSessionProviderStorage(private val project: Project) {
private val librariesCache by cachedValue(project, LibraryModificationTracker.getInstance(project)) { LibrariesCache() }
fun getSessionProvider(rootModule: ModuleSourceInfo): FirIdeSessionProvider {
fun getSessionProvider(
rootModule: ModuleSourceInfo,
configureSession: (FirIdeSession.() -> Unit)? = null
): FirIdeSessionProvider {
val firPhaseRunner = FirPhaseRunner()
val builtinTypes = BuiltinTypes()
@@ -45,6 +48,7 @@ internal class FirIdeSessionProviderStorage(private val project: Project) {
sessions,
isRootModule = true,
librariesCache,
configureSession = configureSession,
)
}
sessions to session
@@ -0,0 +1,3 @@
fun foo(f: Float.(Int, String) -> Boolean) {
}
@@ -0,0 +1,3 @@
class A(val x: Int = 10, val b: String) {
}
@@ -0,0 +1,3 @@
fun foo() {
fun local() = 0
}
@@ -0,0 +1,5 @@
fun foo() {
fun local() {
println("local")
}
}
@@ -0,0 +1,7 @@
class A {
fun foo1() = 10
fun foo2() {
}
}
@@ -0,0 +1,7 @@
class A {
fun foo1() = 10
fun foo2() {
}
}
@@ -0,0 +1,15 @@
class A {
}
class B {
}
class C {
}
class D {
}
@@ -0,0 +1,7 @@
fun foo1() = 1
fun foo2() = 2
fun foo3() = 3
fun foo4() = 4
@@ -0,0 +1,15 @@
fun foo1() {
println("foo1")
}
fun foo2() {
println("foo2")
}
fun foo3() {
println("foo3")
}
fun foo4() {
println("foo4")
}
@@ -0,0 +1,11 @@
class A {
class B {
}
object C {
class D {
}
}
}
@@ -0,0 +1,5 @@
var withGetterAndSetter: Int = 42
get() = field
set(value) {
field = value
}
@@ -0,0 +1,3 @@
class A(val x: Int = 10, val b: String) {
constructor(i: Int) : this(x = 1, b = i.toString())
}
@@ -0,0 +1,112 @@
/*
* 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.idea.fir.low.level.api.diagnostic
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.fir.FirElement
import org.jetbrains.kotlin.fir.SessionConfiguration
import org.jetbrains.kotlin.fir.visitors.FirVisitorVoid
import org.jetbrains.kotlin.idea.caches.project.getModuleInfo
import org.jetbrains.kotlin.idea.fir.low.level.api.api.DiagnosticCheckerFilter
import org.jetbrains.kotlin.idea.fir.low.level.api.api.collectDiagnosticsForFile
import org.jetbrains.kotlin.idea.fir.low.level.api.api.getOrBuildFir
import org.jetbrains.kotlin.idea.fir.low.level.api.createResolveStateForNoCaching
import org.jetbrains.kotlin.idea.fir.low.level.api.diagnostics.BeforeElementDiagnosticCollectionHandler
import org.jetbrains.kotlin.idea.fir.low.level.api.renderWithClassName
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.psi.KtFile
import java.io.File
/**
* Check that every declaration is visited exactly one time during diagnostic collection
*/
abstract class AbstractDiagnosticTraversalCounterTest : KotlinLightCodeInsightFixtureTestCase() {
override fun isFirPlugin(): Boolean = true
fun doTest(path: String) {
val testDataFile = File(path)
val ktFile = myFixture.configureByText(testDataFile.name, FileUtil.loadFile(testDataFile)) as KtFile
val handler = BeforeElementTestDiagnosticCollectionHandler()
@OptIn(SessionConfiguration::class)
val resolveState = createResolveStateForNoCaching(ktFile.getModuleInfo()) {
register(BeforeElementDiagnosticCollectionHandler::class, handler)
}
// we should get diagnostics before we resolve the whole file by ktFile.getOrBuildFir
ktFile.collectDiagnosticsForFile(resolveState, DiagnosticCheckerFilter.ONLY_COMMON_CHECKERS)
val firFile = ktFile.getOrBuildFir(resolveState)
val errorElements = collectErrorElements(firFile, handler)
if (errorElements.isNotEmpty()) {
val zeroElements = errorElements.filter { it.second == 0 }
val nonZeroElements = errorElements.filter { it.second > 1 }
val message = buildString {
if (zeroElements.isNotEmpty()) {
appendLine(
""" |The following elements were not visited
|${zeroElements.joinToString(separator = "\n\n") { it.first.renderWithClassName() }}
""".trimMargin()
)
}
if (nonZeroElements.isNotEmpty()) {
appendLine(
""" |The following elements were visited more than one time
|${nonZeroElements.joinToString(separator = "\n\n") { it.second.toString() + " times " + it.first.renderWithClassName() }}
""".trimMargin()
)
}
}
fail(message)
}
}
private fun collectErrorElements(
firFile: FirElement,
handler: BeforeElementTestDiagnosticCollectionHandler
): List<Pair<FirElement, Int>> {
val errorElements = mutableListOf<Pair<FirElement, Int>>()
val nonDuplicatingElements = findNonDuplicatingFirElements(firFile)
firFile.accept(object : FirVisitorVoid() {
override fun visitElement(element: FirElement) {
if (element !in nonDuplicatingElements) return
val visitedTimes = handler.visitedTimes[element] ?: 0
if (visitedTimes != 1) {
errorElements += element to visitedTimes
}
element.acceptChildren(this)
}
})
return errorElements
}
private fun findNonDuplicatingFirElements(
firFile: FirElement,
): Set<FirElement> {
val elementUsageCount = mutableMapOf<FirElement, Int>()
firFile.accept(object : FirVisitorVoid() {
override fun visitElement(element: FirElement) {
elementUsageCount.compute(element) { _, count -> (count ?: 0) + 1 }
}
})
return elementUsageCount.filterValues { it == 1 }.keys
}
class BeforeElementTestDiagnosticCollectionHandler : BeforeElementDiagnosticCollectionHandler() {
val visitedTimes = mutableMapOf<FirElement, Int>()
override fun beforeCollectingForElement(element: FirElement) {
if (!visitedTimes.containsKey(element)) {
visitedTimes[element] = 1
} else {
visitedTimes.compute(element) { _, count -> (count ?: 0) + 1 }
}
}
}
}
@@ -0,0 +1,91 @@
/*
* 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.idea.fir.low.level.api.diagnostic;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class DiagnosticTraversalCounterTestGenerated extends AbstractDiagnosticTraversalCounterTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
public void testAllFilesPresentInDiagnosticTraversalCounter() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
@TestMetadata("constructor.kt")
public void testConstructor() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/constructor.kt");
}
@TestMetadata("functionalType.kt")
public void testFunctionalType() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/functionalType.kt");
}
@TestMetadata("localFunctionWithImplicitType.kt")
public void testLocalFunctionWithImplicitType() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/localFunctionWithImplicitType.kt");
}
@TestMetadata("localUnitFunction.kt")
public void testLocalUnitFunction() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/localUnitFunction.kt");
}
@TestMetadata("memberFunctions.kt")
public void testMemberFunctions() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/memberFunctions.kt");
}
@TestMetadata("memberProperties.kt")
public void testMemberProperties() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/memberProperties.kt");
}
@TestMetadata("multipleTopLevelClasses.kt")
public void testMultipleTopLevelClasses() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/multipleTopLevelClasses.kt");
}
@TestMetadata("multipleTopLevelFunctionsWithImplicitTypes.kt")
public void testMultipleTopLevelFunctionsWithImplicitTypes() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/multipleTopLevelFunctionsWithImplicitTypes.kt");
}
@TestMetadata("multipleTopLevelUnitFunctions.kt")
public void testMultipleTopLevelUnitFunctions() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/multipleTopLevelUnitFunctions.kt");
}
@TestMetadata("nestedClases.kt")
public void testNestedClases() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/nestedClases.kt");
}
@TestMetadata("propertyWithGetterAndSetter.kt")
public void testPropertyWithGetterAndSetter() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/propertyWithGetterAndSetter.kt");
}
@TestMetadata("secondaryConstructor.kt")
public void testSecondaryConstructor() throws Exception {
runTest("idea/idea-frontend-fir/idea-fir-low-level-api/testdata/diagnosticTraversalCounter/secondaryConstructor.kt");
}
}
@@ -9,6 +9,9 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.fir.FirElement
import org.jetbrains.kotlin.fir.FirRenderer
import org.jetbrains.kotlin.fir.render
import org.jetbrains.kotlin.idea.caches.project.getModuleInfo
import org.jetbrains.kotlin.idea.fir.low.level.api.api.FirModuleResolveState
import org.jetbrains.kotlin.idea.fir.low.level.api.trackers.KotlinFirModificationTrackerService
@@ -21,6 +24,9 @@ inline fun resolveWithClearCaches(context: KtElement, action: (FirModuleResolveS
action(resolveState)
}
internal fun FirElement.renderWithClassName(renderMode: FirRenderer.RenderMode = FirRenderer.RenderMode.Normal): String =
"${this::class.simpleName} `${render(renderMode)}`"
internal fun Module.incModificationTracker() {
project.service<KotlinFirModificationTrackerService>().increaseModificationCountForModule(this)
}