< Summary

Information
Class: NexusLabs.Needlr.Generators.CaptiveDependencyAnalyzer
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/CaptiveDependencyAnalyzer.cs
Line coverage
89%
Covered lines: 42
Uncovered lines: 5
Coverable lines: 47
Total lines: 139
Line coverage: 89.3%
Branch coverage
88%
Covered branches: 37
Total branches: 42
Branch coverage: 88%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ReportDisposableCaptiveDependencies(...)100%88100%
CheckForCaptiveDependencies(...)100%1212100%
IsFactoryPattern(...)50%14855.55%
IsShorterLifetime(...)100%1010100%
GetLifetimeName(...)75%4485.71%

File(s)

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

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6
 7using Microsoft.CodeAnalysis;
 8
 9using NexusLabs.Needlr.Generators.Models;
 10
 11namespace NexusLabs.Needlr.Generators;
 12
 13/// <summary>
 14/// Detects disposable captive dependencies and reports diagnostics.
 15/// A captive dependency occurs when a longer-lived service depends on a shorter-lived disposable.
 16/// </summary>
 17internal static class CaptiveDependencyAnalyzer
 18{
 19    /// <summary>
 20    /// Detects disposable captive dependencies using inferred lifetimes from DiscoveryResult.
 21    /// Reports NDLRGEN022 when a longer-lived service depends on a shorter-lived disposable.
 22    /// </summary>
 23    internal static void ReportDisposableCaptiveDependencies(SourceProductionContext spc, DiscoveryResult discoveryResul
 24    {
 25        // Build lookup from type name to DiscoveredType for O(1) lifetime lookups
 45526        var typeLookup = new Dictionary<string, DiscoveredType>();
 46798627        foreach (var type in discoveryResult.InjectableTypes)
 28        {
 23353829            typeLookup[type.TypeName] = type;
 30            // Also map by interfaces so we can look up dependencies by interface
 46758231            foreach (var iface in type.InterfaceNames)
 32            {
 33                // Only add if not already present (first registration wins for interface resolution)
 25334                if (!typeLookup.ContainsKey(iface))
 35                {
 24836                    typeLookup[iface] = type;
 37                }
 38            }
 39        }
 40
 41        // Check each injectable type for captive dependencies
 46798642        foreach (var type in discoveryResult.InjectableTypes)
 43        {
 23353844            CheckForCaptiveDependencies(spc, type, typeLookup);
 45        }
 45546    }
 47
 48    /// <summary>
 49    /// Checks a single type for captive dependency issues.
 50    /// </summary>
 51    private static void CheckForCaptiveDependencies(
 52        SourceProductionContext spc,
 53        DiscoveredType type,
 54        Dictionary<string, DiscoveredType> typeLookup)
 55    {
 56        // Skip types with transient lifetime - they can't capture shorter-lived dependencies
 23353857        if (type.Lifetime == GeneratorLifetime.Transient)
 558            return;
 59
 65864660        foreach (var param in type.ConstructorParameters)
 61        {
 62            // Skip factory patterns that create new instances on demand
 9579063            if (IsFactoryPattern(param.TypeName))
 64                continue;
 65
 66            // Try to find the dependency in our discovered types
 9579067            if (!typeLookup.TryGetValue(param.TypeName, out var dependency))
 68                continue;
 69
 70            // Check if the dependency is shorter-lived
 3132071            if (!IsShorterLifetime(type.Lifetime, dependency.Lifetime))
 72                continue;
 73
 74            // Check if the dependency is disposable
 775            if (!dependency.IsDisposable)
 76                continue;
 77
 78            // Report the captive dependency
 579            spc.ReportDiagnostic(Diagnostic.Create(
 580                DiagnosticDescriptors.DisposableCaptiveDependency,
 581                Location.None,
 582                type.TypeName,
 583                GetLifetimeName(type.Lifetime),
 584                dependency.TypeName,
 585                GetLifetimeName(dependency.Lifetime)));
 86        }
 23353387    }
 88
 89    /// <summary>
 90    /// Checks if a type name represents a factory pattern that creates new instances on demand.
 91    /// </summary>
 92    private static bool IsFactoryPattern(string typeName)
 93    {
 94        // Func<T> - factory delegate
 9579095        if (typeName.StartsWith("System.Func<", StringComparison.Ordinal))
 096            return true;
 97
 98        // Lazy<T> - deferred creation
 9579099        if (typeName.StartsWith("System.Lazy<", StringComparison.Ordinal))
 0100            return true;
 101
 102        // IServiceScopeFactory - creates new scopes
 95790103        if (typeName == "Microsoft.Extensions.DependencyInjection.IServiceScopeFactory")
 0104            return true;
 105
 106        // IServiceProvider - resolves services dynamically
 95790107        if (typeName == "System.IServiceProvider")
 0108            return true;
 109
 95790110        return false;
 111    }
 112
 113    /// <summary>
 114    /// Checks if dependency lifetime is shorter than consumer lifetime.
 115    /// </summary>
 116    private static bool IsShorterLifetime(GeneratorLifetime consumer, GeneratorLifetime dependency)
 117    {
 118        // Singleton > Scoped > Transient (in terms of lifetime duration)
 119        // A shorter lifetime means the dependency will be disposed sooner
 31320120        return (consumer, dependency) switch
 31320121        {
 4122            (GeneratorLifetime.Singleton, GeneratorLifetime.Scoped) => true,
 1123            (GeneratorLifetime.Singleton, GeneratorLifetime.Transient) => true,
 2124            (GeneratorLifetime.Scoped, GeneratorLifetime.Transient) => true,
 31313125            _ => false
 31320126        };
 127    }
 128
 129    /// <summary>
 130    /// Gets the human-readable name for a lifetime.
 131    /// </summary>
 10132    internal static string GetLifetimeName(GeneratorLifetime lifetime) => lifetime switch
 10133    {
 4134        GeneratorLifetime.Singleton => "Singleton",
 4135        GeneratorLifetime.Scoped => "Scoped",
 2136        GeneratorLifetime.Transient => "Transient",
 0137        _ => lifetime.ToString()
 10138    };
 139}