forked from detekt/detekt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
UnusedImports.kt
154 lines (135 loc) · 6.59 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
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>()
fun unusedImports(): List<KtImportDirective> {
fun KtImportDirective.isFromSamePackage() =
importedFqName?.parent() == currentPackage && alias == null
@Suppress("ReturnCount")
fun KtImportDirective.isNotUsed(): Boolean {
val namedReferencesAsString = namedReferences.map { it.text.trim('`') }
val staticReferencesAsString = staticReferences.map { it.text.trim('`') }
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 fqNames = namedReferences.mapNotNull {
val descriptor = bindingContext[BindingContext.SHORT_REFERENCE_TO_COMPANION_OBJECT, it]
?: bindingContext[BindingContext.REFERENCE_TARGET, it]
descriptor?.getImportableDescriptor()?.fqNameOrNull()
}
importPath?.fqName?.let { it !in fqNames } == true
}
}
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])
}
}
}
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