Skip to content

Commit d0c98f8

Browse files
committed
feat: minor version strategy rules
1 parent e1864e3 commit d0c98f8

File tree

6 files changed

+242
-3
lines changed

6 files changed

+242
-3
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Minor version bump
2+
description: Verifies branch is ready to merge before minor version bump
3+
runs:
4+
using: composite
5+
steps:
6+
- name: Checkout repository
7+
uses: actions/checkout@v4
8+
9+
- name: Configure Gradle
10+
uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main
11+
12+
- name: Verify minor version bump
13+
shell: bash
14+
run: ./gradlew minorVersionBumpScan
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package aws.sdk.kotlin.gradle.dsl
7+
8+
import aws.sdk.kotlin.gradle.util.verifyRootProject
9+
import org.gradle.api.Project
10+
import org.gradle.api.attributes.Bundling
11+
import org.gradle.api.tasks.JavaExec
12+
import org.gradle.kotlin.dsl.*
13+
import org.gradle.language.base.plugins.LifecycleBasePlugin
14+
15+
/**
16+
* Configures Gradle tasks that run or fix minor-version-bump-specific Ktlint rules.
17+
*/
18+
fun Project.configureMinorVersionStrategyRules(lintPaths: List<String>) {
19+
verifyRootProject { "Task configuration is expected to be configured on the root project" }
20+
21+
val ktlintVersion = object {} // Can't use Project.javaClass because that's using the Gradle classloader
22+
.javaClass
23+
.getResource("ktlint-version.txt")
24+
?.readText()
25+
?: error("Missing ktlint-version.txt")
26+
27+
val minorVersionBumpKtlint by configurations.creating
28+
29+
dependencies {
30+
minorVersionBumpKtlint("com.pinterest.ktlint:ktlint-cli:$ktlintVersion") {
31+
attributes {
32+
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED))
33+
}
34+
}
35+
minorVersionBumpKtlint(project(":ktlint-rules:minor-version-strategy"))
36+
}
37+
38+
tasks.register<JavaExec>("minorVersionBumpScan") {
39+
group = LifecycleBasePlugin.VERIFICATION_GROUP
40+
description = "Check minor version bump rules"
41+
classpath = minorVersionBumpKtlint
42+
mainClass.set("com.pinterest.ktlint.Main")
43+
args = lintPaths
44+
}
45+
46+
tasks.register<JavaExec>("minorVersionBumpFix") {
47+
group = LifecycleBasePlugin.VERIFICATION_GROUP
48+
description = "Check minor version bump rules"
49+
classpath = minorVersionBumpKtlint
50+
mainClass.set("com.pinterest.ktlint.Main")
51+
args = listOf("-F") + lintPaths
52+
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
53+
}
54+
}

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ aws-sdk-cloudwatch = { module = "aws.sdk.kotlin:cloudwatch", version.ref = "aws-
1515
aws-sdk-s3 = { module = "aws.sdk.kotlin:s3", version.ref = "aws-sdk-version" }
1616
ktlint-cli = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
1717
ktlint-cli-ruleset-core = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlint" }
18-
ktlint-test = {module = "com.pinterest.ktlint:ktlint-test", version.ref = "ktlint" }
18+
ktlint-rule-engine = { module = "com.pinterest.ktlint:ktlint-rule-engine", version.ref = "ktlint" }
1919
nexus-publish-plugin = { module = "io.github.gradle-nexus:publish-plugin", version.ref = "nexus-plugin-version" }
2020
jreleaser-plugin = { module = "org.jreleaser:jreleaser-gradle-plugin", version.ref = "jreleaser-plugin-version" }
2121
smithy-model = { module = "software.amazon.smithy:smithy-model", version.ref = "smithy-version" }

ktlint-rules/build.gradle.kts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,31 @@ kotlin {
1616
sourceSets {
1717
main {
1818
dependencies {
19-
implementation(libs.ktlint.cli.ruleset.core)
19+
api(libs.ktlint.cli.ruleset.core)
2020
}
2121
}
2222

2323
test {
2424
dependencies {
25-
implementation(libs.ktlint.test)
25+
implementation(kotlin("test"))
26+
implementation(libs.ktlint.rule.engine)
27+
implementation("org.slf4j:slf4j-simple:2.0.7") // TODO: Move to libs
2628
}
2729
}
2830
}
2931
}
3032

33+
tasks.test {
34+
useJUnitPlatform()
35+
testLogging {
36+
events("passed", "skipped", "failed")
37+
showStandardStreams = true
38+
showStackTraces = true
39+
showExceptions = true
40+
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
41+
}
42+
}
43+
3144
tasks.withType<KotlinCompile> {
3245
compilerOptions {
3346
jvmTarget.set(JvmTarget.JVM_1_8)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.aws.ktlint.rules
7+
8+
import com.pinterest.ktlint.rule.engine.core.api.ElementType
9+
import com.pinterest.ktlint.rule.engine.core.api.Rule
10+
import com.pinterest.ktlint.rule.engine.core.api.RuleId
11+
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
12+
13+
/**
14+
* Matches @DeprecatedUntilVersion with either named args (major=x, minor=y) or positional args (x, y)
15+
*/
16+
internal fun deprecatedUntilVersionRegex(major: Int, minor: Int): Regex =
17+
Regex(
18+
"""@DeprecatedUntilVersion\s*\(\s*(?:major\s*=\s*$major\s*,\s*minor\s*=\s*$minor\s*|\s*$major\s*,\s*$minor\s*)\s*\)""",
19+
)
20+
21+
/**
22+
* Creates a ktlint rule that detects APIs annotated with @DeprecatedUntilVersion for the specified versions.
23+
* If autocorrect is enabled, the API will be deleted.
24+
*/
25+
fun apisScheduledForRemovalRule(major: Int, minor: Int): Rule =
26+
object : Rule(RuleId("minor-version-strategy:apis-scheduled-for-removal"), About()) {
27+
override fun beforeVisitChildNodes(
28+
node: ASTNode,
29+
autoCorrect: Boolean,
30+
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
31+
) {
32+
if (node.elementType == ElementType.ANNOTATION_ENTRY) {
33+
val regex = deprecatedUntilVersionRegex(major, minor)
34+
if (regex.containsMatchIn(node.text)) {
35+
emit(
36+
node.startOffset,
37+
"The deprecated API is scheduled for removal, please remove it before releasing the next minor version.",
38+
true,
39+
)
40+
41+
if (autoCorrect) {
42+
node.treeParent.treeParent?.let {
43+
it.treeParent.removeChild(it)
44+
}
45+
}
46+
}
47+
}
48+
}
49+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.aws.ktlint.rules
7+
8+
import com.pinterest.ktlint.rule.engine.api.Code
9+
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine
10+
import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision
11+
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
12+
import kotlin.test.Test
13+
import kotlin.test.assertEquals
14+
import kotlin.test.assertFalse
15+
import kotlin.test.assertTrue
16+
17+
class ApisScheduledForRemovalRuleTest {
18+
val ruleEngine = KtLintRuleEngine(
19+
ruleProviders = setOf(
20+
RuleProvider { apisScheduledForRemovalRule(1, 2) },
21+
),
22+
)
23+
24+
private fun runAutoCorrectTest(codeSnippet: String, expected: String) {
25+
val unformattedCode = Code.fromSnippet(codeSnippet)
26+
val formattedCode = ruleEngine
27+
.format(unformattedCode) { AutocorrectDecision.ALLOW_AUTOCORRECT }
28+
.trimStart()
29+
.trimEnd()
30+
31+
assertEquals(expected, formattedCode)
32+
}
33+
34+
@Test
35+
fun testAutoCorrect() {
36+
runAutoCorrectTest(
37+
"""
38+
@DeprecatedUntilVersion(1, 2)
39+
class Foo {
40+
fun foo() {}
41+
}
42+
43+
class Bar {
44+
fun bar() {}
45+
}
46+
""".trimIndent(),
47+
"""
48+
class Bar {
49+
fun bar() {}
50+
}
51+
""".trimIndent(),
52+
)
53+
54+
runAutoCorrectTest(
55+
"""
56+
@DeprecatedUntilVersion(1, 2)
57+
class Foo {
58+
fun foo() {}
59+
}
60+
""".trimIndent(),
61+
"""
62+
""".trimIndent(),
63+
)
64+
65+
runAutoCorrectTest(
66+
"""
67+
@DeprecatedUntilVersion(1, 2)
68+
fun foo() {
69+
// foo foo
70+
}
71+
72+
class Bar {
73+
fun bar() {}
74+
}
75+
""".trimIndent(),
76+
"""
77+
class Bar {
78+
fun bar() {}
79+
}
80+
""".trimIndent(),
81+
)
82+
}
83+
84+
fun runRegexTestCases(minor: Int, major: Int) {
85+
val regex = deprecatedUntilVersionRegex(major, minor)
86+
87+
assertTrue(regex.containsMatchIn("@DeprecatedUntilVersion($major,$minor)"))
88+
assertTrue(regex.containsMatchIn("@DeprecatedUntilVersion($major,$minor )"))
89+
assertTrue(regex.containsMatchIn("@DeprecatedUntilVersion($major, $minor)"))
90+
assertTrue(regex.containsMatchIn("@DeprecatedUntilVersion($major ,$minor)"))
91+
assertTrue(regex.containsMatchIn("@DeprecatedUntilVersion( $major,$minor)"))
92+
assertTrue(regex.containsMatchIn("@DeprecatedUntilVersion( $major , $minor )"))
93+
assertTrue(regex.containsMatchIn("@DeprecatedUntilVersion(major=$major,minor=$minor)"))
94+
assertTrue(regex.containsMatchIn("@DeprecatedUntilVersion( major= $major , minor= $minor )"))
95+
96+
assertFalse(regex.containsMatchIn("@DeprecatedUntilVersion"))
97+
assertFalse(regex.containsMatchIn("@DeprecatedUntilVersion()"))
98+
assertFalse(regex.containsMatchIn("@DeprecatedUntilVersion($minor,$minor)"))
99+
assertFalse(regex.containsMatchIn("@DeprecatedUntilVersion($major,$major)"))
100+
assertFalse(regex.containsMatchIn("@DeprecatedUntilVersion($minor,$major)"))
101+
}
102+
103+
@Test
104+
fun testRegex() {
105+
runRegexTestCases(0, 1)
106+
runRegexTestCases(1, 70)
107+
runRegexTestCases(100, 1_000_000)
108+
}
109+
}

0 commit comments

Comments
 (0)