-
Notifications
You must be signed in to change notification settings - Fork 501
/
KtLintRuleEngine.kt
352 lines (323 loc) · 16.8 KB
/
KtLintRuleEngine.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
@file:Suppress("MemberVisibilityCanBePrivate")
package com.pinterest.ktlint.rule.engine.api
import com.pinterest.ktlint.logger.api.initKtLintKLogger
import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults.Companion.EMPTY_EDITOR_CONFIG_DEFAULTS
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride.Companion.EMPTY_EDITOR_CONFIG_OVERRIDE
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.END_OF_LINE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.propertyTypes
import com.pinterest.ktlint.rule.engine.core.util.safeAs
import com.pinterest.ktlint.rule.engine.internal.AutoCorrectDisabledHandler
import com.pinterest.ktlint.rule.engine.internal.AutoCorrectEnabledHandler
import com.pinterest.ktlint.rule.engine.internal.AutoCorrectHandler
import com.pinterest.ktlint.rule.engine.internal.AutoCorrectOffsetRangeHandler
import com.pinterest.ktlint.rule.engine.internal.EditorConfigFinder
import com.pinterest.ktlint.rule.engine.internal.EditorConfigGenerator
import com.pinterest.ktlint.rule.engine.internal.EditorConfigLoader
import com.pinterest.ktlint.rule.engine.internal.EditorConfigLoaderEc4j
import com.pinterest.ktlint.rule.engine.internal.RuleExecutionContext
import com.pinterest.ktlint.rule.engine.internal.RuleExecutionContext.Companion.createRuleExecutionContext
import com.pinterest.ktlint.rule.engine.internal.ThreadSafeEditorConfigCache.Companion.THREAD_SAFE_EDITOR_CONFIG_CACHE
import com.pinterest.ktlint.rule.engine.internal.VisitorProvider
import io.github.oshai.kotlinlogging.KotlinLogging
import org.ec4j.core.Resource
import org.ec4j.core.model.PropertyType.EndOfLineValue.crlf
import org.ec4j.core.model.PropertyType.EndOfLineValue.lf
import org.jetbrains.kotlin.com.intellij.lang.FileASTNode
import java.nio.charset.StandardCharsets
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Path
private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger()
public class KtLintRuleEngine(
/**
* The set of [RuleProvider]s to be invoked by the [KtLintRuleEngine]. A [RuleProvider] is able to create a new instance of a [Rule] so
* that it can keep internal state and be called thread-safe manner
*/
public val ruleProviders: Set<RuleProvider> = emptySet(),
/**
* The default values for `.editorconfig` properties which are not set explicitly in any '.editorconfig' file located on the path of the
* file which is processed with the [KtLintRuleEngine]. If a property is set in [editorConfigDefaults] this takes precedence above the
* default values defined in the KtLint project.
*/
public val editorConfigDefaults: EditorConfigDefaults = EMPTY_EDITOR_CONFIG_DEFAULTS,
/**
* Override values for `.editorconfig` properties. If a property is set in [editorConfigOverride] it takes precedence above the same
* property being set in any other way.
*/
public val editorConfigOverride: EditorConfigOverride = EMPTY_EDITOR_CONFIG_OVERRIDE,
/**
* **For internal use only**: indicates that linting was invoked from KtLint CLI tool. It enables some internals workarounds for Kotlin
* Compiler initialization. This property is likely to be removed in any of next versions without further notice.
*/
public val isInvokedFromCli: Boolean = false,
/**
* The [FileSystem] to be used. This property is primarily intended to be used in unit tests. By specifying an alternative [FileSystem]
* the unit test gains control on whether the [EditorConfigLoader] should or should not read specific ".editorconfig" files. For
* example, it is considered unwanted that a unit test is influenced by the ".editorconfig" file of the project in which the unit test
* is included.
*/
public val fileSystem: FileSystem = FileSystems.getDefault(),
) {
init {
require(ruleProviders.any()) {
"A non-empty set of 'ruleProviders' need to be provided"
}
}
internal val editorConfigLoaderEc4j = EditorConfigLoaderEc4j(ruleProviders.propertyTypes())
internal val editorConfigLoader =
EditorConfigLoader(
fileSystem,
editorConfigLoaderEc4j,
editorConfigDefaults,
editorConfigOverride,
)
/**
* Check the [code] for lint errors. If [code] is path as file reference then the '.editorconfig' files on the path to file are taken
* into account. For each lint violation found, the [callback] is invoked.
*
* @throws KtLintParseException if text is not a valid Kotlin code
* @throws KtLintRuleException in case of internal failure caused by a bug in rule implementation
*/
public fun lint(
code: Code,
callback: (LintError) -> Unit = { },
) {
LOGGER.debug { "Starting with linting file '${code.fileNameOrStdin()}'" }
val ruleExecutionContext = createRuleExecutionContext(this, code)
val errors = mutableListOf<LintError>()
VisitorProvider(ruleExecutionContext.ruleProviders)
.visitor()
.invoke { rule ->
ruleExecutionContext.executeRule(rule, AutoCorrectDisabledHandler) { offset, errorMessage, canBeAutoCorrected ->
val (line, col) = ruleExecutionContext.positionInTextLocator(offset)
LintError(line, col, rule.ruleId, errorMessage, canBeAutoCorrected)
.let { lintError ->
errors.add(lintError)
// In trace mode report the violation immediately. The order in which violations are actually found might be
// different from the order in which they are reported. For debugging purposes it can be helpful to know the
// exact order in which violations are being solved.
LOGGER.trace { "Lint violation: ${lintError.logMessage(code)}" }
}
}
}
errors
.sortedWith { l, r -> if (l.line != r.line) l.line - r.line else l.col - r.col }
.forEach { e -> callback(e) }
LOGGER.debug { "Finished with linting file '${code.fileNameOrStdin()}'" }
}
/**
* Fix style violations in [code] for lint errors when possible. If [code] is passed as file reference then the '.editorconfig' files on
* the path are taken into account. For each lint violation found, the [callback] is invoked.
*
* @throws KtLintParseException if text is not a valid Kotlin code
* @throws KtLintRuleException in case of internal failure caused by a bug in rule implementation
*/
public fun format(
code: Code,
callback: (LintError, Boolean) -> Unit = { _, _ -> },
): String = format(code, AutoCorrectEnabledHandler, callback)
/**
* Fix style violations in [code] for lint errors found in the [autoCorrectOffsetRange] when possible. If [code] is passed as file
* reference then the '.editorconfig' files on the path are taken into account. For each lint violation found, the [callback] is
* invoked.
*
* IMPORTANT: Partial formatting not always works as expected. The offset of the node which is triggering the violation does not
* necessarily to be close to the offset at which the violation is reported. Counter-intuitively the offset of the trigger node must be
* located inside the [autoCorrectOffsetRange] instead of the offset at which the violation is reported.
*
* For example, the given code might contain the when-statement below:
* ```
* // code with lint violations
*
* when(foobar) {
* FOO -> "Single line"
* BAR ->
* """
* Multi line
* """.trimIndent()
* else -> null
* }
*
* // more code with lint violations
* ```
* The `blank-line-between-when-conditions` rule requires blank lines to be added between the conditions. If the when-keyword above is
* included in the range which is to be formatted then the blank lines before the conditions are added. If only the when-conditions
* itself are selected, but not the when-keyword, then the blank lines are not added.
*
* This unexpected behavior is a side effect of the way the partial formatting is implemented currently. The side effects can be
* prevented by delaying the decision to autocorrect as late as possible and the exact offset of the error is known. This however would
* cause a breaking change, and needs to wait until Ktlint V2.x.
*
* @throws KtLintParseException if text is not a valid Kotlin code
* @throws KtLintRuleException in case of internal failure caused by a bug in rule implementation
*/
public fun format(
code: Code,
autoCorrectOffsetRange: IntRange,
callback: (LintError, Boolean) -> Unit = { _, _ -> },
): String = format(code, AutoCorrectOffsetRangeHandler(autoCorrectOffsetRange), callback)
private fun format(
code: Code,
autoCorrectHandler: AutoCorrectHandler,
callback: (LintError, Boolean) -> Unit = { _, _ -> },
): String {
LOGGER.debug { "Starting with formatting file '${code.fileNameOrStdin()}'" }
val hasUTF8BOM = code.content.startsWith(UTF8_BOM)
val ruleExecutionContext = createRuleExecutionContext(this, code)
val visitorProvider = VisitorProvider(ruleExecutionContext.ruleProviders)
var formatRunCount = 0
var mutated: Boolean
val errors = mutableSetOf<Pair<LintError, Boolean>>()
do {
mutated = false
formatRunCount++
visitorProvider
.visitor()
.invoke { rule ->
ruleExecutionContext.executeRule(rule, autoCorrectHandler) { offset, errorMessage, canBeAutoCorrected ->
if (canBeAutoCorrected) {
mutated = true
/*
* Rebuild the suppression locator after each change in the AST as the offsets of the suppression hints might
* have changed.
*/
ruleExecutionContext.rebuildSuppressionLocator()
}
val (line, col) = ruleExecutionContext.positionInTextLocator(offset)
LintError(line, col, rule.ruleId, errorMessage, canBeAutoCorrected)
.let { lintError ->
errors.add(
Pair(
lintError,
// It is assumed that a rule that emits that an error can be autocorrected, also does correct the error.
canBeAutoCorrected,
),
)
// In trace mode report the violation immediately. The order in which violations are actually found might be
// different from the order in which they are reported. For debugging purposes it can be helpful to know the
// exact order in which violations are being solved.
LOGGER.trace { "Format violation: ${lintError.logMessage(code)}" }
}
}
}
} while (mutated && formatRunCount < MAX_FORMAT_RUNS_PER_FILE)
if (formatRunCount == MAX_FORMAT_RUNS_PER_FILE && mutated) {
// It is unknown if the last format run introduces new lint violations which can be autocorrected. So run lint once more so that
// the user can be informed about this correctly.
var hasErrorsWhichCanBeAutocorrected = false
visitorProvider
.visitor()
.invoke { rule ->
if (!hasErrorsWhichCanBeAutocorrected) {
ruleExecutionContext.executeRule(rule, AutoCorrectDisabledHandler) { _, _, canBeAutoCorrected ->
if (canBeAutoCorrected) {
hasErrorsWhichCanBeAutocorrected = true
}
}
}
}
if (hasErrorsWhichCanBeAutocorrected) {
LOGGER.warn {
"Format was not able to resolve all violations which (theoretically) can be autocorrected in file " +
"${code.filePathOrStdin()} in $MAX_FORMAT_RUNS_PER_FILE consecutive runs of format."
}
}
}
errors
.sortedWith { (l), (r) -> if (l.line != r.line) l.line - r.line else l.col - r.col }
.forEach { (e, corrected) -> callback(e, corrected) }
if (!mutated && formatRunCount == 1) {
return code.content
}
val formattedCode =
ruleExecutionContext
.rootNode
.text
.replace("\n", ruleExecutionContext.determineLineSeparator(code.content))
return if (hasUTF8BOM) {
UTF8_BOM + formattedCode
} else {
formattedCode
}.also {
LOGGER.debug { "Finished with formatting file '${code.fileNameOrStdin()}'" }
}
}
private fun RuleExecutionContext.determineLineSeparator(fileContent: String): String {
val eol = editorConfig[END_OF_LINE_PROPERTY]
return when {
eol == crlf || eol != lf && fileContent.lastIndexOf('\r') != -1 -> "\r\n"
else -> "\n"
}
}
/**
* Generates Kotlin `.editorconfig` file section content based on a path to a file or directory. Given that path, all '.editorconfig'
* files on that path are taken into account to determine the values of properties which are already used.
*
* @return Kotlin section editorconfig content. For example:
* ```properties
* final-newline=true
* indent-size=4
* ```
*/
public fun generateKotlinEditorConfigSection(filePath: Path): String {
val codeStyle =
editorConfigOverride
.properties[CODE_STYLE_PROPERTY]
?.parsed
?.safeAs<CodeStyleValue>()
?: CODE_STYLE_PROPERTY.defaultValue
val rules =
ruleProviders
.map { it.createNewRuleInstance() }
.distinctBy { it.ruleId }
.toSet()
return EditorConfigGenerator(fileSystem, editorConfigLoaderEc4j)
.generateEditorconfig(
rules,
codeStyle,
filePath,
)
}
/**
* Reduce memory usage by cleaning internal caches.
*/
public fun trimMemory() {
THREAD_SAFE_EDITOR_CONFIG_CACHE.clear()
}
/**
* Get the list of files which will be accessed by KtLint when linting or formatting the given file or directory.
* The API consumer can use this list to observe changes in '.editorconfig' files. Whenever such a change is
* observed, the API consumer should call [reloadEditorConfigFile].
* To avoid unnecessary access to the file system, it is best to call this method only once for the root of the
* project which is to be [lint] or [format].
*/
public fun editorConfigFilePaths(path: Path): List<Path> = EditorConfigFinder(editorConfigLoaderEc4j).findEditorConfigs(path)
/**
* Reloads an '.editorconfig' file given that it is currently loaded into the KtLint cache. This method is intended
* to be called by the API consumer when it is aware of changes in the '.editorconfig' file that should be taken
* into account with next calls to [lint] and/or [format]. See [editorConfigFilePaths] to get the list of
* '.editorconfig' files which need to be observed.
*/
public fun reloadEditorConfigFile(path: Path) {
THREAD_SAFE_EDITOR_CONFIG_CACHE.reloadIfExists(
Resource.Resources.ofPath(path, StandardCharsets.UTF_8),
)
}
public fun transformToAst(code: Code): FileASTNode = createRuleExecutionContext(this, code).rootNode
private fun LintError.logMessage(code: Code) =
"${code.fileNameOrStdin()}:$line:$col: $detail ($ruleId)" +
if (canBeAutoCorrected) {
""
} else {
" [cannot be autocorrected]"
}
public companion object {
internal const val UTF8_BOM = "\uFEFF"
public const val STDIN_FILE: String = "<stdin>"
private const val MAX_FORMAT_RUNS_PER_FILE = 3
}
}