/
UnusedImports.kt
178 lines (155 loc) · 7.54 KB
/
UnusedImports.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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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.DetektVisitor
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.rules.isPartOf
import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtImportList
import org.jetbrains.kotlin.psi.KtPackageDirective
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.kotlin.psi.psiUtil.isDotReceiver
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.resolve.descriptorUtil.getImportableDescriptor
/**
* This rule reports unused imports. Unused imports are dead code and should be removed.
* Exempt from this rule are imports resulting from references to elements within KDoc and
* from destructuring declarations (componentN imports).
*/
class UnusedImports(config: Config) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.Style,
"Unused Imports are dead code and should be removed.",
Debt.FIVE_MINS
)
override fun visit(root: KtFile) {
with(UnusedImportsVisitor(bindingContext)) {
root.accept(this)
unusedImports().forEach {
report(CodeSmell(issue, Entity.from(it), "The import '${it.importedFqName}' is unused."))
}
}
super.visit(root)
}
private class UnusedImportsVisitor(private val bindingContext: BindingContext) : DetektVisitor() {
private var currentPackage: FqName? = null
private var imports: List<KtImportDirective>? = null
private val namedReferences = mutableSetOf<KtReferenceExpression>()
private val staticReferences = mutableSetOf<KtReferenceExpression>()
private val namedReferencesInKDoc = mutableSetOf<String>()
private val namedReferencesAsString: Set<String> by lazy {
namedReferences.mapTo(mutableSetOf()) { it.text.trim('`') }
}
private val staticReferencesAsString: Set<String> by lazy {
staticReferences.mapTo(mutableSetOf()) { it.text.trim('`') }
}
private val fqNames: Set<FqName> by lazy {
namedReferences.mapNotNullTo(mutableSetOf()) { it.fqNameOrNull() }
}
/**
* All [namedReferences] whose [KtReferenceExpression.fqNameOrNull] cannot be resolved
* mapped to their text. String matches to such references shouldn't be marked as unused
* imports since they could match the unknown value being imported.
*/
@Suppress("CommentOverPrivateProperty")
private val unresolvedNamedReferencesAsString: Set<String> by lazy {
namedReferences.mapNotNullTo(mutableSetOf()) {
if (it.fqNameOrNull() == null) it.text.trim('`') else null
}
}
fun unusedImports(): List<KtImportDirective> {
fun KtImportDirective.isFromSamePackage() =
importedFqName?.parent() == currentPackage && alias == null
@Suppress("ReturnCount")
fun KtImportDirective.isNotUsed(): Boolean {
if (aliasName in (namedReferencesInKDoc + namedReferencesAsString)) return false
val identifier = identifier()
if (identifier in namedReferencesInKDoc || identifier in staticReferencesAsString) return false
return if (bindingContext == BindingContext.EMPTY) {
identifier !in namedReferencesAsString
} else {
val fqNameUsed = importPath?.fqName?.let { it in fqNames } == true
val unresolvedNameUsed = identifier in unresolvedNamedReferencesAsString
!fqNameUsed && !unresolvedNameUsed
}
}
return imports?.filter { it.isFromSamePackage() || it.isNotUsed() }.orEmpty()
}
override fun visitPackageDirective(directive: KtPackageDirective) {
currentPackage = directive.fqName
super.visitPackageDirective(directive)
}
override fun visitImportList(importList: KtImportList) {
imports = importList.imports.asSequence()
.filter { it.isValidImport }
.filter {
val identifier = it.identifier()
identifier?.contains("*")?.not() == true &&
!operatorSet.contains(identifier) &&
!componentNRegex.matches(identifier)
}
.toList()
super.visitImportList(importList)
}
override fun visitReferenceExpression(expression: KtReferenceExpression) {
expression
.takeIf { !it.isPartOf<KtImportDirective>() && !it.isPartOf<KtPackageDirective>() }
?.takeIf { it.children.isEmpty() }
?.run {
if (this.isDotReceiver()) {
staticReferences.add(this)
} else {
namedReferences.add(this)
}
}
super.visitReferenceExpression(expression)
}
override fun visitDeclaration(dcl: KtDeclaration) {
val kdoc = dcl.docComment?.getAllSections()
kdoc?.forEach { kdocSection ->
kdocSection.getChildrenOfType<KDocTag>()
.map { it.text }
.forEach { handleKDoc(it) }
handleKDoc(kdocSection.getContent())
}
super.visitDeclaration(dcl)
}
private fun handleKDoc(content: String) {
kotlinDocReferencesRegExp.findAll(content, 0)
.map { it.groupValues[1] }
.forEach { namedReferencesInKDoc.add(it.split(".")[0]) }
kotlinDocBlockTagReferenceRegExp.find(content)?.let {
val str = it.groupValues[2].split(whiteSpaceRegex)[0]
namedReferencesInKDoc.add(str.split(".")[0])
}
}
private fun KtReferenceExpression.fqNameOrNull(): FqName? {
val descriptor = bindingContext[BindingContext.SHORT_REFERENCE_TO_COMPANION_OBJECT, this]
?: bindingContext[BindingContext.REFERENCE_TARGET, this]
return descriptor?.getImportableDescriptor()?.fqNameOrNull()
}
}
companion object {
private val operatorSet = setOf(
"unaryPlus", "unaryMinus", "not", "inc", "dec", "plus", "minus", "times", "div",
"mod", "rangeTo", "contains", "get", "set", "invoke", "plusAssign", "minusAssign", "timesAssign",
"divAssign", "modAssign", "equals", "compareTo", "iterator", "getValue", "setValue", "provideDelegate"
)
private val kotlinDocReferencesRegExp = Regex("\\[([^]]+)](?!\\[)")
private val kotlinDocBlockTagReferenceRegExp = Regex("^@(see|throws|exception) (.+)")
private val whiteSpaceRegex = Regex("\\s+")
private val componentNRegex = Regex("component\\d+")
}
}
private fun KtImportDirective.identifier() = this.importPath?.importedName?.identifier