/
UnnecessaryAbstractClass.kt
136 lines (122 loc) · 5.63 KB
/
UnnecessaryAbstractClass.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package io.gitlab.arturbosch.detekt.rules.style
import io.gitlab.arturbosch.detekt.api.AnnotationExcluder
import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.rules.isAbstract
import io.gitlab.arturbosch.detekt.rules.isInternal
import io.gitlab.arturbosch.detekt.rules.isProtected
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.descriptors.MemberDescriptor
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.isAbstract
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyClassMemberScope
import org.jetbrains.kotlin.types.typeUtil.isInterface
/**
* This rule inspects `abstract` classes. In case an `abstract class` does not have any concrete members it should be
* refactored into an interface. Abstract classes which do not define any `abstract` members should instead be
* refactored into concrete classes.
*
* <noncompliant>
* abstract class OnlyAbstractMembersInAbstractClass { // violation: no concrete members
*
* abstract val i: Int
* abstract fun f()
* }
*
* abstract class OnlyConcreteMembersInAbstractClass { // violation: no abstract members
*
* val i: Int = 0
* fun f() { }
* }
* </noncompliant>
*/
@ActiveByDefault(since = "1.2.0")
class UnnecessaryAbstractClass(config: Config = Config.empty) : Rule(config) {
private val noConcreteMember = "An abstract class without a concrete member can be refactored to an interface."
private val noAbstractMember = "An abstract class without an abstract member can be refactored to a concrete class."
override val issue =
Issue(
"UnnecessaryAbstractClass",
Severity.Style,
"An abstract class is unnecessary and can be refactored. " +
"An abstract class should have both abstract and concrete properties or functions. " +
noConcreteMember + " " + noAbstractMember,
Debt.FIVE_MINS
)
@Configuration("Allows you to provide a list of annotations that disable this check.")
@Deprecated("Use `ignoreAnnotated` instead")
private val excludeAnnotatedClasses: List<Regex> by config(emptyList<String>()) { list ->
list.map { it.replace(".", "\\.").replace("*", ".*").toRegex() }
}
private lateinit var annotationExcluder: AnnotationExcluder
override fun visitKtFile(file: KtFile) {
annotationExcluder = AnnotationExcluder(
file,
@Suppress("DEPRECATION") excludeAnnotatedClasses,
bindingContext,
)
super.visitKtFile(file)
}
override fun visitClass(klass: KtClass) {
klass.check()
super.visitClass(klass)
}
private fun KtClass.check() {
val nameIdentifier = this.nameIdentifier ?: return
if (annotationExcluder.shouldExclude(annotationEntries) || isInterface() || !isAbstract()) return
val members = members()
when {
members.isNotEmpty() -> checkMembers(members, nameIdentifier)
hasInheritedMember(true) && isAnyParentAbstract() -> return
!hasConstructorParameter() ->
report(CodeSmell(issue, Entity.from(nameIdentifier), noConcreteMember))
else ->
report(CodeSmell(issue, Entity.from(nameIdentifier), noAbstractMember))
}
}
private fun KtClass.checkMembers(
members: List<KtCallableDeclaration>,
nameIdentifier: PsiElement
) {
val (abstractMembers, concreteMembers) = members.partition { it.isAbstract() }
when {
abstractMembers.isEmpty() && !hasInheritedMember(true) ->
report(CodeSmell(issue, Entity.from(nameIdentifier), noAbstractMember))
abstractMembers.any { it.isInternal() || it.isProtected() } || hasConstructorParameter() ->
Unit
concreteMembers.isEmpty() && !hasInheritedMember(false) ->
report(CodeSmell(issue, Entity.from(nameIdentifier), noConcreteMember))
}
}
private fun KtClass.members() = body?.children?.filterIsInstance<KtCallableDeclaration>().orEmpty() +
primaryConstructor?.valueParameters?.filter { it.hasValOrVar() }.orEmpty()
private fun KtClass.hasConstructorParameter() = primaryConstructor?.valueParameters?.isNotEmpty() == true
private fun KtClass.hasInheritedMember(isAbstract: Boolean): Boolean {
return when {
superTypeListEntries.isEmpty() -> false
bindingContext == BindingContext.EMPTY -> true
else -> {
val descriptor = bindingContext[BindingContext.CLASS, this]
descriptor?.unsubstitutedMemberScope?.getContributedDescriptors().orEmpty().any {
(it as? MemberDescriptor)?.modality == Modality.ABSTRACT == isAbstract
}
}
}
}
private fun KtClass.isAnyParentAbstract() =
(bindingContext[BindingContext.CLASS, this]?.unsubstitutedMemberScope as? LazyClassMemberScope)
?.supertypes
?.all { it.isInterface() } == false
}