Add inspection: Refactor sealed sub-class to object #KT-20305 Fixed

This commit is contained in:
Gabryel Monteiro
2018-02-12 20:50:36 +03:00
committed by Mikhail Glukhikh
parent 7cf6c70fbf
commit df7968678a
31 changed files with 514 additions and 0 deletions
+9
View File
@@ -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() }
}
}
@@ -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)
}
}
}
}