@@ -4,10 +4,11 @@ import com.github.javaparser.ParserConfiguration
44import com.github.javaparser.StaticJavaParser
55import com.github.javaparser.ast.CompilationUnit
66import com.github.javaparser.ast.Modifier
7- import com.github.javaparser.ast.body.FieldDeclaration
8- import com.github.javaparser.ast.body.VariableDeclarator
7+ import com.github.javaparser.ast.body.*
98import com.github.javaparser.ast.expr.StringLiteralExpr
109import com.github.javaparser.ast.nodeTypes.NodeWithModifiers
10+ import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt
11+ import com.github.javaparser.ast.stmt.ReturnStmt
1112import org.gradle.api.GradleException
1213import org.gradle.api.Plugin
1314import org.gradle.api.Project
@@ -23,13 +24,16 @@ class ConfigInversionLinter : Plugin<Project> {
2324 registerLogEnvVarUsages(target, extension)
2425 registerCheckEnvironmentVariablesUsage(target)
2526 registerCheckConfigStringsTask(target, extension)
27+ registerCheckInstrumenterModuleConfigurations(target, extension)
28+ registerCheckDecoratorAnalyticsConfigurations(target, extension)
2629 }
2730}
2831
2932// Data class for fields from generated class
3033private data class LoadedConfigFields (
3134 val supported : Set <String >,
32- val aliasMapping : Map <String , String > = emptyMap()
35+ val aliasMapping : Map <String , String > = emptyMap(),
36+ val aliases : Map <String , List <String >> = emptyMap()
3337)
3438
3539// Cache for fields from generated class
@@ -55,7 +59,9 @@ private fun loadConfigFields(
5559
5660 @Suppress(" UNCHECKED_CAST" )
5761 val aliasMappingMap = clazz.getField(" ALIAS_MAPPING" ).get(null ) as Map <String , String >
58- LoadedConfigFields (supportedSet, aliasMappingMap)
62+ @Suppress(" UNCHECKED_CAST" )
63+ val aliasesMap = clazz.getField(" ALIASES" ).get(null ) as Map <String , List <String >>
64+ LoadedConfigFields (supportedSet, aliasMappingMap, aliasesMap)
5965 }.also { cachedConfigFields = it }
6066 }
6167}
@@ -248,3 +254,180 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte
248254 }
249255 }
250256}
257+
258+ private val INSTRUMENTER_MODULE_TYPES = setOf (
259+ " InstrumenterModule" ,
260+ " InstrumenterModule.Tracing" ,
261+ " InstrumenterModule.Profiling" ,
262+ " InstrumenterModule.AppSec" ,
263+ " InstrumenterModule.Iast" ,
264+ " InstrumenterModule.Usm" ,
265+ " InstrumenterModule.CiVisibility" ,
266+ " InstrumenterModule.ContextTracking"
267+ )
268+
269+ /* * Checks that [key] exists in [supported] and [aliases], and that all [expectedAliases] are values of that alias entry. */
270+ private fun MutableList<String>.checkKeyAndAliases (
271+ key : String ,
272+ expectedAliases : List <String >,
273+ supported : Set <String >,
274+ aliases : Map <String , List <String >>,
275+ location : String ,
276+ context : String
277+ ) {
278+ if (key !in supported) {
279+ add(" $location -> $context : '$key ' is missing from SUPPORTED" )
280+ }
281+ if (key !in aliases) {
282+ add(" $location -> $context : '$key ' is missing from ALIASES" )
283+ } else {
284+ val aliasValues = aliases[key] ? : emptyList()
285+ for (expected in expectedAliases) {
286+ if (expected !in aliasValues) {
287+ add(" $location -> $context : '$expected ' is missing from ALIASES['$key ']" )
288+ }
289+ }
290+ }
291+ }
292+
293+ /* *
294+ * Shared setup for tasks that scan instrumentation source files against the generated config class.
295+ * Registers a task that parses Java files in dd-java-agent/instrumentation/ and calls [checker]
296+ * for each parsed CompilationUnit to collect violations.
297+ */
298+ private fun registerInstrumentationCheckTask (
299+ project : Project ,
300+ extension : SupportedTracerConfigurations ,
301+ taskName : String ,
302+ taskDescription : String ,
303+ errorHeader : String ,
304+ errorMessage : String ,
305+ successMessage : String ,
306+ checker : MutableList <String >.(LoadedConfigFields , String , CompilationUnit ) -> Unit
307+ ) {
308+ val ownerPath = extension.configOwnerPath
309+ val generatedFile = extension.className
310+
311+ project.tasks.register(taskName) {
312+ group = " verification"
313+ description = taskDescription
314+
315+ val mainSourceSetOutput = ownerPath.map {
316+ project.project(it)
317+ .extensions.getByType<SourceSetContainer >()
318+ .named(SourceSet .MAIN_SOURCE_SET_NAME )
319+ .map { main -> main.output }
320+ }
321+
322+ val instrumentationFiles = project.fileTree(project.rootProject.projectDir) {
323+ include(" dd-java-agent/instrumentation/**/src/main/java/**/*.java" )
324+ }
325+ doLast {
326+ val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get())
327+
328+ val parserConfig = ParserConfiguration ()
329+ parserConfig.setLanguageLevel(ParserConfiguration .LanguageLevel .JAVA_8 )
330+ StaticJavaParser .setConfiguration(parserConfig)
331+
332+ val repoRoot = project.rootProject.projectDir.toPath()
333+ val violations = buildList {
334+ instrumentationFiles.files.forEach { file ->
335+ val rel = repoRoot.relativize(file.toPath()).toString()
336+ val cu: CompilationUnit = try {
337+ StaticJavaParser .parse(file)
338+ } catch (_: Exception ) {
339+ return @forEach
340+ }
341+ checker(configFields, rel, cu)
342+ }
343+ }
344+
345+ if (violations.isNotEmpty()) {
346+ logger.error(errorHeader)
347+ violations.forEach { logger.lifecycle(it) }
348+ throw GradleException (errorMessage)
349+ } else {
350+ logger.info(successMessage)
351+ }
352+ }
353+ }
354+ }
355+
356+ /* * Registers `checkInstrumenterModuleConfigurations` to verify each InstrumenterModule's integration name has proper entries in SUPPORTED and ALIASES. */
357+ private fun registerCheckInstrumenterModuleConfigurations (project : Project , extension : SupportedTracerConfigurations ) {
358+ registerInstrumentationCheckTask(
359+ project, extension,
360+ taskName = " checkInstrumenterModuleConfigurations" ,
361+ taskDescription = " Validates that InstrumenterModule integration names have corresponding entries in SUPPORTED and ALIASES" ,
362+ errorHeader = " \n Found InstrumenterModule integration names with missing SUPPORTED/ALIASES entries:" ,
363+ errorMessage = " InstrumenterModule integration names are missing from SUPPORTED or ALIASES in '${extension.jsonFile.get()} '." ,
364+ successMessage = " All InstrumenterModule integration names have proper SUPPORTED and ALIASES entries."
365+ ) { configFields, rel, cu ->
366+ cu.findAll(ClassOrInterfaceDeclaration ::class .java).forEach classLoop@{ classDecl ->
367+ // Only examine classes extending InstrumenterModule.*
368+ val extendsModule = classDecl.extendedTypes.any { it.toString() in INSTRUMENTER_MODULE_TYPES }
369+ if (! extendsModule) return @classLoop
370+
371+ classDecl.findAll(ExplicitConstructorInvocationStmt ::class .java)
372+ .filter { ! it.isThis }
373+ .forEach { superCall ->
374+ val names = superCall.arguments
375+ .filterIsInstance<StringLiteralExpr >()
376+ .map { it.value }
377+ val line = superCall.range.map { it.begin.line }.orElse(1 )
378+
379+ for (name in names) {
380+ val normalized = name.uppercase().replace(" -" , " _" ).replace(" ." , " _" )
381+ val enabledKey = " DD_TRACE_${normalized} _ENABLED"
382+ val context = " Integration '$name ' (super arg)"
383+ val location = " $rel :$line "
384+
385+ checkKeyAndAliases(
386+ enabledKey,
387+ listOf (" DD_TRACE_INTEGRATION_${normalized} _ENABLED" , " DD_INTEGRATION_${normalized} _ENABLED" ),
388+ configFields.supported, configFields.aliases, location, context
389+ )
390+ }
391+ }
392+ }
393+ }
394+ }
395+
396+ /* * Registers `checkDecoratorAnalyticsConfigurations` to verify each BaseDecorator subclass's instrumentationNames have proper analytics entries in SUPPORTED and ALIASES. */
397+ private fun registerCheckDecoratorAnalyticsConfigurations (project : Project , extension : SupportedTracerConfigurations ) {
398+ registerInstrumentationCheckTask(
399+ project, extension,
400+ taskName = " checkDecoratorAnalyticsConfigurations" ,
401+ taskDescription = " Validates that Decorator instrumentationNames have corresponding analytics entries in SUPPORTED and ALIASES" ,
402+ errorHeader = " \n Found Decorator instrumentationNames with missing analytics SUPPORTED/ALIASES entries:" ,
403+ errorMessage = " Decorator instrumentationNames are missing analytics entries from SUPPORTED or ALIASES in '${extension.jsonFile.get()} '." ,
404+ successMessage = " All Decorator instrumentationNames have proper analytics SUPPORTED and ALIASES entries."
405+ ) { configFields, rel, cu ->
406+ cu.findAll(MethodDeclaration ::class .java)
407+ .filter { it.nameAsString == " instrumentationNames" && it.parameters.isEmpty() }
408+ .forEach { method ->
409+ val names = method.findAll(ReturnStmt ::class .java).flatMap { ret ->
410+ ret.expression.map { it.findAll(StringLiteralExpr ::class .java).map { s -> s.value } }
411+ .orElse(emptyList())
412+ }
413+ val line = method.range.map { it.begin.line }.orElse(1 )
414+
415+ for (name in names) {
416+ val normalized = name.uppercase().replace(" -" , " _" ).replace(" ." , " _" )
417+ val context = " Decorator instrumentationName '$name '"
418+ val location = " $rel :$line "
419+
420+ checkKeyAndAliases(
421+ " DD_TRACE_${normalized} _ANALYTICS_ENABLED" ,
422+ listOf (" DD_${normalized} _ANALYTICS_ENABLED" ),
423+ configFields.supported, configFields.aliases, location, context
424+ )
425+ checkKeyAndAliases(
426+ " DD_TRACE_${normalized} _ANALYTICS_SAMPLE_RATE" ,
427+ listOf (" DD_${normalized} _ANALYTICS_SAMPLE_RATE" ),
428+ configFields.supported, configFields.aliases, location, context
429+ )
430+ }
431+ }
432+ }
433+ }
0 commit comments