Skip to content

Commit 3e8b524

Browse files
committed
adding new config inversion linter
1 parent 2d28a85 commit 3e8b524

File tree

2 files changed

+102
-32
lines changed

2 files changed

+102
-32
lines changed

.gitlab-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ config-inversion-linter:
374374
needs: []
375375
script:
376376
- ./gradlew --version
377-
- ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings
377+
- ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings verifyAliasKeysAreSupported
378378

379379
test_published_artifacts:
380380
extends: .gradle_build

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

Lines changed: 101 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,46 @@ class ConfigInversionLinter : Plugin<Project> {
2323
registerLogEnvVarUsages(target, extension)
2424
registerCheckEnvironmentVariablesUsage(target)
2525
registerCheckConfigStringsTask(target, extension)
26+
verifyAliasKeysAreSupported(target, extension)
27+
}
28+
}
29+
30+
// Data class for fields from generated class
31+
private data class LoadedConfigFields(
32+
val supported: Set<String>,
33+
val aliases: Map<String, List<String>> = emptyMap(),
34+
val aliasMapping: Map<String, String> = emptyMap()
35+
)
36+
37+
// Cache for fields from generated class
38+
private var cachedConfigFields: LoadedConfigFields? = null
39+
40+
// Helper function to load fields from the generated class
41+
private fun loadConfigFields(
42+
mainSourceSetOutput: org.gradle.api.file.FileCollection,
43+
generatedClassName: String
44+
): LoadedConfigFields {
45+
return cachedConfigFields ?: run {
46+
val urls = mainSourceSetOutput.files.map { it.toURI().toURL() }.toTypedArray()
47+
URLClassLoader(urls, LoadedConfigFields::class.java.classLoader).use { cl ->
48+
val clazz = Class.forName(generatedClassName, true, cl)
49+
50+
val supportedField = clazz.getField("SUPPORTED").get(null)
51+
@Suppress("UNCHECKED_CAST")
52+
val supportedSet = when (supportedField) {
53+
is Set<*> -> supportedField as Set<String>
54+
is Map<*, *> -> supportedField.keys as Set<String>
55+
else -> throw IllegalStateException("SUPPORTED field must be either Set<String> or Map<String, Any>, but was ${supportedField?.javaClass}")
56+
}
57+
58+
@Suppress("UNCHECKED_CAST")
59+
val aliases = clazz.getField("ALIASES").get(null) as Map<String, List<String>>
60+
61+
@Suppress("UNCHECKED_CAST")
62+
val aliasMappingMap = clazz.getField("ALIAS_MAPPING").get(null) as Map<String, String>
63+
64+
LoadedConfigFields(supportedSet, aliases, aliasMappingMap)
65+
}.also { cachedConfigFields = it }
2666
}
2767
}
2868

@@ -52,16 +92,11 @@ private fun registerLogEnvVarUsages(target: Project, extension: SupportedTracerC
5292
inputs.files(javaFiles)
5393
outputs.upToDateWhen { true }
5494
doLast {
55-
// 1) Build classloader from the owner project’s runtime classpath
56-
val urls = mainSourceSetOutput.get().get().files.map { it.toURI().toURL() }.toTypedArray()
57-
val supported: Set<String> = URLClassLoader(urls, javaClass.classLoader).use { cl ->
58-
// 2) Load the generated class + read static field
59-
val clazz = Class.forName(generatedFile.get(), true, cl)
60-
@Suppress("UNCHECKED_CAST")
61-
clazz.getField("SUPPORTED").get(null) as Set<String>
62-
}
95+
// 1) Load configuration fields from the generated class
96+
val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get())
97+
val supported = configFields.supported
6398

64-
// 3) Scan our sources and compare
99+
// 2) Scan our sources and compare
65100
val repoRoot = target.projectDir.toPath()
66101
val tokenRegex = Regex("\"(?:DD_|OTEL_)[A-Za-z0-9_]+\"")
67102

@@ -79,7 +114,7 @@ private fun registerLogEnvVarUsages(target: Project, extension: SupportedTracerC
79114
}
80115
tokenRegex.findAll(raw).forEach { m ->
81116
val token = m.value.trim('"')
82-
if (token !in supported) add("$rel:${i + 1} -> Unsupported token'$token'")
117+
if (token !in supported) add("$rel:${i + 1} -> Unsupported token '$token'")
83118
}
84119
}
85120
}
@@ -167,15 +202,9 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte
167202
throw GradleException("Config directory not found: ${configDir.absolutePath}")
168203
}
169204

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-
}
205+
val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get())
206+
val supported = configFields.supported
207+
val aliasMapping = configFields.aliasMapping
179208

180209
var parserConfig = ParserConfiguration()
181210
parserConfig.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8)
@@ -192,23 +221,23 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte
192221
.map { it as? FieldDeclaration }
193222
.ifPresent { field ->
194223
if (field.hasModifiers(Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC, Modifier.Keyword.FINAL) &&
195-
varDecl.typeAsString == "String") {
224+
varDecl.typeAsString == "String") {
196225

197-
val fieldName = varDecl.nameAsString
198-
if (fieldName.endsWith("_DEFAULT")) return@ifPresent
199-
val init = varDecl.initializer.orElse(null) ?: return@ifPresent
226+
val fieldName = varDecl.nameAsString
227+
if (fieldName.endsWith("_DEFAULT")) return@ifPresent
228+
val init = varDecl.initializer.orElse(null) ?: return@ifPresent
200229

201-
if (init !is StringLiteralExpr) return@ifPresent
202-
val rawValue = init.value
230+
if (init !is StringLiteralExpr) return@ifPresent
231+
val rawValue = init.value
203232

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()}'")
233+
val normalized = normalize(rawValue)
234+
if (normalized !in supported && normalized !in aliasMapping) {
235+
val line = varDecl.range.map { it.begin.line }.orElse(1)
236+
add("$fileName:$line -> Config '$rawValue' normalizes to '$normalized' " +
237+
"which is missing from '${extension.jsonFile.get()}'")
238+
}
209239
}
210240
}
211-
}
212241
}
213242
}
214243
}
@@ -223,3 +252,44 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte
223252
}
224253
}
225254
}
255+
256+
257+
/** Registers `verifyAliasKeysAreSupported` to ensure all alias keys are documented as supported configurations. */
258+
private fun verifyAliasKeysAreSupported(project: Project, extension: SupportedTracerConfigurations) {
259+
val ownerPath = extension.configOwnerPath
260+
val generatedFile = extension.className
261+
262+
project.tasks.register("verifyAliasKeysAreSupported") {
263+
group = "verification"
264+
description =
265+
"Verifies that all alias keys in `metadata/supported-configurations.json` are also documented as supported configurations."
266+
267+
val mainSourceSetOutput = ownerPath.map {
268+
project.project(it)
269+
.extensions.getByType<SourceSetContainer>()
270+
.named(SourceSet.MAIN_SOURCE_SET_NAME)
271+
.map { main -> main.output }
272+
}
273+
inputs.files(mainSourceSetOutput)
274+
275+
doLast {
276+
val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get())
277+
val supported = configFields.supported
278+
val aliases = configFields.aliases.keys
279+
280+
val unsupportedAliasKeys = aliases - supported
281+
val violations = buildList {
282+
unsupportedAliasKeys.forEach { key ->
283+
add("$key is listed as an alias key but is not documented as a supported configuration in the `supportedConfigurations` key")
284+
}
285+
}
286+
if (violations.isNotEmpty()) {
287+
logger.error("\nFound alias keys not documented as supported configurations:")
288+
violations.forEach { logger.lifecycle(it) }
289+
throw GradleException("Undocumented alias keys found. Please add the above keys to the `supportedConfigurations` in '${extension.jsonFile.get()}'.")
290+
} else {
291+
logger.info("All alias keys are documented as supported configurations.")
292+
}
293+
}
294+
}
295+
}

0 commit comments

Comments
 (0)