-
Notifications
You must be signed in to change notification settings - Fork 39
/
LocalVariablesRule.kt
229 lines (205 loc) · 10.9 KB
/
LocalVariablesRule.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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
package org.cqfn.diktat.ruleset.rules.chapter3.identifiers
import org.cqfn.diktat.common.config.rules.RulesConfig
import org.cqfn.diktat.ruleset.constants.Warnings.LOCAL_VARIABLE_EARLY_DECLARATION
import org.cqfn.diktat.ruleset.rules.DiktatRule
import org.cqfn.diktat.ruleset.utils.containsOnlyConstants
import org.cqfn.diktat.ruleset.utils.getDeclarationScope
import org.cqfn.diktat.ruleset.utils.getLineNumber
import org.cqfn.diktat.ruleset.utils.lastLineNumber
import org.cqfn.diktat.ruleset.utils.numNewLines
import org.cqfn.diktat.ruleset.utils.search.findAllVariablesWithUsages
import com.pinterest.ktlint.core.ast.ElementType.FILE
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.isPartOfComment
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
import org.jetbrains.kotlin.psi.psiUtil.siblings
import org.jetbrains.kotlin.psi.psiUtil.startOffset
/**
* This rule checks that local variables are declared close to the point where they are first used.
* Current algorithm assumes that scopes are always `BLOCK`s.
* 1. Warns if there are statements between variable declaration and it's first usage
* 2. It is allowed to declare variables in outer scope compared to usage scope. It could be useful to store state, e.g. between loop iterations.
*
* Current limitations due to usage of AST only:
* * Only properties without initialization or initialized with expressions based on constants are supported.
* * Properties initialized with constructor calls cannot be distinguished from method call and are no supported.
*/
class LocalVariablesRule(configRules: List<RulesConfig>) : DiktatRule(
NAME_ID,
configRules,
listOf(LOCAL_VARIABLE_EARLY_DECLARATION)
) {
override fun logic(node: ASTNode) {
if (node.elementType == FILE) {
// collect all local properties and associate with corresponding references
val propertiesToUsages = collectLocalPropertiesWithUsages(node)
// find all usages which include more than one property
val multiPropertyUsages = groupPropertiesByUsages(propertiesToUsages)
multiPropertyUsages
.forEach { (statement, properties) ->
handleConsecutiveDeclarations(statement, properties)
}
propertiesToUsages
.filterNot { it.key in multiPropertyUsages.values.flatten() }
.forEach { handleLocalProperty(it.key, it.value) }
}
}
private fun collectLocalPropertiesWithUsages(node: ASTNode) = node
.findAllVariablesWithUsages { propertyNode ->
propertyNode.isLocal && propertyNode.name != null && propertyNode.parent is KtBlockExpression &&
(propertyNode.isVar && propertyNode.initializer == null ||
(propertyNode.initializer?.containsOnlyConstants() ?: false) ||
(propertyNode.initializer as? KtCallExpression).isWhitelistedMethod())
}
.filterNot { it.value.isEmpty() }
@Suppress("TYPE_ALIAS")
private fun groupPropertiesByUsages(propertiesToUsages: Map<KtProperty, List<KtNameReferenceExpression>>) = propertiesToUsages
.mapValues { (property, usages) ->
getFirstUsageStatementOrBlock(usages, property.getDeclarationScope())
}
.map { it.value to it.key }
.groupByTo(mutableMapOf(), { it.first }) { it.second }
.filter { it.value.size > 1 }
.toMap<PsiElement, List<KtProperty>>()
@Suppress("UnsafeCallOnNullableType")
private fun handleLocalProperty(property: KtProperty, usages: List<KtNameReferenceExpression>) {
val declarationScope = property.getDeclarationScope()
val firstUsageStatementLine = getFirstUsageStatementOrBlock(usages, declarationScope).node.getLineNumber()
val firstUsage = usages.minByOrNull { it.node.getLineNumber() }!!
// should skip val and var before it's statement
val offset = property
.siblings(forward = true, withItself = false)
.takeWhile { it != getFirstUsageStatementOrBlock(usages, declarationScope) }
.filter { it is KtProperty }
.count()
checkLineNumbers(property, firstUsageStatementLine, firstUsageLine = firstUsage.node.getLineNumber(), offset = offset)
}
/**
* Check declarations, for which the properties are used on the same line.
* If properties are used for the first time in the same statement, then they can be declared on consecutive lines
* with maybe empty lines in between.
*/
@Suppress("TOO_LONG_FUNCTION")
private fun handleConsecutiveDeclarations(statement: PsiElement, properties: List<KtProperty>) {
val numLinesAfterLastProp =
properties
.last()
.node
.treeNext
.takeIf { it.elementType == WHITE_SPACE }
?.let {
// minus one is needed to except \n after property
it.numNewLines() - 1
}
?: 0
val sortedProperties = properties.sortedBy { it.node.getLineNumber() }
// need to check that properties are declared consecutively with only maybe empty lines
sortedProperties
.zip(
(properties.size - 1 downTo 0).map { index ->
val siblings = sortedProperties[properties.lastIndex - index].siblings(forward = true, withItself = false)
// Also we need to count number of comments to skip. See `should skip comments` test
// For the last property we don't need to count, because they will be counted in checkLineNumbers
// We count number of comments beginning from next property
val numberOfComments = siblings
.takeWhile { it != statement }
.dropWhile { it !is KtProperty }
.filter { it.node.isPartOfComment() }
.count()
// We should also skip all vars that were not included in properties list, but they are between statement and current property
val numberOfVarWithInitializer = siblings
.takeWhile { it != statement }
.filter { it is KtProperty && it !in properties }
.count()
// If it is not last property we should consider number on new lines after last property in list
if (index != 0) {
index + numLinesAfterLastProp + numberOfComments + numberOfVarWithInitializer
} else {
index + numberOfComments + numberOfVarWithInitializer
}
}
)
.forEach { (property, offset) ->
checkLineNumbers(property, statement.node.getLineNumber(), offset)
}
}
@Suppress("UnsafeCallOnNullableType")
private fun checkLineNumbers(
property: KtProperty,
firstUsageStatementLine: Int,
offset: Int = 0,
firstUsageLine: Int? = null
) {
val numLinesToSkip = property
.siblings(forward = true, withItself = false)
.takeWhile { it is PsiWhiteSpace || it.node.isPartOfComment() }
.let { siblings ->
siblings
.last()
.node
.lastLineNumber() - siblings
.first()
.node
.getLineNumber() - 1
}
if (firstUsageStatementLine - numLinesToSkip != property.node.lastLineNumber() + 1 + offset) {
LOCAL_VARIABLE_EARLY_DECLARATION.warn(configRules, emitWarn, isFixMode,
warnMessage(property.name!!, property.node.getLineNumber(), firstUsageLine
?: firstUsageStatementLine), property.startOffset, property.node)
}
}
/**
* Returns the [KtBlockExpression] with which a property should be compared.
* If the usage is in nested block, compared to declaration, then statement from declaration scope, which contains block
* with usage, is returned.
*
* @return either the line on which the property is used if it is first used in the same scope, or the block in the same scope as declaration
*/
@Suppress("UnsafeCallOnNullableType", "GENERIC_VARIABLE_WRONG_DECLARATION")
private fun getFirstUsageStatementOrBlock(usages: List<KtNameReferenceExpression>, declarationScope: KtBlockExpression?): PsiElement {
val firstUsage = usages.minByOrNull { it.node.getLineNumber() }!!
val firstUsageScope = firstUsage.getParentOfType<KtBlockExpression>(true)
return if (firstUsageScope == declarationScope) {
// property is first used in the same scope where it is declared, we check line of statement where it is first used
firstUsage
.parents
.find { it.parent == declarationScope }!!
} else {
// first usage is in deeper block compared to declaration, need to check how close is declaration to the first line of the block
usages.minByOrNull { it.node.getLineNumber() }!!
.parentsWithSelf
.find { it.parent == declarationScope }!!
}
}
private fun KtCallExpression?.isWhitelistedMethod() =
this?.run {
// `referenceExpression()` can return something different than just a name, e.g. when function returns a function:
// `foo()()` `referenceExpression()` will be a `KtCallExpression` as well
(referenceExpression() as? KtNameReferenceExpression)?.getReferencedName() in functionInitializers &&
valueArguments.isEmpty()
} ?: false
private fun warnMessage(
name: String,
declared: Int,
used: Int
) = "<$name> is declared on line <$declared> and is used for the first time on line <$used>"
companion object {
const val NAME_ID = "abc-local-variables"
private val functionInitializers = listOf(
"emptyList", "emptySet", "emptyMap", "emptyArray", "emptySequence",
"listOf", "setOf", "mapOf", "arrayOf", "arrayListOf",
"mutableListOf", "mutableSetOf", "mutableMapOf",
"linkedMapOf", "linkedSetOf"
)
}
}