Skip to content

Commit ff3c612

Browse files
authored
Add Config Inversion Linter for Config Definitions (#9849)
* adding gradle task to verify configs defined are documented * updating task to account for default values and multi-line config definitions * adding missing config * PR comments without javaparser * replacing with JavaParser * remove unused imports * lint * pr comments
1 parent 46649f7 commit ff3c612

File tree

3 files changed

+101
-0
lines changed

3 files changed

+101
-0
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
id("datadog.dependency-locking")
77
id("datadog.tracer-version")
88
id("datadog.dump-hanged-test")
9+
id("config-inversion-linter")
910
id("datadog.ci-jobs")
1011

1112
id("com.diffplug.spotless") version "6.13.0"

buildSrc/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ dependencies {
6464
implementation("org.apache.maven", "maven-aether-provider", "3.3.9")
6565

6666
implementation("com.github.zafarkhaja:java-semver:0.10.2")
67+
implementation("com.github.javaparser", "javaparser-symbol-solver-core", "3.24.4")
6768

6869
implementation("com.google.guava", "guava", "20.0")
6970
implementation(libs.asm)

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
package datadog.gradle.plugin.config
22

3+
import com.github.javaparser.ParserConfiguration
4+
import com.github.javaparser.StaticJavaParser
5+
import com.github.javaparser.ast.CompilationUnit
6+
import com.github.javaparser.ast.expr.StringLiteralExpr
7+
import com.github.javaparser.ast.nodeTypes.NodeWithModifiers
8+
import com.github.javaparser.ast.Modifier
9+
import com.github.javaparser.ast.body.FieldDeclaration
10+
import com.github.javaparser.ast.body.VariableDeclarator
311
import org.gradle.api.Plugin
412
import org.gradle.api.Project
513
import org.gradle.api.GradleException
@@ -14,6 +22,7 @@ class ConfigInversionLinter : Plugin<Project> {
1422
val extension = target.extensions.create("supportedTracerConfigurations", SupportedTracerConfigurations::class.java)
1523
registerLogEnvVarUsages(target, extension)
1624
registerCheckEnvironmentVariablesUsage(target)
25+
registerCheckConfigStringsTask(target, extension)
1726
}
1827
}
1928

@@ -124,3 +133,93 @@ private fun registerCheckEnvironmentVariablesUsage(project: Project) {
124133
}
125134
}
126135
}
136+
137+
// Helper functions for checking Config Strings
138+
private fun normalize(configValue: String) =
139+
"DD_" + configValue.uppercase().replace("-", "_").replace(".", "_")
140+
141+
// Checking "public" "static" "final"
142+
private fun NodeWithModifiers<*>.hasModifiers(vararg mods: Modifier.Keyword) =
143+
mods.all { hasModifier(it) }
144+
145+
/** Registers `checkConfigStrings` to validate config definitions against documented supported configurations. */
146+
private fun registerCheckConfigStringsTask(project: Project, extension: SupportedTracerConfigurations) {
147+
val ownerPath = extension.configOwnerPath
148+
val generatedFile = extension.className
149+
150+
project.tasks.register("checkConfigStrings") {
151+
group = "verification"
152+
description = "Validates that all config definitions in `dd-trace-api/src/main/java/datadog/trace/api/config` exist in `metadata/supported-configurations.json`"
153+
154+
val mainSourceSetOutput = ownerPath.map {
155+
project.project(it)
156+
.extensions.getByType<SourceSetContainer>()
157+
.named(SourceSet.MAIN_SOURCE_SET_NAME)
158+
.map { main -> main.output }
159+
}
160+
inputs.files(mainSourceSetOutput)
161+
162+
doLast {
163+
val repoRoot: Path = project.rootProject.projectDir.toPath()
164+
val configDir = repoRoot.resolve("dd-trace-api/src/main/java/datadog/trace/api/config").toFile()
165+
166+
if (!configDir.exists()) {
167+
throw GradleException("Config directory not found: ${configDir.absolutePath}")
168+
}
169+
170+
val urls = mainSourceSetOutput.get().get().files.map { it.toURI().toURL() }.toTypedArray()
171+
val (supported, aliasMapping) = URLClassLoader(urls, javaClass.classLoader).use { cl ->
172+
val clazz = Class.forName(generatedFile.get(), true, cl)
173+
@Suppress("UNCHECKED_CAST")
174+
val supportedSet = clazz.getField("SUPPORTED").get(null) as Set<String>
175+
@Suppress("UNCHECKED_CAST")
176+
val aliasMappingMap = clazz.getField("ALIAS_MAPPING").get(null) as Map<String, String>
177+
Pair(supportedSet, aliasMappingMap)
178+
}
179+
180+
var parserConfig = ParserConfiguration()
181+
parserConfig.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8)
182+
183+
StaticJavaParser.setConfiguration(parserConfig)
184+
185+
val violations = buildList {
186+
configDir.listFiles()?.forEach { file ->
187+
val fileName = file.name
188+
val cu: CompilationUnit = StaticJavaParser.parse(file)
189+
190+
cu.findAll(VariableDeclarator::class.java).forEach { varDecl ->
191+
varDecl.parentNode
192+
.map { it as? FieldDeclaration }
193+
.ifPresent { field ->
194+
if (field.hasModifiers(Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC, Modifier.Keyword.FINAL) &&
195+
varDecl.typeAsString == "String") {
196+
197+
val fieldName = varDecl.nameAsString
198+
if (fieldName.endsWith("_DEFAULT")) return@ifPresent
199+
val init = varDecl.initializer.orElse(null) ?: return@ifPresent
200+
201+
if (init !is StringLiteralExpr) return@ifPresent
202+
val rawValue = init.value
203+
204+
val normalized = normalize(rawValue)
205+
if (normalized !in supported && normalized !in aliasMapping) {
206+
val line = varDecl.range.map { it.begin.line }.orElse(1)
207+
add("$fileName:$line -> Config '$rawValue' normalizes to '$normalized' " +
208+
"which is missing from '${extension.jsonFile.get()}'")
209+
}
210+
}
211+
}
212+
}
213+
}
214+
}
215+
216+
if (violations.isNotEmpty()) {
217+
logger.error("\nFound config definitions not in '${extension.jsonFile.get()}':")
218+
violations.forEach { logger.lifecycle(it) }
219+
throw GradleException("Undocumented Environment Variables found. Please add the above Environment Variables to '${extension.jsonFile.get()}'.")
220+
} else {
221+
logger.info("All config strings are present in '${extension.jsonFile.get()}'.")
222+
}
223+
}
224+
}
225+
}

0 commit comments

Comments
 (0)