From e2945b1d12ca1f1a7418faeccc5864529080ad5e Mon Sep 17 00:00:00 2001 From: Toshiaki Kameyama Date: Tue, 17 Jul 2018 11:50:50 +0900 Subject: [PATCH] Add quickfix for "Interface doesn't have constructors" #KT-17687 Fixed --- .../quickfix/ConvertToAnonymousObjectFix.kt | 76 +++++++++++++++++++ .../kotlin/idea/quickfix/QuickFixRegistrar.kt | 2 + .../convertToAnonymousObject/labeledReturn.kt | 10 +++ .../labeledReturn.kt.after | 12 +++ .../convertToAnonymousObject/multiMethod.kt | 11 +++ .../convertToAnonymousObject/parameter.kt | 10 +++ .../parameter.kt.after | 12 +++ .../convertToAnonymousObject/simple.kt | 8 ++ .../convertToAnonymousObject/simple.kt.after | 12 +++ .../quickfix/convertToAnonymousObject/unit.kt | 13 ++++ .../convertToAnonymousObject/unit.kt.after | 15 ++++ .../QuickFixMultiFileTestGenerated.java | 13 ++++ .../idea/quickfix/QuickFixTestGenerated.java | 38 ++++++++++ 13 files changed, 232 insertions(+) create mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/ConvertToAnonymousObjectFix.kt create mode 100644 idea/testData/quickfix/convertToAnonymousObject/labeledReturn.kt create mode 100644 idea/testData/quickfix/convertToAnonymousObject/labeledReturn.kt.after create mode 100644 idea/testData/quickfix/convertToAnonymousObject/multiMethod.kt create mode 100644 idea/testData/quickfix/convertToAnonymousObject/parameter.kt create mode 100644 idea/testData/quickfix/convertToAnonymousObject/parameter.kt.after create mode 100644 idea/testData/quickfix/convertToAnonymousObject/simple.kt create mode 100644 idea/testData/quickfix/convertToAnonymousObject/simple.kt.after create mode 100644 idea/testData/quickfix/convertToAnonymousObject/unit.kt create mode 100644 idea/testData/quickfix/convertToAnonymousObject/unit.kt.after diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ConvertToAnonymousObjectFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/ConvertToAnonymousObjectFix.kt new file mode 100644 index 00000000000..dec3625cc8e --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/ConvertToAnonymousObjectFix.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.quickfix + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.calls.tower.WrongResolutionToClassifier +import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyClassDescriptor +import org.jetbrains.kotlin.types.typeUtil.isUnit + +class ConvertToAnonymousObjectFix( + callExpression: KtCallExpression, + private val interfaceName: String, + private val functionName: String, + private val functionParameters: String, + private val functionReturnType: String? +) : KotlinQuickFixAction(callExpression) { + override fun getFamilyName() = "Convert to anonymous object" + + override fun getText() = familyName + + override fun invoke(project: Project, editor: Editor?, file: KtFile) { + val callExpression = element ?: return + val lambda = callExpression.lambdaArguments.singleOrNull()?.getLambdaExpression() ?: return + + val psiFactory = KtPsiFactory(project) + val body = lambda.bodyExpression + if (body != null) { + body.forEachDescendantOfType { + if (it.getLabelName() == interfaceName) it.labeledExpression?.delete() + } + val last = body.statements.lastOrNull() + if (last !is KtReturnExpression) last?.replace(psiFactory.createExpressionByPattern("return $0", last)) + } + + val anonymousObject = psiFactory.buildExpression { + appendFixedText("object : $interfaceName {") + appendFixedText("override fun $functionName$functionParameters") + if (functionReturnType != null) appendFixedText(": $functionReturnType") + appendFixedText("{") + if (body != null) appendExpression(body) + appendFixedText("}") + appendFixedText("}") + } + callExpression.replace(anonymousObject) + } + + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): KotlinQuickFixAction? { + val casted = Errors.RESOLUTION_TO_CLASSIFIER.cast(diagnostic) + if (casted.b != WrongResolutionToClassifier.INTERFACE_AS_FUNCTION) return null + + val callExpression = casted.psiElement.parent as? KtCallExpression ?: return null + if (callExpression.lambdaArguments.singleOrNull()?.getLambdaExpression() == null) return null + val interfaceName = callExpression.calleeExpression?.text ?: return null + + val classDescriptor = casted.a as? LazyClassDescriptor ?: return null + val function = classDescriptor.declaredCallableMembers.singleOrNull() as? SimpleFunctionDescriptor ?: return null + val functionDeclaration = DescriptorToSourceUtils.descriptorToDeclaration(function) as? KtNamedFunction ?: return null + val functionName = functionDeclaration.name ?: return null + val functionParameters = functionDeclaration.valueParameterList?.text ?: return null + val functionReturnType = function.returnType?.takeIf { !it.isUnit() }?.toString() + + return ConvertToAnonymousObjectFix(callExpression, interfaceName, functionName, functionParameters, functionReturnType) + } + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt index eab741fc3e1..e413e1d526a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -572,5 +572,7 @@ class QuickFixRegistrar : QuickFixContributor { DEFAULT_VALUE_NOT_ALLOWED_IN_OVERRIDE.registerFactory(RemoveDefaultParameterValueFix) ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS.registerFactory(RemoveDefaultParameterValueFix) + + RESOLUTION_TO_CLASSIFIER.registerFactory(ConvertToAnonymousObjectFix) } } diff --git a/idea/testData/quickfix/convertToAnonymousObject/labeledReturn.kt b/idea/testData/quickfix/convertToAnonymousObject/labeledReturn.kt new file mode 100644 index 00000000000..cd6fd226e17 --- /dev/null +++ b/idea/testData/quickfix/convertToAnonymousObject/labeledReturn.kt @@ -0,0 +1,10 @@ +// "Convert to anonymous object" "true" +interface I { + fun foo(): String +} + +fun test() { + I { + return@I "" + } +} diff --git a/idea/testData/quickfix/convertToAnonymousObject/labeledReturn.kt.after b/idea/testData/quickfix/convertToAnonymousObject/labeledReturn.kt.after new file mode 100644 index 00000000000..b6522b8614c --- /dev/null +++ b/idea/testData/quickfix/convertToAnonymousObject/labeledReturn.kt.after @@ -0,0 +1,12 @@ +// "Convert to anonymous object" "true" +interface I { + fun foo(): String +} + +fun test() { + object : I { + override fun foo(): String { + return "" + } + } +} diff --git a/idea/testData/quickfix/convertToAnonymousObject/multiMethod.kt b/idea/testData/quickfix/convertToAnonymousObject/multiMethod.kt new file mode 100644 index 00000000000..703d49e64ba --- /dev/null +++ b/idea/testData/quickfix/convertToAnonymousObject/multiMethod.kt @@ -0,0 +1,11 @@ +// "Convert to anonymous object" "false" +// ERROR: Interface I does not have constructors +interface I { + fun foo(): String + fun bar(): Unit +} + +fun test() { + I { + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/convertToAnonymousObject/parameter.kt b/idea/testData/quickfix/convertToAnonymousObject/parameter.kt new file mode 100644 index 00000000000..4da470ab9be --- /dev/null +++ b/idea/testData/quickfix/convertToAnonymousObject/parameter.kt @@ -0,0 +1,10 @@ +// "Convert to anonymous object" "true" +interface I { + fun foo(a: String, b: Int): Int +} + +fun test() { + I { + 1 + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/convertToAnonymousObject/parameter.kt.after b/idea/testData/quickfix/convertToAnonymousObject/parameter.kt.after new file mode 100644 index 00000000000..c069e435475 --- /dev/null +++ b/idea/testData/quickfix/convertToAnonymousObject/parameter.kt.after @@ -0,0 +1,12 @@ +// "Convert to anonymous object" "true" +interface I { + fun foo(a: String, b: Int): Int +} + +fun test() { + object : I { + override fun foo(a: String, b: Int): Int { + return 1 + } + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/convertToAnonymousObject/simple.kt b/idea/testData/quickfix/convertToAnonymousObject/simple.kt new file mode 100644 index 00000000000..7affeccb9ac --- /dev/null +++ b/idea/testData/quickfix/convertToAnonymousObject/simple.kt @@ -0,0 +1,8 @@ +// "Convert to anonymous object" "true" +interface I { + fun foo(): String +} + +fun test() { + val i = I { "" } +} \ No newline at end of file diff --git a/idea/testData/quickfix/convertToAnonymousObject/simple.kt.after b/idea/testData/quickfix/convertToAnonymousObject/simple.kt.after new file mode 100644 index 00000000000..593b1587be9 --- /dev/null +++ b/idea/testData/quickfix/convertToAnonymousObject/simple.kt.after @@ -0,0 +1,12 @@ +// "Convert to anonymous object" "true" +interface I { + fun foo(): String +} + +fun test() { + val i = object : I { + override fun foo(): String { + return "" + } + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/convertToAnonymousObject/unit.kt b/idea/testData/quickfix/convertToAnonymousObject/unit.kt new file mode 100644 index 00000000000..e4887d72fb6 --- /dev/null +++ b/idea/testData/quickfix/convertToAnonymousObject/unit.kt @@ -0,0 +1,13 @@ +// "Convert to anonymous object" "true" +interface I { + fun bar(): Unit +} + +fun foo() { +} + +fun test() { + I { + foo() + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/convertToAnonymousObject/unit.kt.after b/idea/testData/quickfix/convertToAnonymousObject/unit.kt.after new file mode 100644 index 00000000000..e0b3e389b3e --- /dev/null +++ b/idea/testData/quickfix/convertToAnonymousObject/unit.kt.after @@ -0,0 +1,15 @@ +// "Convert to anonymous object" "true" +interface I { + fun bar(): Unit +} + +fun foo() { +} + +fun test() { + object : I { + override fun bar() { + return foo() + } + } +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java index e7d529a225a..af9554de3cd 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java @@ -1257,6 +1257,19 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes } } + @TestMetadata("idea/testData/quickfix/convertToAnonymousObject") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ConvertToAnonymousObject extends AbstractQuickFixMultiFileTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInConvertToAnonymousObject() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/convertToAnonymousObject"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true); + } + } + @TestMetadata("idea/testData/quickfix/createFromUsage") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index 910ebf48861..08ca7ca156b 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -2002,6 +2002,44 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { } } + @TestMetadata("idea/testData/quickfix/convertToAnonymousObject") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ConvertToAnonymousObject extends AbstractQuickFixTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInConvertToAnonymousObject() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/convertToAnonymousObject"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("labeledReturn.kt") + public void testLabeledReturn() throws Exception { + runTest("idea/testData/quickfix/convertToAnonymousObject/labeledReturn.kt"); + } + + @TestMetadata("multiMethod.kt") + public void testMultiMethod() throws Exception { + runTest("idea/testData/quickfix/convertToAnonymousObject/multiMethod.kt"); + } + + @TestMetadata("parameter.kt") + public void testParameter() throws Exception { + runTest("idea/testData/quickfix/convertToAnonymousObject/parameter.kt"); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + runTest("idea/testData/quickfix/convertToAnonymousObject/simple.kt"); + } + + @TestMetadata("unit.kt") + public void testUnit() throws Exception { + runTest("idea/testData/quickfix/convertToAnonymousObject/unit.kt"); + } + } + @TestMetadata("idea/testData/quickfix/createFromUsage") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)