< Summary

Information
Class: NexusLabs.Needlr.Generators.OptionsAttributeAnalyzer
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/OptionsAttributeAnalyzer.cs
Line coverage
95%
Covered lines: 134
Uncovered lines: 6
Coverable lines: 140
Total lines: 265
Line coverage: 95.7%
Branch coverage
85%
Covered branches: 82
Total branches: 96
Branch coverage: 85.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_SupportedDiagnostics()100%11100%
Initialize(...)100%11100%
AnalyzeOptionsAttribute(...)87.87%666696.22%
IsOptionsAttribute(...)66.66%7675%
IsRecognizedByValidatorProvider(...)88.88%1818100%
InheritsFromByMetadataName(...)66.66%6687.5%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/OptionsAttributeAnalyzer.cs

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System.Collections.Generic;
 5using System.Collections.Immutable;
 6using System.Linq;
 7
 8using Microsoft.CodeAnalysis;
 9using Microsoft.CodeAnalysis.CSharp;
 10using Microsoft.CodeAnalysis.CSharp.Syntax;
 11using Microsoft.CodeAnalysis.Diagnostics;
 12
 13namespace NexusLabs.Needlr.Generators;
 14
 15/// <summary>
 16/// Analyzer that validates [Options] attribute usage for validation configuration:
 17/// - NDLRGEN014: Validator type has no validation method
 18/// - NDLRGEN015: Validator type mismatch
 19/// - NDLRGEN016: Validation method not found
 20/// - NDLRGEN017: Validation method has wrong signature
 21/// - NDLRGEN018: Validator won't run (ValidateOnStart = false)
 22/// - NDLRGEN019: ValidateMethod won't run (ValidateOnStart = false)
 23/// </summary>
 24[DiagnosticAnalyzer(LanguageNames.CSharp)]
 25public sealed class OptionsAttributeAnalyzer : DiagnosticAnalyzer
 26{
 27    private const string OptionsAttributeName = "OptionsAttribute";
 28    private const string GeneratorsNamespace = "NexusLabs.Needlr.Generators";
 29
 30    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 3231        ImmutableArray.Create(
 3232            DiagnosticDescriptors.ValidatorTypeMissingInterface,
 3233            DiagnosticDescriptors.ValidatorTypeMismatch,
 3234            DiagnosticDescriptors.ValidateMethodNotFound,
 3235            DiagnosticDescriptors.ValidateMethodWrongSignature,
 3236            DiagnosticDescriptors.ValidatorWontRun,
 3237            DiagnosticDescriptors.ValidateMethodWontRun);
 38
 39    public override void Initialize(AnalysisContext context)
 40    {
 3241        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 3242        context.EnableConcurrentExecution();
 43
 3244        context.RegisterSyntaxNodeAction(AnalyzeOptionsAttribute, SyntaxKind.Attribute);
 3245    }
 46
 47    private static void AnalyzeOptionsAttribute(SyntaxNodeAnalysisContext context)
 48    {
 3349        var attributeSyntax = (AttributeSyntax)context.Node;
 3350        var attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol?.ContainingType;
 51
 3352        if (attributeSymbol == null)
 053            return;
 54
 55        // Check if this is an [Options] attribute
 3356        if (!IsOptionsAttribute(attributeSymbol))
 257            return;
 58
 59        // Get the type this attribute is applied to
 3160        var typeDeclaration = attributeSyntax.Parent?.Parent as TypeDeclarationSyntax;
 3161        if (typeDeclaration == null)
 062            return;
 63
 3164        var optionsType = context.SemanticModel.GetDeclaredSymbol(typeDeclaration);
 3165        if (optionsType == null)
 066            return;
 67
 68        // Extract attribute properties
 3169        var attributeData = optionsType.GetAttributes()
 6270            .FirstOrDefault(a => IsOptionsAttribute(a.AttributeClass));
 71
 3172        if (attributeData == null)
 073            return;
 74
 3175        bool validateOnStart = false;
 3176        string? validateMethod = null;
 3177        INamedTypeSymbol? validatorType = null;
 78
 15479        foreach (var namedArg in attributeData.NamedArguments)
 80        {
 4681            switch (namedArg.Key)
 82            {
 83                case "ValidateOnStart":
 2884                    validateOnStart = namedArg.Value.Value is true;
 2885                    break;
 86                case "ValidateMethod":
 487                    validateMethod = namedArg.Value.Value as string;
 488                    break;
 89                case "Validator":
 1490                    validatorType = namedArg.Value.Value as INamedTypeSymbol;
 91                    break;
 92            }
 93        }
 94
 95        // NDLRGEN018: Validator specified but ValidateOnStart is false
 3196        if (validatorType != null && !validateOnStart)
 97        {
 198            context.ReportDiagnostic(Diagnostic.Create(
 199                DiagnosticDescriptors.ValidatorWontRun,
 1100                attributeSyntax.GetLocation(),
 1101                validatorType.Name));
 102        }
 103
 104        // NDLRGEN019: ValidateMethod specified but ValidateOnStart is false
 31105        if (validateMethod != null && !validateOnStart)
 106        {
 1107            context.ReportDiagnostic(Diagnostic.Create(
 1108                DiagnosticDescriptors.ValidateMethodWontRun,
 1109                attributeSyntax.GetLocation(),
 1110                validateMethod));
 111        }
 112
 113        // If ValidateOnStart is true, validate the configuration
 31114        if (validateOnStart)
 115        {
 28116            var targetType = validatorType ?? optionsType;
 28117            var methodName = validateMethod ?? "Validate";
 118
 119            // Check if validator is recognized by an extension (e.g., FluentValidation)
 120            // If so, skip our method signature checks - the extension handles it
 28121            var isRecognizedByExtension = validatorType != null && IsRecognizedByValidatorProvider(validatorType, contex
 28122            if (isRecognizedByExtension)
 2123                return;
 124
 26125            var validationMethods = OptionsAttributeHelper
 26126                .GetValidationMethods(targetType, methodName)
 26127                .ToArray();
 26128            var validMethod = validationMethods.FirstOrDefault(method =>
 48129                OptionsAttributeHelper.GetValidationMethodSignatureError(
 48130                    method,
 48131                    optionsType,
 48132                    validatorType != null) == null &&
 48133                (validatorType == null ||
 48134                 (method.Parameters.Length == 1 &&
 48135                  SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, optionsType))));
 136
 26137            if (validMethod != null)
 7138                return;
 139
 19140            if (validationMethods.Length > 0)
 141            {
 13142                var validationMethod = validationMethods[0];
 13143                var signatureError =
 13144                    OptionsAttributeHelper.GetValidationMethodSignatureError(
 13145                        validationMethod,
 13146                        optionsType,
 13147                        validatorType != null);
 13148                if (signatureError != null)
 149                {
 12150                    context.ReportDiagnostic(Diagnostic.Create(
 12151                        DiagnosticDescriptors.ValidateMethodWrongSignature,
 12152                        attributeSyntax.GetLocation(),
 12153                        methodName,
 12154                        targetType.Name,
 12155                        signatureError));
 156                }
 1157                else if (validatorType != null)
 158                {
 1159                    var parameterType = validationMethod.Parameters[0].Type;
 1160                    context.ReportDiagnostic(Diagnostic.Create(
 1161                        DiagnosticDescriptors.ValidatorTypeMismatch,
 1162                        attributeSyntax.GetLocation(),
 1163                        validatorType.Name,
 1164                        parameterType.Name,
 1165                        optionsType.Name));
 166                }
 167
 1168                return;
 169            }
 170
 6171            if (validatorType != null && validateMethod == null)
 172            {
 3173                var interfaceTypeArguments = OptionsAttributeHelper
 3174                    .GetIOptionsValidatorTypeArguments(validatorType)
 3175                    .ToArray();
 3176                if (interfaceTypeArguments.Any(typeArgument =>
 5177                    SymbolEqualityComparer.Default.Equals(typeArgument, optionsType)))
 178                {
 1179                    return;
 180                }
 181
 2182                if (interfaceTypeArguments.Length > 0)
 183                {
 1184                    context.ReportDiagnostic(Diagnostic.Create(
 1185                        DiagnosticDescriptors.ValidatorTypeMismatch,
 1186                        attributeSyntax.GetLocation(),
 1187                        validatorType.Name,
 1188                        interfaceTypeArguments[0].Name,
 1189                        optionsType.Name));
 1190                    return;
 191                }
 192
 1193                context.ReportDiagnostic(Diagnostic.Create(
 1194                    DiagnosticDescriptors.ValidatorTypeMissingInterface,
 1195                    attributeSyntax.GetLocation(),
 1196                    validatorType.Name,
 1197                    optionsType.Name));
 1198                return;
 199            }
 200
 201            // Convention-based self-validation is optional.
 3202            if (validateMethod != null)
 203            {
 3204                context.ReportDiagnostic(Diagnostic.Create(
 3205                    DiagnosticDescriptors.ValidateMethodNotFound,
 3206                    attributeSyntax.GetLocation(),
 3207                    methodName,
 3208                    targetType.Name));
 209            }
 210        }
 6211    }
 212
 213    private static bool IsOptionsAttribute(INamedTypeSymbol? attributeClass)
 214    {
 64215        if (attributeClass == null)
 0216            return false;
 217
 64218        return attributeClass.Name == OptionsAttributeName &&
 64219               attributeClass.ContainingNamespace?.ToDisplayString() == GeneratorsNamespace;
 220    }
 221
 222    private static bool IsRecognizedByValidatorProvider(INamedTypeSymbol validatorType, Compilation compilation)
 223    {
 224        // Collect all ValidatorProvider attributes from all referenced assemblies
 13225        var validatorBaseTypes = new HashSet<string>();
 226
 4426227        foreach (var reference in compilation.References)
 228        {
 2200229            if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assemblySymbol)
 230                continue;
 231
 86948232            foreach (var attr in assemblySymbol.GetAttributes())
 233            {
 41287234                if (attr.AttributeClass?.Name != "ValidatorProviderAttribute")
 235                    continue;
 3236                if (attr.AttributeClass.ContainingNamespace?.ToDisplayString() != GeneratorsNamespace)
 237                    continue;
 238
 2239                if (attr.ConstructorArguments.Length > 0 &&
 2240                    attr.ConstructorArguments[0].Value is string baseTypeName)
 241                {
 2242                    validatorBaseTypes.Add(baseTypeName);
 243                }
 244            }
 245        }
 246
 247        // Check if validatorType inherits from any recognized base
 13248        return validatorBaseTypes.Any(baseTypeName =>
 15249            InheritsFromByMetadataName(validatorType, baseTypeName));
 250    }
 251
 252    private static bool InheritsFromByMetadataName(INamedTypeSymbol type, string metadataName)
 253    {
 2254        var current = type.BaseType;
 3255        while (current != null)
 256        {
 3257            var fullName = current.OriginalDefinition.ContainingNamespace?.ToDisplayString() + "." +
 3258                           current.OriginalDefinition.MetadataName;
 3259            if (fullName == metadataName)
 2260                return true;
 1261            current = current.BaseType;
 262        }
 0263        return false;
 264    }
 265}