Extract Class: Implement 'Extract Interface' refactoring

#KT-11017 Fixed
This commit is contained in:
Alexey Sedunov
2016-09-22 14:29:03 +03:00
parent af2de09840
commit b412edf2a3
22 changed files with 592 additions and 232 deletions
+1
View File
@@ -144,6 +144,7 @@ These artifacts include extensions for the types available in the latter JDKs, s
- [`KT-13155`](https://youtrack.jetbrains.com/issue/KT-13155) Implement "Introduce Type Parameter" refactoring
- [`KT-11017`](https://youtrack.jetbrains.com/issue/KT-11017) Implement "Extract Superclass" refactoring
- [`KT-11017`](https://youtrack.jetbrains.com/issue/KT-11017) Implement "Extract Interface" refactoring
#### Android Lint
@@ -799,6 +799,7 @@ fun main(args: Array<String>) {
model("refactoring/introduceTypeParameter", pattern = KT_OR_KTS, testMethod = "doIntroduceTypeParameterTest")
model("refactoring/introduceTypeAlias", pattern = KT_OR_KTS, testMethod = "doIntroduceTypeAliasTest")
model("refactoring/extractSuperclass", pattern = KT_OR_KTS, testMethod = "doExtractSuperclassTest")
model("refactoring/extractInterface", pattern = KT_OR_KTS, testMethod = "doExtractInterfaceTest")
}
testClass<AbstractPullUpTest>() {
@@ -22,6 +22,7 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.refactoring.RefactoringActionHandler
import org.jetbrains.kotlin.idea.refactoring.changeSignature.KotlinChangeSignatureHandler
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.KotlinExtractInterfaceHandler
import org.jetbrains.kotlin.idea.refactoring.introduce.extractFunction.ExtractKotlinFunctionHandler
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.KotlinExtractSuperclassHandler
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter.KotlinIntroduceLambdaParameterHandler
@@ -81,6 +82,8 @@ class KotlinRefactoringSupportProvider : RefactoringSupportProvider() {
override fun getPushDownHandler() = KotlinPushDownHandler()
override fun getExtractSuperClassHandler() = KotlinExtractSuperclassHandler
override fun getExtractInterfaceHandler() = KotlinExtractInterfaceHandler
}
class KotlinVetoRenameCondition: Condition<PsiElement> {
@@ -68,22 +68,24 @@ import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier
import java.util.*
data class ExtractSuperclassInfo(
data class ExtractSuperInfo(
val originalClass: KtClassOrObject,
val memberInfos: Collection<KotlinMemberInfo>,
val targetParent: PsiElement,
val targetFileName: String,
val newClassName: String,
val isInterface: Boolean,
val docPolicy: DocCommentPolicy<*>
)
class ExtractSuperclassRefactoring(
private var extractInfo: ExtractSuperclassInfo
class ExtractSuperRefactoring(
private var extractInfo: ExtractSuperInfo
) {
companion object {
private fun getElementsToMove(
memberInfos: Collection<KotlinMemberInfo>,
originalClass: KtClassOrObject
originalClass: KtClassOrObject,
isExtractInterface: Boolean
): Map<KtElement, KotlinMemberInfo?> {
val project = originalClass.project
val elementsToMove = LinkedHashMap<KtElement, KotlinMemberInfo?>()
@@ -106,7 +108,7 @@ class ExtractSuperclassRefactoring(
?: continue
val superClassDescriptor = superType.constructor.declarationDescriptor ?: continue
val superClass = DescriptorToSourceUtilsIde.getAnyDeclaration(project, superClassDescriptor) as? KtClass ?: continue
if (!superClass.isInterface() || superClass in superInterfacesToMove) {
if ((!isExtractInterface && !superClass.isInterface()) || superClass in superInterfacesToMove) {
elementsToMove[superTypeListEntry] = null
}
}
@@ -119,7 +121,8 @@ class ExtractSuperclassRefactoring(
originalClass: KtClassOrObject,
memberInfos: List<KotlinMemberInfo>,
targetParent: PsiElement,
newClassName: String
newClassName: String,
isExtractInterface: Boolean
): MultiMap<PsiElement, String> {
val conflicts = MultiMap<PsiElement, String>()
@@ -133,7 +136,7 @@ class ExtractSuperclassRefactoring(
?.let { conflicts.putValue(it, "Class $newClassName already exists in the target scope") }
}
val elementsToMove = getElementsToMove(memberInfos, originalClass).keys
val elementsToMove = getElementsToMove(memberInfos, originalClass, isExtractInterface).keys
val moveTarget = if (targetParent is PsiDirectory) {
val targetPackage = targetParent.getPackage() ?: return conflicts
@@ -192,7 +195,7 @@ class ExtractSuperclassRefactoring(
collectTypeParameters(refTarget)
}
}
getElementsToMove(extractInfo.memberInfos, extractInfo.originalClass)
getElementsToMove(extractInfo.memberInfos, extractInfo.originalClass, extractInfo.isInterface)
.asSequence()
.flatMap {
val (element, info) = it
@@ -206,18 +209,22 @@ class ExtractSuperclassRefactoring(
val newClassName = extractInfo.newClassName
val originalClass = extractInfo.originalClass
val kind = if (extractInfo.isInterface) "interface" else "class"
val prototype = psiFactory.createClass("$kind $newClassName")
val newClass = if (targetParent is PsiDirectory) {
val template = FileTemplateManager.getInstance(project).getInternalTemplate("Kotlin File")
val newFile = NewKotlinFileAction.createFileFromTemplate(extractInfo.targetFileName, template, targetParent) as KtFile
newFile.add(psiFactory.createClass("class $newClassName")) as KtClass
newFile.add(prototype) as KtClass
}
else {
val targetSibling = originalClass.parentsWithSelf.first { it.parent == targetParent }
insertDeclaration(psiFactory.createClass("class $newClassName"), targetSibling)
insertDeclaration(prototype, targetSibling)
}
val shouldBeAbstract = extractInfo.memberInfos.any { it.isToAbstract }
newClass.addModifier(if (shouldBeAbstract) KtTokens.ABSTRACT_KEYWORD else KtTokens.OPEN_KEYWORD)
if (!extractInfo.isInterface) {
newClass.addModifier(if (shouldBeAbstract) KtTokens.ABSTRACT_KEYWORD else KtTokens.OPEN_KEYWORD)
}
if (typeParameters.isNotEmpty()) {
val typeParameterListText = typeParameters.sortedBy { it.startOffset }.map { it.text }.joinToString(prefix = "<", postfix = ">")
@@ -235,10 +242,11 @@ class ExtractSuperclassRefactoring(
append(typeParameters.sortedBy { it.startOffset }.map { it.name }.joinToString(prefix = "<", postfix = ">"))
}
}
val needSuperCall = superClassEntry is KtSuperTypeCallEntry
val needSuperCall = !extractInfo.isInterface
&& (superClassEntry is KtSuperTypeCallEntry
|| originalClass.hasPrimaryConstructor()
|| originalClass.getSecondaryConstructors().isEmpty()
val newSuperTypeCallEntry = if (needSuperCall) {
|| originalClass.getSecondaryConstructors().isEmpty())
val newSuperTypeListEntry = if (needSuperCall) {
psiFactory.createSuperTypeCallEntry("$superTypeText()")
}
else {
@@ -253,10 +261,10 @@ class ExtractSuperclassRefactoring(
}
else superClassEntry
newClass.addSuperTypeListEntry(superClassEntryToAdd)
ShortenReferences.DEFAULT.process(superClassEntry.replaced(newSuperTypeCallEntry))
ShortenReferences.DEFAULT.process(superClassEntry.replaced(newSuperTypeListEntry))
}
else {
ShortenReferences.DEFAULT.process(originalClass.addSuperTypeListEntry(newSuperTypeCallEntry))
ShortenReferences.DEFAULT.process(originalClass.addSuperTypeListEntry(newSuperTypeListEntry))
}
ShortenReferences.DEFAULT.process(newClass)
@@ -267,15 +275,17 @@ class ExtractSuperclassRefactoring(
fun performRefactoring() {
val originalClass = extractInfo.originalClass
KotlinExtractSuperclassHandler.getErrorMessage(originalClass)?.let {
throw CommonRefactoringUtil.RefactoringErrorHintException(it)
}
val handler = if (extractInfo.isInterface) KotlinExtractInterfaceHandler else KotlinExtractSuperclassHandler
handler.getErrorMessage(originalClass)?.let { throw CommonRefactoringUtil.RefactoringErrorHintException(it) }
val originalClassDescriptor = originalClass.resolveToDescriptor() as ClassDescriptor
val superClassDescriptor = originalClassDescriptor.getSuperClassNotAny()
val superClassEntry = originalClass.getSuperTypeListEntries().firstOrNull {
bindingContext[BindingContext.TYPE, it.typeReference]?.constructor?.declarationDescriptor == superClassDescriptor
val superClassEntry = if (!extractInfo.isInterface) {
val originalClassDescriptor = originalClass.resolveToDescriptor() as ClassDescriptor
val superClassDescriptor = originalClassDescriptor.getSuperClassNotAny()
originalClass.getSuperTypeListEntries().firstOrNull {
bindingContext[BindingContext.TYPE, it.typeReference]?.constructor?.declarationDescriptor == superClassDescriptor
}
}
else null
project.runSynchronouslyWithProgress(RefactoringBundle.message("progress.text"), true) { runReadAction { analyzeContext() } }
@@ -0,0 +1,45 @@
/*
* Copyright 2010-2016 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.refactoring.introduce.extractClass
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.refactoring.util.CommonRefactoringUtil
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ui.KotlinExtractInterfaceDialog
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
object KotlinExtractInterfaceHandler : KotlinExtractSuperHandlerBase(true) {
val REFACTORING_NAME = "Extract Interface"
override fun getErrorMessage(klass: KtClassOrObject): String? {
if (klass is KtClass && klass.isAnnotation()) return "Interface cannot be extracted from an annotation class"
return null
}
override fun doInvoke(klass: KtClassOrObject, targetParent: PsiElement, project: Project, editor: Editor?) {
if (!CommonRefactoringUtil.checkReadOnlyStatus(project, klass)) return
KotlinExtractInterfaceDialog(
originalClass = klass,
targetParent = targetParent,
conflictChecker = { checkConflicts(klass, it) },
refactoring = { ExtractSuperRefactoring(it).performRefactoring() }
).show()
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2010-2016 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.refactoring.introduce.extractClass
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.refactoring.RefactoringActionHandler
import com.intellij.refactoring.extractSuperclass.ExtractSuperClassUtil
import com.intellij.refactoring.lang.ElementsHandler
import org.jetbrains.kotlin.idea.refactoring.SeparateFileWrapper
import org.jetbrains.kotlin.idea.refactoring.chooseContainerElementIfNecessary
import org.jetbrains.kotlin.idea.refactoring.getExtractionContainers
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ui.KotlinExtractSuperDialogBase
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
abstract class KotlinExtractSuperHandlerBase(private val isExtractInterface: Boolean) : RefactoringActionHandler, ElementsHandler {
override fun isEnabledOnElements(elements: Array<out PsiElement>) = elements.singleOrNull() is KtClassOrObject
override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) {
val offset = editor.caretModel.offset
val element = file.findElementAt(offset) ?: return
val klass = element.getNonStrictParentOfType<KtClassOrObject>() ?: return
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
selectElements(klass, project, editor)
}
override fun invoke(project: Project, elements: Array<out PsiElement>, dataContext: DataContext?) {
if (dataContext == null) return
val editor = CommonDataKeys.EDITOR.getData(dataContext)
val klass = PsiTreeUtil.findCommonParent(*elements)?.getNonStrictParentOfType<KtClassOrObject>() ?: return
selectElements(klass, project, editor)
}
fun selectElements(klass: KtClassOrObject, project: Project, editor: Editor?) {
val containers = klass.getExtractionContainers(strict = true, includeAll = true) + SeparateFileWrapper(klass.manager)
if (editor == null) return doInvoke(klass, containers.first(), project, editor)
chooseContainerElementIfNecessary(
containers,
editor,
if (containers.first() is KtFile) "Select target file" else "Select target code block / file",
true,
{ it },
{ doInvoke(klass, if (it is SeparateFileWrapper) klass.containingFile.parent!! else it, project, editor) }
)
}
protected fun checkConflicts(originalClass: KtClassOrObject, dialog: KotlinExtractSuperDialogBase): Boolean {
val conflicts = ExtractSuperRefactoring.collectConflicts(
originalClass,
dialog.selectedMembers,
dialog.selectedTargetParent,
dialog.extractedSuperName,
isExtractInterface
)
return ExtractSuperClassUtil.showConflicts(dialog, conflicts, originalClass.project)
}
internal abstract fun getErrorMessage(klass: KtClassOrObject): String?
protected abstract fun doInvoke(klass: KtClassOrObject, targetParent: PsiElement, project: Project, editor: Editor?)
}
@@ -16,65 +16,20 @@
package org.jetbrains.kotlin.idea.refactoring.introduce.extractClass
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.refactoring.HelpID
import com.intellij.refactoring.RefactoringActionHandler
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.extractSuperclass.ExtractSuperClassUtil
import com.intellij.refactoring.lang.ElementsHandler
import com.intellij.refactoring.util.CommonRefactoringUtil
import org.jetbrains.kotlin.idea.refactoring.SeparateFileWrapper
import org.jetbrains.kotlin.idea.refactoring.chooseContainerElementIfNecessary
import org.jetbrains.kotlin.idea.refactoring.getExtractionContainers
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ui.KotlinExtractSuperclassDialog
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
object KotlinExtractSuperclassHandler : RefactoringActionHandler, ElementsHandler {
object KotlinExtractSuperclassHandler : KotlinExtractSuperHandlerBase(false) {
val REFACTORING_NAME = "Extract Superclass"
override fun isEnabledOnElements(elements: Array<out PsiElement>) = elements.singleOrNull() is KtClassOrObject
override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) {
val offset = editor.caretModel.offset
val element = file.findElementAt(offset) ?: return
val klass = element.getNonStrictParentOfType<KtClassOrObject>() ?: return
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
selectElements(klass, project, editor)
}
override fun invoke(project: Project, elements: Array<out PsiElement>, dataContext: DataContext?) {
if (dataContext == null) return
val editor = CommonDataKeys.EDITOR.getData(dataContext)
val klass = PsiTreeUtil.findCommonParent(*elements)?.getNonStrictParentOfType<KtClassOrObject>() ?: return
selectElements(klass, project, editor)
}
fun selectElements(klass: KtClassOrObject, project: Project, editor: Editor?) {
val containers = klass.getExtractionContainers(strict = true, includeAll = true) + SeparateFileWrapper(klass.manager)
if (editor == null) return doInvoke(klass, containers.first(), project, editor)
chooseContainerElementIfNecessary(
containers,
editor,
if (containers.first() is KtFile) "Select target file" else "Select target code block / file",
true,
{ it },
{ doInvoke(klass, it, project, editor) }
)
}
fun getErrorMessage(klass: KtClassOrObject): String? {
override fun getErrorMessage(klass: KtClassOrObject): String? {
if (klass is KtClass) {
if (klass.isInterface()) return RefactoringBundle.message("superclass.cannot.be.extracted.from.an.interface")
if (klass.isEnum()) return RefactoringBundle.message("superclass.cannot.be.extracted.from.an.enum")
@@ -83,30 +38,18 @@ object KotlinExtractSuperclassHandler : RefactoringActionHandler, ElementsHandle
return null
}
private fun checkConflicts(originalClass: KtClassOrObject, dialog: KotlinExtractSuperclassDialog): Boolean {
val conflicts = ExtractSuperclassRefactoring.collectConflicts(
originalClass,
dialog.selectedMembers,
dialog.selectedTargetParent,
dialog.extractedSuperName
)
return ExtractSuperClassUtil.showConflicts(dialog, conflicts, originalClass.project)
}
private fun doInvoke(klass: KtClassOrObject, container: PsiElement, project: Project, editor: Editor?) {
override fun doInvoke(klass: KtClassOrObject, targetParent: PsiElement, project: Project, editor: Editor?) {
if (!CommonRefactoringUtil.checkReadOnlyStatus(project, klass)) return
getErrorMessage(klass)?.let {
CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.getCannotRefactorMessage(it), REFACTORING_NAME, HelpID.EXTRACT_SUPERCLASS)
}
val targetParent = (if (container is SeparateFileWrapper) klass.containingFile.parent else container) ?: return
KotlinExtractSuperclassDialog(
originalClass = klass,
targetParent = targetParent,
conflictChecker = { checkConflicts(klass, it) },
refactoring = { ExtractSuperclassRefactoring(it).performRefactoring() }
refactoring = { ExtractSuperRefactoring(it).performRefactoring() }
).show()
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2010-2016 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.refactoring.introduce.extractClass.ui
import com.intellij.psi.PsiElement
import com.intellij.refactoring.HelpID
import com.intellij.refactoring.JavaRefactoringSettings
import com.intellij.refactoring.RefactoringBundle
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ExtractSuperInfo
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.KotlinExtractInterfaceHandler
import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo
import org.jetbrains.kotlin.idea.refactoring.memberInfo.extractClassMembers
import org.jetbrains.kotlin.idea.refactoring.pullUp.mustBeAbstractInInterface
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty
class KotlinExtractInterfaceDialog(
originalClass: KtClassOrObject,
targetParent: PsiElement,
conflictChecker: (KotlinExtractSuperDialogBase) -> Boolean,
refactoring: (ExtractSuperInfo) -> Unit
) : KotlinExtractSuperDialogBase(originalClass, targetParent, conflictChecker, true, KotlinExtractInterfaceHandler.REFACTORING_NAME, refactoring) {
companion object {
private val DESTINATION_PACKAGE_RECENT_KEY = "KotlinExtractInterfaceDialog.RECENT_KEYS"
}
init {
init()
}
override fun createMemberInfoModel(): MemberInfoModelBase {
val extractableMemberInfos = extractClassMembers(originalClass).filterNot {
val member = it.member
member is KtClass && member.hasModifier(KtTokens.INNER_KEYWORD)
}
extractableMemberInfos.forEach { it.isToAbstract = true }
return object : MemberInfoModelBase(extractableMemberInfos) {
override fun isAbstractEnabled(memberInfo: KotlinMemberInfo): Boolean {
val member = memberInfo.member
return member is KtNamedFunction || (member is KtProperty && !member.mustBeAbstractInInterface())
}
override fun isAbstractWhenDisabled(member: KotlinMemberInfo) = member.member is KtProperty
}
}
override fun getDestinationPackageRecentKey() = DESTINATION_PACKAGE_RECENT_KEY
override fun getClassNameLabelText() = RefactoringBundle.message("interface.name.prompt")!!
override fun getPackageNameLabelText() = RefactoringBundle.message("package.for.new.interface")!!
override fun getEntityName() = RefactoringBundle.message("extractSuperInterface.interface")!!
override fun getTopLabelText() = RefactoringBundle.message("extract.interface.from")!!
override fun getDocCommentPolicySetting() = JavaRefactoringSettings.getInstance().EXTRACT_INTERFACE_JAVADOC
override fun setDocCommentPolicySetting(policy: Int) {
JavaRefactoringSettings.getInstance().EXTRACT_INTERFACE_JAVADOC = policy
}
override fun getExtractedSuperNameNotSpecifiedMessage() = RefactoringBundle.message("no.interface.name.specified")!!
override fun getHelpId() = HelpID.EXTRACT_INTERFACE
override fun createExtractedSuperNameField() = super.createExtractedSuperNameField()!!.apply { text = "I${originalClass.name}" }
}
@@ -0,0 +1,144 @@
/*
* Copyright 2010-2016 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.refactoring.introduce.extractClass.ui
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.classMembers.AbstractMemberInfoModel
import com.intellij.refactoring.classMembers.MemberInfoChange
import com.intellij.refactoring.extractSuperclass.JavaExtractSuperBaseDialog
import com.intellij.refactoring.util.DocCommentPolicy
import com.intellij.refactoring.util.RefactoringMessageUtil
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.asJava.unwrapped
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
import org.jetbrains.kotlin.idea.core.quoteIfNeeded
import org.jetbrains.kotlin.idea.core.unquote
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ExtractSuperInfo
import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo
import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionPanel
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import java.awt.BorderLayout
import javax.swing.Box
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTextField
abstract class KotlinExtractSuperDialogBase(
protected val originalClass: KtClassOrObject,
protected val targetParent: PsiElement,
private val conflictChecker: (KotlinExtractSuperDialogBase) -> Boolean,
private val isExtractInterface: Boolean,
refactoringName: String,
private val refactoring: (ExtractSuperInfo) -> Unit
) : JavaExtractSuperBaseDialog(originalClass.project, originalClass.toLightClass()!!, emptyList(), refactoringName) {
private lateinit var memberInfoModel: MemberInfoModelBase
val selectedMembers: List<KotlinMemberInfo>
get() = memberInfoModel.memberInfos.filter { it.isChecked }
private val fileNameField = JTextField()
open class MemberInfoModelBase(
val memberInfos: List<KotlinMemberInfo>
) : AbstractMemberInfoModel<KtNamedDeclaration, KotlinMemberInfo>() {
override fun isFixedAbstract(memberInfo: KotlinMemberInfo?) = true
}
val selectedTargetParent: PsiElement
get() = if (targetParent is PsiDirectory) targetDirectory else targetParent
val targetFileName: String
get() = fileNameField.text
protected abstract fun createMemberInfoModel(): MemberInfoModelBase
override fun getDocCommentPanelName() = "KDoc for abstracts"
override fun checkConflicts() = conflictChecker(this)
override fun createActionComponent() = Box.createHorizontalBox()!!
override fun createDestinationRootPanel() = if (targetParent is PsiDirectory) super.createDestinationRootPanel() else null
override fun createNorthPanel(): JComponent? {
return super.createNorthPanel().apply {
if (targetParent !is PsiDirectory) {
myPackageNameLabel.parent.remove(myPackageNameLabel)
myPackageNameField.parent.remove(myPackageNameField)
}
}
}
override fun createCenterPanel(): JComponent? {
memberInfoModel = createMemberInfoModel().apply {
memberInfoChanged(MemberInfoChange(memberInfos))
}
return JPanel(BorderLayout()).apply {
val memberSelectionPanel = KotlinMemberSelectionPanel(
RefactoringBundle.message(if (isExtractInterface) "members.to.form.interface" else "members.to.form.superclass"),
memberInfoModel.memberInfos,
RefactoringBundle.message("make.abstract")
)
memberSelectionPanel.table.memberInfoModel = memberInfoModel
memberSelectionPanel.table.addMemberInfoChangeListener(memberInfoModel)
add(memberSelectionPanel, BorderLayout.CENTER)
add(myDocCommentPanel, BorderLayout.EAST)
}
}
override fun init() {
super.init()
fileNameField.text = "$extractedSuperName.${KotlinFileType.EXTENSION}"
}
override fun preparePackage() {
if (targetParent is PsiDirectory) super.preparePackage()
}
override fun isExtractSuperclass() = true
override fun validateName(name: String): String? {
return when {
!KotlinNameSuggester.isIdentifier(name.quoteIfNeeded()) -> RefactoringMessageUtil.getIncorrectIdentifierMessage(name)
name.unquote() == mySourceClass.name -> "Different name expected"
else -> null
}
}
override fun createProcessor() = null
override fun executeRefactoring() {
val extractInfo = ExtractSuperInfo(
mySourceClass.unwrapped as KtClassOrObject,
selectedMembers,
if (targetParent is PsiDirectory) targetDirectory else targetParent,
targetFileName,
extractedSuperName.quoteIfNeeded(),
isExtractInterface,
DocCommentPolicy<PsiComment>(docCommentPolicy)
)
refactoring(extractInfo)
}
}
@@ -16,81 +16,39 @@
package org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ui
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.refactoring.HelpID
import com.intellij.refactoring.JavaRefactoringSettings
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.classMembers.AbstractMemberInfoModel
import com.intellij.refactoring.classMembers.MemberInfoChange
import com.intellij.refactoring.extractSuperclass.ExtractSuperBaseDialog
import com.intellij.refactoring.extractSuperclass.JavaExtractSuperBaseDialog
import com.intellij.refactoring.util.DocCommentPolicy
import com.intellij.refactoring.util.RefactoringMessageUtil
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.FormBuilder
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.asJava.unwrapped
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
import org.jetbrains.kotlin.idea.core.quoteIfNeeded
import org.jetbrains.kotlin.idea.core.unquote
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ExtractSuperclassInfo
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ExtractSuperInfo
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.KotlinExtractSuperclassHandler
import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo
import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionPanel
import org.jetbrains.kotlin.idea.refactoring.memberInfo.extractClassMembers
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty
import java.awt.BorderLayout
import javax.swing.*
class KotlinExtractSuperclassDialog(
originalClass: KtClassOrObject,
private val targetParent: PsiElement,
private val conflictChecker: (KotlinExtractSuperclassDialog) -> Boolean,
private val refactoring: (ExtractSuperclassInfo) -> Unit
) : JavaExtractSuperBaseDialog(
originalClass.project,
originalClass.toLightClass()!!,
emptyList(),
KotlinExtractSuperclassHandler.REFACTORING_NAME
) {
targetParent: PsiElement,
conflictChecker: (KotlinExtractSuperDialogBase) -> Boolean,
refactoring: (ExtractSuperInfo) -> Unit
) : KotlinExtractSuperDialogBase(originalClass, targetParent, conflictChecker, false, KotlinExtractSuperclassHandler.REFACTORING_NAME, refactoring) {
companion object {
private val DESTINATION_PACKAGE_RECENT_KEY = "KotlinExtractSuperclassDialog.RECENT_KEYS"
}
val kotlinMemberInfos = extractClassMembers(originalClass)
val selectedMembers: List<KotlinMemberInfo>
get() = kotlinMemberInfos.filter { it.isChecked }
private val fileNameField = JTextField()
private val memberInfoModel = object : AbstractMemberInfoModel<KtNamedDeclaration, KotlinMemberInfo>() {
override fun isFixedAbstract(memberInfo: KotlinMemberInfo?) = true
override fun isAbstractEnabled(memberInfo: KotlinMemberInfo): Boolean {
val member = memberInfo.member
return member is KtNamedFunction || member is KtProperty
}
}.apply {
memberInfoChanged(MemberInfoChange(kotlinMemberInfos))
}
val selectedTargetParent: PsiElement
get() = if (targetParent is PsiDirectory) targetDirectory else targetParent
val targetFileName: String
get() = fileNameField.text
init {
init()
}
fileNameField.text = "$extractedSuperName.${KotlinFileType.EXTENSION}"
override fun createMemberInfoModel(): MemberInfoModelBase {
return object : MemberInfoModelBase(extractClassMembers(originalClass)) {
override fun isAbstractEnabled(memberInfo: KotlinMemberInfo): Boolean {
val member = memberInfo.member
return member is KtNamedFunction || member is KtProperty
}
}
}
override fun getDestinationPackageRecentKey() = DESTINATION_PACKAGE_RECENT_KEY
@@ -109,94 +67,7 @@ class KotlinExtractSuperclassDialog(
JavaRefactoringSettings.getInstance().EXTRACT_SUPERCLASS_JAVADOC = policy
}
override fun getDocCommentPanelName() = "KDoc for abstracts"
override fun getExtractedSuperNameNotSpecifiedMessage() = RefactoringBundle.message("no.superclass.name.specified")!!
override fun getHelpId() = HelpID.EXTRACT_SUPERCLASS
override fun validateName(name: String): String? {
return when {
!KotlinNameSuggester.isIdentifier(name.quoteIfNeeded()) -> RefactoringMessageUtil.getIncorrectIdentifierMessage(name)
name.unquote() == mySourceClass.name -> "Different name expected"
else -> null
}
}
override fun checkConflicts() = conflictChecker(this)
override fun createActionComponent() = Box.createHorizontalBox()!!
override fun createDestinationRootPanel(): JPanel? {
if (targetParent !is PsiDirectory) return null
val targetDirectoryPanel = super.createDestinationRootPanel()
val targetFileNamePanel = JPanel(BorderLayout()).apply {
border = BorderFactory.createEmptyBorder(10, 0, 0, 0)
val label = JBLabel("Target file name:")
add(label, BorderLayout.NORTH)
label.labelFor = fileNameField
add(fileNameField, BorderLayout.CENTER)
}
return FormBuilder
.createFormBuilder()
.addComponent(targetDirectoryPanel)
.addComponent(targetFileNamePanel)
.panel
}
override fun createNorthPanel(): JComponent? {
return super.createNorthPanel().apply {
if (targetParent !is PsiDirectory) {
myPackageNameLabel.parent.remove(myPackageNameLabel)
myPackageNameField.parent.remove(myPackageNameField)
}
}
}
override fun createCenterPanel(): JComponent? {
return JPanel(BorderLayout()).apply {
val memberSelectionPanel = KotlinMemberSelectionPanel(
RefactoringBundle.message("members.to.form.superclass"),
kotlinMemberInfos,
RefactoringBundle.message("make.abstract")
)
memberSelectionPanel.table.memberInfoModel = memberInfoModel
memberSelectionPanel.table.addMemberInfoChangeListener(memberInfoModel)
add(memberSelectionPanel, BorderLayout.CENTER)
add(myDocCommentPanel, BorderLayout.EAST)
}
}
override fun isExtractSuperclass() = true
override fun preparePackage() {
if (targetParent !is PsiDirectory) return
super.preparePackage()
val fileName = targetFileName
if (!fileName.endsWith(".${KotlinFileType.EXTENSION}")) {
throw ExtractSuperBaseDialog.OperationFailedException("Invalid Kotlin file name: $fileName")
}
RefactoringMessageUtil.checkCanCreateFile(myTargetDirectory, fileName)?.let {
throw ExtractSuperBaseDialog.OperationFailedException(it)
}
}
override fun createProcessor() = null
override fun executeRefactoring() {
val extractInfo = ExtractSuperclassInfo(
mySourceClass.unwrapped as KtClassOrObject,
selectedMembers,
if (targetParent is PsiDirectory) targetDirectory else targetParent,
targetFileName,
extractedSuperName.quoteIfNeeded(),
DocCommentPolicy<PsiComment>(docCommentPolicy)
)
refactoring(extractInfo)
}
}
@@ -0,0 +1,13 @@
// NAME: X
// INFO: {checked: "true"}
interface T {}
open class A
// SIBLING:
class <caret>B : A(), T {
// INFO: {checked: "true"}
fun foo() {
}
}
@@ -0,0 +1,16 @@
// NAME: X
// INFO: {checked: "true"}
interface T {}
open class A
interface X : T {
// INFO: {checked: "true"}
fun foo() {
}
}
// SIBLING:
class B : A(), X {
}
@@ -0,0 +1,13 @@
// NAME: X
// INFO: {checked: "true"}
interface T {}
open class A
// SIBLING:
class <caret>B : A(), T {
// INFO: {checked: "true", toAbstract: "true"}
fun foo() {
}
}
@@ -0,0 +1,18 @@
// NAME: X
// INFO: {checked: "true"}
interface T {}
open class A
interface X : T {
// INFO: {checked: "true", toAbstract: "true"}
fun foo()
}
// SIBLING:
class B : A(), X {
// INFO: {checked: "true", toAbstract: "true"}
override fun foo() {
}
}
@@ -0,0 +1,14 @@
// NAME: B
// INFO: {checked: "true"}
interface I<T>
open class J<T>
// SIBLING:
class <caret>A<T, U : List<T>, V, W, X> : J<X>(), I<W> {
// INFO: {checked: "true"}
fun foo(u: U) {
}
}
@@ -0,0 +1,17 @@
// NAME: B
// INFO: {checked: "true"}
interface I<T>
open class J<T>
interface B<T, U : List<T>, W> : I<W> {
// INFO: {checked: "true"}
fun foo(u: U) {
}
}
// SIBLING:
class A<T, U : List<T>, V, W, X> : J<X>(), B<T, U, W> {
}
@@ -0,0 +1,14 @@
// NAME: B
// INFO: {checked: "true"}
interface I<T>
open class J<T>
// SIBLING:
class <caret>A<T, U : List<T>, V, W, X> : J<X>(), I<W> {
// INFO: {checked: "true", toAbstract: "true"}
fun foo() {
val u: U
}
}
@@ -0,0 +1,19 @@
// NAME: B
// INFO: {checked: "true"}
interface I<T>
open class J<T>
interface B<W> : I<W> {
// INFO: {checked: "true", toAbstract: "true"}
fun foo()
}
// SIBLING:
class A<T, U : List<T>, V, W, X> : J<X>(), B<W> {
// INFO: {checked: "true", toAbstract: "true"}
override fun foo() {
val u: U
}
}
@@ -0,0 +1,3 @@
// NAME: B
// SIBLING:
annotation class <caret>A
@@ -0,0 +1 @@
Interface cannot be extracted from an annotation class
@@ -42,8 +42,8 @@ import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
import org.jetbrains.kotlin.idea.refactoring.checkConflictsInteractively
import org.jetbrains.kotlin.idea.refactoring.chooseMembers
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ExtractSuperclassInfo
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ExtractSuperclassRefactoring
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ExtractSuperInfo
import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.ExtractSuperRefactoring
import org.jetbrains.kotlin.idea.refactoring.introduce.extractFunction.EXTRACT_FUNCTION
import org.jetbrains.kotlin.idea.refactoring.introduce.extractFunction.ExtractKotlinFunctionHandler
import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.*
@@ -354,7 +354,7 @@ abstract class AbstractExtractionTest() : KotlinLightCodeInsightFixtureTestCase(
}
}
protected fun doExtractSuperclassTest(path: String) {
protected fun doExtractSuperTest(path: String, isInterface: Boolean) {
doTest(path) { file ->
file as KtFile
@@ -366,21 +366,26 @@ abstract class AbstractExtractionTest() : KotlinLightCodeInsightFixtureTestCase(
val editor = fixture.editor
val originalClass = file.findElementAt(editor.caretModel.offset)?.getStrictParentOfType<KtClassOrObject>()!!
val memberInfos = chooseMembers(extractClassMembers(originalClass))
val conflicts = ExtractSuperclassRefactoring.collectConflicts(originalClass, memberInfos, targetParent, className)
val conflicts = ExtractSuperRefactoring.collectConflicts(originalClass, memberInfos, targetParent, className, isInterface)
project.checkConflictsInteractively(conflicts) {
val extractInfo = ExtractSuperclassInfo(
val extractInfo = ExtractSuperInfo(
originalClass,
memberInfos,
targetParent,
"$className.${KotlinFileType.EXTENSION}",
className,
isInterface,
DocCommentPolicy<PsiComment>(DocCommentPolicy.ASIS)
)
ExtractSuperclassRefactoring(extractInfo).performRefactoring()
ExtractSuperRefactoring(extractInfo).performRefactoring()
}
}
}
protected fun doExtractSuperclassTest(path: String) = doExtractSuperTest(path, false)
protected fun doExtractInterfaceTest(path: String) = doExtractSuperTest(path, true)
protected fun doTest(path: String, checkAdditionalAfterdata: Boolean = false, action: (PsiFile) -> Unit) {
val mainFile = File(path)
val afterFile = File("$path.after")
@@ -4258,4 +4258,43 @@ public class ExtractionTestGenerated extends AbstractExtractionTest {
doExtractSuperclassTest(fileName);
}
}
@TestMetadata("idea/testData/refactoring/extractInterface")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ExtractInterface extends AbstractExtractionTest {
@TestMetadata("addInterface.kt")
public void testAddInterface() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractInterface/addInterface.kt");
doExtractInterfaceTest(fileName);
}
@TestMetadata("addInterfaceWithAbstract.kt")
public void testAddInterfaceWithAbstract() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractInterface/addInterfaceWithAbstract.kt");
doExtractInterfaceTest(fileName);
}
@TestMetadata("addTypeParameters.kt")
public void testAddTypeParameters() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractInterface/addTypeParameters.kt");
doExtractInterfaceTest(fileName);
}
@TestMetadata("addTypeParametersWithAbstract.kt")
public void testAddTypeParametersWithAbstract() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractInterface/addTypeParametersWithAbstract.kt");
doExtractInterfaceTest(fileName);
}
public void testAllFilesPresentInExtractInterface() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/extractInterface"), Pattern.compile("^(.+)\\.(kt|kts)$"), true);
}
@TestMetadata("annotation.kt")
public void testAnnotation() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractInterface/annotation.kt");
doExtractInterfaceTest(fileName);
}
}
}