diff --git a/ChangeLog.md b/ChangeLog.md index 3de9db47d13..095310b8551 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -143,6 +143,7 @@ These artifacts include extensions for the types available in the latter JDKs, s ##### New features - [`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 #### Android Lint diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index da5b346f885..5452cca7c4f 100755 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -798,6 +798,7 @@ fun main(args: Array) { model("refactoring/introduceJavaParameter", extension = "java", testMethod = "doIntroduceJavaParameterTest") 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") } testClass() { diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinFileAction.kt b/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinFileAction.kt index 540ad2edd80..033883b58e7 100644 --- a/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinFileAction.kt +++ b/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinFileAction.kt @@ -82,58 +82,63 @@ class NewKotlinFileAction return obj is NewKotlinFileAction } - override fun createFileFromTemplate(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? { - val directorySeparators = if (template.name == "Kotlin File") arrayOf('/', '\\') else arrayOf('/', '\\', '.') - val (className, targetDir) = findOrCreateTarget(dir, name, directorySeparators) + override fun createFileFromTemplate(name: String, template: FileTemplate, dir: PsiDirectory) = + Companion.createFileFromTemplate(name, template, dir) - val service = DumbService.getInstance(dir.project) - service.isAlternativeResolveEnabled = true - try { - return createFromTemplate(targetDir, className, template) - } - finally { - service.isAlternativeResolveEnabled = false - } - } + companion object { + private fun findOrCreateTarget(dir: PsiDirectory, name: String, directorySeparators: Array): Pair { + var className = name.removeSuffix(".kt") + var targetDir = dir - private fun findOrCreateTarget(dir: PsiDirectory, name: String, directorySeparators: Array): Pair { - var className = name.removeSuffix(".kt") - var targetDir = dir + for (splitChar in directorySeparators) { + if (splitChar in className) { + val names = className.trim().split(splitChar) - for (splitChar in directorySeparators) { - if (splitChar in className) { - val names = className.trim().split(splitChar) + for (dirName in names.dropLast(1)) { + targetDir = targetDir.findSubdirectory(dirName) ?: targetDir.createSubdirectory(dirName) + } - for (dirName in names.dropLast(1)) { - targetDir = targetDir.findSubdirectory(dirName) ?: targetDir.createSubdirectory(dirName) + className = names.last() + break } + } + return Pair(className, targetDir) + } - className = names.last() - break + private fun createFromTemplate(dir: PsiDirectory, className: String, template: FileTemplate): PsiFile? { + val project = dir.project + val defaultProperties = FileTemplateManager.getInstance(project).defaultProperties + + val properties = Properties(defaultProperties) + + val element = try { + CreateFromTemplateDialog(project, dir, template, + AttributesDefaults(className).withFixedName(true), + properties).create() + } + catch (e: IncorrectOperationException) { + throw e + } + catch (e: Exception) { + LOG.error(e) + return null + } + + return element?.containingFile + } + + fun createFileFromTemplate(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? { + val directorySeparators = if (template.name == "Kotlin File") arrayOf('/', '\\') else arrayOf('/', '\\', '.') + val (className, targetDir) = findOrCreateTarget(dir, name, directorySeparators) + + val service = DumbService.getInstance(dir.project) + service.isAlternativeResolveEnabled = true + try { + return createFromTemplate(targetDir, className, template) + } + finally { + service.isAlternativeResolveEnabled = false } } - return Pair(className, targetDir) - } - - private fun createFromTemplate(dir: PsiDirectory, className: String, template: FileTemplate): PsiFile? { - val project = dir.project - val defaultProperties = FileTemplateManager.getInstance(project).defaultProperties - - val properties = Properties(defaultProperties) - - val element = try { - CreateFromTemplateDialog(project, dir, template, - AttributesDefaults(className).withFixedName(true), - properties).create() - } - catch (e: IncorrectOperationException) { - throw e - } - catch (e: Exception) { - LOG.error(e) - return null - } - - return element?.containingFile } } diff --git a/idea/src/org/jetbrains/kotlin/idea/findUsages/KotlinElementDescriptionProvider.kt b/idea/src/org/jetbrains/kotlin/idea/findUsages/KotlinElementDescriptionProvider.kt index 3ad7cd0fc01..f357a39ce04 100644 --- a/idea/src/org/jetbrains/kotlin/idea/findUsages/KotlinElementDescriptionProvider.kt +++ b/idea/src/org/jetbrains/kotlin/idea/findUsages/KotlinElementDescriptionProvider.kt @@ -17,6 +17,7 @@ package org.jetbrains.kotlin.idea.findUsages import com.intellij.codeInsight.highlighting.HighlightUsagesDescriptionLocation +import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.ElementDescriptionLocation import com.intellij.psi.ElementDescriptionProvider import com.intellij.psi.PsiElement @@ -33,6 +34,7 @@ import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.refactoring.rename.RenameJavaSyntheticPropertyHandler import org.jetbrains.kotlin.idea.refactoring.rename.RenameKotlinPropertyProcessor import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor +import org.jetbrains.kotlin.idea.util.string.collapseSpaces import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.renderer.DescriptorRenderer import org.jetbrains.kotlin.resolve.DescriptorUtils @@ -72,7 +74,12 @@ class KotlinElementDescriptionProvider : ElementDescriptionProvider { targetElement.parent as? KtProperty } else targetElement as? PsiNamedElement - if (namedElement == null || namedElement.language != KotlinLanguage.INSTANCE) return null + if (namedElement == null) { + return if (targetElement is KtElement) "'" + StringUtil.shortenTextWithEllipsis(targetElement.text.collapseSpaces(), 53, 0) + "'" else null + } + + if (namedElement.language != KotlinLanguage.INSTANCE) return null + return when(location) { is UsageViewTypeLocation -> elementKind() is UsageViewShortNameLocation, is UsageViewLongNameLocation -> namedElement.name diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt index 7cc0224d0c0..468f1a1f8f8 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt @@ -23,6 +23,7 @@ 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.extractFunction.ExtractKotlinFunctionHandler +import org.jetbrains.kotlin.idea.refactoring.introduce.extractClass.KotlinExtractSuperclassHandler import org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter.KotlinIntroduceLambdaParameterHandler import org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter.KotlinIntroduceParameterHandler import org.jetbrains.kotlin.idea.refactoring.introduce.introduceProperty.KotlinIntroducePropertyHandler @@ -78,6 +79,8 @@ class KotlinRefactoringSupportProvider : RefactoringSupportProvider() { override fun getPullUpHandler() = KotlinPullUpHandler() override fun getPushDownHandler() = KotlinPushDownHandler() + + override fun getExtractSuperClassHandler() = KotlinExtractSuperclassHandler } class KotlinVetoRenameCondition: Condition { diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ExtractSuperclassRefactoring.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ExtractSuperclassRefactoring.kt new file mode 100644 index 00000000000..1f72c9c370e --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ExtractSuperclassRefactoring.kt @@ -0,0 +1,298 @@ +/* + * 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.ide.fileTemplates.FileTemplateManager +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.search.searches.MethodReferencesSearch +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.refactoring.RefactoringBundle +import com.intellij.refactoring.extractSuperclass.ExtractSuperClassUtil +import com.intellij.refactoring.memberPullUp.PullUpProcessor +import com.intellij.refactoring.util.CommonRefactoringUtil +import com.intellij.refactoring.util.DocCommentPolicy +import com.intellij.refactoring.util.MoveRenameUsageInfo +import com.intellij.usageView.UsageInfo +import com.intellij.util.containers.MultiMap +import org.jetbrains.kotlin.asJava.toLightClass +import org.jetbrains.kotlin.asJava.toLightMethods +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.idea.actions.NewKotlinFileAction +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor +import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde +import org.jetbrains.kotlin.idea.codeInsight.shorten.performDelayedShortening +import org.jetbrains.kotlin.idea.core.ShortenReferences +import org.jetbrains.kotlin.idea.core.copied +import org.jetbrains.kotlin.idea.core.getPackage +import org.jetbrains.kotlin.idea.core.replaced +import org.jetbrains.kotlin.idea.refactoring.introduce.insertDeclaration +import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo +import org.jetbrains.kotlin.idea.refactoring.memberInfo.getChildrenToAnalyze +import org.jetbrains.kotlin.idea.refactoring.memberInfo.toJavaMemberInfo +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.KotlinMoveTargetForDeferredFile +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.KotlinMoveTargetForExistingElement +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveConflictChecker +import org.jetbrains.kotlin.idea.refactoring.runSynchronouslyWithProgress +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.idea.util.getResolutionScope +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier +import java.util.* + +data class ExtractSuperclassInfo( + val originalClass: KtClassOrObject, + val memberInfos: Collection, + val targetParent: PsiElement, + val targetFileName: String, + val newClassName: String, + val docPolicy: DocCommentPolicy<*> +) + +class ExtractSuperclassRefactoring( + private var extractInfo: ExtractSuperclassInfo +) { + companion object { + private fun getElementsToMove( + memberInfos: Collection, + originalClass: KtClassOrObject + ): Map { + val project = originalClass.project + val elementsToMove = LinkedHashMap() + runReadAction { + val superInterfacesToMove = ArrayList() + for (memberInfo in memberInfos) { + val member = memberInfo.member ?: continue + if (memberInfo.isSuperClass) { + superInterfacesToMove += member + } + else { + elementsToMove[member] = memberInfo + } + } + + val superTypeList = originalClass.getSuperTypeList() + if (superTypeList != null) { + for (superTypeListEntry in originalClass.getSuperTypeListEntries()) { + val superType = superTypeListEntry.analyze(BodyResolveMode.PARTIAL)[BindingContext.TYPE, superTypeListEntry.typeReference] + ?: continue + val superClassDescriptor = superType.constructor.declarationDescriptor ?: continue + val superClass = DescriptorToSourceUtilsIde.getAnyDeclaration(project, superClassDescriptor) as? KtClass ?: continue + if (!superClass.isInterface() || superClass in superInterfacesToMove) { + elementsToMove[superTypeListEntry] = null + } + } + } + } + return elementsToMove + } + + fun collectConflicts( + originalClass: KtClassOrObject, + memberInfos: List, + targetParent: PsiElement, + newClassName: String + ): MultiMap { + val conflicts = MultiMap() + + val project = originalClass.project + + if (targetParent is KtElement) { + val targetSibling = originalClass.parentsWithSelf.first { it.parent == targetParent } as KtElement + targetSibling.getResolutionScope() + .findClassifier(Name.identifier(newClassName), NoLookupLocation.FROM_IDE) + ?.let { DescriptorToSourceUtilsIde.getAnyDeclaration(project, it) } + ?.let { conflicts.putValue(it, "Class $newClassName already exists in the target scope") } + } + + val elementsToMove = getElementsToMove(memberInfos, originalClass).keys + + val moveTarget = if (targetParent is PsiDirectory) { + val targetPackage = targetParent.getPackage() ?: return conflicts + KotlinMoveTargetForDeferredFile(FqName(targetPackage.qualifiedName), targetParent) { null } + } + else { + KotlinMoveTargetForExistingElement(targetParent as KtElement) + } + val conflictChecker = MoveConflictChecker(project, elementsToMove, moveTarget, originalClass) + + project.runSynchronouslyWithProgress(RefactoringBundle.message("detecting.possible.conflicts"), true) { + runReadAction { + val usages = ArrayList() + for (element in elementsToMove) { + ReferencesSearch.search(element).mapTo(usages) { MoveRenameUsageInfo(it, element) } + if (element is KtCallableDeclaration) { + element.toLightMethods().flatMapTo(usages) { + MethodReferencesSearch.search(it).map { MoveRenameUsageInfo(it, element) } + } + } + } + conflictChecker.checkAllConflicts(usages, conflicts) + if (targetParent is PsiDirectory) { + ExtractSuperClassUtil.checkSuperAccessible(targetParent, conflicts, originalClass.toLightClass()) + } + } + } + + return conflicts + } + } + + private val project = extractInfo.originalClass.project + private val psiFactory = KtPsiFactory(project) + private val typeParameters = LinkedHashSet() + + private val bindingContext = extractInfo.originalClass.analyze(BodyResolveMode.PARTIAL) + + private fun collectTypeParameters(refTarget: PsiElement?) { + if (refTarget is KtTypeParameter && refTarget.getStrictParentOfType() == extractInfo.originalClass) { + typeParameters += refTarget + refTarget.accept( + object : KtTreeVisitorVoid() { + override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) { + (expression.mainReference.resolve() as? KtTypeParameter)?.let { typeParameters += it } + } + } + ) + } + } + + private fun analyzeContext() { + val visitor = object : KtTreeVisitorVoid() { + override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) { + val refTarget = expression.mainReference.resolve() + collectTypeParameters(refTarget) + } + } + getElementsToMove(extractInfo.memberInfos, extractInfo.originalClass) + .asSequence() + .flatMap { + val (element, info) = it + if (info != null) info.getChildrenToAnalyze().asSequence() else sequenceOf(element) + } + .forEach { it.accept(visitor) } + } + + private fun createClass(superClassEntry: KtSuperTypeListEntry?): KtClass { + val targetParent = extractInfo.targetParent + val newClassName = extractInfo.newClassName + val originalClass = extractInfo.originalClass + + 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 + } + else { + val targetSibling = originalClass.parentsWithSelf.first { it.parent == targetParent } + insertDeclaration(psiFactory.createClass("class $newClassName"), targetSibling) + } + + val shouldBeAbstract = extractInfo.memberInfos.any { it.isToAbstract } + 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 = ">") + newClass.addAfter(psiFactory.createTypeParameterList(typeParameterListText), newClass.nameIdentifier) + } + + val targetPackageFqName = (targetParent as? PsiDirectory)?.getPackage()?.qualifiedName + + val superTypeText = buildString { + if (!targetPackageFqName.isNullOrEmpty()) { + append(targetPackageFqName).append('.') + } + append(newClassName) + if (typeParameters.isNotEmpty()) { + append(typeParameters.sortedBy { it.startOffset }.map { it.name }.joinToString(prefix = "<", postfix = ">")) + } + } + val needSuperCall = superClassEntry is KtSuperTypeCallEntry + || originalClass.hasPrimaryConstructor() + || originalClass.getSecondaryConstructors().isEmpty() + val newSuperTypeCallEntry = if (needSuperCall) { + psiFactory.createSuperTypeCallEntry("$superTypeText()") + } + else { + psiFactory.createSuperTypeEntry(superTypeText) + } + if (superClassEntry != null) { + val qualifiedTypeRefText = bindingContext[BindingContext.TYPE, superClassEntry.typeReference]?.let { + IdeDescriptorRenderers.SOURCE_CODE.renderType(it) + } + val superClassEntryToAdd = if (qualifiedTypeRefText != null) { + superClassEntry.copied().apply { typeReference?.replace(psiFactory.createType(qualifiedTypeRefText)) } + } + else superClassEntry + newClass.addSuperTypeListEntry(superClassEntryToAdd) + ShortenReferences.DEFAULT.process(superClassEntry.replaced(newSuperTypeCallEntry)) + } + else { + ShortenReferences.DEFAULT.process(originalClass.addSuperTypeListEntry(newSuperTypeCallEntry)) + } + + ShortenReferences.DEFAULT.process(newClass) + + return newClass + } + + fun performRefactoring() { + val originalClass = extractInfo.originalClass + + KotlinExtractSuperclassHandler.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 + } + + project.runSynchronouslyWithProgress(RefactoringBundle.message("progress.text"), true) { runReadAction { analyzeContext() } } + + project.executeWriteCommand(KotlinExtractSuperclassHandler.REFACTORING_NAME) { + val newClass = createClass(superClassEntry) + + val subClass = extractInfo.originalClass.toLightClass() + val superClass = newClass.toLightClass() + + PullUpProcessor( + subClass, + superClass ?: return@executeWriteCommand, + extractInfo.memberInfos.mapNotNull { it.toJavaMemberInfo() }.toTypedArray(), + extractInfo.docPolicy + ).moveMembersToBase() + + performDelayedShortening(project) + } + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/KotlinExtractSuperclassHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/KotlinExtractSuperclassHandler.kt new file mode 100644 index 00000000000..7ba723ab223 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/KotlinExtractSuperclassHandler.kt @@ -0,0 +1,112 @@ +/* + * 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.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 { + val REFACTORING_NAME = "Extract Superclass" + + override fun isEnabledOnElements(elements: Array) = 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() ?: return + editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE) + selectElements(klass, project, editor) + } + + override fun invoke(project: Project, elements: Array, dataContext: DataContext?) { + if (dataContext == null) return + val editor = CommonDataKeys.EDITOR.getData(dataContext) + val klass = PsiTreeUtil.findCommonParent(*elements)?.getNonStrictParentOfType() ?: 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? { + 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") + if (klass.isAnnotation()) return "Superclass cannot be extracted from an annotation class" + } + 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?) { + 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() } + ).show() + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ui/KotlinExtractSuperclassDialog.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ui/KotlinExtractSuperclassDialog.kt new file mode 100644 index 00000000000..a9e95e44c46 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ui/KotlinExtractSuperclassDialog.kt @@ -0,0 +1,202 @@ +/* + * 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.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.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 +) { + companion object { + private val DESTINATION_PACKAGE_RECENT_KEY = "KotlinExtractSuperclassDialog.RECENT_KEYS" + } + + val kotlinMemberInfos = extractClassMembers(originalClass) + + val selectedMembers: List + get() = kotlinMemberInfos.filter { it.isChecked } + + private val fileNameField = JTextField() + + private val memberInfoModel = object : AbstractMemberInfoModel() { + 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 getDestinationPackageRecentKey() = DESTINATION_PACKAGE_RECENT_KEY + + override fun getClassNameLabelText() = RefactoringBundle.message("superclass.name")!! + + override fun getPackageNameLabelText() = RefactoringBundle.message("package.for.new.superclass")!! + + override fun getEntityName() = RefactoringBundle.message("ExtractSuperClass.superclass")!! + + override fun getTopLabelText() = RefactoringBundle.message("extract.superclass.from")!! + + override fun getDocCommentPolicySetting() = JavaRefactoringSettings.getInstance().EXTRACT_SUPERCLASS_JAVADOC + + override fun setDocCommentPolicySetting(policy: Int) { + 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(docCommentPolicy) + ) + refactoring(extractInfo) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceTypeAlias/introduceTypeAliasImpl.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceTypeAlias/introduceTypeAliasImpl.kt index 809b725bfb0..e676b2d954c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceTypeAlias/introduceTypeAliasImpl.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceTypeAlias/introduceTypeAliasImpl.kt @@ -27,6 +27,7 @@ import org.jetbrains.kotlin.idea.core.CollectingNameValidator import org.jetbrains.kotlin.idea.core.KotlinNameSuggester import org.jetbrains.kotlin.idea.core.compareDescriptors import org.jetbrains.kotlin.idea.core.quoteIfNeeded +import org.jetbrains.kotlin.idea.refactoring.introduce.insertDeclaration import org.jetbrains.kotlin.idea.util.getResolutionScope import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiRange import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiUnifier @@ -220,22 +221,6 @@ fun IntroduceTypeAliasDescriptor.generateTypeAlias(previewOnly: Boolean = false) } } - fun insertDeclaration(): KtTypeAlias { - val targetParent = originalData.targetSibling.parent - - val anchorCandidates = SmartList() - anchorCandidates.add(targetSibling) - if (targetSibling is KtEnumEntry) { - anchorCandidates.add(targetSibling.siblings().last { it is KtEnumEntry }) - } - - val anchor = anchorCandidates.minBy { it.startOffset }!!.parentsWithSelf.first { it.parent == targetParent } - val targetContainer = anchor.parent!! - return (targetContainer.addBefore(typeAlias, anchor) as KtTypeAlias).apply { - targetContainer.addBefore(psiFactory.createWhiteSpace("\n\n"), anchor) - } - } - return if (previewOnly) { introduceTypeParameters() typeAlias @@ -243,6 +228,6 @@ fun IntroduceTypeAliasDescriptor.generateTypeAlias(previewOnly: Boolean = false) else { replaceUsage() introduceTypeParameters() - insertDeclaration() + insertDeclaration(typeAlias, originalData.targetSibling) } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceUtil.kt index 15f14563647..f568bc647e0 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceUtil.kt @@ -31,6 +31,7 @@ import org.jetbrains.kotlin.idea.refactoring.selectElement import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiRange import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.* +import org.jetbrains.kotlin.utils.SmartList fun showErrorHint(project: Project, editor: Editor, message: String, title: String) { CodeInsightUtils.showErrorHint(project, editor, message, title, null) @@ -217,4 +218,22 @@ fun KtExpression.mustBeParenthesizedInInitializerPosition(): Boolean { return PsiChildRange(left, operationReference).any { (it is PsiWhiteSpace) && it.textContains('\n') } } -fun isObjectOrNonInnerClass(e: PsiElement): Boolean = e is KtObjectDeclaration || (e is KtClass && !e.isInner()) \ No newline at end of file +fun isObjectOrNonInnerClass(e: PsiElement): Boolean = e is KtObjectDeclaration || (e is KtClass && !e.isInner()) + +fun insertDeclaration(declaration: T, targetSibling: PsiElement): T { + val targetParent = targetSibling.parent + + val anchorCandidates = SmartList() + anchorCandidates.add(targetSibling) + if (targetSibling is KtEnumEntry) { + anchorCandidates.add(targetSibling.siblings().last { it is KtEnumEntry }) + } + + val anchor = anchorCandidates.minBy { it.startOffset }!!.parentsWithSelf.first { it.parent == targetParent } + val targetContainer = anchor.parent!! + + @Suppress("UNCHECKED_CAST") + return (targetContainer.addBefore(declaration, anchor) as T).apply { + targetContainer.addBefore(KtPsiFactory(declaration).createWhiteSpace("\n\n"), anchor) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt index 10d182d3bec..f7960a9bdfc 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt @@ -48,6 +48,7 @@ import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.* +import com.intellij.psi.impl.light.LightElement import com.intellij.psi.util.PsiTreeUtil import com.intellij.refactoring.BaseRefactoringProcessor.ConflictsInTestsException import com.intellij.refactoring.changeSignature.ChangeSignatureUtil @@ -68,6 +69,7 @@ import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.idea.KotlinBundle import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.getJavaMemberDescriptor import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor @@ -96,6 +98,7 @@ import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver import org.jetbrains.kotlin.resolve.source.getPsi import java.io.File +import java.lang.AssertionError import java.lang.annotation.Retention import java.util.* import javax.swing.Icon @@ -291,9 +294,11 @@ class SelectionAwareScopeHighlighter(val editor: Editor) { fun highlight(wholeAffected: PsiElement) { dropHighlight() + val affectedRange = wholeAffected.textRange ?: return + val attributes = EditorColorsManager.getInstance().globalScheme.getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES)!! val selectedRange = with(editor.selectionModel) { TextRange(selectionStart, selectionEnd) } - for (r in RangeSplitter.split(wholeAffected.textRange!!, Collections.singletonList(selectedRange))) { + for (r in RangeSplitter.split(affectedRange, Collections.singletonList(selectedRange))) { addHighlighter(r, attributes) } } @@ -340,6 +345,10 @@ fun PsiElement.getLineCount(): Int { fun PsiElement.isMultiLine(): Boolean = getLineCount() > 1 +class SeparateFileWrapper(manager: PsiManager) : LightElement(manager, KotlinLanguage.INSTANCE) { + override fun toString() = "" +} + fun chooseContainerElement( containers: List, editor: Editor, @@ -380,6 +389,7 @@ fun chooseContainerElement( } private fun PsiElement.renderText(): String { + if (this is SeparateFileWrapper) return "Extract to separate file" return StringUtil.shortenTextWithEllipsis(text!!.collapseSpaces(), 53, 0) } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/KotlinMemberInfo.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/KotlinMemberInfo.kt index f64c90c182c..f916031ade6 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/KotlinMemberInfo.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/KotlinMemberInfo.kt @@ -23,6 +23,7 @@ import com.intellij.refactoring.util.classMembers.MemberInfo import org.jetbrains.kotlin.asJava.LightClassUtil import org.jetbrains.kotlin.asJava.getRepresentativeLightMethod import org.jetbrains.kotlin.asJava.toLightClass +import org.jetbrains.kotlin.asJava.unwrapped import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor import org.jetbrains.kotlin.descriptors.MemberDescriptor import org.jetbrains.kotlin.descriptors.Modality @@ -78,4 +79,11 @@ fun KotlinMemberInfo.toJavaMemberInfo(): MemberInfo? { val info = MemberInfo(psiMember ?: return null, isSuperClass, null) info.isToAbstract = isToAbstract return info +} + +fun MemberInfo.toKotlinMemberInfo(): KotlinMemberInfo? { + val declaration = member.unwrapped as? KtNamedDeclaration ?: return null + return KotlinMemberInfo(declaration, declaration is KtClass && overrides != null).apply { + this.isToAbstract = this@toKotlinMemberInfo.isToAbstract + } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/KotlinMemberInfoStorage.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/KotlinMemberInfoStorage.kt index 79cc507e518..1b519a39536 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/KotlinMemberInfoStorage.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/KotlinMemberInfoStorage.kt @@ -30,6 +30,7 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.resolve.OverloadChecker +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.source.getPsi import org.jetbrains.kotlin.types.typeUtil.immediateSupertypes import java.util.* @@ -73,25 +74,40 @@ class KotlinMemberInfoStorage( } override fun extractClassMembers(aClass: PsiNamedElement, temp: ArrayList) { - if (aClass !is KtClassOrObject) return - - val context = aClass.analyze() - aClass.declarations - .filter { it is KtNamedDeclaration - && it !is KtConstructor<*> - && !(it is KtObjectDeclaration && it.isCompanion()) - && myFilter.includeMember(it) } - .mapTo(temp) { KotlinMemberInfo(it as KtNamedDeclaration) } - if (aClass == myClass) { - aClass.getSuperTypeListEntries() - .filterIsInstance() - .map { - val type = context[BindingContext.TYPE, it.typeReference] - val classDescriptor = type?.constructor?.declarationDescriptor as? ClassDescriptor - classDescriptor?.source?.getPsi() as? KtClass - } - .filter { it != null && it.isInterface() } - .mapTo(temp) { KotlinMemberInfo(it!!, true) } + if (aClass is KtClassOrObject) { + temp += extractClassMembers(aClass, aClass == myClass) { myFilter.includeMember(it) } } } +} + +fun extractClassMembers( + aClass: KtClassOrObject, + collectSuperTypeEntries: Boolean = true, + filter: ((KtNamedDeclaration) -> Boolean)? = null +): List { + if (aClass !is KtClassOrObject) return emptyList() + + val result = ArrayList() + + if (collectSuperTypeEntries) { + aClass.getSuperTypeListEntries() + .filterIsInstance() + .mapNotNull { + val typeReference = it.typeReference ?: return@mapNotNull null + val type = typeReference.analyze(BodyResolveMode.PARTIAL)[BindingContext.TYPE, typeReference] + val classDescriptor = type?.constructor?.declarationDescriptor as? ClassDescriptor + classDescriptor?.source?.getPsi() as? KtClass + } + .filter { it.isInterface() } + .mapTo(result) { KotlinMemberInfo(it, true) } + } + + aClass.declarations + .filter { it is KtNamedDeclaration + && it !is KtConstructor<*> + && !(it is KtObjectDeclaration && it.isCompanion()) + && (filter == null || filter(it)) } + .mapTo(result) { KotlinMemberInfo(it as KtNamedDeclaration) } + + return result } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/memberInfoUtils.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/memberInfoUtils.kt index d21d3fc58d6..bd4020ea53a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/memberInfoUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/memberInfo/memberInfoUtils.kt @@ -17,13 +17,14 @@ package org.jetbrains.kotlin.idea.refactoring.memberInfo import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement import com.intellij.psi.PsiNamedElement import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.idea.caches.resolve.getJavaClassDescriptor import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade import org.jetbrains.kotlin.idea.resolve.ResolutionFacade -import org.jetbrains.kotlin.psi.KtClass -import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.allChildren import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext fun PsiNamedElement.getClassDescriptorIfAny(resolutionFacade: ResolutionFacade? = null): ClassDescriptor? { @@ -42,4 +43,20 @@ fun PsiNamedElement.qualifiedClassNameForRendering(): String { else -> throw AssertionError("Not a class: ${getElementTextWithContext()}") } return fqName ?: name ?: "[Anonymous]" +} + +fun KotlinMemberInfo.getChildrenToAnalyze(): List { + val member = member + val childrenToCheck = member.allChildren.toMutableList() + if (isToAbstract && member is KtCallableDeclaration) { + when (member) { + is KtNamedFunction -> childrenToCheck.remove(member.bodyExpression as PsiElement?) + is KtProperty -> { + childrenToCheck.remove(member.initializer as PsiElement?) + childrenToCheck.remove(member.delegateExpression as PsiElement?) + childrenToCheck.removeAll(member.accessors) + } + } + } + return childrenToCheck } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt index 655f14ffcf7..abb778274f3 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt @@ -168,7 +168,7 @@ class MoveKotlinDeclarationsProcessor( } val usages = ArrayList() - val conflictChecker = MoveConflictChecker(project, elementsToMove, descriptor.moveTarget) + val conflictChecker = MoveConflictChecker(project, elementsToMove, descriptor.moveTarget, elementsToMove.first()) for ((sourceFile, kotlinToLightElements) in kotlinToLightElementsBySourceFile) { kotlinToLightElements.keys.forEach { if (descriptor.updateInternalReferences) { diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/moveConflictUtils.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/moveConflictUtils.kt index 01ea7929539..60b21e8182b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/moveConflictUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/moveConflictUtils.kt @@ -29,7 +29,6 @@ import com.intellij.usageView.UsageInfo import com.intellij.util.containers.MultiMap import org.jetbrains.kotlin.asJava.namedUnwrappedElement import org.jetbrains.kotlin.asJava.toLightMethods -import org.jetbrains.kotlin.caches.resolve.KotlinCacheService import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.impl.MutablePackageFragmentDescriptor import org.jetbrains.kotlin.idea.caches.resolve.* @@ -47,10 +46,11 @@ import java.util.* class MoveConflictChecker( private val project: Project, - private val elementsToMove: List, - private val moveTarget: KotlinMoveTarget + private val elementsToMove: Collection, + private val moveTarget: KotlinMoveTarget, + contextElement: KtElement ) { - private val resolutionFacade by lazy { KotlinCacheService.getInstance(project).getResolutionFacade(elementsToMove) } + private val resolutionFacade = contextElement.getResolutionFacade() private val fakeFile = KtPsiFactory(project).createFile("") @@ -152,6 +152,7 @@ class MoveConflictChecker( val target = ref.resolve() ?: return@forEach if (target.isInsideOf(elementsToMove)) return@forEach if (target in resolveScope) return@forEach + if (target is KtTypeParameter) return@forEach val superMethods = SmartSet.create() target.toLightMethods().forEach { superMethods += it.findDeepestSuperMethods() } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/pullUp/pullUpConflictsUtils.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/pullUp/pullUpConflictsUtils.kt index 7fb8bf4d2bb..0ad76a6603d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/pullUp/pullUpConflictsUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/pullUp/pullUpConflictsUtils.kt @@ -25,13 +25,13 @@ import org.jetbrains.kotlin.asJava.unwrapped import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.idea.refactoring.checkConflictsInteractively import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo +import org.jetbrains.kotlin.idea.refactoring.memberInfo.getChildrenToAnalyze import org.jetbrains.kotlin.idea.references.KtReference import org.jetbrains.kotlin.idea.search.declarationsSearch.HierarchySearchRequest import org.jetbrains.kotlin.idea.search.declarationsSearch.searchInheritors import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.psi.psiUtil.allChildren import org.jetbrains.kotlin.renderer.DescriptorRenderer import org.jetbrains.kotlin.renderer.ParameterNameRenderingPolicy import org.jetbrains.kotlin.resolve.source.getPsi @@ -161,17 +161,8 @@ private fun KotlinPullUpData.checkVisibility( } val member = memberInfo.member - val childrenToCheck = member.allChildren.toMutableList() + val childrenToCheck = memberInfo.getChildrenToAnalyze() if (memberInfo.isToAbstract && member is KtCallableDeclaration) { - when (member) { - is KtNamedFunction -> childrenToCheck.remove(member.bodyExpression as PsiElement?) - is KtProperty -> { - childrenToCheck.remove(member.initializer as PsiElement?) - childrenToCheck.remove(member.delegateExpression as PsiElement?) - childrenToCheck.removeAll(member.accessors) - } - } - if (member.typeReference == null) { (memberDescriptor as CallableDescriptor).returnType?.let { returnType -> val typeInTargetClass = sourceToTargetClassSubstitutor.substitute(returnType, Variance.INVARIANT) diff --git a/idea/testData/refactoring/extractSuperclass/addSuperclassNoSecondaryConstructors.kt b/idea/testData/refactoring/extractSuperclass/addSuperclassNoSecondaryConstructors.kt new file mode 100644 index 00000000000..9463f2a3b7e --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addSuperclassNoSecondaryConstructors.kt @@ -0,0 +1,10 @@ +// NAME: X +interface T {} + +// SIBLING: +class A : T { + // INFO: {checked: "true"} + fun foo() { + + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/addSuperclassNoSecondaryConstructors.kt.after b/idea/testData/refactoring/extractSuperclass/addSuperclassNoSecondaryConstructors.kt.after new file mode 100644 index 00000000000..3e36e7b367d --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addSuperclassNoSecondaryConstructors.kt.after @@ -0,0 +1,13 @@ +// NAME: X +interface T {} + +open class X { + // INFO: {checked: "true"} + fun foo() { + + } +} + +// SIBLING: +class A : T, X() { +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/addSuperclassOnlySecondaryConstructors.kt b/idea/testData/refactoring/extractSuperclass/addSuperclassOnlySecondaryConstructors.kt new file mode 100644 index 00000000000..eb187d6137e --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addSuperclassOnlySecondaryConstructors.kt @@ -0,0 +1,12 @@ +// NAME: X +interface T {} + +// SIBLING: +class A : T { + constructor() + + // INFO: {checked: "true"} + fun foo() { + + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/addSuperclassOnlySecondaryConstructors.kt.after b/idea/testData/refactoring/extractSuperclass/addSuperclassOnlySecondaryConstructors.kt.after new file mode 100644 index 00000000000..a2a2e99d762 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addSuperclassOnlySecondaryConstructors.kt.after @@ -0,0 +1,15 @@ +// NAME: X +interface T {} + +open class X { + // INFO: {checked: "true"} + fun foo() { + + } +} + +// SIBLING: +class A : T, X { + constructor() + +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/addSuperclassPrimaryConstructor.kt b/idea/testData/refactoring/extractSuperclass/addSuperclassPrimaryConstructor.kt new file mode 100644 index 00000000000..23d1f5a7467 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addSuperclassPrimaryConstructor.kt @@ -0,0 +1,10 @@ +// NAME: X +interface T {} + +// SIBLING: +class A(n: Int) : T { + // INFO: {checked: "true"} + fun foo() { + + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/addSuperclassPrimaryConstructor.kt.after b/idea/testData/refactoring/extractSuperclass/addSuperclassPrimaryConstructor.kt.after new file mode 100644 index 00000000000..c03966d7bab --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addSuperclassPrimaryConstructor.kt.after @@ -0,0 +1,13 @@ +// NAME: X +interface T {} + +open class X { + // INFO: {checked: "true"} + fun foo() { + + } +} + +// SIBLING: +class A(n: Int) : T, X() { +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/addTypeParameters.kt b/idea/testData/refactoring/extractSuperclass/addTypeParameters.kt new file mode 100644 index 00000000000..91d56c698e3 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addTypeParameters.kt @@ -0,0 +1,14 @@ +// NAME: B + +// INFO: {checked: "true"} +interface I + +open class J + +// SIBLING: +class A, V, W, X> : J(), I { + // INFO: {checked: "true"} + fun foo(u: U) { + + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/addTypeParameters.kt.after b/idea/testData/refactoring/extractSuperclass/addTypeParameters.kt.after new file mode 100644 index 00000000000..76a478ec40f --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addTypeParameters.kt.after @@ -0,0 +1,17 @@ +// NAME: B + +// INFO: {checked: "true"} +interface I + +open class J + +open class B, W, X> : J(), I { + // INFO: {checked: "true"} + fun foo(u: U) { + + } +} + +// SIBLING: +class A, V, W, X> : B() { +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/addTypeParametersWithAbstract.kt b/idea/testData/refactoring/extractSuperclass/addTypeParametersWithAbstract.kt new file mode 100644 index 00000000000..34758d2e4c7 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addTypeParametersWithAbstract.kt @@ -0,0 +1,14 @@ +// NAME: B + +// INFO: {checked: "true"} +interface I + +open class J + +// SIBLING: +class A, V, W, X> : J(), I { + // INFO: {checked: "true", toAbstract: "true"} + fun foo() { + val u: U + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/addTypeParametersWithAbstract.kt.after b/idea/testData/refactoring/extractSuperclass/addTypeParametersWithAbstract.kt.after new file mode 100644 index 00000000000..2c06ce68c12 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/addTypeParametersWithAbstract.kt.after @@ -0,0 +1,19 @@ +// NAME: B + +// INFO: {checked: "true"} +interface I + +open class J + +abstract class B : J(), I { + // INFO: {checked: "true", toAbstract: "true"} + abstract fun foo() +} + +// SIBLING: +class A, V, W, X> : B() { + // INFO: {checked: "true", toAbstract: "true"} + override fun foo() { + val u: U + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/annotation.kt b/idea/testData/refactoring/extractSuperclass/annotation.kt new file mode 100644 index 00000000000..0e3439dc8b1 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/annotation.kt @@ -0,0 +1,3 @@ +// NAME: B +// SIBLING: +annotation class A \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/annotation.kt.conflicts b/idea/testData/refactoring/extractSuperclass/annotation.kt.conflicts new file mode 100644 index 00000000000..0c322f2eae4 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/annotation.kt.conflicts @@ -0,0 +1 @@ +Superclass cannot be extracted from an annotation class \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/enum.kt b/idea/testData/refactoring/extractSuperclass/enum.kt new file mode 100644 index 00000000000..af7971e2d72 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/enum.kt @@ -0,0 +1,9 @@ +// NAME: B +// SIBLING: +enum class A { + X, Y, Z; + + fun foo() { + + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/enum.kt.conflicts b/idea/testData/refactoring/extractSuperclass/enum.kt.conflicts new file mode 100644 index 00000000000..325b9056bdb --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/enum.kt.conflicts @@ -0,0 +1 @@ +Superclass cannot be extracted from enum \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/interface.kt b/idea/testData/refactoring/extractSuperclass/interface.kt new file mode 100644 index 00000000000..a9302efb4f7 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/interface.kt @@ -0,0 +1,7 @@ +// NAME: B +// SIBLING: +interface A { + fun foo() { + + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/interface.kt.conflicts b/idea/testData/refactoring/extractSuperclass/interface.kt.conflicts new file mode 100644 index 00000000000..1e032c825e6 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/interface.kt.conflicts @@ -0,0 +1 @@ +Superclass cannot be extracted from interface \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/privateClass.kt b/idea/testData/refactoring/extractSuperclass/privateClass.kt new file mode 100644 index 00000000000..026536204e7 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/privateClass.kt @@ -0,0 +1,7 @@ +// NAME: X +// SIBLING: +class A { + private open class B + + private class C : B() +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/privateClass.kt.conflicts b/idea/testData/refactoring/extractSuperclass/privateClass.kt.conflicts new file mode 100644 index 00000000000..f8501e6cea9 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/privateClass.kt.conflicts @@ -0,0 +1 @@ +'B()' uses class B which will be inaccessible after move \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/privateMember.kt b/idea/testData/refactoring/extractSuperclass/privateMember.kt new file mode 100644 index 00000000000..50523567a86 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/privateMember.kt @@ -0,0 +1,16 @@ +// NAME: X +class A { + open class B + + // SIBLING: + class C : B() { + // INFO: {checked: "true"} + private fun foo() { + + } + + fun test() { + foo() + } + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/privateMember.kt.after b/idea/testData/refactoring/extractSuperclass/privateMember.kt.after new file mode 100644 index 00000000000..47685fa1d58 --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/privateMember.kt.after @@ -0,0 +1,19 @@ +// NAME: X +class A { + open class B + + open class X : B() { + // INFO: {checked: "true"} + protected fun foo() { + + } + } + + // SIBLING: + class C : X() { + + fun test() { + foo() + } + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/replaceSuperclass.kt b/idea/testData/refactoring/extractSuperclass/replaceSuperclass.kt new file mode 100644 index 00000000000..d58e6c97acb --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/replaceSuperclass.kt @@ -0,0 +1,12 @@ +// NAME: X +interface T {} + +open class A + +// SIBLING: +class B : A(), T { + // INFO: {checked: "true"} + fun foo() { + + } +} \ No newline at end of file diff --git a/idea/testData/refactoring/extractSuperclass/replaceSuperclass.kt.after b/idea/testData/refactoring/extractSuperclass/replaceSuperclass.kt.after new file mode 100644 index 00000000000..27bb82c726d --- /dev/null +++ b/idea/testData/refactoring/extractSuperclass/replaceSuperclass.kt.after @@ -0,0 +1,15 @@ +// NAME: X +interface T {} + +open class A + +open class X : A() { + // INFO: {checked: "true"} + fun foo() { + + } +} + +// SIBLING: +class B : X(), T { +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/AbstractMemberPullPushTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/AbstractMemberPullPushTest.kt index 7fd37ca62ba..098fb04c3d2 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/AbstractMemberPullPushTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/AbstractMemberPullPushTest.kt @@ -36,13 +36,6 @@ import org.jetbrains.kotlin.test.util.findElementsByCommentPrefix import java.io.File abstract class AbstractMemberPullPushTest : KotlinLightCodeInsightFixtureTestCase() { - private data class ElementInfo(val checked: Boolean, val toAbstract: Boolean) - - companion object { - private var PsiElement.elementInfo: ElementInfo - by NotNullableUserDataProperty(Key.create("ELEMENT_INFO"), ElementInfo(false, false)) - } - override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE val fixture: JavaCodeInsightTestFixture get() = myFixture @@ -70,11 +63,7 @@ abstract class AbstractMemberPullPushTest : KotlinLightCodeInsightFixtureTestCas } try { - for ((element, info) in file.findElementsByCommentPrefix("// INFO: ")) { - val parsedInfo = JsonParser().parse(info).asJsonObject - element.elementInfo = ElementInfo(parsedInfo["checked"]?.asBoolean ?: false, - parsedInfo["toAbstract"]?.asBoolean ?: false) - } + markMembersInfo(file) action(file) @@ -99,12 +88,26 @@ abstract class AbstractMemberPullPushTest : KotlinLightCodeInsightFixtureTestCas } } - protected fun > chooseMembers(members: List): List { - members.forEach { - val info = it.member.elementInfo - it.isChecked = info.checked - it.isToAbstract = info.toAbstract - } - return members.filter { it.isChecked } + +} + +internal fun markMembersInfo(file: PsiFile) { + for ((element, info) in file.findElementsByCommentPrefix("// INFO: ")) { + val parsedInfo = JsonParser().parse(info).asJsonObject + element.elementInfo = ElementInfo(parsedInfo["checked"]?.asBoolean ?: false, + parsedInfo["toAbstract"]?.asBoolean ?: false) } +} + +internal data class ElementInfo(val checked: Boolean, val toAbstract: Boolean) + +internal var PsiElement.elementInfo: ElementInfo by NotNullableUserDataProperty(Key.create("ELEMENT_INFO"), ElementInfo(false, false)) + +internal fun > chooseMembers(members: List): List { + members.forEach { + val info = it.member.elementInfo + it.isChecked = info.checked + it.isToAbstract = info.toAbstract + } + return members.filter { it.isChecked } } \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractExtractionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractExtractionTest.kt index 7cceecb7f7a..60ee6111245 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractExtractionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractExtractionTest.kt @@ -22,6 +22,7 @@ import com.intellij.ide.DataManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.openapi.util.io.FileUtil +import com.intellij.psi.PsiComment import com.intellij.psi.PsiFile import com.intellij.psi.PsiJavaFile import com.intellij.psi.codeStyle.JavaCodeStyleManager @@ -33,10 +34,16 @@ import com.intellij.refactoring.introduceParameter.AbstractJavaInplaceIntroducer import com.intellij.refactoring.introduceParameter.IntroduceParameterProcessor import com.intellij.refactoring.introduceParameter.Util import com.intellij.refactoring.util.CommonRefactoringUtil +import com.intellij.refactoring.util.DocCommentPolicy import com.intellij.refactoring.util.occurrences.ExpressionOccurrenceManager import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase +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.extractFunction.EXTRACT_FUNCTION import org.jetbrains.kotlin.idea.refactoring.introduce.extractFunction.ExtractKotlinFunctionHandler import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.* @@ -46,22 +53,23 @@ import org.jetbrains.kotlin.idea.refactoring.introduce.introduceProperty.KotlinI import org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeAlias.KotlinIntroduceTypeAliasHandler import org.jetbrains.kotlin.idea.refactoring.introduce.introduceTypeParameter.KotlinIntroduceTypeParameterHandler import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.KotlinIntroduceVariableHandler +import org.jetbrains.kotlin.idea.refactoring.markMembersInfo +import org.jetbrains.kotlin.idea.refactoring.memberInfo.extractClassMembers import org.jetbrains.kotlin.idea.refactoring.selectElement import org.jetbrains.kotlin.idea.test.ConfigLibraryUtil import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase import org.jetbrains.kotlin.idea.test.PluginTestCaseBase import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers import org.jetbrains.kotlin.lexer.KtModifierKeywordToken -import org.jetbrains.kotlin.psi.KtExpression -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtNamedDeclaration -import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType import org.jetbrains.kotlin.renderer.DescriptorRenderer import org.jetbrains.kotlin.test.InTextDirectivesUtils import org.jetbrains.kotlin.test.KotlinTestUtils import org.jetbrains.kotlin.test.util.findElementByCommentPrefix import org.jetbrains.kotlin.utils.emptyOrSingletonList import java.io.File +import java.lang.AssertionError import java.util.* import kotlin.test.assertEquals @@ -346,6 +354,33 @@ abstract class AbstractExtractionTest() : KotlinLightCodeInsightFixtureTestCase( } } + protected fun doExtractSuperclassTest(path: String) { + doTest(path) { file -> + file as KtFile + + markMembersInfo(file) + + val targetParent = file.findElementByCommentPrefix("// SIBLING:")?.parent ?: file.parent!! + val fileText = file.text + val className = InTextDirectivesUtils.findStringWithPrefixes(fileText, "// NAME:")!! + val editor = fixture.editor + val originalClass = file.findElementAt(editor.caretModel.offset)?.getStrictParentOfType()!! + val memberInfos = chooseMembers(extractClassMembers(originalClass)) + val conflicts = ExtractSuperclassRefactoring.collectConflicts(originalClass, memberInfos, targetParent, className) + project.checkConflictsInteractively(conflicts) { + val extractInfo = ExtractSuperclassInfo( + originalClass, + memberInfos, + targetParent, + "$className.${KotlinFileType.EXTENSION}", + className, + DocCommentPolicy(DocCommentPolicy.ASIS) + ) + ExtractSuperclassRefactoring(extractInfo).performRefactoring() + } + } + } + protected fun doTest(path: String, checkAdditionalAfterdata: Boolean = false, action: (PsiFile) -> Unit) { val mainFile = File(path) val afterFile = File("$path.after") diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java index 0c6ec3007a0..bd2980a5b16 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java @@ -4183,4 +4183,79 @@ public class ExtractionTestGenerated extends AbstractExtractionTest { doIntroduceTypeAliasTest(fileName); } } + + @TestMetadata("idea/testData/refactoring/extractSuperclass") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ExtractSuperclass extends AbstractExtractionTest { + @TestMetadata("addSuperclassNoSecondaryConstructors.kt") + public void testAddSuperclassNoSecondaryConstructors() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/addSuperclassNoSecondaryConstructors.kt"); + doExtractSuperclassTest(fileName); + } + + @TestMetadata("addSuperclassOnlySecondaryConstructors.kt") + public void testAddSuperclassOnlySecondaryConstructors() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/addSuperclassOnlySecondaryConstructors.kt"); + doExtractSuperclassTest(fileName); + } + + @TestMetadata("addSuperclassPrimaryConstructor.kt") + public void testAddSuperclassPrimaryConstructor() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/addSuperclassPrimaryConstructor.kt"); + doExtractSuperclassTest(fileName); + } + + @TestMetadata("addTypeParameters.kt") + public void testAddTypeParameters() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/addTypeParameters.kt"); + doExtractSuperclassTest(fileName); + } + + @TestMetadata("addTypeParametersWithAbstract.kt") + public void testAddTypeParametersWithAbstract() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/addTypeParametersWithAbstract.kt"); + doExtractSuperclassTest(fileName); + } + + public void testAllFilesPresentInExtractSuperclass() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/extractSuperclass"), Pattern.compile("^(.+)\\.(kt|kts)$"), true); + } + + @TestMetadata("annotation.kt") + public void testAnnotation() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/annotation.kt"); + doExtractSuperclassTest(fileName); + } + + @TestMetadata("enum.kt") + public void testEnum() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/enum.kt"); + doExtractSuperclassTest(fileName); + } + + @TestMetadata("interface.kt") + public void testInterface() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/interface.kt"); + doExtractSuperclassTest(fileName); + } + + @TestMetadata("privateClass.kt") + public void testPrivateClass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/privateClass.kt"); + doExtractSuperclassTest(fileName); + } + + @TestMetadata("privateMember.kt") + public void testPrivateMember() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/privateMember.kt"); + doExtractSuperclassTest(fileName); + } + + @TestMetadata("replaceSuperclass.kt") + public void testReplaceSuperclass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/extractSuperclass/replaceSuperclass.kt"); + doExtractSuperclassTest(fileName); + } + } } diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/pullUp/AbstractPullUpTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/pullUp/AbstractPullUpTest.kt index 4b29d13287d..69d68746352 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/pullUp/AbstractPullUpTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/pullUp/AbstractPullUpTest.kt @@ -27,6 +27,7 @@ import com.intellij.refactoring.util.classMembers.MemberInfoStorage import com.intellij.util.ui.UIUtil import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.refactoring.AbstractMemberPullPushTest +import org.jetbrains.kotlin.idea.refactoring.chooseMembers import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo import org.jetbrains.kotlin.idea.refactoring.memberInfo.qualifiedClassNameForRendering import org.jetbrains.kotlin.test.InTextDirectivesUtils diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/pushDown/AbstractPushDownTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/pushDown/AbstractPushDownTest.kt index 9419d11864d..90be328a12a 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/pushDown/AbstractPushDownTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/pushDown/AbstractPushDownTest.kt @@ -17,6 +17,7 @@ package org.jetbrains.kotlin.idea.refactoring.pushDown import org.jetbrains.kotlin.idea.refactoring.AbstractMemberPullPushTest +import org.jetbrains.kotlin.idea.refactoring.chooseMembers import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo abstract class AbstractPushDownTest : AbstractMemberPullPushTest() {