diff --git a/ChangeLog.md b/ChangeLog.md index 1c996de9f18..207e913e659 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -476,6 +476,7 @@ These artifacts include extensions for the types available in the latter JDKs, s - [`KT-15068`](https://youtrack.jetbrains.com/issue/KT-15068) Implement intention which rename file according to the top-level class name - Implement quickfix which enables/disables coroutine support in module or project +- [`KT-15056`](https://youtrack.jetbrains.com/issue/KT-15056) Implement intention which converts object literal to class ## 1.0.6 diff --git a/idea/resources/intentionDescriptions/ConvertObjectLiteralToClassIntention/after.kt.template b/idea/resources/intentionDescriptions/ConvertObjectLiteralToClassIntention/after.kt.template new file mode 100644 index 00000000000..2615d1d542e --- /dev/null +++ b/idea/resources/intentionDescriptions/ConvertObjectLiteralToClassIntention/after.kt.template @@ -0,0 +1,9 @@ +open class K + +fun foo(n: Int) { + val x = O(n) +} + +class O(private val n: Int) : K() { + fun bar() = n +} \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/ConvertObjectLiteralToClassIntention/before.kt.template b/idea/resources/intentionDescriptions/ConvertObjectLiteralToClassIntention/before.kt.template new file mode 100644 index 00000000000..befc623d785 --- /dev/null +++ b/idea/resources/intentionDescriptions/ConvertObjectLiteralToClassIntention/before.kt.template @@ -0,0 +1,7 @@ +open class K + +fun foo(n: Int) { + val x = object : K() { + fun bar() = n + } +} \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/ConvertObjectLiteralToClassIntention/description.html b/idea/resources/intentionDescriptions/ConvertObjectLiteralToClassIntention/description.html new file mode 100644 index 00000000000..f36f0e99a02 --- /dev/null +++ b/idea/resources/intentionDescriptions/ConvertObjectLiteralToClassIntention/description.html @@ -0,0 +1,5 @@ + + +This intention converts object literal to class and replaces object literal expression with constructor call. + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 63608a8caf1..9628e54eb6d 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1485,6 +1485,11 @@ Kotlin + + org.jetbrains.kotlin.idea.intentions.ConvertObjectLiteralToClassIntention + Kotlin + + ( + KtObjectLiteralExpression::class.java, + "Convert object literal to class" +) { + override fun applicabilityRange(element: KtObjectLiteralExpression) = element.objectDeclaration.getObjectKeyword()?.textRange + + override fun startInWriteAction() = false + + private fun doApply(editor: Editor, element: KtObjectLiteralExpression, targetParent: KtElement) { + val project = element.project + + val scope = element.getResolutionScope() + val context = element.analyze(BodyResolveMode.PARTIAL) + val objectLiteralType = element.getType(context) + val validator: (String) -> Boolean = { scope.findClassifier(Name.identifier(it), NoLookupLocation.FROM_IDE) == null } + val className = if (objectLiteralType != null) { + KotlinNameSuggester.suggestNamesByType(objectLiteralType, validator, "O").first() + } + else { + KotlinNameSuggester.suggestNameByName("O", validator) + } + + val psiFactory = KtPsiFactory(element) + + val targetSibling = element.parentsWithSelf.first { it.parent == targetParent } + + val objectDeclaration = element.objectDeclaration + val newClass = psiFactory.createClass("class $className") + objectDeclaration.getSuperTypeList()?.let { + newClass.add(psiFactory.createColon()) + newClass.add(it) + } + objectDeclaration.getBody()?.let { + newClass.add(it) + } + + project.executeWriteCommand(text) { + ExtractionEngine( + object : ExtractionEngineHelper(text) { + override fun configureAndRun( + project: Project, + editor: Editor, + descriptorWithConflicts: ExtractableCodeDescriptorWithConflicts, + onFinish: (ExtractionResult) -> Unit + ) { + val descriptor = descriptorWithConflicts.descriptor.copy(suggestedNames = className.singletonList()) + doRefactor( + ExtractionGeneratorConfiguration(descriptor, ExtractionGeneratorOptions.DEFAULT), + onFinish + ) + } + } + ).run(editor, ExtractionData(element.containingKtFile, element.toRange(), targetSibling)) { + val functionDeclaration = it.declaration as KtFunction + if (functionDeclaration.valueParameters.isNotEmpty()) { + val valKeyword = psiFactory.createValKeyword() + newClass + .createPrimaryConstructorParameterListIfAbsent() + .replaced(functionDeclaration.valueParameterList!!) + .parameters + .forEach { + it.addAfter(valKeyword, null) + it.addModifier(KtTokens.PRIVATE_KEYWORD) + } + } + functionDeclaration.replaced(newClass).apply { + primaryConstructor?.let { CodeStyleManager.getInstance(project).reformat(it) } + } + } + } + } + + override fun applyTo(element: KtObjectLiteralExpression, editor: Editor?) { + if (editor == null) return + + val containers = element.getExtractionContainers(strict = true, includeAll = true) + + if (ApplicationManager.getApplication().isUnitTestMode) { + return doApply(editor, element, containers.last()) + } + + chooseContainerElementIfNecessary( + containers, + editor, + if (containers.first() is KtFile) "Select target file" else "Select target code block / file", + true, + { it }, + { doApply(editor, element, it) } + ) + } +} diff --git a/idea/testData/intentions/convertObjectLiteralToClass/.intention b/idea/testData/intentions/convertObjectLiteralToClass/.intention new file mode 100644 index 00000000000..ba8f864844c --- /dev/null +++ b/idea/testData/intentions/convertObjectLiteralToClass/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.ConvertObjectLiteralToClassIntention \ No newline at end of file diff --git a/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoCapture.kt b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoCapture.kt new file mode 100644 index 00000000000..7d1bf534efb --- /dev/null +++ b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoCapture.kt @@ -0,0 +1,7 @@ +open class K + +fun foo(n: Int) { + val x = object : K() { + fun bar() = 1 + } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoCapture.kt.after b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoCapture.kt.after new file mode 100644 index 00000000000..d056a578aa0 --- /dev/null +++ b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoCapture.kt.after @@ -0,0 +1,9 @@ +open class K + +fun foo(n: Int) { + val x = O() +} + +class O : K() { + fun bar() = 1 +} \ No newline at end of file diff --git a/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoSupers.kt b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoSupers.kt new file mode 100644 index 00000000000..f142e3b210e --- /dev/null +++ b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoSupers.kt @@ -0,0 +1,5 @@ +fun foo(n: Int) { + val x = object { + fun bar() = n + } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoSupers.kt.after b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoSupers.kt.after new file mode 100644 index 00000000000..a7e6a9c60dc --- /dev/null +++ b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoSupers.kt.after @@ -0,0 +1,7 @@ +fun foo(n: Int) { + val x = O(n) +} + +class O(private val n: Int) { + fun bar() = n +} \ No newline at end of file diff --git a/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralWithCapture.kt b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralWithCapture.kt new file mode 100644 index 00000000000..0f647d8cba9 --- /dev/null +++ b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralWithCapture.kt @@ -0,0 +1,7 @@ +open class K + +fun foo(n: Int) { + val x = object : K() { + fun bar() = n + } +} \ No newline at end of file diff --git a/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralWithCapture.kt.after b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralWithCapture.kt.after new file mode 100644 index 00000000000..8ac91c2d271 --- /dev/null +++ b/idea/testData/intentions/convertObjectLiteralToClass/objectLiteralWithCapture.kt.after @@ -0,0 +1,9 @@ +open class K + +fun foo(n: Int) { + val x = O(n) +} + +class O(private val n: Int) : K() { + fun bar() = n +} \ No newline at end of file diff --git a/idea/testData/quickfix/modifiers/noAbstractForAnonymousObject.kt b/idea/testData/quickfix/modifiers/noAbstractForAnonymousObject.kt index 9835de5b2dc..f13947c5cf3 100644 --- a/idea/testData/quickfix/modifiers/noAbstractForAnonymousObject.kt +++ b/idea/testData/quickfix/modifiers/noAbstractForAnonymousObject.kt @@ -1,6 +1,7 @@ // "Make 'object : T {}' abstract" "false" // ACTION: Implement members // ACTION: Split property declaration +// ACTION: Convert object literal to class // ERROR: Object must be declared abstract or implement abstract member public abstract fun foo(): Unit defined in T interface T { fun foo() diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 36704fcf13c..d00ab0d93ee 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -4680,6 +4680,33 @@ public class IntentionTestGenerated extends AbstractIntentionTest { } } + @TestMetadata("idea/testData/intentions/convertObjectLiteralToClass") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ConvertObjectLiteralToClass extends AbstractIntentionTest { + public void testAllFilesPresentInConvertObjectLiteralToClass() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertObjectLiteralToClass"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("objectLiteralNoCapture.kt") + public void testObjectLiteralNoCapture() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoCapture.kt"); + doTest(fileName); + } + + @TestMetadata("objectLiteralNoSupers.kt") + public void testObjectLiteralNoSupers() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertObjectLiteralToClass/objectLiteralNoSupers.kt"); + doTest(fileName); + } + + @TestMetadata("objectLiteralWithCapture.kt") + public void testObjectLiteralWithCapture() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertObjectLiteralToClass/objectLiteralWithCapture.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/intentions/convertParameterToReceiver") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)