Skip to content

Commit fdb9bc7

Browse files
[No Jira] Add Kotlin script to generate rule stubs (#4515)
1 parent ee37634 commit fdb9bc7

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#!/usr/bin/env kotlin
2+
3+
// README: To use this script you need to have kotlin installed.
4+
// You can do this e.g. with `sdk install kotlin <version>`.
5+
// This script has been tested with Kotlin 1.8.20.
6+
7+
import kotlin.io.path.createTempDirectory
8+
import kotlin.io.path.listDirectoryEntries
9+
import kotlin.io.path.name
10+
import kotlin.system.exitProcess
11+
12+
if (args.size < 2) {
13+
println("Usage: generate-rule-stubs.main.kts <rule-key> <check-name> [rspec-branch]")
14+
println(" For any RSPEC to be generated, you need to provide the path to the rule-api jar file in the RULE_API_JAR environment variable.")
15+
exitProcess(1)
16+
}
17+
18+
val javaChecksModulePath = __FILE__.absoluteFile.parentFile
19+
20+
val ruleKey = args[0]
21+
22+
val checkNameParts = args[1].let {
23+
if (it.endsWith(".java")) {
24+
println("ERROR: Do not append \".java\" to check name")
25+
exitProcess(2)
26+
} else if (!it.endsWith("Check")) {
27+
print("INFO: Appending \"Check\" to check name")
28+
it + "Check"
29+
} else {
30+
it
31+
}
32+
}.replace('/', '.').replace('\\', '.').split('.')
33+
34+
val checkQualifier = checkNameParts.dropLast(1)
35+
val checkName = checkNameParts.last()
36+
37+
val checkPath = (listOf("src", "main", "java", "org", "sonar", "java", "checks") + checkQualifier + "$checkName.java")
38+
.fold(javaChecksModulePath) { acc, part ->
39+
acc.resolve(part)
40+
}
41+
42+
val testPath = (listOf("src", "test", "java", "org", "sonar", "java", "checks") + checkQualifier + "${checkName}Test.java")
43+
.fold(javaChecksModulePath) { acc, part ->
44+
acc.resolve(part)
45+
}
46+
47+
val samplePath = (listOf("..", "java-checks-test-sources", "src", "main", "java", "checks") + checkQualifier + "${checkName}Sample.java")
48+
.fold(javaChecksModulePath) { acc, part ->
49+
acc.resolve(part)
50+
}
51+
52+
val samplePathForTest = if (checkQualifier.isEmpty()) {
53+
"checks/${checkName}Sample.java"
54+
} else {
55+
"checks/" + checkQualifier.joinToString("/") + "/${checkName}Sample.java"
56+
}
57+
58+
val pckg = listOf("org", "sonar", "java", "checks") + checkQualifier
59+
60+
if (checkPath.exists() || testPath.exists() || samplePath.exists()) {
61+
println("ERROR: Check already exists")
62+
exitProcess(3)
63+
}
64+
65+
println("INFO: Generating check stubs for $ruleKey with check name $checkName...")
66+
67+
checkPath.writeText(
68+
"""
69+
package ${pckg.joinToString(".")};
70+
71+
import java.util.List;
72+
import org.sonar.check.Rule;
73+
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
74+
import org.sonar.plugins.java.api.tree.Tree;
75+
76+
77+
@Rule(key = "$ruleKey")
78+
public class $checkName extends IssuableSubscriptionVisitor {
79+
80+
@Override
81+
public List<Tree.Kind> nodesToVisit() {
82+
// TODO: Specify the kind of nodes you want to be called to visit here.
83+
return List.of();
84+
}
85+
86+
@Override
87+
public void visitNode(Tree tree) {
88+
throw new UnsupportedOperationException("Not implemented yet");
89+
}
90+
91+
}
92+
93+
""".trimIndent()
94+
)
95+
96+
testPath.writeText(
97+
"""
98+
package ${pckg.joinToString(".")};
99+
100+
import org.junit.jupiter.api.Test;
101+
import org.sonar.java.checks.verifier.CheckVerifier;
102+
import org.sonar.java.checks.verifier.TestUtils;
103+
104+
class ${checkName}Test {
105+
106+
@Test
107+
void test() {
108+
CheckVerifier.newVerifier()
109+
.onFile(TestUtils.mainCodeSourcesPath("$samplePathForTest"))
110+
.withCheck(new $checkName())
111+
.verifyIssues();
112+
}
113+
114+
}
115+
116+
""".trimIndent()
117+
)
118+
119+
samplePath.writeText(
120+
"""
121+
package ${(listOf("checks") + checkQualifier).joinToString(".")};
122+
123+
public class ${checkName}Sample {
124+
// TODO: Implement the sample class
125+
}
126+
127+
""".trimIndent()
128+
)
129+
130+
// Add check to check list
131+
val checkListPath = listOf("src", "main", "java", "org", "sonar", "java", "checks", "CheckList.java")
132+
.fold(javaChecksModulePath) { acc, part ->
133+
acc.resolve(part)
134+
}
135+
136+
val remainingLines = checkListPath.readLines().toMutableList()
137+
val newLines = mutableListOf<String>()
138+
139+
if (checkQualifier.isNotEmpty()) {
140+
while (!remainingLines.first().startsWith("import")) newLines.add(remainingLines.removeFirst())
141+
142+
val newCheckImportLine = "import ${pckg.joinToString(".")}.${checkName};"
143+
while (remainingLines.first()
144+
.startsWith("import") && remainingLines.first().lowercase() < newCheckImportLine.lowercase()
145+
) newLines.add(remainingLines.removeFirst())
146+
newLines.add(newCheckImportLine)
147+
}
148+
149+
while (newLines.isEmpty() || !newLines.last().contains("// IssuableSubscriptionVisitor")) newLines.add(remainingLines.removeFirst())
150+
151+
val checkListLine = " $checkName.class,"
152+
while (remainingLines.first().endsWith(".class,") && remainingLines.first().lowercase() < checkListLine.lowercase()) newLines.add(remainingLines.removeFirst())
153+
newLines.add(checkListLine)
154+
155+
newLines.addAll(remainingLines)
156+
157+
checkListPath.writeText(newLines.joinToString("\n", postfix = "\n"))
158+
159+
// License headers using "mvn license:format"
160+
val mvnCmd = if(System.getProperty("os.name").lowercase().startsWith("windows")) {
161+
"mvn.cmd"
162+
} else {
163+
"mvn"
164+
}
165+
val runPath = javaChecksModulePath.parentFile
166+
167+
ProcessBuilder(mvnCmd, "license:format")
168+
.directory(runPath)
169+
.inheritIO()
170+
.start()
171+
.waitFor()
172+
173+
// Add to git
174+
ProcessBuilder("git", "add", checkPath.toString(), testPath.toString(), samplePath.toString())
175+
.directory(runPath)
176+
.inheritIO()
177+
.start()
178+
.waitFor()
179+
180+
// Rule API
181+
// First download latest version using maven
182+
val tmpDir = createTempDirectory("rules-stubs-gen-script")
183+
ProcessBuilder(mvnCmd, "dependency:copy", "-Dartifact=com.sonarsource.rule-api:rule-api:LATEST", "-DoutputDirectory=$tmpDir")
184+
.directory(runPath)
185+
.inheritIO()
186+
.start()
187+
.waitFor()
188+
189+
190+
val ruleApiJar = tmpDir.listDirectoryEntries().firstOrNull().let {
191+
if (it == null || !it.name.endsWith(".jar")) {
192+
System.err.println("ERROR: Could not download rule-api jar ($it)")
193+
tmpDir.toFile().deleteRecursively()
194+
exitProcess(4)
195+
} else {
196+
println("INFO: Using rule-api jar $it")
197+
it
198+
}
199+
}
200+
201+
val command = arrayOf("java", "-jar", ruleApiJar.toString(), "generate", "-rule", ruleKey).let {
202+
if (args.size > 2) it + "-branch" + args[2] else it
203+
}
204+
205+
ProcessBuilder(*command)
206+
.directory(runPath)
207+
.inheritIO()
208+
.start()
209+
.waitFor()
210+
211+
tmpDir.toFile().deleteRecursively()

0 commit comments

Comments
 (0)