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

Define EditorConfigOverride for dynamically loaded ruleset #2194

Merged
merged 5 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ This project adheres to [Semantic Versioning](https://semver.org/).

Class "org.jetbrains.kotlin.com.intellij.treeCopyHandler" is no longer registered as extension point for the compiler as this is not supported in Kotlin 1.9. Please test your custom rules. In case of unexpected exceptions during formatting of code, see [#2044](https://github.com/pinterest/ktlint/pull/2044) for possible remediation.

#### EditorConfigOverride - Ktlint API Consumer example

The `EditorConfigOverride` parameter of the `KtlintRuleEngine` can be defined using the factory method `EditorConfigOverride.from(vararg properties: Pair<EditorConfigProperty<*>, *>)`. This requires the `EditorConfigProperty`'s to be available at compile time. Some common `EditorConfigProperty`'s are defined in `ktlint-rule-engine-core` which is loaded as transitive dependency of `ktlint-rule-engine` and as of that are available at compile.

If an `EditorConfigProperty` is defined in a `Rule` that is only provided via a runtime dependency, it gets a bit more complicated. The `ktlint-api-consumer` example has now been updated to show how the `EditorConfigProperty` can be retrieved from the `Rule`.

### Added

* Add experimental rule `class-signature`. This rule rewrites the class header to a consistent format. In code style `ktlint_official`, super types are always wrapped to a separate line. In other code styles, super types are only wrapped in classes having multiple super types. Especially for code style `ktlint_official` the class headers are rewritten in a more consistent format. See [examples in documentation](https://pinterest.github.io/ktlint/latest/rules/experimental/#class-signature). `class-signature` [#875](https://github.com/pinterest/ktlint/issues/1349), [#1349](https://github.com/pinterest/ktlint/issues/875)
* Add experimental rule `function-expression-body`. This rule rewrites function bodies only contain a `return` or `throw` expression to an expression body. [#2150](https://github.com/pinterest/ktlint/issues/2150)
* Add new experimental rule `statement-wrapping` which ensures function, class, or other blocks statement body doesn't start or end at starting or ending braces of the block ([#1938](https://github.com/pinterest/ktlint/issues/1938)). This rule was added in `0.50` release, but was never executed outside the unit tests. The rule is now added to the `StandardRuleSetProvider` ([#2170](https://github.com/pinterest/ktlint/issues/2170))
* Add experimental rule `chain-method-continuation` to the `ktlint_official` code style, but it can be enabled explicitly for the other code styles as well. This rule requires the operators (`.` or `?.`) for chaining method calls, to be aligned with each other. This rule is enabled by ([#1953](https://github.com/pinterest/ktlint/issues/1953))
* Add EditorConfigPropertyRegistry to assist API Consumers that load rulesets at runtime to define the EditorConfigOverride ([#2190](https://github.com/pinterest/ktlint/issues/2190))

### Removed

Expand Down
17 changes: 14 additions & 3 deletions ktlint-api-consumer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,20 @@ dependencies {

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
// into a new rule set.
implementation(projects.ktlintRulesetStandard)

// If the API consumer depends on a fixed set of ruleset, it might be best to provide those dependencies at compile time. In this way
// statically typing can be used when defining the EditorConfigOverride for the KtlintRuleEngine. However, in this example, the
// dependencies are provided at runtime.
// implementation(projects.ktlintRulesetStandard)

// For advanced use cases, the API consumer might prefer to provide the ruleset dependencies at runtime and load them dynamically using
// the RuleSetProvider of ktlint-cli-ruleset-core.
implementation(projects.ktlintCliRulesetCore)
runtimeOnly(projects.ktlintRulesetStandard)

// The standard ruleset is also provided as test dependency to demonstrate that rules that are provided at compile time can also be unit
// tested.
testImplementation(projects.ktlintRulesetStandard)

testImplementation(projects.ktlintTest)
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
package com.example.ktlint.api.consumer

import com.example.ktlint.api.consumer.rules.KTLINT_API_CONSUMER_RULE_PROVIDERS
import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
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.EditorConfigPropertyRegistry
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.EXPERIMENTAL_RULES_EXECUTION_PROPERTY
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.editorconfig.RuleExecution
import com.pinterest.ktlint.rule.engine.core.api.propertyTypes
import io.github.oshai.kotlinlogging.KotlinLogging
import java.io.File
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Paths
import java.util.ServiceConfigurationError
import java.util.ServiceLoader

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

Expand All @@ -22,19 +30,54 @@ private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger()
* needs.
*/
public fun main() {
// RuleProviders can be supplied by a custom rule provider of the API Consumer itself. This custom rule provider by definition can only
// provide rules that are available at compile time. But it is also possible to provide rules from jars that are loaded on runtime.
val ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS.plus(runtimeLoadedRuleProviders)

val editorConfigPropertyRegistry = EditorConfigPropertyRegistry(ruleProviders)

// Providing rule dependencies at compile time has the advantage that properties defined in those rules can be used to build the
// EditorConfigOverride using static types. However, providing the rule dependencies at runtime can offer flexibility to the API
// Consumer. It comes with the cost that the EditorConfigOverride can not be build with static types.
val editorConfigOverride =
EditorConfigOverride
.from(
// Properties provided by ktlint-rule-engine-core are best to be loaded statically as they are available at compile time as
// they are provided by the ktlint-rule-engine-core module.
INDENT_STYLE_PROPERTY to IndentConfig.IndentStyle.SPACE,
INDENT_SIZE_PROPERTY to 4,
EXPERIMENTAL_RULES_EXECUTION_PROPERTY to RuleExecution.enabled,
//
// Properties defined in the ktlint-ruleset-standard can only be loaded statically when that dependency is provided at
// compile time. In this example project this ruleset is loaded at runtime, so following decommenting next line results in
// a compilation error:
// FUNCTION_BODY_EXPRESSION_WRAPPING_PROPERTY to always
//
// For properties that are defined in rules for which the dependency is provided at runtime only, the property name can be
// provided as String and the value as Any type. As the property is not available at compile time, the value has to be
// specified as a String (exactly as would be done in the `.editorconfig` file).
// In case the property value is invalid, the KtlintRuleEngine logs a warning.
editorConfigPropertyRegistry.find("ktlint_function_signature_body_expression_wrapping") to "always",
//
// The properties for enabling/disabling a rule or entire rule set can be set as well. Note that the values of this
// property can be set via the `RuleExecution` enum which is available at compile time as it is provided by the
// ktlint-rule-engine-core module.
editorConfigPropertyRegistry.find("ktlint_standard_function-signature") to RuleExecution.disabled,
editorConfigPropertyRegistry.find("ktlint_standard") to RuleExecution.disabled,
//
// In case an unknown property is provided, an exception is thrown:
// ruleProviders.findEditorConfigProperty("unknown_property") to "some-value",
)

// The KtLint RuleEngine only needs to be instantiated once and can be reused in multiple invocations
val ktLintRuleEngine =
val apiConsumerKtLintRuleEngine =
KtLintRuleEngine(
ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS,
editorConfigOverride =
EditorConfigOverride.from(
INDENT_STYLE_PROPERTY to IndentConfig.IndentStyle.SPACE,
INDENT_SIZE_PROPERTY to 4,
),
ruleProviders = ruleProviders,
editorConfigOverride = editorConfigOverride,
editorConfigDefaults =
EditorConfigDefaults.load(
path = Paths.get("/some/path/to/editorconfig/file/or/directory"),
propertyTypes = KTLINT_API_CONSUMER_RULE_PROVIDERS.propertyTypes(),
propertyTypes = ruleProviders.propertyTypes(),
),
)

Expand All @@ -55,7 +98,7 @@ public fun main() {
---
""".trimIndent()
}
ktLintRuleEngine
apiConsumerKtLintRuleEngine
.lint(codeFile) {
LOGGER.info { "LintViolation reported by KtLint: $it" }
}
Expand All @@ -68,7 +111,7 @@ public fun main() {
---
""".trimIndent()
}
ktLintRuleEngine
apiConsumerKtLintRuleEngine
.format(codeFile)
.also {
LOGGER.info { "Code formatted by KtLint:\n$it" }
Expand All @@ -83,3 +126,16 @@ public fun main() {
""".trimIndent()
}
}

private val runtimeLoadedRuleProviders =
try {
ServiceLoader
.load(
RuleSetProviderV3::class.java,
URLClassLoader(emptyArray<URL?>()),
).flatMap { it.getRuleProviders() }
.toSet()
} catch (e: ServiceConfigurationError) {
LOGGER.warn { "Error while loading the rulesets:\n${e.printStackTrace()}" }
emptySet()
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.example.ktlint.api.consumer.rules

import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
import com.pinterest.ktlint.ruleset.standard.rules.IndentationRule

internal val CUSTOM_RULE_SET_ID = "custom-rule-set-id"

internal val KTLINT_API_CONSUMER_RULE_PROVIDERS =
setOf(
// Can provide custom rules
RuleProvider { NoVarRule() },
// but also reuse rules from KtLint rulesets
RuleProvider { IndentationRule() },
// If rulesets are include at compile time, they can be added to the custom rule provider.
// RuleProvider { IndentationRule() },
)
2 changes: 2 additions & 0 deletions ktlint-rule-engine-core/api/ktlint-rule-engine-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ public final class com/pinterest/ktlint/rule/engine/core/api/RuleId {
}

public final class com/pinterest/ktlint/rule/engine/core/api/RuleId$Companion {
public final fun isValid (Ljava/lang/String;)Z
public final fun prefixWithStandardRuleSetIdWhenMissing (Ljava/lang/String;)Ljava/lang/String;
}

Expand Down Expand Up @@ -471,6 +472,7 @@ public final class com/pinterest/ktlint/rule/engine/core/api/RuleSetId {

public final class com/pinterest/ktlint/rule/engine/core/api/RuleSetId$Companion {
public final fun getSTANDARD ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleSetId;
public final fun isValid (Ljava/lang/String;)Z
}

public final class com/pinterest/ktlint/rule/engine/core/api/editorconfig/CodeStyleEditorConfigPropertyKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class RuleId(
} else {
"${RuleSetId.STANDARD.value}$DELIMITER$id"
}

public fun isValid(value: String): Boolean = IdNamingPolicy.isValidRuleId(value)
}
}

Expand All @@ -45,6 +47,8 @@ public class RuleSetId(
* maintenance of the rule (set).
*/
public val STANDARD: RuleSetId = RuleSetId("standard")

public fun isValid(value: String): Boolean = IdNamingPolicy.isValidRuleSetId(value)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ internal object IdNamingPolicy {
* Will throw [IllegalArgumentException] on invalid [ruleId] name.
*/
internal fun enforceRuleIdNaming(ruleId: String) =
require(ruleId.matches(RULE_ID_REGEX)) { "Rule with id '$ruleId' must match regexp '${RULE_ID_REGEX.pattern}'" }
require(isValidRuleId(ruleId)) { "Rule with id '$ruleId' must match regexp '${RULE_ID_REGEX.pattern}'" }

internal fun isValidRuleId(ruleId: String) = ruleId.matches(RULE_ID_REGEX)

/**
* Checks provided [ruleSetId] is valid.
*
* Will throw [IllegalArgumentException] on invalid [ruleSetId] name.
*/
internal fun enforceRuleSetIdNaming(ruleSetId: String) =
require(ruleSetId.matches(RULE_SET_ID_REGEX)) { "Rule set id '$ruleSetId' must match '${RULE_SET_ID_REGEX.pattern}'" }
require(isValidRuleSetId(ruleSetId)) { "Rule set id '$ruleSetId' must match '${RULE_SET_ID_REGEX.pattern}'" }

internal fun isValidRuleSetId(ruleSetId: String) = ruleSetId.matches(RULE_SET_ID_REGEX)
}
9 changes: 9 additions & 0 deletions ktlint-rule-engine/api/ktlint-rule-engine.api
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ public final class com/pinterest/ktlint/rule/engine/api/EditorConfigOverride$Com
public final fun plus (Lcom/pinterest/ktlint/rule/engine/api/EditorConfigOverride;[Lkotlin/Pair;)Lcom/pinterest/ktlint/rule/engine/api/EditorConfigOverride;
}

public final class com/pinterest/ktlint/rule/engine/api/EditorConfigPropertyNotFoundException : java/lang/RuntimeException {
public fun <init> (Ljava/lang/String;)V
}

public final class com/pinterest/ktlint/rule/engine/api/EditorConfigPropertyRegistry {
public fun <init> (Ljava/util/Set;)V
public final fun find (Ljava/lang/String;)Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigProperty;
}

public final class com/pinterest/ktlint/rule/engine/api/KtLintParseException : java/lang/RuntimeException {
public fun <init> (IILjava/lang/String;)V
public final fun getCol ()I
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProper
import org.ec4j.core.model.PropertyType.PropertyValue

/**
* The [EditorConfigOverride] allows to add or replace properties which are loaded from the ".editorconfig" file. It
* serves two purposes.
* The [EditorConfigOverride] allows to add or replace properties which are loaded from the ".editorconfig" file. It serves two purposes.
*
* Firstly, the [EditorConfigOverride] can be used by API consumers to run a rule with values which are not actually
* save to the ".editorconfig" file. When doing so, this should be clearly communicated to their consumers who will
* expect the settings in that file to be respected.
* Firstly, the [EditorConfigOverride] can be used by API consumers to run a rule with values which are not actually saved to the
* ".editorconfig" file. When doing so, this should be clearly communicated to their consumers who will expect the settings in that file to
* be respected.
*
* Secondly, the [EditorConfigOverride] is used in unit tests, to test a rule with distinct values of a property without
* having to access an ".editorconfig" file from physical storage. This also improves readability of the tests.
* Secondly, the [EditorConfigOverride] is used in unit tests, to test a rule with distinct values of a property without having to access an
* ".editorconfig" file from physical storage. This also improves readability of the tests.
*/
public class EditorConfigOverride {
private val _properties = mutableMapOf<EditorConfigProperty<*>, PropertyValue<*>>()
Expand All @@ -37,7 +36,8 @@ public class EditorConfigOverride {

public companion object {
/**
* Creates the [EditorConfigOverride] based on one or more property-value mappings.
* Creates the [EditorConfigOverride] based on one or more property-value mappings. In case rule sets are only loaded at runtime,
* you can use [EditorConfigPropertyRegistry] to retrieve the [EditorConfigProperty] for which a value is to be overridden.
*/
public fun from(vararg properties: Pair<EditorConfigProperty<*>, *>): EditorConfigOverride {
require(properties.isNotEmpty()) {
Expand Down
Loading