|
| 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