From 3e8b52486be26eda06686e0e326fb5498cb3ccb8 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Tue, 2 Dec 2025 13:56:55 -0800 Subject: [PATCH 1/2] adding new config inversion linter --- .gitlab-ci.yml | 2 +- .../plugin/config/ConfigInversionLinter.kt | 132 ++++++++++++++---- 2 files changed, 102 insertions(+), 32 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 21759852a4e..aaae3a2e6ac 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -374,7 +374,7 @@ config-inversion-linter: needs: [] script: - ./gradlew --version - - ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings + - ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings verifyAliasKeysAreSupported test_published_artifacts: extends: .gradle_build diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt index 6233a1d9e29..f1f346d12cb 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt @@ -23,6 +23,46 @@ class ConfigInversionLinter : Plugin { registerLogEnvVarUsages(target, extension) registerCheckEnvironmentVariablesUsage(target) registerCheckConfigStringsTask(target, extension) + verifyAliasKeysAreSupported(target, extension) + } +} + +// Data class for fields from generated class +private data class LoadedConfigFields( + val supported: Set, + val aliases: Map> = emptyMap(), + val aliasMapping: Map = emptyMap() +) + +// Cache for fields from generated class +private var cachedConfigFields: LoadedConfigFields? = null + +// Helper function to load fields from the generated class +private fun loadConfigFields( + mainSourceSetOutput: org.gradle.api.file.FileCollection, + generatedClassName: String +): LoadedConfigFields { + return cachedConfigFields ?: run { + val urls = mainSourceSetOutput.files.map { it.toURI().toURL() }.toTypedArray() + URLClassLoader(urls, LoadedConfigFields::class.java.classLoader).use { cl -> + val clazz = Class.forName(generatedClassName, true, cl) + + val supportedField = clazz.getField("SUPPORTED").get(null) + @Suppress("UNCHECKED_CAST") + val supportedSet = when (supportedField) { + is Set<*> -> supportedField as Set + is Map<*, *> -> supportedField.keys as Set + else -> throw IllegalStateException("SUPPORTED field must be either Set or Map, but was ${supportedField?.javaClass}") + } + + @Suppress("UNCHECKED_CAST") + val aliases = clazz.getField("ALIASES").get(null) as Map> + + @Suppress("UNCHECKED_CAST") + val aliasMappingMap = clazz.getField("ALIAS_MAPPING").get(null) as Map + + LoadedConfigFields(supportedSet, aliases, aliasMappingMap) + }.also { cachedConfigFields = it } } } @@ -52,16 +92,11 @@ private fun registerLogEnvVarUsages(target: Project, extension: SupportedTracerC inputs.files(javaFiles) outputs.upToDateWhen { true } doLast { - // 1) Build classloader from the owner project’s runtime classpath - val urls = mainSourceSetOutput.get().get().files.map { it.toURI().toURL() }.toTypedArray() - val supported: Set = URLClassLoader(urls, javaClass.classLoader).use { cl -> - // 2) Load the generated class + read static field - val clazz = Class.forName(generatedFile.get(), true, cl) - @Suppress("UNCHECKED_CAST") - clazz.getField("SUPPORTED").get(null) as Set - } + // 1) Load configuration fields from the generated class + val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get()) + val supported = configFields.supported - // 3) Scan our sources and compare + // 2) Scan our sources and compare val repoRoot = target.projectDir.toPath() val tokenRegex = Regex("\"(?:DD_|OTEL_)[A-Za-z0-9_]+\"") @@ -79,7 +114,7 @@ private fun registerLogEnvVarUsages(target: Project, extension: SupportedTracerC } tokenRegex.findAll(raw).forEach { m -> val token = m.value.trim('"') - if (token !in supported) add("$rel:${i + 1} -> Unsupported token'$token'") + if (token !in supported) add("$rel:${i + 1} -> Unsupported token '$token'") } } } @@ -167,15 +202,9 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte throw GradleException("Config directory not found: ${configDir.absolutePath}") } - val urls = mainSourceSetOutput.get().get().files.map { it.toURI().toURL() }.toTypedArray() - val (supported, aliasMapping) = URLClassLoader(urls, javaClass.classLoader).use { cl -> - val clazz = Class.forName(generatedFile.get(), true, cl) - @Suppress("UNCHECKED_CAST") - val supportedSet = clazz.getField("SUPPORTED").get(null) as Set - @Suppress("UNCHECKED_CAST") - val aliasMappingMap = clazz.getField("ALIAS_MAPPING").get(null) as Map - Pair(supportedSet, aliasMappingMap) - } + val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get()) + val supported = configFields.supported + val aliasMapping = configFields.aliasMapping var parserConfig = ParserConfiguration() parserConfig.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8) @@ -192,23 +221,23 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte .map { it as? FieldDeclaration } .ifPresent { field -> if (field.hasModifiers(Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC, Modifier.Keyword.FINAL) && - varDecl.typeAsString == "String") { + varDecl.typeAsString == "String") { - val fieldName = varDecl.nameAsString - if (fieldName.endsWith("_DEFAULT")) return@ifPresent - val init = varDecl.initializer.orElse(null) ?: return@ifPresent + val fieldName = varDecl.nameAsString + if (fieldName.endsWith("_DEFAULT")) return@ifPresent + val init = varDecl.initializer.orElse(null) ?: return@ifPresent - if (init !is StringLiteralExpr) return@ifPresent - val rawValue = init.value + if (init !is StringLiteralExpr) return@ifPresent + val rawValue = init.value - val normalized = normalize(rawValue) - if (normalized !in supported && normalized !in aliasMapping) { - val line = varDecl.range.map { it.begin.line }.orElse(1) - add("$fileName:$line -> Config '$rawValue' normalizes to '$normalized' " + - "which is missing from '${extension.jsonFile.get()}'") + val normalized = normalize(rawValue) + if (normalized !in supported && normalized !in aliasMapping) { + val line = varDecl.range.map { it.begin.line }.orElse(1) + add("$fileName:$line -> Config '$rawValue' normalizes to '$normalized' " + + "which is missing from '${extension.jsonFile.get()}'") + } } } - } } } } @@ -223,3 +252,44 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte } } } + + +/** Registers `verifyAliasKeysAreSupported` to ensure all alias keys are documented as supported configurations. */ +private fun verifyAliasKeysAreSupported(project: Project, extension: SupportedTracerConfigurations) { + val ownerPath = extension.configOwnerPath + val generatedFile = extension.className + + project.tasks.register("verifyAliasKeysAreSupported") { + group = "verification" + description = + "Verifies that all alias keys in `metadata/supported-configurations.json` are also documented as supported configurations." + + val mainSourceSetOutput = ownerPath.map { + project.project(it) + .extensions.getByType() + .named(SourceSet.MAIN_SOURCE_SET_NAME) + .map { main -> main.output } + } + inputs.files(mainSourceSetOutput) + + doLast { + val configFields = loadConfigFields(mainSourceSetOutput.get().get(), generatedFile.get()) + val supported = configFields.supported + val aliases = configFields.aliases.keys + + val unsupportedAliasKeys = aliases - supported + val violations = buildList { + unsupportedAliasKeys.forEach { key -> + add("$key is listed as an alias key but is not documented as a supported configuration in the `supportedConfigurations` key") + } + } + if (violations.isNotEmpty()) { + logger.error("\nFound alias keys not documented as supported configurations:") + violations.forEach { logger.lifecycle(it) } + throw GradleException("Undocumented alias keys found. Please add the above keys to the `supportedConfigurations` in '${extension.jsonFile.get()}'.") + } else { + logger.info("All alias keys are documented as supported configurations.") + } + } + } +} From f907aa74c8c9b58c8e02818d37fca52a603e90d0 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Tue, 2 Dec 2025 14:22:36 -0800 Subject: [PATCH 2/2] removing redeclaration --- .../plugin/config/ConfigInversionLinter.kt | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt index c49bd47778f..f1f346d12cb 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt @@ -66,40 +66,6 @@ private fun loadConfigFields( } } -// Data class for fields from generated class -private data class LoadedConfigFields( - val supported: Set, - val aliasMapping: Map = emptyMap() -) - -// Cache for fields from generated class -private var cachedConfigFields: LoadedConfigFields? = null - -// Helper function to load fields from the generated class -private fun loadConfigFields( - mainSourceSetOutput: org.gradle.api.file.FileCollection, - generatedClassName: String -): LoadedConfigFields { - return cachedConfigFields ?: run { - val urls = mainSourceSetOutput.files.map { it.toURI().toURL() }.toTypedArray() - URLClassLoader(urls, LoadedConfigFields::class.java.classLoader).use { cl -> - val clazz = Class.forName(generatedClassName, true, cl) - - val supportedField = clazz.getField("SUPPORTED").get(null) - @Suppress("UNCHECKED_CAST") - val supportedSet = when (supportedField) { - is Set<*> -> supportedField as Set - is Map<*, *> -> supportedField.keys as Set - else -> throw IllegalStateException("SUPPORTED field must be either Set or Map, but was ${supportedField?.javaClass}") - } - - @Suppress("UNCHECKED_CAST") - val aliasMappingMap = clazz.getField("ALIAS_MAPPING").get(null) as Map - LoadedConfigFields(supportedSet, aliasMappingMap) - }.also { cachedConfigFields = it } - } -} - /** Registers `logEnvVarUsages` (scan for DD_/OTEL_ tokens and fail if unsupported). */ private fun registerLogEnvVarUsages(target: Project, extension: SupportedTracerConfigurations) { val ownerPath = extension.configOwnerPath