Skip to content
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
id("datadog.dependency-locking")
id("datadog.tracer-version")
id("datadog.dump-hanged-test")
id("config-inversion-linter")
id("datadog.ci-jobs")

id("com.diffplug.spotless") version "6.13.0"
Expand Down
1 change: 1 addition & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ dependencies {
implementation("org.apache.maven", "maven-aether-provider", "3.3.9")

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

implementation("com.google.guava", "guava", "20.0")
implementation(libs.asm)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package datadog.gradle.plugin.config

import com.github.javaparser.ParserConfiguration
import com.github.javaparser.StaticJavaParser
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.expr.StringLiteralExpr
import com.github.javaparser.ast.nodeTypes.NodeWithModifiers
import com.github.javaparser.ast.Modifier
import com.github.javaparser.ast.body.FieldDeclaration
import com.github.javaparser.ast.body.VariableDeclarator
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.GradleException
Expand All @@ -14,6 +22,7 @@ class ConfigInversionLinter : Plugin<Project> {
val extension = target.extensions.create("supportedTracerConfigurations", SupportedTracerConfigurations::class.java)
registerLogEnvVarUsages(target, extension)
registerCheckEnvironmentVariablesUsage(target)
registerCheckConfigStringsTask(target, extension)
}
}

Expand Down Expand Up @@ -124,3 +133,91 @@ private fun registerCheckEnvironmentVariablesUsage(project: Project) {
}
}
}

/** Registers `checkConfigStrings` to validate config definitions against documented supported configurations. */
private fun registerCheckConfigStringsTask(project: Project, extension: SupportedTracerConfigurations) {
val ownerPath = extension.configOwnerPath
val generatedFile = extension.className

project.tasks.register("checkConfigStrings") {
group = "verification"
description = "Validates that all config definitions in `dd-trace-api/src/main/java/datadog/trace/api/config` exist in `metadata/supported-configurations.json`"

val mainSourceSetOutput = ownerPath.map {
project.project(it)
.extensions.getByType<SourceSetContainer>()
.named(SourceSet.MAIN_SOURCE_SET_NAME)
.map { main -> main.output }
}
inputs.files(mainSourceSetOutput)

doLast {
val repoRoot: Path = project.rootProject.projectDir.toPath()
val configDir = repoRoot.resolve("dd-trace-api/src/main/java/datadog/trace/api/config").toFile()

if (!configDir.exists()) {
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<String>
@Suppress("UNCHECKED_CAST")
val aliasMappingMap = clazz.getField("ALIAS_MAPPING").get(null) as Map<String, String>
Pair(supportedSet, aliasMappingMap)
}

StaticJavaParser.setConfiguration(ParserConfiguration())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: I wonder if we should set explicitly the language level 🤔 to 1.8, when looking at the code of java parser, I see that the default is set to the POPULAR constant, however POPULAR appear to change regularly in minor versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to currently be set as Java 11, and was changed from Java 8 four years ago. But I agree that we can manually set it for consistency


// Checking "public" "static" "final"
fun NodeWithModifiers<*>.hasModifiers(vararg mods: Modifier.Keyword) =
mods.all { hasModifier(it) }

fun normalize(configValue: String) =
"DD_" + configValue.uppercase().replace("-", "_").replace(".", "_")

val violations = buildList {
configDir.listFiles()?.forEach { file ->
val fileName = file.name
val cu: CompilationUnit = StaticJavaParser.parse(file)

cu.findAll(VariableDeclarator::class.java).forEach { varDecl ->
val field = varDecl.parentNode
.filter { it is FieldDeclaration }
.map { it as FieldDeclaration }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I believe it's possible to avoid the filter operation by returning null in the mapping lambda, note the as? in kotlin which casts to given type or return null.

Suggested change
.filter { it is FieldDeclaration }
.map { it as FieldDeclaration }
.map { it as? FieldDeclaration }

.orElse(null)

if (field != null &&
field.hasModifiers(Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC, Modifier.Keyword.FINAL) &&
varDecl.typeAsString == "String") {

val fieldName = varDecl.nameAsString
if (fieldName.endsWith("_DEFAULT")) return@forEach
val init = varDecl.initializer.orElse(null) ?: return@forEach

if (init !is StringLiteralExpr) return@forEach
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()}'")
}
}
}
}
}

if (violations.isNotEmpty()) {
logger.error("\nFound config definitions not in '${extension.jsonFile.get()}':")
violations.forEach { logger.lifecycle(it) }
throw GradleException("Undocumented Environment Variables found. Please add the above Environment Variables to '${extension.jsonFile.get()}'.")
} else {
logger.info("All config strings are present in '${extension.jsonFile.get()}'.")
}
}
}
}