[LL] Add PSI resilience tests for LLFirDeclarationModificationService

- The cause of KT-63130 is that `LLFirDeclarationModificationService`
  tries to analyze inconsistent PSI. These new tests replicate the
  inconsistent PSI by deleting different children and then call the
  declaration modification service with it, making sure that the service
  doesn't throw any exceptions. Both exceptions mentioned in the issue
  are reproduced by the added tests, specifically, the receiver and
  selector resilience tests for dot-qualified expressions.
- A few of the tests need to be marked with `IGNORE_FIR` until the error
  has been fixed in the subsequent commit.

^KT-63130
This commit is contained in:
Marco Pennekamp
2023-11-13 18:56:06 +01:00
committed by Space Team
parent 12b6096b67
commit 6c67835128
13 changed files with 330 additions and 0 deletions
@@ -0,0 +1,10 @@
import kotlin.contracts.contract
import kotlin.contracts.InvocationKind
inline fun foo(block: () -> Unit) {
<expr>contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}</expr>
block()
}
@@ -0,0 +1,3 @@
fun foo(string: String) {
<expr>println("Hello world, $string!")</expr>
}
@@ -0,0 +1,4 @@
fun foo(string: String) {
val a = 7
<expr>println("Hello world, $string!")</expr>
}
@@ -0,0 +1,11 @@
// IGNORE_FIR
import kotlin.contracts.InvocationKind
inline fun foo(block: () -> Unit) {
<expr>kotlin.contracts.contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}</expr>
block()
}
@@ -0,0 +1,5 @@
// IGNORE_FIR
fun foo(string: String) {
<expr>string.length</expr>
}
@@ -0,0 +1,4 @@
fun foo(string: String) {
println()
<expr>string.length</expr>
}
@@ -0,0 +1 @@
<expr>val property: Int = 5</expr>
@@ -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 <reified E : PsiElement> 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<KtCallExpression>(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<KtDotQualifiedExpression>(element)
element.checkConsistency()
element.firstChild.delete()
}
}
abstract class AbstractDeclarationModificationServiceDotQualifiedExpressionSelectorResilienceTest :
AbstractDeclarationModificationServicePsiResilienceTest() {
override fun modifySelectedElement(element: PsiElement) {
assertSelectedElementType<KtDotQualifiedExpression>(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<KtProperty>(element)
val initializer = element.initializer
require(initializer != null) {
"The property declaration is expected to have an initializer. Property declaration: $this"
}
initializer.delete()
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -82,6 +82,38 @@ internal fun TestGroupSuite.generateFirLowLevelApiTests() {
model("inBlockModification/codeFragments", recursive = false, pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME)
}
testClass<AbstractDeclarationModificationServiceCallExpressionCalleeResilienceTest> {
model(
"declarationModificationService/psiResilience/callExpression",
recursive = false,
pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME,
)
}
testClass<AbstractDeclarationModificationServiceDotQualifiedExpressionReceiverResilienceTest> {
model(
"declarationModificationService/psiResilience/dotQualifiedExpression",
recursive = false,
pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME,
)
}
testClass<AbstractDeclarationModificationServiceDotQualifiedExpressionSelectorResilienceTest> {
model(
"declarationModificationService/psiResilience/dotQualifiedExpression",
recursive = false,
pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME,
)
}
testClass<AbstractDeclarationModificationServicePropertyDeclarationInitializerResilienceTest> {
model(
"declarationModificationService/psiResilience/propertyDeclaration",
recursive = false,
pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME,
)
}
testClass<AbstractSourceFileStructureTest> {
model("fileStructure", pattern = TestGeneratorUtil.KT)
}