-
-
Notifications
You must be signed in to change notification settings - Fork 755
/
UnnecessaryInnerClass.kt
135 lines (120 loc) · 4.74 KB
/
UnnecessaryInnerClass.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
package io.gitlab.arturbosch.detekt.rules.style
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.internal.RequiresTypeResolution
import org.jetbrains.kotlin.backend.common.peek
import org.jetbrains.kotlin.backend.common.pop
import org.jetbrains.kotlin.descriptors.ClassifierDescriptor
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.psi.KtThisExpression
import org.jetbrains.kotlin.psi.psiUtil.containingClass
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.classId
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
/**
* This rule reports unnecessary inner classes. Nested classes that do not access members from the outer class do
* not require the `inner` qualifier.
*
* <noncompliant>
* class A {
* val foo = "BAR"
*
* inner class B {
* val fizz = "BUZZ"
*
* fun printFizz() {
* println(fizz)
* }
* }
* }
* </noncompliant>
*/
@Suppress("TooManyFunctions")
@RequiresTypeResolution
class UnnecessaryInnerClass(config: Config = Config.empty) : Rule(config) {
private val candidateClassToParentClasses = mutableMapOf<KtClass, List<KtClass>>()
private val classChain = ArrayDeque<KtClass>()
override val issue: Issue = Issue(
javaClass.simpleName,
Severity.Style,
"The 'inner' qualifier is unnecessary.",
Debt.FIVE_MINS
)
override fun visit(root: KtFile) {
if (bindingContext == BindingContext.EMPTY) return
super.visit(root)
}
override fun visitClass(klass: KtClass) {
classChain.add(klass)
if (klass.isInner()) {
candidateClassToParentClasses[klass] = findParentClasses(klass)
}
// Visit the class to determine whether it contains any references
// to outer class members.
super.visitClass(klass)
if (klass.isInner() && candidateClassToParentClasses.contains(klass)) {
report(
CodeSmell(
issue,
Entity.Companion.from(klass),
"Class '${klass.name}' does not require `inner` keyword."
)
)
candidateClassToParentClasses.remove(klass)
}
classChain.pop()
}
override fun visitReferenceExpression(expression: KtReferenceExpression) {
super.visitReferenceExpression(expression)
checkForOuterUsage { findResolvedContainingClassId(expression) }
}
override fun visitThisExpression(expression: KtThisExpression) {
checkForOuterUsage { expression.referenceClassId() }
}
// Replace this "constructor().apply{}" pattern with buildList() when the Kotlin
// API version is upgraded to 1.6
private fun findParentClasses(ktClass: KtClass): List<KtClass> = ArrayList<KtClass>().apply {
var containingClass = ktClass.containingClass()
while (containingClass != null) {
add(containingClass)
containingClass = containingClass.containingClass()
}
}
private fun checkForOuterUsage(getTargetClassId: () -> ClassId?) {
val currentClass = classChain.peek() ?: return
val parentClasses = candidateClassToParentClasses[currentClass] ?: return
val targetClassId = getTargetClassId() ?: return
/*
* If class A -> inner class B -> inner class C, and class C has outer usage of A,
* then both B and C should stay as inner classes.
*/
val index = parentClasses.indexOfFirst { it.getClassId() == targetClassId }
if (index >= 0) {
candidateClassToParentClasses.remove(currentClass)
parentClasses.subList(0, index).forEach { candidateClassToParentClasses.remove(it) }
}
}
private fun findResolvedContainingClassId(expression: KtReferenceExpression): ClassId? {
return bindingContext[BindingContext.REFERENCE_TARGET, expression]
?.containingDeclaration
?.safeAs<ClassifierDescriptor>()
?.classId
}
private fun KtThisExpression.referenceClassId(): ClassId? {
return getResolvedCall(bindingContext)
?.resultingDescriptor
?.returnType
?.constructor
?.declarationDescriptor
?.classId
}
}