Skip to content

Commit 09ed5a9

Browse files
committed
Platform key analyzer
1 parent ddcd60f commit 09ed5a9

File tree

2 files changed

+272
-0
lines changed

2 files changed

+272
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// <copyright file="PlatformKeysAnalyzer.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+
#nullable enable
7+
using System;
8+
using System.Collections.Immutable;
9+
using System.Linq;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.Diagnostics;
12+
13+
namespace Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers;
14+
15+
/// <summary>
16+
/// DD0010: Invalid PlatformKeys constant naming
17+
///
18+
/// Ensures that constants in the PlatformKeys class do not start with reserved prefixes:
19+
/// - OTEL (OpenTelemetry prefix)
20+
/// - DD_ (Datadog configuration prefix)
21+
/// - _DD_ (Internal Datadog configuration prefix)
22+
/// - DATADOG_ (Older Datadog configuration prefix)
23+
///
24+
/// Platform keys should represent environment variables from external platforms/services,
25+
/// not Datadog-specific or OpenTelemetry configuration keys.
26+
/// </summary>
27+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
28+
public sealed class PlatformKeysAnalyzer : DiagnosticAnalyzer
29+
{
30+
/// <summary>
31+
/// The diagnostic ID displayed in error messages
32+
/// </summary>
33+
public const string DiagnosticId = "DD0010";
34+
35+
private const string PlatformKeysClassName = "PlatformKeys";
36+
private const string PlatformKeysNamespace = "Datadog.Trace.Configuration";
37+
38+
private static readonly string[] ForbiddenPrefixes = { "OTEL", "DD_", "_DD_", "DATADOG_ " };
39+
40+
private static readonly DiagnosticDescriptor Rule = new(
41+
DiagnosticId,
42+
title: "Invalid PlatformKeys constant naming",
43+
messageFormat: "PlatformKeys constant '{0}' should not start with '{1}'. Platform keys should represent external environment variables, not Datadog or OpenTelemetry configuration keys. Use ConfigurationKeys instead.",
44+
category: "CodeQuality",
45+
defaultSeverity: DiagnosticSeverity.Error,
46+
isEnabledByDefault: true,
47+
description: "Constants in PlatformKeys class should not start with OTEL, DD_, or _DD_ prefixes as these are reserved for OpenTelemetry and Datadog configuration keys. Platform keys should represent environment variables from external platforms and services.");
48+
49+
/// <inheritdoc />
50+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);
51+
52+
/// <inheritdoc />
53+
public override void Initialize(AnalysisContext context)
54+
{
55+
context.EnableConcurrentExecution();
56+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
57+
context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
58+
}
59+
60+
private static void AnalyzeNamedType(SymbolAnalysisContext context)
61+
{
62+
var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
63+
64+
// Check if this is the PlatformKeys class in the correct namespace
65+
if (!IsPlatformKeysClass(namedTypeSymbol))
66+
{
67+
return;
68+
}
69+
70+
// Analyze all const fields in the PlatformKeys class and its nested classes
71+
AnalyzeConstFields(context, namedTypeSymbol);
72+
}
73+
74+
private static bool IsPlatformKeysClass(INamedTypeSymbol namedTypeSymbol)
75+
{
76+
// Check if this is the PlatformKeys class (including partial classes)
77+
if (namedTypeSymbol.Name != PlatformKeysClassName)
78+
{
79+
return false;
80+
}
81+
82+
// Check if it's in the correct namespace
83+
var containingNamespace = namedTypeSymbol.ContainingNamespace;
84+
return containingNamespace?.ToDisplayString() == PlatformKeysNamespace;
85+
}
86+
87+
private static void AnalyzeConstFields(SymbolAnalysisContext context, INamedTypeSymbol typeSymbol)
88+
{
89+
// Analyze const fields in the current type
90+
foreach (var member in typeSymbol.GetMembers())
91+
{
92+
if (member is IFieldSymbol { IsConst: true, Type.SpecialType: SpecialType.System_String } field)
93+
{
94+
AnalyzeConstField(context, field);
95+
}
96+
else if (member is INamedTypeSymbol nestedType)
97+
{
98+
// Recursively analyze nested classes (like Aws, AzureAppService, etc.)
99+
AnalyzeConstFields(context, nestedType);
100+
}
101+
}
102+
}
103+
104+
private static void AnalyzeConstField(SymbolAnalysisContext context, IFieldSymbol field)
105+
{
106+
if (field.ConstantValue is not string constantValue)
107+
{
108+
return;
109+
}
110+
111+
// Check if the constant value starts with any forbidden prefix (case-insensitive)
112+
var forbiddenPrefix = ForbiddenPrefixes.FirstOrDefault(prefix => constantValue.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
113+
if (forbiddenPrefix == null)
114+
{
115+
return;
116+
}
117+
118+
var diagnostic = Diagnostic.Create(
119+
Rule,
120+
field.Locations.FirstOrDefault(),
121+
constantValue,
122+
forbiddenPrefix);
123+
124+
context.ReportDiagnostic(diagnostic);
125+
}
126+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// <copyright file="PlatformKeysAnalyzerTests.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.Threading.Tasks;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.Testing;
9+
using Xunit;
10+
using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<
11+
Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers.PlatformKeysAnalyzer,
12+
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
13+
14+
namespace Datadog.Trace.Tools.Analyzers.Tests.ConfigurationAnalyzers;
15+
16+
public class PlatformKeysAnalyzerTests
17+
{
18+
private const string DiagnosticId = Datadog.Trace.Tools.Analyzers.ConfigurationAnalyzers.PlatformKeysAnalyzer.DiagnosticId;
19+
20+
[Fact]
21+
public async Task ValidPlatformKeysAndEdgeCasesShouldNotHaveDiagnostics()
22+
{
23+
var code = """
24+
#nullable enable
25+
namespace Datadog.Trace.Configuration;
26+
27+
internal static partial class PlatformKeys
28+
{
29+
// Valid platform keys
30+
public const string ValidKey1 = "CORECLR_PROFILER_PATH";
31+
public const string ValidKey2 = "AWS_LAMBDA_FUNCTION_NAME";
32+
public const string ValidKey3 = "WEBSITE_SITE_NAME";
33+
34+
// Non-const fields should be ignored
35+
public static readonly string ReadOnlyField = "DD_TRACE_ENABLED";
36+
public static string StaticField = "OTEL_SERVICE_NAME";
37+
38+
// Non-string constants should be ignored
39+
public const int IntConstant = 42;
40+
public const bool BoolConstant = true;
41+
42+
// Edge cases - prefixes in middle/end should NOT trigger
43+
public const string OtelButNotPrefix = "SOMETHING_OTEL_VALUE";
44+
public const string DdButNotPrefix = "SOMETHING_DD_VALUE";
45+
46+
internal class Aws
47+
{
48+
public const string FunctionName = "AWS_LAMBDA_FUNCTION_NAME";
49+
public const string Region = "AWS_REGION";
50+
}
51+
}
52+
""";
53+
54+
await Verifier.VerifyAnalyzerAsync(code);
55+
}
56+
57+
[Theory]
58+
[InlineData("OTEL_RESOURCE_ATTRIBUTES", "OTEL")] // Uppercase
59+
[InlineData("otel_service_name", "OTEL")] // Lowercase (case insensitive)
60+
[InlineData("Otel_Exporter_Endpoint", "OTEL")] // Mixed case
61+
[InlineData("DD_TRACE_ENABLED", "DD_")] // Uppercase
62+
[InlineData("dd_agent_host", "DD_")] // Lowercase (case insensitive)
63+
[InlineData("Dd_Version", "DD_")] // Mixed case
64+
[InlineData("_DD_TRACE_DEBUG", "_DD_")] // Uppercase
65+
[InlineData("_dd_profiler_enabled", "_DD_")] // Lowercase (case insensitive)
66+
[InlineData("_Dd_Test_Config", "_DD_")] // Mixed case
67+
public async Task InvalidPlatformKeysConstantsShouldHaveDiagnostics(string invalidValue, string expectedPrefix)
68+
{
69+
var code = $$"""
70+
#nullable enable
71+
namespace Datadog.Trace.Configuration;
72+
73+
internal static partial class PlatformKeys
74+
{
75+
public const string {|#0:InvalidKey|} = "{{invalidValue}}";
76+
}
77+
""";
78+
79+
var expected = new DiagnosticResult(DiagnosticId, DiagnosticSeverity.Error)
80+
.WithLocation(0)
81+
.WithArguments(invalidValue, expectedPrefix);
82+
83+
await Verifier.VerifyAnalyzerAsync(code, expected);
84+
}
85+
86+
[Fact]
87+
public async Task MultipleInvalidConstantsIncludingNestedClassesShouldHaveMultipleDiagnostics()
88+
{
89+
var code = """
90+
#nullable enable
91+
namespace Datadog.Trace.Configuration;
92+
93+
internal static partial class PlatformKeys
94+
{
95+
public const string {|#0:InvalidOtelKey|} = "OTEL_SERVICE_NAME";
96+
public const string ValidKey = "AWS_LAMBDA_FUNCTION_NAME";
97+
public const string {|#1:InvalidDdKey|} = "dd_trace_enabled";
98+
99+
internal class TestPlatform
100+
{
101+
public const string {|#2:InvalidInternalKey|} = "_DD_PROFILER_ENABLED";
102+
public const string ValidNestedKey = "WEBSITE_SITE_NAME";
103+
}
104+
}
105+
""";
106+
107+
var expected1 = new DiagnosticResult(DiagnosticId, DiagnosticSeverity.Error)
108+
.WithLocation(0)
109+
.WithArguments("OTEL_SERVICE_NAME", "OTEL");
110+
111+
var expected2 = new DiagnosticResult(DiagnosticId, DiagnosticSeverity.Error)
112+
.WithLocation(1)
113+
.WithArguments("dd_trace_enabled", "DD_");
114+
115+
var expected3 = new DiagnosticResult(DiagnosticId, DiagnosticSeverity.Error)
116+
.WithLocation(2)
117+
.WithArguments("_DD_PROFILER_ENABLED", "_DD_");
118+
119+
await Verifier.VerifyAnalyzerAsync(code, expected1, expected2, expected3);
120+
}
121+
122+
[Fact]
123+
public async Task DifferentNamespaceAndClassNameShouldNotHaveDiagnostics()
124+
{
125+
var code = """
126+
#nullable enable
127+
namespace SomeOther.Namespace
128+
{
129+
internal static partial class PlatformKeys
130+
{
131+
public const string ShouldNotBeAnalyzed = "DD_TRACE_ENABLED";
132+
}
133+
}
134+
135+
namespace Datadog.Trace.Configuration
136+
{
137+
internal static partial class ConfigurationKeys
138+
{
139+
public const string AlsoNotAnalyzed = "OTEL_SERVICE_NAME";
140+
}
141+
}
142+
""";
143+
144+
await Verifier.VerifyAnalyzerAsync(code);
145+
}
146+
}

0 commit comments

Comments
 (0)