| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using System.Collections.Immutable; |
| | | 3 | | using System.Linq; |
| | | 4 | | |
| | | 5 | | using Microsoft.CodeAnalysis; |
| | | 6 | | using Microsoft.CodeAnalysis.Diagnostics; |
| | | 7 | | |
| | | 8 | | namespace NexusLabs.Needlr.AgentFramework.Analyzers; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Analyzer that validates <c>Order</c> values within <c>[AgentSequenceMember]</c> pipeline declarations. |
| | | 12 | | /// </summary> |
| | | 13 | | /// <remarks> |
| | | 14 | | /// <para> |
| | | 15 | | /// <b>NDLRMAF006</b> (Error): Two or more agents in the same pipeline declare the same <c>Order</c> value. |
| | | 16 | | /// </para> |
| | | 17 | | /// <para> |
| | | 18 | | /// <b>NDLRMAF007</b> (Warning): The <c>Order</c> values in a pipeline are not contiguous — a gap exists. |
| | | 19 | | /// </para> |
| | | 20 | | /// </remarks> |
| | | 21 | | [DiagnosticAnalyzer(LanguageNames.CSharp)] |
| | | 22 | | public sealed class AgentSequenceOrderAnalyzer : DiagnosticAnalyzer |
| | | 23 | | { |
| | | 24 | | private const string AgentSequenceMemberAttributeName = "NexusLabs.Needlr.AgentFramework.AgentSequenceMemberAttribut |
| | | 25 | | |
| | | 26 | | public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => |
| | 489 | 27 | | ImmutableArray.Create( |
| | 489 | 28 | | MafDiagnosticDescriptors.DuplicateSequenceOrder, |
| | 489 | 29 | | MafDiagnosticDescriptors.GapInSequenceOrder); |
| | | 30 | | |
| | | 31 | | public override void Initialize(AnalysisContext context) |
| | | 32 | | { |
| | 24 | 33 | | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); |
| | 24 | 34 | | context.EnableConcurrentExecution(); |
| | | 35 | | |
| | 24 | 36 | | context.RegisterCompilationStartAction(compilationContext => |
| | 24 | 37 | | { |
| | 24 | 38 | | // pipeline name → list of (agent type name, order, attribute location) |
| | 15 | 39 | | var pipelineEntries = new ConcurrentDictionary<string, ConcurrentBag<(string AgentName, int Order, Location |
| | 15 | 40 | | StringComparer.Ordinal); |
| | 24 | 41 | | |
| | 15 | 42 | | compilationContext.RegisterSymbolAction(symbolContext => |
| | 15 | 43 | | { |
| | 179 | 44 | | var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol; |
| | 15 | 45 | | |
| | 774 | 46 | | foreach (var attr in typeSymbol.GetAttributes()) |
| | 15 | 47 | | { |
| | 208 | 48 | | if (attr.AttributeClass?.ToDisplayString() != AgentSequenceMemberAttributeName) |
| | 15 | 49 | | continue; |
| | 15 | 50 | | |
| | 44 | 51 | | if (attr.ConstructorArguments.Length < 2) |
| | 15 | 52 | | continue; |
| | 15 | 53 | | |
| | 44 | 54 | | if (attr.ConstructorArguments[0].Value is not string pipelineName |
| | 44 | 55 | | || string.IsNullOrWhiteSpace(pipelineName)) |
| | 15 | 56 | | continue; |
| | 15 | 57 | | |
| | 44 | 58 | | if (attr.ConstructorArguments[1].Value is not int order) |
| | 15 | 59 | | continue; |
| | 15 | 60 | | |
| | 44 | 61 | | var attrLocation = attr.ApplicationSyntaxReference?.SyntaxTree is { } tree |
| | 44 | 62 | | ? Location.Create(tree, attr.ApplicationSyntaxReference.Span) |
| | 44 | 63 | | : typeSymbol.Locations[0]; |
| | 15 | 64 | | |
| | 62 | 65 | | pipelineEntries.GetOrAdd(pipelineName, _ => new ConcurrentBag<(string, int, Location)>()) |
| | 44 | 66 | | .Add((typeSymbol.Name, order, attrLocation)); |
| | 15 | 67 | | } |
| | 194 | 68 | | }, SymbolKind.NamedType); |
| | 24 | 69 | | |
| | 15 | 70 | | compilationContext.RegisterCompilationEndAction(endContext => |
| | 15 | 71 | | { |
| | 66 | 72 | | foreach (var kvp in pipelineEntries) |
| | 15 | 73 | | { |
| | 18 | 74 | | var pipelineName = kvp.Key; |
| | 18 | 75 | | var entries = kvp.Value.ToList(); |
| | 15 | 76 | | |
| | 15 | 77 | | // NDLRMAF006: duplicate Order values |
| | 96 | 78 | | var duplicateGroups = entries.GroupBy(e => e.Order).Where(g => g.Count() > 1).ToList(); |
| | 52 | 79 | | foreach (var group in duplicateGroups) |
| | 15 | 80 | | { |
| | 52 | 81 | | foreach (var (agentName, order, location) in group) |
| | 15 | 82 | | { |
| | 18 | 83 | | endContext.ReportDiagnostic(Diagnostic.Create( |
| | 18 | 84 | | MafDiagnosticDescriptors.DuplicateSequenceOrder, |
| | 18 | 85 | | location, |
| | 18 | 86 | | pipelineName, |
| | 18 | 87 | | order, |
| | 18 | 88 | | agentName)); |
| | 15 | 89 | | } |
| | 15 | 90 | | } |
| | 15 | 91 | | |
| | 15 | 92 | | // NDLRMAF007: gap in Order sequence — only when no duplicate errors and 2+ members |
| | 18 | 93 | | if (duplicateGroups.Count == 0 && entries.Count >= 2) |
| | 15 | 94 | | { |
| | 55 | 95 | | var sortedOrders = entries.Select(e => e.Order).OrderBy(o => o).ToList(); |
| | 34 | 96 | | for (var i = 1; i < sortedOrders.Count; i++) |
| | 15 | 97 | | { |
| | 12 | 98 | | if (sortedOrders[i] != sortedOrders[i - 1] + 1) |
| | 15 | 99 | | { |
| | 4 | 100 | | var missingOrder = sortedOrders[i - 1] + 1; |
| | 32 | 101 | | foreach (var (_, _, location) in entries) |
| | 15 | 102 | | { |
| | 12 | 103 | | endContext.ReportDiagnostic(Diagnostic.Create( |
| | 12 | 104 | | MafDiagnosticDescriptors.GapInSequenceOrder, |
| | 12 | 105 | | location, |
| | 12 | 106 | | pipelineName, |
| | 12 | 107 | | missingOrder)); |
| | 15 | 108 | | } |
| | 15 | 109 | | break; // Report the first gap only per pipeline |
| | 15 | 110 | | } |
| | 15 | 111 | | } |
| | 15 | 112 | | } |
| | 15 | 113 | | } |
| | 30 | 114 | | }); |
| | 39 | 115 | | }); |
| | 24 | 116 | | } |
| | | 117 | | } |