FIR/FIR IDE: Use entire FirVariableAssignment when reporting UNSAFE_CALL
(e.g., `nullable.a = b`), and use positioning strategies to locate the dot in the LHS expression. Without it, only the callee reference is reported on, which makes the highlighting of the error and application of quickfixes incorrect in the IDE. Also fixed issue with annotated and/or labeled expressions on LHS of assignment (e.g., `(@Ann label@ i) = 34`).
This commit is contained in:
committed by
Ilya Kirillov
parent
1d9247ae0f
commit
d2b8204fdc
+21
-15
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.idea.frontend.api.fir.diagnostics.KtFirDiagnostic
|
||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFunctionSymbol
|
||||
import org.jetbrains.kotlin.idea.quickfix.AddExclExclCallFix
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.unwrapParenthesesLabelsAndAnnotations
|
||||
import org.jetbrains.kotlin.util.OperatorNameConventions
|
||||
|
||||
object AddExclExclCallFixFactories {
|
||||
@@ -29,46 +30,51 @@ object AddExclExclCallFixFactories {
|
||||
}
|
||||
|
||||
private fun KtAnalysisSession.getFixForUnsafeCall(psi: PsiElement): List<IntentionAction> {
|
||||
val (target, hasImplicitReceiver) = when (psi) {
|
||||
val (target, hasImplicitReceiver) = when (val unwrapped = psi.unwrapParenthesesLabelsAndAnnotations()) {
|
||||
// `foo.bar` -> `foo!!.bar`
|
||||
is KtDotQualifiedExpression -> psi.receiverExpression to false
|
||||
is KtDotQualifiedExpression -> unwrapped.receiverExpression to false
|
||||
|
||||
// `foo[bar]` -> `foo!![bar]`
|
||||
is KtArrayAccessExpression -> psi.arrayExpression to false
|
||||
is KtArrayAccessExpression -> unwrapped.arrayExpression to false
|
||||
|
||||
is KtCallableReferenceExpression -> psi.lhs.let { lhs ->
|
||||
is KtCallableReferenceExpression -> unwrapped.lhs.let { lhs ->
|
||||
if (lhs != null) {
|
||||
// `foo::bar` -> `foo!!::bar`
|
||||
lhs to false
|
||||
} else {
|
||||
// `::bar -> this!!::bar`
|
||||
psi to true
|
||||
unwrapped to true
|
||||
}
|
||||
}
|
||||
|
||||
// `bar` -> `this!!.bar`
|
||||
is KtNameReferenceExpression -> psi to true
|
||||
is KtNameReferenceExpression -> unwrapped to true
|
||||
|
||||
// `bar()` -> `this!!.bar()`
|
||||
is KtCallExpression -> psi to true
|
||||
is KtCallExpression -> unwrapped to true
|
||||
|
||||
// `-foo` -> `-foo!!`
|
||||
// NOTE: Unsafe unary operator call is reported as UNSAFE_CALL, _not_ UNSAFE_OPERATOR_CALL
|
||||
is KtUnaryExpression -> psi.baseExpression to false
|
||||
is KtUnaryExpression -> unwrapped.baseExpression to false
|
||||
|
||||
is KtBinaryExpression -> {
|
||||
val receiver = if (KtPsiUtil.isInOrNotInOperation(psi)) {
|
||||
// `bar in foo` -> `bar in foo!!`
|
||||
psi.right
|
||||
} else {
|
||||
// `foo + bar` -> `foo!! + bar` OR `foo infixFun bar` -> `foo!! infixFun bar`
|
||||
psi.left
|
||||
val receiver = when {
|
||||
KtPsiUtil.isInOrNotInOperation(unwrapped) ->
|
||||
// `bar in foo` -> `bar in foo!!`
|
||||
unwrapped.right
|
||||
KtPsiUtil.isAssignment(unwrapped) ->
|
||||
// UNSAFE_CALL for assignments (e.g., `foo.bar = value`) is reported on the entire statement (KtBinaryExpression).
|
||||
// The unsafe call is on the LHS of the assignment.
|
||||
return getFixForUnsafeCall(unwrapped.left ?: return emptyList())
|
||||
else ->
|
||||
// `foo + bar` -> `foo!! + bar` OR `foo infixFun bar` -> `foo!! infixFun bar`
|
||||
unwrapped.left
|
||||
}
|
||||
receiver to false
|
||||
}
|
||||
|
||||
// UNSAFE_INFIX_CALL/UNSAFE_OPERATOR_CALL on KtBinaryExpression is reported on the child KtOperationReferenceExpression
|
||||
is KtOperationReferenceExpression -> return getFixForUnsafeCall(psi.parent)
|
||||
is KtOperationReferenceExpression -> return getFixForUnsafeCall(unwrapped.parent)
|
||||
|
||||
else -> return emptyList()
|
||||
}
|
||||
|
||||
+15
-6
@@ -11,9 +11,9 @@ import org.jetbrains.kotlin.idea.frontend.api.types.KtTypeNullability
|
||||
import org.jetbrains.kotlin.idea.frontend.api.types.KtTypeWithNullability
|
||||
import org.jetbrains.kotlin.idea.quickfix.ReplaceImplicitReceiverCallFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.ReplaceWithSafeCallFix
|
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.unwrapParenthesesLabelsAndAnnotations
|
||||
|
||||
object ReplaceCallFixFactories {
|
||||
val unsafeCallFactory =
|
||||
@@ -25,14 +25,23 @@ object ReplaceCallFixFactories {
|
||||
return expectedType?.nullability == KtTypeNullability.NON_NULLABLE
|
||||
}
|
||||
|
||||
when (val psi = diagnostic.psi) {
|
||||
is KtDotQualifiedExpression -> listOf(ReplaceWithSafeCallFix(psi, psi.shouldHaveNotNullType()))
|
||||
val psi = diagnostic.psi
|
||||
val target = if (psi is KtBinaryExpression && psi.operationToken in KtTokens.ALL_ASSIGNMENTS) {
|
||||
// UNSAFE_CALL for assignments (e.g., `foo.bar = value`) is reported on the entire statement (KtBinaryExpression).
|
||||
// The unsafe call is on the LHS of the assignment.
|
||||
psi.left
|
||||
} else {
|
||||
psi
|
||||
}.unwrapParenthesesLabelsAndAnnotations()
|
||||
|
||||
when (target) {
|
||||
is KtDotQualifiedExpression -> listOf(ReplaceWithSafeCallFix(target, target.shouldHaveNotNullType()))
|
||||
is KtNameReferenceExpression -> {
|
||||
// TODO: As a safety precaution, resolve the expression to determine if it is a call with an implicit receiver.
|
||||
// This is a defensive check to ensure that the diagnostic was reported on such a call and not some other name reference.
|
||||
// This isn't strictly needed because FIR checkers aren't reporting on wrong elements, but ReplaceWithSafeCallFixFactory
|
||||
// in FE1.0 does so.
|
||||
listOf(ReplaceImplicitReceiverCallFix(psi, psi.shouldHaveNotNullType()))
|
||||
listOf(ReplaceImplicitReceiverCallFix(target, target.shouldHaveNotNullType()))
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
Generated
+15
@@ -229,6 +229,16 @@ public class HighLevelQuickFixTestGenerated extends AbstractHighLevelQuickFixTes
|
||||
runTest("idea/testData/quickfix/addExclExclCall/array4.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("assignment.kt")
|
||||
public void testAssignment() throws Exception {
|
||||
runTest("idea/testData/quickfix/addExclExclCall/assignment.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("assignmentToUnsafeCallExpression.kt")
|
||||
public void testAssignmentToUnsafeCallExpression() throws Exception {
|
||||
runTest("idea/testData/quickfix/addExclExclCall/assignmentToUnsafeCallExpression.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("functionReference.kt")
|
||||
public void testFunctionReference() throws Exception {
|
||||
runTest("idea/testData/quickfix/addExclExclCall/functionReference.kt");
|
||||
@@ -1271,6 +1281,11 @@ public class HighLevelQuickFixTestGenerated extends AbstractHighLevelQuickFixTes
|
||||
runTest("idea/testData/quickfix/replaceWithSafeCall/assignmentToPropertyWithNoExplicitType.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("assignmentToUnsafeCallExpression.kt")
|
||||
public void testAssignmentToUnsafeCallExpression() throws Exception {
|
||||
runTest("idea/testData/quickfix/replaceWithSafeCall/assignmentToUnsafeCallExpression.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("comment.kt")
|
||||
public void testComment() throws Exception {
|
||||
runTest("idea/testData/quickfix/replaceWithSafeCall/comment.kt");
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Add non-null asserted (!!) call" "true"
|
||||
// WITH_RUNTIME
|
||||
var i = 0
|
||||
|
||||
fun foo(s: String?) {
|
||||
i = s<caret>.length
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Add non-null asserted (!!) call" "true"
|
||||
// WITH_RUNTIME
|
||||
var i = 0
|
||||
|
||||
fun foo(s: String?) {
|
||||
i = s!!.length
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// "Add non-null asserted (!!) call" "true"
|
||||
class A(var s: String)
|
||||
|
||||
fun foo(a: A?) {
|
||||
a<caret>.s = ""
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// "Add non-null asserted (!!) call" "true"
|
||||
class A(var s: String)
|
||||
|
||||
fun foo(a: A?) {
|
||||
a!!.s = ""
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// "Replace with safe (?.) call" "true"
|
||||
class A(var s: String? = null)
|
||||
|
||||
fun foo(a: A?) {
|
||||
a<caret>.s = ""
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// "Replace with safe (?.) call" "true"
|
||||
class A(var s: String? = null)
|
||||
|
||||
fun foo(a: A?) {
|
||||
a?.s = ""
|
||||
}
|
||||
@@ -642,6 +642,16 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
runTest("idea/testData/quickfix/addExclExclCall/array4.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("assignment.kt")
|
||||
public void testAssignment() throws Exception {
|
||||
runTest("idea/testData/quickfix/addExclExclCall/assignment.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("assignmentToUnsafeCallExpression.kt")
|
||||
public void testAssignmentToUnsafeCallExpression() throws Exception {
|
||||
runTest("idea/testData/quickfix/addExclExclCall/assignmentToUnsafeCallExpression.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("functionReference.kt")
|
||||
public void testFunctionReference() throws Exception {
|
||||
runTest("idea/testData/quickfix/addExclExclCall/functionReference.kt");
|
||||
@@ -11825,6 +11835,11 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
runTest("idea/testData/quickfix/replaceWithSafeCall/assignmentToPropertyWithNoExplicitType.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("assignmentToUnsafeCallExpression.kt")
|
||||
public void testAssignmentToUnsafeCallExpression() throws Exception {
|
||||
runTest("idea/testData/quickfix/replaceWithSafeCall/assignmentToUnsafeCallExpression.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("comment.kt")
|
||||
public void testComment() throws Exception {
|
||||
runTest("idea/testData/quickfix/replaceWithSafeCall/comment.kt");
|
||||
|
||||
Reference in New Issue
Block a user