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)
}