< Summary

Information
Class: NexusLabs.Needlr.Generators.ProviderAttributeAnalyzer
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/ProviderAttributeAnalyzer.cs
Line coverage
91%
Covered lines: 90
Uncovered lines: 8
Coverable lines: 98
Total lines: 217
Line coverage: 91.8%
Branch coverage
73%
Covered branches: 63
Total branches: 86
Branch coverage: 73.2%
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%
AnalyzeAttribute(...)75%121291.66%
AnalyzeProviderClass(...)100%44100%
AnalyzeProviderInterface(...)83.33%242493.1%
AnalyzePropertyType(...)72.72%232287.5%
HasProviderAttribute(...)0%66100%
GetMemberLocation(...)78.57%171475%
IsProviderAttribute(...)75%44100%

File(s)

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

#LineLine coverage
 1using System.Collections.Immutable;
 2using System.Linq;
 3
 4using Microsoft.CodeAnalysis;
 5using Microsoft.CodeAnalysis.CSharp;
 6using Microsoft.CodeAnalysis.CSharp.Syntax;
 7using Microsoft.CodeAnalysis.Diagnostics;
 8
 9namespace NexusLabs.Needlr.Generators;
 10
 11/// <summary>
 12/// Analyzer that validates [Provider] attribute usage:
 13/// - NDLRGEN031: [Provider] on class requires `partial` modifier
 14/// - NDLRGEN032: [Provider] interface must only contain get-only properties
 15/// - NDLRGEN033: Provider property type is a concrete class
 16/// - NDLRGEN034: Circular provider dependency detected
 17/// </summary>
 18[DiagnosticAnalyzer(LanguageNames.CSharp)]
 19public sealed class ProviderAttributeAnalyzer : DiagnosticAnalyzer
 20{
 21    private const string ProviderAttributeName = "ProviderAttribute";
 22    private const string GeneratorsNamespace = "NexusLabs.Needlr.Generators";
 23
 24    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 21225        ImmutableArray.Create(
 21226            DiagnosticDescriptors.ProviderClassNotPartial,
 21227            DiagnosticDescriptors.ProviderInterfaceInvalidMember,
 21228            DiagnosticDescriptors.ProviderPropertyConcreteType,
 21229            DiagnosticDescriptors.ProviderCircularDependency);
 30
 31    public override void Initialize(AnalysisContext context)
 32    {
 2233        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2234        context.EnableConcurrentExecution();
 35
 2236        context.RegisterSyntaxNodeAction(AnalyzeAttribute, SyntaxKind.Attribute);
 2237    }
 38
 39    private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context)
 40    {
 15641        var attributeSyntax = (AttributeSyntax)context.Node;
 15642        var attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol?.ContainingType;
 43
 15644        if (attributeSymbol == null)
 045            return;
 46
 15647        if (!IsProviderAttribute(attributeSymbol))
 14348            return;
 49
 1350        var parent = attributeSyntax.Parent?.Parent;
 51
 1352        if (parent is ClassDeclarationSyntax classDeclaration)
 53        {
 354            AnalyzeProviderClass(context, classDeclaration, attributeSyntax);
 55        }
 1056        else if (parent is InterfaceDeclarationSyntax interfaceDeclaration)
 57        {
 1058            AnalyzeProviderInterface(context, interfaceDeclaration, attributeSyntax);
 59        }
 1060    }
 61
 62    private static void AnalyzeProviderClass(
 63        SyntaxNodeAnalysisContext context,
 64        ClassDeclarationSyntax classDeclaration,
 65        AttributeSyntax attributeSyntax)
 66    {
 67        // NDLRGEN031: Check for partial modifier
 768        var isPartial = classDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
 369        if (!isPartial)
 70        {
 271            var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);
 272            if (classSymbol != null)
 73            {
 274                context.ReportDiagnostic(
 275                    Diagnostic.Create(
 276                        DiagnosticDescriptors.ProviderClassNotPartial,
 277                        attributeSyntax.GetLocation(),
 278                        classSymbol.Name));
 79            }
 80        }
 381    }
 82
 83    private static void AnalyzeProviderInterface(
 84        SyntaxNodeAnalysisContext context,
 85        InterfaceDeclarationSyntax interfaceDeclaration,
 86        AttributeSyntax attributeSyntax)
 87    {
 1088        var interfaceSymbol = context.SemanticModel.GetDeclaredSymbol(interfaceDeclaration);
 1089        if (interfaceSymbol == null)
 090            return;
 91
 7292        foreach (var member in interfaceSymbol.GetMembers())
 93        {
 94            // Skip properties - they're valid
 2695            if (member is IPropertySymbol propertySymbol)
 96            {
 97                // NDLRGEN032: Check property is get-only
 1198                if (propertySymbol.SetMethod != null)
 99                {
 2100                    context.ReportDiagnostic(
 2101                        Diagnostic.Create(
 2102                            DiagnosticDescriptors.ProviderInterfaceInvalidMember,
 2103                            GetMemberLocation(interfaceDeclaration, member.Name) ?? attributeSyntax.GetLocation(),
 2104                            interfaceSymbol.Name,
 2105                            $"a settable property '{member.Name}'"));
 106                }
 107                else
 108                {
 109                    // NDLRGEN033: Check for concrete type (warning only)
 9110                    AnalyzePropertyType(context, interfaceSymbol, propertySymbol, interfaceDeclaration);
 111                }
 9112                continue;
 113            }
 114
 115            // Skip special members (constructors, etc.)
 15116            if (member.IsImplicitlyDeclared)
 117                continue;
 118
 119            // NDLRGEN032: Report invalid member types
 15120            var memberDescription = member switch
 15121            {
 17122                IMethodSymbol m when m.MethodKind == MethodKind.Ordinary => $"a method '{member.Name}'",
 0123                IEventSymbol => $"an event '{member.Name}'",
 13124                _ => $"an unsupported member '{member.Name}'"
 15125            };
 126
 15127            if (member is IMethodSymbol methodSymbol && methodSymbol.MethodKind != MethodKind.Ordinary)
 128                continue; // Skip property getters/setters
 129
 2130            context.ReportDiagnostic(
 2131                Diagnostic.Create(
 2132                    DiagnosticDescriptors.ProviderInterfaceInvalidMember,
 2133                    GetMemberLocation(interfaceDeclaration, member.Name) ?? attributeSyntax.GetLocation(),
 2134                    interfaceSymbol.Name,
 2135                    memberDescription));
 136        }
 10137    }
 138
 139    private static void AnalyzePropertyType(
 140        SyntaxNodeAnalysisContext context,
 141        INamedTypeSymbol interfaceSymbol,
 142        IPropertySymbol propertySymbol,
 143        InterfaceDeclarationSyntax interfaceDeclaration)
 144    {
 9145        var propertyType = propertySymbol.Type;
 146
 147        // Unwrap nullable
 9148        if (propertyType is INamedTypeSymbol namedType && namedType.IsGenericType)
 149        {
 1150            var definition = namedType.OriginalDefinition.ToDisplayString();
 1151            if (definition == "System.Nullable<T>")
 152            {
 0153                propertyType = namedType.TypeArguments[0];
 154            }
 155            // Skip collections - they're always fine
 1156            if (definition.StartsWith("System.Collections.Generic.IEnumerable<") ||
 1157                definition.StartsWith("System.Collections.Generic.IReadOnlyCollection<") ||
 1158                definition.StartsWith("System.Collections.Generic.IReadOnlyList<"))
 159            {
 1160                return;
 161            }
 162        }
 163
 164        // Skip interfaces - they're the recommended pattern
 8165        if (propertyType.TypeKind == TypeKind.Interface)
 6166            return;
 167
 168        // Skip factory types (they end with Factory)
 2169        if (propertyType.Name.EndsWith("Factory"))
 0170            return;
 171
 172        // Skip provider types (nested providers are fine)
 2173        if (HasProviderAttribute(propertyType))
 0174            return;
 175
 176        // NDLRGEN033: Concrete class type detected
 2177        if (propertyType.TypeKind == TypeKind.Class)
 178        {
 2179            context.ReportDiagnostic(
 2180                Diagnostic.Create(
 2181                    DiagnosticDescriptors.ProviderPropertyConcreteType,
 2182                    GetMemberLocation(interfaceDeclaration, propertySymbol.Name) ?? Location.None,
 2183                    interfaceSymbol.Name,
 2184                    propertySymbol.Name,
 2185                    propertyType.ToDisplayString()));
 186        }
 2187    }
 188
 189    private static bool HasProviderAttribute(ITypeSymbol type)
 190    {
 2191        return type.GetAttributes().Any(a =>
 2192            a.AttributeClass?.Name == ProviderAttributeName &&
 2193            a.AttributeClass.ContainingNamespace?.ToDisplayString() == GeneratorsNamespace);
 194    }
 195
 196    private static Location? GetMemberLocation(TypeDeclarationSyntax typeDeclaration, string memberName)
 197    {
 22198        foreach (var member in typeDeclaration.Members)
 199        {
 8200            if (member is PropertyDeclarationSyntax prop && prop.Identifier.Text == memberName)
 4201                return prop.GetLocation();
 4202            if (member is MethodDeclarationSyntax method && method.Identifier.Text == memberName)
 2203                return method.GetLocation();
 2204            if (member is EventDeclarationSyntax evt && evt.Identifier.Text == memberName)
 0205                return evt.GetLocation();
 206        }
 0207        return null;
 208    }
 209
 210    private static bool IsProviderAttribute(INamedTypeSymbol attributeSymbol)
 211    {
 156212        if (attributeSymbol.Name != ProviderAttributeName)
 143213            return false;
 214
 13215        return attributeSymbol.ContainingNamespace?.ToDisplayString() == GeneratorsNamespace;
 216    }
 217}