Skip to content

Commit b6f0dab

Browse files
committed
init
1 parent aa7c70f commit b6f0dab

1 file changed

Lines changed: 187 additions & 4 deletions

File tree

buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt

Lines changed: 187 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import com.github.javaparser.ParserConfiguration
44
import com.github.javaparser.StaticJavaParser
55
import com.github.javaparser.ast.CompilationUnit
66
import 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.*
98
import com.github.javaparser.ast.expr.StringLiteralExpr
109
import com.github.javaparser.ast.nodeTypes.NodeWithModifiers
10+
import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt
11+
import com.github.javaparser.ast.stmt.ReturnStmt
1112
import org.gradle.api.GradleException
1213
import org.gradle.api.Plugin
1314
import 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
3033
private 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 = "\nFound 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 = "\nFound 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

Comments
 (0)