From b1de2066c795e2e27b3c91fab6f3c211db10ea7a Mon Sep 17 00:00:00 2001 From: Denis Zharkov Date: Mon, 23 Mar 2015 15:17:26 +0300 Subject: [PATCH] Add quickfixes inserting explicit delegation calls #KT-6963 Fixed --- .../kotlin/psi/JetSecondaryConstructor.java | 21 +++++ .../kotlin/psi/JetValueArgumentList.java | 4 + .../kotlin/idea/JetBundle.properties | 1 + .../quickfix/InsertDelegationCallQuickfix.kt | 83 +++++++++++++++++++ .../idea/quickfix/QuickFixRegistrar.java | 5 ++ .../JetConstructorDelegationCallUsage.kt | 9 +- .../afterNonApplicableInsertSuper.kt | 8 ++ .../afterNonApplicableInsertThis.kt | 9 ++ .../afterNonApplicableOnEmpty.kt | 10 +++ .../afterPrimaryRequiredParameterless.kt | 5 ++ .../afterPrimaryRequiredWithBody.kt | 7 ++ .../afterPrimaryRequiredWithParameter.kt | 6 ++ .../beforeNonApplicableInsertSuper.kt | 8 ++ .../beforeNonApplicableInsertThis.kt | 9 ++ .../beforeNonApplicableOnEmpty.kt | 10 +++ .../beforeNonApplicableWithOneConstructor.kt | 9 ++ .../beforePrimaryRequiredNoSuper.kt | 9 ++ .../beforePrimaryRequiredParameterless.kt | 5 ++ .../beforePrimaryRequiredWithBody.kt | 7 ++ .../beforePrimaryRequiredWithParameter.kt | 6 ++ .../idea/quickfix/QuickFixTestGenerated.java | 58 +++++++++++++ 21 files changed, 283 insertions(+), 6 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/InsertDelegationCallQuickfix.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/afterNonApplicableInsertSuper.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/afterNonApplicableInsertThis.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/afterNonApplicableOnEmpty.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredParameterless.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredWithBody.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredWithParameter.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertSuper.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertThis.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/beforeNonApplicableOnEmpty.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/beforeNonApplicableWithOneConstructor.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredNoSuper.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredParameterless.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithBody.kt create mode 100644 idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithParameter.kt diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetSecondaryConstructor.java b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetSecondaryConstructor.java index b3c18918f61..9a403e09e6b 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetSecondaryConstructor.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetSecondaryConstructor.java @@ -177,11 +177,32 @@ public class JetSecondaryConstructor extends JetDeclarationStub(element) { + override fun getText() = JetBundle.message("insert.delegation.call", keywordToUse) + override fun getFamilyName() = "Insert explicit delegation call" + + private val keywordToUse = if (isThis) "this" else "super" + + override fun invoke(project: Project, editor: Editor?, file: JetFile?) { + val newDelegationCall = element.replaceEmptyDelegationCallWithExplicit(isThis) + + val context = element.analyzeFully() + val resolvedCall = newDelegationCall.getResolvedCall(context) + val descriptor = element.descriptor + + // if empty call is ok and it's resolved to another constructor, do not move caret + if (resolvedCall?.getStatus()?.isSuccess() ?: false && resolvedCall!!.getCandidateDescriptor().getOriginal() != descriptor) return + + val leftParOffset = newDelegationCall.getValueArgumentList()!!.getLeftParenthesis()!!.getTextOffset() + + editor?.moveCaret(leftParOffset + 1) + } + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean { + return super.isAvailable(project, editor, file) && element.hasEmptyDelegationCall() + } + + object InsertThisDelegationCallFactory : JetSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + val secondaryConstructor = diagnostic.getPsiElement().getNonStrictParentOfType() ?: return null + if (secondaryConstructor.getClassOrObject().getConstructorsCount() <= 1 || + !secondaryConstructor.hasEmptyDelegationCall()) return null + + return InsertDelegationCallQuickfix(isThis = true, element = secondaryConstructor) + } + + private fun JetClassOrObject.getConstructorsCount() = (descriptor as ClassDescriptor).getConstructors().size() + } + + object InsertSuperDelegationCallFactory : JetSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + val secondaryConstructor = diagnostic.getPsiElement().getNonStrictParentOfType() ?: return null + if (!secondaryConstructor.hasEmptyDelegationCall()) return null + val klass = secondaryConstructor.getClassOrObject() as? JetClass ?: return null + if (klass.hasPrimaryConstructor()) return null + + return InsertDelegationCallQuickfix(isThis = false, element = secondaryConstructor) + } + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java index 3a8e32e03de..7a9a5862958 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java @@ -304,5 +304,10 @@ public class QuickFixRegistrar { QuickFixes.factories.put(DEPRECATED_CLASS_OBJECT_SYNTAX, ClassObjectToCompanionObjectInWholeProjectFix.Factory); QuickFixes.factories.put(INIT_KEYWORD_BEFORE_CLASS_INITIALIZER_EXPECTED, AddInitKeywordFix.Factory); QuickFixes.factories.put(INIT_KEYWORD_BEFORE_CLASS_INITIALIZER_EXPECTED, AddInitKeywordFixInWholeProjectFix.Factory); + + QuickFixes.factories.put(PRIMARY_CONSTRUCTOR_DELEGATION_CALL_EXPECTED, InsertDelegationCallQuickfix.InsertThisDelegationCallFactory.INSTANCE$); + + QuickFixes.factories.put(EXPLICIT_DELEGATION_CALL_REQUIRED, InsertDelegationCallQuickfix.InsertThisDelegationCallFactory.INSTANCE$); + QuickFixes.factories.put(EXPLICIT_DELEGATION_CALL_REQUIRED, InsertDelegationCallQuickfix.InsertSuperDelegationCallFactory.INSTANCE$); } } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/usages/JetConstructorDelegationCallUsage.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/usages/JetConstructorDelegationCallUsage.kt index 419a20c5078..ddb873d5a81 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/usages/JetConstructorDelegationCallUsage.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/usages/JetConstructorDelegationCallUsage.kt @@ -28,13 +28,10 @@ public class JetConstructorDelegationCallUsage(call: JetConstructorDelegationCal override fun processUsage(changeInfo: JetChangeInfo, element: JetConstructorDelegationCall): Boolean { val isThisCall = element.isCallToThis() - val psiFactory = JetPsiFactory(element) var elementToWorkWith = element if (changeInfo.getNewParametersCount() > 0 && element.isEmpty()) { - val delegationKindName = if (isThisCall) "this" else "super" - elementToWorkWith = - element.replace(psiFactory.createConstructorDelegationCall("$delegationKindName()")) as JetConstructorDelegationCall - elementToWorkWith.getParent()!!.addBefore(psiFactory.createColon(), elementToWorkWith) + val constructor = element.getParent() as JetSecondaryConstructor + elementToWorkWith = constructor.replaceEmptyDelegationCallWithExplicit(isThisCall) } val result = JetFunctionCallUsage( @@ -42,7 +39,7 @@ public class JetConstructorDelegationCallUsage(call: JetConstructorDelegationCal if (changeInfo.getNewParametersCount() == 0 && !isThisCall && !elementToWorkWith.isEmpty()) { (elementToWorkWith.getParent() as? JetSecondaryConstructor)?.getColon()?.delete() - elementToWorkWith.replace(psiFactory.createConstructorDelegationCall("")) + elementToWorkWith.replace(JetPsiFactory(element).createConstructorDelegationCall("")) } return result diff --git a/idea/testData/quickfix/insertDelegationCall/afterNonApplicableInsertSuper.kt b/idea/testData/quickfix/insertDelegationCall/afterNonApplicableInsertSuper.kt new file mode 100644 index 00000000000..4b894c3fea3 --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/afterNonApplicableInsertSuper.kt @@ -0,0 +1,8 @@ +// "Insert 'super()' call" "true" +// ERROR: No value passed for parameter x + +open class B(val x: Int) + +class A : B { + constructor(x: String) : super() +} diff --git a/idea/testData/quickfix/insertDelegationCall/afterNonApplicableInsertThis.kt b/idea/testData/quickfix/insertDelegationCall/afterNonApplicableInsertThis.kt new file mode 100644 index 00000000000..fd55257544d --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/afterNonApplicableInsertThis.kt @@ -0,0 +1,9 @@ +// "Insert 'this()' call" "true" + +open class B(val x: Int) + +class A : B { + constructor(x: String) : this() + + constructor() : super(1) +} diff --git a/idea/testData/quickfix/insertDelegationCall/afterNonApplicableOnEmpty.kt b/idea/testData/quickfix/insertDelegationCall/afterNonApplicableOnEmpty.kt new file mode 100644 index 00000000000..13e6a80a1ed --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/afterNonApplicableOnEmpty.kt @@ -0,0 +1,10 @@ +// "Insert 'this()' call" "true" +// ERROR: There's a cycle in the delegation calls chain + +open class B(val x: Int) + +class A : B { + constructor() : this() + + constructor(x: String) : super(1) +} diff --git a/idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredParameterless.kt b/idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredParameterless.kt new file mode 100644 index 00000000000..e6ad310e08f --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredParameterless.kt @@ -0,0 +1,5 @@ +// "Insert 'this()' call" "true" + +class A() { + constructor(x: String) : this() +} diff --git a/idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredWithBody.kt b/idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredWithBody.kt new file mode 100644 index 00000000000..73509bdb04a --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredWithBody.kt @@ -0,0 +1,7 @@ +// "Insert 'this()' call" "true" + +class A() { + constructor(x: String) : this() { + + } +} diff --git a/idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredWithParameter.kt b/idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredWithParameter.kt new file mode 100644 index 00000000000..99a50ab5997 --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/afterPrimaryRequiredWithParameter.kt @@ -0,0 +1,6 @@ +// "Insert 'this()' call" "true" +// ERROR: None of the following functions can be called with the arguments supplied.
  • (String) defined in A
  • (Int) defined in A
+ +class A(val x: Int) { + constructor(x: String) : this() +} diff --git a/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertSuper.kt b/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertSuper.kt new file mode 100644 index 00000000000..8928a3649c9 --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertSuper.kt @@ -0,0 +1,8 @@ +// "Insert 'super()' call" "true" +// ERROR: No value passed for parameter x + +open class B(val x: Int) + +class A : B { + constructor(x: String) +} diff --git a/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertThis.kt b/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertThis.kt new file mode 100644 index 00000000000..d2250f238b4 --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertThis.kt @@ -0,0 +1,9 @@ +// "Insert 'this()' call" "true" + +open class B(val x: Int) + +class A : B { + constructor(x: String) + + constructor() : super(1) +} diff --git a/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableOnEmpty.kt b/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableOnEmpty.kt new file mode 100644 index 00000000000..15bda09cf73 --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableOnEmpty.kt @@ -0,0 +1,10 @@ +// "Insert 'this()' call" "true" +// ERROR: There's a cycle in the delegation calls chain + +open class B(val x: Int) + +class A : B { + constructor() + + constructor(x: String) : super(1) +} diff --git a/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableWithOneConstructor.kt b/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableWithOneConstructor.kt new file mode 100644 index 00000000000..b611d2aa8e6 --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/beforeNonApplicableWithOneConstructor.kt @@ -0,0 +1,9 @@ +// "Insert 'this()' call" "false" +// ACTION: Insert 'super()' call +// ERROR: Explicit 'this' or 'super' call is required. There is no constructor in superclass that can be called without arguments + +open class B(val x: Int) + +class A : B { + constructor(x: String) +} diff --git a/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredNoSuper.kt b/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredNoSuper.kt new file mode 100644 index 00000000000..e426305265e --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredNoSuper.kt @@ -0,0 +1,9 @@ +// "Insert 'super()' call" "false" +// ACTION: Insert 'this()' call +// ERROR: Primary constructor call expected + +open class B() + +class A(val x: Int) : B() { + constructor(x: String) +} diff --git a/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredParameterless.kt b/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredParameterless.kt new file mode 100644 index 00000000000..01bce14eb62 --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredParameterless.kt @@ -0,0 +1,5 @@ +// "Insert 'this()' call" "true" + +class A() { + constructor(x: String) +} diff --git a/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithBody.kt b/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithBody.kt new file mode 100644 index 00000000000..bf9c295d2d7 --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithBody.kt @@ -0,0 +1,7 @@ +// "Insert 'this()' call" "true" + +class A() { + constructor(x: String) { + + } +} diff --git a/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithParameter.kt b/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithParameter.kt new file mode 100644 index 00000000000..1a8f833400f --- /dev/null +++ b/idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithParameter.kt @@ -0,0 +1,6 @@ +// "Insert 'this()' call" "true" +// ERROR: None of the following functions can be called with the arguments supplied.
  • (String) defined in A
  • (Int) defined in A
+ +class A(val x: Int) { + constructor(x: String) +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index 0851566cc26..0df0f048386 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -39,6 +39,7 @@ import java.util.regex.Pattern; QuickFixTestGenerated.ConflictingImports.class, QuickFixTestGenerated.CreateFromUsage.class, QuickFixTestGenerated.Expressions.class, + QuickFixTestGenerated.InsertDelegationCall.class, QuickFixTestGenerated.Migration.class, QuickFixTestGenerated.Modifiers.class, QuickFixTestGenerated.Nullables.class, @@ -2922,6 +2923,63 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { } } + @TestMetadata("idea/testData/quickfix/insertDelegationCall") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class InsertDelegationCall extends AbstractQuickFixTest { + public void testAllFilesPresentInInsertDelegationCall() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/insertDelegationCall"), Pattern.compile("^before(\\w+)\\.kt$"), true); + } + + @TestMetadata("beforeNonApplicableInsertSuper.kt") + public void testNonApplicableInsertSuper() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertSuper.kt"); + doTest(fileName); + } + + @TestMetadata("beforeNonApplicableInsertThis.kt") + public void testNonApplicableInsertThis() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/insertDelegationCall/beforeNonApplicableInsertThis.kt"); + doTest(fileName); + } + + @TestMetadata("beforeNonApplicableOnEmpty.kt") + public void testNonApplicableOnEmpty() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/insertDelegationCall/beforeNonApplicableOnEmpty.kt"); + doTest(fileName); + } + + @TestMetadata("beforeNonApplicableWithOneConstructor.kt") + public void testNonApplicableWithOneConstructor() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/insertDelegationCall/beforeNonApplicableWithOneConstructor.kt"); + doTest(fileName); + } + + @TestMetadata("beforePrimaryRequiredNoSuper.kt") + public void testPrimaryRequiredNoSuper() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredNoSuper.kt"); + doTest(fileName); + } + + @TestMetadata("beforePrimaryRequiredParameterless.kt") + public void testPrimaryRequiredParameterless() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredParameterless.kt"); + doTest(fileName); + } + + @TestMetadata("beforePrimaryRequiredWithBody.kt") + public void testPrimaryRequiredWithBody() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithBody.kt"); + doTest(fileName); + } + + @TestMetadata("beforePrimaryRequiredWithParameter.kt") + public void testPrimaryRequiredWithParameter() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/insertDelegationCall/beforePrimaryRequiredWithParameter.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/quickfix/migration") @TestDataPath("$PROJECT_ROOT") @InnerTestClasses({