Skip to content

Commit f1e72e5

Browse files
committed
updating config inversion linter to include profiling specific handling
1 parent aa7c70f commit f1e72e5

File tree

3 files changed

+127
-32
lines changed

3 files changed

+127
-32
lines changed

.gitlab-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ config-inversion-linter:
402402
needs: []
403403
script:
404404
- ./gradlew --version
405-
- ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings
405+
- ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings checkDatadogProfilerConfigs
406406

407407
test_published_artifacts:
408408
extends: .gradle_build

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

Lines changed: 118 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package datadog.gradle.plugin.config
22

3-
import com.github.javaparser.ParserConfiguration
43
import com.github.javaparser.StaticJavaParser
5-
import com.github.javaparser.ast.CompilationUnit
64
import com.github.javaparser.ast.Modifier
75
import com.github.javaparser.ast.body.FieldDeclaration
86
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
910
import com.github.javaparser.ast.expr.StringLiteralExpr
1011
import com.github.javaparser.ast.nodeTypes.NodeWithModifiers
1112
import org.gradle.api.GradleException
@@ -14,6 +15,7 @@ import org.gradle.api.Project
1415
import org.gradle.api.tasks.SourceSet
1516
import org.gradle.api.tasks.SourceSetContainer
1617
import org.gradle.kotlin.dsl.getByType
18+
import java.io.File
1719
import java.net.URLClassLoader
1820
import 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+
}

metadata/supported-configurations.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2577,6 +2577,14 @@
25772577
"aliases": []
25782578
}
25792579
],
2580+
"DD_PROFILING_ASYNC_LIVEHEAP_TRACK_SIZE_ENABLED": [
2581+
{
2582+
"version": "A",
2583+
"type": "boolean",
2584+
"default": "true",
2585+
"aliases": []
2586+
}
2587+
],
25802588
"DD_PROFILING_ASYNC_LOGLEVEL": [
25812589
{
25822590
"version": "A",

0 commit comments

Comments
 (0)