Skip to content

Commit 4eeaecd

Browse files
committed
Add analyzer to enforce ConfigurationBuilder only with configuration keys or platform keys
1 parent c6e02b6 commit 4eeaecd

File tree

4 files changed

+773
-10
lines changed

4 files changed

+773
-10
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// <copyright file="ConfigurationBuilderWithKeysAnalyzer.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
using System.Collections.Immutable;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
12+
namespace Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers
13+
{
14+
/// <summary>
15+
/// Analyzer to ensure that ConfigurationBuilder.WithKeys method calls only accept string constants
16+
/// from PlatformKeys or ConfigurationKeys classes, not hardcoded strings or variables.
17+
/// </summary>
18+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
19+
public class ConfigurationBuilderWithKeysAnalyzer : DiagnosticAnalyzer
20+
{
21+
/// <summary>
22+
/// Diagnostic descriptor for when WithKeys or Or is called with a hardcoded string instead of a constant from PlatformKeys or ConfigurationKeys.
23+
/// </summary>
24+
public static readonly DiagnosticDescriptor UseConfigurationConstantsRule = new(
25+
id: "DD0007",
26+
title: "Use configuration constants instead of hardcoded strings in WithKeys/Or calls",
27+
messageFormat: "{0} method should use constants from PlatformKeys or ConfigurationKeys classes instead of hardcoded string '{1}'",
28+
category: "Usage",
29+
defaultSeverity: DiagnosticSeverity.Error,
30+
isEnabledByDefault: true,
31+
description: "ConfigurationBuilder.WithKeys and HasKeys.Or method calls should only accept string constants from PlatformKeys or ConfigurationKeys classes to ensure consistency and avoid typos.");
32+
33+
/// <summary>
34+
/// Diagnostic descriptor for when WithKeys or Or is called with a variable instead of a constant from PlatformKeys or ConfigurationKeys.
35+
/// </summary>
36+
public static readonly DiagnosticDescriptor UseConfigurationConstantsNotVariablesRule = new(
37+
id: "DD0008",
38+
title: "Use configuration constants instead of variables in WithKeys/Or calls",
39+
messageFormat: "{0} method should use constants from PlatformKeys or ConfigurationKeys classes instead of variable '{1}'",
40+
category: "Usage",
41+
defaultSeverity: DiagnosticSeverity.Error,
42+
isEnabledByDefault: true,
43+
description: "ConfigurationBuilder.WithKeys and HasKeys.Or method calls should only accept string constants from PlatformKeys or ConfigurationKeys classes, not variables or computed values.");
44+
45+
/// <summary>
46+
/// Gets the supported diagnostics
47+
/// </summary>
48+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
49+
ImmutableArray.Create(UseConfigurationConstantsRule, UseConfigurationConstantsNotVariablesRule);
50+
51+
/// <summary>
52+
/// Initialize the analyzer
53+
/// </summary>
54+
/// <param name="context">context</param>
55+
public override void Initialize(AnalysisContext context)
56+
{
57+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
58+
context.EnableConcurrentExecution();
59+
context.RegisterSyntaxNodeAction(AnalyzeInvocationExpression, SyntaxKind.InvocationExpression);
60+
}
61+
62+
private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context)
63+
{
64+
var invocation = (InvocationExpressionSyntax)context.Node;
65+
66+
// Check if this is a WithKeys or Or method call
67+
var methodName = GetConfigurationMethodName(invocation, context.SemanticModel);
68+
if (methodName == null)
69+
{
70+
return;
71+
}
72+
73+
// Analyze each argument to the method
74+
var argumentList = invocation.ArgumentList;
75+
if (argumentList?.Arguments.Count > 0)
76+
{
77+
var argument = argumentList.Arguments[0]; // Both WithKeys and Or take a single string argument
78+
AnalyzeConfigurationArgument(context, argument, methodName);
79+
}
80+
}
81+
82+
private static string GetConfigurationMethodName(InvocationExpressionSyntax invocation, SemanticModel semanticModel)
83+
{
84+
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
85+
{
86+
var methodName = memberAccess.Name.Identifier.ValueText;
87+
88+
// Check if the method being called is "WithKeys" or "Or"
89+
const string withKeysMethodName = "WithKeys";
90+
const string orMethodName = "Or";
91+
if (methodName is withKeysMethodName or orMethodName)
92+
{
93+
// Get the symbol info for the method
94+
var symbolInfo = semanticModel.GetSymbolInfo(memberAccess);
95+
if (symbolInfo.Symbol is IMethodSymbol method)
96+
{
97+
var containingType = method.ContainingType?.Name;
98+
var containingNamespace = method.ContainingNamespace?.ToDisplayString();
99+
100+
// Check if this is the ConfigurationBuilder.WithKeys method
101+
if (methodName == withKeysMethodName &&
102+
containingType == "ConfigurationBuilder" &&
103+
containingNamespace == "Datadog.Trace.Configuration.Telemetry")
104+
{
105+
return withKeysMethodName;
106+
}
107+
108+
// Check if this is the HasKeys.Or method
109+
if (methodName == orMethodName &&
110+
containingType == "HasKeys" &&
111+
containingNamespace == "Datadog.Trace.Configuration.Telemetry")
112+
{
113+
return orMethodName;
114+
}
115+
}
116+
}
117+
}
118+
119+
return null;
120+
}
121+
122+
private static void AnalyzeConfigurationArgument(SyntaxNodeAnalysisContext context, ArgumentSyntax argument, string methodName)
123+
{
124+
var expression = argument.Expression;
125+
126+
switch (expression)
127+
{
128+
case LiteralExpressionSyntax literal when literal.Token.IsKind(SyntaxKind.StringLiteralToken):
129+
// This is a hardcoded string literal - report diagnostic
130+
var literalValue = literal.Token.ValueText;
131+
var diagnostic = Diagnostic.Create(
132+
UseConfigurationConstantsRule,
133+
literal.GetLocation(),
134+
methodName,
135+
literalValue);
136+
context.ReportDiagnostic(diagnostic);
137+
break;
138+
139+
case MemberAccessExpressionSyntax memberAccess:
140+
// Check if this is accessing a constant from PlatformKeys or ConfigurationKeys
141+
if (!IsValidConfigurationConstant(memberAccess, context.SemanticModel))
142+
{
143+
// This is accessing something else - report diagnostic
144+
var memberName = memberAccess.ToString();
145+
var memberDiagnostic = Diagnostic.Create(
146+
UseConfigurationConstantsNotVariablesRule,
147+
memberAccess.GetLocation(),
148+
methodName,
149+
memberName);
150+
context.ReportDiagnostic(memberDiagnostic);
151+
}
152+
153+
break;
154+
155+
case IdentifierNameSyntax identifier:
156+
// This is a variable or local constant - report diagnostic
157+
var identifierName = identifier.Identifier.ValueText;
158+
var variableDiagnostic = Diagnostic.Create(
159+
UseConfigurationConstantsNotVariablesRule,
160+
identifier.GetLocation(),
161+
methodName,
162+
identifierName);
163+
context.ReportDiagnostic(variableDiagnostic);
164+
break;
165+
166+
default:
167+
// Any other expression type (method calls, computed values, etc.) - report diagnostic
168+
var expressionText = expression.ToString();
169+
var defaultDiagnostic = Diagnostic.Create(
170+
UseConfigurationConstantsNotVariablesRule,
171+
expression.GetLocation(),
172+
methodName,
173+
expressionText);
174+
context.ReportDiagnostic(defaultDiagnostic);
175+
break;
176+
}
177+
}
178+
179+
private static bool IsValidConfigurationConstant(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel)
180+
{
181+
var symbolInfo = semanticModel.GetSymbolInfo(memberAccess);
182+
if (symbolInfo.Symbol is IFieldSymbol field)
183+
{
184+
// Check if this is a const string field
185+
if (field.IsConst && field.Type?.SpecialType == SpecialType.System_String)
186+
{
187+
var containingType = field.ContainingType;
188+
if (containingType != null)
189+
{
190+
// Check if the containing type is PlatformKeys or ConfigurationKeys (or their nested classes)
191+
return IsValidConfigurationClass(containingType);
192+
}
193+
}
194+
}
195+
196+
return false;
197+
}
198+
199+
private static bool IsValidConfigurationClass(INamedTypeSymbol typeSymbol)
200+
{
201+
// Check if this is PlatformKeys or ConfigurationKeys class or their nested classes
202+
var currentType = typeSymbol;
203+
while (currentType != null)
204+
{
205+
var typeName = currentType.Name;
206+
var namespaceName = currentType.ContainingNamespace?.ToDisplayString();
207+
208+
// Check for PlatformKeys class
209+
if (typeName == "PlatformKeys" && namespaceName == "Datadog.Trace.Configuration")
210+
{
211+
return true;
212+
}
213+
214+
// Check for ConfigurationKeys class
215+
if (typeName == "ConfigurationKeys" && namespaceName == "Datadog.Trace.Configuration")
216+
{
217+
return true;
218+
}
219+
220+
// Check nested classes within PlatformKeys or ConfigurationKeys
221+
currentType = currentType.ContainingType;
222+
}
223+
224+
return false;
225+
}
226+
}
227+
}

tracer/src/Datadog.Trace/Configuration/MutableSettings.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,9 @@ private MutableSettings(
265265

266266
internal IConfigurationTelemetry Telemetry { get; }
267267

268-
internal static ReadOnlyDictionary<string, string>? InitializeHeaderTags(ConfigurationBuilder config, string key, bool headerTagsNormalizationFixEnabled)
268+
internal static ReadOnlyDictionary<string, string>? InitializeHeaderTags(ConfigurationBuilder.HasKeys key, bool headerTagsNormalizationFixEnabled)
269269
=> InitializeHeaderTags(
270-
config.WithKeys(key).AsDictionaryResult(allowOptionalMappings: true),
270+
key.AsDictionaryResult(allowOptionalMappings: true),
271271
headerTagsNormalizationFixEnabled);
272272

273273
private static ReadOnlyDictionary<string, string>? InitializeHeaderTags(
@@ -1001,10 +1001,10 @@ public static MutableSettings CreateInitialMutableSettings(
10011001
.AsBool(defaultValue: true);
10021002

10031003
// Filter out tags with empty keys or empty values, and trim whitespaces
1004-
var headerTags = InitializeHeaderTags(config, ConfigurationKeys.HeaderTags, headerTagsNormalizationFixEnabled) ?? ReadOnlyDictionary.Empty;
1004+
var headerTags = InitializeHeaderTags(config.WithKeys(ConfigurationKeys.HeaderTags), headerTagsNormalizationFixEnabled) ?? ReadOnlyDictionary.Empty;
10051005

10061006
// Filter out tags with empty keys or empty values, and trim whitespaces
1007-
var grpcTags = InitializeHeaderTags(config, ConfigurationKeys.GrpcTags, headerTagsNormalizationFixEnabled: true) ?? ReadOnlyDictionary.Empty;
1007+
var grpcTags = InitializeHeaderTags(config.WithKeys(ConfigurationKeys.GrpcTags), headerTagsNormalizationFixEnabled: true) ?? ReadOnlyDictionary.Empty;
10081008

10091009
var customSamplingRules = config.WithKeys(ConfigurationKeys.CustomSamplingRules).AsString();
10101010

@@ -1023,7 +1023,7 @@ public static MutableSettings CreateInitialMutableSettings(
10231023
var kafkaCreateConsumerScopeEnabled = config
10241024
.WithKeys(ConfigurationKeys.KafkaCreateConsumerScopeEnabled)
10251025
.AsBool(defaultValue: true);
1026-
var serviceNameMappings = TracerSettings.InitializeServiceNameMappings(config, ConfigurationKeys.ServiceNameMappings) ?? ReadOnlyDictionary.Empty;
1026+
var serviceNameMappings = TracerSettings.TrimConfigKeysValues(config.WithKeys(ConfigurationKeys.ServiceNameMappings)) ?? ReadOnlyDictionary.Empty;
10271027

10281028
var tracerMetricsEnabled = config
10291029
.WithKeys(ConfigurationKeys.TracerMetricsEnabled)

tracer/src/Datadog.Trace/Configuration/TracerSettings.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ internal TracerSettings(IConfigurationSource? source, IConfigurationTelemetry te
148148
.WithKeys(ConfigurationKeys.SpanPointersEnabled)
149149
.AsBool(defaultValue: true);
150150

151-
PeerServiceNameMappings = InitializeServiceNameMappings(config, ConfigurationKeys.PeerServiceNameMappings);
151+
PeerServiceNameMappings = TrimConfigKeysValues(config.WithKeys(ConfigurationKeys.PeerServiceNameMappings));
152152

153153
MetadataSchemaVersion = config
154154
.WithKeys(ConfigurationKeys.MetadataSchemaVersion)
@@ -1328,11 +1328,9 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) =
13281328
internal static TracerSettings FromDefaultSourcesInternal()
13291329
=> new(GlobalConfigurationSource.Instance, new ConfigurationTelemetry(), new());
13301330

1331-
internal static ReadOnlyDictionary<string, string>? InitializeServiceNameMappings(ConfigurationBuilder config, string key)
1331+
internal static ReadOnlyDictionary<string, string>? TrimConfigKeysValues(ConfigurationBuilder.HasKeys key)
13321332
{
1333-
var mappings = config
1334-
.WithKeys(key)
1335-
.AsDictionary()
1333+
var mappings = key.AsDictionary()
13361334
?.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key) && !string.IsNullOrWhiteSpace(kvp.Value))
13371335
.ToDictionary(kvp => kvp.Key.Trim(), kvp => kvp.Value.Trim());
13381336
return mappings is not null ? new(mappings) : null;

0 commit comments

Comments
 (0)