Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let API Consumer decide whether a LintError has to be autocorrected, or not #2671

Merged
merged 33 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9b57c23
Add RuleAutocorrectApproveHandler interface
paul-dingemans May 15, 2024
dbb21c5
Extract CodeLinter and CodeFormatter from KtLintRuleEngine
paul-dingemans May 18, 2024
8710723
Make interactiveFormat actually work
paul-dingemans May 19, 2024
faef0a4
Simplifly SuppressHandler
paul-dingemans May 19, 2024
a930246
Update api contract
paul-dingemans May 19, 2024
8187494
Refactor and remove SuppressHandler
paul-dingemans May 19, 2024
358087d
Refactor AutoCorrectOffsetRangeHandler to RangeAutoCorrectHandler so …
paul-dingemans May 19, 2024
f27e4ff
Disable autocorrect in rules that do not implement the RuleApproveAut…
paul-dingemans May 22, 2024
b5cc08b
Ensure backward compatibility by adding parameter used to set the def…
paul-dingemans May 23, 2024
8084c01
Add FormatDecision as return result from callback used in format func…
paul-dingemans May 23, 2024
38f27d2
Update documentation
paul-dingemans May 24, 2024
998bc31
Refactor FormatDecision to AutocorrectDecision
paul-dingemans May 24, 2024
c8c9f51
Update ktlint-api-consumer
paul-dingemans May 25, 2024
913d259
Fix all rules
paul-dingemans May 25, 2024
171e9d2
Fix "cannot be auto-corrected" in lint error log
paul-dingemans May 26, 2024
1db5cae
Use new format function in ktlint CLI
paul-dingemans May 26, 2024
4b3c5ea
Rename variable
paul-dingemans May 26, 2024
fe6e0fd
Fix documentation
paul-dingemans May 26, 2024
a89d9ab
Add deprecation notice
paul-dingemans May 26, 2024
10c2236
Improve API documentation
paul-dingemans May 26, 2024
8aa7bf0
Update API contract
paul-dingemans May 26, 2024
a54218e
Update documentation
paul-dingemans May 27, 2024
c3504d0
Remove CodeLinter
paul-dingemans May 27, 2024
dede1d4
Update log message
paul-dingemans May 28, 2024
22b0efc
Inline extension methods
paul-dingemans May 28, 2024
ec0b87f
Rename methods
paul-dingemans May 28, 2024
004c7a7
Try to fix test on Windows
paul-dingemans May 28, 2024
9363bda
Try to fix test on Windows
paul-dingemans May 28, 2024
78cb2d8
Replace call to deprecated "format", except the one that need to test…
paul-dingemans May 28, 2024
036fcbb
Try to fix test Windows OS
paul-dingemans May 28, 2024
f9f7847
Try to fix test Windows OS
paul-dingemans May 28, 2024
ab72b63
Try to fix test Windows OS
paul-dingemans May 28, 2024
3460464
Try to fix test Windows OS
paul-dingemans May 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 126 additions & 42 deletions documentation/snapshot/docs/api/custom-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@

The `Ktlint Rule Engine` is the central entry point for custom integrations with the `Ktlint API`. See [basic API Consumer](https://github.com/pinterest/ktlint/blob/master/ktlint-api-consumer/src/main/kotlin/com/example/ktlint/api/consumer/KtlintApiConsumer.kt) for a basic example on how to invoke the `Ktlint Rule Engine`. This example also explains how the logging of the `Ktlint Rule Engine` can be configured to your needs.

The `KtLintRuleEngine` instance only needs to be created once for the entire lifetime of your application. Reusing the same instance results in better performance due to caching.
The `KtLintRuleEngine` instance only needs to be created once for the entire lifetime of your application. Reusing the same instance results in better performance due to caching.

```kotlin title="Creating the KtLintRuleEngine"
val ktLintRuleEngine =
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
)
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
)
```

### Rule provider

The `KtLintRuleEngine` must be configured with at least one `RuleProvider`. A `RuleProvider` is a lambda which upon request of the `KtLintRuleEngine` provides a new instance of a specific rule. You can either provide any of the standard rules provided by KtLint or with your own custom rules, or with a combination of both.
The `KtLintRuleEngine` must be configured with at least one `RuleProvider`. A `RuleProvider` is a lambda which upon request of the `KtLintRuleEngine` provides a new instance of a specific rule. You can either provide any of the standard rules provided by KtLint, or your own custom rules, or a combination of both.
```kotlin title="Creating a set of RuleProviders"
val KTLINT_API_CONSUMER_RULE_PROVIDERS =
setOf(
// Can provide custom rules
RuleProvider { NoVarRule() },
// but also reuse rules from KtLint rulesets
RuleProvider { IndentationRule() },
)
setOf(
// Can provide custom rules
RuleProvider { NoVarRule() },
// but also reuse rules from KtLint rulesets
RuleProvider { IndentationRule() },
)
```

### Editor config: defaults & overrides
Expand All @@ -32,29 +32,29 @@ When linting and formatting files, the `KtlintRuleEngine` takes the `.editorconf

```kotlin title="Specifying the editorConfigOverride"
val ktLintRuleEngine =
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
editorConfigOverride = EditorConfigOverride.from(
INDENT_STYLE_PROPERTY to IndentConfig.IndentStyle.SPACE,
INDENT_SIZE_PROPERTY to 4
)
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
editorConfigOverride = EditorConfigOverride.from(
INDENT_STYLE_PROPERTY to IndentConfig.IndentStyle.SPACE,
INDENT_SIZE_PROPERTY to 4
)
)
```

The `editorConfigOverride` property takes an `EditorConfigProperty` as key. KtLint defines several such properties, but they can also be defined as part of a custom rule.

The `editorConfigDefaults` property is more cumbersome to define as it is based directly on the data format of the `ec4j` library which is used for parsing the `.editorconfig` file.

The defaults can be loaded from a path or a directory. If a path to a file is specified, the name of the file does not necessarily have to end with `.editorconfig`. If a path to a directory is specified, the directory should contain a file with name `.editorconfig`. Note that the `propertyTypes` have to be derived from the same collection of rule providers that are specified in the `ruleProviders` property of the `KtLintRuleEngine`.
The defaults can be loaded from a path or a directory. If a path to a file is specified, the name of the file does not necessarily have to end with `.editorconfig`. If a path to a directory is specified, the directory should contain a file with name `.editorconfig`. Note that the `propertyTypes` have to be derived from the same collection of rule providers that are specified in the `ruleProviders` property of the `KtLintRuleEngine`.

```kotlin title="Specifying the editorConfigDefaults using an '.editorconfig' file"
val ktLintRuleEngine =
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
editorConfigDefaults = EditorConfigDefaults.load(
path = Paths.get("/some/path/to/editorconfig/file/or/directory"),
propertyTypes = KTLINT_API_CONSUMER_RULE_PROVIDERS.propertyTypes(),
)
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
editorConfigDefaults = EditorConfigDefaults.load(
path = Paths.get("/some/path/to/editorconfig/file/or/directory"),
propertyTypes = KTLINT_API_CONSUMER_RULE_PROVIDERS.propertyTypes(),
)
)
```
If you want to include all RuleProviders of the Ktlint project than you can easily retrieve the collection using `StandardRuleSetProvider().getRuleProviders()`.
Expand All @@ -63,20 +63,20 @@ The `EditorConfigDefaults` property can also be specified programmatically as is

```kotlin title="Specifying the editorConfigDefaults programmatically"
val ktLintRuleEngine =
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
editorConfigDefaults = EditorConfigDefaults(
org.ec4j.core.model.EditorConfig
.builder()
// .. add relevant properties
.build()
)
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
editorConfigDefaults = EditorConfigDefaults(
org.ec4j.core.model.EditorConfig
.builder()
// .. add relevant properties
.build()
)
)
```

### Lint & format

Once the `KtLintRuleEngine` has been defined, it is ready to be invoked for each file or code snippet that has to be linted or formatted. The the `lint` and `format` functions take a `Code` instance as parameter. Such an instance can either be created from a file
Once the `KtLintRuleEngine` has been defined, it is ready to be invoked for code that has to be linted or formatted. The `lint` and `format` functions take a `Code` instance as parameter. Such an instance can either be created from a file
```kotlin title="Code from file"
val code = Code.fromFile(
File("/some/path/to/file")
Expand All @@ -91,23 +91,107 @@ val code = Code.fromSnippet(
)
```

The `lint` function is invoked with a lambda which is called each time a `LintError` is found and does not return a result.
```kotlin title="Specifying the editorConfigDefaults programmatically"
The `lint` function is invoked with an optional lambda. Once linting is complete, the lambda will be called for each `LintError` which is found.
```kotlin title="Invoking lint"
ktLintRuleEngine
.lint(codeFile) { lintError ->
// handle
}
.lint(code) { lintError ->
// handle
}
```

The `format` function is invoked with a lambda which is called each time a `LintError` is found and returns the formatted code as result. Note that the `LintError` should be inspected for errors that could not be autocorrected.
```kotlin title="Specifying the editorConfigDefaults programmatically"
The `format` function is invoked with a lambda. The lambda is called for each `LintError` which is found. If the `LintError` can be autocorrected, the return value of the lambda instructs the rule whether this specific `LintError` is to be autocorrected, or not. If the `LintError` can not be autocorrected, the return result of the lambda is ignored. The formatted code is returned as result of the function.

The new `format` function allows the API Consumer to decide which LintError is to be autocorrected, or not. This is most interesting for API Consumers that let their user interactively decide per `LintError` how it has to be handled. For example see the `ktlint-intellij-plugin` which in 'manual' mode displays all lint violations, which allows the user to decide which `LintError` is to be autocorrected.

!!! note
The difference with the legacy version of the `format` is subtle. It takes two parameters (a `LintError` and `Boolean` denoting whether the `LintError` is corrected), and it does not return a value.

```kotlin title="Invoke format (preferred, starting from Ktlint 1.3)"
val formattedCode =
ktLintRuleEngine
.format(code) { lintError ->
if (lintError.canBeAutoCorrected) {
// Return AutocorrectDecision.ALLOW_AUTOCORRECT to execute the autocorrect of this lintError if this is supported by the rule.
// Return AutocorrectDecision.NO_AUTOCORRECT if the LintError should not be corrected even if is supported by the rule.
} else {
// In case the LintError can not be autocorrected, the return value of the lambda will be ignored.
// For clarity reasons it is advised to return AutocorrectDecision.NO_AUTOCORRECT in case the LintError can not be autocorrected.
AutocorrectDecision.NO_AUTOCORRECT
}
}
```

!!! warning
Rules need to implement the interface `RuleAutocorrectApproveHandler` in order to let the API Consumer decide whether a `LintError` is to be autocorrected, or not. This interface is implemented for all rules provided via the Ktlint project starting from version 1.3. However, external rulesets may not have implemented this interface on their rulesets though. Contact the maintainer of such a ruleset to implement this interface.

The (legacy) `format` function is invoked with an optional lambda. Once formatting is complete, the lambda will be called for each `LintError` which is found. The (legacy) `format` function fixes all `LintErrors` for which an autocorrect is available. The formatted code is returned as result of the function.

```kotlin title="Invoke format (deprecated as of Ktlint 1.3, will be removed in Ktlint 2.0)"
// Up until Ktlint 1.2.1 the format was invoked with a lambda having two parameters and not returning a result. This function will be removed in Ktlint 2.0
val formattedCode =
ktLintRuleEngine
.format(codeFile) { lintError ->
// handle
.format(code) { lintError, corrected ->
// handle
}
```

### Rule & RuleAutocorrectApproveHandler

!!! note
Providers of custom rules are strongly encouraged to implement `RuleAutocorrectApproveHandler` interface as described below. The `ktlint-intellij-plugin`, which will be updated soon after the 1.3 release of Ktlint, make use of this new functionality. If your ruleset is used by users of the plugin, it is very likely that they want to be able to autocorrect individual `LintErrors` or to format a block of code (e.g. a selection) in a file. This functionality will only be available for rules that have implemented this interface.

In Ktlint 1.3 the `RuleAutocorrectApproveHandler` interface is added. This interface adds the ability that the API Consumer decides per `LintError` whether it needs to autocorrected, or not. In Ktlint 2.0 the methods `beforeVisitChildNodes` and `afterVisitChildNodes` of the `Rule` class will be replaced with the new versions which are now added to the `RuleAutocorrectApproveHandler` interface as is shown below (the signature for `afterVisitChildNodes` is changed similarly):

<table>
<tr>
<td>

```kotlin title="Deprecated signature in `Rule` class"
public open fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (
offset: Int,
errorMessage: String,
canBeAutoCorrected: Boolean
) -> Unit,
)
```

</td>
<td>

```kotlin title="New signature in `RuleAutocorrectApproveHandler` interface"
public fun beforeVisitChildNodes(
node: ASTNode,
emit: (
offset: Int,
errorMessage: String,
canBeAutoCorrected: Boolean
) -> AutocorrectDecision,
)
```

</td>
</tr>
</table>

The `autoCorrect` parameter is no longer passed to the method. Instead, the `emit` lambda now returns the value `AutocorrectDecision.ALLOW_AUTOCORRECT` or `AutocorrectDecision.NO_AUTOCORRECT`.

In case a `LintError` is detected, and can be autocorrected, the `LintError` can be processed as shown below:

```kotlin
emit(node.startOffset, "some detail message", true)
.ifAutocorrectAllowed {
// Autocorrect the LintError
}
```

In case the `LintError` can not be autocorrected, if suffices to emit the violation only:
```kotlin
emit(node.startOffset, "some detail message", false)
```

## Logging

Ktlint uses the `io.github.oshai:kotlin-logging` which is a `slf4j` wrapper. As API consumer you can choose which logging framework you want to use and configure that framework to your exact needs. The [basic API Consumer](https://github.com/pinterest/ktlint/blob/master/ktlint-api-consumer/src/main/kotlin/com/example/ktlint/api/consumer/KtlintApiConsumer.kt) contains an example with `org.slf4j:slf4j-simple` as logging provider and a customized configuration which shows logging at `DEBUG` level for all classes except one specific class which only displays logging at `WARN` level.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride
import com.pinterest.ktlint.rule.engine.api.EditorConfigPropertyRegistry
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine
import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EXPERIMENTAL_RULES_EXECUTION_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY
Expand Down Expand Up @@ -112,7 +113,7 @@ public fun main() {
""".trimIndent()
}
apiConsumerKtLintRuleEngine
.format(codeFile)
.format(codeFile) { _ -> AutocorrectDecision.ALLOW_AUTOCORRECT }
.also {
LOGGER.info { "Code formatted by KtLint:\n$it" }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
package com.example.ktlint.api.consumer.rules

import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.RuleAutocorrectApproveHandler
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

public class NoVarRule :
Rule(
ruleId = RuleId("$CUSTOM_RULE_SET_ID:no-var"),
about = About(),
) {
),
RuleAutocorrectApproveHandler {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision,
) {
if (node.elementType == ElementType.VAR_KEYWORD) {
emit(node.startOffset, "Unexpected var, use val instead", false)
// In case that LintError can be autocorrected, use syntax below
// .ifAutocorrectAllowed {
// // Fix
// }
}
}
}

This file was deleted.

Loading