< Summary

Information
Class: NexusLabs.Needlr.Analyzers.CollectionResolutionAnalyzer
Assembly: NexusLabs.Needlr.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/CollectionResolutionAnalyzer.cs
Line coverage
94%
Covered lines: 84
Uncovered lines: 5
Coverable lines: 89
Total lines: 142
Line coverage: 94.3%
Branch coverage
80%
Covered branches: 34
Total branches: 42
Branch coverage: 80.9%
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(...)87.5%242498.46%
HasGenerateTypeRegistryAttribute(...)83.33%66100%
CollectEnumerableParameter(...)66.66%141276.47%
IsDiscoverableClass(...)100%11100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/CollectionResolutionAnalyzer.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
 9using NexusLabs.Needlr.Roslyn.Shared;
 10
 11namespace NexusLabs.Needlr.Analyzers;
 12
 13/// <summary>
 14/// Analyzer that detects IEnumerable&lt;T&gt; dependencies where no implementations of T
 15/// are discovered by source generation. Only active when [assembly: GenerateTypeRegistry] is present.
 16/// </summary>
 17[DiagnosticAnalyzer(LanguageNames.CSharp)]
 18public sealed class CollectionResolutionAnalyzer : DiagnosticAnalyzer
 19{
 20    private const string GenerateTypeRegistryAttributeName = "NexusLabs.Needlr.Generators.GenerateTypeRegistryAttribute"
 21
 22    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 12323        ImmutableArray.Create(DiagnosticDescriptors.CollectionResolutionEmpty);
 24
 25    public override void Initialize(AnalysisContext context)
 26    {
 1627        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 1628        context.EnableConcurrentExecution();
 29
 1630        context.RegisterCompilationStartAction(compilationContext =>
 1631        {
 932            if (!HasGenerateTypeRegistryAttribute(compilationContext.Compilation))
 133                return;
 1634
 835            var enumerableType = compilationContext.Compilation.GetTypeByMetadataName("System.Collections.Generic.IEnume
 836            if (enumerableType == null)
 037                return;
 1638
 1639            // Collect all discovered interface implementations during compilation
 840            var discoveredInterfaces = new ConcurrentDictionary<INamedTypeSymbol, byte>(SymbolEqualityComparer.Default);
 1641
 1642            // Collect pending diagnostics to verify at compilation end
 843            var pendingDiagnostics = new ConcurrentBag<(Location Location, INamedTypeSymbol InnerType)>();
 1644
 1645            // First pass: collect all interfaces that have implementations
 846            compilationContext.RegisterSymbolAction(symbolContext =>
 847            {
 7748                var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol;
 849
 7750                if (!IsDiscoverableClass(typeSymbol))
 6551                    return;
 852
 3053                foreach (var iface in typeSymbol.AllInterfaces)
 854                {
 355                    discoveredInterfaces.TryAdd(iface, 0);
 856                }
 2057            }, SymbolKind.NamedType);
 1658
 1659            // Second pass: collect IEnumerable<T> parameters for later verification
 860            compilationContext.RegisterSyntaxNodeAction(
 1661                ctx => CollectEnumerableParameter(ctx, enumerableType, pendingDiagnostics),
 862                SyntaxKind.Parameter);
 1663
 1664            // At compilation end, verify and report diagnostics
 865            compilationContext.RegisterCompilationEndAction(endContext =>
 866            {
 3067                foreach (var (location, innerType) in pendingDiagnostics)
 868                {
 869                    // Framework interfaces are typically populated by the framework
 770                    var ns = innerType.ContainingNamespace?.ToDisplayString() ?? "";
 771                    if (ns.StartsWith("Microsoft.Extensions.") ||
 772                        ns.StartsWith("Microsoft.AspNetCore.") ||
 773                        ns == "System" ||
 774                        ns.StartsWith("System."))
 875                    {
 876                        continue;
 877                    }
 878
 879                    // Check if we discovered any implementations
 680                    if (discoveredInterfaces.ContainsKey(innerType))
 881                        continue;
 882
 483                    var diagnostic = Diagnostic.Create(
 484                        DiagnosticDescriptors.CollectionResolutionEmpty,
 485                        location,
 486                        innerType.Name);
 887
 488                    endContext.ReportDiagnostic(diagnostic);
 889                }
 1690            });
 2491        });
 1692    }
 93
 94    private static bool HasGenerateTypeRegistryAttribute(Compilation compilation)
 95    {
 2696        foreach (var attribute in compilation.Assembly.GetAttributes())
 97        {
 898            var fullName = attribute.AttributeClass?.ToDisplayString();
 899            if (fullName == GenerateTypeRegistryAttributeName)
 8100                return true;
 101        }
 102
 1103        return false;
 104    }
 105
 106    private static void CollectEnumerableParameter(
 107        SyntaxNodeAnalysisContext context,
 108        INamedTypeSymbol enumerableType,
 109        ConcurrentBag<(Location, INamedTypeSymbol)> pendingDiagnostics)
 110    {
 16111        var parameter = (ParameterSyntax)context.Node;
 16112        if (parameter.Type == null)
 0113            return;
 114
 16115        var typeInfo = context.SemanticModel.GetTypeInfo(parameter.Type);
 16116        if (typeInfo.Type is not INamedTypeSymbol parameterType)
 0117            return;
 118
 16119        if (!SymbolEqualityComparer.Default.Equals(parameterType.OriginalDefinition, enumerableType))
 8120            return;
 121
 8122        if (parameterType.TypeArguments.Length != 1)
 0123            return;
 124
 8125        var innerType = parameterType.TypeArguments[0] as INamedTypeSymbol;
 8126        if (innerType == null)
 0127            return;
 128
 129        // Only warn for interface types - concrete types in IEnumerable<T> are unusual
 8130        if (innerType.TypeKind != TypeKind.Interface)
 1131            return;
 132
 7133        pendingDiagnostics.Add((parameter.Type.GetLocation(), innerType));
 7134    }
 135
 136    /// <summary>
 137    /// Uses the shared TypeDiscoveryHelper to determine if a type is discoverable.
 138    /// This ensures consistency with the source generator's logic.
 139    /// </summary>
 140    private static bool IsDiscoverableClass(INamedTypeSymbol classSymbol)
 77141        => TypeDiscoveryHelper.IsInjectableType(classSymbol, isCurrentAssembly: true);
 142}