11package datadog.gradle.plugin.config
22
3- import com.github.javaparser.ParserConfiguration
43import com.github.javaparser.StaticJavaParser
5- import com.github.javaparser.ast.CompilationUnit
64import com.github.javaparser.ast.Modifier
75import com.github.javaparser.ast.body.FieldDeclaration
86import 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
910import com.github.javaparser.ast.expr.StringLiteralExpr
1011import com.github.javaparser.ast.nodeTypes.NodeWithModifiers
1112import org.gradle.api.GradleException
@@ -14,6 +15,7 @@ import org.gradle.api.Project
1415import org.gradle.api.tasks.SourceSet
1516import org.gradle.api.tasks.SourceSetContainer
1617import org.gradle.kotlin.dsl.getByType
18+ import java.io.File
1719import java.net.URLClassLoader
1820import java.nio.file.Path
1921
@@ -23,6 +25,7 @@ class ConfigInversionLinter : Plugin<Project> {
2325 registerLogEnvVarUsages(target, extension)
2426 registerCheckEnvironmentVariablesUsage(target)
2527 registerCheckConfigStringsTask(target, extension)
28+ registerCheckDatadogProfilerConfigTask(target, extension)
2629 }
2730}
2831
@@ -202,38 +205,16 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte
202205 val supported = configFields.supported
203206 val aliasMapping = configFields.aliasMapping
204207
205- var parserConfig = ParserConfiguration ()
206- parserConfig.setLanguageLevel(ParserConfiguration .LanguageLevel .JAVA_8 )
207-
208- StaticJavaParser .setConfiguration(parserConfig)
209-
210208 val violations = buildList {
211209 configDir.listFiles()?.forEach { file ->
212210 val fileName = file.name
213- val cu: CompilationUnit = StaticJavaParser .parse(file)
214-
215- cu.findAll(VariableDeclarator ::class .java).forEach { varDecl ->
216- varDecl.parentNode
217- .map { it as ? FieldDeclaration }
218- .ifPresent { field ->
219- if (field.hasModifiers(Modifier .Keyword .PUBLIC , Modifier .Keyword .STATIC , Modifier .Keyword .FINAL ) &&
220- varDecl.typeAsString == " String" ) {
221-
222- val fieldName = varDecl.nameAsString
223- if (fieldName.endsWith(" _DEFAULT" )) return @ifPresent
224- val init = varDecl.initializer.orElse(null ) ? : return @ifPresent
225-
226- if (init !is StringLiteralExpr ) return @ifPresent
227- val rawValue = init .value
228-
229- val normalized = normalize(rawValue)
230- if (normalized !in supported && normalized !in aliasMapping) {
231- val line = varDecl.range.map { it.begin.line }.orElse(1 )
232- add(" $fileName :$line -> Config '$rawValue ' normalizes to '$normalized ' " +
233- " which is missing from '${extension.jsonFile.get()} '" )
234- }
235- }
236- }
211+ extractStringConstants(file).forEach { (fieldName, entry) ->
212+ if (fieldName.endsWith(" _DEFAULT" )) return @forEach
213+ val normalized = normalize(entry.value)
214+ if (normalized !in supported && normalized !in aliasMapping) {
215+ add(" $fileName :${entry.line} -> Config '${entry.value} ' normalizes to '$normalized ' " +
216+ " which is missing from '${extension.jsonFile.get()} '" )
217+ }
237218 }
238219 }
239220 }
@@ -248,3 +229,109 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte
248229 }
249230 }
250231}
232+
233+ /* *
234+ * Registers `checkDatadogProfilerConfigs` to validate that every `.ddprof.` config key used as a
235+ * primary key in `DatadogProfilerConfig`'s static helpers also has its async-translated form
236+ * (`profiling.ddprof.*` → `profiling.async.*`) documented in `supported-configurations.json`.
237+ *
238+ * The raw form is already validated by `checkConfigStrings`. This task only covers the additional
239+ * async form produced by `DatadogProfilerConfig.normalizeKey`.
240+ */
241+ private fun registerCheckDatadogProfilerConfigTask (project : Project , extension : SupportedTracerConfigurations ) {
242+ val ownerPath = extension.configOwnerPath
243+ val generatedFile = extension.className
244+
245+ project.tasks.register(" checkDatadogProfilerConfigs" ) {
246+ group = " verification"
247+ description = " Validates all configs read in DatadogProfilerConfig are documented in supported-configurations.json"
248+
249+ val mainSourceSetOutput = ownerPath.map {
250+ project.project(it)
251+ .extensions.getByType<SourceSetContainer >()
252+ .named(SourceSet .MAIN_SOURCE_SET_NAME )
253+ .map { main -> main.output }
254+ }
255+ inputs.files(mainSourceSetOutput)
256+
257+ doLast {
258+ val repoRoot = project.rootProject.projectDir.toPath()
259+
260+ // Only ProfilingConfig.java is needed — all .ddprof. keys are defined there
261+ val constantMap = extractStringConstants(
262+ repoRoot.resolve(" dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java" ).toFile()
263+ )
264+
265+ val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get())
266+ val supported = configFields.supported
267+ val aliasMapping = configFields.aliasMapping
268+
269+ val ddprofConfigFile = repoRoot.resolve(
270+ " dd-java-agent/agent-profiling/profiling-ddprof/src/main/java/com/datadog/profiling/ddprof/DatadogProfilerConfig.java"
271+ ).toFile()
272+ val cu = StaticJavaParser .parse(ddprofConfigFile)
273+
274+ val helperMethodNames = setOf (" getBoolean" , " getInteger" , " getLong" , " getString" )
275+ val violations = mutableListOf<String >()
276+
277+ cu.findAll(MethodCallExpr ::class .java).forEach { call ->
278+ // Only care about static helper calls: getXxx(configProvider, primaryKey, default, ...aliases)
279+ // Direct configProvider.getXxx() calls are already covered by checkConfigStrings
280+ if (call.scope.isPresent) return @forEach
281+ if (call.nameAsString !in helperMethodNames) return @forEach
282+ val args = call.arguments
283+ if (args.size < 2 || args[0 ] !is NameExpr || (args[0 ] as NameExpr ).nameAsString != " configProvider" ) return @forEach
284+
285+ // Primary key goes through normalizeKey — validate its async-translated form
286+ val primaryKeyEntry = resolveConstant(args[1 ], constantMap) ? : return @forEach
287+ checkDocumented(primaryKeyEntry, supported, aliasMapping, call, violations, extension)
288+ }
289+
290+ if (violations.isNotEmpty()) {
291+ violations.forEach { logger.error(it) }
292+ throw GradleException (" Undocumented configs found in DatadogProfilerConfig. Please add the above to '${extension.jsonFile.get()} '." )
293+ } else {
294+ logger.info(" All DatadogProfilerConfig configs are documented." )
295+ }
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