11package datadog.gradle.plugin.config
22
3- import com.github.javaparser.StaticJavaParser
43import com.github.javaparser.ast.Modifier
5- import com.github.javaparser.ast.body.FieldDeclaration
6- import com.github.javaparser.ast.body.VariableDeclarator
7- import com.github.javaparser.ast.expr.Expression
8- import com.github.javaparser.ast.expr.MethodCallExpr
9- import com.github.javaparser.ast.expr.NameExpr
10- import com.github.javaparser.ast.expr.StringLiteralExpr
114import com.github.javaparser.ast.nodeTypes.NodeWithModifiers
12- import org.gradle.api.Action
135import org.gradle.api.GradleException
146import org.gradle.api.Plugin
157import org.gradle.api.Project
168import org.gradle.api.Task
17- import org.gradle.api.provider.Provider
9+ import org.gradle.api.tasks.TaskProvider
1810import org.gradle.api.tasks.SourceSet
1911import org.gradle.api.tasks.SourceSetContainer
20- import org.gradle.api.tasks.SourceSetOutput
2112import org.gradle.kotlin.dsl.getByType
22- import java.io.File
2313import java.net.URLClassLoader
2414import java.nio.file.Path
2515
2616class ConfigInversionLinter : Plugin <Project > {
2717 override fun apply (target : Project ) {
2818 val extension = target.extensions.create(" supportedTracerConfigurations" , SupportedTracerConfigurations ::class .java)
29- registerLogEnvVarUsages(target, extension)
30- registerCheckEnvironmentVariablesUsage(target)
31- registerCheckConfigStringsTask(target, extension)
19+ val logEnvVarUsages = registerLogEnvVarUsages(target, extension)
20+ val checkEnvVarUsage = registerCheckEnvironmentVariablesUsage(target)
21+ val checkConfigStrings = registerCheckConfigStringsTask(target, extension)
3222
3323 target.tasks.register(" checkConfigurations" ) {
3424 group = " verification"
3525 description = " Runs all config inversion validation checks"
36- dependsOn(" logEnvVarUsages" , " checkEnvironmentVariablesUsage " , " checkConfigStrings" )
26+ dependsOn(logEnvVarUsages, checkEnvVarUsage, checkConfigStrings)
3727 }
3828 }
3929}
4030
4131// Data class for fields from generated class
42- private data class LoadedConfigFields (
32+ internal data class LoadedConfigFields (
4333 val supported : Set <String >,
4434 val aliasMapping : Map <String , String > = emptyMap()
4535)
4636
4737// Cache for fields from generated class
48- private var cachedConfigFields: LoadedConfigFields ? = null
38+ internal var cachedConfigFields: LoadedConfigFields ? = null
4939
5040// Helper function to load fields from the generated class
51- private fun loadConfigFields (
41+ internal fun loadConfigFields (
5242 mainSourceSetOutput : org.gradle.api.file.FileCollection ,
5343 generatedClassName : String
5444): LoadedConfigFields {
@@ -73,12 +63,12 @@ private fun loadConfigFields(
7363}
7464
7565/* * Registers `logEnvVarUsages` (scan for DD_/OTEL_ tokens and fail if unsupported). */
76- private fun registerLogEnvVarUsages (target : Project , extension : SupportedTracerConfigurations ) {
66+ private fun registerLogEnvVarUsages (target : Project , extension : SupportedTracerConfigurations ): TaskProvider < Task > {
7767 val ownerPath = extension.configOwnerPath
7868 val generatedFile = extension.className
7969
8070 // token check that uses the generated class instead of JSON
81- target.tasks.register(" logEnvVarUsages" ) {
71+ return target.tasks.register(" logEnvVarUsages" ) {
8272 group = " verification"
8373 description = " Scan Java files for DD_/OTEL_ tokens and fail if unsupported (using generated constants)"
8474
@@ -139,8 +129,8 @@ private fun registerLogEnvVarUsages(target: Project, extension: SupportedTracerC
139129}
140130
141131/* * Registers `checkEnvironmentVariablesUsage` (forbid EnvironmentVariables.get(...)). */
142- private fun registerCheckEnvironmentVariablesUsage (project : Project ) {
143- project.tasks.register(" checkEnvironmentVariablesUsage" ) {
132+ private fun registerCheckEnvironmentVariablesUsage (project : Project ): TaskProvider < Task > {
133+ return project.tasks.register(" checkEnvironmentVariablesUsage" ) {
144134 group = " verification"
145135 description = " Scans src/main/java for direct usages of EnvironmentVariables.get(...)"
146136
@@ -178,19 +168,19 @@ private fun registerCheckEnvironmentVariablesUsage(project: Project) {
178168}
179169
180170// Helper functions for checking Config Strings
181- private fun normalize (configValue : String ) =
171+ internal fun normalize (configValue : String ) =
182172 " DD_" + configValue.uppercase().replace(" -" , " _" ).replace(" ." , " _" )
183173
184174// Checking "public" "static" "final"
185- private fun NodeWithModifiers <* >.hasModifiers (vararg mods : Modifier .Keyword ) =
175+ internal fun NodeWithModifiers <* >.hasModifiers (vararg mods : Modifier .Keyword ) =
186176 mods.all { hasModifier(it) }
187177
188178/* * Registers `checkConfigStrings` to validate config definitions against documented supported configurations. */
189- private fun registerCheckConfigStringsTask (project : Project , extension : SupportedTracerConfigurations ) {
179+ private fun registerCheckConfigStringsTask (project : Project , extension : SupportedTracerConfigurations ): TaskProvider < Task > {
190180 val ownerPath = extension.configOwnerPath
191181 val generatedFile = extension.className
192182
193- project.tasks.register(" checkConfigStrings" ) {
183+ return project.tasks.register(" checkConfigStrings" ) {
194184 group = " verification"
195185 description = " Validates that all config definitions in `dd-trace-api/src/main/java/datadog/trace/api/config` exist in `metadata/supported-configurations.json`"
196186
@@ -207,131 +197,3 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte
207197 }
208198}
209199
210- /* * Validates that all config definitions in the config directory are documented in supported-configurations.json. */
211- private class RegularConfigCheckAction (
212- private val mainSourceSetOutput : Provider <Provider <SourceSetOutput >>,
213- private val generatedClassName : Provider <String >,
214- private val extension : SupportedTracerConfigurations
215- ) : Action<Task> {
216- override fun execute (task : Task ) {
217- val repoRoot: Path = task.project.rootProject.projectDir.toPath()
218- val configDir = repoRoot.resolve(" dd-trace-api/src/main/java/datadog/trace/api/config" ).toFile()
219-
220- if (! configDir.exists()) {
221- throw GradleException (" Config directory not found: ${configDir.absolutePath} " )
222- }
223-
224- val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedClassName.get())
225- val supported = configFields.supported
226- val aliasMapping = configFields.aliasMapping
227-
228- val violations = buildList {
229- configDir.listFiles()?.forEach { file ->
230- val fileName = file.name
231- extractStringConstants(file).forEach { (fieldName, entry) ->
232- if (fieldName.endsWith(" _DEFAULT" )) return @forEach
233- val normalized = normalize(entry.value)
234- if (normalized !in supported && normalized !in aliasMapping) {
235- add(" $fileName :${entry.line} -> Config '${entry.value} ' normalizes to '$normalized ' " +
236- " which is missing from '${extension.jsonFile.get()} '" )
237- }
238- }
239- }
240- }
241-
242- if (violations.isNotEmpty()) {
243- task.logger.error(" \n Found config definitions not in '${extension.jsonFile.get()} ':" )
244- violations.forEach { task.logger.lifecycle(it) }
245- throw GradleException (" Undocumented Environment Variables found. Please add the above Environment Variables to '${extension.jsonFile.get()} '." )
246- } else {
247- task.logger.info(" All config strings are present in '${extension.jsonFile.get()} '." )
248- }
249- }
250- }
251-
252- /* *
253- * Validates that every `.ddprof.` config key used as a primary key in `DatadogProfilerConfig`'s
254- * static helpers also has its async-translated form (`profiling.ddprof.*` → `profiling.async.*`)
255- * documented in `supported-configurations.json`.
256- */
257- private class ProfilingConfigCheckAction (
258- private val mainSourceSetOutput : Provider <Provider <SourceSetOutput >>,
259- private val generatedClassName : Provider <String >,
260- private val extension : SupportedTracerConfigurations
261- ) : Action<Task> {
262- override fun execute (task : Task ) {
263- val repoRoot = task.project.rootProject.projectDir.toPath()
264-
265- val constantMap = extractStringConstants(
266- repoRoot.resolve(" dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java" ).toFile()
267- )
268-
269- val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedClassName.get())
270- val supported = configFields.supported
271- val aliasMapping = configFields.aliasMapping
272-
273- val ddprofConfigFile = repoRoot.resolve(
274- " dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java"
275- ).toFile()
276- val cu = StaticJavaParser .parse(ddprofConfigFile)
277-
278- val helperMethodNames = setOf (" getBoolean" , " getInteger" , " getLong" , " getString" )
279- val violations = mutableListOf<String >()
280-
281- cu.findAll(MethodCallExpr ::class .java).forEach { call ->
282- if (call.scope.isPresent) return @forEach
283- if (call.nameAsString !in helperMethodNames) return @forEach
284- val args = call.arguments
285- if (args.size < 2 || args[0 ] !is NameExpr || (args[0 ] as NameExpr ).nameAsString != " configProvider" ) return @forEach
286-
287- val primaryKeyEntry = resolveConstant(args[1 ], constantMap) ? : return @forEach
288- checkDocumented(primaryKeyEntry, supported, aliasMapping, call, violations, extension)
289- }
290-
291- if (violations.isNotEmpty()) {
292- violations.forEach { task.logger.error(it) }
293- throw GradleException (" Undocumented configs found in DatadogProfilerConfig. Please add the above to '${extension.jsonFile.get()} '." )
294- } else {
295- task.logger.info(" All DatadogProfilerConfig configs are documented." )
296- }
297- }
298- }
299-
300- private data class ConstantEntry (val value : String , val line : Int )
301-
302- private fun extractStringConstants (file : File ): Map <String , ConstantEntry > {
303- val map = mutableMapOf<String , ConstantEntry >()
304- StaticJavaParser .parse(file).findAll(VariableDeclarator ::class .java).forEach { varDecl ->
305- val field = varDecl.parentNode.map { it as ? FieldDeclaration }.orElse(null ) ? : return @forEach
306- if (field.hasModifiers(Modifier .Keyword .PUBLIC , Modifier .Keyword .STATIC , Modifier .Keyword .FINAL )
307- && varDecl.typeAsString == " String" ) {
308- val init = varDecl.initializer.orElse(null ) as ? StringLiteralExpr ? : return @forEach
309- val line = varDecl.range.map { it.begin.line }.orElse(- 1 )
310- map[varDecl.nameAsString] = ConstantEntry (init .value, line)
311- }
312- }
313- return map
314- }
315-
316- private fun resolveConstant (expr : Expression ? , constantMap : Map <String , ConstantEntry >): ConstantEntry ? = when (expr) {
317- is StringLiteralExpr -> ConstantEntry (expr.value, - 1 )
318- is NameExpr -> constantMap[expr.nameAsString]
319- else -> null
320- }
321-
322- // Only check the async-translated form produced by DatadogProfilerConfig.normalizeKey.
323- private fun checkDocumented (
324- entry : ConstantEntry ,
325- supported : Set <String >,
326- aliasMapping : Map <String , String >,
327- call : MethodCallExpr ,
328- violations : MutableList <String >,
329- extension : SupportedTracerConfigurations
330- ) {
331- if (! entry.value.contains(" .ddprof." )) return
332- val asyncNormalized = normalize(entry.value.replace(" .ddprof." , " .async." ))
333- if (asyncNormalized !in supported && asyncNormalized !in aliasMapping) {
334- val callLine = call.range.map { it.begin.line }.orElse(- 1 )
335- violations.add(" ProfilingConfig.java:${entry.line} (DatadogProfilerConfig.java:$callLine ) -> '${entry.value} ' (async form) → '$asyncNormalized ' is missing from '${extension.jsonFile.get()} '" )
336- }
337- }
0 commit comments