< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Analyzers.AgentGraphReachabilityAnalyzer
Assembly: NexusLabs.Needlr.AgentFramework.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Analyzers/AgentGraphReachabilityAnalyzer.cs
Line coverage
100%
Covered lines: 111
Uncovered lines: 0
Coverable lines: 111
Total lines: 141
Line coverage: 100%
Branch coverage
92%
Covered branches: 39
Total branches: 42
Branch coverage: 92.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(...)92.85%4242100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Analyzers/AgentGraphReachabilityAnalyzer.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Collections.Immutable;
 5using System.Linq;
 6
 7using Microsoft.CodeAnalysis;
 8using Microsoft.CodeAnalysis.Diagnostics;
 9
 10namespace NexusLabs.Needlr.AgentFramework.Analyzers;
 11
 12/// <summary>
 13/// Analyzer that detects agents in a graph that are not reachable from the entry point.
 14/// </summary>
 15/// <remarks>
 16/// <b>NDLRMAF022</b> (Warning): An agent declares edges in a named graph but is not reachable
 17/// from that graph's entry point via any path.
 18/// </remarks>
 19[DiagnosticAnalyzer(LanguageNames.CSharp)]
 20public sealed class AgentGraphReachabilityAnalyzer : DiagnosticAnalyzer
 21{
 22    private const string AgentGraphEdgeAttributeName = "NexusLabs.Needlr.AgentFramework.AgentGraphEdgeAttribute";
 23    private const string AgentGraphEntryAttributeName = "NexusLabs.Needlr.AgentFramework.AgentGraphEntryAttribute";
 24
 25    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 5226        ImmutableArray.Create(MafDiagnosticDescriptors.GraphUnreachableAgent);
 27
 28    public override void Initialize(AnalysisContext context)
 29    {
 930        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 931        context.EnableConcurrentExecution();
 32
 933        context.RegisterCompilationStartAction(compilationContext =>
 934        {
 935            // graphName → entryFqn
 536            var entryPoints = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
 937            // graphName → { sourceFqn → list of targetFqns }
 538            var graphEdges = new ConcurrentDictionary<string, ConcurrentDictionary<string, ConcurrentBag<string>>>(Strin
 939            // graphName → { fqn → (symbol, location) } for all edge source nodes
 540            var edgeSourceNodes = new ConcurrentDictionary<string, ConcurrentDictionary<string, (INamedTypeSymbol Symbol
 941
 542            compilationContext.RegisterSymbolAction(symbolContext =>
 543            {
 8244                var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol;
 8245                var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 546
 32847                foreach (var attr in typeSymbol.GetAttributes())
 548                {
 8249                    var attrName = attr.AttributeClass?.ToDisplayString();
 550
 8251                    if (attrName == AgentGraphEntryAttributeName)
 552                    {
 353                        if (attr.ConstructorArguments.Length >= 1
 354                            && attr.ConstructorArguments[0].Value is string graphName)
 555                        {
 356                            entryPoints.TryAdd(graphName, fqn);
 557                        }
 558                    }
 559
 8260                    if (attrName == AgentGraphEdgeAttributeName)
 561                    {
 762                        if (attr.ConstructorArguments.Length < 2)
 563                            continue;
 564
 765                        if (attr.ConstructorArguments[0].Value is not string graphName)
 566                            continue;
 567
 768                        if (attr.ConstructorArguments[1].Kind != TypedConstantKind.Type
 769                            || attr.ConstructorArguments[1].Value is not INamedTypeSymbol targetType)
 570                            continue;
 571
 772                        var targetFqn = targetType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 573
 1174                        var perGraph = graphEdges.GetOrAdd(graphName, _ => new ConcurrentDictionary<string, ConcurrentBa
 1475                        perGraph.GetOrAdd(fqn, _ => new ConcurrentBag<string>()).Add(targetFqn);
 576
 777                        var location = attr.ApplicationSyntaxReference?.SyntaxTree is { } tree
 778                            ? Location.Create(tree, attr.ApplicationSyntaxReference.Span)
 779                            : typeSymbol.Locations[0];
 580
 1181                        var nodeMap = edgeSourceNodes.GetOrAdd(graphName, _ => new ConcurrentDictionary<string, (INamedT
 782                        nodeMap.TryAdd(fqn, (typeSymbol, location));
 583                    }
 584                }
 8785            }, SymbolKind.NamedType);
 986
 587            compilationContext.RegisterCompilationEndAction(endContext =>
 588            {
 1889                foreach (var graphKvp in graphEdges)
 590                {
 491                    var graphName = graphKvp.Key;
 592
 493                    if (!entryPoints.TryGetValue(graphName, out var entryFqn))
 594                        continue;
 595
 396                    var adjacency = graphKvp.Value.ToDictionary(
 697                        kvp => kvp.Key,
 698                        kvp => kvp.Value.Distinct().ToList(),
 399                        StringComparer.Ordinal);
 5100
 5101                    // BFS from entry point
 3102                    var reachable = new HashSet<string>(StringComparer.Ordinal);
 3103                    var queue = new Queue<string>();
 3104                    queue.Enqueue(entryFqn);
 3105                    reachable.Add(entryFqn);
 5106
 10107                    while (queue.Count > 0)
 5108                    {
 7109                        var current = queue.Dequeue();
 7110                        if (adjacency.TryGetValue(current, out var targets))
 5111                        {
 16112                            foreach (var target in targets)
 5113                            {
 4114                                if (reachable.Add(target))
 5115                                {
 4116                                    queue.Enqueue(target);
 5117                                }
 5118                            }
 5119                        }
 5120                    }
 5121
 5122                    // Report unreachable edge-source nodes
 3123                    if (!edgeSourceNodes.TryGetValue(graphName, out var nodeMap))
 5124                        continue;
 5125
 18126                    foreach (var nodeKvp in nodeMap)
 5127                    {
 6128                        if (!reachable.Contains(nodeKvp.Key))
 5129                        {
 2130                            endContext.ReportDiagnostic(Diagnostic.Create(
 2131                                MafDiagnosticDescriptors.GraphUnreachableAgent,
 2132                                nodeKvp.Value.Location,
 2133                                nodeKvp.Value.Symbol.Name,
 2134                                graphName));
 5135                        }
 5136                    }
 5137                }
 10138            });
 14139        });
 9140    }
 141}