< Summary

Information
Class: NexusLabs.Needlr.Analyzers.CircularDependencyAnalyzer
Assembly: NexusLabs.Needlr.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/CircularDependencyAnalyzer.cs
Line coverage
93%
Covered lines: 114
Uncovered lines: 8
Coverable lines: 122
Total lines: 288
Line coverage: 93.4%
Branch coverage
77%
Covered branches: 51
Total branches: 66
Branch coverage: 77.2%
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%
CollectDependencies(...)92.85%282893.75%
AnalyzeForCycles(...)100%22100%
IsRegisteredService(...)87.5%88100%
.ctor()100%11100%
AddNode(...)100%11100%
DetectCycles()100%44100%
DetectCyclesDfs(...)100%1010100%
ResolveDependency(...)14.28%971425%
get_Dependencies()100%11100%
get_Location()100%11100%
.ctor(...)100%11100%
get_Path()100%11100%
get_Location()100%11100%
.ctor(...)100%11100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/CircularDependencyAnalyzer.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
 8using NexusLabs.Needlr.Roslyn.Shared;
 9
 10namespace NexusLabs.Needlr.Analyzers;
 11
 12/// <summary>
 13/// Analyzer that detects circular dependencies in service registrations.
 14/// A circular dependency occurs when a service directly or indirectly depends on itself.
 15/// </summary>
 16/// <remarks>
 17/// Examples:
 18/// - A → B → A (direct cycle)
 19/// - A → B → C → A (indirect cycle)
 20/// </remarks>
 21[DiagnosticAnalyzer(LanguageNames.CSharp)]
 22public sealed class CircularDependencyAnalyzer : DiagnosticAnalyzer
 23{
 24    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 27025        ImmutableArray.Create(DiagnosticDescriptors.CircularDependency);
 26
 27    public override void Initialize(AnalysisContext context)
 28    {
 2529        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2530        context.EnableConcurrentExecution();
 31
 32        // We need to analyze at compilation level to build the full dependency graph
 2533        context.RegisterCompilationStartAction(compilationContext =>
 2534        {
 1535            var dependencyGraph = new DependencyGraphBuilder();
 2536
 2537            // First pass: collect all types and their dependencies
 1538            compilationContext.RegisterSyntaxNodeAction(
 13339                nodeContext => CollectDependencies(nodeContext, dependencyGraph),
 1540                SyntaxKind.ClassDeclaration);
 2541
 2542            // End of compilation: analyze for cycles
 1543            compilationContext.RegisterCompilationEndAction(
 3044                endContext => AnalyzeForCycles(endContext, dependencyGraph));
 4045        });
 2546    }
 47
 48    private static void CollectDependencies(SyntaxNodeAnalysisContext context, DependencyGraphBuilder graph)
 49    {
 13350        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 51
 52        // Skip abstract classes
 13353        if (classDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword))
 54        {
 055            return;
 56        }
 57
 13358        var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);
 13359        if (classSymbol == null)
 60        {
 061            return;
 62        }
 63
 64        // Check if this is a registered service (has registration attributes)
 13365        if (!IsRegisteredService(classSymbol))
 66        {
 10367            return;
 68        }
 69
 3070        var dependencies = new List<INamedTypeSymbol>();
 3071        var location = classDeclaration.Identifier.GetLocation();
 72
 73        // Collect dependencies from primary constructor
 3074        if (classDeclaration.ParameterList != null)
 75        {
 2076            foreach (var parameter in classDeclaration.ParameterList.Parameters)
 77            {
 578                if (parameter.Type == null) continue;
 79
 580                var typeInfo = context.SemanticModel.GetTypeInfo(parameter.Type);
 581                if (typeInfo.Type is INamedTypeSymbol paramType)
 82                {
 583                    dependencies.Add(paramType);
 84                }
 85            }
 86        }
 87
 88        // Collect dependencies from explicit constructors
 3089        var constructors = classDeclaration.Members
 3090            .OfType<ConstructorDeclarationSyntax>()
 1691            .Where(c => !c.Modifiers.Any(SyntaxKind.StaticKeyword))
 3092            .ToList();
 93
 9294        foreach (var constructor in constructors)
 95        {
 6496            foreach (var parameter in constructor.ParameterList.Parameters)
 97            {
 1698                if (parameter.Type == null) continue;
 99
 16100                var typeInfo = context.SemanticModel.GetTypeInfo(parameter.Type);
 16101                if (typeInfo.Type is INamedTypeSymbol paramType)
 102                {
 16103                    dependencies.Add(paramType);
 104                }
 105            }
 106        }
 107
 108        // A type with [GenerateConstructor] or a positive field-level constructor guard
 109        // trigger has its effective constructor emitted by a sibling source generator
 110        // rather than authored in this class's own syntax tree, so its dependencies must
 111        // be derived from the shared eligible-field model instead of from constructor
 112        // parameter syntax. This makes such a dependency participate in cycle detection
 113        // exactly like a hand-written constructor parameter. GetEligibleConstructorFields
 114        // already excludes fields marked [ConstructorIgnore] and fields with an
 115        // initializer, so those never contribute a false dependency here.
 30116        if (GeneratedConstructorEligibility.IsEligibleForGeneratedConstructor(classSymbol))
 117        {
 28118            foreach (var field in GeneratedConstructorEligibility.GetEligibleConstructorFields(classSymbol))
 119            {
 7120                if (field.Type is INamedTypeSymbol fieldType)
 121                {
 7122                    dependencies.Add(fieldType);
 123                }
 124            }
 125        }
 126
 30127        graph.AddNode(classSymbol, dependencies, location);
 30128    }
 129
 130    private static void AnalyzeForCycles(CompilationAnalysisContext context, DependencyGraphBuilder graph)
 131    {
 15132        var cycles = graph.DetectCycles();
 133
 50134        foreach (var cycle in cycles)
 135        {
 32136            var cycleDescription = string.Join(" → ", cycle.Path.Select(t => t.Name)) + " → " + cycle.Path[0].Name;
 137
 10138            var diagnostic = Diagnostic.Create(
 10139                DiagnosticDescriptors.CircularDependency,
 10140                cycle.Location,
 10141                cycleDescription);
 142
 10143            context.ReportDiagnostic(diagnostic);
 144        }
 15145    }
 146
 147    private static bool IsRegisteredService(INamedTypeSymbol typeSymbol)
 148    {
 133149        var registrationAttributes = new[]
 133150        {
 133151            "RegisterAsAttribute", "RegisterAs",
 133152            "SingletonAttribute", "Singleton",
 133153            "ScopedAttribute", "Scoped",
 133154            "TransientAttribute", "Transient",
 133155            "AutoRegisterAttribute", "AutoRegister"
 133156        };
 157
 496158        foreach (var attribute in typeSymbol.GetAttributes())
 159        {
 130160            var attributeName = attribute.AttributeClass?.Name;
 130161            if (attributeName != null && registrationAttributes.Contains(attributeName))
 162            {
 30163                return true;
 164            }
 165        }
 166
 103167        return false;
 168    }
 169
 170    /// <summary>
 171    /// Builds a dependency graph and detects cycles.
 172    /// </summary>
 173    private class DependencyGraphBuilder
 174    {
 15175        private readonly Dictionary<INamedTypeSymbol, NodeInfo> _nodes = new(SymbolEqualityComparer.Default);
 15176        private readonly object _lock = new();
 177
 178        public void AddNode(INamedTypeSymbol type, List<INamedTypeSymbol> dependencies, Location location)
 179        {
 30180            lock (_lock)
 181            {
 30182                _nodes[type] = new NodeInfo(dependencies, location);
 30183            }
 30184        }
 185
 186        public List<CycleInfo> DetectCycles()
 187        {
 15188            var cycles = new List<CycleInfo>();
 15189            var visited = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
 15190            var recursionStack = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
 15191            var path = new List<INamedTypeSymbol>();
 192
 193            // Sort by full type name for deterministic iteration order
 44194            var sortedNodes = _nodes.Keys.OrderBy(n => n.ToDisplayString()).ToList();
 90195            foreach (var node in sortedNodes)
 196            {
 30197                if (!visited.Contains(node))
 198                {
 17199                    DetectCyclesDfs(node, visited, recursionStack, path, cycles);
 200                }
 201            }
 202
 15203            return cycles;
 204        }
 205
 206        private void DetectCyclesDfs(
 207            INamedTypeSymbol current,
 208            HashSet<INamedTypeSymbol> visited,
 209            HashSet<INamedTypeSymbol> recursionStack,
 210            List<INamedTypeSymbol> path,
 211            List<CycleInfo> cycles)
 212        {
 32213            visited.Add(current);
 32214            recursionStack.Add(current);
 32215            path.Add(current);
 216
 32217            if (_nodes.TryGetValue(current, out var nodeInfo))
 218            {
 116219                foreach (var dependency in nodeInfo.Dependencies)
 220                {
 221                    // Resolve interface to implementation if possible
 28222                    var resolvedDep = ResolveDependency(dependency);
 223
 28224                    if (!visited.Contains(resolvedDep))
 225                    {
 15226                        DetectCyclesDfs(resolvedDep, visited, recursionStack, path, cycles);
 227                    }
 13228                    else if (recursionStack.Contains(resolvedDep))
 229                    {
 230                        // Found a cycle - extract the cycle path
 10231                        var cycleStartIndex = path.IndexOf(resolvedDep);
 10232                        if (cycleStartIndex >= 0)
 233                        {
 10234                            var cyclePath = path.Skip(cycleStartIndex).ToList();
 10235                            cycles.Add(new CycleInfo(cyclePath, nodeInfo.Location));
 236                        }
 237                    }
 238                }
 239            }
 240
 32241            path.RemoveAt(path.Count - 1);
 32242            recursionStack.Remove(current);
 32243        }
 244
 245        private INamedTypeSymbol ResolveDependency(INamedTypeSymbol dependency)
 246        {
 247            // If it's an interface or abstract, try to find an implementation in our graph
 28248            if (dependency.TypeKind == TypeKind.Interface || dependency.IsAbstract)
 249            {
 0250                foreach (var kvp in _nodes)
 251                {
 0252                    var type = kvp.Key;
 0253                    if (type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, dependency)) ||
 0254                        (type.BaseType != null && SymbolEqualityComparer.Default.Equals(type.BaseType, dependency)))
 255                    {
 0256                        return type;
 257                    }
 258                }
 259            }
 260
 28261            return dependency;
 0262        }
 263
 264        private sealed class NodeInfo
 265        {
 30266            public List<INamedTypeSymbol> Dependencies { get; }
 10267            public Location Location { get; }
 268
 30269            public NodeInfo(List<INamedTypeSymbol> dependencies, Location location)
 270            {
 30271                Dependencies = dependencies;
 30272                Location = location;
 30273            }
 274        }
 275    }
 276
 277    private sealed class CycleInfo
 278    {
 20279        public List<INamedTypeSymbol> Path { get; }
 10280        public Location Location { get; }
 281
 10282        public CycleInfo(List<INamedTypeSymbol> path, Location location)
 283        {
 10284            Path = path;
 10285            Location = location;
 10286        }
 287    }
 288}