diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 7e2a8f1e61a..170bf43cca6 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -47,6 +47,11 @@ gradlePlugin { id = "dd-trace-java.config-inversion-linter" implementationClass = "datadog.gradle.plugin.config.ConfigInversionLinter" } + + create("instrumentation-naming") { + id = "dd-trace-java.instrumentation-naming" + implementationClass = "datadog.gradle.plugin.naming.InstrumentationNamingPlugin" + } } } diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingExtension.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingExtension.kt new file mode 100644 index 00000000000..24d11e1020e --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingExtension.kt @@ -0,0 +1,43 @@ +package datadog.gradle.plugin.naming + +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty + +/** + * Extension for configuring instrumentation naming convention checks. + * + * Example usage: + * ``` + * instrumentationNaming { + * instrumentationsDir.set(file("dd-java-agent/instrumentation")) + * exclusions.set(setOf("http-url-connection", "sslsocket")) + * suffixes.set(setOf("-common", "-stubs")) + * } + * ``` + */ +abstract class InstrumentationNamingExtension { + /** + * The directory containing instrumentation modules. + * Defaults to "dd-java-agent/instrumentation". + */ + abstract val instrumentationsDir: Property + + /** + * Set of module names to exclude from naming convention checks. + * These modules will not be validated against the naming rules. + */ + abstract val exclusions: SetProperty + + /** + * Set of allowed suffixes for module names (e.g., "-common", "-stubs"). + * Module names must end with either one of these suffixes or a version number. + * Defaults to ["-common", "-stubs"]. + */ + abstract val suffixes: SetProperty + + init { + instrumentationsDir.convention("dd-java-agent/instrumentation") + exclusions.convention(emptySet()) + suffixes.convention(setOf("-common", "-stubs")) + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingPlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingPlugin.kt new file mode 100644 index 00000000000..2a41f796161 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/naming/InstrumentationNamingPlugin.kt @@ -0,0 +1,184 @@ +package datadog.gradle.plugin.naming + +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.create +import java.io.File + +/** + * Plugin that validates naming conventions for instrumentation modules. + * + * Rules: + * 1. Module name must end with a version (e.g., "2.0", "3.1") OR end with "-common" + * 2. Module name must include the parent directory name + * (e.g., "couchbase-2.0" must contain "couchbase" which is the parent directory name) + * + * Apply this plugin: + * ``` + * plugins { + * id("dd-trace-java.instrumentation-naming") + * } + * ``` + */ +class InstrumentationNamingPlugin : Plugin { + private val versionPattern : Regex = Regex("""\d+\.\d+(\.\d+)?$""") + + override fun apply(target: Project) { + val extension = target.extensions.create("instrumentationNaming") + + target.tasks.register("checkInstrumentationNaming") { + group = "verification" + description = "Validates naming conventions for instrumentation modules" + + doLast { + val instrumentationsDir = target.rootProject.file(extension.instrumentationsDir) + val exclusions = extension.exclusions.get() + val suffixes = extension.suffixes.get() + + if (!instrumentationsDir.exists() || !instrumentationsDir.isDirectory) { + throw GradleException( + "Instrumentations directory not found: ${instrumentationsDir.absolutePath}" + ) + } + + val violations = validateInstrumentations(instrumentationsDir, exclusions, suffixes) + + if (violations.isNotEmpty()) { + val suffixesStr = suffixes.joinToString("', '", "'", "'") + val errorMessage = buildString { + appendLine(""" + + Instrumentation naming convention violations found: + + """.trimIndent()) + violations.forEach { violation -> + appendLine(""" + • ${violation.path} + ${violation.message} + """.trimIndent()) + } + append(""" + Naming rules: + 1. Module name must end with a version (e.g., '2.0', '3.1') OR one of: $suffixesStr + 2. Module name must include the parent directory name + Example: 'couchbase/couchbase-2.0' ✓ (contains 'couchbase') + + To exclude specific modules or customize suffixes, configure the plugin: + instrumentationNaming { + exclusions.set(setOf("module-name")) + suffixes.set(setOf("-common", "-stubs")) + } + """.trimIndent()) + } + throw GradleException(errorMessage) + } else { + target.logger.lifecycle("✓ All instrumentation modules follow naming conventions") + } + } + } + } + + private fun validateInstrumentations( + instrumentationsDir: File, + exclusions: Set, + suffixes: Set + ): List { + val violations = mutableListOf() + + fun hasBuildFile(dir: File): Boolean = dir.listFiles()?.any { + it.name == "build.gradle" || it.name == "build.gradle.kts" + } ?: false + + fun traverseModules(currentDir: File, parentName: String?) { + currentDir.listFiles { file -> file.isDirectory }?.forEach childLoop@{ childDir -> + val moduleName = childDir.name + + // Skip build directories and other non-instrumentation directories + if (moduleName in setOf("build", "src", ".gradle")) { + return@childLoop + } + + val childHasBuildFile = hasBuildFile(childDir) + val nestedModules = childDir.listFiles { file -> file.isDirectory }?.filter { hasBuildFile(it) } ?: emptyList() + + if (childHasBuildFile && moduleName !in exclusions) { + val relativePath = childDir.relativeTo(instrumentationsDir).path + if (parentName == null && nestedModules.isEmpty()) { + validateLeafModuleName(moduleName, relativePath, suffixes)?.let { violations.add(it) } + } else if (parentName != null) { + violations.addAll(validateModuleName(moduleName, parentName, relativePath, suffixes)) + } + } + + // Continue traversing to validate deeply nested modules + if (nestedModules.isNotEmpty() || !childHasBuildFile) { + traverseModules(childDir, moduleName) + } + } + } + + traverseModules(instrumentationsDir, null) + + return violations + } + + private fun validateModuleName( + moduleName: String, + parentName: String, + relativePath: String, + suffixes: Set + ): List { + // Rule 1: Module name must end with version pattern or one of the configured suffixes + validateVersionOrSuffix(moduleName, relativePath, suffixes)?.let { return listOf(it) } + + // Rule 2: Module name must contain parent directory name + if (!moduleName.contains(parentName, ignoreCase = true)) { + return listOf(NamingViolation( + relativePath, + "Module name '$moduleName' should contain parent directory name '$parentName'" + )) + } + + return emptyList() + } + + /** + * Validates naming for leaf modules (modules at the top level with no parent grouping). + * These only need to check the version/suffix requirement. + */ + private fun validateLeafModuleName( + moduleName: String, + relativePath: String, + suffixes: Set + ): NamingViolation? { + return validateVersionOrSuffix(moduleName, relativePath, suffixes) + } + + /** + * Validates that a module name ends with either a version or one of the configured suffixes. + */ + private fun validateVersionOrSuffix( + moduleName: String, + relativePath: String, + suffixes: Set + ): NamingViolation? { + val endsWithSuffix = suffixes.any { moduleName.endsWith(it) } + val endsWithVersion = versionPattern.containsMatchIn(moduleName) + + if (!endsWithVersion && !endsWithSuffix) { + val suffixesStr = suffixes.joinToString("', '", "'", "'") + return NamingViolation( + relativePath, + "Module name '$moduleName' must end with a version (e.g., '2.0', '3.1.0') or one of: $suffixesStr" + ) + } + + return null + } + + private data class NamingViolation( + val path: String, + val message: String + ) +} diff --git a/dd-java-agent/instrumentation/build.gradle b/dd-java-agent/instrumentation/build.gradle index 454c70fb083..6f757d3faae 100644 --- a/dd-java-agent/instrumentation/build.gradle +++ b/dd-java-agent/instrumentation/build.gradle @@ -2,6 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { id 'com.gradleup.shadow' + id("dd-trace-java.instrumentation-naming") } apply from: "$rootDir/gradle/java.gradle"