Add quickfixes inserting explicit delegation calls
#KT-6963 Fixed
This commit is contained in:
@@ -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$);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user