From d04ed56a4db2e532baccf0fa50d3b0fca78eac1c Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Thu, 11 May 2023 22:00:48 +0900 Subject: [PATCH] [LL API] Add tests for FIR tree guards ^KT-59297 Fixed --- .../errorResistance/builderInference.kt | 12 + .../errorResistance/propertyDelegate.kt | 5 + .../errorResistance/propertyInitializer.kt | 7 + .../resolve/AbstractErrorResistanceTest.kt | 179 +++++++++++++ .../resolve/ErrorResistanceTestGenerated.java | 44 ++++ .../level/api/fir/resolve/StateKeeperTest.kt | 237 ++++++++++++++++++ .../AnalysisApiFirSourceTestConfigurator.kt | 2 +- .../tests/analysis/api/firLowLevel.kt | 5 + 8 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 analysis/low-level-api-fir/testdata/errorResistance/builderInference.kt create mode 100644 analysis/low-level-api-fir/testdata/errorResistance/propertyDelegate.kt create mode 100644 analysis/low-level-api-fir/testdata/errorResistance/propertyInitializer.kt create mode 100644 analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/AbstractErrorResistanceTest.kt create mode 100644 analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/ErrorResistanceTestGenerated.java create mode 100644 analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/StateKeeperTest.kt diff --git a/analysis/low-level-api-fir/testdata/errorResistance/builderInference.kt b/analysis/low-level-api-fir/testdata/errorResistance/builderInference.kt new file mode 100644 index 00000000000..6fe88f1fbc5 --- /dev/null +++ b/analysis/low-level-api-fir/testdata/errorResistance/builderInference.kt @@ -0,0 +1,12 @@ +// WITH_STDLIB + +import broken.lib.Foo + +fun test() { + buildList { + add(1) + add(Foo("foo", 1).result) + } +} + +fun consume(a: T) {} \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/errorResistance/propertyDelegate.kt b/analysis/low-level-api-fir/testdata/errorResistance/propertyDelegate.kt new file mode 100644 index 00000000000..d08f5d4cebb --- /dev/null +++ b/analysis/low-level-api-fir/testdata/errorResistance/propertyDelegate.kt @@ -0,0 +1,5 @@ +// WITH_STDLIB + +import broken.lib.Foo + +val x by lazy { Foo("foo", 1).result } \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/errorResistance/propertyInitializer.kt b/analysis/low-level-api-fir/testdata/errorResistance/propertyInitializer.kt new file mode 100644 index 00000000000..72ca1cd69be --- /dev/null +++ b/analysis/low-level-api-fir/testdata/errorResistance/propertyInitializer.kt @@ -0,0 +1,7 @@ +// WITH_STDLIB + +import broken.lib.Foo + +val x = run { + Foo("foo", 1).result +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/AbstractErrorResistanceTest.kt b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/AbstractErrorResistanceTest.kt new file mode 100644 index 00000000000..63ee5d3efe2 --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/AbstractErrorResistanceTest.kt @@ -0,0 +1,179 @@ +/* + * 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.analysis.low.level.api.fir.resolve + +import com.intellij.lang.java.JavaLanguage +import com.intellij.mock.MockApplication +import com.intellij.mock.MockProject +import com.intellij.openapi.project.Project +import com.intellij.psi.* +import com.intellij.psi.impl.file.PsiPackageImpl +import com.intellij.psi.impl.light.LightMethodBuilder +import com.intellij.psi.impl.light.LightModifierList +import com.intellij.psi.impl.light.LightPsiClassBase +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.PsiSearchScopeUtil +import org.jetbrains.kotlin.analysis.low.level.api.fir.api.DiagnosticCheckerFilter +import org.jetbrains.kotlin.analysis.low.level.api.fir.api.collectDiagnosticsForFile +import org.jetbrains.kotlin.analysis.low.level.api.fir.resolveWithClearCaches +import org.jetbrains.kotlin.analysis.low.level.api.fir.test.base.AbstractLowLevelApiSingleFileTest +import org.jetbrains.kotlin.analysis.low.level.api.fir.test.configurators.AnalysisApiFirSourceTestConfigurator +import org.jetbrains.kotlin.analysis.test.framework.test.configurators.AnalysisApiTestServiceRegistrar +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.test.TestInfrastructureInternals +import org.jetbrains.kotlin.test.impl.testConfiguration +import org.jetbrains.kotlin.test.services.TestModuleStructure +import org.jetbrains.kotlin.test.services.TestServices +import kotlin.test.fail + +abstract class AbstractErrorResistanceTest : AbstractLowLevelApiSingleFileTest() { + override val configurator: AnalysisApiFirSourceTestConfigurator = ErrorResistanceConfigurator + + override fun doTestByFileStructure(ktFile: KtFile, moduleStructure: TestModuleStructure, testServices: TestServices) { + resolveWithClearCaches(ktFile) { firResolveSession -> + ENABLE_INTERRUPTION.set(true) + + try { + ktFile.collectDiagnosticsForFile(firResolveSession, DiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) + fail("Analysis should be interrupted") + } catch (e: Throwable) { + val errors = generateSequence(e) { it.cause } + if (errors.none { it is AnalysisInterruptedException }) { + throw e + } + } + + ENABLE_INTERRUPTION.set(false) + + val diagnostics = ktFile.collectDiagnosticsForFile(firResolveSession, DiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) + assert(diagnostics.isEmpty()) { + val messages = diagnostics.map { it.factoryName } + "There should be no diagnostics, found:\n" + messages.joinToString("\n") + } + } + } +} + +private object ErrorResistanceConfigurator : AnalysisApiFirSourceTestConfigurator(analyseInDependentSession = false) { + override val serviceRegistrars: List + get() = buildList { + addAll(super.serviceRegistrars) + add(ErrorResistanceServiceRegistrar) + } +} + +private object ErrorResistanceServiceRegistrar : AnalysisApiTestServiceRegistrar() { + override fun registerApplicationServices(application: MockApplication, testServices: TestServices) {} + override fun registerProjectExtensionPoints(project: MockProject, testServices: TestServices) {} + override fun registerProjectServices(project: MockProject, testServices: TestServices) {} + + @OptIn(TestInfrastructureInternals::class) + override fun registerProjectModelServices(project: MockProject, testServices: TestServices) { + with(PsiElementFinder.EP.getPoint(project)) { + registerExtension(BrokenLibraryElementFinder(project), testServices.testConfiguration.rootDisposable) + } + } +} + +private class AnalysisInterruptedException : RuntimeException() + +private var ENABLE_INTERRUPTION = ThreadLocal.withInitial { false } + +private fun interruptAnalysis() { + if (ENABLE_INTERRUPTION.get()) { + throw AnalysisInterruptedException() + } +} + +private class BrokenLibraryElementFinder(project: Project) : PsiElementFinder() { + private val manager = PsiManager.getInstance(project) + private val brokenPackage = BrokenPackage("broken.lib", manager) + private val brokenClass = BrokenClass(brokenPackage.qualifiedName, "Foo", manager) + + override fun findPackage(qualifiedName: String): PsiPackage? { + return when (qualifiedName) { + brokenPackage.qualifiedName -> brokenPackage + else -> null + } + } + + override fun findClass(qualifiedName: String, scope: GlobalSearchScope): PsiClass? { + val klass = when (qualifiedName) { + brokenClass.qualifiedName -> brokenClass + else -> null + } + + return klass?.takeIf { PsiSearchScopeUtil.isInScope(scope, it) } + } + + override fun findClasses(qualifiedName: String, scope: GlobalSearchScope): Array { + val klass = findClass(qualifiedName, scope) ?: return PsiClass.EMPTY_ARRAY + return arrayOf(klass) + } +} + +private class BrokenPackage(packageName: String, manager: PsiManager) : PsiPackageImpl(manager, packageName) { + override fun isValid(): Boolean = true +} + +private class BrokenClass( + private val packageName: String, + name: String, + manager: PsiManager +) : LightPsiClassBase(manager, JavaLanguage.INSTANCE, name) { + private val modifierList: PsiModifierList = LightModifierList(manager, JavaLanguage.INSTANCE, PsiModifier.PUBLIC) + private val methods: Array = arrayOf(ConstructorMethod(this), GetterMethod(this)) + + override fun getQualifiedName(): String = "$packageName.$name" + override fun getModifierList(): PsiModifierList = modifierList + override fun getContainingClass(): PsiClass? = null + override fun getTypeParameterList(): PsiTypeParameterList? = null + + override fun getMethods(): Array { + return methods + } + + override fun getFields(): Array { + return PsiField.EMPTY_ARRAY + } + + override fun getInnerClasses(): Array = PsiClass.EMPTY_ARRAY + override fun getInitializers(): Array = PsiClassInitializer.EMPTY_ARRAY + + override fun getExtendsList(): PsiReferenceList? = null + override fun getImplementsList(): PsiReferenceList? = null + + override fun getScope(): PsiElement? = null + + private class ConstructorMethod(owner: PsiClass) : LightMethodBuilder(owner, JavaLanguage.INSTANCE) { + init { + isConstructor = true + setModifiers(PsiModifier.PUBLIC) + + val projectScope = GlobalSearchScope.allScope(manager.project) + addParameter("first", PsiType.getJavaLangString(manager, projectScope)) + addParameter("second", PsiType.INT) + } + } + + private class GetterMethod(owner: PsiClass) : LightMethodBuilder(owner.manager, "getResult") { + init { + containingClass = owner + setModifiers(PsiModifier.PUBLIC) + setMethodReturnType(PsiType.BOOLEAN) + } + + override fun getTypeParameterList(): PsiTypeParameterList? { + interruptAnalysis() + return super.getTypeParameterList() + } + + override fun getTypeParameters(): Array { + interruptAnalysis() + return super.getTypeParameters() + } + } +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/ErrorResistanceTestGenerated.java b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/ErrorResistanceTestGenerated.java new file mode 100644 index 00000000000..ad3a17812ba --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/ErrorResistanceTestGenerated.java @@ -0,0 +1,44 @@ +/* + * 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.analysis.low.level.api.fir.resolve; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.util.KtTestUtil; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.analysis.api.GenerateAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("analysis/low-level-api-fir/testdata/errorResistance") +@TestDataPath("$PROJECT_ROOT") +public class ErrorResistanceTestGenerated extends AbstractErrorResistanceTest { + @Test + public void testAllFilesPresentInErrorResistance() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/low-level-api-fir/testdata/errorResistance"), Pattern.compile("^(.+)\\.kt$"), null, true); + } + + @Test + @TestMetadata("builderInference.kt") + public void testBuilderInference() throws Exception { + runTest("analysis/low-level-api-fir/testdata/errorResistance/builderInference.kt"); + } + + @Test + @TestMetadata("propertyDelegate.kt") + public void testPropertyDelegate() throws Exception { + runTest("analysis/low-level-api-fir/testdata/errorResistance/propertyDelegate.kt"); + } + + @Test + @TestMetadata("propertyInitializer.kt") + public void testPropertyInitializer() throws Exception { + runTest("analysis/low-level-api-fir/testdata/errorResistance/propertyInitializer.kt"); + } +} diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/StateKeeperTest.kt b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/StateKeeperTest.kt new file mode 100644 index 00000000000..6a510ce4588 --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/resolve/StateKeeperTest.kt @@ -0,0 +1,237 @@ +/* + * 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.analysis.low.level.api.fir.resolve + +import org.jetbrains.kotlin.analysis.low.level.api.fir.transformers.StateKeeper +import org.jetbrains.kotlin.analysis.low.level.api.fir.transformers.entity +import org.jetbrains.kotlin.analysis.low.level.api.fir.transformers.entityList +import org.jetbrains.kotlin.analysis.low.level.api.fir.transformers.stateKeeper +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class StateKeeperTest { + @Test + fun testSimple() { + class Foo(private var value: String) { + fun getValue() = value + fun setValue(newValue: String) { + value = newValue + } + } + + val keeper: StateKeeper = stateKeeper { + add(Foo::getValue, Foo::setValue) + } + + val foo = Foo("Foo") + + keeper.withRestoration(foo) { + assertEquals("Foo", foo.getValue()) + foo.setValue("Bar") + assertEquals("Bar", foo.getValue()) + } + + assertEquals("Foo", foo.getValue()) + } + + @Test + fun testProperty() { + class Foo(var value: String) + + val keeper: StateKeeper = stateKeeper { + add(Foo::value::get, Foo::value::set) + } + + val foo = Foo("Foo") + + keeper.withRestoration(foo) { + assertEquals("Foo", foo.value) + foo.value = "Bar" + assertEquals("Bar", foo.value) + } + + assertEquals("Foo", foo.value) + } + + @Test + fun testArranger() { + class Foo(var value: String) + + val keeper: StateKeeper = stateKeeper { + add(Foo::value::get, Foo::value::set) { "Baz" } + } + + val foo = Foo("Foo") + + keeper.withRestoration(foo) { + assertEquals("Baz", foo.value) + foo.value = "Bar" + assertEquals("Bar", foo.value) + } + + assertEquals("Foo", foo.value) + } + + @Test + fun testInclusion() { + open class Foo(var one: String) + class Bar(one: String, var two: Int) : Foo(one) + + val fooKeeper: StateKeeper = stateKeeper { + add(Foo::one::get, Foo::one::set) + } + + val barKeeper: StateKeeper = stateKeeper { + add(fooKeeper) + add(Bar::two::get, Bar::two::set) + } + + val bar = Bar("Foo", 1) + + barKeeper.withRestoration(bar) { + assertEquals("Foo", bar.one) + assertEquals(1, bar.two) + bar.one = "Bar" + bar.two = 2 + assertEquals("Bar", bar.one) + assertEquals(2, bar.two) + } + + assertEquals("Foo", bar.one) + assertEquals(1, bar.two) + } + + @Test + fun testEntity() { + data class Foo(var value: Int) + class Bar(var one: String, var foo: Foo) + + val barKeeper: StateKeeper = stateKeeper { bar -> + add(Bar::one::get, Bar::one::set) + entity(bar.foo) { + add(Foo::value::get, Foo::value::set) + } + } + + val foo = Foo(1) + val bar = Bar("Foo", foo) + + barKeeper.withRestoration(bar) { + assertEquals("Foo", bar.one) + assertEquals(1, bar.foo.value) + + bar.one = "Bar" + bar.foo.value = 2 + + assertEquals("Bar", bar.one) + assertEquals(2, bar.foo.value) + } + + assertEquals("Foo", bar.one) + assertEquals(1, bar.foo.value) + assert(bar.foo === foo) + } + + @Test + fun testEntityInclusion() { + data class Foo(var value: Int) + class Bar(var one: String, var foo: Foo) + + val fooKeeper: StateKeeper = stateKeeper { + add(Foo::value::get, Foo::value::set) + } + + val barKeeper: StateKeeper = stateKeeper { bar -> + add(Bar::one::get, Bar::one::set) + entity(bar.foo, fooKeeper) + } + + val foo = Foo(1) + val bar = Bar("Foo", foo) + + barKeeper.withRestoration(bar) { + assertEquals("Foo", bar.one) + assertEquals(1, bar.foo.value) + + bar.one = "Bar" + bar.foo.value = 2 + + assertEquals("Bar", bar.one) + assertEquals(2, bar.foo.value) + } + + assertEquals("Foo", bar.one) + assertEquals(1, bar.foo.value) + assert(bar.foo === foo) + } + + @Test + fun testEntityList() { + data class Foo(var value: Int) + class Bar(var one: String, var foos: List) + + val barKeeper: StateKeeper = stateKeeper { bar -> + add(Bar::one::get, Bar::one::set) + entityList(bar.foos) { + add(Foo::value::get, Foo::value::set) { it + 1 } + } + } + + val foo0 = Foo(1) + val foo1 = Foo(2) + val bar = Bar("Foo", listOf(foo0, foo1)) + + barKeeper.withRestoration(bar) { + assertEquals("Foo", bar.one) + assertEquals(2, bar.foos[0].value) // +1 because of the arranger + assertEquals(3, bar.foos[1].value) + + bar.one = "Bar" + bar.foos[0].value = 100 + bar.foos[1].value = 200 + + assertEquals("Bar", bar.one) + assertEquals(100, bar.foos[0].value) + assertEquals(200, bar.foos[1].value) + } + + assertEquals("Foo", bar.one) + assertEquals(1, bar.foos[0].value) + assertEquals(2, bar.foos[1].value) + assert(bar.foos[0] === foo0) + assert(bar.foos[1] === foo1) + } + + @Test + fun testPostProcess() { + class Foo(var value: String) + + val keeper: StateKeeper = stateKeeper { foo -> + add(Foo::value::get, Foo::value::set) { "Baz" } + + postProcess { + foo.value = "Boo" + } + } + + val foo = Foo("Foo") + + keeper.withRestoration(foo) { + assertEquals("Boo", foo.value) + foo.value = "Bar" + assertEquals("Bar", foo.value) + } + + assertEquals("Foo", foo.value) + } +} + +private fun StateKeeper.withRestoration(owner: T, block: () -> Unit) { + val state = prepare(owner) + state.postProcess() + block() + state.restore() +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/test/configurators/AnalysisApiFirSourceTestConfigurator.kt b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/test/configurators/AnalysisApiFirSourceTestConfigurator.kt index cce7cdb237f..c03d704db97 100644 --- a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/test/configurators/AnalysisApiFirSourceTestConfigurator.kt +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/test/configurators/AnalysisApiFirSourceTestConfigurator.kt @@ -10,7 +10,7 @@ import org.jetbrains.kotlin.analysis.test.framework.project.structure.KtModuleFa import org.jetbrains.kotlin.analysis.test.framework.project.structure.KtSourceModuleFactory import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder -class AnalysisApiFirSourceTestConfigurator( +open class AnalysisApiFirSourceTestConfigurator( analyseInDependentSession: Boolean ) : AnalysisApiFirSourceLikeTestConfigurator(analyseInDependentSession) { override fun configureTest(builder: TestConfigurationBuilder, disposable: Disposable) { diff --git a/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/firLowLevel.kt b/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/firLowLevel.kt index 119e02d1661..229568130f2 100644 --- a/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/firLowLevel.kt +++ b/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/firLowLevel.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlin.analysis.low.level.api.fir.diagnostic.compiler.based import org.jetbrains.kotlin.analysis.low.level.api.fir.diagnostic.compiler.based.AbstractLLFirPreresolvedReversedDiagnosticCompilerTestDataTest import org.jetbrains.kotlin.analysis.low.level.api.fir.file.structure.AbstractOutOfContentRootFileStructureTest import org.jetbrains.kotlin.analysis.low.level.api.fir.file.structure.AbstractSourceFileStructureTest +import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.AbstractErrorResistanceTest import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.AbstractOutOfContentRootInnerDeclarationsResolvePhaseTest import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.AbstractSourceInnerDeclarationsResolvePhaseTest import org.jetbrains.kotlin.generators.TestGroup @@ -52,6 +53,10 @@ internal fun TestGroupSuite.generateFirLowLevelApiTests() { model("lazyResolve") } + testClass { + model("errorResistance") + } + testClass { model("fileStructure") }