Add better quickfix for scope functions #KT-13676 Fixed

This commit is contained in:
shiraji
2017-06-11 01:50:45 +03:00
committed by Mikhail Glukhikh
parent 09d8c31433
commit ab4eb1dd20
26 changed files with 335 additions and 0 deletions
@@ -229,6 +229,7 @@ class QuickFixRegistrar : QuickFixContributor {
UNSAFE_INFIX_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
UNSAFE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix) // [] only
UNSAFE_IMPLICIT_INVOKE_CALL.registerFactory(ReplaceInfixOrOperatorCallFix)
UNSAFE_CALL.registerFactory(ReplaceWithSafeCallForScopeFunctionFix)
AMBIGUOUS_ANONYMOUS_TYPE_INFERRED.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.psi.*
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.resolvedCallUtil.getImplicitReceiverValue
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
abstract class ReplaceCallFix(
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 {
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
}
}
@@ -0,0 +1,7 @@
// "Replace scope function with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a?.apply {
this.length
}
}
@@ -0,0 +1,7 @@
// "Replace scope function with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply { ->
this<caret>.length
}
}
@@ -0,0 +1,7 @@
// "Replace scope function with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a?.apply { ->
this.length
}
}
@@ -0,0 +1,7 @@
// "Replace scope function with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply {
this@apply<caret>.length
}
}
@@ -0,0 +1,7 @@
// "Replace scope function with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a?.apply {
this@apply.length
}
}
@@ -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
}
}
@@ -0,0 +1,7 @@
// "Replace scope function with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply {
<caret>length
}
}
@@ -0,0 +1,7 @@
// "Replace scope function with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a?.apply {
length
}
}
@@ -0,0 +1,7 @@
// "Replace scope function with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply {
<caret>toLowerCase()
}
}
@@ -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
}
}
@@ -0,0 +1,7 @@
// "Replace scope function with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a?.let { s ->
s.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: 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
}
}
@@ -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")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)