From e1b542314a5de9382d894ceab3c86ccb10335468 Mon Sep 17 00:00:00 2001 From: Tianyu Geng Date: Wed, 28 Apr 2021 23:20:53 +0200 Subject: [PATCH] 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(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. --- .../kotlin/fir/resolve/calls/Arguments.kt | 2 +- .../jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt | 2 + .../src/org/jetbrains/kotlin/name/Name.java | 7 + .../idea/quickfix/MainKtQuickFixRegistrar.kt | 6 + .../fixes/WrapWithSafeLetCallFixFactories.kt | 342 ++++++++++++++++++ .../HighLevelQuickFixTestGenerated.java | 168 +++++++++ .../kotlin/idea/frontend/api/calls/KtCall.kt | 16 +- .../api/fir/components/KtFirCallResolver.kt | 32 +- .../KtFirInvokeFunctionReference.kt | 2 +- .../callWithLambdaArgument.fir.kt | 12 + .../callWithLambdaArgument.fir.kt.after | 12 + .../callWithLambdaArgument.kt | 14 + .../callWithLambdaArgument.kt.after | 14 + .../chainedCallTypeMismatch.fir.kt | 8 + .../chainedCallTypeMismatch.fir.kt.after | 8 + .../chainedCallTypeMismatch.kt | 3 +- .../chainedCallTypeMismatch.kt.after | 3 +- .../chainedCallTypeMismatch2.kt | 9 + .../chainedCallTypeMismatch2.kt.after | 9 + .../wrapWithSafeLetCall/inOperator.fir.kt | 9 + .../inOperator.fir.kt.after | 9 + .../wrapWithSafeLetCall/inOperator.kt | 10 + .../wrapWithSafeLetCall/insideLet.fir.kt | 8 + .../insideLet.fir.kt.after | 8 + .../wrapWithSafeLetCall/nameAlreadyInScope.kt | 11 + .../nameAlreadyInScope.kt.after | 11 + .../nullableBinaryExpression.fir.kt | 12 + .../nullableBinaryExpression.fir.kt.after | 12 + .../nullableBinaryExpression.kt | 12 + .../nullableBinaryExpression.kt.after | 12 + .../wrapWithSafeLetCall/nullableReceiver.kt | 16 + .../refactorNullableFunctionTypeProperty1.kt | 15 + ...ctorNullableFunctionTypeProperty1.kt.after | 15 + ...factorNullableFunctionTypeProperty2.fir.kt | 15 + ...NullableFunctionTypeProperty2.fir.kt.after | 15 + .../refactorNullableFunctionTypeProperty2.kt | 15 + ...ctorNullableFunctionTypeProperty2.kt.after | 18 + .../unsafeForNullableParameter.kt.after | 12 + .../wrapAllNonNullablePositions1.fir.kt | 9 + .../wrapAllNonNullablePositions1.fir.kt.after | 9 + .../wrapAllNonNullablePositions1.kt | 9 + .../wrapAllNonNullablePositions1.kt.after | 11 + .../wrapAllNonNullablePositions2.fir.kt | 8 + .../wrapAllNonNullablePositions2.fir.kt.after | 8 + .../wrapAllNonNullablePositions2.kt | 8 + .../wrapAllNonNullablePositions2.kt.after | 10 + .../wrapAllNonNullablePositions3.fir.kt | 8 + .../wrapAllNonNullablePositions3.fir.kt.after | 8 + .../wrapAllNonNullablePositions3.kt | 8 + .../wrapAllNonNullablePositions3.kt.after | 10 + .../wrapAllNonNullablePositions4.fir.kt | 10 + .../wrapAllNonNullablePositions4.fir.kt.after | 10 + .../wrapAllNonNullablePositions4.kt | 10 + .../wrapAllNonNullablePositions4.kt.after | 13 + .../wrapAllNonNullablePositions5.fir.kt | 8 + .../wrapAllNonNullablePositions5.fir.kt.after | 8 + .../wrapAllNonNullablePositions5.kt | 8 + .../wrapAllNonNullablePositions5.kt.after | 10 + .../wrapAllNonNullablePositions6.fir.kt | 19 + .../wrapAllNonNullablePositions6.fir.kt.after | 19 + .../wrapAllNonNullablePositions6.kt | 21 ++ .../idea/quickfix/QuickFixTestGenerated.java | 70 ++++ 62 files changed, 1190 insertions(+), 26 deletions(-) create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/fixes/WrapWithSafeLetCallFixFactories.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/inOperator.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/inOperator.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/inOperator.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/insideLet.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/insideLet.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/nullableReceiver.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/unsafeForNullableParameter.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.fir.kt create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.fir.kt.after create mode 100644 idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.kt diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/calls/Arguments.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/calls/Arguments.kt index 30dd9e6aee1..9c2171c1aa7 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/calls/Arguments.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/calls/Arguments.kt @@ -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) 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 7f8e6c7e08a..4d368fae6ee 100644 --- a/compiler/psi/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt +++ b/compiler/psi/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt @@ -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) diff --git a/core/compiler.common/src/org/jetbrains/kotlin/name/Name.java b/core/compiler.common/src/org/jetbrains/kotlin/name/Name.java index 6dfef21186d..17146e6f129 100644 --- a/core/compiler.common/src/org/jetbrains/kotlin/name/Name.java +++ b/core/compiler.common/src/org/jetbrains/kotlin/name/Name.java @@ -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 { @NotNull @@ -85,6 +86,12 @@ public final class Name implements Comparable { } } + @Nullable + public String getIdentifierOrNullIfSpecial() { + if (special) return null; + return asString(); + } + @Override public String toString() { return name; diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/MainKtQuickFixRegistrar.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/MainKtQuickFixRegistrar.kt index b36340b028a..62f77507554 100644 --- a/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/MainKtQuickFixRegistrar.kt +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/MainKtQuickFixRegistrar.kt @@ -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( diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/fixes/WrapWithSafeLetCallFixFactories.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/fixes/WrapWithSafeLetCallFixFactories.kt new file mode 100644 index 00000000000..83c512b6e4a --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/fixes/WrapWithSafeLetCallFixFactories.kt @@ -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, + 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 = 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(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 { diagnostic -> + val nullableExpression = diagnostic.receiverExpression + createWrapWithSafeLetCallInputForNullableExpressionIfMoreThanImmediateParentIsWrapped(nullableExpression) + } + + val forUnsafeImplicitInvokeCall = diagnosticFixFactory { diagnostic -> + val callExpression = diagnostic.psi.parentOfType(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 { diagnostic -> + createWrapWithSafeLetCallInputForNullableExpressionIfMoreThanImmediateParentIsWrapped(diagnostic.receiverExpression) + } + + val forUnsafeOperatorCall = diagnosticFixFactory { diagnostic -> + createWrapWithSafeLetCallInputForNullableExpressionIfMoreThanImmediateParentIsWrapped(diagnostic.receiverExpression) + } + + val forArgumentTypeMismatch = diagnosticFixFactory { diagnostic -> + if (diagnostic.isMismatchDueToNullability) createWrapWithSafeLetCallInputForNullableExpression(diagnostic.psi.wrappingExpressionOrSelf) + else emptyList() + } + + private fun KtAnalysisSession.createWrapWithSafeLetCallInputForNullableExpressionIfMoreThanImmediateParentIsWrapped( + nullableExpression: KtExpression?, + isImplicitInvokeCallToMemberProperty: Boolean = false, + ): List { + 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 { + 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()?.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(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) +} \ No newline at end of file diff --git a/idea/idea-fir/tests/org/jetbrains/kotlin/idea/quickfix/HighLevelQuickFixTestGenerated.java b/idea/idea-fir/tests/org/jetbrains/kotlin/idea/quickfix/HighLevelQuickFixTestGenerated.java index 2220ddbe7ba..1dc7779a842 100644 --- a/idea/idea-fir/tests/org/jetbrains/kotlin/idea/quickfix/HighLevelQuickFixTestGenerated.java +++ b/idea/idea-fir/tests/org/jetbrains/kotlin/idea/quickfix/HighLevelQuickFixTestGenerated.java @@ -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"); + } + } } diff --git a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/calls/KtCall.kt b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/calls/KtCall.kt index cdf7d90a585..19c8f634971 100644 --- a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/calls/KtCall.kt +++ b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/calls/KtCall.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., diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirCallResolver.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirCallResolver.kt index aa5d536eb34..e95fd960864 100644 --- a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirCallResolver.kt +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirCallResolver.kt @@ -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) { diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/references/KtFirInvokeFunctionReference.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/references/KtFirInvokeFunctionReference.kt index 810cabc38ae..bf8d61b2cc7 100644 --- a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/references/KtFirInvokeFunctionReference.kt +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/references/KtFirInvokeFunctionReference.kt @@ -19,7 +19,7 @@ class KtFirInvokeFunctionReference(expression: KtCallExpression) : KtInvokeFunct override fun KtAnalysisSession.resolveToSymbols(): Collection { val call = expression.resolveCall() ?: return emptyList() if (call is KtVariableWithInvokeFunctionCall) { - return call.invokeFunction.candidates + return call.targetFunction.candidates } return emptyList() } diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.fir.kt new file mode 100644 index 00000000000..81ea289bae4 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.fir.kt @@ -0,0 +1,12 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +interface Foo { + val f: ((() -> Unit) -> String)? +} + +fun test(foo: Foo) { + bar(foo.f {}) +} + +fun bar(s: String) {} diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.fir.kt.after new file mode 100644 index 00000000000..ee5a8f17689 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.fir.kt.after @@ -0,0 +1,12 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +interface Foo { + val f: ((() -> Unit) -> String)? +} + +fun test(foo: Foo) { + foo.f?.let { bar(it {}) } +} + +fun bar(s: String) {} diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt b/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt new file mode 100644 index 00000000000..9c7eded012f --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt @@ -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 {}) +} + +fun bar(s: String) {} diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt.after new file mode 100644 index 00000000000..170187057ae --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/callWithLambdaArgument.kt.after @@ -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) {} diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.fir.kt new file mode 100644 index 00000000000..7e3da633cc0 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.fir.kt @@ -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(arg) + 1 diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.fir.kt.after new file mode 100644 index 00000000000..c8c5c10be30 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.fir.kt.after @@ -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.hashCode().foo(it) + 1 } diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt index c1717e3684a..9f1a04458a6 100644 --- a/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt +++ b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt @@ -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(arg) +val res = 24.hashCode().foo(arg) + 1 diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt.after index 0c32237543f..43281ab6ab6 100644 --- a/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt.after +++ b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch.kt.after @@ -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 diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt new file mode 100644 index 00000000000..2a419a79e87 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt @@ -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(arg) + 1 diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt.after new file mode 100644 index 00000000000..4c77835200a --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/chainedCallTypeMismatch2.kt.after @@ -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 diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/inOperator.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/inOperator.fir.kt new file mode 100644 index 00000000000..567a6000b3a --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/inOperator.fir.kt @@ -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?, s: String) { + if (s in l) {} +} diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/inOperator.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/inOperator.fir.kt.after new file mode 100644 index 00000000000..47d22036933 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/inOperator.fir.kt.after @@ -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?, s: String) { + l?.let { if (s in it) {} } +} diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/inOperator.kt b/idea/testData/quickfix/wrapWithSafeLetCall/inOperator.kt new file mode 100644 index 00000000000..ed5b823ae21 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/inOperator.kt @@ -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?, s: String) { + if (s in l) {} +} diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/insideLet.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/insideLet.fir.kt new file mode 100644 index 00000000000..3177eb40747 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/insideLet.fir.kt @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun foo(x: String?, y: String) { + y.let { bar(x, it) } +} + +fun bar(s: String, t: String) = s.hashCode() + t.hashCode() \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/insideLet.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/insideLet.fir.kt.after new file mode 100644 index 00000000000..a77cf9cce03 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/insideLet.fir.kt.after @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun foo(x: String?, y: String) { + y.let { x?.let { s -> bar(s, it) } } +} + +fun bar(s: String, t: String) = s.hashCode() + t.hashCode() \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt b/idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt new file mode 100644 index 00000000000..dc3a8d0fbcf --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt @@ -0,0 +1,11 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +val it = "" + +fun test(s: String?) { + val name = "" + bar(s) +} + +fun bar(name: String) {} diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt.after new file mode 100644 index 00000000000..17167f593cb --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/nameAlreadyInScope.kt.after @@ -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) {} diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.fir.kt new file mode 100644 index 00000000000..ab47ca5e7d1 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.fir.kt @@ -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(a1 + a2) +} + +fun notNull(t: A): A = t diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.fir.kt.after new file mode 100644 index 00000000000..93d7d8ce598 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.fir.kt.after @@ -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 diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt b/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt new file mode 100644 index 00000000000..ab47ca5e7d1 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt @@ -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(a1 + a2) +} + +fun notNull(t: A): A = t diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt.after new file mode 100644 index 00000000000..e807a2f8ee3 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/nullableBinaryExpression.kt.after @@ -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 diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/nullableReceiver.kt b/idea/testData/quickfix/wrapWithSafeLetCall/nullableReceiver.kt new file mode 100644 index 00000000000..b68153e306c --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/nullableReceiver.kt @@ -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.a.foo() // b.a is UNSAFE_CALL +} \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt new file mode 100644 index 00000000000..3c2c9fabf3e --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt @@ -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() + } +} + diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt.after new file mode 100644 index 00000000000..c49732303fe --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty1.kt.after @@ -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() } + } +} + diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.fir.kt new file mode 100644 index 00000000000..d35ca132740 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.fir.kt @@ -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.foo2() + } +} + diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.fir.kt.after new file mode 100644 index 00000000000..0d84e5fcad4 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.fir.kt.after @@ -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) { + foo2?.let { s.it() } + } +} + diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt new file mode 100644 index 00000000000..d35ca132740 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt @@ -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.foo2() + } +} + diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt.after new file mode 100644 index 00000000000..cdbc8bc08e2 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/refactorNullableFunctionTypeProperty2.kt.after @@ -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() } + } +} + diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/unsafeForNullableParameter.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/unsafeForNullableParameter.kt.after new file mode 100644 index 00000000000..164a2517068 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/unsafeForNullableParameter.kt.after @@ -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.substring(1)) +} \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.fir.kt new file mode 100644 index 00000000000..588431b0290 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.fir.kt @@ -0,0 +1,9 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?) { + nullable(nullable(notNull(notNull(s)))) +} + +fun notNull(name: String): String = name +fun nullable(name: String?): String = "" \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.fir.kt.after new file mode 100644 index 00000000000..29b023f5e14 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.fir.kt.after @@ -0,0 +1,9 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?) { + nullable(nullable(s?.let { notNull(notNull(it)) })) +} + +fun notNull(name: String): String = name +fun nullable(name: String?): String = "" \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt new file mode 100644 index 00000000000..588431b0290 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt @@ -0,0 +1,9 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?) { + nullable(nullable(notNull(notNull(s)))) +} + +fun notNull(name: String): String = name +fun nullable(name: String?): String = "" \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt.after new file mode 100644 index 00000000000..30c7e6ff6cb --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions1.kt.after @@ -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 = "" \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.fir.kt new file mode 100644 index 00000000000..ec657b8a715 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.fir.kt @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?) { + val s2 = notNull(notNull(s)) +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.fir.kt.after new file mode 100644 index 00000000000..55ea9773e03 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.fir.kt.after @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?) { + val s2 = s?.let { notNull(notNull(it)) } +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt new file mode 100644 index 00000000000..ec657b8a715 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?) { + val s2 = notNull(notNull(s)) +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt.after new file mode 100644 index 00000000000..04c62edbe3d --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions2.kt.after @@ -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 \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.fir.kt new file mode 100644 index 00000000000..e32437d750a --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.fir.kt @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?) { + notNull(notNull(s)) +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.fir.kt.after new file mode 100644 index 00000000000..133a520ad9c --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.fir.kt.after @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?) { + s?.let { notNull(notNull(it)) } +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt new file mode 100644 index 00000000000..e32437d750a --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?) { + notNull(notNull(s)) +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt.after new file mode 100644 index 00000000000..3c914882cdb --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions3.kt.after @@ -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 \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.fir.kt new file mode 100644 index 00000000000..1c6c7b8003f --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.fir.kt @@ -0,0 +1,10 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?): String? { + if (true) { + notNull(notNull(s)) + } +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.fir.kt.after new file mode 100644 index 00000000000..0b9d84fc1b4 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.fir.kt.after @@ -0,0 +1,10 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?): String? { + if (true) { + s?.let { notNull(notNull(it)) } + } +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt new file mode 100644 index 00000000000..1c6c7b8003f --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt @@ -0,0 +1,10 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?): String? { + if (true) { + notNull(notNull(s)) + } +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt.after new file mode 100644 index 00000000000..a6701df5c98 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions4.kt.after @@ -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 \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.fir.kt new file mode 100644 index 00000000000..4b19f640c04 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.fir.kt @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?): String { + while (true) notNull(notNull(s)) +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.fir.kt.after new file mode 100644 index 00000000000..cb8aef4b9b6 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.fir.kt.after @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?): String { + while (true) s?.let { notNull(notNull(it)) } +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt new file mode 100644 index 00000000000..4b19f640c04 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt @@ -0,0 +1,8 @@ +// "Wrap with '?.let { ... }' call" "true" +// WITH_RUNTIME + +fun test(s: String?): String { + while (true) notNull(notNull(s)) +} + +fun notNull(name: String): String = name \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt.after new file mode 100644 index 00000000000..87954a31d02 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions5.kt.after @@ -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 \ No newline at end of file diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.fir.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.fir.kt new file mode 100644 index 00000000000..2d8f0dc7909 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.fir.kt @@ -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 + a2) +} + +fun notNull(a: A): A = a diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.fir.kt.after b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.fir.kt.after new file mode 100644 index 00000000000..48ac47ddf69 --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.fir.kt.after @@ -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 { notNull(it + a2) } +} + +fun notNull(a: A): A = a diff --git a/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.kt b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.kt new file mode 100644 index 00000000000..de27437ab6d --- /dev/null +++ b/idea/testData/quickfix/wrapWithSafeLetCall/wrapAllNonNullablePositions6.kt @@ -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 + a2) +} + +fun notNull(a: A): A = a diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index 05c481cf0b7..d437b1e48ee 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -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")