|
| 1 | +package datadog.gradle.plugin.lint |
| 2 | + |
| 3 | +import org.gradle.api.Plugin |
| 4 | +import org.gradle.api.Project |
| 5 | + |
| 6 | +class UnnecessaryElseLinter : Plugin<Project> { |
| 7 | + override fun apply(target: Project) { |
| 8 | + target.tasks.register("checkUnnecessaryElse") { |
| 9 | + group = "verification" |
| 10 | + description = "Scan changed Java files for unnecessary else blocks after return/throw/continue/break" |
| 11 | + |
| 12 | + doLast { |
| 13 | + val baseSha = "9b933669729ea8a7af00f5cf3c36b6720ec433bd" |
| 14 | + val changedFiles = getChangedJavaFiles(target, baseSha) |
| 15 | + val repoRoot = target.rootProject.projectDir.toPath() |
| 16 | + val warnings = mutableListOf<String>() |
| 17 | + |
| 18 | + changedFiles.forEach { file -> |
| 19 | + if (file.exists()) { |
| 20 | + val relPath = repoRoot.relativize(file.toPath()).toString() |
| 21 | + checkFile(file, relPath, warnings) |
| 22 | + } |
| 23 | + } |
| 24 | + |
| 25 | + if (warnings.isNotEmpty()) { |
| 26 | + warnings.forEach { target.logger.warn(it) } |
| 27 | + target.logger.warn("Found ${warnings.size} unnecessary else block(s) — advisory only.") |
| 28 | + } else { |
| 29 | + target.logger.info("No unnecessary else blocks found in changed files.") |
| 30 | + } |
| 31 | + } |
| 32 | + } |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +private fun getChangedJavaFiles(project: Project, baseSha: String): List<java.io.File> { |
| 37 | + return try { |
| 38 | + val process = ProcessBuilder("git", "diff", "--name-only", baseSha, "HEAD") |
| 39 | + .directory(project.rootProject.projectDir) |
| 40 | + .redirectErrorStream(true) |
| 41 | + .start() |
| 42 | + val output = process.inputStream.bufferedReader().readText() |
| 43 | + process.waitFor() |
| 44 | + output.trim().lines() |
| 45 | + .filter { it.isNotBlank() && it.endsWith(".java") } |
| 46 | + .map { project.rootProject.projectDir.resolve(it) } |
| 47 | + .filter { it.exists() } |
| 48 | + } catch (e: Exception) { |
| 49 | + project.logger.warn("checkUnnecessaryElse: could not get changed files — ${e.message}") |
| 50 | + emptyList() |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +private fun checkFile(file: java.io.File, relPath: String, warnings: MutableList<String>) { |
| 55 | + val lines = file.readLines() |
| 56 | + val elsePattern = Regex("""^\s*\}\s*else\s*(\{.*)?$""") |
| 57 | + |
| 58 | + for (i in lines.indices) { |
| 59 | + if (!elsePattern.containsMatchIn(lines[i])) continue |
| 60 | + if (isUnnecessaryElse(lines, i)) { |
| 61 | + warnings.add("STYLE: $relPath:${i + 1} — unnecessary else after return/throw/continue/break") |
| 62 | + } |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +private fun isUnnecessaryElse(lines: List<String>, elseLineIdx: Int): Boolean { |
| 67 | + // Walk backwards from "} else {" to find the last meaningful statement |
| 68 | + var j = elseLineIdx - 1 |
| 69 | + while (j >= 0) { |
| 70 | + val trimmed = lines[j].trim() |
| 71 | + if (trimmed.isEmpty() || isCommentLine(trimmed)) { |
| 72 | + j-- |
| 73 | + continue |
| 74 | + } |
| 75 | + // The last non-empty line before "} else {" must end with ";" to be a statement boundary |
| 76 | + if (!trimmed.endsWith(";")) return false |
| 77 | + |
| 78 | + // Check if this single line is itself an exit statement |
| 79 | + if (isExitStatement(trimmed)) return true |
| 80 | + |
| 81 | + // For multi-line statements (e.g. multi-line return/throw), walk further back |
| 82 | + // looking for the start of the statement (a line not ending with a continuation) |
| 83 | + var k = j - 1 |
| 84 | + while (k >= 0) { |
| 85 | + val prev = lines[k].trim() |
| 86 | + if (prev.isEmpty() || isCommentLine(prev)) { |
| 87 | + k-- |
| 88 | + continue |
| 89 | + } |
| 90 | + // Another complete statement or block boundary — stop |
| 91 | + if (prev.endsWith(";") || prev.endsWith("{") || prev.endsWith("}")) return false |
| 92 | + // This line is a continuation of the same statement |
| 93 | + if (startsWithExitKeyword(prev)) return true |
| 94 | + k-- |
| 95 | + } |
| 96 | + return false |
| 97 | + } |
| 98 | + return false |
| 99 | +} |
| 100 | + |
| 101 | +private fun isCommentLine(trimmed: String) = |
| 102 | + trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*") |
| 103 | + |
| 104 | +private fun isExitStatement(trimmed: String) = |
| 105 | + startsWithExitKeyword(trimmed) |
| 106 | + |
| 107 | +private fun startsWithExitKeyword(trimmed: String) = |
| 108 | + trimmed.startsWith("return ") || trimmed == "return;" || |
| 109 | + trimmed.startsWith("throw ") || |
| 110 | + trimmed == "continue;" || trimmed.startsWith("continue ") || |
| 111 | + trimmed == "break;" || trimmed.startsWith("break ") |
0 commit comments