Add quickfixes inserting explicit delegation calls

#KT-6963 Fixed
This commit is contained in:
Denis Zharkov
2015-03-23 15:17:26 +03:00
parent d0c72c2c3d
commit b1de2066c7
21 changed files with 283 additions and 6 deletions
@@ -177,11 +177,32 @@ public class JetSecondaryConstructor extends JetDeclarationStub<KotlinPlaceHolde
return findChildByClass(JetConstructorDelegationCall.class);
}
public boolean hasEmptyDelegationCall() {
JetConstructorDelegationCall call = getDelegationCall();
return call != null && call.isEmpty();
}
@NotNull
public JetClassOrObject getClassOrObject() {
return (JetClassOrObject) getParent().getParent();
}
@NotNull
public JetConstructorDelegationCall replaceEmptyDelegationCallWithExplicit(boolean isThis) {
JetPsiFactory psiFactory = new JetPsiFactory(getProject());
JetConstructorDelegationCall current = getDelegationCall();
assert current != null && current.isEmpty()
: "Method should not be called with non-existing or non-empty delegation call: " + getText();
current.delete();
PsiElement colon = addAfter(psiFactory.createColon(), getValueParameterList());
String delegationName = isThis ? "this" : "super";
return (JetConstructorDelegationCall) addAfter(psiFactory.createConstructorDelegationCall(delegationName + "()"), colon);
}
@NotNull
public PsiElement getConstructorKeyword() {
return findNotNullChildByType(JetTokens.CONSTRUCTOR_KEYWORD);
@@ -45,4 +45,8 @@ public class JetValueArgumentList extends JetElementImpl {
return findChildByType(JetTokens.RPAR);
}
@Nullable
public PsiElement getLeftParenthesis() {
return findChildByType(JetTokens.LPAR);
}
}
@@ -17,6 +17,7 @@ add.init.keyword.family=Add 'init' keyword
add.init.keyword.in.whole.project=Add 'init' keyword in whole project
add.init.keyword.in.whole.project.modal.title=Adding 'init' keyword in whole project
add.init.keyword.in.whole.project.family=Add 'init' keyword in whole project
insert.delegation.call=Insert ''{0}()'' call
make.element.in.classifiers.open=Make ''{0}'' in {1} open
make.class.annotation.class=Make ''{0}'' an annotation class
make.class.annotation.class.family=Make Class an Annotation Class
@@ -0,0 +1,83 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.quickfix
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.JetBundle
import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully
import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor
import org.jetbrains.kotlin.psi.JetClass
import org.jetbrains.kotlin.psi.JetClassOrObject
import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.psi.JetSecondaryConstructor
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
public class InsertDelegationCallQuickfix(val isThis: Boolean, element: JetSecondaryConstructor) : JetIntentionAction<JetSecondaryConstructor>(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<JetSecondaryConstructor>() ?: 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<JetSecondaryConstructor>() ?: 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)
}
}
}
@@ -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$);
}
}
@@ -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
@@ -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(<caret>)
}
@@ -0,0 +1,9 @@
// "Insert 'this()' call" "true"
open class B(val x: Int)
class A : B {
constructor(x: String)<caret> : this()
constructor() : super(1)
}
@@ -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(<caret>)
constructor(x: String) : super(1)
}
@@ -0,0 +1,5 @@
// "Insert 'this()' call" "true"
class A() {
constructor(x: String)<caret> : this()
}
@@ -0,0 +1,7 @@
// "Insert 'this()' call" "true"
class A() {
constructor(x: String)<caret> : this() {
}
}
@@ -0,0 +1,6 @@
// "Insert 'this()' call" "true"
// ERROR: <html>None of the following functions can be called with the arguments supplied. <ul><li><init>(<font color=red><b>String</b></font>) <i>defined in</i> A</li><li><init>(<font color=red><b>Int</b></font>) <i>defined in</i> A</li></ul></html>
class A(val x: Int) {
constructor(x: String) : this(<caret>)
}
@@ -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)<caret>
}
@@ -0,0 +1,9 @@
// "Insert 'this()' call" "true"
open class B(val x: Int)
class A : B {
constructor(x: String)<caret>
constructor() : super(1)
}
@@ -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()<caret>
constructor(x: String) : super(1)
}
@@ -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)<caret>
}
@@ -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)<caret>
}
@@ -0,0 +1,5 @@
// "Insert 'this()' call" "true"
class A() {
constructor(x: String)<caret>
}
@@ -0,0 +1,7 @@
// "Insert 'this()' call" "true"
class A() {
constructor(x: String)<caret> {
}
}
@@ -0,0 +1,6 @@
// "Insert 'this()' call" "true"
// ERROR: <html>None of the following functions can be called with the arguments supplied. <ul><li><init>(<font color=red><b>String</b></font>) <i>defined in</i> A</li><li><init>(<font color=red><b>Int</b></font>) <i>defined in</i> A</li></ul></html>
class A(val x: Int) {
constructor(x: String)<caret>
}
@@ -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({