< 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: 110
Uncovered lines: 8
Coverable lines: 118
Total lines: 267
Line coverage: 93.2%
Branch coverage
75%
Covered branches: 45
Total branches: 60
Branch coverage: 75%
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(...)90.9%222292.85%
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
 8namespace NexusLabs.Needlr.Analyzers;
 9
 10/// <summary>
 11/// Analyzer that detects circular dependencies in service registrations.
 12/// A circular dependency occurs when a service directly or indirectly depends on itself.
 13/// </summary>
 14/// <remarks>
 15/// Examples:
 16/// - A → B → A (direct cycle)
 17/// - A → B → C → A (indirect cycle)
 18/// </remarks>
 19[DiagnosticAnalyzer(LanguageNames.CSharp)]
 20public sealed class CircularDependencyAnalyzer : DiagnosticAnalyzer
 21{
 22    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 16723        ImmutableArray.Create(DiagnosticDescriptors.CircularDependency);
 24
 25    public override void Initialize(AnalysisContext context)
 26    {
 1727        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 1728        context.EnableConcurrentExecution();
 29
 30        // We need to analyze at compilation level to build the full dependency graph
 1731        context.RegisterCompilationStartAction(compilationContext =>
 1732        {
 1033            var dependencyGraph = new DependencyGraphBuilder();
 1734
 1735            // First pass: collect all types and their dependencies
 1036            compilationContext.RegisterSyntaxNodeAction(
 7337                nodeContext => CollectDependencies(nodeContext, dependencyGraph),
 1038                SyntaxKind.ClassDeclaration);
 1739
 1740            // End of compilation: analyze for cycles
 1041            compilationContext.RegisterCompilationEndAction(
 2042                endContext => AnalyzeForCycles(endContext, dependencyGraph));
 2743        });
 1744    }
 45
 46    private static void CollectDependencies(SyntaxNodeAnalysisContext context, DependencyGraphBuilder graph)
 47    {
 7348        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 49
 50        // Skip abstract classes
 7351        if (classDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword))
 52        {
 053            return;
 54        }
 55
 7356        var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);
 7357        if (classSymbol == null)
 58        {
 059            return;
 60        }
 61
 62        // Check if this is a registered service (has registration attributes)
 7363        if (!IsRegisteredService(classSymbol))
 64        {
 5365            return;
 66        }
 67
 2068        var dependencies = new List<INamedTypeSymbol>();
 2069        var location = classDeclaration.Identifier.GetLocation();
 70
 71        // Collect dependencies from primary constructor
 2072        if (classDeclaration.ParameterList != null)
 73        {
 2074            foreach (var parameter in classDeclaration.ParameterList.Parameters)
 75            {
 576                if (parameter.Type == null) continue;
 77
 578                var typeInfo = context.SemanticModel.GetTypeInfo(parameter.Type);
 579                if (typeInfo.Type is INamedTypeSymbol paramType)
 80                {
 581                    dependencies.Add(paramType);
 82                }
 83            }
 84        }
 85
 86        // Collect dependencies from explicit constructors
 2087        var constructors = classDeclaration.Members
 2088            .OfType<ConstructorDeclarationSyntax>()
 1389            .Where(c => !c.Modifiers.Any(SyntaxKind.StaticKeyword))
 2090            .ToList();
 91
 6692        foreach (var constructor in constructors)
 93        {
 5294            foreach (var parameter in constructor.ParameterList.Parameters)
 95            {
 1396                if (parameter.Type == null) continue;
 97
 1398                var typeInfo = context.SemanticModel.GetTypeInfo(parameter.Type);
 1399                if (typeInfo.Type is INamedTypeSymbol paramType)
 100                {
 13101                    dependencies.Add(paramType);
 102                }
 103            }
 104        }
 105
 20106        graph.AddNode(classSymbol, dependencies, location);
 20107    }
 108
 109    private static void AnalyzeForCycles(CompilationAnalysisContext context, DependencyGraphBuilder graph)
 110    {
 10111        var cycles = graph.DetectCycles();
 112
 32113        foreach (var cycle in cycles)
 114        {
 20115            var cycleDescription = string.Join(" → ", cycle.Path.Select(t => t.Name)) + " → " + cycle.Path[0].Name;
 116
 6117            var diagnostic = Diagnostic.Create(
 6118                DiagnosticDescriptors.CircularDependency,
 6119                cycle.Location,
 6120                cycleDescription);
 121
 6122            context.ReportDiagnostic(diagnostic);
 123        }
 10124    }
 125
 126    private static bool IsRegisteredService(INamedTypeSymbol typeSymbol)
 127    {
 73128        var registrationAttributes = new[]
 73129        {
 73130            "RegisterAsAttribute", "RegisterAs",
 73131            "SingletonAttribute", "Singleton",
 73132            "ScopedAttribute", "Scoped",
 73133            "TransientAttribute", "Transient",
 73134            "AutoRegisterAttribute", "AutoRegister"
 73135        };
 136
 266137        foreach (var attribute in typeSymbol.GetAttributes())
 138        {
 70139            var attributeName = attribute.AttributeClass?.Name;
 70140            if (attributeName != null && registrationAttributes.Contains(attributeName))
 141            {
 20142                return true;
 143            }
 144        }
 145
 53146        return false;
 147    }
 148
 149    /// <summary>
 150    /// Builds a dependency graph and detects cycles.
 151    /// </summary>
 152    private class DependencyGraphBuilder
 153    {
 10154        private readonly Dictionary<INamedTypeSymbol, NodeInfo> _nodes = new(SymbolEqualityComparer.Default);
 10155        private readonly object _lock = new();
 156
 157        public void AddNode(INamedTypeSymbol type, List<INamedTypeSymbol> dependencies, Location location)
 158        {
 20159            lock (_lock)
 160            {
 20161                _nodes[type] = new NodeInfo(dependencies, location);
 20162            }
 20163        }
 164
 165        public List<CycleInfo> DetectCycles()
 166        {
 10167            var cycles = new List<CycleInfo>();
 10168            var visited = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
 10169            var recursionStack = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
 10170            var path = new List<INamedTypeSymbol>();
 171
 172            // Sort by full type name for deterministic iteration order
 29173            var sortedNodes = _nodes.Keys.OrderBy(n => n.ToDisplayString()).ToList();
 60174            foreach (var node in sortedNodes)
 175            {
 20176                if (!visited.Contains(node))
 177                {
 12178                    DetectCyclesDfs(node, visited, recursionStack, path, cycles);
 179                }
 180            }
 181
 10182            return cycles;
 183        }
 184
 185        private void DetectCyclesDfs(
 186            INamedTypeSymbol current,
 187            HashSet<INamedTypeSymbol> visited,
 188            HashSet<INamedTypeSymbol> recursionStack,
 189            List<INamedTypeSymbol> path,
 190            List<CycleInfo> cycles)
 191        {
 21192            visited.Add(current);
 21193            recursionStack.Add(current);
 21194            path.Add(current);
 195
 21196            if (_nodes.TryGetValue(current, out var nodeInfo))
 197            {
 76198                foreach (var dependency in nodeInfo.Dependencies)
 199                {
 200                    // Resolve interface to implementation if possible
 18201                    var resolvedDep = ResolveDependency(dependency);
 202
 18203                    if (!visited.Contains(resolvedDep))
 204                    {
 9205                        DetectCyclesDfs(resolvedDep, visited, recursionStack, path, cycles);
 206                    }
 9207                    else if (recursionStack.Contains(resolvedDep))
 208                    {
 209                        // Found a cycle - extract the cycle path
 6210                        var cycleStartIndex = path.IndexOf(resolvedDep);
 6211                        if (cycleStartIndex >= 0)
 212                        {
 6213                            var cyclePath = path.Skip(cycleStartIndex).ToList();
 6214                            cycles.Add(new CycleInfo(cyclePath, nodeInfo.Location));
 215                        }
 216                    }
 217                }
 218            }
 219
 21220            path.RemoveAt(path.Count - 1);
 21221            recursionStack.Remove(current);
 21222        }
 223
 224        private INamedTypeSymbol ResolveDependency(INamedTypeSymbol dependency)
 225        {
 226            // If it's an interface or abstract, try to find an implementation in our graph
 18227            if (dependency.TypeKind == TypeKind.Interface || dependency.IsAbstract)
 228            {
 0229                foreach (var kvp in _nodes)
 230                {
 0231                    var type = kvp.Key;
 0232                    if (type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, dependency)) ||
 0233                        (type.BaseType != null && SymbolEqualityComparer.Default.Equals(type.BaseType, dependency)))
 234                    {
 0235                        return type;
 236                    }
 237                }
 238            }
 239
 18240            return dependency;
 0241        }
 242
 243        private sealed class NodeInfo
 244        {
 20245            public List<INamedTypeSymbol> Dependencies { get; }
 6246            public Location Location { get; }
 247
 20248            public NodeInfo(List<INamedTypeSymbol> dependencies, Location location)
 249            {
 20250                Dependencies = dependencies;
 20251                Location = location;
 20252            }
 253        }
 254    }
 255
 256    private sealed class CycleInfo
 257    {
 12258        public List<INamedTypeSymbol> Path { get; }
 6259        public Location Location { get; }
 260
 6261        public CycleInfo(List<INamedTypeSymbol> path, Location location)
 262        {
 6263            Path = path;
 6264            Location = location;
 6265        }
 266    }
 267}