diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/AddPropertyToSupertypeFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/AddPropertyToSupertypeFix.kt new file mode 100644 index 00000000000..d16d799b9d9 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/AddPropertyToSupertypeFix.kt @@ -0,0 +1,177 @@ +/* + * 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.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.intention.LowPriorityAction +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.ListPopupStep +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep +import com.intellij.util.PlatformIcons +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.Modality.ABSTRACT +import org.jetbrains.kotlin.descriptors.Modality.SEALED +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny +import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde +import org.jetbrains.kotlin.idea.core.ShortenReferences +import org.jetbrains.kotlin.idea.core.implicitModality +import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.lexer.KtModifierKeywordToken +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.modalityModifier +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.checker.KotlinTypeChecker +import org.jetbrains.kotlin.types.typeUtil.supertypes + +class AddPropertyToSupertypeFix private constructor( + element: KtProperty, + private val properties: List +) : KotlinQuickFixAction(element), LowPriorityAction { + + private class PropertyData(val signaturePreview: String, val sourceCode: String, val targetClass: KtClass) + + override fun getText(): String { + val single = properties.singleOrNull() + return if (single != null) actionName(single) else "Add property to supertype..." + } + + override fun getFamilyName() = "Add property to supertype" + + override fun invoke(project: Project, editor: Editor?, file: KtFile) { + CommandProcessor.getInstance().runUndoTransparentAction { + if (properties.size == 1 || editor == null || !editor.component.isShowing) { + addProperty(properties.first(), project) + } else { + JBPopupFactory.getInstance().createListPopup(createPropertyPopup(project)).showInBestPositionFor(editor) + } + } + } + + private fun addProperty(propertyData: PropertyData, project: Project) { + project.executeWriteCommand("Add Property to Type") { + val classBody = propertyData.targetClass.getOrCreateBody() + + val propertyElement = KtPsiFactory(project).createProperty(propertyData.sourceCode) + val insertedPropertyElement = classBody.addBefore(propertyElement, classBody.rBrace) as KtProperty + + ShortenReferences.DEFAULT.process(insertedPropertyElement) + val modifierToken = insertedPropertyElement.modalityModifier()?.node?.elementType as? KtModifierKeywordToken + ?: return@executeWriteCommand + if (insertedPropertyElement.implicitModality() == modifierToken) { + RemoveModifierFix(insertedPropertyElement, modifierToken, true).invoke() + } + } + } + + private fun createPropertyPopup(project: Project): ListPopupStep<*> { + return object : BaseListPopupStep("Choose Type", properties) { + override fun isAutoSelectionEnabled() = false + + override fun onChosen(selectedValue: PropertyData, finalChoice: Boolean): PopupStep<*>? { + if (finalChoice) { + addProperty(selectedValue, project) + } + return PopupStep.FINAL_CHOICE + } + + override fun getIconFor(value: PropertyData) = PlatformIcons.PROPERTY_ICON + override fun getTextFor(value: PropertyData) = actionName(value) + } + } + + private fun actionName(propertyData: PropertyData) = "Add '${propertyData.signaturePreview}' to '${propertyData.targetClass.name}'" + + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + val property = diagnostic.psiElement as? KtProperty ?: return null + + val descriptors = generatePropertiesToAdd(property) + if (descriptors.isEmpty()) return null + + val project = diagnostic.psiFile.project + val propertyData = descriptors.mapNotNull { createPropertyData(it, property.initializer, project) } + if (propertyData.isEmpty()) return null + + return AddPropertyToSupertypeFix(property, propertyData) + } + + private fun createPropertyData( + propertyDescriptor: PropertyDescriptor, + initializer: KtExpression?, + project: Project + ): PropertyData? { + val classDescriptor = propertyDescriptor.containingDeclaration as ClassDescriptor + var signaturePreview = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_NO_ANNOTATIONS.render(propertyDescriptor) + if (classDescriptor.kind == ClassKind.INTERFACE) { + signaturePreview = signaturePreview.substring("abstract ".length) + } + + var sourceCode = IdeDescriptorRenderers.SOURCE_CODE.render(propertyDescriptor) + if (classDescriptor.kind == ClassKind.CLASS && classDescriptor.modality == Modality.OPEN && initializer != null) { + sourceCode += " = ${initializer.text}" + } + + val targetClass = DescriptorToSourceUtilsIde.getAnyDeclaration(project, classDescriptor) as? KtClass ?: return null + return PropertyData(signaturePreview, sourceCode, targetClass) + } + + private fun generatePropertiesToAdd(propertyElement: KtProperty): List { + val propertyDescriptor = + propertyElement.resolveToDescriptorIfAny(BodyResolveMode.FULL) as? PropertyDescriptor ?: return emptyList() + val classDescriptor = propertyDescriptor.containingDeclaration as? ClassDescriptor ?: return emptyList() + + return getSuperClasses(classDescriptor) + .asSequence() + .filterNot { KotlinBuiltIns.isAnyOrNullableAny(it.defaultType) } + .map { generatePropertySignatureForType(propertyDescriptor, it) } + .toList() + } + + private fun getSuperClasses(classDescriptor: ClassDescriptor): List { + val supertypes = classDescriptor.defaultType.supertypes().toMutableList().sortSubtypesFirst() + return supertypes.mapNotNull { it.constructor.declarationDescriptor as? ClassDescriptor } + } + + private fun MutableList.sortSubtypesFirst(): List { + // TODO: rewrite this + val typeChecker = KotlinTypeChecker.DEFAULT + for (i in 1 until size) { + val currentType = this[i] + for (j in 0 until i) { + if (typeChecker.isSubtypeOf(currentType, this[j])) { + this.removeAt(i) + this.add(j, currentType) + break + } + } + } + return this + } + + private fun generatePropertySignatureForType( + propertyDescriptor: PropertyDescriptor, + typeDescriptor: ClassDescriptor + ): PropertyDescriptor { + val containerModality = typeDescriptor.modality + val modality = if (containerModality == SEALED || containerModality == ABSTRACT) ABSTRACT else propertyDescriptor.modality + return propertyDescriptor.newCopyBuilder() + .setOwner(typeDescriptor) + .setModality(modality) + .setVisibility(propertyDescriptor.visibility) + .setKind(CallableMemberDescriptor.Kind.DECLARATION) + .setCopyOverrides(false) + .build()!! + } + } +} \ 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 5504afd44e4..abe8680890d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -98,7 +98,8 @@ class QuickFixRegistrar : QuickFixContributor { NOTHING_TO_OVERRIDE.registerFactory( RemoveModifierFix.createRemoveModifierFromListOwnerFactory(OVERRIDE_KEYWORD), ChangeMemberFunctionSignatureFix, - AddFunctionToSupertypeFix + AddFunctionToSupertypeFix, + AddPropertyToSupertypeFix ) VIRTUAL_MEMBER_HIDDEN.registerFactory(AddModifierFix.createFactory(OVERRIDE_KEYWORD)) diff --git a/idea/testData/quickfix/addPropertyToSupertype/abstractClass.kt b/idea/testData/quickfix/addPropertyToSupertype/abstractClass.kt new file mode 100644 index 00000000000..6dc770a78c2 --- /dev/null +++ b/idea/testData/quickfix/addPropertyToSupertype/abstractClass.kt @@ -0,0 +1,6 @@ +// "Add 'abstract val hoge: Int' to 'Foo'" "true" +abstract class Foo + +class Bar: Foo() { + override val hoge = 3 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addPropertyToSupertype/abstractClass.kt.after b/idea/testData/quickfix/addPropertyToSupertype/abstractClass.kt.after new file mode 100644 index 00000000000..fff90fde21f --- /dev/null +++ b/idea/testData/quickfix/addPropertyToSupertype/abstractClass.kt.after @@ -0,0 +1,8 @@ +// "Add 'abstract val hoge: Int' to 'Foo'" "true" +abstract class Foo { + abstract val hoge: Int +} + +class Bar: Foo() { + override val hoge = 3 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addPropertyToSupertype/interface.kt b/idea/testData/quickfix/addPropertyToSupertype/interface.kt new file mode 100644 index 00000000000..9f3f66fe10f --- /dev/null +++ b/idea/testData/quickfix/addPropertyToSupertype/interface.kt @@ -0,0 +1,6 @@ +// "Add 'val hoge: Int' to 'Foo'" "true" +interface Foo + +class Bar: Foo { + override val hoge = 3 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addPropertyToSupertype/interface.kt.after b/idea/testData/quickfix/addPropertyToSupertype/interface.kt.after new file mode 100644 index 00000000000..1e576efc49a --- /dev/null +++ b/idea/testData/quickfix/addPropertyToSupertype/interface.kt.after @@ -0,0 +1,8 @@ +// "Add 'val hoge: Int' to 'Foo'" "true" +interface Foo { + val hoge: Int +} + +class Bar: Foo { + override val hoge = 3 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addPropertyToSupertype/openClass.kt b/idea/testData/quickfix/addPropertyToSupertype/openClass.kt new file mode 100644 index 00000000000..be9377c526b --- /dev/null +++ b/idea/testData/quickfix/addPropertyToSupertype/openClass.kt @@ -0,0 +1,6 @@ +// "Add 'open val hoge: Int' to 'Foo'" "true" +open class Foo + +class Bar: Foo() { + override val hoge = 3 +} \ No newline at end of file diff --git a/idea/testData/quickfix/addPropertyToSupertype/openClass.kt.after b/idea/testData/quickfix/addPropertyToSupertype/openClass.kt.after new file mode 100644 index 00000000000..c88e46d1faa --- /dev/null +++ b/idea/testData/quickfix/addPropertyToSupertype/openClass.kt.after @@ -0,0 +1,8 @@ +// "Add 'open val hoge: Int' to 'Foo'" "true" +open class Foo { + open val hoge: Int = 3 +} + +class Bar: Foo() { + override val hoge = 3 +} \ 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 c8570308457..954d12782a1 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java @@ -221,6 +221,19 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes } } + @TestMetadata("idea/testData/quickfix/addPropertyToSupertype") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class AddPropertyToSupertype extends AbstractQuickFixMultiFileTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInAddPropertyToSupertype() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/addPropertyToSupertype"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true); + } + } + @TestMetadata("idea/testData/quickfix/addReifiedToTypeParameterOfFunctionFix") @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 5664ba6d2c6..d6213a22cc1 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -821,6 +821,34 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { } } + @TestMetadata("idea/testData/quickfix/addPropertyToSupertype") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class AddPropertyToSupertype extends AbstractQuickFixTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + @TestMetadata("abstractClass.kt") + public void testAbstractClass() throws Exception { + runTest("idea/testData/quickfix/addPropertyToSupertype/abstractClass.kt"); + } + + public void testAllFilesPresentInAddPropertyToSupertype() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/addPropertyToSupertype"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("interface.kt") + public void testInterface() throws Exception { + runTest("idea/testData/quickfix/addPropertyToSupertype/interface.kt"); + } + + @TestMetadata("openClass.kt") + public void testOpenClass() throws Exception { + runTest("idea/testData/quickfix/addPropertyToSupertype/openClass.kt"); + } + } + @TestMetadata("idea/testData/quickfix/addReifiedToTypeParameterOfFunctionFix") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)