Skip to content

Commit 7215cb0

Browse files
authored
feat: plannedRemoval ktlint rule (#136)
1 parent 9677f96 commit 7215cb0

File tree

12 files changed

+217
-55
lines changed

12 files changed

+217
-55
lines changed

gradle/libs.versions.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
[versions]
22
aws-sdk-version = "1.5.26"
33
kotlin-version = "2.2.0"
4-
ktlint = "1.3.0"
4+
ktlint-version = "1.3.0"
55
nexus-plugin-version = "2.0.0"
66
jreleaser-plugin-version = "1.18.0"
77
publish-plugin-version = "1.3.1"
88
smithy-version = "1.60.2"
99
smithy-gradle-plugin-version = "1.3.0"
1010
junit-version = "5.10.1"
1111
coroutines-version = "1.10.2"
12+
slf4j-version = "2.0.17"
1213

1314
[libraries]
1415
aws-sdk-cloudwatch = { module = "aws.sdk.kotlin:cloudwatch", version.ref = "aws-sdk-version" }
1516
aws-sdk-s3 = { module = "aws.sdk.kotlin:s3", version.ref = "aws-sdk-version" }
16-
ktlint-cli = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
17-
ktlint-cli-ruleset-core = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlint" }
17+
ktlint-cli = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint-version" }
18+
ktlint-cli-ruleset-core = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlint-version" }
19+
ktlint-rule-engine = { module = "com.pinterest.ktlint:ktlint-rule-engine", version.ref = "ktlint-version" }
1820
nexus-publish-plugin = { module = "io.github.gradle-nexus:publish-plugin", version.ref = "nexus-plugin-version" }
1921
jreleaser-plugin = { module = "org.jreleaser:jreleaser-gradle-plugin", version.ref = "jreleaser-plugin-version" }
2022
smithy-model = { module = "software.amazon.smithy:smithy-model", version.ref = "smithy-version" }
2123
smithy-gradle-base-plugin = { module = "software.amazon.smithy.gradle:smithy-base", version.ref = "smithy-gradle-plugin-version" }
2224
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-version" }
2325
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines-version" }
26+
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j-version" }
2427

2528
[plugins]
2629
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" }

ktlint/minor-version-rules/src/main/kotlin/software/aws/ktlint/rules/MinorVersionRuleSetProvider.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
88
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
99
import com.pinterest.ktlint.rule.engine.core.api.RuleSetId
1010

11-
class MinorVersionRuleSetProvider : RuleSetProviderV3(RuleSetId("minor-version-strategy-rules")) {
11+
internal const val RULE_SET = "minor-version-strategy-rules"
12+
13+
class MinorVersionRuleSetProvider : RuleSetProviderV3(RuleSetId(RULE_SET)) {
1214
override fun getRuleProviders() = setOf(
13-
RuleProvider { DeprecatedApiRule() },
15+
RuleProvider { PlannedRemovalRule() },
1416
)
1517
}
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ import java.io.File
1313
import java.util.Properties
1414

1515
/**
16-
* Matches @DeprecatedUntilVersion with either named args (major=x, minor=y) or positional args (x, y)
16+
* Matches @PlannedRemoval with either named args (major=x, minor=y) or positional args (x, y)
1717
*/
18-
internal fun deprecatedUntilVersionRegex(major: Int, minor: Int): Regex =
18+
internal fun plannedRemovalRegex(major: Int, minor: Int): Regex =
1919
Regex(
20-
"""@DeprecatedUntilVersion\s*\(\s*(?:major\s*=\s*$major\s*,\s*minor\s*=\s*$minor\s*|\s*$major\s*,\s*$minor\s*)\s*\)""",
20+
"""@PlannedRemoval\s*\(\s*(?:major\s*=\s*$major\s*,\s*minor\s*=\s*$minor\s*|\s*$major\s*,\s*$minor\s*)\s*\)""",
2121
)
2222

2323
/**
24-
* Creates a ktlint rule that detects APIs annotated with @DeprecatedUntilVersion for the upcoming minor version.
24+
* Creates a ktlint rule that detects APIs annotated with @PlannedRemoval for the upcoming minor version.
2525
*/
26-
class DeprecatedApiRule : Rule(RuleId("minor-version-strategy-rules:deprecated-apis"), About()) {
26+
class PlannedRemovalRule : Rule(RuleId("$RULE_SET:planned-removal"), About()) {
2727
override fun beforeVisitChildNodes(
2828
node: ASTNode,
2929
autoCorrect: Boolean,
@@ -38,12 +38,12 @@ class DeprecatedApiRule : Rule(RuleId("minor-version-strategy-rules:deprecated-a
3838
val majorVersion = sdkVersion[0].toInt()
3939
val minorVersion = sdkVersion[1].toInt()
4040

41-
val regex = deprecatedUntilVersionRegex(majorVersion, minorVersion + 1)
41+
val regex = plannedRemovalRegex(majorVersion, minorVersion + 1)
4242
if (regex.containsMatchIn(node.text)) {
4343
emit(
4444
node.startOffset,
45-
"The deprecated API is scheduled for removal, please remove it before releasing the next minor version.",
46-
true,
45+
"API is scheduled for removal, remove it before releasing the next minor version.",
46+
false,
4747
)
4848
}
4949
}

ktlint/minor-version-rules/src/test/kotlin/software/aws/ktlint/rules/DeprecatedApiRuleTest.kt

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 kotlin.test.Test
9+
import kotlin.test.assertFalse
10+
import kotlin.test.assertTrue
11+
12+
class PlannedRemovalRuleTest {
13+
fun runRegexTestCases(minor: Int, major: Int) {
14+
val regex = plannedRemovalRegex(major, minor)
15+
16+
assertTrue(regex.containsMatchIn("@PlannedRemoval($major,$minor)"))
17+
assertTrue(regex.containsMatchIn("@PlannedRemoval($major,$minor )"))
18+
assertTrue(regex.containsMatchIn("@PlannedRemoval($major, $minor)"))
19+
assertTrue(regex.containsMatchIn("@PlannedRemoval($major ,$minor)"))
20+
assertTrue(regex.containsMatchIn("@PlannedRemoval( $major,$minor)"))
21+
assertTrue(regex.containsMatchIn("@PlannedRemoval( $major , $minor )"))
22+
assertTrue(regex.containsMatchIn("@PlannedRemoval(major=$major,minor=$minor)"))
23+
assertTrue(regex.containsMatchIn("@PlannedRemoval( major= $major , minor= $minor )"))
24+
25+
assertFalse(regex.containsMatchIn("@PlannedRemoval"))
26+
assertFalse(regex.containsMatchIn("@PlannedRemoval()"))
27+
assertFalse(regex.containsMatchIn("@PlannedRemoval($minor,$minor)"))
28+
assertFalse(regex.containsMatchIn("@PlannedRemoval($major,$major)"))
29+
assertFalse(regex.containsMatchIn("@PlannedRemoval($minor,$major)"))
30+
}
31+
32+
@Test
33+
fun testRegex() {
34+
runRegexTestCases(0, 1)
35+
runRegexTestCases(1, 70)
36+
runRegexTestCases(100, 1_000_000)
37+
}
38+
}

ktlint/style-rules/build.gradle.kts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ kotlin {
1919
implementation(libs.ktlint.cli.ruleset.core)
2020
}
2121
}
22+
test {
23+
dependencies {
24+
implementation(kotlin("test"))
25+
implementation(libs.ktlint.rule.engine)
26+
implementation(libs.slf4j.simple) // Required by ktlint rule engine tests
27+
}
28+
}
29+
}
30+
}
31+
32+
tasks.test {
33+
testLogging {
34+
events("passed", "skipped", "failed")
35+
showStandardStreams = true
36+
showStackTraces = true
37+
showExceptions = true
38+
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
2239
}
2340
}
2441

ktlint/style-rules/src/main/kotlin/software/aws/ktlint/rules/CopyrightHeaderRule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiCommentImpl
1111
import org.jetbrains.kotlin.lexer.KtTokens
1212
import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType
1313

14-
class CopyrightHeaderRule : Rule(RuleId("aws-kotlin-repo-tools-rules:copyright-header"), About()) {
14+
class CopyrightHeaderRule : Rule(RuleId("$RULE_SET:copyright-header"), About()) {
1515
companion object {
1616
private val header = """
1717
/*

ktlint/style-rules/src/main/kotlin/software/aws/ktlint/rules/ExpressionBodyRule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import org.jetbrains.kotlin.psi.KtBlockExpression
1111
import org.jetbrains.kotlin.psi.KtNamedFunction
1212
import org.jetbrains.kotlin.psi.KtReturnExpression
1313

14-
class ExpressionBodyRule : Rule(RuleId("aws-kotlin-repo-tools-rules:expression-body"), About()) {
14+
class ExpressionBodyRule : Rule(RuleId("$RULE_SET:expression-body"), About()) {
1515
override fun beforeVisitChildNodes(
1616
node: ASTNode,
1717
autoCorrect: Boolean,

ktlint/style-rules/src/main/kotlin/software/aws/ktlint/rules/MultilineIfElseBlockRule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import com.pinterest.ktlint.rule.engine.core.api.Rule
99
import com.pinterest.ktlint.rule.engine.core.api.RuleId
1010
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
1111

12-
class MultilineIfElseBlockRule : Rule(RuleId("aws-kotlin-repo-tools-rules:multiline-if-else-block"), About()) {
12+
class MultilineIfElseBlockRule : Rule(RuleId("$RULE_SET:multiline-if-else-block"), About()) {
1313
override fun beforeVisitChildNodes(
1414
node: ASTNode,
1515
autoCorrect: Boolean,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
* Creates a ktlint rule that forces APIs annotated with @PlannedRemoval to also be annotated with @Deprecated.
15+
*/
16+
class PlannedRemovalRule : Rule(RuleId("$RULE_SET:planned-removal"), About()) {
17+
override fun beforeVisitChildNodes(
18+
node: ASTNode,
19+
autoCorrect: Boolean,
20+
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
21+
) {
22+
if (node.elementType == ElementType.MODIFIER_LIST) {
23+
val annotations = node.getChildren(null).filter { it.elementType == ElementType.ANNOTATION_ENTRY }
24+
val deprecated = annotations.any { it.text.startsWith("@Deprecated") }
25+
val plannedRemoval = annotations.any { it.text.startsWith("@PlannedRemoval") }
26+
27+
if (plannedRemoval && !deprecated) {
28+
emit(
29+
node.startOffset,
30+
"APIs annotated with @PlannedRemoval must also be annotated with @Deprecated",
31+
false,
32+
)
33+
}
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)