Extract Class: Implement 'Extract Superclass' refactoring

#KT-11017 In Progress
This commit is contained in:
Alexey Sedunov
2016-09-05 15:56:16 +03:00
parent 8f5725345f
commit af2de09840
44 changed files with 1145 additions and 125 deletions
+1
View File
@@ -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
@@ -798,6 +798,7 @@ fun main(args: Array<String>) {
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<AbstractPullUpTest>() {
@@ -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<Char>): Pair<String, PsiDirectory> {
var className = name.removeSuffix(".kt")
var targetDir = dir
private fun findOrCreateTarget(dir: PsiDirectory, name: String, directorySeparators: Array<Char>): Pair<String, PsiDirectory> {
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
}
}
@@ -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
@@ -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<PsiElement> {
@@ -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<KotlinMemberInfo>,
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<KotlinMemberInfo>,
originalClass: KtClassOrObject
): Map<KtElement, KotlinMemberInfo?> {
val project = originalClass.project
val elementsToMove = LinkedHashMap<KtElement, KotlinMemberInfo?>()
runReadAction {
val superInterfacesToMove = ArrayList<KtElement>()
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<KotlinMemberInfo>,
targetParent: PsiElement,
newClassName: String
): MultiMap<PsiElement, String> {
val conflicts = MultiMap<PsiElement, String>()
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<UsageInfo>()
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<KtTypeParameter>()
private val bindingContext = extractInfo.originalClass.analyze(BodyResolveMode.PARTIAL)
private fun collectTypeParameters(refTarget: PsiElement?) {
if (refTarget is KtTypeParameter && refTarget.getStrictParentOfType<KtTypeParameterListOwner>() == 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)
}
}
}
@@ -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<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? {
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()
}
}
@@ -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<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 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<PsiComment>(docCommentPolicy)
)
refactoring(extractInfo)
}
}
@@ -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<PsiElement>()
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)
}
}
@@ -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())
fun isObjectOrNonInnerClass(e: PsiElement): Boolean = e is KtObjectDeclaration || (e is KtClass && !e.isInner())
fun <T : KtDeclaration> insertDeclaration(declaration: T, targetSibling: PsiElement): T {
val targetParent = targetSibling.parent
val anchorCandidates = SmartList<PsiElement>()
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)
}
}
@@ -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 <T> chooseContainerElement(
containers: List<T>,
editor: Editor,
@@ -380,6 +389,7 @@ fun <T> chooseContainerElement(
}
private fun PsiElement.renderText(): String {
if (this is SeparateFileWrapper) return "Extract to separate file"
return StringUtil.shortenTextWithEllipsis(text!!.collapseSpaces(), 53, 0)
}
@@ -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
}
}
@@ -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<KotlinMemberInfo>) {
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<KtSuperTypeEntry>()
.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<KotlinMemberInfo> {
if (aClass !is KtClassOrObject) return emptyList()
val result = ArrayList<KotlinMemberInfo>()
if (collectSuperTypeEntries) {
aClass.getSuperTypeListEntries()
.filterIsInstance<KtSuperTypeEntry>()
.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
}
@@ -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<PsiElement> {
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
}
@@ -168,7 +168,7 @@ class MoveKotlinDeclarationsProcessor(
}
val usages = ArrayList<UsageInfo>()
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) {
@@ -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<KtNamedDeclaration>,
private val moveTarget: KotlinMoveTarget
private val elementsToMove: Collection<KtElement>,
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<PsiMethod>()
target.toLightMethods().forEach { superMethods += it.findDeepestSuperMethods() }
@@ -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)
@@ -0,0 +1,10 @@
// NAME: X
interface T {}
// SIBLING:
class <caret>A : T {
// INFO: {checked: "true"}
fun foo() {
}
}
@@ -0,0 +1,13 @@
// NAME: X
interface T {}
open class X {
// INFO: {checked: "true"}
fun foo() {
}
}
// SIBLING:
class A : T, X() {
}
@@ -0,0 +1,12 @@
// NAME: X
interface T {}
// SIBLING:
class <caret>A : T {
constructor()
// INFO: {checked: "true"}
fun foo() {
}
}
@@ -0,0 +1,15 @@
// NAME: X
interface T {}
open class X {
// INFO: {checked: "true"}
fun foo() {
}
}
// SIBLING:
class A : T, X {
constructor()
}
@@ -0,0 +1,10 @@
// NAME: X
interface T {}
// SIBLING:
class <caret>A(n: Int) : T {
// INFO: {checked: "true"}
fun foo() {
}
}
@@ -0,0 +1,13 @@
// NAME: X
interface T {}
open class X {
// INFO: {checked: "true"}
fun foo() {
}
}
// SIBLING:
class A(n: Int) : T, X() {
}
@@ -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>
open class B<T, U : List<T>, W, X> : J<X>(), I<W> {
// INFO: {checked: "true"}
fun foo(u: U) {
}
}
// SIBLING:
class A<T, U : List<T>, V, W, X> : B<T, U, W, X>() {
}
@@ -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>
abstract class B<W, X> : J<X>(), I<W> {
// INFO: {checked: "true", toAbstract: "true"}
abstract fun foo()
}
// SIBLING:
class A<T, U : List<T>, V, W, X> : B<W, X>() {
// 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 @@
Superclass cannot be extracted from an annotation class
+9
View File
@@ -0,0 +1,9 @@
// NAME: B
// SIBLING:
enum class <caret>A {
X, Y, Z;
fun foo() {
}
}
@@ -0,0 +1 @@
Superclass cannot be extracted from enum
@@ -0,0 +1,7 @@
// NAME: B
// SIBLING:
interface <caret>A {
fun foo() {
}
}
@@ -0,0 +1 @@
Superclass cannot be extracted from interface
@@ -0,0 +1,7 @@
// NAME: X
// SIBLING:
class A {
private open class B
private class <caret>C : B()
}
@@ -0,0 +1 @@
'B()' uses class B which will be inaccessible after move
@@ -0,0 +1,16 @@
// NAME: X
class A {
open class B
// SIBLING:
class <caret>C : B() {
// INFO: {checked: "true"}
private fun foo() {
}
fun test() {
foo()
}
}
}
@@ -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()
}
}
}
@@ -0,0 +1,12 @@
// NAME: X
interface T {}
open class A
// SIBLING:
class <caret>B : A(), T {
// INFO: {checked: "true"}
fun foo() {
}
}
@@ -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 {
}
@@ -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 <T : MemberInfoBase<*>> chooseMembers(members: List<T>): List<T> {
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 <T : MemberInfoBase<*>> chooseMembers(members: List<T>): List<T> {
members.forEach {
val info = it.member.elementInfo
it.isChecked = info.checked
it.isToAbstract = info.toAbstract
}
return members.filter { it.isChecked }
}
@@ -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<KtClassOrObject>()!!
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<PsiComment>(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")
@@ -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);
}
}
}
@@ -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
@@ -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() {