Skip to content

Commit c6a9984

Browse files
author
Hugo Costa
committed
Add maxRuleVersion configuration for version-based rule filtering
Based on #942
1 parent 0525dbd commit c6a9984

File tree

8 files changed

+121
-5
lines changed

8 files changed

+121
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
77

88
## [Unreleased]
99

10+
- Add maxRuleVersion configuration for version-based rule filtering [#943](https://github.com/JLLeitschuh/ktlint-gradle/pull/943)
11+
1012
## [13.1.0] - 2025-08-21
1113

1214
- Gradle 9 Support [#937](https://github.com/JLLeitschuh/ktlint-gradle/pull/937)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ ktlint {
309309
"max_line_length": "20"
310310
]
311311
disabledRules = ["final-newline"] // not supported with ktlint 0.48+
312+
maxRuleVersion = "1.0.0" // Only include rules introduced up to this version
312313
baseline = file("my-project-ktlint-baseline.xml")
313314
reporters {
314315
reporter "plain"
@@ -366,6 +367,7 @@ configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
366367
)
367368
)
368369
disabledRules.set(setOf("final-newline")) // not supported with ktlint 0.48+
370+
maxRuleVersion.set("1.0.0") // Only include rules introduced up to this version
369371
baseline.set(file("my-project-ktlint-baseline.xml"))
370372
reporters {
371373
reporter(ReporterType.PLAIN)

plugin/src/adapter100/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintInvocation100.kt

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@ import com.pinterest.ktlint.rule.engine.api.EditorConfigPropertyRegistry
77
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine
88
import com.pinterest.ktlint.rule.engine.api.LintError
99
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
10+
import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint
11+
import net.swiftzer.semver.SemVer
1012
import java.io.File
1113
import java.util.ServiceLoader
1214

1315
class KtLintInvocation100(
1416
private val engine: KtLintRuleEngine
1517
) : KtLintInvocation {
1618
companion object Factory : KtLintInvocationFactory {
17-
fun initialize(editorConfigOverrides: Map<String, String>): KtLintInvocation {
18-
val ruleProviders = loadRuleSetsFromClasspathWithRuleSetProviderV3()
19+
fun initialize(
20+
editorConfigOverrides: Map<String, String>,
21+
maxRuleVersion: String? = null
22+
): KtLintInvocation {
23+
val ruleProviders = loadRuleSetsFromClasspathWithRuleSetProviderV3(maxRuleVersion)
1924
val editorConfigPropertyRegistry = EditorConfigPropertyRegistry(ruleProviders)
2025
val engine = if (editorConfigOverrides.isEmpty()) {
2126
KtLintRuleEngine(ruleProviders = ruleProviders)
@@ -34,11 +39,62 @@ class KtLintInvocation100(
3439
return KtLintInvocation100(engine)
3540
}
3641

37-
private fun loadRuleSetsFromClasspathWithRuleSetProviderV3(): Set<RuleProvider> {
38-
return ServiceLoader
42+
private fun loadRuleSetsFromClasspathWithRuleSetProviderV3(maxRuleVersion: String?): Set<RuleProvider> {
43+
val allRuleProviders = ServiceLoader
3944
.load(RuleSetProviderV3::class.java)
4045
.flatMap { it.getRuleProviders() }
4146
.toSet()
47+
48+
return if (maxRuleVersion != null) {
49+
filterRulesByVersion(allRuleProviders, maxRuleVersion)
50+
} else {
51+
allRuleProviders
52+
}
53+
}
54+
55+
private fun filterRulesByVersion(ruleProviders: Set<RuleProvider>, maxVersion: String): Set<RuleProvider> {
56+
return ruleProviders.filter { ruleProvider ->
57+
isRuleCompatibleWithVersion(ruleProvider, maxVersion)
58+
}.toSet()
59+
}
60+
61+
private fun isRuleCompatibleWithVersion(ruleProvider: RuleProvider, maxVersion: String): Boolean {
62+
// Use reflection to check for @SinceKtlint annotation
63+
val ruleClass = ruleProvider.createNewRuleInstance()::class.java
64+
val sinceAnnotation = ruleClass.getAnnotation(SinceKtlint::class.java)
65+
66+
return if (sinceAnnotation != null) {
67+
isVersionCompatible(sinceAnnotation.version, maxVersion)
68+
} else {
69+
// If no annotation, assume it's from an older ktlint version (before annotations were included
70+
// at runtime) and include it for backward compatibility
71+
true
72+
}
73+
}
74+
75+
/**
76+
* Checks if a rule version is compatible with the specified maximum version.
77+
*
78+
* A rule is considered compatible if its version is less than or equal to the maximum version.
79+
* Version comparison follows semantic versioning rules:
80+
* - "0.46" <= "1.0.0" → true
81+
* - "1.2.1" <= "1.2.0" → false
82+
* - "1.0" <= "1.0.0" → true (missing parts treated as 0)
83+
*
84+
* @param ruleVersion The version when the rule was introduced (from @SinceKtlint annotation)
85+
* @param maxVersion The maximum allowed version (from maxRuleVersion configuration)
86+
* @return true if the rule version is compatible (should be included), false otherwise
87+
*/
88+
private fun isVersionCompatible(ruleVersion: String, maxVersion: String): Boolean {
89+
return try {
90+
SemVer.parse(ruleVersion) <= SemVer.parse(maxVersion)
91+
} catch (_: Exception) {
92+
// If version parsing fails (invalid format), assume it's incompatible.
93+
// We would only get to this point if the user is using a version with the runtime annotations
94+
// and they are specifically asking for a lower version.
95+
// All standard ktlint rules should have valid version formats.
96+
false
97+
}
4298
}
4399
}
44100

plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/KtlintExtension.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ open class KtlintExtension @Inject internal constructor(
9999
set(false)
100100
}
101101

102+
/**
103+
* Only include rules that were introduced in KtLint versions up to and including this version.
104+
* When set, rules with @SinceKtlint annotations indicating they were added after this version will be excluded.
105+
* Rules without the annotation (from older ktlint versions) are always included for backward compatibility.
106+
*
107+
* Format: "0.46.0", "0.47.1", etc.
108+
* Default: null (no filtering - all rules are included)
109+
*
110+
* Example: `maxRuleVersion.set("0.46.0")` will exclude rules added in 0.47.0 and later.
111+
*/
112+
val maxRuleVersion: Property<String> = objectFactory.property(String::class.java)
113+
102114
/**
103115
* Provide additional `.editorconfig` properties, that are not in the project or project parent folders.
104116
*/

plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/TaskCreation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ private fun BaseKtLintCheckTask.configureBaseCheckTask(
136136
android.set(pluginHolder.extension.android)
137137
loadedReporters.set(pluginHolder.loadReportersTask.get().loadedReporters)
138138
enableExperimentalRules.set(pluginHolder.extension.enableExperimentalRules)
139+
maxRuleVersion.set(pluginHolder.extension.maxRuleVersion)
139140

140141
additionalTaskConfig()
141142
}

plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/tasks/BaseKtLintCheckTask.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.gradle.api.tasks.Input
2020
import org.gradle.api.tasks.InputFile
2121
import org.gradle.api.tasks.InputFiles
2222
import org.gradle.api.tasks.Internal
23+
import org.gradle.api.tasks.Optional
2324
import org.gradle.api.tasks.OutputFile
2425
import org.gradle.api.tasks.PathSensitive
2526
import org.gradle.api.tasks.PathSensitivity
@@ -80,6 +81,10 @@ abstract class BaseKtLintCheckTask @Inject constructor(
8081
@get:Input
8182
internal abstract val enableExperimentalRules: Property<Boolean>
8283

84+
@get:Input
85+
@get:Optional
86+
internal abstract val maxRuleVersion: Property<String>
87+
8388
/**
8489
* Max lint worker heap size. Default is "256m".
8590
*/
@@ -279,6 +284,7 @@ abstract class BaseKtLintCheckTask @Inject constructor(
279284
ktLintVersion.set(task.ktLintVersion)
280285
editorconfigFilesWereChanged.set(editorConfigUpdated)
281286
this.formatSnapshot.set(formatSnapshot)
287+
maxRuleVersion.set(task.maxRuleVersion)
282288
}
283289
}
284290

plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintWorkAction.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ abstract class KtLintWorkAction : WorkAction<KtLintWorkAction.KtLintWorkParamete
2929
val ktlintInvokerFactory = selectInvocation(parameters.ktLintVersion.get())
3030
) {
3131
is KtLintInvocation100.Factory -> {
32-
ktlintInvokerFactory.initialize(parameters.additionalEditorconfig.get())
32+
ktlintInvokerFactory.initialize(
33+
parameters.additionalEditorconfig.get(),
34+
parameters.maxRuleVersion.orNull
35+
)
3336
}
3437

3538
else -> {
@@ -97,6 +100,7 @@ abstract class KtLintWorkAction : WorkAction<KtLintWorkAction.KtLintWorkParamete
97100
val ktLintVersion: Property<String>
98101
val editorconfigFilesWereChanged: Property<Boolean>
99102
val formatSnapshot: RegularFileProperty
103+
val maxRuleVersion: Property<String>
100104
}
101105

102106
/**

plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/KtlintPluginTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,4 +716,37 @@ dependencies {
716716
}
717717
}
718718
}
719+
720+
@DisplayName("Should allow setting maxRuleVersion")
721+
@CommonTest
722+
fun allowMaxRuleVersionConfiguration(gradleVersion: GradleVersion) {
723+
project(gradleVersion) {
724+
withCleanSources()
725+
//language=Groovy
726+
buildGradle.appendText(
727+
"""
728+
729+
ktlint {
730+
maxRuleVersion = "1.0.0"
731+
}
732+
""".trimIndent()
733+
)
734+
735+
build(CHECK_PARENT_TASK_NAME) {
736+
assertThat(task(":$mainSourceSetCheckTaskName")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
737+
}
738+
}
739+
}
740+
741+
@DisplayName("Should work without maxRuleVersion set")
742+
@CommonTest
743+
fun workWithoutMaxRuleVersion(gradleVersion: GradleVersion) {
744+
project(gradleVersion) {
745+
withCleanSources()
746+
747+
build(CHECK_PARENT_TASK_NAME) {
748+
assertThat(task(":$mainSourceSetCheckTaskName")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
749+
}
750+
}
751+
}
719752
}

0 commit comments

Comments
 (0)