< Summary

Information
Class: NexusLabs.Needlr.Analyzers.LifetimeMismatchAnalyzer
Assembly: NexusLabs.Needlr.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/LifetimeMismatchAnalyzer.cs
Line coverage
94%
Covered lines: 68
Uncovered lines: 4
Coverable lines: 72
Total lines: 194
Line coverage: 94.4%
Branch coverage
88%
Covered branches: 46
Total branches: 52
Branch coverage: 88.4%
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%
AnalyzeClassDeclaration(...)80%101092.85%
AnalyzeParameters(...)100%1010100%
GetLifetimeFromType(...)66.66%6683.33%
GetLifetimeFromAttributes(...)95.45%2222100%
GetLifetimeName(...)75%4485.71%

File(s)

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

#LineLine coverage
 1using System.Collections.Immutable;
 2
 3using Microsoft.CodeAnalysis;
 4using Microsoft.CodeAnalysis.CSharp;
 5using Microsoft.CodeAnalysis.CSharp.Syntax;
 6using Microsoft.CodeAnalysis.Diagnostics;
 7
 8namespace NexusLabs.Needlr.Analyzers;
 9
 10/// <summary>
 11/// Analyzer that detects lifetime mismatches in service registrations.
 12/// A lifetime mismatch occurs when a longer-lived service depends on a shorter-lived service.
 13/// </summary>
 14/// <remarks>
 15/// Examples of mismatches:
 16/// - Singleton depends on Scoped → captive dependency
 17/// - Singleton depends on Transient → captive dependency
 18/// - Scoped depends on Transient → captive dependency
 19/// </remarks>
 20[DiagnosticAnalyzer(LanguageNames.CSharp)]
 21public sealed class LifetimeMismatchAnalyzer : DiagnosticAnalyzer
 22{
 23    // Lifetime ranking: higher number = longer lifetime
 24    private enum LifetimeRank
 25    {
 26        Unknown = -1,
 27        Transient = 0,
 28        Scoped = 1,
 29        Singleton = 2
 30    }
 31
 32    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 36033        ImmutableArray.Create(DiagnosticDescriptors.LifetimeMismatch);
 34
 35    public override void Initialize(AnalysisContext context)
 36    {
 2837        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2838        context.EnableConcurrentExecution();
 39
 2840        context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration);
 2841    }
 42
 43    private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
 44    {
 12245        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 46
 47        // Skip abstract classes
 12248        if (classDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword))
 49        {
 150            return;
 51        }
 52
 12153        var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);
 12154        if (classSymbol == null)
 55        {
 056            return;
 57        }
 58
 59        // Get the lifetime of this class from attributes, defaulting to Singleton for injectable types
 12160        var consumerLifetime = GetLifetimeFromType(classSymbol);
 12161        if (consumerLifetime == LifetimeRank.Unknown)
 62        {
 063            return; // Not an injectable type
 64        }
 65
 66        // Find constructors and analyze their parameters
 12167        var constructors = classDeclaration.Members
 12168            .OfType<ConstructorDeclarationSyntax>()
 1469            .Where(c => !c.Modifiers.Any(SyntaxKind.StaticKeyword))
 12170            .ToList();
 71
 72        // If no explicit constructors, check primary constructor parameters
 12173        if (classDeclaration.ParameterList != null)
 74        {
 275            AnalyzeParameters(
 276                context,
 277                classDeclaration.ParameterList.Parameters,
 278                classSymbol,
 279                consumerLifetime,
 280                classDeclaration.Identifier.GetLocation());
 81        }
 82
 83        // Analyze explicit constructor parameters
 27084        foreach (var constructor in constructors)
 85        {
 1486            AnalyzeParameters(
 1487                context,
 1488                constructor.ParameterList.Parameters,
 1489                classSymbol,
 1490                consumerLifetime,
 1491                constructor.Identifier.GetLocation());
 92        }
 12193    }
 94
 95    private static void AnalyzeParameters(
 96        SyntaxNodeAnalysisContext context,
 97        SeparatedSyntaxList<ParameterSyntax> parameters,
 98        INamedTypeSymbol consumerSymbol,
 99        LifetimeRank consumerLifetime,
 100        Location reportLocation)
 101    {
 70102        foreach (var parameter in parameters)
 103        {
 19104            if (parameter.Type == null)
 105            {
 106                continue;
 107            }
 108
 19109            var typeInfo = context.SemanticModel.GetTypeInfo(parameter.Type);
 19110            var parameterType = typeInfo.Type as INamedTypeSymbol;
 19111            if (parameterType == null)
 112            {
 113                continue;
 114            }
 115
 116            // Get the lifetime of the dependency type
 19117            var dependencyLifetime = GetLifetimeFromType(parameterType);
 19118            if (dependencyLifetime == LifetimeRank.Unknown)
 119            {
 120                continue; // Unknown lifetime (interface or non-injectable type), skip
 121            }
 122
 123            // Check for mismatch: consumer lifetime > dependency lifetime
 19124            if ((int)consumerLifetime > (int)dependencyLifetime)
 125            {
 14126                var diagnostic = Diagnostic.Create(
 14127                    DiagnosticDescriptors.LifetimeMismatch,
 14128                    parameter.GetLocation(),
 14129                    consumerSymbol.Name,
 14130                    GetLifetimeName(consumerLifetime),
 14131                    parameterType.Name,
 14132                    GetLifetimeName(dependencyLifetime));
 133
 14134                context.ReportDiagnostic(diagnostic);
 135            }
 136        }
 16137    }
 138
 139    /// <summary>
 140    /// Gets the lifetime for a type, defaulting to Singleton for injectable concrete classes.
 141    /// </summary>
 142    private static LifetimeRank GetLifetimeFromType(INamedTypeSymbol typeSymbol)
 143    {
 144        // Check for explicit lifetime attributes first
 140145        var explicitLifetime = GetLifetimeFromAttributes(typeSymbol);
 140146        if (explicitLifetime != LifetimeRank.Unknown)
 147        {
 51148            return explicitLifetime;
 149        }
 150
 151        // For concrete classes, default to Singleton (matching Needlr's default behavior)
 89152        if (typeSymbol.TypeKind == TypeKind.Class && !typeSymbol.IsAbstract)
 153        {
 89154            return LifetimeRank.Singleton;
 155        }
 156
 157        // For interfaces and abstract types, we cannot determine lifetime statically
 0158        return LifetimeRank.Unknown;
 159    }
 160
 161    private static LifetimeRank GetLifetimeFromAttributes(INamedTypeSymbol typeSymbol)
 162    {
 501163        foreach (var attribute in typeSymbol.GetAttributes())
 164        {
 136165            var attributeName = attribute.AttributeClass?.Name;
 166
 167            // Check for specific lifetime attributes (the only real lifetime attributes in Needlr)
 136168            if (attributeName is "SingletonAttribute" or "Singleton")
 169            {
 16170                return LifetimeRank.Singleton;
 171            }
 172
 120173            if (attributeName is "ScopedAttribute" or "Scoped")
 174            {
 22175                return LifetimeRank.Scoped;
 176            }
 177
 98178            if (attributeName is "TransientAttribute" or "Transient")
 179            {
 13180                return LifetimeRank.Transient;
 181            }
 182        }
 183
 89184        return LifetimeRank.Unknown;
 185    }
 186
 28187    private static string GetLifetimeName(LifetimeRank lifetime) => lifetime switch
 28188    {
 12189        LifetimeRank.Singleton => "Singleton",
 10190        LifetimeRank.Scoped => "Scoped",
 6191        LifetimeRank.Transient => "Transient",
 0192        _ => "Unknown"
 28193    };
 194}