Skip to content

Commit ff6bd9d

Browse files
amarzialibric3AlexeyKuznetsov-DD
authored
Add an instrumentation naming checker plugin (#10224)
* Create a gradle plugin to check instrumenation names * Factorise the regexp * make suffixes configurable * Fix module recursion * Ensure instrumentation naming validation recurses into nested modules * Update buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingPlugin.kt Co-authored-by: Brice Dutheil <[email protected]> * Update buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingPlugin.kt Co-authored-by: Brice Dutheil <[email protected]> * Update buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingPlugin.kt Co-authored-by: Alexey Kuznetsov <[email protected]> * Update buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingPlugin.kt Co-authored-by: Brice Dutheil <[email protected]> * Fix build after suggestions * use raw strings * convert suffixes and exclusions to SetProperty * Update buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingPlugin.kt Co-authored-by: Brice Dutheil <[email protected]> * fix the typo + remove the readme --------- Co-authored-by: Brice Dutheil <[email protected]> Co-authored-by: Alexey Kuznetsov <[email protected]>
1 parent 7f969ea commit ff6bd9d

File tree

4 files changed

+233
-0
lines changed

4 files changed

+233
-0
lines changed

buildSrc/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ gradlePlugin {
4747
id = "dd-trace-java.config-inversion-linter"
4848
implementationClass = "datadog.gradle.plugin.config.ConfigInversionLinter"
4949
}
50+
51+
create("instrumentation-naming") {
52+
id = "dd-trace-java.instrumentation-naming"
53+
implementationClass = "datadog.gradle.plugin.naming.InstrumentationNamingPlugin"
54+
}
5055
}
5156
}
5257

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package datadog.gradle.plugin.naming
2+
3+
import org.gradle.api.provider.Property
4+
import org.gradle.api.provider.SetProperty
5+
6+
/**
7+
* Extension for configuring instrumentation naming convention checks.
8+
*
9+
* Example usage:
10+
* ```
11+
* instrumentationNaming {
12+
* instrumentationsDir.set(file("dd-java-agent/instrumentation"))
13+
* exclusions.set(setOf("http-url-connection", "sslsocket"))
14+
* suffixes.set(setOf("-common", "-stubs"))
15+
* }
16+
* ```
17+
*/
18+
abstract class InstrumentationNamingExtension {
19+
/**
20+
* The directory containing instrumentation modules.
21+
* Defaults to "dd-java-agent/instrumentation".
22+
*/
23+
abstract val instrumentationsDir: Property<String>
24+
25+
/**
26+
* Set of module names to exclude from naming convention checks.
27+
* These modules will not be validated against the naming rules.
28+
*/
29+
abstract val exclusions: SetProperty<String>
30+
31+
/**
32+
* Set of allowed suffixes for module names (e.g., "-common", "-stubs").
33+
* Module names must end with either one of these suffixes or a version number.
34+
* Defaults to ["-common", "-stubs"].
35+
*/
36+
abstract val suffixes: SetProperty<String>
37+
38+
init {
39+
instrumentationsDir.convention("dd-java-agent/instrumentation")
40+
exclusions.convention(emptySet())
41+
suffixes.convention(setOf("-common", "-stubs"))
42+
}
43+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package datadog.gradle.plugin.naming
2+
3+
import org.gradle.api.GradleException
4+
import org.gradle.api.Plugin
5+
import org.gradle.api.Project
6+
import org.gradle.kotlin.dsl.create
7+
import java.io.File
8+
9+
/**
10+
* Plugin that validates naming conventions for instrumentation modules.
11+
*
12+
* Rules:
13+
* 1. Module name must end with a version (e.g., "2.0", "3.1") OR end with "-common"
14+
* 2. Module name must include the parent directory name
15+
* (e.g., "couchbase-2.0" must contain "couchbase" which is the parent directory name)
16+
*
17+
* Apply this plugin:
18+
* ```
19+
* plugins {
20+
* id("dd-trace-java.instrumentation-naming")
21+
* }
22+
* ```
23+
*/
24+
class InstrumentationNamingPlugin : Plugin<Project> {
25+
private val versionPattern : Regex = Regex("""\d+\.\d+(\.\d+)?$""")
26+
27+
override fun apply(target: Project) {
28+
val extension = target.extensions.create<InstrumentationNamingExtension>("instrumentationNaming")
29+
30+
target.tasks.register("checkInstrumentationNaming") {
31+
group = "verification"
32+
description = "Validates naming conventions for instrumentation modules"
33+
34+
doLast {
35+
val instrumentationsDir = target.rootProject.file(extension.instrumentationsDir)
36+
val exclusions = extension.exclusions.get()
37+
val suffixes = extension.suffixes.get()
38+
39+
if (!instrumentationsDir.exists() || !instrumentationsDir.isDirectory) {
40+
throw GradleException(
41+
"Instrumentations directory not found: ${instrumentationsDir.absolutePath}"
42+
)
43+
}
44+
45+
val violations = validateInstrumentations(instrumentationsDir, exclusions, suffixes)
46+
47+
if (violations.isNotEmpty()) {
48+
val suffixesStr = suffixes.joinToString("', '", "'", "'")
49+
val errorMessage = buildString {
50+
appendLine("""
51+
52+
Instrumentation naming convention violations found:
53+
54+
""".trimIndent())
55+
violations.forEach { violation ->
56+
appendLine("""
57+
${violation.path}
58+
${violation.message}
59+
""".trimIndent())
60+
}
61+
append("""
62+
Naming rules:
63+
1. Module name must end with a version (e.g., '2.0', '3.1') OR one of: $suffixesStr
64+
2. Module name must include the parent directory name
65+
Example: 'couchbase/couchbase-2.0' ✓ (contains 'couchbase')
66+
67+
To exclude specific modules or customize suffixes, configure the plugin:
68+
instrumentationNaming {
69+
exclusions.set(setOf("module-name"))
70+
suffixes.set(setOf("-common", "-stubs"))
71+
}
72+
""".trimIndent())
73+
}
74+
throw GradleException(errorMessage)
75+
} else {
76+
target.logger.lifecycle("✓ All instrumentation modules follow naming conventions")
77+
}
78+
}
79+
}
80+
}
81+
82+
private fun validateInstrumentations(
83+
instrumentationsDir: File,
84+
exclusions: Set<String>,
85+
suffixes: Set<String>
86+
): List<NamingViolation> {
87+
val violations = mutableListOf<NamingViolation>()
88+
89+
fun hasBuildFile(dir: File): Boolean = dir.listFiles()?.any {
90+
it.name == "build.gradle" || it.name == "build.gradle.kts"
91+
} ?: false
92+
93+
fun traverseModules(currentDir: File, parentName: String?) {
94+
currentDir.listFiles { file -> file.isDirectory }?.forEach childLoop@{ childDir ->
95+
val moduleName = childDir.name
96+
97+
// Skip build directories and other non-instrumentation directories
98+
if (moduleName in setOf("build", "src", ".gradle")) {
99+
return@childLoop
100+
}
101+
102+
val childHasBuildFile = hasBuildFile(childDir)
103+
val nestedModules = childDir.listFiles { file -> file.isDirectory }?.filter { hasBuildFile(it) } ?: emptyList()
104+
105+
if (childHasBuildFile && moduleName !in exclusions) {
106+
val relativePath = childDir.relativeTo(instrumentationsDir).path
107+
if (parentName == null && nestedModules.isEmpty()) {
108+
validateLeafModuleName(moduleName, relativePath, suffixes)?.let { violations.add(it) }
109+
} else if (parentName != null) {
110+
violations.addAll(validateModuleName(moduleName, parentName, relativePath, suffixes))
111+
}
112+
}
113+
114+
// Continue traversing to validate deeply nested modules
115+
if (nestedModules.isNotEmpty() || !childHasBuildFile) {
116+
traverseModules(childDir, moduleName)
117+
}
118+
}
119+
}
120+
121+
traverseModules(instrumentationsDir, null)
122+
123+
return violations
124+
}
125+
126+
private fun validateModuleName(
127+
moduleName: String,
128+
parentName: String,
129+
relativePath: String,
130+
suffixes: Set<String>
131+
): List<NamingViolation> {
132+
// Rule 1: Module name must end with version pattern or one of the configured suffixes
133+
validateVersionOrSuffix(moduleName, relativePath, suffixes)?.let { return listOf(it) }
134+
135+
// Rule 2: Module name must contain parent directory name
136+
if (!moduleName.contains(parentName, ignoreCase = true)) {
137+
return listOf(NamingViolation(
138+
relativePath,
139+
"Module name '$moduleName' should contain parent directory name '$parentName'"
140+
))
141+
}
142+
143+
return emptyList()
144+
}
145+
146+
/**
147+
* Validates naming for leaf modules (modules at the top level with no parent grouping).
148+
* These only need to check the version/suffix requirement.
149+
*/
150+
private fun validateLeafModuleName(
151+
moduleName: String,
152+
relativePath: String,
153+
suffixes: Set<String>
154+
): NamingViolation? {
155+
return validateVersionOrSuffix(moduleName, relativePath, suffixes)
156+
}
157+
158+
/**
159+
* Validates that a module name ends with either a version or one of the configured suffixes.
160+
*/
161+
private fun validateVersionOrSuffix(
162+
moduleName: String,
163+
relativePath: String,
164+
suffixes: Set<String>
165+
): NamingViolation? {
166+
val endsWithSuffix = suffixes.any { moduleName.endsWith(it) }
167+
val endsWithVersion = versionPattern.containsMatchIn(moduleName)
168+
169+
if (!endsWithVersion && !endsWithSuffix) {
170+
val suffixesStr = suffixes.joinToString("', '", "'", "'")
171+
return NamingViolation(
172+
relativePath,
173+
"Module name '$moduleName' must end with a version (e.g., '2.0', '3.1.0') or one of: $suffixesStr"
174+
)
175+
}
176+
177+
return null
178+
}
179+
180+
private data class NamingViolation(
181+
val path: String,
182+
val message: String
183+
)
184+
}

dd-java-agent/instrumentation/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
22

33
plugins {
44
id 'com.gradleup.shadow'
5+
id("dd-trace-java.instrumentation-naming")
56
}
67
apply from: "$rootDir/gradle/java.gradle"
78

0 commit comments

Comments
 (0)