Skip to content

Commit 2da2eb2

Browse files
committed
Merge task-008: add camelCase naming convention linter (resolved conflict)
2 parents ed5148a + be96cce commit 2da2eb2

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

buildSrc/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ gradlePlugin {
6969
id = "dd-trace-java.unnecessary-else-linter"
7070
implementationClass = "datadog.gradle.plugin.lint.UnnecessaryElseLinter"
7171
}
72+
73+
create("naming-convention-linter") {
74+
id = "dd-trace-java.naming-convention-linter"
75+
implementationClass = "datadog.gradle.plugin.lint.NamingConventionLinter"
76+
}
7277
}
7378
}
7479

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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

Comments
 (0)