Add better quickfix for scope functions #KT-13676 Fixed
This commit is contained in:
committed by
Mikhail Glukhikh
parent
09d8c31433
commit
ab4eb1dd20
@@ -229,6 +229,7 @@ class QuickFixRegistrar : QuickFixContributor {
|
|||||||
UNSAFE_INFIX_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
|
UNSAFE_INFIX_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
|
||||||
UNSAFE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix) // [] only
|
UNSAFE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix) // [] only
|
||||||
UNSAFE_IMPLICIT_INVOKE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
|
UNSAFE_IMPLICIT_INVOKE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
|
||||||
|
UNSAFE_CALL.registerFactory(ReplaceWithSafeCallForScopeFunctionFix)
|
||||||
|
|
||||||
AMBIGUOUS_ANONYMOUS_TYPE_INFERRED.registerActions(SpecifyTypeExplicitlyFix())
|
AMBIGUOUS_ANONYMOUS_TYPE_INFERRED.registerActions(SpecifyTypeExplicitlyFix())
|
||||||
PROPERTY_WITH_NO_TYPE_NO_INITIALIZER.registerActions(SpecifyTypeExplicitlyFix())
|
PROPERTY_WITH_NO_TYPE_NO_INITIALIZER.registerActions(SpecifyTypeExplicitlyFix())
|
||||||
|
|||||||
@@ -24,8 +24,11 @@ import org.jetbrains.kotlin.diagnostics.Diagnostic
|
|||||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||||
import org.jetbrains.kotlin.psi.*
|
import org.jetbrains.kotlin.psi.*
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
|
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
|
||||||
|
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
|
||||||
|
import org.jetbrains.kotlin.resolve.BindingContext
|
||||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
|
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
|
||||||
import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.getImplicitReceiverValue
|
import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.getImplicitReceiverValue
|
||||||
|
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
|
||||||
|
|
||||||
abstract class ReplaceCallFix(
|
abstract class ReplaceCallFix(
|
||||||
expression: KtQualifiedExpression,
|
expression: KtQualifiedExpression,
|
||||||
@@ -81,6 +84,55 @@ class ReplaceWithSafeCallFix(expression: KtDotQualifiedExpression): ReplaceCallF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ReplaceWithSafeCallForScopeFunctionFix(expression: KtDotQualifiedExpression) : ReplaceCallFix(expression, "?.") {
|
||||||
|
|
||||||
|
override fun getText() = "Replace scope function with safe (?.) call"
|
||||||
|
|
||||||
|
companion object : KotlinSingleIntentionActionFactory() {
|
||||||
|
override fun createAction(diagnostic: Diagnostic): KotlinQuickFixAction<KtExpression>? {
|
||||||
|
val element = diagnostic.psiElement
|
||||||
|
val scopeFunctionLiteral = element.getStrictParentOfType<KtFunctionLiteral>() ?: return null
|
||||||
|
val scopeCallExpression = scopeFunctionLiteral.getStrictParentOfType<KtCallExpression>() ?: return null
|
||||||
|
val scopeDotQualifiedExpression = scopeCallExpression.getStrictParentOfType<KtDotQualifiedExpression>() ?: return null
|
||||||
|
|
||||||
|
val context = scopeCallExpression.analyze()
|
||||||
|
val scopeFunctionLiteralDescriptor = context[BindingContext.FUNCTION, scopeFunctionLiteral] ?: return null
|
||||||
|
val scopeFunctionKind = scopeCallExpression.scopeFunctionKind(context) ?: return null
|
||||||
|
|
||||||
|
val internalReceiver = (element.parent as? KtDotQualifiedExpression)?.receiverExpression
|
||||||
|
val internalReceiverDescriptor = internalReceiver.getResolvedCall(context)?.candidateDescriptor
|
||||||
|
val internalResolvedCall = (element.getParentOfType<KtElement>(strict = false))?.getResolvedCall(context)
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
when (scopeFunctionKind) {
|
||||||
|
ScopeFunctionKind.WITH_PARAMETER -> {
|
||||||
|
if (internalReceiverDescriptor != scopeFunctionLiteralDescriptor.valueParameters.singleOrNull()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScopeFunctionKind.WITH_RECEIVER -> {
|
||||||
|
if (internalReceiverDescriptor != scopeFunctionLiteralDescriptor.extensionReceiverParameter &&
|
||||||
|
internalResolvedCall.getImplicitReceiverValue() == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReplaceWithSafeCallForScopeFunctionFix(scopeDotQualifiedExpression)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun KtCallExpression.scopeFunctionKind(context: BindingContext): ScopeFunctionKind? {
|
||||||
|
val methodName = getResolvedCall(context)?.resultingDescriptor?.fqNameUnsafe?.asString()
|
||||||
|
return ScopeFunctionKind.values().firstOrNull { kind -> kind.names.contains(methodName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class ScopeFunctionKind(vararg val names: String) {
|
||||||
|
WITH_PARAMETER("kotlin.let", "kotlin.also"),
|
||||||
|
WITH_RECEIVER("kotlin.apply", "kotlin.run")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ReplaceWithDotCallFix(expression: KtSafeQualifiedExpression): ReplaceCallFix(expression, "."), CleanupFix {
|
class ReplaceWithDotCallFix(expression: KtSafeQualifiedExpression): ReplaceCallFix(expression, "."), CleanupFix {
|
||||||
override fun getText() = "Replace with dot call"
|
override fun getText() = "Replace with dot call"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a.also {
|
||||||
|
it.<caret>length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a?.also {
|
||||||
|
it.length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "false"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
// ACTION: Add non-null asserted (!!) call
|
||||||
|
// ACTION: Introduce local variable
|
||||||
|
// 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 String?
|
||||||
|
|
||||||
|
fun foo(a: String?, b: String?) {
|
||||||
|
a.apply { ->
|
||||||
|
b<caret>.length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a.apply {
|
||||||
|
this<caret>.length
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a?.apply {
|
||||||
|
this.length
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a.apply { ->
|
||||||
|
this<caret>.length
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a?.apply { ->
|
||||||
|
this.length
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a.apply {
|
||||||
|
this@apply<caret>.length
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a?.apply {
|
||||||
|
this@apply.length
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "false"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
// ACTION: Introduce local variable
|
||||||
|
// ACTION: Add non-null asserted (!!) call
|
||||||
|
// ACTION: Replace with safe (?.) call
|
||||||
|
// ERROR: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type My?
|
||||||
|
|
||||||
|
class My(val prop: Int)
|
||||||
|
|
||||||
|
fun My?.foo(a: String?) {
|
||||||
|
a.apply {
|
||||||
|
this@foo<caret>.prop
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a.apply {
|
||||||
|
<caret>length
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a?.apply {
|
||||||
|
length
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a.apply {
|
||||||
|
<caret>toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a?.apply {
|
||||||
|
toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a.let {
|
||||||
|
it<caret>.length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a?.let {
|
||||||
|
it.length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a.let { s ->
|
||||||
|
s<caret>.length
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a?.let { s ->
|
||||||
|
s.length
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "false"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
// ACTION: Add non-null asserted (!!) call
|
||||||
|
// ACTION: Introduce local variable
|
||||||
|
// ACTION: Move lambda argument into parentheses
|
||||||
|
// ACTION: Replace with safe (this?.) call
|
||||||
|
// ERROR: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
|
||||||
|
|
||||||
|
fun String?.foo(a: String?) {
|
||||||
|
a.let { s ->
|
||||||
|
<caret>length
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "false"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
// ACTION: Add non-null asserted (!!) call
|
||||||
|
// ACTION: Introduce local variable
|
||||||
|
// 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 String?
|
||||||
|
|
||||||
|
fun foo(a: String?, b: String?) {
|
||||||
|
a.let { s ->
|
||||||
|
b<caret>.length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "false"
|
||||||
|
// ACTION: Add non-null asserted (!!) call
|
||||||
|
// ACTION: Introduce local variable
|
||||||
|
// 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 String?
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a<caret>.length
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a.run {
|
||||||
|
<caret>length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// "Replace scope function with safe (?.) call" "true"
|
||||||
|
// WITH_RUNTIME
|
||||||
|
fun foo(a: String?) {
|
||||||
|
a?.run {
|
||||||
|
length
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8763,6 +8763,99 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction")
|
||||||
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
|
@RunWith(JUnit3RunnerWithInners.class)
|
||||||
|
public static class ReplaceWithSafeCallForScopeFunction extends AbstractQuickFixTest {
|
||||||
|
public void testAllFilesPresentInReplaceWithSafeCallForScopeFunction() throws Exception {
|
||||||
|
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/replaceWithSafeCallForScopeFunction"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("also.kt")
|
||||||
|
public void testAlso() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/also.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("applyWithFake.kt")
|
||||||
|
public void testApplyWithFake() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/applyWithFake.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("applyWithThis.kt")
|
||||||
|
public void testApplyWithThis() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/applyWithThis.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("applyWithThisAndArrow.kt")
|
||||||
|
public void testApplyWithThisAndArrow() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/applyWithThisAndArrow.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("applyWithThisLabeled.kt")
|
||||||
|
public void testApplyWithThisLabeled() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/applyWithThisLabeled.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("applyWithThisLabeledFake.kt")
|
||||||
|
public void testApplyWithThisLabeledFake() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/applyWithThisLabeledFake.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("applyWithoutThis.kt")
|
||||||
|
public void testApplyWithoutThis() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/applyWithoutThis.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("applyWithoutThisMethodCall.kt")
|
||||||
|
public void testApplyWithoutThisMethodCall() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/applyWithoutThisMethodCall.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("let.kt")
|
||||||
|
public void testLet() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/let.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("letWithParam.kt")
|
||||||
|
public void testLetWithParam() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/letWithParam.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("letWithWrongImplicitThis.kt")
|
||||||
|
public void testLetWithWrongImplicitThis() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/letWithWrongImplicitThis.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("letWithWrongParam.kt")
|
||||||
|
public void testLetWithWrongParam() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/letWithWrongParam.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("notInsideScope.kt")
|
||||||
|
public void testNotInsideScope() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/notInsideScope.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("run.kt")
|
||||||
|
public void testRun() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCallForScopeFunction/run.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("idea/testData/quickfix/simplifyComparison")
|
@TestMetadata("idea/testData/quickfix/simplifyComparison")
|
||||||
@TestDataPath("$PROJECT_ROOT")
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
@RunWith(JUnit3RunnerWithInners.class)
|
@RunWith(JUnit3RunnerWithInners.class)
|
||||||
|
|||||||
Reference in New Issue
Block a user