From d550f175ed6c92cd8c0c8ac801aeb846eda1c86b Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Tue, 7 Apr 2026 14:55:23 -0400 Subject: [PATCH 1/9] adding logback env var to supported-configurations.json --- metadata/supported-configurations.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 8f2eec5b73d..92de4676295 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -7761,6 +7761,14 @@ "aliases": ["DD_TRACE_INTEGRATION_LOGS_INTAKE_LOG4J_2_ENABLED", "DD_INTEGRATION_LOGS_INTAKE_LOG4J_2_ENABLED"] } ], + "DD_TRACE_LOGS_INTAKE_LOGBACK_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "true", + "aliases": ["DD_TRACE_INTEGRATION_LOGS_INTAKE_LOGBACK_ENABLED", "DD_INTEGRATION_LOGS_INTAKE_LOGBACK_ENABLED"] + } + ], "DD_TRACE_MAVEN_ENABLED": [ { "version": "A", From c44baa36b5afd875619b66ba2039aad9f0b4aaeb Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 8 Apr 2026 12:52:15 -0400 Subject: [PATCH 2/9] adding spring-messaging-kotlin --- metadata/supported-configurations.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 92de4676295..48e8ea945d9 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -10129,6 +10129,14 @@ "aliases": ["DD_TRACE_INTEGRATION_SPRING_MESSAGING_ENABLED", "DD_INTEGRATION_SPRING_MESSAGING_ENABLED"] } ], + "DD_TRACE_SPRING_MESSAGING_KOTLIN_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "true", + "aliases": ["DD_TRACE_INTEGRATION_SPRING_MESSAGING_KOTLIN_ENABLED", "DD_INTEGRATION_SPRING_MESSAGING_KOTLIN_ENABLED"] + } + ], "DD_TRACE_SPRING_PATH_FILTER_ENABLED": [ { "version": "A", From f8b124ad2ce8e47db6b923343a6319327ab97abd Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 8 Apr 2026 14:01:46 -0400 Subject: [PATCH 3/9] adding profiling config --- metadata/supported-configurations.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 48e8ea945d9..ac7935039e3 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2577,6 +2577,14 @@ "aliases": [] } ], + "DD_PROFILING_ASYNC_LIVEHEAP_TRACK_SIZE_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "true", + "aliases": [] + } + ], "DD_PROFILING_ASYNC_LOGLEVEL": [ { "version": "A", From b6f0dab5c26837e0c6a709571267423639ff0dd8 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 9 Apr 2026 13:52:34 -0400 Subject: [PATCH 4/9] init --- .../plugin/config/ConfigInversionLinter.kt | 191 +++++++++++++++++- 1 file changed, 187 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt index 74c184a47ae..8561a670494 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt @@ -4,10 +4,11 @@ import com.github.javaparser.ParserConfiguration import com.github.javaparser.StaticJavaParser import com.github.javaparser.ast.CompilationUnit import com.github.javaparser.ast.Modifier -import com.github.javaparser.ast.body.FieldDeclaration -import com.github.javaparser.ast.body.VariableDeclarator +import com.github.javaparser.ast.body.* import com.github.javaparser.ast.expr.StringLiteralExpr import com.github.javaparser.ast.nodeTypes.NodeWithModifiers +import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt +import com.github.javaparser.ast.stmt.ReturnStmt import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project @@ -23,13 +24,16 @@ class ConfigInversionLinter : Plugin { registerLogEnvVarUsages(target, extension) registerCheckEnvironmentVariablesUsage(target) registerCheckConfigStringsTask(target, extension) + registerCheckInstrumenterModuleConfigurations(target, extension) + registerCheckDecoratorAnalyticsConfigurations(target, extension) } } // Data class for fields from generated class private data class LoadedConfigFields( val supported: Set, - val aliasMapping: Map = emptyMap() + val aliasMapping: Map = emptyMap(), + val aliases: Map> = emptyMap() ) // Cache for fields from generated class @@ -55,7 +59,9 @@ private fun loadConfigFields( @Suppress("UNCHECKED_CAST") val aliasMappingMap = clazz.getField("ALIAS_MAPPING").get(null) as Map - LoadedConfigFields(supportedSet, aliasMappingMap) + @Suppress("UNCHECKED_CAST") + val aliasesMap = clazz.getField("ALIASES").get(null) as Map> + LoadedConfigFields(supportedSet, aliasMappingMap, aliasesMap) }.also { cachedConfigFields = it } } } @@ -248,3 +254,180 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte } } } + +private val INSTRUMENTER_MODULE_TYPES = setOf( + "InstrumenterModule", + "InstrumenterModule.Tracing", + "InstrumenterModule.Profiling", + "InstrumenterModule.AppSec", + "InstrumenterModule.Iast", + "InstrumenterModule.Usm", + "InstrumenterModule.CiVisibility", + "InstrumenterModule.ContextTracking" +) + +/** Checks that [key] exists in [supported] and [aliases], and that all [expectedAliases] are values of that alias entry. */ +private fun MutableList.checkKeyAndAliases( + key: String, + expectedAliases: List, + supported: Set, + aliases: Map>, + location: String, + context: String +) { + if (key !in supported) { + add("$location -> $context: '$key' is missing from SUPPORTED") + } + if (key !in aliases) { + add("$location -> $context: '$key' is missing from ALIASES") + } else { + val aliasValues = aliases[key] ?: emptyList() + for (expected in expectedAliases) { + if (expected !in aliasValues) { + add("$location -> $context: '$expected' is missing from ALIASES['$key']") + } + } + } +} + +/** + * Shared setup for tasks that scan instrumentation source files against the generated config class. + * Registers a task that parses Java files in dd-java-agent/instrumentation/ and calls [checker] + * for each parsed CompilationUnit to collect violations. + */ +private fun registerInstrumentationCheckTask( + project: Project, + extension: SupportedTracerConfigurations, + taskName: String, + taskDescription: String, + errorHeader: String, + errorMessage: String, + successMessage: String, + checker: MutableList.(LoadedConfigFields, String, CompilationUnit) -> Unit +) { + val ownerPath = extension.configOwnerPath + val generatedFile = extension.className + + project.tasks.register(taskName) { + group = "verification" + description = taskDescription + + val mainSourceSetOutput = ownerPath.map { + project.project(it) + .extensions.getByType() + .named(SourceSet.MAIN_SOURCE_SET_NAME) + .map { main -> main.output } + } + + val instrumentationFiles = project.fileTree(project.rootProject.projectDir) { + include("dd-java-agent/instrumentation/**/src/main/java/**/*.java") + } + doLast { + val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get()) + + val parserConfig = ParserConfiguration() + parserConfig.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8) + StaticJavaParser.setConfiguration(parserConfig) + + val repoRoot = project.rootProject.projectDir.toPath() + val violations = buildList { + instrumentationFiles.files.forEach { file -> + val rel = repoRoot.relativize(file.toPath()).toString() + val cu: CompilationUnit = try { + StaticJavaParser.parse(file) + } catch (_: Exception) { + return@forEach + } + checker(configFields, rel, cu) + } + } + + if (violations.isNotEmpty()) { + logger.error(errorHeader) + violations.forEach { logger.lifecycle(it) } + throw GradleException(errorMessage) + } else { + logger.info(successMessage) + } + } + } +} + +/** Registers `checkInstrumenterModuleConfigurations` to verify each InstrumenterModule's integration name has proper entries in SUPPORTED and ALIASES. */ +private fun registerCheckInstrumenterModuleConfigurations(project: Project, extension: SupportedTracerConfigurations) { + registerInstrumentationCheckTask( + project, extension, + taskName = "checkInstrumenterModuleConfigurations", + taskDescription = "Validates that InstrumenterModule integration names have corresponding entries in SUPPORTED and ALIASES", + errorHeader = "\nFound InstrumenterModule integration names with missing SUPPORTED/ALIASES entries:", + errorMessage = "InstrumenterModule integration names are missing from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.", + successMessage = "All InstrumenterModule integration names have proper SUPPORTED and ALIASES entries." + ) { configFields, rel, cu -> + cu.findAll(ClassOrInterfaceDeclaration::class.java).forEach classLoop@{ classDecl -> + // Only examine classes extending InstrumenterModule.* + val extendsModule = classDecl.extendedTypes.any { it.toString() in INSTRUMENTER_MODULE_TYPES } + if (!extendsModule) return@classLoop + + classDecl.findAll(ExplicitConstructorInvocationStmt::class.java) + .filter { !it.isThis } + .forEach { superCall -> + val names = superCall.arguments + .filterIsInstance() + .map { it.value } + val line = superCall.range.map { it.begin.line }.orElse(1) + + for (name in names) { + val normalized = name.uppercase().replace("-", "_").replace(".", "_") + val enabledKey = "DD_TRACE_${normalized}_ENABLED" + val context = "Integration '$name' (super arg)" + val location = "$rel:$line" + + checkKeyAndAliases( + enabledKey, + listOf("DD_TRACE_INTEGRATION_${normalized}_ENABLED", "DD_INTEGRATION_${normalized}_ENABLED"), + configFields.supported, configFields.aliases, location, context + ) + } + } + } + } +} + +/** Registers `checkDecoratorAnalyticsConfigurations` to verify each BaseDecorator subclass's instrumentationNames have proper analytics entries in SUPPORTED and ALIASES. */ +private fun registerCheckDecoratorAnalyticsConfigurations(project: Project, extension: SupportedTracerConfigurations) { + registerInstrumentationCheckTask( + project, extension, + taskName = "checkDecoratorAnalyticsConfigurations", + taskDescription = "Validates that Decorator instrumentationNames have corresponding analytics entries in SUPPORTED and ALIASES", + errorHeader = "\nFound Decorator instrumentationNames with missing analytics SUPPORTED/ALIASES entries:", + errorMessage = "Decorator instrumentationNames are missing analytics entries from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.", + successMessage = "All Decorator instrumentationNames have proper analytics SUPPORTED and ALIASES entries." + ) { configFields, rel, cu -> + cu.findAll(MethodDeclaration::class.java) + .filter { it.nameAsString == "instrumentationNames" && it.parameters.isEmpty() } + .forEach { method -> + val names = method.findAll(ReturnStmt::class.java).flatMap { ret -> + ret.expression.map { it.findAll(StringLiteralExpr::class.java).map { s -> s.value } } + .orElse(emptyList()) + } + val line = method.range.map { it.begin.line }.orElse(1) + + for (name in names) { + val normalized = name.uppercase().replace("-", "_").replace(".", "_") + val context = "Decorator instrumentationName '$name'" + val location = "$rel:$line" + + checkKeyAndAliases( + "DD_TRACE_${normalized}_ANALYTICS_ENABLED", + listOf("DD_${normalized}_ANALYTICS_ENABLED"), + configFields.supported, configFields.aliases, location, context + ) + checkKeyAndAliases( + "DD_TRACE_${normalized}_ANALYTICS_SAMPLE_RATE", + listOf("DD_${normalized}_ANALYTICS_SAMPLE_RATE"), + configFields.supported, configFields.aliases, location, context + ) + } + } + } +} From 08dd521e632cfdf781888a4334159081a71057a6 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 9 Apr 2026 13:57:36 -0400 Subject: [PATCH 5/9] adding tasks to gitlab job --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 939a9025109..2bfeb85049d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -402,7 +402,7 @@ config-inversion-linter: needs: [] script: - ./gradlew --version - - ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings + - ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings checkInstrumenterModuleConfigurations checkDecoratorAnalyticsConfigurations test_published_artifacts: extends: .gradle_build From 01ad1308cf78f46c12e64e86c32759453dfa85dd Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 9 Apr 2026 14:08:31 -0400 Subject: [PATCH 6/9] ensure GeneratedSupportedConfigurations is generated before the task runs --- .../kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt index 8561a670494..d55a879feb2 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt @@ -318,6 +318,7 @@ private fun registerInstrumentationCheckTask( .named(SourceSet.MAIN_SOURCE_SET_NAME) .map { main -> main.output } } + inputs.files(mainSourceSetOutput) val instrumentationFiles = project.fileTree(project.rootProject.projectDir) { include("dd-java-agent/instrumentation/**/src/main/java/**/*.java") From d011080219d6fb10d93b5bc81e8686ab78243dc8 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Mon, 13 Apr 2026 13:05:45 -0400 Subject: [PATCH 7/9] updating pr feedback --- .../plugin/config/ConfigInversionLinter.kt | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt index d55a879feb2..87520ffc997 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt @@ -4,7 +4,10 @@ import com.github.javaparser.ParserConfiguration import com.github.javaparser.StaticJavaParser import com.github.javaparser.ast.CompilationUnit import com.github.javaparser.ast.Modifier -import com.github.javaparser.ast.body.* +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration +import com.github.javaparser.ast.body.FieldDeclaration +import com.github.javaparser.ast.body.MethodDeclaration +import com.github.javaparser.ast.body.VariableDeclarator import com.github.javaparser.ast.expr.StringLiteralExpr import com.github.javaparser.ast.nodeTypes.NodeWithModifiers import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt @@ -255,17 +258,6 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte } } -private val INSTRUMENTER_MODULE_TYPES = setOf( - "InstrumenterModule", - "InstrumenterModule.Tracing", - "InstrumenterModule.Profiling", - "InstrumenterModule.AppSec", - "InstrumenterModule.Iast", - "InstrumenterModule.Usm", - "InstrumenterModule.CiVisibility", - "InstrumenterModule.ContextTracking" -) - /** Checks that [key] exists in [supported] and [aliases], and that all [expectedAliases] are values of that alias entry. */ private fun MutableList.checkKeyAndAliases( key: String, @@ -366,7 +358,7 @@ private fun registerCheckInstrumenterModuleConfigurations(project: Project, exte ) { configFields, rel, cu -> cu.findAll(ClassOrInterfaceDeclaration::class.java).forEach classLoop@{ classDecl -> // Only examine classes extending InstrumenterModule.* - val extendsModule = classDecl.extendedTypes.any { it.toString() in INSTRUMENTER_MODULE_TYPES } + val extendsModule = classDecl.extendedTypes.any { it.toString().startsWith("InstrumenterModule") } if (!extendsModule) return@classLoop classDecl.findAll(ExplicitConstructorInvocationStmt::class.java) From d3e04ce0771801bdbf5fa1f6a18c98efd996dad0 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 15 Apr 2026 19:57:58 -0400 Subject: [PATCH 8/9] updating PR comments 1 --- .../plugin/config/ConfigInversionLinter.kt | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt index 87520ffc997..27d5a50543a 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt @@ -258,15 +258,15 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte } } -/** Checks that [key] exists in [supported] and [aliases], and that all [expectedAliases] are values of that alias entry. */ -private fun MutableList.checkKeyAndAliases( +/** Collects violations for [key] against [supported] and [aliases], checking that all [expectedAliases] are values of that alias entry. */ +private fun collectMissingKeysAndAliases( key: String, expectedAliases: List, supported: Set, aliases: Map>, location: String, context: String -) { +): List = buildList { if (key !in supported) { add("$location -> $context: '$key' is missing from SUPPORTED") } @@ -284,7 +284,7 @@ private fun MutableList.checkKeyAndAliases( /** * Shared setup for tasks that scan instrumentation source files against the generated config class. - * Registers a task that parses Java files in dd-java-agent/instrumentation/ and calls [checker] + * Registers a task that parses Java files in dd-java-agent/instrumentation/ and calls [collectPropertyViolations] * for each parsed CompilationUnit to collect violations. */ private fun registerInstrumentationCheckTask( @@ -295,7 +295,7 @@ private fun registerInstrumentationCheckTask( errorHeader: String, errorMessage: String, successMessage: String, - checker: MutableList.(LoadedConfigFields, String, CompilationUnit) -> Unit + collectPropertyViolations: (LoadedConfigFields, String, CompilationUnit) -> List ) { val ownerPath = extension.configOwnerPath val generatedFile = extension.className @@ -323,16 +323,14 @@ private fun registerInstrumentationCheckTask( StaticJavaParser.setConfiguration(parserConfig) val repoRoot = project.rootProject.projectDir.toPath() - val violations = buildList { - instrumentationFiles.files.forEach { file -> - val rel = repoRoot.relativize(file.toPath()).toString() - val cu: CompilationUnit = try { - StaticJavaParser.parse(file) - } catch (_: Exception) { - return@forEach - } - checker(configFields, rel, cu) + val violations = instrumentationFiles.files.flatMap { file -> + val rel = repoRoot.relativize(file.toPath()).toString() + val cu: CompilationUnit = try { + StaticJavaParser.parse(file) + } catch (_: Exception) { + return@flatMap emptyList() } + collectPropertyViolations(configFields, rel, cu) } if (violations.isNotEmpty()) { @@ -356,6 +354,8 @@ private fun registerCheckInstrumenterModuleConfigurations(project: Project, exte errorMessage = "InstrumenterModule integration names are missing from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.", successMessage = "All InstrumenterModule integration names have proper SUPPORTED and ALIASES entries." ) { configFields, rel, cu -> + val cuViolations = mutableListOf() + cu.findAll(ClassOrInterfaceDeclaration::class.java).forEach classLoop@{ classDecl -> // Only examine classes extending InstrumenterModule.* val extendsModule = classDecl.extendedTypes.any { it.toString().startsWith("InstrumenterModule") } @@ -375,14 +375,16 @@ private fun registerCheckInstrumenterModuleConfigurations(project: Project, exte val context = "Integration '$name' (super arg)" val location = "$rel:$line" - checkKeyAndAliases( + cuViolations.addAll(collectMissingKeysAndAliases( enabledKey, listOf("DD_TRACE_INTEGRATION_${normalized}_ENABLED", "DD_INTEGRATION_${normalized}_ENABLED"), configFields.supported, configFields.aliases, location, context - ) + )) } } } + + cuViolations } } @@ -396,6 +398,8 @@ private fun registerCheckDecoratorAnalyticsConfigurations(project: Project, exte errorMessage = "Decorator instrumentationNames are missing analytics entries from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.", successMessage = "All Decorator instrumentationNames have proper analytics SUPPORTED and ALIASES entries." ) { configFields, rel, cu -> + val cuViolations = mutableListOf() + cu.findAll(MethodDeclaration::class.java) .filter { it.nameAsString == "instrumentationNames" && it.parameters.isEmpty() } .forEach { method -> @@ -410,17 +414,19 @@ private fun registerCheckDecoratorAnalyticsConfigurations(project: Project, exte val context = "Decorator instrumentationName '$name'" val location = "$rel:$line" - checkKeyAndAliases( + cuViolations.addAll(collectMissingKeysAndAliases( "DD_TRACE_${normalized}_ANALYTICS_ENABLED", listOf("DD_${normalized}_ANALYTICS_ENABLED"), configFields.supported, configFields.aliases, location, context - ) - checkKeyAndAliases( + )) + cuViolations.addAll(collectMissingKeysAndAliases( "DD_TRACE_${normalized}_ANALYTICS_SAMPLE_RATE", listOf("DD_${normalized}_ANALYTICS_SAMPLE_RATE"), configFields.supported, configFields.aliases, location, context - ) + )) } } + + cuViolations } } From 244d3c3b09432171ef4e2766b6f8478f4c59d05f Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 15 Apr 2026 20:13:11 -0400 Subject: [PATCH 9/9] abstracting instrumentation checks into abstract class and extending from it --- .../plugin/config/ConfigInversionLinter.kt | 200 +++++++++++------- 1 file changed, 118 insertions(+), 82 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt index 27d5a50543a..d2afd581957 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt @@ -12,11 +12,17 @@ import com.github.javaparser.ast.expr.StringLiteralExpr import com.github.javaparser.ast.nodeTypes.NodeWithModifiers import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt import com.github.javaparser.ast.stmt.ReturnStmt +import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.TaskAction import org.gradle.kotlin.dsl.getByType import java.net.URLClassLoader import java.nio.file.Path @@ -33,7 +39,7 @@ class ConfigInversionLinter : Plugin { } // Data class for fields from generated class -private data class LoadedConfigFields( +data class LoadedConfigFields( val supported: Set, val aliasMapping: Map = emptyMap(), val aliases: Map> = emptyMap() @@ -282,82 +288,67 @@ private fun collectMissingKeysAndAliases( } } -/** - * Shared setup for tasks that scan instrumentation source files against the generated config class. - * Registers a task that parses Java files in dd-java-agent/instrumentation/ and calls [collectPropertyViolations] - * for each parsed CompilationUnit to collect violations. - */ -private fun registerInstrumentationCheckTask( - project: Project, - extension: SupportedTracerConfigurations, - taskName: String, - taskDescription: String, - errorHeader: String, - errorMessage: String, - successMessage: String, - collectPropertyViolations: (LoadedConfigFields, String, CompilationUnit) -> List -) { - val ownerPath = extension.configOwnerPath - val generatedFile = extension.className +/** Abstract base for tasks that scan instrumentation source files against the generated config class. */ +abstract class InstrumentationConfigCheckTask : DefaultTask() { + @get:InputFiles + abstract val mainSourceSetOutput: ConfigurableFileCollection - project.tasks.register(taskName) { - group = "verification" - description = taskDescription + @get:InputFiles + abstract val instrumentationFiles: ConfigurableFileCollection - val mainSourceSetOutput = ownerPath.map { - project.project(it) - .extensions.getByType() - .named(SourceSet.MAIN_SOURCE_SET_NAME) - .map { main -> main.output } - } - inputs.files(mainSourceSetOutput) + @get:Input + abstract val generatedClassName: Property - val instrumentationFiles = project.fileTree(project.rootProject.projectDir) { - include("dd-java-agent/instrumentation/**/src/main/java/**/*.java") - } - doLast { - val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get()) + @get:Input + abstract val errorHeader: Property - val parserConfig = ParserConfiguration() - parserConfig.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8) - StaticJavaParser.setConfiguration(parserConfig) + @get:Input + abstract val errorMessage: Property - val repoRoot = project.rootProject.projectDir.toPath() - val violations = instrumentationFiles.files.flatMap { file -> - val rel = repoRoot.relativize(file.toPath()).toString() - val cu: CompilationUnit = try { - StaticJavaParser.parse(file) - } catch (_: Exception) { - return@flatMap emptyList() - } - collectPropertyViolations(configFields, rel, cu) - } + @get:Input + abstract val successMessage: Property - if (violations.isNotEmpty()) { - logger.error(errorHeader) - violations.forEach { logger.lifecycle(it) } - throw GradleException(errorMessage) - } else { - logger.info(successMessage) + @TaskAction + fun execute() { + val configFields = loadConfigFields(mainSourceSetOutput, generatedClassName.get()) + + val parserConfig = ParserConfiguration() + parserConfig.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8) + StaticJavaParser.setConfiguration(parserConfig) + + val repoRoot = project.rootProject.projectDir.toPath() + val violations = instrumentationFiles.files.flatMap { file -> + val rel = repoRoot.relativize(file.toPath()).toString() + val cu: CompilationUnit = try { + StaticJavaParser.parse(file) + } catch (_: Exception) { + return@flatMap emptyList() } + collectPropertyViolations(configFields, rel, cu) + } + + if (violations.isNotEmpty()) { + logger.error(errorHeader.get()) + violations.forEach { logger.lifecycle(it) } + throw GradleException(errorMessage.get()) + } else { + logger.info(successMessage.get()) } } + + protected abstract fun collectPropertyViolations( + configFields: LoadedConfigFields, relativePath: String, cu: CompilationUnit + ): List } -/** Registers `checkInstrumenterModuleConfigurations` to verify each InstrumenterModule's integration name has proper entries in SUPPORTED and ALIASES. */ -private fun registerCheckInstrumenterModuleConfigurations(project: Project, extension: SupportedTracerConfigurations) { - registerInstrumentationCheckTask( - project, extension, - taskName = "checkInstrumenterModuleConfigurations", - taskDescription = "Validates that InstrumenterModule integration names have corresponding entries in SUPPORTED and ALIASES", - errorHeader = "\nFound InstrumenterModule integration names with missing SUPPORTED/ALIASES entries:", - errorMessage = "InstrumenterModule integration names are missing from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.", - successMessage = "All InstrumenterModule integration names have proper SUPPORTED and ALIASES entries." - ) { configFields, rel, cu -> - val cuViolations = mutableListOf() +/** Checks that InstrumenterModule integration names have proper entries in SUPPORTED and ALIASES. */ +abstract class CheckInstrumenterModuleConfigTask : InstrumentationConfigCheckTask() { + override fun collectPropertyViolations( + configFields: LoadedConfigFields, relativePath: String, cu: CompilationUnit + ): List { + val violations = mutableListOf() cu.findAll(ClassOrInterfaceDeclaration::class.java).forEach classLoop@{ classDecl -> - // Only examine classes extending InstrumenterModule.* val extendsModule = classDecl.extendedTypes.any { it.toString().startsWith("InstrumenterModule") } if (!extendsModule) return@classLoop @@ -373,9 +364,9 @@ private fun registerCheckInstrumenterModuleConfigurations(project: Project, exte val normalized = name.uppercase().replace("-", "_").replace(".", "_") val enabledKey = "DD_TRACE_${normalized}_ENABLED" val context = "Integration '$name' (super arg)" - val location = "$rel:$line" + val location = "$relativePath:$line" - cuViolations.addAll(collectMissingKeysAndAliases( + violations.addAll(collectMissingKeysAndAliases( enabledKey, listOf("DD_TRACE_INTEGRATION_${normalized}_ENABLED", "DD_INTEGRATION_${normalized}_ENABLED"), configFields.supported, configFields.aliases, location, context @@ -384,21 +375,16 @@ private fun registerCheckInstrumenterModuleConfigurations(project: Project, exte } } - cuViolations + return violations } } -/** Registers `checkDecoratorAnalyticsConfigurations` to verify each BaseDecorator subclass's instrumentationNames have proper analytics entries in SUPPORTED and ALIASES. */ -private fun registerCheckDecoratorAnalyticsConfigurations(project: Project, extension: SupportedTracerConfigurations) { - registerInstrumentationCheckTask( - project, extension, - taskName = "checkDecoratorAnalyticsConfigurations", - taskDescription = "Validates that Decorator instrumentationNames have corresponding analytics entries in SUPPORTED and ALIASES", - errorHeader = "\nFound Decorator instrumentationNames with missing analytics SUPPORTED/ALIASES entries:", - errorMessage = "Decorator instrumentationNames are missing analytics entries from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.", - successMessage = "All Decorator instrumentationNames have proper analytics SUPPORTED and ALIASES entries." - ) { configFields, rel, cu -> - val cuViolations = mutableListOf() +/** Checks that Decorator instrumentationNames have proper analytics entries in SUPPORTED and ALIASES. */ +abstract class CheckDecoratorAnalyticsConfigTask : InstrumentationConfigCheckTask() { + override fun collectPropertyViolations( + configFields: LoadedConfigFields, relativePath: String, cu: CompilationUnit + ): List { + val violations = mutableListOf() cu.findAll(MethodDeclaration::class.java) .filter { it.nameAsString == "instrumentationNames" && it.parameters.isEmpty() } @@ -412,14 +398,14 @@ private fun registerCheckDecoratorAnalyticsConfigurations(project: Project, exte for (name in names) { val normalized = name.uppercase().replace("-", "_").replace(".", "_") val context = "Decorator instrumentationName '$name'" - val location = "$rel:$line" + val location = "$relativePath:$line" - cuViolations.addAll(collectMissingKeysAndAliases( + violations.addAll(collectMissingKeysAndAliases( "DD_TRACE_${normalized}_ANALYTICS_ENABLED", listOf("DD_${normalized}_ANALYTICS_ENABLED"), configFields.supported, configFields.aliases, location, context )) - cuViolations.addAll(collectMissingKeysAndAliases( + violations.addAll(collectMissingKeysAndAliases( "DD_TRACE_${normalized}_ANALYTICS_SAMPLE_RATE", listOf("DD_${normalized}_ANALYTICS_SAMPLE_RATE"), configFields.supported, configFields.aliases, location, context @@ -427,6 +413,56 @@ private fun registerCheckDecoratorAnalyticsConfigurations(project: Project, exte } } - cuViolations + return violations + } +} + +/** Registers `checkInstrumenterModuleConfigurations` to verify each InstrumenterModule's integration name has proper entries in SUPPORTED and ALIASES. */ +private fun registerCheckInstrumenterModuleConfigurations(project: Project, extension: SupportedTracerConfigurations) { + val ownerPath = extension.configOwnerPath + val generatedFile = extension.className + + project.tasks.register("checkInstrumenterModuleConfigurations", CheckInstrumenterModuleConfigTask::class.java) { + group = "verification" + description = "Validates that InstrumenterModule integration names have corresponding entries in SUPPORTED and ALIASES" + + mainSourceSetOutput.from(ownerPath.map { + project.project(it) + .extensions.getByType() + .named(SourceSet.MAIN_SOURCE_SET_NAME) + .map { main -> main.output } + }) + instrumentationFiles.from(project.fileTree(project.rootProject.projectDir) { + include("dd-java-agent/instrumentation/**/src/main/java/**/*.java") + }) + generatedClassName.set(generatedFile) + errorHeader.set("\nFound InstrumenterModule integration names with missing SUPPORTED/ALIASES entries:") + errorMessage.set("InstrumenterModule integration names are missing from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.") + successMessage.set("All InstrumenterModule integration names have proper SUPPORTED and ALIASES entries.") + } +} + +/** Registers `checkDecoratorAnalyticsConfigurations` to verify each BaseDecorator subclass's instrumentationNames have proper analytics entries in SUPPORTED and ALIASES. */ +private fun registerCheckDecoratorAnalyticsConfigurations(project: Project, extension: SupportedTracerConfigurations) { + val ownerPath = extension.configOwnerPath + val generatedFile = extension.className + + project.tasks.register("checkDecoratorAnalyticsConfigurations", CheckDecoratorAnalyticsConfigTask::class.java) { + group = "verification" + description = "Validates that Decorator instrumentationNames have corresponding analytics entries in SUPPORTED and ALIASES" + + mainSourceSetOutput.from(ownerPath.map { + project.project(it) + .extensions.getByType() + .named(SourceSet.MAIN_SOURCE_SET_NAME) + .map { main -> main.output } + }) + instrumentationFiles.from(project.fileTree(project.rootProject.projectDir) { + include("dd-java-agent/instrumentation/**/src/main/java/**/*.java") + }) + generatedClassName.set(generatedFile) + errorHeader.set("\nFound Decorator instrumentationNames with missing analytics SUPPORTED/ALIASES entries:") + errorMessage.set("Decorator instrumentationNames are missing analytics entries from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.") + successMessage.set("All Decorator instrumentationNames have proper analytics SUPPORTED and ALIASES entries.") } }