@@ -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(" \n Found 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