< 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
74%
Covered lines: 101
Uncovered lines: 35
Coverable lines: 136
Total lines: 313
Line coverage: 74.2%
Branch coverage
70%
Covered branches: 100
Total branches: 142
Branch coverage: 70.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(...)85.71%857085.33%
IsOptionsAttribute(...)50%7675%
FindValidationMethod(...)100%88100%
ValidateMethodSignature(...)60%382064.28%
ImplementsIOptionsValidator(...)42.85%971425%
IsRecognizedByValidatorProvider(...)61.11%341863.63%
InheritsFromByMetadataName(...)0%4260%

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    private const string IOptionsValidatorName = "IOptionsValidator";
 30
 31    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 832        ImmutableArray.Create(
 833            DiagnosticDescriptors.ValidatorTypeMissingInterface,
 834            DiagnosticDescriptors.ValidatorTypeMismatch,
 835            DiagnosticDescriptors.ValidateMethodNotFound,
 836            DiagnosticDescriptors.ValidateMethodWrongSignature,
 837            DiagnosticDescriptors.ValidatorWontRun,
 838            DiagnosticDescriptors.ValidateMethodWontRun);
 39
 40    public override void Initialize(AnalysisContext context)
 41    {
 842        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 843        context.EnableConcurrentExecution();
 44
 845        context.RegisterSyntaxNodeAction(AnalyzeOptionsAttribute, SyntaxKind.Attribute);
 846    }
 47
 48    private static void AnalyzeOptionsAttribute(SyntaxNodeAnalysisContext context)
 49    {
 850        var attributeSyntax = (AttributeSyntax)context.Node;
 851        var attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol?.ContainingType;
 52
 853        if (attributeSymbol == null)
 054            return;
 55
 56        // Check if this is an [Options] attribute
 857        if (!IsOptionsAttribute(attributeSymbol))
 058            return;
 59
 60        // Get the class this attribute is applied to
 861        var classDeclaration = attributeSyntax.Parent?.Parent as ClassDeclarationSyntax;
 862        if (classDeclaration == null)
 063            return;
 64
 865        var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);
 866        if (classSymbol == null)
 067            return;
 68
 69        // Extract attribute properties
 870        var attributeData = classSymbol.GetAttributes()
 1671            .FirstOrDefault(a => IsOptionsAttribute(a.AttributeClass));
 72
 873        if (attributeData == null)
 074            return;
 75
 876        bool validateOnStart = false;
 877        string? validateMethod = null;
 878        INamedTypeSymbol? validatorType = null;
 79
 4080        foreach (var namedArg in attributeData.NamedArguments)
 81        {
 1282            switch (namedArg.Key)
 83            {
 84                case "ValidateOnStart":
 585                    validateOnStart = namedArg.Value.Value is true;
 586                    break;
 87                case "ValidateMethod":
 388                    validateMethod = namedArg.Value.Value as string;
 389                    break;
 90                case "Validator":
 491                    validatorType = namedArg.Value.Value as INamedTypeSymbol;
 92                    break;
 93            }
 94        }
 95
 96        // NDLRGEN018: Validator specified but ValidateOnStart is false
 897        if (validatorType != null && !validateOnStart)
 98        {
 199            context.ReportDiagnostic(Diagnostic.Create(
 1100                DiagnosticDescriptors.ValidatorWontRun,
 1101                attributeSyntax.GetLocation(),
 1102                validatorType.Name));
 103        }
 104
 105        // NDLRGEN019: ValidateMethod specified but ValidateOnStart is false
 8106        if (validateMethod != null && !validateOnStart)
 107        {
 1108            context.ReportDiagnostic(Diagnostic.Create(
 1109                DiagnosticDescriptors.ValidateMethodWontRun,
 1110                attributeSyntax.GetLocation(),
 1111                validateMethod));
 112        }
 113
 114        // If ValidateOnStart is true, validate the configuration
 8115        if (validateOnStart)
 116        {
 5117            var targetType = validatorType ?? classSymbol;
 5118            var methodName = validateMethod ?? "Validate";
 119
 120            // Check if validator is recognized by an extension (e.g., FluentValidation)
 121            // If so, skip our method signature checks - the extension handles it
 5122            var isRecognizedByExtension = validatorType != null && IsRecognizedByValidatorProvider(validatorType, contex
 123
 124            // Find the validation method
 5125            var validationMethod = FindValidationMethod(targetType, methodName);
 126
 127            // NDLRGEN016: Method not found
 128            // Skip if validator is recognized by an extension - they have their own method signatures
 5129            if (validationMethod == null && !isRecognizedByExtension)
 130            {
 131                // Only report if ValidateMethod was explicitly specified or Validator was specified
 132                // (convention-based discovery is optional - no method is OK if not specified)
 2133                if (validateMethod != null || validatorType != null)
 134                {
 2135                    context.ReportDiagnostic(Diagnostic.Create(
 2136                        DiagnosticDescriptors.ValidateMethodNotFound,
 2137                        attributeSyntax.GetLocation(),
 2138                        methodName,
 2139                        targetType.Name));
 140                }
 141            }
 3142            else if (validationMethod != null && !isRecognizedByExtension)
 143            {
 144                // NDLRGEN017: Check method signature
 3145                var signatureError = ValidateMethodSignature(validationMethod, classSymbol, validatorType != null);
 3146                if (signatureError != null)
 147                {
 0148                    context.ReportDiagnostic(Diagnostic.Create(
 0149                        DiagnosticDescriptors.ValidateMethodWrongSignature,
 0150                        attributeSyntax.GetLocation(),
 0151                        methodName,
 0152                        targetType.Name,
 0153                        signatureError));
 154                }
 155
 156                // NDLRGEN015: Validator type mismatch (if external validator with parameter)
 3157                if (validatorType != null && !validationMethod.IsStatic && validationMethod.Parameters.Length == 1)
 158                {
 2159                    var paramType = validationMethod.Parameters[0].Type;
 2160                    if (!SymbolEqualityComparer.Default.Equals(paramType, classSymbol))
 161                    {
 1162                        context.ReportDiagnostic(Diagnostic.Create(
 1163                            DiagnosticDescriptors.ValidatorTypeMismatch,
 1164                            attributeSyntax.GetLocation(),
 1165                            validatorType.Name,
 1166                            paramType.Name,
 1167                            classSymbol.Name));
 168                    }
 169                }
 170            }
 171
 172            // NDLRGEN014: Check if validator implements IOptionsValidator<T> or is recognized by an extension
 5173            if (validatorType != null && validationMethod == null && !isRecognizedByExtension)
 174            {
 1175                var implementsInterface = ImplementsIOptionsValidator(validatorType, classSymbol);
 1176                if (!implementsInterface)
 177                {
 1178                    context.ReportDiagnostic(Diagnostic.Create(
 1179                        DiagnosticDescriptors.ValidatorTypeMissingInterface,
 1180                        attributeSyntax.GetLocation(),
 1181                        validatorType.Name,
 1182                        classSymbol.Name));
 183                }
 184            }
 185        }
 8186    }
 187
 188    private static bool IsOptionsAttribute(INamedTypeSymbol? attributeClass)
 189    {
 16190        if (attributeClass == null)
 0191            return false;
 192
 16193        return attributeClass.Name == OptionsAttributeName &&
 16194               attributeClass.ContainingNamespace?.ToDisplayString() == GeneratorsNamespace;
 195    }
 196
 197    private static IMethodSymbol? FindValidationMethod(INamedTypeSymbol targetType, string methodName)
 198    {
 33199        foreach (var member in targetType.GetMembers())
 200        {
 13201            if (member is IMethodSymbol method && method.Name == methodName)
 202            {
 203                // Accept methods with 0 or 1 parameters
 3204                if (method.Parameters.Length <= 1)
 3205                    return method;
 206            }
 207        }
 208
 2209        return null;
 210    }
 211
 212    private static string? ValidateMethodSignature(IMethodSymbol method, INamedTypeSymbol optionsType, bool isExternalVa
 213    {
 214        // Check return type - should be IEnumerable<something>
 3215        if (method.ReturnType is not INamedTypeSymbol returnType)
 0216            return "IEnumerable<ValidationError> or IEnumerable<string>";
 217
 3218        var isEnumerable = returnType.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IEnumerable<T>
 3219                           returnType.AllInterfaces.Any(i => i.OriginalDefinition.ToDisplayString() == "System.Collectio
 220
 3221        if (!isEnumerable && returnType.ToDisplayString() != "System.Collections.IEnumerable")
 222        {
 0223            return "IEnumerable<ValidationError> or IEnumerable<string>";
 224        }
 225
 226        // Check parameters
 3227        if (isExternalValidator)
 228        {
 229            // External validator should have one parameter of the options type
 2230            if (method.Parameters.Length != 1)
 231            {
 0232                return $"IEnumerable<ValidationError> {method.Name}({optionsType.Name} options)";
 233            }
 234        }
 235        else
 236        {
 237            // Self-validation should have no parameters (unless static with one param)
 1238            if (!method.IsStatic && method.Parameters.Length != 0)
 239            {
 0240                return $"IEnumerable<ValidationError> {method.Name}()";
 241            }
 242
 1243            if (method.IsStatic && method.Parameters.Length != 1)
 244            {
 0245                return $"static IEnumerable<ValidationError> {method.Name}({optionsType.Name} options)";
 246            }
 247        }
 248
 3249        return null; // Valid signature
 250    }
 251
 252    private static bool ImplementsIOptionsValidator(INamedTypeSymbol validatorType, INamedTypeSymbol optionsType)
 253    {
 2254        foreach (var iface in validatorType.AllInterfaces)
 255        {
 0256            if (iface.Name == IOptionsValidatorName &&
 0257                iface.ContainingNamespace?.ToDisplayString() == GeneratorsNamespace &&
 0258                iface.IsGenericType &&
 0259                iface.TypeArguments.Length == 1)
 260            {
 261                // Check if the type argument matches the options type
 0262                if (SymbolEqualityComparer.Default.Equals(iface.TypeArguments[0], optionsType))
 0263                    return true;
 264            }
 265        }
 266
 1267        return false;
 268    }
 269
 270    private static bool IsRecognizedByValidatorProvider(INamedTypeSymbol validatorType, Compilation compilation)
 271    {
 272        // Collect all ValidatorProvider attributes from all referenced assemblies
 3273        var validatorBaseTypes = new HashSet<string>();
 274
 1020275        foreach (var reference in compilation.References)
 276        {
 507277            if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assemblySymbol)
 278                continue;
 279
 20058280            foreach (var attr in assemblySymbol.GetAttributes())
 281            {
 9525282                if (attr.AttributeClass?.Name != "ValidatorProviderAttribute")
 283                    continue;
 0284                if (attr.AttributeClass.ContainingNamespace?.ToDisplayString() != GeneratorsNamespace)
 285                    continue;
 286
 0287                if (attr.ConstructorArguments.Length > 0 &&
 0288                    attr.ConstructorArguments[0].Value is string baseTypeName)
 289                {
 0290                    validatorBaseTypes.Add(baseTypeName);
 291                }
 292            }
 293        }
 294
 295        // Check if validatorType inherits from any recognized base
 3296        return validatorBaseTypes.Any(baseTypeName =>
 3297            InheritsFromByMetadataName(validatorType, baseTypeName));
 298    }
 299
 300    private static bool InheritsFromByMetadataName(INamedTypeSymbol type, string metadataName)
 301    {
 0302        var current = type.BaseType;
 0303        while (current != null)
 304        {
 0305            var fullName = current.OriginalDefinition.ContainingNamespace?.ToDisplayString() + "." +
 0306                           current.OriginalDefinition.MetadataName;
 0307            if (fullName == metadataName)
 0308                return true;
 0309            current = current.BaseType;
 310        }
 0311        return false;
 312    }
 313}