|  | 
|  | 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 | +} | 
0 commit comments