< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Analyzers.AgentGraphOptionalFanOutAnalyzer
Assembly: NexusLabs.Needlr.AgentFramework.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Analyzers/AgentGraphOptionalFanOutAnalyzer.cs
Line coverage
100%
Covered lines: 75
Uncovered lines: 0
Coverable lines: 75
Total lines: 104
Line coverage: 100%
Branch coverage
95%
Covered branches: 23
Total branches: 24
Branch coverage: 95.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(...)95.83%2424100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Analyzers/AgentGraphOptionalFanOutAnalyzer.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 fan-out nodes where all outgoing edges are optional.
 14/// </summary>
 15/// <remarks>
 16/// <b>NDLRMAF024</b> (Warning): All outgoing edges from a fan-out node have
 17/// <c>IsRequired = false</c>. If all optional branches fail, the graph produces empty results.
 18/// </remarks>
 19[DiagnosticAnalyzer(LanguageNames.CSharp)]
 20public sealed class AgentGraphOptionalFanOutAnalyzer : DiagnosticAnalyzer
 21{
 22    private const string AgentGraphEdgeAttributeName = "NexusLabs.Needlr.AgentFramework.AgentGraphEdgeAttribute";
 23
 24    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 5725        ImmutableArray.Create(MafDiagnosticDescriptors.GraphAllEdgesOptional);
 26
 27    public override void Initialize(AnalysisContext context)
 28    {
 1129        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 1130        context.EnableConcurrentExecution();
 31
 1132        context.RegisterCompilationStartAction(compilationContext =>
 1133        {
 1134            // graphName → { sourceFqn → list of (isRequired, symbol, location) }
 635            var edgeData = new ConcurrentDictionary<string, ConcurrentDictionary<string, ConcurrentBag<(bool IsRequired,
 636                StringComparer.Ordinal);
 1137
 638            compilationContext.RegisterSymbolAction(symbolContext =>
 639            {
 9940                var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol;
 9941                var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 642
 39043                foreach (var attr in typeSymbol.GetAttributes())
 644                {
 9645                    if (attr.AttributeClass?.ToDisplayString() != AgentGraphEdgeAttributeName)
 646                        continue;
 647
 948                    if (attr.ConstructorArguments.Length < 2)
 649                        continue;
 650
 951                    if (attr.ConstructorArguments[0].Value is not string graphName)
 652                        continue;
 653
 654                    // IsRequired defaults to true
 955                    var isRequired = true;
 3056                    foreach (var namedArg in attr.NamedArguments)
 657                    {
 658                        if (namedArg.Key == "IsRequired" && namedArg.Value.Value is bool val)
 659                        {
 660                            isRequired = val;
 661                        }
 662                    }
 663
 964                    var location = typeSymbol.Locations[0];
 665
 1466                    var perGraph = edgeData.GetOrAdd(graphName, _ => new ConcurrentDictionary<string, ConcurrentBag<(boo
 1467                    perGraph.GetOrAdd(fqn, _ => new ConcurrentBag<(bool, INamedTypeSymbol, Location)>())
 968                        .Add((isRequired, typeSymbol, location));
 669                }
 10570            }, SymbolKind.NamedType);
 1171
 672            compilationContext.RegisterCompilationEndAction(endContext =>
 673            {
 2274                foreach (var graphKvp in edgeData)
 675                {
 576                    var graphName = graphKvp.Key;
 677
 2078                    foreach (var sourceKvp in graphKvp.Value)
 679                    {
 580                        var edges = sourceKvp.Value.ToList();
 681
 682                        // Only flag fan-out (2+ outgoing edges) where all are optional
 583                        if (edges.Count < 2)
 684                            continue;
 685
 1186                        if (edges.Any(e => e.IsRequired))
 687                            continue;
 688
 689                        // All edges are optional
 290                        var symbol = edges[0].Symbol;
 291                        var location = edges[0].Location;
 692
 293                        endContext.ReportDiagnostic(Diagnostic.Create(
 294                            MafDiagnosticDescriptors.GraphAllEdgesOptional,
 295                            location,
 296                            edges.Count,
 297                            symbol.Name,
 298                            graphName));
 699                    }
 6100                }
 12101            });
 17102        });
 11103    }
 104}