Add inspection: Refactor sealed sub-class to object #KT-20305 Fixed
This commit is contained in:
committed by
Mikhail Glukhikh
parent
7cf6c70fbf
commit
df7968678a
@@ -2765,6 +2765,15 @@
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.CanSealedSubClassBeObjectInspection"
|
||||
displayName="Refactor Sealed Sub-class to Object"
|
||||
groupPath="Kotlin"
|
||||
groupName="Style issues"
|
||||
enabledByDefault="true"
|
||||
level="INFO"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
||||
|
||||
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2010-2017 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.inspections
|
||||
|
||||
import com.intellij.codeInspection.LocalInspectionToolSession
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import org.jetbrains.kotlin.asJava.classes.KtLightClassImpl
|
||||
import org.jetbrains.kotlin.idea.core.getModalityFromDescriptor
|
||||
import org.jetbrains.kotlin.idea.quickfix.sealedSubClassToObject.ConvertSealedSubClassToObjectFix
|
||||
import org.jetbrains.kotlin.idea.search.declarationsSearch.HierarchySearchRequest
|
||||
import org.jetbrains.kotlin.idea.search.declarationsSearch.searchInheritors
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.KtClass
|
||||
import org.jetbrains.kotlin.psi.KtVisitorVoid
|
||||
|
||||
class CanSealedSubClassBeObjectInspection : AbstractKotlinInspection() {
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
|
||||
return object : KtVisitorVoid() {
|
||||
override fun visitClass(klass: KtClass) {
|
||||
if (!klass.hasModifier(KtTokens.SEALED_KEYWORD)) return
|
||||
if (klass.getModalityFromDescriptor() != KtTokens.SEALED_KEYWORD) return
|
||||
|
||||
klass.getSubclasses()
|
||||
.withEmptyConstructors()
|
||||
.thatAreFinal()
|
||||
.thatHasNoInnerClasses()
|
||||
.thatHasNoCompanionObjects()
|
||||
.forEach { reportPossibleObject(it) }
|
||||
}
|
||||
|
||||
private fun reportPossibleObject(klass: KtClass) {
|
||||
val keyword = klass.getClassOrInterfaceKeyword() ?: return
|
||||
holder.registerProblem(
|
||||
keyword,
|
||||
"Sealed Sub-class should be changed To Object",
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
ConvertSealedSubClassToObjectFix()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun KtClass.getSubclasses(): List<KtLightClassImpl> {
|
||||
return HierarchySearchRequest(this, this.useScope, false)
|
||||
.searchInheritors().filterIsInstance<KtLightClassImpl>()
|
||||
}
|
||||
|
||||
private fun List<KtLightClassImpl>.withEmptyConstructors(): List<KtClass> {
|
||||
return map { it.kotlinOrigin }.filterIsInstance<KtClass>()
|
||||
.filter { it.primaryConstructorParameters.isEmpty() }
|
||||
.filter { klass -> klass.secondaryConstructors.all { cons -> cons.valueParameters.isEmpty() } }
|
||||
}
|
||||
|
||||
private fun List<KtClass>.thatHasNoCompanionObjects(): List<KtClass> {
|
||||
return filter { klass -> klass.companionObjects.isEmpty() }
|
||||
}
|
||||
|
||||
private fun List<KtClass>.thatAreFinal(): List<KtClass> {
|
||||
return filter { klass -> klass.getModalityFromDescriptor() == KtTokens.FINAL_KEYWORD }
|
||||
}
|
||||
|
||||
private fun List<KtClass>.thatHasNoInnerClasses(): List<KtClass> {
|
||||
return filter { klass -> klass.hasNoInnerClass() }
|
||||
}
|
||||
|
||||
private fun KtClass.hasNoInnerClass(): Boolean {
|
||||
val internalClasses = getBody()
|
||||
?.declarations
|
||||
?.filterIsInstance<KtClass>() ?: return true
|
||||
|
||||
return internalClasses.none { klass -> klass.isInner() }
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.quickfix.sealedSubClassToObject
|
||||
|
||||
import com.intellij.codeInspection.LocalQuickFix
|
||||
import com.intellij.codeInspection.ProblemDescriptor
|
||||
import com.intellij.lang.Language
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.JavaPsiFacade
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.impl.source.tree.JavaElementType
|
||||
import com.intellij.psi.search.searches.ReferencesSearch
|
||||
import org.jetbrains.kotlin.KtNodeTypes
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.KtClass
|
||||
import org.jetbrains.kotlin.psi.KtPsiFactory
|
||||
import org.jetbrains.kotlin.psi.buildExpression
|
||||
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
|
||||
|
||||
class ConvertSealedSubClassToObjectFix : LocalQuickFix {
|
||||
|
||||
override fun getFamilyName() = "Convert Sealed Sub-class to Object"
|
||||
|
||||
companion object {
|
||||
val JAVA_LANG = Language.findLanguageByID("JAVA")
|
||||
val KOTLIN_LANG = Language.findLanguageByID("kotlin")
|
||||
}
|
||||
|
||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||
val klass = descriptor.psiElement.getParentOfType<KtClass>(false) ?: return
|
||||
|
||||
changeInstances(klass)
|
||||
changeDeclaration(klass)
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes declaration of class to object.
|
||||
*/
|
||||
private fun changeDeclaration(element: KtClass) {
|
||||
val factory = KtPsiFactory(element)
|
||||
|
||||
element.changeToObject(factory)
|
||||
element.transformToObject(factory)
|
||||
}
|
||||
|
||||
private fun KtClass.changeToObject(factory: KtPsiFactory) {
|
||||
getClassOrInterfaceKeyword()?.replace(factory.createExpression(KtTokens.OBJECT_KEYWORD.value))
|
||||
secondaryConstructors.forEach { delete() }
|
||||
primaryConstructor?.delete()
|
||||
}
|
||||
|
||||
private fun KtClass.transformToObject(factory: KtPsiFactory) {
|
||||
replace(factory.createObject(text))
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace instantiations of the class with links to the singleton instance of the object.
|
||||
*/
|
||||
private fun changeInstances(klass: KtClass) {
|
||||
mapReferencesByLanguage(klass)
|
||||
.apply {
|
||||
replaceKotlin(klass)
|
||||
replaceJava(klass)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map references to this class by language
|
||||
*/
|
||||
private fun mapReferencesByLanguage(klass: KtClass) = ReferencesSearch.search(klass)
|
||||
.groupBy({ it.element.language }, { it.element.parent })
|
||||
|
||||
/**
|
||||
* Replace Kotlin instantiations to a straightforward call to the singleton.
|
||||
*/
|
||||
private fun Map<Language, List<PsiElement>>.replaceKotlin(klass: KtClass) {
|
||||
val list = this[KOTLIN_LANG] ?: return
|
||||
val singletonCall = KtPsiFactory(klass).buildExpression { appendName(klass.nameAsSafeName) }
|
||||
|
||||
list.filter { it.node.elementType == KtNodeTypes.CALL_EXPRESSION }
|
||||
.forEach { it.replace(singletonCall) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace Java instantiations to an instance of the object, unless it is the only thing
|
||||
* done in the statement, in which IDEA will consider wrong, so I delete the line.
|
||||
*/
|
||||
private fun Map<Language, List<PsiElement>>.replaceJava(klass: KtClass) {
|
||||
val list = this[JAVA_LANG] ?: return
|
||||
val first = list.firstOrNull() ?: return
|
||||
val elementFactory = JavaPsiFacade.getElementFactory(klass.project)
|
||||
val javaSingletonCall = elementFactory.createExpressionFromText("${klass.name}.INSTANCE", first)
|
||||
|
||||
list.filter { it.node.elementType == JavaElementType.NEW_EXPRESSION }
|
||||
.forEach {
|
||||
when (it.parent.node.elementType) {
|
||||
JavaElementType.EXPRESSION_STATEMENT -> it.delete()
|
||||
else -> it.replace(javaSingletonCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user