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
index 4386803e7a7..79cff317223 100644
--- a/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/contractCall.kt
+++ b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/contractCall.kt
@@ -1,5 +1,3 @@
-// IGNORE_FIR
-
import kotlin.contracts.InvocationKind
inline fun foo(block: () -> Unit) {
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
index 71da7975268..81c5c0c7c98 100644
--- a/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthFirstStatement.kt
+++ b/analysis/low-level-api-fir/testData/declarationModificationService/psiResilience/dotQualifiedExpression/stringLengthFirstStatement.kt
@@ -1,5 +1,3 @@
-// IGNORE_FIR
-
fun foo(string: String) {
string.length
}
diff --git a/compiler/psi/src/org/jetbrains/kotlin/psi/KtDotQualifiedExpression.kt b/compiler/psi/src/org/jetbrains/kotlin/psi/KtDotQualifiedExpression.kt
index 0cc211dcbc6..8fdbfb3762e 100644
--- a/compiler/psi/src/org/jetbrains/kotlin/psi/KtDotQualifiedExpression.kt
+++ b/compiler/psi/src/org/jetbrains/kotlin/psi/KtDotQualifiedExpression.kt
@@ -34,15 +34,15 @@ class KtDotQualifiedExpression : KtExpressionImplStub
+ getChildExpressionsByStub(stub)?.getOrNull(0)
}
override val selectorExpression: KtExpression?
diff --git a/compiler/psi/src/org/jetbrains/kotlin/psi/KtQualifiedExpression.kt b/compiler/psi/src/org/jetbrains/kotlin/psi/KtQualifiedExpression.kt
index a0641c0ebb8..de1655d5d4c 100644
--- a/compiler/psi/src/org/jetbrains/kotlin/psi/KtQualifiedExpression.kt
+++ b/compiler/psi/src/org/jetbrains/kotlin/psi/KtQualifiedExpression.kt
@@ -26,20 +26,33 @@ import java.util.*
interface KtQualifiedExpression : KtExpression {
val receiverExpression: KtExpression
- get() = getExpression(false) ?: throw AssertionError("No receiver found: ${getElementTextWithContext()}")
+ @OptIn(KtPsiInconsistencyHandling::class)
+ get() = receiverExpressionOrNull ?: throw AssertionError("No receiver found: ${getElementTextWithContext()}")
+
+ /**
+ * A consistent [KtQualifiedExpression] should always have a receiver, so [receiverExpression] should be preferred if possible. Only use
+ * [receiverExpressionOrNull] if you suspect that the PSI might be inconsistent (e.g. due to ongoing modification) and need a `null`
+ * value instead of an error.
+ */
+ @KtPsiInconsistencyHandling
+ val receiverExpressionOrNull: KtExpression?
+ get() = getExpression(false)
val selectorExpression: KtExpression?
get() = getExpression(true)
val operationTokenNode: ASTNode
- get() = node.findChildByType(KtTokens.OPERATIONS) ?: error(
+ get() = operationTokenNodeOrNull ?: error(
"No operation node for ${node.elementType}. Children: ${Arrays.toString(children)}"
)
+ private val operationTokenNodeOrNull: ASTNode?
+ get() = node.findChildByType(KtTokens.OPERATIONS)
+
val operationSign: KtSingleValueToken
get() = operationTokenNode.elementType as KtSingleValueToken
private fun getExpression(afterOperation: Boolean): KtExpression? {
- return operationTokenNode.psi?.siblings(afterOperation, false)?.firstIsInstanceOrNull()
+ return operationTokenNodeOrNull?.psi?.siblings(afterOperation, false)?.firstIsInstanceOrNull()
}
}
diff --git a/compiler/psi/src/org/jetbrains/kotlin/psi/annotations.kt b/compiler/psi/src/org/jetbrains/kotlin/psi/annotations.kt
new file mode 100644
index 00000000000..591c00ec437
--- /dev/null
+++ b/compiler/psi/src/org/jetbrains/kotlin/psi/annotations.kt
@@ -0,0 +1,16 @@
+/*
+ * 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.psi
+
+/**
+ * Functions and classes annotated with [KtPsiInconsistencyHandling] are not intended for general-purpose use, but for working with possibly
+ * inconsistent PSI. The specific circumstances need to be described in the documentation of the annotated function/class.
+ *
+ * Inconsistent PSI cannot be produced by the Kotlin parser. It occurs rarely, for example during modification of the PSI by the IDE. In
+ * general, it can be assumed that all PSI is consistent. Inconsistent PSI should only be assumed when there is sufficient proof.
+ */
+@RequiresOptIn
+annotation class KtPsiInconsistencyHandling
diff --git a/compiler/psi/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt b/compiler/psi/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt
index fb26a9bb36a..a230d2404ab 100644
--- a/compiler/psi/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt
+++ b/compiler/psi/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt
@@ -297,9 +297,10 @@ fun KtNamedFunction.isContractPresentPsiCheck(isAllowedOnMembers: Boolean): Bool
fun KtExpression.isContractDescriptionCallPsiCheck(): Boolean =
(this is KtCallExpression && calleeExpression?.text == "contract") || (this is KtQualifiedExpression && isContractDescriptionCallPsiCheck())
+@OptIn(KtPsiInconsistencyHandling::class)
fun KtQualifiedExpression.isContractDescriptionCallPsiCheck(): Boolean {
val expression = selectorExpression ?: return false
- return receiverExpression.text == "kotlin.contracts" && expression.isContractDescriptionCallPsiCheck()
+ return receiverExpressionOrNull?.text == "kotlin.contracts" && expression.isContractDescriptionCallPsiCheck()
}
fun KtElement.isFirstStatement(): Boolean {