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 {