< Summary

Information
Class: NexusLabs.Needlr.Analyzers.LazyResolutionAnalyzer
Assembly: NexusLabs.Needlr.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/LazyResolutionAnalyzer.cs
Line coverage
94%
Covered lines: 92
Uncovered lines: 5
Coverable lines: 97
Total lines: 148
Line coverage: 94.8%
Branch coverage
81%
Covered branches: 36
Total branches: 44
Branch coverage: 81.8%
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(...)89.28%282898.66%
HasGenerateTypeRegistryAttribute(...)83.33%66100%
CollectLazyParameter(...)60%121073.33%
IsDiscoverableClass(...)100%11100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/LazyResolutionAnalyzer.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 Lazy&lt;T&gt; dependencies where T is not discovered
 15/// by source generation. Only active when [assembly: GenerateTypeRegistry] is present.
 16/// </summary>
 17[DiagnosticAnalyzer(LanguageNames.CSharp)]
 18public sealed class LazyResolutionAnalyzer : DiagnosticAnalyzer
 19{
 20    private const string GenerateTypeRegistryAttributeName = "NexusLabs.Needlr.Generators.GenerateTypeRegistryAttribute"
 21
 22    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 11823        ImmutableArray.Create(DiagnosticDescriptors.LazyResolutionUnknown);
 24
 25    public override void Initialize(AnalysisContext context)
 26    {
 1427        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 1428        context.EnableConcurrentExecution();
 29
 1430        context.RegisterCompilationStartAction(compilationContext =>
 1431        {
 832            if (!HasGenerateTypeRegistryAttribute(compilationContext.Compilation))
 133                return;
 1434
 735            var lazyType = compilationContext.Compilation.GetTypeByMetadataName("System.Lazy`1");
 736            if (lazyType == null)
 037                return;
 1438
 1439            // Collect all discovered types and interfaces during compilation
 740            var discoveredTypes = new ConcurrentDictionary<INamedTypeSymbol, byte>(SymbolEqualityComparer.Default);
 741            var discoveredInterfaces = new ConcurrentDictionary<INamedTypeSymbol, byte>(SymbolEqualityComparer.Default);
 1442
 1443            // Collect pending diagnostics to verify at compilation end
 744            var pendingDiagnostics = new ConcurrentBag<(Location Location, INamedTypeSymbol InnerType)>();
 1445
 1446            // First pass: collect all discoverable types and their interfaces
 747            compilationContext.RegisterSymbolAction(symbolContext =>
 748            {
 6549                var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol;
 750
 6551                if (!IsDiscoverableClass(typeSymbol))
 5652                    return;
 753
 954                discoveredTypes.TryAdd(typeSymbol, 0);
 2055                foreach (var iface in typeSymbol.AllInterfaces)
 756                {
 157                    discoveredInterfaces.TryAdd(iface, 0);
 758                }
 1659            }, SymbolKind.NamedType);
 1460
 1461            // Second pass: collect Lazy<T> parameters for later verification
 762            compilationContext.RegisterSyntaxNodeAction(
 1463                ctx => CollectLazyParameter(ctx, lazyType, pendingDiagnostics),
 764                SyntaxKind.Parameter);
 1465
 1466            // At compilation end, verify and report diagnostics
 767            compilationContext.RegisterCompilationEndAction(endContext =>
 768            {
 2869                foreach (var (location, innerType) in pendingDiagnostics)
 770                {
 771                    // Framework types are typically registered by the framework
 772                    var ns = innerType.ContainingNamespace?.ToDisplayString() ?? "";
 773                    if (ns.StartsWith("Microsoft.Extensions.") ||
 774                        ns.StartsWith("Microsoft.AspNetCore.") ||
 775                        ns == "System" ||
 776                        ns.StartsWith("System."))
 777                    {
 778                        continue;
 779                    }
 780
 781                    // Check if the type or its interface is discovered
 682                    if (innerType.TypeKind == TypeKind.Interface)
 783                    {
 584                        if (discoveredInterfaces.ContainsKey(innerType))
 185                            continue;
 786                    }
 787                    else
 788                    {
 189                        if (discoveredTypes.ContainsKey(innerType))
 790                            continue;
 791                    }
 792
 493                    var diagnostic = Diagnostic.Create(
 494                        DiagnosticDescriptors.LazyResolutionUnknown,
 495                        location,
 496                        innerType.Name);
 797
 498                    endContext.ReportDiagnostic(diagnostic);
 799                }
 14100            });
 21101        });
 14102    }
 103
 104    private static bool HasGenerateTypeRegistryAttribute(Compilation compilation)
 105    {
 23106        foreach (var attribute in compilation.Assembly.GetAttributes())
 107        {
 7108            var fullName = attribute.AttributeClass?.ToDisplayString();
 7109            if (fullName == GenerateTypeRegistryAttributeName)
 7110                return true;
 111        }
 112
 1113        return false;
 114    }
 115
 116    private static void CollectLazyParameter(
 117        SyntaxNodeAnalysisContext context,
 118        INamedTypeSymbol lazyType,
 119        ConcurrentBag<(Location, INamedTypeSymbol)> pendingDiagnostics)
 120    {
 14121        var parameter = (ParameterSyntax)context.Node;
 14122        if (parameter.Type == null)
 0123            return;
 124
 14125        var typeInfo = context.SemanticModel.GetTypeInfo(parameter.Type);
 14126        if (typeInfo.Type is not INamedTypeSymbol parameterType)
 0127            return;
 128
 14129        if (!SymbolEqualityComparer.Default.Equals(parameterType.OriginalDefinition, lazyType))
 7130            return;
 131
 7132        if (parameterType.TypeArguments.Length != 1)
 0133            return;
 134
 7135        var innerType = parameterType.TypeArguments[0] as INamedTypeSymbol;
 7136        if (innerType == null)
 0137            return;
 138
 7139        pendingDiagnostics.Add((parameter.Type.GetLocation(), innerType));
 7140    }
 141
 142    /// <summary>
 143    /// Uses the shared TypeDiscoveryHelper to determine if a type is discoverable.
 144    /// This ensures consistency with the source generator's logic.
 145    /// </summary>
 146    private static bool IsDiscoverableClass(INamedTypeSymbol typeSymbol)
 65147        => TypeDiscoveryHelper.IsInjectableType(typeSymbol, isCurrentAssembly: true);
 148}