-
Notifications
You must be signed in to change notification settings - Fork 501
/
KtLint.kt
433 lines (400 loc) · 20 KB
/
KtLint.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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
package com.pinterest.ktlint.core
import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties
import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.codeStyleSetProperty
import com.pinterest.ktlint.core.api.EditorConfigDefaults
import com.pinterest.ktlint.core.api.EditorConfigDefaults.Companion.emptyEditorConfigDefaults
import com.pinterest.ktlint.core.api.EditorConfigOverride
import com.pinterest.ktlint.core.api.EditorConfigOverride.Companion.emptyEditorConfigOverride
import com.pinterest.ktlint.core.api.EditorConfigProperties
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.core.internal.EditorConfigGenerator
import com.pinterest.ktlint.core.internal.EditorConfigLoader
import com.pinterest.ktlint.core.internal.PreparedCode
import com.pinterest.ktlint.core.internal.SuppressionLocatorBuilder
import com.pinterest.ktlint.core.internal.ThreadSafeEditorConfigCache.Companion.threadSafeEditorConfigCache
import com.pinterest.ktlint.core.internal.VisitorProvider
import com.pinterest.ktlint.core.internal.prepareCodeForLinting
import com.pinterest.ktlint.core.internal.toQualifiedRuleId
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Locale
import org.ec4j.core.model.PropertyType
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.openapi.util.Key
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
public object KtLint {
public val FILE_PATH_USER_DATA_KEY: Key<String> = Key<String>("FILE_PATH")
@Deprecated("Marked for removal in Ktlint 0.48.0")
public val EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY: Key<EditorConfigProperties> =
Key<EditorConfigProperties>("EDITOR_CONFIG_PROPERTIES")
internal const val UTF8_BOM = "\uFEFF"
public const val STDIN_FILE: String = "<stdin>"
internal val editorConfigLoader = EditorConfigLoader(FileSystems.getDefault())
/**
* Parameters to invoke [KtLint.lint] and [KtLint.format] API's.
*
* [fileName] path of file to lint/format
* [text] Contents of file to lint/format
* [ruleSets] a collection of "RuleSet"s used to validate source. This field is deprecated and will be removed in
* KtLint 0.48.
* [ruleProviders] a collection of [RuleProvider]s used to create new instances of [Rule]s so that it can keep
* internal state and be called thread-safe
* [userData] Map of user options. This field is deprecated and will be removed in a future version.
* [cb] callback invoked for each lint error
* [script] true if this is a Kotlin script file
* [editorConfigPath] optional path of the .editorconfig file (otherwise will use working directory). Marked for
* removal in KtLint 0.48. Use [editorConfigDefaults] instead
* [debug] True if invoked with the --debug flag
* [editorConfigDefaults] contains default values for `.editorconfig` properties which are not set explicitly in
* any '.editorconfig' file located on the path of the [fileName]. If a property is set in [editorConfigDefaults]
* this takes precedence above the default values defined in the KtLint project.
* [editorConfigOverride] should contain entries to add/replace from loaded `.editorconfig` files. If a property is
* set in [editorConfigOverride] it takes precedence above the same property being set in any other way.
*
* For possible keys check related [Rule]s that implements [UsesEditorConfigProperties] interface.
*
* For values use `PropertyType.PropertyValue.valid("override", <expected type>)` approach.
* It is also possible to set value into "unset" state by using [PropertyType.PropertyValue.UNSET].
*
* [isInvokedFromCli] **For internal use only**: indicates that linting was invoked from KtLint CLI tool.
* Enables some internals workarounds for Kotlin Compiler initialization.
* Usually you don't need to use it and most probably it will be removed in one of next versions.
*/
public data class ExperimentalParams(
val fileName: String? = null,
val text: String,
@Deprecated(
message = "Marked for removal in KtLint 0.48",
replaceWith = ReplaceWith("ruleProviders"),
)
val ruleSets: Iterable<RuleSet> = Iterable { emptySet<RuleSet>().iterator() },
val ruleProviders: Set<RuleProvider> = emptySet(),
val userData: Map<String, String> = emptyMap(), // TODO: remove in a future version
val cb: (e: LintError, corrected: Boolean) -> Unit,
val script: Boolean = false,
@Deprecated("Marked for removal in KtLint 0.48. Use 'editorConfigDefaults' to specify default property values")
val editorConfigPath: String? = null,
val debug: Boolean = false,
val editorConfigDefaults: EditorConfigDefaults = emptyEditorConfigDefaults,
val editorConfigOverride: EditorConfigOverride = emptyEditorConfigOverride,
val isInvokedFromCli: Boolean = false,
) {
internal val ruleRunners: Set<RuleRunner> =
ruleProviders
.map { RuleRunner(it) }
.plus(
/** Support backward compatibility for API consumers in KtLint 0.47 by changing rule sets to rule
* providers with limitation that for those rules *no* new instances can be created during
* [KtLint.format].
* KtLint CLI already transforms rules provided by external rule providers to real RuleProviders
* for which new instances can be created. The same workaround is not possible for as
* [KtLint.ExperimentalParams.ruleSets] already contain the created [Rule] instance.
*/
// TODO: remove when removing the deprecated ruleSets.
ruleSets
.flatMap { it.rules.toList() }
.map { RuleRunner(createStaticRuleProvider(it)) },
).distinctBy { it.ruleId }
.toSet()
internal fun getRules(): Set<Rule> =
ruleRunners
.map { it.getRule() }
.toSet()
init {
require(ruleSets.any() xor ruleProviders.any()) {
"Provide exactly one of parameters 'ruleSets' or 'ruleProviders'"
}
// Extract all default and custom ".editorconfig" properties which are registered
val editorConfigProperties =
getRules()
.asSequence()
.filterIsInstance<UsesEditorConfigProperties>()
.map { it.editorConfigProperties }
.flatten()
.plus(DefaultEditorConfigProperties.editorConfigProperties)
.map { it.type.name }
.distinct()
.toSet()
userData
.keys
.intersect(editorConfigProperties)
.let {
check(it.isEmpty()) {
"UserData should not contain '.editorconfig' properties ${it.sorted()}. Such properties " +
"should be passed via the 'ExperimentalParams.editorConfigOverride' field. Note that this is " +
"only required for properties that (potentially) contain a value that differs from the " +
"actual value in the '.editorconfig' file."
}
}
userData
.keys
.minus(editorConfigProperties)
.let {
check(it.isEmpty()) {
"UserData contains properties ${it.sorted()}. However, userData is deprecated and will be " +
"removed in a future version. Please create an issue that shows how this field is " +
"actively used."
}
}
}
internal val normalizedFilePath: Path?
get() = if (fileName == STDIN_FILE || fileName == null) {
null
} else {
Paths.get(fileName)
}
internal val isStdIn: Boolean get() = fileName == STDIN_FILE
}
/**
* Check source for lint errors.
*
* @throws ParseException if text is not a valid Kotlin code
* @throws RuleExecutionException in case of internal failure caused by a bug in rule implementation
*/
public fun lint(params: ExperimentalParams) {
val preparedCode = prepareCodeForLinting(params)
val errors = mutableListOf<LintError>()
VisitorProvider(params)
.visitor(preparedCode.editorConfigProperties)
.invoke { rule, fqRuleId ->
preparedCode.executeRule(rule, fqRuleId, false) { offset, errorMessage, canBeAutoCorrected ->
val (line, col) = preparedCode.positionInTextLocator(offset)
errors.add(LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected))
}
}
errors
.sortedWith { l, r -> if (l.line != r.line) l.line - r.line else l.col - r.col }
.forEach { e -> params.cb(e, false) }
}
private fun PreparedCode.executeRule(
rule: Rule,
fqRuleId: String,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
rule.startTraversalOfAST()
rule.beforeFirstNode(editorConfigProperties)
this.executeRuleOnNodeRecursively(rootNode, rule, fqRuleId, autoCorrect, emit)
rule.afterLastNode()
}
private fun PreparedCode.executeRuleOnNodeRecursively(
node: ASTNode,
rule: Rule,
fqRuleId: String,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
if (rule.shouldContinueTraversalOfAST()) {
try {
rule.beforeVisitChildNodes(node, autoCorrect, emit)
if (!rule.runsOnRootNodeOnly() && rule.shouldContinueTraversalOfAST()) {
node
.getChildren(null)
.forEach { childNode ->
// https://github.com/shyiko/ktlint/issues/158#issuecomment-462728189
// fixme: enforcing suppression based on node.startOffset is wrong (not just because not all nodes are leaves
// but because rules are free to emit (and fix!) errors at any position)
if (!suppressedRegionLocator(childNode.startOffset, fqRuleId, childNode === rootNode)) {
this.executeRuleOnNodeRecursively(childNode, rule, fqRuleId, autoCorrect, emit)
}
}
}
rule.afterVisitChildNodes(node, autoCorrect, emit)
} catch (e: Exception) {
if (autoCorrect) {
// line/col cannot be reliably mapped as exception might originate from a node not present in the
// original AST
throw RuleExecutionException(0, 0, fqRuleId, e)
} else {
val (line, col) = positionInTextLocator(node.startOffset)
throw RuleExecutionException(line, col, fqRuleId, e)
}
}
}
}
/**
* Fix style violations.
*
* @throws ParseException if text is not a valid Kotlin code
* @throws RuleExecutionException in case of internal failure caused by a bug in rule implementation
*/
public fun format(params: ExperimentalParams): String {
val hasUTF8BOM = params.text.startsWith(UTF8_BOM)
val preparedCode = prepareCodeForLinting(params)
var tripped = false
var mutated = false
val errors = mutableSetOf<Pair<LintError, Boolean>>()
val visitorProvider = VisitorProvider(params = params)
visitorProvider
.visitor(preparedCode.editorConfigProperties)
.invoke { rule, fqRuleId ->
preparedCode.executeRule(rule, fqRuleId, true) { offset, errorMessage, canBeAutoCorrected ->
tripped = true
if (canBeAutoCorrected) {
mutated = true
if (preparedCode.suppressedRegionLocator !== SuppressionLocatorBuilder.noSuppression) {
// Offsets of start and end positions of suppressed regions might have changed due to
// updating the code
preparedCode.suppressedRegionLocator =
SuppressionLocatorBuilder.buildSuppressedRegionsLocator(
preparedCode.rootNode,
)
}
}
val (line, col) = preparedCode.positionInTextLocator(offset)
errors.add(
Pair(
LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected),
// It is assumed that a rule that emits that an error can be autocorrected, also
// does correct the error.
canBeAutoCorrected,
),
)
}
}
if (tripped) {
visitorProvider
.visitor(preparedCode.editorConfigProperties)
.invoke { rule, fqRuleId ->
preparedCode.executeRule(rule, fqRuleId, false) { offset, errorMessage, canBeAutoCorrected ->
val (line, col) = preparedCode.positionInTextLocator(offset)
errors.add(
Pair(
LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected),
// It is assumed that a rule only corrects an error after it has emitted an
// error and indicating that it actually can be autocorrected.
false,
),
)
}
}
}
errors
.sortedWith { (l), (r) -> if (l.line != r.line) l.line - r.line else l.col - r.col }
.forEach { (e, corrected) -> params.cb(e, corrected) }
if (!mutated) {
return params.text
}
val code = preparedCode
.rootNode
.text
.replace("\n", determineLineSeparator(params.text, params.userData))
return if (hasUTF8BOM) {
UTF8_BOM + code
} else {
code
}
}
private fun Rule.runsOnRootNodeOnly() =
visitorModifiers.contains(Rule.VisitorModifier.RunOnRootNodeOnly)
/**
* Reduce memory usage by cleaning internal caches.
*/
public fun trimMemory() {
threadSafeEditorConfigCache.clear()
}
/**
* Generates Kotlin `.editorconfig` file section content based on [ExperimentalParams].
*
* Method loads merged `.editorconfig` content from [ExperimentalParams] path,
* and then, by querying rules from [ExperimentalParams] for missing properties default values,
* generates Kotlin section (default is `[*.{kt,kts}]`) new content.
*
* Rule should implement [UsesEditorConfigProperties] interface to support this.
*
* @return Kotlin section editorconfig content. For example:
* ```properties
* final-newline=true
* indent-size=4
* ```
*/
public fun generateKotlinEditorConfigSection(
params: ExperimentalParams,
): String {
val filePath = params.normalizedFilePath
requireNotNull(filePath) {
"Please pass path to existing Kotlin file"
}
val codeStyle =
params
.editorConfigOverride
.properties[codeStyleSetProperty]
?.parsed
?.safeAs<DefaultEditorConfigProperties.CodeStyleValue>()
?: codeStyleSetProperty.defaultValue
return EditorConfigGenerator(editorConfigLoader).generateEditorconfig(
filePath,
params.getRules(),
params.debug,
codeStyle,
)
}
private fun determineLineSeparator(fileContent: String, userData: Map<String, String>): String {
val eol = userData["end_of_line"]?.trim()?.lowercase(Locale.getDefault())
return when {
eol == "native" -> System.lineSeparator()
eol == "crlf" || eol != "lf" && fileContent.lastIndexOf('\r') != -1 -> "\r\n"
else -> "\n"
}
}
}
internal class RuleRunner(private val provider: RuleProvider) {
private var rule = provider.createNewRuleInstance()
internal val qualifiedRuleId = rule.toQualifiedRuleId()
internal val shortenedQualifiedRuleId = qualifiedRuleId.removePrefix("standard:")
internal val ruleId = rule.id
internal val ruleSetId = qualifiedRuleId.substringBefore(':')
val runOnRootNodeOnly =
rule.visitorModifiers.contains(Rule.VisitorModifier.RunOnRootNodeOnly)
val runAsLateAsPossible = rule.visitorModifiers.contains(Rule.VisitorModifier.RunAsLateAsPossible)
var runAfterRule = setRunAfterRule()
/**
* Gets the [Rule]. If the [Rule] has already been used for traversal of the AST, a new instance of the [Rule] is
* provided. This prevents leakage of the state of the Rule between executions.
*/
internal fun getRule(): Rule {
if (rule.isUsedForTraversalOfAST()) {
rule = provider.createNewRuleInstance()
}
return rule
}
private fun setRunAfterRule(): Rule.VisitorModifier.RunAfterRule? =
rule
.visitorModifiers
.find { it is Rule.VisitorModifier.RunAfterRule }
?.let {
val runAfterRuleVisitorModifier = it as Rule.VisitorModifier.RunAfterRule
val qualifiedAfterRuleId = runAfterRuleVisitorModifier.ruleId.toQualifiedRuleId()
check(qualifiedRuleId != qualifiedAfterRuleId) {
// Do not print the fully qualified rule id in the error message as it might not appear in the code
// in case it is a rule from the 'standard' rule set.
"Rule with id '${rule.id}' has a visitor modifier of type " +
"'${Rule.VisitorModifier.RunAfterRule::class.simpleName}' but it is not referring to another " +
"rule but to the rule itself. A rule can not run after itself. This should be fixed by the " +
"maintainer of the rule."
}
runAfterRuleVisitorModifier.copy(
ruleId = qualifiedAfterRuleId,
)
}
internal fun clearRunAfterRule() {
require(!rule.isUsedForTraversalOfAST()) {
"RunAfterRule can not be cleared when rule has already been used for traversal of the AST"
}
runAfterRule = null
}
}
/**
* This provider is added for backward compatibility of KtLint 0.47 so that API consumers can either use
* [KtLint.ExperimentalParams.ruleSets] or [KtLint.ExperimentalParams.ruleProviders] per [RuleSetProvider]. This method
* created a [RuleProvider] which returns a *static* instance of a rule and should only be used for rules provided via
* [KtLint.ExperimentalParams.ruleSets].
* * Rules provided by this [RuleProvider] might leak state between the first and second invocation of the rule when
* running [KtLint.format]. It is assumed that [Rule] implementations offered by 'KtLint 0.46.x' and custom rule set
* providers are not suffering any problems at this moment as this architectural bug exists in KtLint for quite a long
* * Note that [KtLint.ExperimentalParams.ruleSets] and this helper method will be removed in KtLint 0.48.
*/
@Deprecated(message = "Remove when 'ruleSets' are removed")
private fun createStaticRuleProvider(rule: Rule) =
RuleProvider { rule }