diff --git a/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/contractCall.kt b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/contractCall.kt new file mode 100644 index 00000000000..bdbe38a6ba2 --- /dev/null +++ b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/contractCall.kt @@ -0,0 +1,10 @@ +import kotlin.contracts.contract +import kotlin.contracts.InvocationKind + +inline fun foo(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + + block() +} diff --git a/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/printlnFirstStatement.kt b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/printlnFirstStatement.kt new file mode 100644 index 00000000000..66ac2bb0804 --- /dev/null +++ b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/printlnFirstStatement.kt @@ -0,0 +1,3 @@ +fun foo(string: String) { + println("Hello world, $string!") +} diff --git a/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/printlnSecondStatement.kt b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/printlnSecondStatement.kt new file mode 100644 index 00000000000..ad31f7e65ef --- /dev/null +++ b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/printlnSecondStatement.kt @@ -0,0 +1,4 @@ +fun foo(string: String) { + val a = 7 + println("Hello world, $string!") +} diff --git a/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/contractCall.kt b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/contractCall.kt new file mode 100644 index 00000000000..4386803e7a7 --- /dev/null +++ b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/contractCall.kt @@ -0,0 +1,11 @@ +// IGNORE_FIR + +import kotlin.contracts.InvocationKind + +inline fun foo(block: () -> Unit) { + kotlin.contracts.contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + + block() +} diff --git a/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthFirstStatement.kt b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthFirstStatement.kt new file mode 100644 index 00000000000..71da7975268 --- /dev/null +++ b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthFirstStatement.kt @@ -0,0 +1,5 @@ +// IGNORE_FIR + +fun foo(string: String) { + string.length +} diff --git a/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthSecondStatement.kt b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthSecondStatement.kt new file mode 100644 index 00000000000..2d9879e4b29 --- /dev/null +++ b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthSecondStatement.kt @@ -0,0 +1,4 @@ +fun foo(string: String) { + println() + string.length +} diff --git a/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/propertyDeclaration/simpleProperty.kt b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/propertyDeclaration/simpleProperty.kt new file mode 100644 index 00000000000..338606be592 --- /dev/null +++ b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/propertyDeclaration/simpleProperty.kt @@ -0,0 +1 @@ +val property: Int = 5 diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/AbstractDeclarationModificationServicePsiResilienceTest.kt b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/AbstractDeclarationModificationServicePsiResilienceTest.kt new file mode 100644 index 00000000000..60b3d2c18ac --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/AbstractDeclarationModificationServicePsiResilienceTest.kt @@ -0,0 +1,96 @@ +/* + * 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.file.structure + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.analysis.low.level.api.fir.test.base.AbstractLowLevelApiModifiablePsiSingleFileTest +import org.jetbrains.kotlin.analysis.test.framework.services.expressionMarkerProvider +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.test.services.TestModuleStructure +import org.jetbrains.kotlin.test.services.TestServices +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +/** + * In the IDE, [LLFirDeclarationModificationService] might be called with PSI elements received via PSI tree change events. Because such + * elements may currently be in the process of modification, they may be inconsistent. This test ensures that the declaration modification + * service can handle such inconsistent PSI, without throwing exceptions. + */ +abstract class AbstractDeclarationModificationServicePsiResilienceTest : AbstractLowLevelApiModifiablePsiSingleFileTest() { + protected abstract fun modifySelectedElement(element: PsiElement) + + override fun doTestWithPsiModification(ktFile: KtFile, moduleStructure: TestModuleStructure, testServices: TestServices) { + val selectedElement = testServices.expressionMarkerProvider.getSelectedElement(ktFile) + modifySelectedElement(selectedElement) + + // The test passes when `LLFirDeclarationModificationService` throws no exceptions. + LLFirDeclarationModificationService.getInstance(ktFile.project).elementModified(selectedElement) + } +} + +@OptIn(ExperimentalContracts::class) +private inline fun assertSelectedElementType(selectedElement: PsiElement) { + contract { returns() implies (selectedElement is E) } + if (selectedElement !is E) { + error("Expected the selected element to be a ${E::class.simpleName}. Selected element: $selectedElement") + } +} + +abstract class AbstractDeclarationModificationServiceCallExpressionCalleeResilienceTest : + AbstractDeclarationModificationServicePsiResilienceTest() { + override fun modifySelectedElement(element: PsiElement) { + assertSelectedElementType(element) + + val calleeExpression = element.calleeExpression + require(calleeExpression != null) { + "A consistent call expression should have a callee expression. Expression: $this" + } + calleeExpression.delete() + } +} + +abstract class AbstractDeclarationModificationServiceDotQualifiedExpressionReceiverResilienceTest : + AbstractDeclarationModificationServicePsiResilienceTest() { + override fun modifySelectedElement(element: PsiElement) { + assertSelectedElementType(element) + element.checkConsistency() + + element.firstChild.delete() + } +} + +abstract class AbstractDeclarationModificationServiceDotQualifiedExpressionSelectorResilienceTest : + AbstractDeclarationModificationServicePsiResilienceTest() { + override fun modifySelectedElement(element: PsiElement) { + assertSelectedElementType(element) + element.checkConsistency() + + element.firstChild.delete() + element.operationTokenNode.psi.delete() + } +} + +private fun KtDotQualifiedExpression.checkConsistency() { + require(children.size == 2) { + "A consistent dot-qualified expression should have two children. Expression: $this" + } +} + +abstract class AbstractDeclarationModificationServicePropertyDeclarationInitializerResilienceTest : + AbstractDeclarationModificationServicePsiResilienceTest() { + override fun modifySelectedElement(element: PsiElement) { + assertSelectedElementType(element) + + val initializer = element.initializer + require(initializer != null) { + "The property declaration is expected to have an initializer. Property declaration: $this" + } + initializer.delete() + } +} diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServiceCallExpressionCalleeResilienceTestGenerated.java b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServiceCallExpressionCalleeResilienceTestGenerated.java new file mode 100644 index 00000000000..2f121443baf --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServiceCallExpressionCalleeResilienceTestGenerated.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.file.structure; + +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/declarationModificationService/psiResilience/callExpression") +@TestDataPath("$PROJECT_ROOT") +public class DeclarationModificationServiceCallExpressionCalleeResilienceTestGenerated extends AbstractDeclarationModificationServiceCallExpressionCalleeResilienceTest { + @Test + public void testAllFilesPresentInCallExpression() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression"), Pattern.compile("^([^.]+)\\.kt$"), null, false); + } + + @Test + @TestMetadata("contractCall.kt") + public void testContractCall() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/contractCall.kt"); + } + + @Test + @TestMetadata("printlnFirstStatement.kt") + public void testPrintlnFirstStatement() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/printlnFirstStatement.kt"); + } + + @Test + @TestMetadata("printlnSecondStatement.kt") + public void testPrintlnSecondStatement() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/callExpression/printlnSecondStatement.kt"); + } +} diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServiceDotQualifiedExpressionReceiverResilienceTestGenerated.java b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServiceDotQualifiedExpressionReceiverResilienceTestGenerated.java new file mode 100644 index 00000000000..3394f1c157c --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServiceDotQualifiedExpressionReceiverResilienceTestGenerated.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.file.structure; + +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/declarationModificationService/psiResilience/dotQualifiedExpression") +@TestDataPath("$PROJECT_ROOT") +public class DeclarationModificationServiceDotQualifiedExpressionReceiverResilienceTestGenerated extends AbstractDeclarationModificationServiceDotQualifiedExpressionReceiverResilienceTest { + @Test + public void testAllFilesPresentInDotQualifiedExpression() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression"), Pattern.compile("^([^.]+)\\.kt$"), null, false); + } + + @Test + @TestMetadata("contractCall.kt") + public void testContractCall() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/contractCall.kt"); + } + + @Test + @TestMetadata("stringLengthFirstStatement.kt") + public void testStringLengthFirstStatement() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthFirstStatement.kt"); + } + + @Test + @TestMetadata("stringLengthSecondStatement.kt") + public void testStringLengthSecondStatement() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthSecondStatement.kt"); + } +} diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServiceDotQualifiedExpressionSelectorResilienceTestGenerated.java b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServiceDotQualifiedExpressionSelectorResilienceTestGenerated.java new file mode 100644 index 00000000000..e5ba03b3fe5 --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServiceDotQualifiedExpressionSelectorResilienceTestGenerated.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.file.structure; + +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/declarationModificationService/psiResilience/dotQualifiedExpression") +@TestDataPath("$PROJECT_ROOT") +public class DeclarationModificationServiceDotQualifiedExpressionSelectorResilienceTestGenerated extends AbstractDeclarationModificationServiceDotQualifiedExpressionSelectorResilienceTest { + @Test + public void testAllFilesPresentInDotQualifiedExpression() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression"), Pattern.compile("^([^.]+)\\.kt$"), null, false); + } + + @Test + @TestMetadata("contractCall.kt") + public void testContractCall() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/contractCall.kt"); + } + + @Test + @TestMetadata("stringLengthFirstStatement.kt") + public void testStringLengthFirstStatement() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthFirstStatement.kt"); + } + + @Test + @TestMetadata("stringLengthSecondStatement.kt") + public void testStringLengthSecondStatement() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthSecondStatement.kt"); + } +} diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServicePropertyDeclarationInitializerResilienceTestGenerated.java b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServicePropertyDeclarationInitializerResilienceTestGenerated.java new file mode 100644 index 00000000000..2d675d86d74 --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/DeclarationModificationServicePropertyDeclarationInitializerResilienceTestGenerated.java @@ -0,0 +1,32 @@ +/* + * 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.file.structure; + +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/declarationModificationService/psiResilience/propertyDeclaration") +@TestDataPath("$PROJECT_ROOT") +public class DeclarationModificationServicePropertyDeclarationInitializerResilienceTestGenerated extends AbstractDeclarationModificationServicePropertyDeclarationInitializerResilienceTest { + @Test + public void testAllFilesPresentInPropertyDeclaration() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/propertyDeclaration"), Pattern.compile("^([^.]+)\\.kt$"), null, false); + } + + @Test + @TestMetadata("simpleProperty.kt") + public void testSimpleProperty() throws Exception { + runTest("analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/propertyDeclaration/simpleProperty.kt"); + } +} 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 50cf80850a6..401f7d82e11 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 @@ -82,6 +82,38 @@ internal fun TestGroupSuite.generateFirLowLevelApiTests() { model("inBlockModification/codeFragments", recursive = false, pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME) } + testClass { + model( + "declarationModificationService/psiResilience/callExpression", + recursive = false, + pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME, + ) + } + + testClass { + model( + "declarationModificationService/psiResilience/dotQualifiedExpression", + recursive = false, + pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME, + ) + } + + testClass { + model( + "declarationModificationService/psiResilience/dotQualifiedExpression", + recursive = false, + pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME, + ) + } + + testClass { + model( + "declarationModificationService/psiResilience/propertyDeclaration", + recursive = false, + pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME, + ) + } + testClass { model("fileStructure", pattern = TestGeneratorUtil.KT) }