< Summary

Information
Class: NexusLabs.Needlr.Analyzers.KeyedServiceResolutionAnalyzer
Assembly: NexusLabs.Needlr.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/KeyedServiceResolutionAnalyzer.cs
Line coverage
98%
Covered lines: 97
Uncovered lines: 1
Coverable lines: 98
Total lines: 199
Line coverage: 98.9%
Branch coverage
90%
Covered branches: 54
Total branches: 60
Branch coverage: 90%
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%88100%
HasGenerateTypeRegistryAttribute(...)83.33%66100%
CollectKeyedRegistration(...)91.66%2424100%
CollectKeyedServiceParameter(...)86.36%222294.73%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/KeyedServiceResolutionAnalyzer.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Collections.Immutable;
 3
 4using Microsoft.CodeAnalysis;
 5using Microsoft.CodeAnalysis.CSharp;
 6using Microsoft.CodeAnalysis.CSharp.Syntax;
 7using Microsoft.CodeAnalysis.Diagnostics;
 8
 9namespace NexusLabs.Needlr.Analyzers;
 10
 11/// <summary>
 12/// Analyzer that validates [FromKeyedServices] usage against discovered [Keyed] registrations.
 13/// Reports Info-level diagnostic when a key is not found in statically-discovered registrations.
 14/// Only active when [assembly: GenerateTypeRegistry] is present.
 15/// </summary>
 16/// <remarks>
 17/// <para>
 18/// This analyzer collects all [Keyed("key")] attributes from types in the compilation
 19/// and validates that [FromKeyedServices("key")] parameters reference known keys.
 20/// </para>
 21/// <para>
 22/// Keys registered via plugins at runtime cannot be validated at compile time.
 23/// Users can suppress this diagnostic for such cases.
 24/// </para>
 25/// </remarks>
 26[DiagnosticAnalyzer(LanguageNames.CSharp)]
 27public sealed class KeyedServiceResolutionAnalyzer : DiagnosticAnalyzer
 28{
 29    private const string GenerateTypeRegistryAttributeName = "NexusLabs.Needlr.Generators.GenerateTypeRegistryAttribute"
 30    private const string FromKeyedServicesAttributeName = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAtt
 31    private const string KeyedAttributeName = "KeyedAttribute";
 32    private const string KeyedAttributeFullName = "NexusLabs.Needlr.KeyedAttribute";
 33
 34    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 26235        ImmutableArray.Create(DiagnosticDescriptors.KeyedServiceUnknownKey);
 36
 37    public override void Initialize(AnalysisContext context)
 38    {
 2039        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2040        context.EnableConcurrentExecution();
 41
 2042        context.RegisterCompilationStartAction(compilationContext =>
 2043        {
 1244            if (!HasGenerateTypeRegistryAttribute(compilationContext.Compilation))
 145                return;
 2046
 1147            var fromKeyedServicesType = compilationContext.Compilation.GetTypeByMetadataName(FromKeyedServicesAttributeN
 1148            if (fromKeyedServicesType == null)
 149                return;
 2050
 2051            // Collect discovered [Keyed] registrations: key -> list of (serviceType, implType)
 1052            var discoveredKeys = new ConcurrentDictionary<string, ConcurrentBag<(string ServiceType, string ImplType)>>(
 2053
 2054            // Collect pending usages to validate: (location, serviceType, key)
 1055            var pendingUsages = new ConcurrentBag<(Location Location, string ServiceType, string Key)>();
 2056
 2057            // Collect [Keyed] attributes from class declarations
 1058            compilationContext.RegisterSymbolAction(
 4859                ctx => CollectKeyedRegistration(ctx, discoveredKeys),
 1060                SymbolKind.NamedType);
 2061
 2062            // Collect [FromKeyedServices] parameters
 1063            compilationContext.RegisterSyntaxNodeAction(
 2964                ctx => CollectKeyedServiceParameter(ctx, fromKeyedServicesType, pendingUsages),
 1065                SyntaxKind.Parameter);
 2066
 2067            // Validate at compilation end
 1068            compilationContext.RegisterCompilationEndAction(endContext =>
 1069            {
 4670                foreach (var (location, serviceType, key) in pendingUsages)
 1071                {
 1072                    // Check if key is found in discovered registrations
 1373                    if (discoveredKeys.TryGetValue(key, out var registrations))
 1074                    {
 1075                        // Key exists - check if any registration matches the service type
 1076                        // For now, just having the key is enough (interface matching is complex)
 1077                        continue;
 1078                    }
 1079
 1080                    // Key not found in statically-discovered registrations
 1081                    var diagnostic = Diagnostic.Create(
 1082                        DiagnosticDescriptors.KeyedServiceUnknownKey,
 1083                        location,
 1084                        serviceType,
 1085                        key);
 1086
 1087                    endContext.ReportDiagnostic(diagnostic);
 1088                }
 2089            });
 3090        });
 2091    }
 92
 93    private static bool HasGenerateTypeRegistryAttribute(Compilation compilation)
 94    {
 3595        foreach (var attribute in compilation.Assembly.GetAttributes())
 96        {
 1197            var fullName = attribute.AttributeClass?.ToDisplayString();
 1198            if (fullName == GenerateTypeRegistryAttributeName)
 1199                return true;
 100        }
 101
 1102        return false;
 103    }
 104
 105    private static void CollectKeyedRegistration(
 106        SymbolAnalysisContext context,
 107        ConcurrentDictionary<string, ConcurrentBag<(string, string)>> discoveredKeys)
 108    {
 48109        var typeSymbol = (INamedTypeSymbol)context.Symbol;
 110
 111        // Skip non-classes
 48112        if (typeSymbol.TypeKind != TypeKind.Class)
 12113            return;
 114
 115        // Look for [Keyed] attributes
 104116        foreach (var attr in typeSymbol.GetAttributes())
 117        {
 16118            var attrClass = attr.AttributeClass;
 16119            if (attrClass == null)
 120                continue;
 121
 16122            var name = attrClass.Name;
 16123            var fullName = attrClass.ToDisplayString();
 124
 16125            if (name != KeyedAttributeName && fullName != KeyedAttributeFullName)
 126                continue;
 127
 128            // Extract the key from the constructor argument
 3129            if (attr.ConstructorArguments.Length == 0)
 130                continue;
 131
 3132            var keyArg = attr.ConstructorArguments[0];
 3133            if (keyArg.Value is not string key)
 134                continue;
 135
 136            // Get the interfaces this type implements
 3137            var implTypeName = typeSymbol.ToDisplayString();
 12138            foreach (var iface in typeSymbol.AllInterfaces)
 139            {
 140                // Skip framework interfaces
 3141                var ns = iface.ContainingNamespace?.ToDisplayString() ?? "";
 3142                if (ns.StartsWith("System") || ns.StartsWith("Microsoft"))
 143                    continue;
 144
 6145                var bag = discoveredKeys.GetOrAdd(key, _ => new ConcurrentBag<(string, string)>());
 3146                bag.Add((iface.ToDisplayString(), implTypeName));
 147            }
 148
 149            // Also add self-registration
 3150            var selfBag = discoveredKeys.GetOrAdd(key, _ => new ConcurrentBag<(string, string)>());
 3151            selfBag.Add((implTypeName, implTypeName));
 152        }
 36153    }
 154
 155    private static void CollectKeyedServiceParameter(
 156        SyntaxNodeAnalysisContext context,
 157        INamedTypeSymbol fromKeyedServicesType,
 158        ConcurrentBag<(Location, string, string)> pendingUsages)
 159    {
 29160        var parameter = (ParameterSyntax)context.Node;
 161
 162        // Get the parameter symbol to access attributes
 29163        var parameterSymbol = context.SemanticModel.GetDeclaredSymbol(parameter);
 29164        if (parameterSymbol == null)
 0165            return;
 166
 167        // Look for [FromKeyedServices] attribute
 73168        foreach (var attr in parameterSymbol.GetAttributes())
 169        {
 14170            if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServicesType))
 171                continue;
 172
 173            // Extract the key from the constructor argument
 14174            if (attr.ConstructorArguments.Length == 0)
 175                continue;
 176
 14177            var keyArg = attr.ConstructorArguments[0];
 14178            if (keyArg.Value is not string key)
 179                continue;
 180
 181            // Get the parameter type
 14182            var parameterType = parameterSymbol.Type;
 14183            var typeName = parameterType.Name;
 184
 185            // Skip framework types
 14186            var ns = parameterType.ContainingNamespace?.ToDisplayString() ?? "";
 14187            if (ns.StartsWith("Microsoft.Extensions.") ||
 14188                ns.StartsWith("Microsoft.AspNetCore.") ||
 14189                ns == "System" ||
 14190                ns.StartsWith("System."))
 191            {
 192                continue;
 193            }
 194
 13195            pendingUsages.Add((parameter.GetLocation(), typeName, key));
 13196            break;
 197        }
 16198    }
 199}