|
| 1 | +package datadog.gradle.plugin.lint |
| 2 | + |
| 3 | +import org.gradle.api.Plugin |
| 4 | +import org.gradle.api.Project |
| 5 | + |
| 6 | +class NamingConventionLinter : Plugin<Project> { |
| 7 | + override fun apply(target: Project) { |
| 8 | + target.tasks.register("checkNamingConventions") { |
| 9 | + group = "verification" |
| 10 | + description = "Checks Java files changed vs. base branch for snake_case method/variable names" |
| 11 | + |
| 12 | + doLast { |
| 13 | + val repoRoot = target.rootProject.projectDir |
| 14 | + |
| 15 | + // Get changed .java files via git diff against base branch |
| 16 | + val gitOutput = try { |
| 17 | + val process = ProcessBuilder("git", "diff", "--name-only", "--diff-filter=ACM", "origin/master...HEAD") |
| 18 | + .directory(repoRoot) |
| 19 | + .redirectErrorStream(true) |
| 20 | + .start() |
| 21 | + process.inputStream.bufferedReader().readText().trim() |
| 22 | + } catch (e: Exception) { |
| 23 | + target.logger.warn("checkNamingConventions: could not run git diff — skipping. ${e.message}") |
| 24 | + return@doLast |
| 25 | + } |
| 26 | + |
| 27 | + if (gitOutput.isBlank()) { |
| 28 | + target.logger.lifecycle("checkNamingConventions: no changed files found.") |
| 29 | + return@doLast |
| 30 | + } |
| 31 | + |
| 32 | + val changedJavaFiles = gitOutput.lines() |
| 33 | + .filter { it.endsWith(".java") } |
| 34 | + .map { repoRoot.resolve(it) } |
| 35 | + .filter { it.exists() } |
| 36 | + |
| 37 | + if (changedJavaFiles.isEmpty()) { |
| 38 | + target.logger.lifecycle("checkNamingConventions: no changed .java files.") |
| 39 | + return@doLast |
| 40 | + } |
| 41 | + |
| 42 | + // Matches method-like declarations with underscores in the name |
| 43 | + // e.g. "void my_method(" or "public String some_name(" |
| 44 | + val methodWithUnderscore = Regex( |
| 45 | + """(?:public|private|protected|static|void|int|long|boolean|String|\w+)\s+(\w*_\w+)\s*\(""" |
| 46 | + ) |
| 47 | + |
| 48 | + // Matches local variable declarations with underscores |
| 49 | + // e.g. "int my_var = " or "final String some_name;" |
| 50 | + val localVarWithUnderscore = Regex( |
| 51 | + """(?:int|long|String|boolean|var|final)\s+(\w*_\w+)\s*[=;]""" |
| 52 | + ) |
| 53 | + |
| 54 | + // All-uppercase constant pattern: words with underscores, all caps |
| 55 | + val constantPattern = Regex("""^[A-Z][A-Z0-9_]*$""") |
| 56 | + |
| 57 | + val warnings = mutableListOf<String>() |
| 58 | + |
| 59 | + for (file in changedJavaFiles) { |
| 60 | + val relativePath = repoRoot.toPath().relativize(file.toPath()).toString() |
| 61 | + val isTestFile = relativePath.contains("/src/test/") || relativePath.contains("\\src\\test\\") |
| 62 | + val isMainFile = relativePath.contains("/src/main/") || relativePath.contains("\\src\\main\\") |
| 63 | + |
| 64 | + val lines = file.readLines() |
| 65 | + var pendingTestAnnotation = false |
| 66 | + |
| 67 | + for ((idx, line) in lines.withIndex()) { |
| 68 | + val trimmed = line.trim() |
| 69 | + val lineNumber = idx + 1 |
| 70 | + |
| 71 | + // Track @Test annotations |
| 72 | + if (trimmed == "@Test" || trimmed.startsWith("@Test(")) { |
| 73 | + pendingTestAnnotation = true |
| 74 | + continue |
| 75 | + } |
| 76 | + // Reset annotation flag on non-annotation, non-empty lines |
| 77 | + if (trimmed.isNotEmpty() && !trimmed.startsWith("@") && !trimmed.startsWith("//")) { |
| 78 | + val hadTestAnnotation = pendingTestAnnotation |
| 79 | + pendingTestAnnotation = false |
| 80 | + |
| 81 | + // Check method names (applies to all files except when @Test annotated or in test dir) |
| 82 | + if (!isTestFile && !hadTestAnnotation) { |
| 83 | + methodWithUnderscore.find(line)?.let { match -> |
| 84 | + val name = match.groupValues[1] |
| 85 | + if (!constantPattern.matches(name) && !isNativeMethod(line)) { |
| 86 | + warnings.add("NAMING: $relativePath:$lineNumber — snake_case identifier '$name' should be camelCase") |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + // Check local variables (only in src/main/ files) |
| 92 | + if (isMainFile) { |
| 93 | + localVarWithUnderscore.find(line)?.let { match -> |
| 94 | + val name = match.groupValues[1] |
| 95 | + if (!constantPattern.matches(name)) { |
| 96 | + warnings.add("NAMING: $relativePath:$lineNumber — snake_case identifier '$name' should be camelCase") |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + } else if (trimmed.startsWith("@")) { |
| 101 | + // Keep the annotation flag active across annotation lines |
| 102 | + } else { |
| 103 | + pendingTestAnnotation = false |
| 104 | + } |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + if (warnings.isNotEmpty()) { |
| 109 | + target.logger.warn("checkNamingConventions: found ${warnings.size} naming convention warning(s):") |
| 110 | + warnings.forEach { target.logger.warn(it) } |
| 111 | + } else { |
| 112 | + target.logger.lifecycle("checkNamingConventions: no naming convention issues found.") |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + private fun isNativeMethod(line: String): Boolean { |
| 119 | + return line.contains("native ") |
| 120 | + } |
| 121 | +} |
0 commit comments