FIR IDE: quickfix for WrapWithSafeLetCall
There is some behavior change regarding the new WrapWithSafeLetCall quickfix
1. it now works correctly on binary expressions by wrapping it with `()`
2. it now looks for a nullable position upward and do the modification there,
if possible. For example, consider the following code
```
fun bar(s: String): String = s
fun test(s: String?) {
bar(bar(bar(<caret>s)))
}
```
After applying this fix, FE1.0 yields
```
bar(bar(s?.let { bar(it) }))
```
while the new implementation yields
```
s?.let { bar(bar(bar(it))) }
```
This behavior aligns with FE1.0 if `bar` accepts nullable values.
This commit is contained in:
committed by
Ilya Kirillov
parent
0eaab6d8a2
commit
e1b542314a
@@ -377,7 +377,7 @@ private fun checkApplicabilityForArgumentType(
|
||||
|
||||
// Reaching here means argument types mismatch, and we want to record whether it's due to the nullability by checking a subtype
|
||||
// relation with nullable expected type.
|
||||
val isMismatchDueToNullability = AbstractTypeChecker.isSubtypeOf(
|
||||
val isMismatchDueToNullability = argumentType.canBeNull && !actualExpectedType.isNullable && AbstractTypeChecker.isSubtypeOf(
|
||||
context.session.typeContext,
|
||||
argumentType,
|
||||
actualExpectedType.withNullability(ConeNullability.NULLABLE, context.session.typeContext)
|
||||
|
||||
@@ -347,6 +347,8 @@ fun PsiElement.parameterIndex(): Int {
|
||||
}
|
||||
}
|
||||
|
||||
val KtValueArgument.argumentIndex: Int get() = (parent as KtValueArgumentList).arguments.indexOf(this)
|
||||
|
||||
fun KtModifierListOwner.isPrivate(): Boolean = hasModifier(KtTokens.PRIVATE_KEYWORD)
|
||||
|
||||
fun KtModifierListOwner.isProtected(): Boolean = hasModifier(KtTokens.PROTECTED_KEYWORD)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.jetbrains.kotlin.name;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public final class Name implements Comparable<Name> {
|
||||
@NotNull
|
||||
@@ -85,6 +86,12 @@ public final class Name implements Comparable<Name> {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getIdentifierOrNullIfSpecial() {
|
||||
if (special) return null;
|
||||
return asString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
|
||||
@@ -112,6 +112,12 @@ class MainKtQuickFixRegistrar : KtQuickFixRegistrar() {
|
||||
|
||||
// TODO: NON_EXHAUSTIVE_WHEN[_ON_SEALED_CLASS] will be replaced in future. We need to register the fix for those diagnostics as well
|
||||
registerPsiQuickFixes(KtFirDiagnostic.NoElseInWhen::class, AddWhenElseBranchFix)
|
||||
|
||||
registerApplicator(WrapWithSafeLetCallFixFactories.forUnsafeCall)
|
||||
registerApplicator(WrapWithSafeLetCallFixFactories.forUnsafeImplicitInvokeCall)
|
||||
registerApplicator(WrapWithSafeLetCallFixFactories.forUnsafeInfixCall)
|
||||
registerApplicator(WrapWithSafeLetCallFixFactories.forUnsafeOperatorCall)
|
||||
registerApplicator(WrapWithSafeLetCallFixFactories.forArgumentTypeMismatch)
|
||||
}
|
||||
|
||||
override val list: KtQuickFixesList = KtQuickFixesList.createCombined(
|
||||
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
* Copyright 2010-2021 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.idea.quickfix.fixes
|
||||
|
||||
import com.intellij.codeInsight.intention.IntentionAction
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.SmartPsiElementPointer
|
||||
import com.intellij.psi.util.parentOfType
|
||||
import org.jetbrains.kotlin.idea.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.api.applicator.HLApplicator
|
||||
import org.jetbrains.kotlin.idea.api.applicator.HLApplicatorInput
|
||||
import org.jetbrains.kotlin.idea.api.applicator.applicator
|
||||
import org.jetbrains.kotlin.idea.core.FirKotlinNameSuggester
|
||||
import org.jetbrains.kotlin.idea.fir.api.fixes.HLQuickFix
|
||||
import org.jetbrains.kotlin.idea.fir.api.fixes.diagnosticFixFactory
|
||||
import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession
|
||||
import org.jetbrains.kotlin.idea.frontend.api.calls.KtCall
|
||||
import org.jetbrains.kotlin.idea.frontend.api.calls.KtVariableWithInvokeFunctionCall
|
||||
import org.jetbrains.kotlin.idea.frontend.api.fir.diagnostics.KtFirDiagnostic
|
||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtCallableSymbol
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.*
|
||||
import org.jetbrains.kotlin.types.expressions.OperatorConventions
|
||||
|
||||
object WrapWithSafeLetCallFixFactories {
|
||||
class Input(
|
||||
val nullableExpressionPointer: SmartPsiElementPointer<KtExpression>,
|
||||
val suggestedVariableName: String,
|
||||
val isImplicitInvokeCallToMemberProperty: Boolean,
|
||||
) : HLApplicatorInput
|
||||
|
||||
private val LOG = Logger.getInstance(this::class.java)
|
||||
|
||||
/**
|
||||
* Applicator that wraps a given target expression inside a `let` call on the input `nullableExpression`.
|
||||
*
|
||||
* Consider the following code snippet:
|
||||
*
|
||||
* ```
|
||||
* fun test(s: String?) {
|
||||
* println(s.length)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* In this case, one use the applicator with the following arguments
|
||||
* - target expression: `s.length`
|
||||
* - nullable expression: `s`
|
||||
* - suggestedVariableName: `myName`
|
||||
* - isImplicitInvokeCallToMemberProperty: false
|
||||
*
|
||||
* Then the applicator changes the code to
|
||||
*
|
||||
* ```
|
||||
* fun test(s: String?) {
|
||||
* println(s?.let { myName -> myName.length })
|
||||
* }
|
||||
* ```
|
||||
* `isImplicitInvokeCallToMemberProperty` controls the behavior when hoisting up the nullable expression. It should be set to true
|
||||
* if the call is to a invocable member property.
|
||||
*/
|
||||
val applicator: HLApplicator<KtExpression, Input> = applicator {
|
||||
familyAndActionName(KotlinBundle.lazyMessage("wrap.with.let.call"))
|
||||
applyTo { targetExpression, input ->
|
||||
val nullableExpression = input.nullableExpressionPointer.element ?: return@applyTo
|
||||
if (!nullableExpression.parents.contains(targetExpression)) {
|
||||
LOG.warn(
|
||||
"Unexpected input for WrapWithSafeLetCall. Nullable expression '${nullableExpression.text}' should be a descendant" +
|
||||
" of '${targetExpression.text}'."
|
||||
)
|
||||
return@applyTo
|
||||
}
|
||||
val suggestedVariableName = input.suggestedVariableName
|
||||
val factory = KtPsiFactory(targetExpression)
|
||||
|
||||
fun getNewExpression(nullableExpressionText: String, expressionUnderLetText: String): KtExpression {
|
||||
return when (suggestedVariableName) {
|
||||
"it" -> factory.createExpressionByPattern("$0?.let { $1 }", nullableExpressionText, expressionUnderLetText)
|
||||
else -> factory.createExpressionByPattern(
|
||||
"$0?.let { $1 -> $2 }",
|
||||
nullableExpressionText,
|
||||
suggestedVariableName,
|
||||
expressionUnderLetText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val callExpression = nullableExpression.parentOfType<KtCallExpression>(withSelf = true)
|
||||
val qualifiedExpression = callExpression?.getQualifiedExpressionForSelector()
|
||||
val receiverExpression = qualifiedExpression?.receiverExpression
|
||||
if (receiverExpression != null && input.isImplicitInvokeCallToMemberProperty) {
|
||||
// In this case, the nullable expression is an invocable member. For example consider the following
|
||||
//
|
||||
// interface Foo {
|
||||
// val bar: (() -> Unit)?
|
||||
// }
|
||||
// fun test(foo: Foo) {
|
||||
// foo.bar()
|
||||
// }
|
||||
//
|
||||
// In this case, `foo.bar` is nullable and this fix should change the code to `foo.bar?.let { it() }`. But note that
|
||||
// the PSI structure of the above code is
|
||||
//
|
||||
// - qualifiedExpression: foo.bar()
|
||||
// - receiver: foo
|
||||
// - operationTokenNode: .
|
||||
// - selectorExpression: bar()
|
||||
// - calleeExpression: bar
|
||||
// - valueArgumentList: ()
|
||||
//
|
||||
// So we need to explicitly construct the nullable expression text `foo.bar`.
|
||||
val nullableExpressionText =
|
||||
"${receiverExpression.text}${qualifiedExpression.operationSign.value}${nullableExpression.text}"
|
||||
val newInvokeCallText =
|
||||
"${suggestedVariableName}${callExpression.valueArgumentList?.text ?: ""}${
|
||||
callExpression.lambdaArguments.joinToString(
|
||||
" ",
|
||||
prefix = " "
|
||||
) { it.text }
|
||||
}"
|
||||
if (qualifiedExpression == targetExpression) {
|
||||
targetExpression.replace(getNewExpression(nullableExpressionText, newInvokeCallText))
|
||||
} else {
|
||||
qualifiedExpression.replace(factory.createExpression(newInvokeCallText))
|
||||
targetExpression.replace(getNewExpression(nullableExpressionText, targetExpression.text))
|
||||
}
|
||||
|
||||
} else {
|
||||
val nullableExpressionText = when (nullableExpression) {
|
||||
is KtBinaryExpression, is KtBinaryExpressionWithTypeRHS -> "(${nullableExpression.text})"
|
||||
else -> nullableExpression.text
|
||||
}
|
||||
nullableExpression.replace(factory.createExpression(suggestedVariableName))
|
||||
targetExpression.replace(getNewExpression(nullableExpressionText, targetExpression.text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val forUnsafeCall = diagnosticFixFactory<KtFirDiagnostic.UnsafeCall> { diagnostic ->
|
||||
val nullableExpression = diagnostic.receiverExpression
|
||||
createWrapWithSafeLetCallInputForNullableExpressionIfMoreThanImmediateParentIsWrapped(nullableExpression)
|
||||
}
|
||||
|
||||
val forUnsafeImplicitInvokeCall = diagnosticFixFactory<KtFirDiagnostic.UnsafeImplicitInvokeCall> { diagnostic ->
|
||||
val callExpression = diagnostic.psi.parentOfType<KtCallExpression>(withSelf = true) ?: return@diagnosticFixFactory emptyList()
|
||||
val callingFunctionalVariableInLocalScope =
|
||||
isCallingFunctionalTypeVariableInLocalScope(callExpression) ?: return@diagnosticFixFactory emptyList()
|
||||
createWrapWithSafeLetCallInputForNullableExpression(
|
||||
callExpression.calleeExpression,
|
||||
isImplicitInvokeCallToMemberProperty = !callingFunctionalVariableInLocalScope
|
||||
)
|
||||
}
|
||||
|
||||
private fun KtAnalysisSession.isCallingFunctionalTypeVariableInLocalScope(callExpression: KtCallExpression): Boolean? {
|
||||
val calleeName = callExpression.calleeExpression?.text ?: return null
|
||||
val callSite = callExpression.parent as? KtQualifiedExpression ?: callExpression
|
||||
val functionalVariableSymbol = (callExpression.resolveCall() as? KtVariableWithInvokeFunctionCall)?.target ?: return false
|
||||
val localScope = callExpression.containingKtFile.getScopeContextForPosition(callSite)
|
||||
// If no symbol in the local scope contains the called symbol, then the symbol must be a member symbol.
|
||||
return localScope.scopes.getCallableSymbols { it.identifierOrNullIfSpecial == calleeName }.any { it == functionalVariableSymbol }
|
||||
}
|
||||
|
||||
val forUnsafeInfixCall = diagnosticFixFactory<KtFirDiagnostic.UnsafeInfixCall> { diagnostic ->
|
||||
createWrapWithSafeLetCallInputForNullableExpressionIfMoreThanImmediateParentIsWrapped(diagnostic.receiverExpression)
|
||||
}
|
||||
|
||||
val forUnsafeOperatorCall = diagnosticFixFactory<KtFirDiagnostic.UnsafeOperatorCall> { diagnostic ->
|
||||
createWrapWithSafeLetCallInputForNullableExpressionIfMoreThanImmediateParentIsWrapped(diagnostic.receiverExpression)
|
||||
}
|
||||
|
||||
val forArgumentTypeMismatch = diagnosticFixFactory<KtFirDiagnostic.ArgumentTypeMismatch> { diagnostic ->
|
||||
if (diagnostic.isMismatchDueToNullability) createWrapWithSafeLetCallInputForNullableExpression(diagnostic.psi.wrappingExpressionOrSelf)
|
||||
else emptyList()
|
||||
}
|
||||
|
||||
private fun KtAnalysisSession.createWrapWithSafeLetCallInputForNullableExpressionIfMoreThanImmediateParentIsWrapped(
|
||||
nullableExpression: KtExpression?,
|
||||
isImplicitInvokeCallToMemberProperty: Boolean = false,
|
||||
): List<IntentionAction> {
|
||||
val surroundingExpression = nullableExpression?.surroundingExpression
|
||||
if (
|
||||
surroundingExpression == null ||
|
||||
// If the surrounding expression is at a place that accepts null value, then we don't provide wrap with let call because the
|
||||
// plain safe call operator (?.) is a better fix.
|
||||
isExpressionAtNullablePosition(surroundingExpression)
|
||||
) {
|
||||
return emptyList()
|
||||
}
|
||||
// In addition, if there is no parent that is at a nullable position, then we don't offer wrapping with let either because
|
||||
// it still doesn't fix the code. Hence, the plain safe call operator is a better fix.
|
||||
val surroundingNullableExpression = findParentExpressionAtNullablePosition(nullableExpression) ?: return emptyList()
|
||||
return createWrapWithSafeLetCallInputForNullableExpression(
|
||||
nullableExpression,
|
||||
isImplicitInvokeCallToMemberProperty,
|
||||
surroundingNullableExpression
|
||||
)
|
||||
}
|
||||
|
||||
private fun KtAnalysisSession.createWrapWithSafeLetCallInputForNullableExpression(
|
||||
nullableExpression: KtExpression?,
|
||||
isImplicitInvokeCallToMemberProperty: Boolean = false,
|
||||
surroundingExpression: KtExpression? = findParentExpressionAtNullablePosition(nullableExpression)
|
||||
?: nullableExpression?.surroundingExpression
|
||||
): List<IntentionAction> {
|
||||
if (nullableExpression == null || surroundingExpression == null) return emptyList()
|
||||
val existingNames =
|
||||
nullableExpression.containingKtFile.getScopeContextForPosition(nullableExpression).scopes.getPossibleCallableNames()
|
||||
.mapNotNull { it.identifierOrNullIfSpecial }
|
||||
// Note, the order of the candidate matters. We would prefer the default `it` so the generated code won't need to declare the
|
||||
// variable explicitly.
|
||||
val candidateNames = listOfNotNull("it", getDeclaredParameterNameForArgument(nullableExpression))
|
||||
val suggestedName = FirKotlinNameSuggester.suggestNameByMultipleNames(candidateNames) { it !in existingNames }
|
||||
return listOf(
|
||||
HLQuickFix(
|
||||
surroundingExpression,
|
||||
Input(nullableExpression.createSmartPointer(), suggestedName, isImplicitInvokeCallToMemberProperty),
|
||||
applicator
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun KtAnalysisSession.getDeclaredParameterNameForArgument(argumentExpression: KtExpression): String? {
|
||||
val valueArgument = argumentExpression.parent as? KtValueArgument ?: return null
|
||||
val successCallTarget =
|
||||
(argumentExpression.parentOfType<KtCallExpression>()?.resolveCall()?.targetFunction?.candidates?.singleOrNull()) ?: return null
|
||||
return successCallTarget.valueParameters.getOrNull(valueArgument.argumentIndex)?.name?.identifierOrNullIfSpecial
|
||||
}
|
||||
|
||||
private fun KtAnalysisSession.findParentExpressionAtNullablePosition(expression: KtExpression?): KtExpression? {
|
||||
if (expression == null) return null
|
||||
var current = expression.surroundingExpression
|
||||
while (current != null && !isExpressionAtNullablePosition(current)) {
|
||||
current = current.surroundingExpression
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
private fun KtAnalysisSession.isExpressionAtNullablePosition(expression: KtExpression): Boolean {
|
||||
val parent = expression.parent
|
||||
return when {
|
||||
parent is KtProperty && expression == parent.initializer -> {
|
||||
if (parent.typeReference == null) return true
|
||||
val symbol = parent.getSymbol()
|
||||
(symbol as? KtCallableSymbol)?.annotatedType?.type?.isMarkedNullable ?: true
|
||||
}
|
||||
parent is KtValueArgument && expression == parent.getArgumentExpression() -> {
|
||||
// In the following logic, if call is missing, unresolved, or contains error, we just stop here so the wrapped call would be
|
||||
// inserted here.
|
||||
val functionCall = parent.getParentOfType<KtCallExpression>(strict = true) ?: return true
|
||||
val resolvedCall = functionCall.resolveCall() ?: return true
|
||||
return doesFunctionAcceptNull(resolvedCall, parent.argumentIndex) ?: true
|
||||
}
|
||||
parent is KtBinaryExpression -> {
|
||||
if (parent.operationToken in KtTokens.ALL_ASSIGNMENTS && parent.left == expression) {
|
||||
// If current expression is an l-value in an assignment, just keep going up because one cannot assign to a let call.
|
||||
return false
|
||||
}
|
||||
val resolvedCall = parent.resolveCall()
|
||||
when {
|
||||
resolvedCall != null -> {
|
||||
// The binary expression is a call to some function
|
||||
val isInExpression = parent.operationToken in OperatorConventions.IN_OPERATIONS
|
||||
val expressionIsArg = when {
|
||||
parent.left == expression -> isInExpression
|
||||
parent.right == expression -> !isInExpression
|
||||
else -> return true
|
||||
}
|
||||
doesFunctionAcceptNull(resolvedCall, if (expressionIsArg) 0 else -1) ?: true
|
||||
}
|
||||
parent.operationToken == KtTokens.EQ -> {
|
||||
// The binary expression is a variable assignment
|
||||
parent.left?.getKtType()?.isMarkedNullable ?: true
|
||||
}
|
||||
// The binary expression is some unrecognized constructs so we stop here.
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
// Qualified expression can always just be updated with a safe call operator to to make it accept nullable receiver. Hence we
|
||||
// don't want to offer the wrap with let call quickfix.
|
||||
parent is KtQualifiedExpression && parent.receiverExpression == expression -> true
|
||||
// Ideally we should do more analysis on the control structure to determine if the type can actually allow null here. But that
|
||||
// may be too fancy and can be counter-intuitive to user.
|
||||
parent is KtContainerNodeForControlStructureBody -> true
|
||||
// Again, for simplicity's sake, we treat block as a place that can accept expression of any type. This is not strictly true
|
||||
// for lambda expressions, but it results in a more deterministic behavior.
|
||||
parent is KtBlockExpression -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the called function can accept null for the argument at the given index. If the index is -1, then we check the receiver
|
||||
* type. The function returns null if any necessary assumptions are not met. For example, if the call is not resolved to a unique
|
||||
* function or the function doesn't have a parameter at the given index. Then caller can do whatever needed to cover such cases.
|
||||
*/
|
||||
private fun KtAnalysisSession.doesFunctionAcceptNull(call: KtCall, index: Int): Boolean? {
|
||||
val symbol = call.targetFunction.candidates.singleOrNull() ?: return null
|
||||
if (index == -1) {
|
||||
// Null extension receiver means the function does not accept extension receiver and hence cannot be invoked on a nullable
|
||||
// value.
|
||||
return (symbol as? KtCallableSymbol)?.receiverType?.type?.isMarkedNullable == true
|
||||
}
|
||||
return symbol.valueParameters.getOrNull(index)?.annotatedType?.type?.isMarkedNullable
|
||||
}
|
||||
|
||||
private val KtExpression.surroundingExpression: KtExpression?
|
||||
get() {
|
||||
var current: PsiElement? = parent
|
||||
while (true) {
|
||||
// Never go above declarations or control structure so that the wrap-with-let quickfix only applies to a "small" scope
|
||||
// around the nullable expression.
|
||||
if (current == null ||
|
||||
current is KtContainerNodeForControlStructureBody ||
|
||||
current is KtWhenEntry ||
|
||||
current is KtParameter ||
|
||||
current is KtProperty ||
|
||||
current is KtReturnExpression ||
|
||||
current is KtDeclaration ||
|
||||
current is KtBlockExpression
|
||||
) {
|
||||
return null
|
||||
}
|
||||
val parent = current.parent
|
||||
if (current is KtExpression &&
|
||||
// We skip parenthesized expression and labeled expressions.
|
||||
current !is KtParenthesizedExpression && current !is KtLabeledExpression &&
|
||||
// We skip KtCallExpression if it's the `selectorExpression` of a qualified expression because the selector expression is
|
||||
// not an actual expression that can be swapped for any arbitrary expressions.
|
||||
(parent !is KtQualifiedExpression || parent.selectorExpression != current)
|
||||
) {
|
||||
return current
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
private val PsiElement.wrappingExpressionOrSelf: KtExpression? get() = parentOfType(withSelf = true)
|
||||
}
|
||||
Generated
+168
@@ -1645,4 +1645,172 @@ public class HighLevelQuickFixTestGenerated extends AbstractHighLevelQuickFixTes
|
||||
runTest("idea/testData/quickfix/when/twoElseBranchesInWhen.kt");
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/wrapWithSafeLetCall")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class WrapWithSafeLetCall extends AbstractHighLevelQuickFixTest {
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
|
||||
}
|
||||
|
||||
public void testAllFilesPresentInWrapWithSafeLetCall() throws Exception {
|
||||
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/quickfix/wrapWithSafeLetCall"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), null, true);
|
||||
}
|
||||
|
||||
@TestMetadata("argumentNullable.kt")
|
||||
public void testArgumentNullable() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/argumentNullable.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("callWithLambdaArgument.kt")
|
||||
public void testCallWithLambdaArgument() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("chainedCallTypeMismatch.kt")
|
||||
public void testChainedCallTypeMismatch() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("chainedCallTypeMismatch2.kt")
|
||||
public void testChainedCallTypeMismatch2() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("chainedUnsafeCall.kt")
|
||||
public void testChainedUnsafeCall() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/chainedUnsafeCall.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("expressionUnsafeCall.kt")
|
||||
public void testExpressionUnsafeCall() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/expressionUnsafeCall.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("extensionMethod.kt")
|
||||
public void testExtensionMethod() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/extensionMethod.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("extentionFunctionCall.kt")
|
||||
public void testExtentionFunctionCall() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/extentionFunctionCall.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("extentionFunctionCall2.kt")
|
||||
public void testExtentionFunctionCall2() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/extentionFunctionCall2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("inOperator.kt")
|
||||
public void testInOperator() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/inOperator.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("insideLet.kt")
|
||||
public void testInsideLet() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/insideLet.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("invokeFuncUnsafe.kt")
|
||||
public void testInvokeFuncUnsafe() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/invokeFuncUnsafe.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("invokeFunctionType.kt")
|
||||
public void testInvokeFunctionType() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/invokeFunctionType.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("invokeFunctionType2.kt")
|
||||
public void testInvokeFunctionType2() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/invokeFunctionType2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("invokeUnsafe.kt")
|
||||
public void testInvokeUnsafe() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/invokeUnsafe.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("nameAlreadyInScope.kt")
|
||||
public void testNameAlreadyInScope() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("nullableBinaryExpression.kt")
|
||||
public void testNullableBinaryExpression() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("nullableReceiver.kt")
|
||||
public void testNullableReceiver() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/nullableReceiver.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("objectQualifier.kt")
|
||||
public void testObjectQualifier() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/objectQualifier.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("refactorNullableFunctionTypeProperty1.kt")
|
||||
public void testRefactorNullableFunctionTypeProperty1() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("refactorNullableFunctionTypeProperty2.kt")
|
||||
public void testRefactorNullableFunctionTypeProperty2() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("unsafe.kt")
|
||||
public void testUnsafe() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/unsafe.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("unsafe2.kt")
|
||||
public void testUnsafe2() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/unsafe2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("unsafeForNullableParameter.kt")
|
||||
public void testUnsafeForNullableParameter() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/unsafeForNullableParameter.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("unstableValue.kt")
|
||||
public void testUnstableValue() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/unstableValue.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions1.kt")
|
||||
public void testWrapAllNonNullablePositions1() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions2.kt")
|
||||
public void testWrapAllNonNullablePositions2() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions3.kt")
|
||||
public void testWrapAllNonNullablePositions3() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions4.kt")
|
||||
public void testWrapAllNonNullablePositions4() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions5.kt")
|
||||
public void testWrapAllNonNullablePositions5() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions6.kt")
|
||||
public void testWrapAllNonNullablePositions6() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.kt");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
package org.jetbrains.kotlin.idea.frontend.api.calls
|
||||
|
||||
import org.jetbrains.kotlin.idea.frontend.api.diagnostics.KtDiagnostic
|
||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.*
|
||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFunctionLikeSymbol
|
||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtVariableLikeSymbol
|
||||
|
||||
/**
|
||||
* Represents direct or indirect (via invoke) function call from Kotlin code
|
||||
*/
|
||||
sealed class KtCall {
|
||||
abstract val isErrorCall: Boolean
|
||||
abstract val targetFunction: KtCallTarget
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,7 +24,10 @@ sealed class KtCall {
|
||||
* f() // functional type call
|
||||
* }
|
||||
*/
|
||||
class KtFunctionalTypeVariableCall(val target: KtVariableLikeSymbol) : KtCall() {
|
||||
class KtFunctionalTypeVariableCall(
|
||||
val target: KtVariableLikeSymbol,
|
||||
override val targetFunction: KtCallTarget
|
||||
) : KtCall() {
|
||||
override val isErrorCall: Boolean get() = false
|
||||
}
|
||||
|
||||
@@ -30,7 +35,6 @@ class KtFunctionalTypeVariableCall(val target: KtVariableLikeSymbol) : KtCall()
|
||||
* Direct or indirect call of function declared by user
|
||||
*/
|
||||
sealed class KtDeclaredFunctionCall : KtCall() {
|
||||
abstract val targetFunction: KtCallTarget
|
||||
override val isErrorCall: Boolean
|
||||
get() = targetFunction is KtErrorCallTarget
|
||||
}
|
||||
@@ -46,10 +50,8 @@ sealed class KtDeclaredFunctionCall : KtCall() {
|
||||
*/
|
||||
class KtVariableWithInvokeFunctionCall(
|
||||
val target: KtVariableLikeSymbol,
|
||||
val invokeFunction: KtCallTarget,
|
||||
) : KtDeclaredFunctionCall() {
|
||||
override val targetFunction: KtCallTarget get() = invokeFunction
|
||||
}
|
||||
override val targetFunction: KtCallTarget
|
||||
) : KtDeclaredFunctionCall()
|
||||
|
||||
/**
|
||||
* Simple function call, e.g.,
|
||||
|
||||
+17
-15
@@ -10,32 +10,30 @@ import org.jetbrains.kotlin.fir.FirSourceElement
|
||||
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
|
||||
import org.jetbrains.kotlin.fir.expressions.FirQualifiedAccessExpression
|
||||
import org.jetbrains.kotlin.fir.expressions.FirSafeCallExpression
|
||||
import org.jetbrains.kotlin.fir.psi
|
||||
import org.jetbrains.kotlin.fir.references.FirErrorNamedReference
|
||||
import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference
|
||||
import org.jetbrains.kotlin.fir.references.impl.FirSimpleNamedReference
|
||||
import org.jetbrains.kotlin.fir.render
|
||||
import org.jetbrains.kotlin.fir.resolve.calls.FirErrorReferenceWithCandidate
|
||||
import org.jetbrains.kotlin.name.CallableId
|
||||
import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol
|
||||
import org.jetbrains.kotlin.idea.fir.getCandidateSymbols
|
||||
import org.jetbrains.kotlin.idea.fir.isImplicitFunctionCall
|
||||
import org.jetbrains.kotlin.idea.fir.low.level.api.api.getOrBuildFir
|
||||
import org.jetbrains.kotlin.idea.fir.low.level.api.api.getOrBuildFirSafe
|
||||
import org.jetbrains.kotlin.idea.frontend.api.*
|
||||
import org.jetbrains.kotlin.idea.frontend.api.calls.*
|
||||
import org.jetbrains.kotlin.idea.frontend.api.components.KtCallResolver
|
||||
import org.jetbrains.kotlin.idea.frontend.api.diagnostics.KtNonBoundToPsiErrorDiagnostic
|
||||
import org.jetbrains.kotlin.idea.frontend.api.fir.KtFirAnalysisSession
|
||||
import org.jetbrains.kotlin.idea.frontend.api.fir.buildSymbol
|
||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.*
|
||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFunctionLikeSymbol
|
||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFunctionSymbol
|
||||
import org.jetbrains.kotlin.idea.frontend.api.symbols.KtVariableLikeSymbol
|
||||
import org.jetbrains.kotlin.idea.frontend.api.tokens.ValidityToken
|
||||
import org.jetbrains.kotlin.idea.frontend.api.withValidityAssertion
|
||||
import org.jetbrains.kotlin.idea.references.FirReferenceResolveHelper
|
||||
import org.jetbrains.kotlin.idea.util.getElementTextInContext
|
||||
import org.jetbrains.kotlin.name.CallableId
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
import org.jetbrains.kotlin.psi.KtBinaryExpression
|
||||
import org.jetbrains.kotlin.psi.KtCallExpression
|
||||
import org.jetbrains.kotlin.psi.KtElement
|
||||
|
||||
internal class KtFirCallResolver(
|
||||
override val analysisSession: KtFirAnalysisSession,
|
||||
@@ -74,16 +72,19 @@ internal class KtFirCallResolver(
|
||||
}
|
||||
}
|
||||
|
||||
private fun FirFunctionCall.createCallByVariableLikeSymbolCall(variableLikeSymbol: KtVariableLikeSymbol) =
|
||||
when (val callReference = calleeReference) {
|
||||
private fun FirFunctionCall.createCallByVariableLikeSymbolCall(variableLikeSymbol: KtVariableLikeSymbol): KtCall? {
|
||||
return when (val callReference = calleeReference) {
|
||||
is FirResolvedNamedReference -> {
|
||||
val functionSymbol = callReference.resolvedSymbol as? FirNamedFunctionSymbol
|
||||
when (functionSymbol?.callableId) {
|
||||
null -> null
|
||||
in kotlinFunctionInvokeCallableIds -> KtFunctionalTypeVariableCall(variableLikeSymbol)
|
||||
else -> (callReference.resolvedSymbol.fir.buildSymbol(firSymbolBuilder) as? KtFunctionSymbol)
|
||||
?.let { KtVariableWithInvokeFunctionCall(variableLikeSymbol, KtSuccessCallTarget(it)) }
|
||||
}
|
||||
val callableId = functionSymbol?.callableId ?: return null
|
||||
(callReference.resolvedSymbol.fir.buildSymbol(firSymbolBuilder) as? KtFunctionSymbol)
|
||||
?.let {
|
||||
if (callableId in kotlinFunctionInvokeCallableIds) {
|
||||
KtFunctionalTypeVariableCall(variableLikeSymbol, KtSuccessCallTarget(it))
|
||||
} else {
|
||||
KtVariableWithInvokeFunctionCall(variableLikeSymbol, KtSuccessCallTarget(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
is FirErrorNamedReference -> KtVariableWithInvokeFunctionCall(
|
||||
variableLikeSymbol,
|
||||
@@ -91,6 +92,7 @@ internal class KtFirCallResolver(
|
||||
)
|
||||
else -> error("Unexpected call reference ${callReference::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun FirFunctionCall.asSimpleFunctionCall(): KtFunctionCall? {
|
||||
val target = when (val calleeReference = calleeReference) {
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ class KtFirInvokeFunctionReference(expression: KtCallExpression) : KtInvokeFunct
|
||||
override fun KtAnalysisSession.resolveToSymbols(): Collection<KtSymbol> {
|
||||
val call = expression.resolveCall() ?: return emptyList()
|
||||
if (call is KtVariableWithInvokeFunctionCall) {
|
||||
return call.invokeFunction.candidates
|
||||
return call.targetFunction.candidates
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Foo {
|
||||
val f: ((() -> Unit) -> String)?
|
||||
}
|
||||
|
||||
fun test(foo: Foo) {
|
||||
bar(foo.<caret>f {})
|
||||
}
|
||||
|
||||
fun bar(s: String) {}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Foo {
|
||||
val f: ((() -> Unit) -> String)?
|
||||
}
|
||||
|
||||
fun test(foo: Foo) {
|
||||
foo.f?<caret>.let { bar(it {}) }
|
||||
}
|
||||
|
||||
fun bar(s: String) {}
|
||||
@@ -0,0 +1,14 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// ERROR: Type mismatch: inferred type is String? but String was expected
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Foo {
|
||||
val f: ((() -> Unit) -> String)?
|
||||
}
|
||||
|
||||
fun test(foo: Foo) {
|
||||
bar(foo.<caret>f {})
|
||||
}
|
||||
|
||||
fun bar(s: String) {}
|
||||
@@ -0,0 +1,14 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// ERROR: Type mismatch: inferred type is String? but String was expected
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Foo {
|
||||
val f: ((() -> Unit) -> String)?
|
||||
}
|
||||
|
||||
fun test(foo: Foo) {
|
||||
bar(foo.f?.let { it {} })
|
||||
}
|
||||
|
||||
fun bar(s: String) {}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun Int.foo(x: Int) = this + x
|
||||
|
||||
val arg: Int? = 42
|
||||
|
||||
val res = 24.hashCode().foo(<caret>arg) + 1
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun Int.foo(x: Int) = this + x
|
||||
|
||||
val arg: Int? = 42
|
||||
|
||||
val res = arg?.let { 24.hash<caret>Code().foo(it) + 1 }
|
||||
@@ -1,8 +1,9 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
// ERROR: Operator call corresponds to a dot-qualified call 'arg?.let { 24.hashCode().foo(it) }.plus(1)' which is not allowed on a nullable receiver 'arg?.let { 24.hashCode().foo(it) }'.
|
||||
|
||||
fun Int.foo(x: Int) = this + x
|
||||
|
||||
val arg: Int? = 42
|
||||
|
||||
val res = 24.hashCode().foo(<caret>arg)
|
||||
val res = 24.hashCode().foo(<caret>arg) + 1
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
// ERROR: Operator call corresponds to a dot-qualified call 'arg?.let { 24.hashCode().foo(it) }.plus(1)' which is not allowed on a nullable receiver 'arg?.let { 24.hashCode().foo(it) }'.
|
||||
|
||||
fun Int.foo(x: Int) = this + x
|
||||
|
||||
val arg: Int? = 42
|
||||
|
||||
val res = arg?.let { 24.hashCode().foo(it) }
|
||||
val res = arg?.let { 24.hashCode().foo(it) } + 1
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
// ERROR: Operator call corresponds to a dot-qualified call 'arg?.let { 24.hashCode().foo(it) }.plus(1)' which is not allowed on a nullable receiver 'arg?.let { 24.hashCode().foo(it) }'.
|
||||
|
||||
fun Int.foo(x: Int) = this + x
|
||||
|
||||
val arg: Int? = 42
|
||||
|
||||
val res: Int = 24.hashCode().foo(<caret>arg) + 1
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
// ERROR: Operator call corresponds to a dot-qualified call 'arg?.let { 24.hashCode().foo(it) }.plus(1)' which is not allowed on a nullable receiver 'arg?.let { 24.hashCode().foo(it) }'.
|
||||
|
||||
fun Int.foo(x: Int) = this + x
|
||||
|
||||
val arg: Int? = 42
|
||||
|
||||
val res: Int = arg?.let { 24.hashCode().foo(it) } + 1
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// ACTION: Add non-null asserted (!!) call
|
||||
// ACTION: Replace overloaded operator with function call
|
||||
// ACTION: Replace with safe (?.) call
|
||||
// ACTION: Surround with null check
|
||||
|
||||
fun test(l: List<String>?, s: String) {
|
||||
if (s <caret>in l) {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// ACTION: Add non-null asserted (!!) call
|
||||
// ACTION: Replace overloaded operator with function call
|
||||
// ACTION: Replace with safe (?.) call
|
||||
// ACTION: Surround with null check
|
||||
|
||||
fun test(l: List<String>?, s: String) {
|
||||
l?.let<caret> { if (s in it) {} }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// "Wrap with '?.let { ... }' call" "false"
|
||||
// ACTION: Add non-null asserted (!!) call
|
||||
// ACTION: Replace overloaded operator with function call
|
||||
// ACTION: Replace with safe (?.) call
|
||||
// ACTION: Surround with null check
|
||||
// ERROR: Operator call corresponds to a dot-qualified call 'l.contains(s)' which is not allowed on a nullable receiver 'l'.
|
||||
|
||||
fun test(l: List<String>?, s: String) {
|
||||
if (s <caret>in l) {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun foo(x: String?, y: String) {
|
||||
y.let { bar(<caret>x, it) }
|
||||
}
|
||||
|
||||
fun bar(s: String, t: String) = s.hashCode() + t.hashCode()
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun foo(x: String?, y: String) {
|
||||
y.let { x?.l<caret>et { s -> bar(s, it) } }
|
||||
}
|
||||
|
||||
fun bar(s: String, t: String) = s.hashCode() + t.hashCode()
|
||||
@@ -0,0 +1,11 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
val it = ""
|
||||
|
||||
fun test(s: String?) {
|
||||
val name = ""
|
||||
bar(<caret>s)
|
||||
}
|
||||
|
||||
fun bar(name: String) {}
|
||||
@@ -0,0 +1,11 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
val it = ""
|
||||
|
||||
fun test(s: String?) {
|
||||
val name = ""
|
||||
s?.let { it1 -> bar(it1) }
|
||||
}
|
||||
|
||||
fun bar(name: String) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface A
|
||||
|
||||
operator fun A?.plus(a: A?): A? = this
|
||||
|
||||
fun test(a1: A, a2: A) {
|
||||
notNull(<caret>a1 + a2)
|
||||
}
|
||||
|
||||
fun notNull(t: A): A = t
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface A
|
||||
|
||||
operator fun A?.plus(a: A?): A? = this
|
||||
|
||||
fun test(a1: A, a2: A) {
|
||||
(a1 + a2<caret>)?.let { notNull(it) }
|
||||
}
|
||||
|
||||
fun notNull(t: A): A = t
|
||||
@@ -0,0 +1,12 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface A
|
||||
|
||||
operator fun A?.plus(a: A?): A? = this
|
||||
|
||||
fun test(a1: A, a2: A) {
|
||||
notNull(<caret>a1 + a2)
|
||||
}
|
||||
|
||||
fun notNull(t: A): A = t
|
||||
@@ -0,0 +1,12 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface A
|
||||
|
||||
operator fun A?.plus(a: A?): A? = this
|
||||
|
||||
fun test(a1: A, a2: A) {
|
||||
a1 + a2?.let { notNull(it) }
|
||||
}
|
||||
|
||||
fun notNull(t: A): A = t
|
||||
@@ -0,0 +1,16 @@
|
||||
// "Wrap with '?.let { ... }' call" "false"
|
||||
// ACTION: Add non-null asserted (!!) call
|
||||
// ACTION: Convert to run
|
||||
// ACTION: Convert to with
|
||||
// ACTION: Replace with safe (?.) call
|
||||
// ACTION: Surround with null check
|
||||
// ERROR: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type B?
|
||||
// WITH_RUNTIME
|
||||
|
||||
class A {
|
||||
fun foo() {}
|
||||
}
|
||||
class B(val a: A)
|
||||
fun test(b: B?) {
|
||||
b<caret>.a.foo() // b.a is UNSAFE_CALL
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Str {
|
||||
val foo: (() -> Unit)?
|
||||
}
|
||||
|
||||
object Str2 {
|
||||
val foo2: (Str.() -> Unit)? = null
|
||||
|
||||
fun bar(s: Str) {
|
||||
s.<caret>foo()
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Str {
|
||||
val foo: (() -> Unit)?
|
||||
}
|
||||
|
||||
object Str2 {
|
||||
val foo2: (Str.() -> Unit)? = null
|
||||
|
||||
fun bar(s: Str) {
|
||||
s.foo?.let { it() }
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Str {
|
||||
val foo: (() -> Unit)?
|
||||
}
|
||||
|
||||
object Str2 {
|
||||
val foo2: (Str.() -> Unit)? = null
|
||||
|
||||
fun bar(s: Str) {
|
||||
s.<caret>foo2()
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Str {
|
||||
val foo: (() -> Unit)?
|
||||
}
|
||||
|
||||
object Str2 {
|
||||
val foo2: (Str.() -> Unit)? = null
|
||||
|
||||
fun bar(s: Str) {
|
||||
fo<caret>o2?.let { s.it() }
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Str {
|
||||
val foo: (() -> Unit)?
|
||||
}
|
||||
|
||||
object Str2 {
|
||||
val foo2: (Str.() -> Unit)? = null
|
||||
|
||||
fun bar(s: Str) {
|
||||
s.<caret>foo2()
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// ERROR: Expression 'it' of type 'Unit' cannot be invoked as a function. The function 'invoke()' is not found
|
||||
// ERROR: Function invocation 'foo2()' expected
|
||||
// ERROR: Reference has a nullable type '(Str.() -> Unit)?', use explicit '?.invoke()' to make a function-like call instead
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface Str {
|
||||
val foo: (() -> Unit)?
|
||||
}
|
||||
|
||||
object Str2 {
|
||||
val foo2: (Str.() -> Unit)? = null
|
||||
|
||||
fun bar(s: Str) {
|
||||
s.foo2?.let { it() }
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// "Wrap with '?.let { ... }' call" "false"
|
||||
// ACTION: Add 's =' to argument
|
||||
// ACTION: Add non-null asserted (!!) call
|
||||
// ACTION: Replace with safe (?.) call
|
||||
// ACTION: Surround with null check
|
||||
// DISABLE-ERRORS
|
||||
// WITH_RUNTIME
|
||||
fun foo(s: String?) {}
|
||||
|
||||
fun bar(s: String?) {
|
||||
foo(s<caret>.substring(1))
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
nullable(nullable(notNull(notNull(<caret>s))))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
fun nullable(name: String?): String = ""
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
nullable(nullable(s?.let { notNull<caret>(notNull(it)) }))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
fun nullable(name: String?): String = ""
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
nullable(nullable(notNull(notNull(<caret>s))))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
fun nullable(name: String?): String = ""
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// ERROR: Type mismatch: inferred type is String? but String was expected
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
nullable(nullable(notNull(s?.let { notNull(it) })))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
fun nullable(name: String?): String = ""
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
val s2 = notNull(notNull(<caret>s))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
val s2 = s?.let { notNull<caret>(notNull(it)) }
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
val s2 = notNull(notNull(<caret>s))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// ERROR: Type mismatch: inferred type is String? but String was expected
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
val s2 = notNull(s?.let { notNull(it) })
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
notNull(notNull(<caret>s))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
s?.let { notNull<caret>(notNull(it)) }
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
notNull(notNull(<caret>s))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// ERROR: Type mismatch: inferred type is String? but String was expected
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?) {
|
||||
notNull(s?.let { notNull(it) })
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?): String? {
|
||||
if (true) {
|
||||
notNull(notNull(<caret>s))
|
||||
}
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?): String? {
|
||||
if (true) {
|
||||
s?.let { notNull<caret>(notNull(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
@@ -0,0 +1,10 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?): String? {
|
||||
if (true) {
|
||||
notNull(notNull(<caret>s))
|
||||
}
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// ERROR: A 'return' expression required in a function with a block body ('{...}')
|
||||
// ERROR: Type mismatch: inferred type is String? but String was expected
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?): String? {
|
||||
if (true) {
|
||||
notNull(s?.let { notNull(it) })
|
||||
}
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?): String {
|
||||
while (true) notNull(notNull(<caret>s))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?): String {
|
||||
while (true) s?.let { notNull<caret>(notNull(it)) }
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?): String {
|
||||
while (true) notNull(notNull(<caret>s))
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// ERROR: Type mismatch: inferred type is String? but String was expected
|
||||
// WITH_RUNTIME
|
||||
|
||||
fun test(s: String?): String {
|
||||
while (true) notNull(s?.let { notNull(it) })
|
||||
}
|
||||
|
||||
fun notNull(name: String): String = name
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// ACTION: Add 'a =' to argument
|
||||
// ACTION: Add non-null asserted (!!) call
|
||||
// ACTION: Flip '+'
|
||||
// ACTION: Introduce local variable
|
||||
// ACTION: Replace overloaded operator with function call
|
||||
// ACTION: Replace with safe (?.) call
|
||||
// ACTION: Surround with null check
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface A {
|
||||
operator fun plus(a: A): A = this
|
||||
}
|
||||
|
||||
fun test(a1: A?, a2: A) {
|
||||
notNull(a1 <caret>+ a2)
|
||||
}
|
||||
|
||||
fun notNull(a: A): A = a
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// "Wrap with '?.let { ... }' call" "true"
|
||||
// ACTION: Add 'a =' to argument
|
||||
// ACTION: Add non-null asserted (!!) call
|
||||
// ACTION: Flip '+'
|
||||
// ACTION: Introduce local variable
|
||||
// ACTION: Replace overloaded operator with function call
|
||||
// ACTION: Replace with safe (?.) call
|
||||
// ACTION: Surround with null check
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface A {
|
||||
operator fun plus(a: A): A = this
|
||||
}
|
||||
|
||||
fun test(a1: A?, a2: A) {
|
||||
a1?.let { n<caret>otNull(it + a2) }
|
||||
}
|
||||
|
||||
fun notNull(a: A): A = a
|
||||
@@ -0,0 +1,21 @@
|
||||
// "Wrap with '?.let { ... }' call" "false"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// ACTION: Add 'a =' to argument
|
||||
// ACTION: Add non-null asserted (!!) call
|
||||
// ACTION: Flip '+'
|
||||
// ACTION: Introduce local variable
|
||||
// ACTION: Replace overloaded operator with function call
|
||||
// ACTION: Replace with safe (?.) call
|
||||
// ACTION: Surround with null check
|
||||
// ERROR: Operator call corresponds to a dot-qualified call 'a1.plus(a2)' which is not allowed on a nullable receiver 'a1'.
|
||||
// WITH_RUNTIME
|
||||
|
||||
interface A {
|
||||
operator fun plus(a: A): A = this
|
||||
}
|
||||
|
||||
fun test(a1: A?, a2: A) {
|
||||
notNull(a1 <caret>+ a2)
|
||||
}
|
||||
|
||||
fun notNull(a: A): A = a
|
||||
@@ -15545,11 +15545,21 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/argumentNullable.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("callWithLambdaArgument.kt")
|
||||
public void testCallWithLambdaArgument() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("chainedCallTypeMismatch.kt")
|
||||
public void testChainedCallTypeMismatch() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("chainedCallTypeMismatch2.kt")
|
||||
public void testChainedCallTypeMismatch2() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("chainedUnsafeCall.kt")
|
||||
public void testChainedUnsafeCall() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/chainedUnsafeCall.kt");
|
||||
@@ -15575,6 +15585,11 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/extentionFunctionCall2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("inOperator.kt")
|
||||
public void testInOperator() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/inOperator.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("insideLet.kt")
|
||||
public void testInsideLet() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/insideLet.kt");
|
||||
@@ -15600,11 +15615,36 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/invokeUnsafe.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("nameAlreadyInScope.kt")
|
||||
public void testNameAlreadyInScope() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("nullableBinaryExpression.kt")
|
||||
public void testNullableBinaryExpression() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("nullableReceiver.kt")
|
||||
public void testNullableReceiver() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/nullableReceiver.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("objectQualifier.kt")
|
||||
public void testObjectQualifier() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/objectQualifier.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("refactorNullableFunctionTypeProperty1.kt")
|
||||
public void testRefactorNullableFunctionTypeProperty1() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("refactorNullableFunctionTypeProperty2.kt")
|
||||
public void testRefactorNullableFunctionTypeProperty2() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("unsafe.kt")
|
||||
public void testUnsafe() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/unsafe.kt");
|
||||
@@ -15624,6 +15664,36 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
public void testUnstableValue() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/unstableValue.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions1.kt")
|
||||
public void testWrapAllNonNullablePositions1() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions2.kt")
|
||||
public void testWrapAllNonNullablePositions2() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions3.kt")
|
||||
public void testWrapAllNonNullablePositions3() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions4.kt")
|
||||
public void testWrapAllNonNullablePositions4() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions5.kt")
|
||||
public void testWrapAllNonNullablePositions5() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("wrapAllNonNullablePositions6.kt")
|
||||
public void testWrapAllNonNullablePositions6() throws Exception {
|
||||
runTest("idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.kt");
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/wrongLongSuffix")
|
||||
|
||||
Reference in New Issue
Block a user