| 
 | 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