Skip to content

Commit

Permalink
Clarify how to implement an API Consumer (#1881)
Browse files Browse the repository at this point in the history
  • Loading branch information
paul-dingemans authored Mar 24, 2023
1 parent 1b66f18 commit 8757eb6
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 44 deletions.
116 changes: 116 additions & 0 deletions docs/api/custom-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Custom integration

!!! warning
This page is based on Ktlint `0.49.x` which has to be released. Most concepts are also applicable for `0.48.x`.

## Ktlint Rule Engine

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.

```kotlin title="Creating the KtLintRuleEngine"
val ktLintRuleEngine =
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.
```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() },
)
```

### Editor config: defaults & overrides

When linting and formatting files, the `KtlintRuleEngine` takes the `.editorconfig` file(s) into account which are found on the path to the file. A property which is specified in the `editorConfigOverride` property of the `KtLintRuleEngine` takes precedence above the value of that same property in the `.editorconfig` file. The `editorConfigDefaults` property of the `KtLintRuleEngine` can be used to specify the fallback values for properties in case that property is not defined in the `.editorconfig` file (or in the `editorConfigOverride` property).

```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
)
)
```

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`.

```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(),
)
)
```
If you want to include all RuleProviders of the Ktlint project than you can easily retrieve the collection using `StandardRuleSetProvider().getRuleProviders()`.

The `EditorConfigDefaults` property can also be specified programmatically as is shown below:

```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()
)
)
```

### 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
```kotlin title="Code from file"
val code = Code.fromFile(
File("/some/path/to/file")
)
```
or a code snippet (set `script` to `true` to handle the snippet as Kotlin script):
```kotlin title="Code from snippet"
val code = Code.fromSnippet(
"""
val code = "some-code"
""".trimIndent()
)
```

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"
ktLintRuleEngine
.lint(codeFile) { 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"
val formattedCode =
ktLintRuleEngine
.format(codeFile) { lintError ->
// handle
}
```

## Logging

Ktlint uses the `io.github.microutils: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.
3 changes: 3 additions & 0 deletions ktlint-api-consumer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ktlint API Consumer

This module contains a very basic example of how to implement an API Consumer on top of the Ktlint API. If you want to implement your own custom API Consumer than you can copy this module into a stand-alone project or as submodule into another project.
4 changes: 4 additions & 0 deletions ktlint-api-consumer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ plugins {
}

dependencies {
// Any SLF4J compatible logging framework can be used. The "slf4j-simple" logging provider is configured in file
// ktlint-api-consumer/src/main/resources/simplelogger.properties
runtimeOnly("org.slf4j:slf4j-simple:2.0.7")

implementation(projects.ktlintLogger)
implementation(projects.ktlintRuleEngine)
// This example API Consumer also depends on ktlint-ruleset-standard as it mixes custom rules and rules from ktlint-ruleset-standard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,83 @@ package com.example.ktlint.api.consumer
import com.example.ktlint.api.consumer.rules.KTLINT_API_CONSUMER_RULE_PROVIDERS
import com.pinterest.ktlint.logger.api.initKtLintKLogger
import com.pinterest.ktlint.rule.engine.api.Code
import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.propertyTypes
import mu.KotlinLogging
import java.io.File
import java.nio.file.Paths

private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger()

public class KtlintApiConsumer {
/**
* This API Consumer shows how the [KtLintRuleEngine] can be invoked and how the amount and format of the logging can be configured.
* This example uses 'slf4j-simple' as logging provider. But any logging provider that implements SLF4J API can be used depending on your
* needs.
*/
public fun main(args: Array<String>) {
// The KtLint RuleEngine only needs to be instantiated once and can be reused in multiple invocations
private val ktLintRuleEngine =
val ktLintRuleEngine =
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
editorConfigOverride =
EditorConfigOverride.from(
INDENT_STYLE_PROPERTY to IndentConfig.IndentStyle.SPACE,
INDENT_SIZE_PROPERTY to 4,
),
editorConfigDefaults =
EditorConfigDefaults.load(
path = Paths.get("/some/path/to/editorconfig/file/or/directory"),
propertyTypes = KTLINT_API_CONSUMER_RULE_PROVIDERS.propertyTypes(),
),
)

public fun run(
command: String,
fileName: String,
) {
val codeFile =
Code.fromFile(
File(fileName),
)

when (command) {
"lint" -> {
ktLintRuleEngine
.lint(codeFile) {
LOGGER.info { "LintViolation reported by KtLint: $it" }
}
val codeFile =
"ktlint-api-consumer/src/main/kotlin/com/example/ktlint/api/consumer/KtlintApiConsumer.kt"
.let { fileName ->
LOGGER.info { "Read code from file '$fileName'" }
Code.fromFile(
File(fileName),
)
}
"format" -> {
ktLintRuleEngine
.format(codeFile)
.also {
LOGGER.info { "Code formatted by KtLint:\n$it" }
}
}
else -> LOGGER.error { "Unexpected argument '$command'" }

LOGGER.info {
"""
==============================================
Run ktlintRuleEngine.lint on sample code
---
""".trimIndent()
}
ktLintRuleEngine
.lint(codeFile) {
LOGGER.info { "LintViolation reported by KtLint: $it" }
}

LOGGER.info {
"""
==============================================
Run ktlintRuleEngine.format on sample code
---
""".trimIndent()
}
ktLintRuleEngine
.format(codeFile)
.also {
LOGGER.info { "Code formatted by KtLint:\n$it" }
}

LOGGER.info {
"""
==============================================
The amount and format of the logging is configured in file
'ktlint-api-consumer/src/main/resources/simplelogger.properties'
""".trimIndent()
}
}

This file was deleted.

40 changes: 40 additions & 0 deletions ktlint-api-consumer/src/main/resources/simplelogger.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SLF4J's SimpleLogger configuration file

# This file contains the configuration for the logging framework which is added as dependency of the module (see build.gradle.kts). When you
# use another framework than "slf4j-simple", the configuration below will not work. Check out the documentation of the chosen framework how
# it should be configured. Most likely, it has comparable capabilities.

# Default logging detail level for all instances of SimpleLogger.
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, defaults to "info".
#org.slf4j.simpleLogger.defaultLogLevel=warn

# Logging detail level which can either be set for a specific class or all classes in a specific package. If your project already contains
# a logger, you might want to set the logging for ktlint classes only.
#
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, the default logging detail level is used. Specifying another loglevel (for example the value "off") results in
# suppressing all logging for the class or package. Although this can be abused to suppress all logging from KtLint, you are advised no to
# do this. It is better to set the loglevel to "debug" to reduce all logging except the error messages that you don't want to miss.
org.slf4j.simpleLogger.log.com.pinterest.ktlint=debug
# Additional logging detail levels can be set. Although rule above suppresses all log messages at levels warn, info, debug and trace, the
# line below adds an exception for a specific class which might be relevant for you.
org.slf4j.simpleLogger.log.com.pinterest.ktlint.rule.engine.internal.EditorConfigDefaultsLoader=warn

# Set to true if you want the current date and time to be included in output messages.
# Default is false, and will output the number of milliseconds elapsed since startup.
#org.slf4j.simpleLogger.showDateTime=false

# The date and time format to be used in the output messages.
# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat.
# If the format is not specified or is invalid, the default format is used.
# The default format is yyyy-MM-dd HH:mm:ss:SSS Z.
#org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z

# Set to true if you want to output the current thread name.
# Defaults to true.
#org.slf4j.simpleLogger.showThreadName=true

# Set to true if you want the Logger instance name to be included in output messages.
# Defaults to true.
#org.slf4j.simpleLogger.showLogName=true
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ package com.pinterest.ktlint.logger.api
import mu.KLogger

/**
* Default modifier for the KLogger. It can be set only once via [setDefaultLoggerModifier] but it should be set before
* the first invocation of [initKtLintKLogger].
* Default modifier for the KLogger. It can be set only once via [setDefaultLoggerModifier] but it should be set before the first invocation
* of [initKtLintKLogger].
*/
private var defaultLoggerModifier: ((KLogger) -> Unit)? = null

/**
* Set the [defaultLoggerModifier]. Note that it can only be set once. It should be set before the first invocation to
* [initKtLintKLogger].
* Set the [defaultLoggerModifier]. Note that it can only be set once. It should be set before the first invocation to [initKtLintKLogger].
* Also note that it depends on the actual logging framework what capabilities can be set at runtime. The Ktlint CLI uses
* 'ch.qos.logback:logback-classic' as it allows the log level to be changed at run time. See the 'ktlint-api-consumer' module for an
* example that uses 'org.slf4j:slf4j-simple' that is configured via a properties file.
*/
public fun KLogger.setDefaultLoggerModifier(loggerModifier: (KLogger) -> Unit): KLogger {
if (defaultLoggerModifier != null) {
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ nav:
- IntelliJ IDEA configuration: rules/configuration-intellij-idea.md
- API:
- Overview: api/overview.md
- Custom integration: api/custom-integration.md
- Custom rule set: api/custom-rule-set.md
- Custom reporter: api/custom-reporter.md
- Badge: api/badge.md
Expand Down

0 comments on commit 8757eb6

Please sign in to comment.