< Summary

Information
Class: NexusLabs.Needlr.Generators.DiagnosticsGenerator
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/DiagnosticsGenerator.cs
Line coverage
97%
Covered lines: 720
Uncovered lines: 15
Coverable lines: 735
Total lines: 1258
Line coverage: 97.9%
Branch coverage
93%
Covered branches: 317
Total branches: 338
Branch coverage: 93.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/DiagnosticsGenerator.cs

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Linq;
 7using System.Text;
 8
 9using NexusLabs.Needlr.Generators.Models;
 10
 11namespace NexusLabs.Needlr.Generators;
 12
 13/// <summary>
 14/// Generates diagnostic markdown output files (DependencyGraph.md, LifetimeSummary.md, RegistrationIndex.md, AnalyzerSt
 15/// </summary>
 16internal static class DiagnosticsGenerator
 17{
 18    internal static string GenerateDiagnosticsSource(DiscoveryResult discoveryResult, string assemblyName, string? proje
 19    {
 9520        var builder = new StringBuilder();
 9521        var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName);
 9522        var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
 23
 9524        builder.AppendLine("// <auto-generated/>");
 9525        builder.AppendLine("// Needlr Diagnostic Output Metadata");
 9526        builder.AppendLine($"// Generated: {timestamp} UTC");
 9527        builder.AppendLine("// This file contains diagnostic markdown content for extraction by MSBuild.");
 9528        builder.AppendLine();
 9529        builder.AppendLine("#nullable enable");
 9530        builder.AppendLine();
 9531        builder.AppendLine($"namespace {safeAssemblyName}.Generated;");
 9532        builder.AppendLine();
 9533        builder.AppendLine("/// <summary>");
 9534        builder.AppendLine("/// Contains diagnostic output markdown content generated at compile time.");
 9535        builder.AppendLine("/// Use NeedlrDiagnostics MSBuild property to enable file generation.");
 9536        builder.AppendLine("/// </summary>");
 9537        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 9538        builder.AppendLine("internal static class NeedlrDiagnostics");
 9539        builder.AppendLine("{");
 40
 41        // Output path for MSBuild target
 9542        builder.AppendLine($"    public const string OutputPath = @\"{GeneratorHelpers.EscapeStringLiteral(options.Outpu
 9543        builder.AppendLine();
 44
 45        // Generate all three diagnostic outputs when enabled
 9546        var dependencyGraphContent = GenerateDependencyGraphMarkdown(discoveryResult, assemblyName, timestamp, options.T
 9547        builder.AppendLine("    /// <summary>DependencyGraph.md content</summary>");
 9548        builder.AppendLine($"    public const string DependencyGraph = @\"{GeneratorHelpers.EscapeVerbatimStringLiteral(
 9549        builder.AppendLine();
 50
 9551        var lifetimeSummaryContent = GenerateLifetimeSummaryMarkdown(discoveryResult, assemblyName, timestamp, options.T
 9552        builder.AppendLine("    /// <summary>LifetimeSummary.md content</summary>");
 9553        builder.AppendLine($"    public const string LifetimeSummary = @\"{GeneratorHelpers.EscapeVerbatimStringLiteral(
 9554        builder.AppendLine();
 55
 9556        var registrationIndexContent = GenerateRegistrationIndexMarkdown(discoveryResult, assemblyName, projectDirectory
 9557        builder.AppendLine("    /// <summary>RegistrationIndex.md content</summary>");
 9558        builder.AppendLine($"    public const string RegistrationIndex = @\"{GeneratorHelpers.EscapeVerbatimStringLitera
 9559        builder.AppendLine();
 60
 9561        var analyzerStatusContent = GenerateAnalyzerStatusMarkdown(timestamp);
 9562        builder.AppendLine("    /// <summary>AnalyzerStatus.md content</summary>");
 9563        builder.AppendLine($"    public const string AnalyzerStatus = @\"{GeneratorHelpers.EscapeVerbatimStringLiteral(a
 9564        builder.AppendLine();
 65
 9566        var optionsSummaryContent = GenerateOptionsSummaryMarkdown(discoveryResult, assemblyName, timestamp, options.Typ
 9567        builder.AppendLine("    /// <summary>OptionsSummary.md content</summary>");
 9568        builder.AppendLine($"    public const string OptionsSummary = @\"{GeneratorHelpers.EscapeVerbatimStringLiteral(o
 69
 9570        builder.AppendLine("}");
 71
 9572        return builder.ToString();
 73    }
 74
 75    internal static string GenerateDependencyGraphMarkdown(DiscoveryResult discovery, string assemblyName, string timest
 76    {
 11477        var sb = new StringBuilder();
 11478        var types = FilterTypes(discovery.InjectableTypes, typeFilter);
 11479        referencedAssemblyTypes = FilterReferencedAssemblyTypes(referencedAssemblyTypes, typeFilter);
 80
 11481        sb.AppendLine("# Needlr Dependency Graph");
 11482        sb.AppendLine();
 11483        sb.AppendLine($"Generated: {timestamp} UTC");
 11484        sb.AppendLine($"Assembly: {assemblyName}");
 11485        sb.AppendLine();
 86
 87        // Show referenced TypeRegistry assemblies with their types
 11488        if (referencedAssemblyTypes.Count > 0)
 89        {
 1590            sb.AppendLine("## Referenced Plugin Assemblies");
 1591            sb.AppendLine();
 1592            sb.AppendLine("Types from referenced assemblies with `[GenerateTypeRegistry]`:");
 1593            sb.AppendLine();
 94
 7895            foreach (var kvp in referencedAssemblyTypes.OrderBy(kv => kv.Key))
 96            {
 1697                var refAsm = kvp.Key;
 1698                var refTypes = kvp.Value;
 99
 16100                sb.AppendLine($"### {refAsm}");
 16101                sb.AppendLine();
 16102                sb.AppendLine("```mermaid");
 16103                sb.AppendLine("graph TD");
 104
 105                // Group by lifetime
 37106                var refSingletons = refTypes.Where(t => t.Lifetime == GeneratorLifetime.Singleton).ToList();
 37107                var refScopeds = refTypes.Where(t => t.Lifetime == GeneratorLifetime.Scoped).ToList();
 37108                var refTransients = refTypes.Where(t => t.Lifetime == GeneratorLifetime.Transient).ToList();
 109
 16110                if (refSingletons.Any())
 111                {
 16112                    sb.AppendLine($"    subgraph Singleton[\"{refAsm} - Singleton\"]");
 68113                    foreach (var type in refSingletons)
 114                    {
 18115                        var shape = type.IsDecorator ? "[[" : (type.HasFactory ? "{{" : "[");
 18116                        var endShape = type.IsDecorator ? "]]" : (type.HasFactory ? "}}" : "]");
 18117                        sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.FullName)}{shape}\"{type.ShortNa
 118                    }
 16119                    sb.AppendLine("    end");
 120                }
 16121                if (refScopeds.Any())
 122                {
 2123                    sb.AppendLine($"    subgraph Scoped[\"{refAsm} - Scoped\"]");
 8124                    foreach (var type in refScopeds)
 125                    {
 2126                        var shape = type.IsDecorator ? "[[" : (type.HasFactory ? "{{" : "[");
 2127                        var endShape = type.IsDecorator ? "]]" : (type.HasFactory ? "}}" : "]");
 2128                        sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.FullName)}{shape}\"{type.ShortNa
 129                    }
 2130                    sb.AppendLine("    end");
 131                }
 16132                if (refTransients.Any())
 133                {
 1134                    sb.AppendLine($"    subgraph Transient[\"{refAsm} - Transient\"]");
 4135                    foreach (var type in refTransients)
 136                    {
 1137                        var shape = type.IsDecorator ? "[[" : (type.HasFactory ? "{{" : "[");
 1138                        var endShape = type.IsDecorator ? "]]" : (type.HasFactory ? "}}" : "]");
 1139                        sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.FullName)}{shape}\"{type.ShortNa
 140                    }
 1141                    sb.AppendLine("    end");
 142                }
 143
 144                // Show dependency edges within this assembly
 37145                var refTypeNames = new HashSet<string>(refTypes.Select(t => t.ShortName), StringComparer.Ordinal);
 74146                foreach (var type in refTypes)
 147                {
 48148                    foreach (var dep in type.Dependencies)
 149                    {
 3150                        var depShort = GeneratorHelpers.GetShortTypeName(dep);
 3151                        var matchingType = refTypes.FirstOrDefault(t =>
 7152                            t.ShortName == depShort ||
 11153                            t.Interfaces.Any(i => GeneratorHelpers.GetShortTypeName(i) == depShort));
 3154                        if (matchingType.FullName != null)
 155                        {
 3156                            sb.AppendLine($"    {GeneratorHelpers.GetMermaidNodeId(type.FullName)} --> {GeneratorHelpers
 157                        }
 158                    }
 159                }
 160
 161                // Show factory→product edges (dotted arrows)
 37162                var factorySources = refTypes.Where(t => t.HasFactory).ToList();
 40163                foreach (var source in factorySources)
 164                {
 4165                    var factoryName = source.ShortName + "Factory";
 8166                    var matchingFactory = refTypes.FirstOrDefault(t => t.ShortName == factoryName);
 4167                    if (matchingFactory.FullName != null)
 168                    {
 0169                        sb.AppendLine($"    {GeneratorHelpers.GetMermaidNodeId(matchingFactory.FullName)} -.->|produces|
 170                    }
 171                }
 172
 16173                sb.AppendLine("```");
 16174                sb.AppendLine();
 175
 176                // Show summary table for this assembly
 16177                sb.AppendLine($"| Service | Lifetime | Interfaces |");
 16178                sb.AppendLine($"|---------|----------|------------|");
 95179                foreach (var type in refTypes.OrderBy(t => t.ShortName))
 180                {
 21181                    var interfaces = type.Interfaces.Any() ? string.Join(", ", type.Interfaces.Select(GeneratorHelpers.G
 21182                    sb.AppendLine($"| {type.ShortName} | {type.Lifetime} | {interfaces} |");
 183                }
 16184                sb.AppendLine();
 185            }
 186        }
 187
 45592188        var singletons = types.Where(t => t.Lifetime == GeneratorLifetime.Singleton).ToList();
 45592189        var scopeds = types.Where(t => t.Lifetime == GeneratorLifetime.Scoped).ToList();
 45592190        var transients = types.Where(t => t.Lifetime == GeneratorLifetime.Transient).ToList();
 191
 114192        sb.AppendLine("## Service Dependencies");
 114193        sb.AppendLine();
 114194        sb.AppendLine("```mermaid");
 114195        sb.AppendLine("graph TD");
 196
 197        // Emit subgraphs by lifetime
 114198        if (singletons.Any())
 199        {
 108200            sb.AppendLine("    subgraph Singleton");
 91170201            foreach (var type in singletons)
 45477202                sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.TypeName)}[\"{GeneratorHelpers.GetShortT
 108203            sb.AppendLine("    end");
 204        }
 114205        if (scopeds.Any())
 206        {
 1207            sb.AppendLine("    subgraph Scoped");
 4208            foreach (var type in scopeds)
 1209                sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.TypeName)}[\"{GeneratorHelpers.GetShortT
 1210            sb.AppendLine("    end");
 211        }
 114212        if (transients.Any())
 213        {
 0214            sb.AppendLine("    subgraph Transient");
 0215            foreach (var type in transients)
 0216                sb.AppendLine($"        {GeneratorHelpers.GetMermaidNodeId(type.TypeName)}[\"{GeneratorHelpers.GetShortT
 0217            sb.AppendLine("    end");
 218        }
 219
 220        // Emit edges for dependencies
 45592221        var typeNames = new HashSet<string>(types.Select(t => GeneratorHelpers.GetShortTypeName(t.TypeName)), StringComp
 91184222        foreach (var type in types)
 223        {
 128226224            foreach (var dep in type.ConstructorParameterTypes)
 225            {
 18635226                var depShort = GeneratorHelpers.GetShortTypeName(dep);
 227                // Find if we have a type implementing this interface
 18635228                var matchingType = types.FirstOrDefault(t =>
 9459657229                    GeneratorHelpers.GetShortTypeName(t.TypeName) == depShort ||
 9479022230                    t.InterfaceNames.Any(i => GeneratorHelpers.GetShortTypeName(i) == depShort));
 231
 18635232                if (matchingType.TypeName != null)
 6109233                    sb.AppendLine($"    {GeneratorHelpers.GetMermaidNodeId(type.TypeName)} --> {GeneratorHelpers.GetMerm
 234            }
 235        }
 236
 114237        sb.AppendLine("```");
 114238        sb.AppendLine();
 239
 240        // Decorator chains section (aggregates host and plugin decorators)
 114241        var pluginDecorators = referencedAssemblyTypes
 40242            .SelectMany(kv => kv.Value.Where(t => t.IsDecorator).Select(t => (Assembly: kv.Key, Type: t)))
 114243            .ToList();
 244
 114245        var decorators = FilterDecorators(discovery.Decorators, typeFilter);
 246
 114247        if (decorators.Any() || pluginDecorators.Any())
 248        {
 11249            sb.AppendLine("## Decorator Chains");
 11250            sb.AppendLine();
 11251            sb.AppendLine("```mermaid");
 11252            sb.AppendLine("graph LR");
 253
 254            // Host decorators - group by service type
 11255            var decoratorsByService = decorators
 13256                .GroupBy(d => d.ServiceTypeName)
 20257                .OrderBy(g => g.Key);
 258
 40259            foreach (var serviceGroup in decoratorsByService)
 260            {
 9261                var serviceShortName = GeneratorHelpers.GetShortTypeName(serviceGroup.Key);
 17262                var orderedDecorators = serviceGroup.OrderByDescending(d => d.Order).ToList();
 263
 264                // Find the underlying implementation for this service
 9265                var implementation = types.FirstOrDefault(t =>
 16266                    t.InterfaceNames.Any(i => GeneratorHelpers.GetShortTypeName(i) == serviceShortName) &&
 29267                    !decorators.Any(d => GeneratorHelpers.GetShortTypeName(d.DecoratorTypeName) == GeneratorHelpers.GetS
 268
 269                // Build the chain: highest order decorator -> ... -> lowest order decorator -> implementation
 44270                for (int i = 0; i < orderedDecorators.Count; i++)
 271                {
 13272                    var decorator = orderedDecorators[i];
 13273                    var decoratorId = GeneratorHelpers.GetMermaidNodeId(decorator.DecoratorTypeName);
 13274                    var decoratorName = GeneratorHelpers.GetShortTypeName(decorator.DecoratorTypeName);
 275
 276                    // Add node definition
 13277                    sb.AppendLine($"    {decoratorId}[[\"{decoratorName}\"]]");
 278
 279                    // Add edge to next decorator or implementation
 13280                    if (i < orderedDecorators.Count - 1)
 281                    {
 4282                        var nextDecorator = orderedDecorators[i + 1];
 4283                        sb.AppendLine($"    {decoratorId} --> {GeneratorHelpers.GetMermaidNodeId(nextDecorator.Decorator
 284                    }
 9285                    else if (implementation.TypeName != null)
 286                    {
 8287                        var implId = GeneratorHelpers.GetMermaidNodeId(implementation.TypeName);
 8288                        var implName = GeneratorHelpers.GetShortTypeName(implementation.TypeName);
 8289                        sb.AppendLine($"    {implId}[\"{implName}\"]");
 8290                        sb.AppendLine($"    {decoratorId} --> {implId}");
 291                    }
 292                }
 293            }
 294
 295            // Plugin decorators (we don't have chain order info, just show the decorator types)
 31296            foreach (var (assembly, type) in pluginDecorators.OrderBy(x => x.Type.ShortName))
 297            {
 3298                var decoratorId = GeneratorHelpers.GetMermaidNodeId(type.FullName);
 3299                var decoratorName = type.ShortName;
 3300                sb.AppendLine($"    {decoratorId}[[\"{decoratorName}\"]]");
 301            }
 302
 11303            sb.AppendLine("```");
 11304            sb.AppendLine();
 305        }
 306
 307        // Intercepted services section (aggregates host and plugin interceptors)
 114308        var interceptedServices = FilterInterceptedServices(discovery.InterceptedServices, typeFilter);
 114309        var pluginInterceptors = referencedAssemblyTypes
 40310            .SelectMany(kv => kv.Value.Where(t => t.HasInterceptorProxy).Select(t => (Assembly: kv.Key, Type: t)))
 114311            .ToList();
 312
 114313        if (interceptedServices.Any() || pluginInterceptors.Any())
 314        {
 4315            sb.AppendLine("## Intercepted Services");
 4316            sb.AppendLine();
 4317            sb.AppendLine("```mermaid");
 4318            sb.AppendLine("graph LR");
 319
 320            // Host intercepted services
 14321            foreach (var service in interceptedServices.OrderBy(s => s.TypeName))
 322            {
 2323                var targetId = GeneratorHelpers.GetMermaidNodeId(service.TypeName);
 2324                var targetName = GeneratorHelpers.GetShortTypeName(service.TypeName);
 2325                var proxyId = targetId + "_Proxy";
 2326                var proxyName = targetName + "_InterceptorProxy";
 327
 328                // Target service
 2329                sb.AppendLine($"    {targetId}[\"{targetName}\"]");
 330                // Proxy with stadium shape
 2331                sb.AppendLine($"    {proxyId}[[\"{proxyName}\"]]");
 332                // Edge showing proxy wraps target
 2333                sb.AppendLine($"    {proxyId} -.->|wraps| {targetId}");
 334
 335                // Show interceptors applied
 8336                foreach (var interceptorType in service.AllInterceptorTypeNames)
 337                {
 2338                    var interceptorId = GeneratorHelpers.GetMermaidNodeId(interceptorType);
 2339                    var interceptorName = GeneratorHelpers.GetShortTypeName(interceptorType);
 2340                    sb.AppendLine($"    {interceptorId}([[\"{interceptorName}\"]])");
 2341                    sb.AppendLine($"    {proxyId} --> {interceptorId}");
 342                }
 343            }
 344
 345            // Plugin intercepted services
 17346            foreach (var (assembly, type) in pluginInterceptors.OrderBy(x => x.Type.ShortName))
 347            {
 3348                var targetId = GeneratorHelpers.GetMermaidNodeId(type.FullName);
 3349                var targetName = type.ShortName;
 3350                var proxyId = targetId + "_Proxy";
 3351                var proxyName = targetName + "_InterceptorProxy";
 352
 3353                sb.AppendLine($"    {targetId}[\"{targetName}\"]");
 3354                sb.AppendLine($"    {proxyId}[[\"{proxyName}\"]]");
 3355                sb.AppendLine($"    {proxyId} -.->|wraps| {targetId}");
 356            }
 357
 4358            sb.AppendLine("```");
 4359            sb.AppendLine();
 360        }
 361
 362        // Keyed services section
 45592363        var keyedTypes = types.Where(t => t.IsKeyed).ToList();
 114364        if (keyedTypes.Any())
 365        {
 4366            sb.AppendLine("## Keyed Services");
 4367            sb.AppendLine();
 4368            sb.AppendLine("```mermaid");
 4369            sb.AppendLine("graph TD");
 370
 371            // Group by service key
 4372            var typesByKey = keyedTypes
 14373                .SelectMany(t => t.ServiceKeys.Select(k => (Key: k, Type: t)))
 7374                .GroupBy(x => x.Key)
 10375                .OrderBy(g => g.Key);
 376
 20377            foreach (var keyGroup in typesByKey)
 378            {
 6379                var safeKey = GeneratorHelpers.SanitizeMermaidId(keyGroup.Key);
 6380                sb.AppendLine($"    subgraph key_{safeKey}[\"{GeneratorHelpers.EscapeMermaidLabel(keyGroup.Key)}\"]");
 33381                foreach (var item in keyGroup.OrderBy(x => x.Type.TypeName))
 382                {
 7383                    var nodeId = GeneratorHelpers.GetMermaidNodeId(item.Type.TypeName);
 7384                    var nodeName = GeneratorHelpers.GetShortTypeName(item.Type.TypeName);
 7385                    sb.AppendLine($"        {nodeId}[\"{nodeName}\"]");
 386                }
 6387                sb.AppendLine("    end");
 388            }
 389
 4390            sb.AppendLine("```");
 4391            sb.AppendLine();
 392        }
 393
 394        // Plugin assemblies section
 114395        var plugins = FilterPluginTypes(discovery.PluginTypes, typeFilter);
 114396        if (plugins.Any())
 397        {
 96398            sb.AppendLine("## Plugin Assemblies");
 96399            sb.AppendLine();
 96400            sb.AppendLine("```mermaid");
 96401            sb.AppendLine("graph TD");
 402
 403            // Group plugins by assembly
 96404            var pluginsByAssembly = plugins
 340405                .GroupBy(p => p.AssemblyName)
 252406                .OrderBy(g => g.Key);
 407
 504408            foreach (var asmGroup in pluginsByAssembly)
 409            {
 156410                var safeAsm = GeneratorHelpers.SanitizeMermaidId(asmGroup.Key);
 156411                var shortAsm = GeneratorHelpers.GetShortTypeName(asmGroup.Key);
 156412                sb.AppendLine($"    subgraph asm_{safeAsm}[\"{GeneratorHelpers.EscapeMermaidLabel(shortAsm)}\"]");
 1332413                foreach (var plugin in asmGroup.OrderBy(p => p.TypeName))
 414                {
 340415                    var nodeId = GeneratorHelpers.GetMermaidNodeId(plugin.TypeName);
 340416                    var nodeName = GeneratorHelpers.GetShortTypeName(plugin.TypeName);
 417                    // Use stadium shape for plugins
 340418                    sb.AppendLine($"        {nodeId}([\"{nodeName}\"])");
 419                }
 156420                sb.AppendLine("    end");
 421            }
 422
 96423            sb.AppendLine("```");
 96424            sb.AppendLine();
 425        }
 426
 427        // Factory services section (aggregates host and plugin factories)
 114428        var factories = FilterFactories(discovery.Factories, typeFilter);
 114429        var pluginFactories = referencedAssemblyTypes
 41430            .SelectMany(kv => kv.Value.Where(t => t.HasFactory).Select(t => (Assembly: kv.Key, Type: t)))
 114431            .ToList();
 432
 114433        if (factories.Any() || pluginFactories.Any())
 434        {
 10435            sb.AppendLine("## Factory Services");
 10436            sb.AppendLine();
 10437            sb.AppendLine("```mermaid");
 10438            sb.AppendLine("graph LR");
 439
 440            // Host factories
 41441            foreach (var factory in factories.OrderBy(f => f.TypeName))
 442            {
 7443                var sourceNodeId = GeneratorHelpers.GetMermaidNodeId(factory.TypeName);
 7444                var sourceName = GeneratorHelpers.GetShortTypeName(factory.TypeName);
 7445                var factoryNodeId = sourceNodeId + "Factory";
 7446                var factoryName = sourceName + "Factory";
 447
 448                // Generated factory with regular shape
 7449                sb.AppendLine($"    {factoryNodeId}[\"{factoryName}\"]");
 450                // Source type (product) with hexagon shape
 7451                sb.AppendLine($"    {sourceNodeId}{{{{\"{sourceName}\"}}}}");
 452                // Edge showing factory produces the type (dotted arrow)
 7453                sb.AppendLine($"    {factoryNodeId} -.->|produces| {sourceNodeId}");
 454            }
 455
 456            // Plugin factories
 32457            foreach (var (assembly, type) in pluginFactories.OrderBy(f => f.Type.ShortName))
 458            {
 4459                var sourceNodeId = GeneratorHelpers.GetMermaidNodeId(type.FullName);
 4460                var sourceName = type.ShortName;
 4461                var factoryNodeId = sourceNodeId + "Factory";
 4462                var factoryName = sourceName + "Factory";
 463
 464                // Generated factory with regular shape
 4465                sb.AppendLine($"    {factoryNodeId}[\"{factoryName}\"]");
 466                // Source type (product) with hexagon shape
 4467                sb.AppendLine($"    {sourceNodeId}{{{{\"{sourceName}\"}}}}");
 468                // Edge showing factory produces the type (dotted arrow)
 4469                sb.AppendLine($"    {factoryNodeId} -.->|produces| {sourceNodeId}");
 470            }
 471
 10472            sb.AppendLine("```");
 10473            sb.AppendLine();
 474        }
 475
 476        // Interface mapping section
 45592477        var typesWithInterfaces = types.Where(t => t.InterfaceNames.Length > 0).ToList();
 114478        if (typesWithInterfaces.Any())
 479        {
 85480            sb.AppendLine("## Interface Mapping");
 85481            sb.AppendLine();
 85482            sb.AppendLine("```mermaid");
 85483            sb.AppendLine("graph LR");
 484
 557485            foreach (var type in typesWithInterfaces.OrderBy(t => t.TypeName))
 486            {
 129487                var implId = GeneratorHelpers.GetMermaidNodeId(type.TypeName);
 129488                var implName = GeneratorHelpers.GetShortTypeName(type.TypeName);
 129489                sb.AppendLine($"    {implId}[\"{implName}\"]");
 490
 516491                foreach (var iface in type.InterfaceNames)
 492                {
 129493                    var ifaceId = GeneratorHelpers.GetMermaidNodeId(iface);
 129494                    var ifaceName = GeneratorHelpers.GetShortTypeName(iface);
 495                    // Interface uses rounded box, dotted edge points from interface to impl
 129496                    sb.AppendLine($"    {ifaceId}((\"{ifaceName}\"))");
 129497                    sb.AppendLine($"    {ifaceId} -.-> {implId}");
 498                }
 499            }
 500
 85501            sb.AppendLine("```");
 85502            sb.AppendLine();
 503        }
 504
 505        // Complexity metrics section
 114506        sb.AppendLine("## Complexity Metrics");
 114507        sb.AppendLine();
 114508        sb.AppendLine("| Metric | Value |");
 114509        sb.AppendLine("|--------|-------|");
 510
 114511        sb.AppendLine($"| Total Services | {types.Count} |");
 512
 513        // Calculate max dependency depth using BFS
 114514        var maxDepth = CalculateMaxDependencyDepth(types);
 114515        sb.AppendLine($"| Max Dependency Depth | {maxDepth} |");
 516
 517        // Calculate hub services (services that appear as dependencies in 3+ other services)
 114518        var hubServices = CalculateHubServices(types, 3);
 114519        sb.AppendLine($"| Hub Services (≥3 dependents) | {hubServices.Count} |");
 520
 114521        if (hubServices.Any())
 522        {
 76523            sb.AppendLine();
 828524            sb.AppendLine("**Hub Services:** " + string.Join(", ", hubServices.Select(h => $"{GeneratorHelpers.GetShortT
 525        }
 526
 114527        sb.AppendLine();
 528
 529        // Dependency details table
 114530        sb.AppendLine("## Dependency Details");
 114531        sb.AppendLine();
 114532        sb.AppendLine("| Service | Lifetime | Dependencies |");
 114533        sb.AppendLine("|---------|----------|--------------|");
 534
 136662535        foreach (var type in types.OrderBy(t => t.TypeName))
 536        {
 45478537            var deps = type.ConstructorParameterTypes.Any()
 45478538                ? string.Join(", ", type.ConstructorParameterTypes.Select(GeneratorHelpers.GetShortTypeName))
 45478539                : "-";
 45478540            sb.AppendLine($"| {GeneratorHelpers.GetShortTypeName(type.TypeName)} | {type.Lifetime} | {deps} |");
 541        }
 542
 114543        return sb.ToString();
 544    }
 545
 546    internal static string GenerateLifetimeSummaryMarkdown(DiscoveryResult discovery, string assemblyName, string timest
 547    {
 102548        var sb = new StringBuilder();
 102549        var types = FilterTypes(discovery.InjectableTypes, typeFilter);
 102550        referencedAssemblyTypes = FilterReferencedAssemblyTypes(referencedAssemblyTypes, typeFilter);
 551
 45551552        var singletons = types.Where(t => t.Lifetime == GeneratorLifetime.Singleton).ToList();
 45551553        var scopeds = types.Where(t => t.Lifetime == GeneratorLifetime.Scoped).ToList();
 45551554        var transients = types.Where(t => t.Lifetime == GeneratorLifetime.Transient).ToList();
 102555        var total = types.Count;
 556
 102557        sb.AppendLine("# Needlr Lifetime Summary");
 102558        sb.AppendLine();
 102559        sb.AppendLine($"Generated: {timestamp} UTC");
 102560        sb.AppendLine($"Assembly: {assemblyName}");
 102561        sb.AppendLine();
 562
 563        // Referenced plugin assemblies lifetime breakdown
 102564        if (referencedAssemblyTypes.Count > 0)
 565        {
 14566            sb.AppendLine("## Referenced Plugin Assemblies");
 14567            sb.AppendLine();
 568
 73569            foreach (var kvp in referencedAssemblyTypes.OrderBy(kv => kv.Key))
 570            {
 15571                var refAsm = kvp.Key;
 15572                var refTypes = kvp.Value;
 35573                var refSingletons = refTypes.Count(t => t.Lifetime == GeneratorLifetime.Singleton);
 35574                var refScopeds = refTypes.Count(t => t.Lifetime == GeneratorLifetime.Scoped);
 35575                var refTransients = refTypes.Count(t => t.Lifetime == GeneratorLifetime.Transient);
 15576                var refTotal = refTypes.Count;
 577
 15578                sb.AppendLine($"### {refAsm}");
 15579                sb.AppendLine();
 15580                sb.AppendLine("| Lifetime | Count | % |");
 15581                sb.AppendLine("|----------|-------|---|");
 15582                sb.AppendLine($"| Singleton | {refSingletons} | {GeneratorHelpers.Percentage(refSingletons, refTotal)}% 
 15583                sb.AppendLine($"| Scoped | {refScopeds} | {GeneratorHelpers.Percentage(refScopeds, refTotal)}% |");
 15584                sb.AppendLine($"| Transient | {refTransients} | {GeneratorHelpers.Percentage(refTransients, refTotal)}% 
 15585                sb.AppendLine($"| **Total** | **{refTotal}** | 100% |");
 15586                sb.AppendLine();
 587            }
 588        }
 589
 102590        sb.AppendLine("## Registration Counts");
 102591        sb.AppendLine();
 102592        sb.AppendLine("| Lifetime | Count | % |");
 102593        sb.AppendLine("|----------|-------|---|");
 594
 102595        if (total > 0)
 596        {
 98597            sb.AppendLine($"| Singleton | {singletons.Count} | {GeneratorHelpers.Percentage(singletons.Count, total)}% |
 98598            sb.AppendLine($"| Scoped | {scopeds.Count} | {GeneratorHelpers.Percentage(scopeds.Count, total)}% |");
 98599            sb.AppendLine($"| Transient | {transients.Count} | {GeneratorHelpers.Percentage(transients.Count, total)}% |
 98600            sb.AppendLine($"| **Total** | **{total}** | 100% |");
 601        }
 602        else
 603        {
 4604            sb.AppendLine("| (none) | 0 | - |");
 605        }
 606
 102607        sb.AppendLine();
 608
 609        // List by category
 102610        if (singletons.Any())
 611        {
 98612            sb.AppendLine($"## Singleton ({singletons.Count})");
 98613            sb.AppendLine();
 136543614            foreach (var type in singletons.OrderBy(t => t.TypeName))
 45449615                sb.AppendLine($"- {GeneratorHelpers.GetShortTypeName(type.TypeName)}");
 98616            sb.AppendLine();
 617        }
 618
 102619        if (scopeds.Any())
 620        {
 0621            sb.AppendLine($"## Scoped ({scopeds.Count})");
 0622            sb.AppendLine();
 0623            foreach (var type in scopeds.OrderBy(t => t.TypeName))
 0624                sb.AppendLine($"- {GeneratorHelpers.GetShortTypeName(type.TypeName)}");
 0625            sb.AppendLine();
 626        }
 627
 102628        if (transients.Any())
 629        {
 0630            sb.AppendLine($"## Transient ({transients.Count})");
 0631            sb.AppendLine();
 0632            foreach (var type in transients.OrderBy(t => t.TypeName))
 0633                sb.AppendLine($"- {GeneratorHelpers.GetShortTypeName(type.TypeName)}");
 0634            sb.AppendLine();
 635        }
 636
 102637        return sb.ToString();
 638    }
 639
 640    internal static string GenerateRegistrationIndexMarkdown(DiscoveryResult discovery, string assemblyName, string? pro
 641    {
 130642        var sb = new StringBuilder();
 130643        var types = FilterTypes(discovery.InjectableTypes, typeFilter);
 130644        var plugins = FilterPluginTypes(discovery.PluginTypes, typeFilter);
 130645        var decorators = FilterDecorators(discovery.Decorators, typeFilter);
 130646        referencedAssemblyTypes = FilterReferencedAssemblyTypes(referencedAssemblyTypes, typeFilter);
 647
 130648        sb.AppendLine("# Needlr Registration Index");
 130649        sb.AppendLine();
 130650        sb.AppendLine($"Generated: {timestamp} UTC");
 130651        sb.AppendLine($"Assembly: {assemblyName}");
 130652        sb.AppendLine();
 653
 654        // Referenced plugin assemblies services
 130655        if (referencedAssemblyTypes.Count > 0)
 656        {
 16657            sb.AppendLine("## Referenced Plugin Assemblies");
 16658            sb.AppendLine();
 659
 83660            foreach (var kvp in referencedAssemblyTypes.OrderBy(kv => kv.Key))
 661            {
 17662                var refAsm = kvp.Key;
 17663                var refTypes = kvp.Value;
 664
 17665                sb.AppendLine($"### {refAsm} ({refTypes.Count} services)");
 17666                sb.AppendLine();
 17667                sb.AppendLine("| # | Interface | Implementation | Lifetime |");
 17668                sb.AppendLine("|---|-----------|----------------|----------|");
 669
 17670                var index = 1;
 97671                foreach (var type in refTypes.OrderBy(t => t.ShortName))
 672                {
 21673                    var iface = type.Interfaces.FirstOrDefault() ?? "-";
 21674                    sb.AppendLine($"| {index} | {GeneratorHelpers.GetShortTypeName(iface)} | {type.ShortName} | {type.Li
 21675                    index++;
 676                }
 17677                sb.AppendLine();
 678            }
 679        }
 680
 681        // Services table
 130682        sb.AppendLine($"## Services ({types.Count})");
 130683        sb.AppendLine();
 684
 130685        if (types.Any())
 686        {
 107687            sb.AppendLine("| # | Interface | Implementation | Lifetime | Source |");
 107688            sb.AppendLine("|---|-----------|----------------|----------|--------|");
 689
 107690            var index = 1;
 136591691            foreach (var type in types.OrderBy(t => t.TypeName))
 692            {
 45459693                var iface = type.InterfaceNames.FirstOrDefault() ?? "-";
 45459694                var source = BreadcrumbWriter.GetRelativeSourcePath(type.SourceFilePath, projectDirectory);
 45459695                sb.AppendLine($"| {index} | {GeneratorHelpers.GetShortTypeName(iface)} | {GeneratorHelpers.GetShortTypeN
 45459696                index++;
 697            }
 107698            sb.AppendLine();
 699        }
 700        else
 701        {
 23702            sb.AppendLine("No injectable services discovered.");
 23703            sb.AppendLine();
 704        }
 705
 706        // Decorators section (aggregates host and plugin decorators)
 130707        var pluginDecorators = referencedAssemblyTypes
 43708            .SelectMany(kv => kv.Value.Where(t => t.IsDecorator).Select(t => (Assembly: kv.Key, Type: t)))
 130709            .ToList();
 710
 130711        if (decorators.Any() || pluginDecorators.Any())
 712        {
 17713            var totalCount = decorators.Count + pluginDecorators.Count;
 17714            sb.AppendLine($"## Decorators ({totalCount})");
 17715            sb.AppendLine();
 17716            sb.AppendLine("| Service | Decorator Chain | Assembly |");
 17717            sb.AppendLine("|---------|-----------------|----------|");
 718
 719            // Host decorators
 17720            var decoratorsByTarget = decorators
 17721                .GroupBy(d => d.ServiceTypeName)
 29722                .OrderBy(g => g.Key);
 723
 58724            foreach (var group in decoratorsByTarget)
 725            {
 12726                var chain = string.Join(" → ",
 17727                    group.OrderBy(d => d.Order)
 29728                         .Select(d => GeneratorHelpers.GetShortTypeName(d.DecoratorTypeName)));
 12729                sb.AppendLine($"| {GeneratorHelpers.GetShortTypeName(group.Key)} | {chain} | (host) |");
 730            }
 731
 732            // Plugin decorators
 49733            foreach (var (assembly, type) in pluginDecorators.OrderBy(x => x.Type.ShortName))
 734            {
 735                // For plugin decorators, we show them individually since we don't have chain info
 5736                var serviceName = type.Interfaces.FirstOrDefault() ?? "-";
 5737                sb.AppendLine($"| {GeneratorHelpers.GetShortTypeName(serviceName)} | {type.ShortName} | {assembly} |");
 738            }
 17739            sb.AppendLine();
 740        }
 741
 742        // Interceptors section (aggregates host and plugin interceptors)
 130743        var interceptedServices = FilterInterceptedServices(discovery.InterceptedServices, typeFilter);
 130744        var pluginIntercepted = referencedAssemblyTypes
 43745            .SelectMany(kv => kv.Value.Where(t => t.HasInterceptorProxy).Select(t => (Assembly: kv.Key, Type: t)))
 130746            .ToList();
 747
 130748        if (interceptedServices.Any() || pluginIntercepted.Any())
 749        {
 12750            var totalCount = interceptedServices.Count + pluginIntercepted.Count;
 12751            sb.AppendLine($"## Intercepted Services ({totalCount})");
 12752            sb.AppendLine();
 12753            sb.AppendLine("| Service | Interceptors | Proxy | Assembly |");
 12754            sb.AppendLine("|---------|--------------|-------|----------|");
 755
 756            // Host intercepted services
 45757            foreach (var service in interceptedServices.OrderBy(s => s.TypeName))
 758            {
 7759                var serviceName = GeneratorHelpers.GetShortTypeName(service.TypeName);
 7760                var interceptors = string.Join(", ", service.AllInterceptorTypeNames.Select(GeneratorHelpers.GetShortTyp
 7761                var proxyName = serviceName + "_InterceptorProxy";
 7762                sb.AppendLine($"| {serviceName} | {interceptors} | {proxyName} | (host) |");
 763            }
 764
 765            // Plugin intercepted services
 39766            foreach (var (assembly, type) in pluginIntercepted.OrderBy(x => x.Type.ShortName))
 767            {
 5768                var proxyName = type.ShortName + "_InterceptorProxy";
 5769                sb.AppendLine($"| {type.ShortName} | (see plugin) | {proxyName} | {assembly} |");
 770            }
 12771            sb.AppendLine();
 772        }
 773
 774        // Factories section (aggregates host and plugin factories)
 130775        var factories = FilterFactories(discovery.Factories, typeFilter);
 130776        var pluginFactories = referencedAssemblyTypes
 44777            .SelectMany(kv => kv.Value.Where(t => t.HasFactory).Select(t => (Assembly: kv.Key, Type: t)))
 130778            .ToList();
 779
 130780        if (factories.Any() || pluginFactories.Any())
 781        {
 17782            var totalCount = factories.Count + pluginFactories.Count;
 17783            sb.AppendLine($"## Factories ({totalCount})");
 17784            sb.AppendLine();
 17785            sb.AppendLine("| Source Type | Factory Interface | Generated Factory | Assembly |");
 17786            sb.AppendLine("|-------------|-------------------|-------------------|----------|");
 787
 788            // Host factories
 67789            foreach (var factory in factories.OrderBy(f => f.TypeName))
 790            {
 11791                var sourceName = factory.SimpleTypeName;
 11792                var factoryInterface = "I" + sourceName + "Factory";
 11793                var factoryImpl = sourceName + "Factory";
 11794                sb.AppendLine($"| {sourceName} | {factoryInterface} | {factoryImpl} | (host) |");
 795            }
 796
 797            // Plugin factories
 52798            foreach (var (assembly, type) in pluginFactories.OrderBy(x => x.Type.ShortName))
 799            {
 6800                var factoryInterface = "I" + type.ShortName + "Factory";
 6801                var factoryImpl = type.ShortName + "Factory";
 6802                sb.AppendLine($"| {type.ShortName} | {factoryInterface} | {factoryImpl} | {assembly} |");
 803            }
 17804            sb.AppendLine();
 805        }
 806
 807        // Plugins section
 130808        if (plugins.Any())
 809        {
 779810            var orderedPlugins = plugins.OrderBy(p => p.Order).ThenBy(p => p.TypeName);
 811
 97812            sb.AppendLine($"## Plugins ({plugins.Count})");
 97813            sb.AppendLine();
 97814            sb.AppendLine("| Order | Plugin | Interfaces |");
 97815            sb.AppendLine("|-------|--------|------------|");
 816
 876817            foreach (var plugin in orderedPlugins)
 818            {
 341819                var interfaces = string.Join(", ", plugin.InterfaceNames.Select(GeneratorHelpers.GetShortTypeName));
 341820                sb.AppendLine($"| {plugin.Order} | {GeneratorHelpers.GetShortTypeName(plugin.TypeName)} | {interfaces} |
 821            }
 97822            sb.AppendLine();
 823        }
 824
 825        // Keyed Services section
 45589826        var keyedTypes = types.Where(t => t.ServiceKeys.Length > 0).ToList();
 130827        if (keyedTypes.Any())
 828        {
 11829            sb.AppendLine($"## Keyed Services ({keyedTypes.Sum(t => t.ServiceKeys.Length)})");
 4830            sb.AppendLine();
 4831            sb.AppendLine("| Key | Interface | Implementation | Lifetime |");
 4832            sb.AppendLine("|-----|-----------|----------------|----------|");
 833
 29834            foreach (var type in keyedTypes.OrderBy(t => t.TypeName))
 835            {
 30836                foreach (var key in type.ServiceKeys)
 837                {
 8838                    var iface = type.InterfaceNames.FirstOrDefault() ?? "-";
 8839                    sb.AppendLine($"| `\"{GeneratorHelpers.EscapeMarkdownTableCell(key)}\"` | {GeneratorHelpers.GetShort
 840                }
 841            }
 4842            sb.AppendLine();
 843        }
 844
 130845        return sb.ToString();
 846    }
 847
 848    internal static string GenerateAnalyzerStatusMarkdown(string timestamp)
 849    {
 95850        var sb = new StringBuilder();
 851
 95852        sb.AppendLine("# Needlr Analyzer Status");
 95853        sb.AppendLine();
 95854        sb.AppendLine($"Generated: {timestamp} UTC");
 95855        sb.AppendLine();
 95856        sb.AppendLine("## Active Analyzers");
 95857        sb.AppendLine();
 95858        sb.AppendLine("| ID | Name | Status | Default Severity | Description |");
 95859        sb.AppendLine("|:---|:-----|:-------|:-----------------|:------------|");
 95860        sb.AppendLine("| NDLRCOR001 | Reflection in AOT | ⚪ Conditional | Error | Detects reflection APIs in AOT project
 95861        sb.AppendLine("| NDLRCOR002 | Plugin Constructor | ✅ Active | Warning | Plugins should have parameterless constr
 95862        sb.AppendLine("| NDLRCOR003 | DeferToContainer in Generated | ✅ Active | Error | [DeferToContainer] must be on u
 95863        sb.AppendLine("| NDLRCOR004 | Global Namespace Type | ✅ Active | Warning | Types in global namespace may not be 
 95864        sb.AppendLine("| NDLRCOR005 | Lifetime Mismatch | ✅ Active | Warning | Detects captive dependencies |");
 95865        sb.AppendLine("| NDLRCOR006 | Circular Dependency | ✅ Active | Error | Detects circular service dependencies |")
 95866        sb.AppendLine("| NDLRCOR007 | Intercept Type | ✅ Active | Error | Intercept type must implement IMethodIntercept
 95867        sb.AppendLine("| NDLRCOR008 | Intercept Without Interface | ✅ Active | Warning | [Intercept] requires interface-
 95868        sb.AppendLine("| NDLRCOR009 | Lazy Resolution | ✅ Active | Info | Lazy<T> references undiscovered type |");
 95869        sb.AppendLine("| NDLRCOR010 | Collection Resolution | ✅ Active | Info | IEnumerable<T> has no implementations |"
 95870        sb.AppendLine("| NDLRCOR011 | Keyed Service Usage | ✅ Active | Info | Tracks [FromKeyedServices] parameter usage
 95871        sb.AppendLine();
 95872        sb.AppendLine("## Mode");
 95873        sb.AppendLine();
 95874        sb.AppendLine("**Source Generation**: Enabled (GenerateTypeRegistry detected)");
 95875        sb.AppendLine();
 95876        sb.AppendLine("## Configuration");
 95877        sb.AppendLine();
 95878        sb.AppendLine("Analyzer severity can be configured via `.editorconfig`:");
 95879        sb.AppendLine();
 95880        sb.AppendLine("```ini");
 95881        sb.AppendLine("# Example: Suppress Lazy resolution warnings");
 95882        sb.AppendLine("dotnet_diagnostic.NDLRCOR009.severity = none");
 95883        sb.AppendLine();
 95884        sb.AppendLine("# Example: Promote to warning");
 95885        sb.AppendLine("dotnet_diagnostic.NDLRCOR009.severity = warning");
 95886        sb.AppendLine("```");
 95887        sb.AppendLine();
 888
 95889        return sb.ToString();
 890    }
 891
 892    internal static IReadOnlyList<DiscoveredType> FilterTypes(IReadOnlyList<DiscoveredType> types, HashSet<string> filte
 893    {
 346894        if (IsUnfiltered(filter))
 283895            return types;
 896
 63897        return types.Where(t =>
 18213898            MatchesTypeName(filter, t.TypeName) ||
 18213899            MatchesAnyTypeName(filter, t.InterfaceNames))
 63900                    .ToList();
 901    }
 902
 903    internal static IReadOnlyList<DiscoveredPlugin> FilterPluginTypes(IReadOnlyList<DiscoveredPlugin> plugins, HashSet<s
 904    {
 244905        if (IsUnfiltered(filter))
 193906            return plugins;
 907
 51908        return plugins.Where(p =>
 112909            MatchesTypeName(filter, p.TypeName) ||
 112910            MatchesAnyTypeName(filter, p.InterfaceNames))
 51911                      .ToList();
 912    }
 913
 914    internal static IReadOnlyList<DiscoveredDecorator> FilterDecorators(IReadOnlyList<DiscoveredDecorator> decorators, H
 915    {
 244916        if (IsUnfiltered(filter))
 193917            return decorators;
 918
 51919        return decorators.Where(d =>
 11920            MatchesTypeName(filter, d.DecoratorTypeName) ||
 11921            MatchesTypeName(filter, d.ServiceTypeName))
 51922                         .ToList();
 923    }
 924
 925    internal static IReadOnlyList<DiscoveredFactory> FilterFactories(IReadOnlyList<DiscoveredFactory> factories, HashSet
 926    {
 244927        if (IsUnfiltered(filter))
 193928            return factories;
 929
 51930        return factories.Where(f =>
 12931            MatchesTypeName(filter, f.TypeName) ||
 12932            MatchesTypeName(filter, f.ReturnTypeName) ||
 12933            MatchesAnyTypeName(filter, f.InterfaceNames))
 51934                        .ToList();
 935    }
 936
 937    internal static IReadOnlyList<DiscoveredInterceptedService> FilterInterceptedServices(IReadOnlyList<DiscoveredInterc
 938    {
 244939        if (IsUnfiltered(filter))
 193940            return interceptedServices.ToList();
 941
 51942        return interceptedServices.Where(s =>
 10943            MatchesTypeName(filter, s.TypeName) ||
 10944            MatchesAnyTypeName(filter, s.InterfaceNames) ||
 10945            MatchesAnyTypeName(filter, s.AllInterceptorTypeNames))
 51946                                  .ToList();
 947    }
 948
 949    /// <summary>
 950    /// Applies the diagnostic type filter to types contributed by referenced plugin assemblies.
 951    /// Assemblies without any matching type are dropped so empty sections are not rendered.
 952    /// </summary>
 953    internal static Dictionary<string, List<DiagnosticTypeInfo>> FilterReferencedAssemblyTypes(Dictionary<string, List<D
 954    {
 346955        var filtered = new Dictionary<string, List<DiagnosticTypeInfo>>(StringComparer.Ordinal);
 800956        foreach (var kvp in referencedAssemblyTypes)
 957        {
 54958            var matching = IsUnfiltered(filter)
 54959                ? kvp.Value
 54960                : kvp.Value.Where(t =>
 9961                    MatchesTypeName(filter, t.FullName) ||
 9962                    filter.Contains(t.ShortName) ||
 9963                    MatchesAnyTypeName(filter, t.Interfaces))
 54964                    .ToList();
 965
 54966            if (matching.Count > 0)
 48967                filtered[kvp.Key] = matching;
 968        }
 969
 346970        return filtered;
 971    }
 972
 973    internal static int CalculateMaxDependencyDepth(IReadOnlyList<DiscoveredType> types)
 974    {
 120975        if (types.Count == 0) return 0;
 976
 977        // Build a lookup from interface/type name to the type that provides it
 108978        var providerLookup = new Dictionary<string, DiscoveredType>(StringComparer.Ordinal);
 91172979        foreach (var type in types)
 980        {
 45478981            providerLookup[GeneratorHelpers.GetShortTypeName(type.TypeName)] = type;
 91214982            foreach (var iface in type.InterfaceNames)
 129983                providerLookup[GeneratorHelpers.GetShortTypeName(iface)] = type;
 984        }
 985
 986        // Calculate depth for each type using memoization
 108987        var depthCache = new Dictionary<string, int>(StringComparer.Ordinal);
 108988        int maxDepth = 0;
 989
 91172990        foreach (var type in types)
 991        {
 45478992            var depth = GetDepth(type, providerLookup, depthCache, new HashSet<string>());
 45737993            if (depth > maxDepth) maxDepth = depth;
 994        }
 995
 108996        return maxDepth;
 997    }
 998
 999    internal static int GetDepth(DiscoveredType type, Dictionary<string, DiscoveredType> providerLookup, Dictionary<stri
 1000    {
 515871001        var key = GeneratorHelpers.GetShortTypeName(type.TypeName);
 1002
 515871003        if (cache.TryGetValue(key, out var cached))
 59581004            return cached;
 1005
 1006        // Cycle detection
 456291007        if (!visiting.Add(key))
 1511008            return 0;
 1009
 454781010        int maxChildDepth = 0;
 1282261011        foreach (var dep in type.ConstructorParameterTypes)
 1012        {
 186351013            var depShort = GeneratorHelpers.GetShortTypeName(dep);
 186351014            if (providerLookup.TryGetValue(depShort, out var depType))
 1015            {
 61091016                var childDepth = GetDepth(depType, providerLookup, cache, visiting);
 61091017                if (childDepth > maxChildDepth)
 52831018                    maxChildDepth = childDepth;
 1019            }
 1020        }
 1021
 454781022        visiting.Remove(key);
 454781023        var result = maxChildDepth + 1;
 454781024        cache[key] = result;
 454781025        return result;
 1026    }
 1027
 1028    internal static List<(string TypeName, int DependentCount)> CalculateHubServices(IReadOnlyList<DiscoveredType> types
 1029    {
 1030        // Count how many times each type/interface appears as a dependency
 1141031        var dependentCounts = new Dictionary<string, int>(StringComparer.Ordinal);
 1032
 911841033        foreach (var type in types)
 1034        {
 1282261035            foreach (var dep in type.ConstructorParameterTypes)
 1036            {
 186351037                var depShort = GeneratorHelpers.GetShortTypeName(dep);
 186351038                if (!dependentCounts.ContainsKey(depShort))
 70741039                    dependentCounts[depShort] = 0;
 186351040                dependentCounts[depShort]++;
 1041            }
 1042        }
 1043
 1044        // Find types that are depended upon by minDependents or more services
 1141045        var hubs = new List<(string TypeName, int DependentCount)>();
 911841046        foreach (var type in types)
 1047        {
 454781048            var shortName = GeneratorHelpers.GetShortTypeName(type.TypeName);
 454781049            var count = 0;
 1050
 1051            // Check if this type's name or any of its interfaces is depended upon
 454781052            if (dependentCounts.TryGetValue(shortName, out var c1))
 21861053                count += c1;
 1054
 912141055            foreach (var iface in type.InterfaceNames)
 1056            {
 1291057                if (dependentCounts.TryGetValue(GeneratorHelpers.GetShortTypeName(iface), out var c2))
 121058                    count += c2;
 1059            }
 1060
 454781061            if (count >= minDependents)
 7521062                hubs.Add((type.TypeName, count));
 1063        }
 1064
 8651065        return hubs.OrderByDescending(h => h.DependentCount).ToList();
 1066    }
 1067
 1068    internal static string GenerateOptionsSummaryMarkdown(DiscoveryResult discovery, string assemblyName, string timesta
 1069    {
 1111070        var sb = new StringBuilder();
 1111071        var options = FilterOptions(discovery.Options, typeFilter);
 1072
 1111073        sb.AppendLine("# Needlr Options Summary");
 1111074        sb.AppendLine();
 1111075        sb.AppendLine($"Generated: {timestamp} UTC");
 1111076        sb.AppendLine($"Assembly: {assemblyName}");
 1111077        sb.AppendLine();
 1078
 1111079        if (options.Count == 0)
 1080        {
 871081            sb.AppendLine("*No options classes discovered. Add `[Options]` attribute to configuration classes.*");
 871082            sb.AppendLine();
 871083            return sb.ToString();
 1084        }
 1085
 241086        sb.AppendLine($"## Overview");
 241087        sb.AppendLine();
 241088        sb.AppendLine($"| Metric | Count |");
 241089        sb.AppendLine($"|:-------|------:|");
 241090        sb.AppendLine($"| Total Options Classes | {options.Count} |");
 531091        sb.AppendLine($"| Named Options | {options.Count(o => o.IsNamed)} |");
 531092        sb.AppendLine($"| With Validation | {options.Count(o => o.ValidateOnStart)} |");
 531093        sb.AppendLine($"| With External Validator | {options.Count(o => o.HasExternalValidator)} |");
 241094        sb.AppendLine();
 1095
 1096        // Options table
 241097        sb.AppendLine("## Options Classes");
 241098        sb.AppendLine();
 241099        sb.AppendLine("| Class | Section | Name | ValidateOnStart | Validator |");
 241100        sb.AppendLine("|:------|:--------|:-----|:---------------:|:----------|");
 1101
 1641102        foreach (var opt in options.OrderBy(o => o.SectionName).ThenBy(o => o.TypeName))
 1103        {
 291104            var shortName = GeneratorHelpers.GetShortTypeName(opt.TypeName);
 291105            var namedLabel = opt.IsNamed ? opt.Name : "-";
 291106            var validateIcon = opt.ValidateOnStart ? "✅" : "❌";
 291107            var validatorInfo = GetValidatorDescription(opt);
 1108
 291109            sb.AppendLine($"| `{shortName}` | `{opt.SectionName}` | {namedLabel} | {validateIcon} | {validatorInfo} |");
 1110        }
 1111
 241112        sb.AppendLine();
 1113
 1114        // Detailed breakdown by section
 821115        var sectionGroups = options.GroupBy(o => o.SectionName).OrderBy(g => g.Key);
 1116
 241117        sb.AppendLine("## Configuration Sections");
 241118        sb.AppendLine();
 1119
 1061120        foreach (var group in sectionGroups)
 1121        {
 291122            sb.AppendLine($"### `{group.Key}`");
 291123            sb.AppendLine();
 1124
 1451125            foreach (var opt in group.OrderBy(o => o.TypeName))
 1126            {
 291127                var shortName = GeneratorHelpers.GetShortTypeName(opt.TypeName);
 291128                sb.AppendLine($"**{shortName}**");
 291129                sb.AppendLine();
 291130                sb.AppendLine($"- Configuration Path: `{group.Key}`");
 1131
 291132                if (opt.IsNamed)
 91133                    sb.AppendLine($"- Named Options: `{opt.Name}`");
 1134
 291135                sb.AppendLine($"- Validate On Start: {(opt.ValidateOnStart ? "Yes" : "No")}");
 1136
 291137                if (opt.HasExternalValidator)
 1138                {
 31139                    sb.AppendLine($"- External Validator: `{GeneratorHelpers.GetShortTypeName(opt.ValidatorTypeName!)}`"
 1140                }
 1141
 291142                if (opt.HasValidatorMethod)
 1143                {
 41144                    var methodType = opt.ValidatorMethod!.Value.IsStatic ? "static" : "instance";
 41145                    sb.AppendLine($"- Validation Method: `{opt.ValidatorMethod.Value.MethodName}()` ({methodType})");
 1146                }
 251147                else if (opt.ValidateMethodOverride != null)
 1148                {
 11149                    sb.AppendLine($"- Validation Method: `{opt.ValidateMethodOverride}()` (specified but not found)");
 1150                }
 1151
 291152                if (opt.SourceFilePath != null)
 1153                {
 1154                    // Use just the filename for brevity in markdown
 131155                    var fileName = System.IO.Path.GetFileName(opt.SourceFilePath);
 131156                    sb.AppendLine($"- Source: `{fileName}`");
 1157                }
 1158
 291159                sb.AppendLine();
 1160            }
 1161        }
 1162
 1163        // Validation warnings
 531164        var missingValidation = options.Where(o => !o.ValidateOnStart && (o.HasValidatorMethod || o.HasExternalValidator
 241165        if (missingValidation.Count > 0)
 1166        {
 21167            sb.AppendLine("## ⚠️ Potential Issues");
 21168            sb.AppendLine();
 21169            sb.AppendLine("The following options have validators configured but `ValidateOnStart` is disabled:");
 21170            sb.AppendLine();
 1171
 81172            foreach (var opt in missingValidation)
 1173            {
 21174                var shortName = GeneratorHelpers.GetShortTypeName(opt.TypeName);
 21175                sb.AppendLine($"- `{shortName}`: Has validator but won't run at startup");
 1176            }
 1177
 21178            sb.AppendLine();
 21179            sb.AppendLine("*Set `ValidateOnStart = true` in the `[Options]` attribute to enable startup validation.*");
 21180            sb.AppendLine();
 1181        }
 1182
 1183        // Usage example
 241184        sb.AppendLine("## Usage");
 241185        sb.AppendLine();
 241186        sb.AppendLine("Options are automatically registered via source generation. Access them via:");
 241187        sb.AppendLine();
 241188        sb.AppendLine("```csharp");
 241189        sb.AppendLine("// Constructor injection");
 241190        sb.AppendLine("public MyService(IOptions<DatabaseOptions> options) { }");
 241191        sb.AppendLine();
 241192        sb.AppendLine("// Named options");
 241193        sb.AppendLine("public MyService(IOptionsSnapshot<ConnectionOptions> options)");
 241194        sb.AppendLine("{");
 241195        sb.AppendLine("    var primary = options.Get(\"Primary\");");
 241196        sb.AppendLine("}");
 241197        sb.AppendLine("```");
 241198        sb.AppendLine();
 1199
 241200        return sb.ToString();
 1201    }
 1202
 1203    private static string GetValidatorDescription(DiscoveredOptions opt)
 1204    {
 291205        if (opt.HasExternalValidator)
 1206        {
 31207            var validatorShort = GeneratorHelpers.GetShortTypeName(opt.ValidatorTypeName!);
 31208            return $"`{validatorShort}`";
 1209        }
 1210
 261211        if (opt.HasValidatorMethod)
 1212        {
 41213            var methodType = opt.ValidatorMethod!.Value.IsStatic ? "static" : "self";
 41214            return $"`{opt.ValidatorMethod.Value.MethodName}()` ({methodType})";
 1215        }
 1216
 221217        if (opt.ValidateMethodOverride != null)
 1218        {
 11219            return $"`{opt.ValidateMethodOverride}()` (not found)";
 1220        }
 1221
 211222        return "-";
 1223    }
 1224
 1225    internal static IReadOnlyList<DiscoveredOptions> FilterOptions(IReadOnlyList<DiscoveredOptions> options, HashSet<str
 1226    {
 1111227        if (IsUnfiltered(filter))
 961228            return options;
 1229
 151230        return options.Where(o =>
 91231            MatchesTypeName(filter, o.TypeName) ||
 91232            filter.Contains(o.SectionName) ||
 91233            (o.IsNamed && filter.Contains(o.Name!)))
 151234                      .ToList();
 1235    }
 1236
 1237    private static bool IsUnfiltered(HashSet<string> filter)
 1238    {
 14871239        return filter == null || filter.Count == 0;
 1240    }
 1241
 1242    /// <summary>
 1243    /// Determines whether a fully qualified type name matches the filter by any of its
 1244    /// supported identities: the fully qualified name, the namespace-qualified name, or the short name.
 1245    /// </summary>
 1246    private static bool MatchesTypeName(HashSet<string> filter, string typeName)
 1247    {
 185331248        return filter.Contains(typeName) ||
 185331249               filter.Contains(GeneratorHelpers.GetShortTypeName(typeName)) ||
 185331250               filter.Contains(GeneratorHelpers.StripGlobalPrefix(typeName));
 1251    }
 1252
 1253    private static bool MatchesAnyTypeName(HashSet<string> filter, IEnumerable<string> typeNames)
 1254    {
 184181255        return typeNames.Any(typeName => MatchesTypeName(filter, typeName));
 1256    }
 1257}
 1258

Methods/Properties

GenerateDiagnosticsSource(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,NexusLabs.Needlr.Generators.DiagnosticOptions,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>)
GenerateDependencyGraphMarkdown(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>)
GenerateLifetimeSummaryMarkdown(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>)
GenerateRegistrationIndexMarkdown(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.String,System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>)
GenerateAnalyzerStatusMarkdown(System.String)
FilterTypes(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>,System.Collections.Generic.HashSet`1<System.String>)
FilterPluginTypes(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredPlugin>,System.Collections.Generic.HashSet`1<System.String>)
FilterDecorators(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredDecorator>,System.Collections.Generic.HashSet`1<System.String>)
FilterFactories(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredFactory>,System.Collections.Generic.HashSet`1<System.String>)
FilterInterceptedServices(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredInterceptedService>,System.Collections.Generic.HashSet`1<System.String>)
FilterReferencedAssemblyTypes(System.Collections.Generic.Dictionary`2<System.String,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiagnosticTypeInfo>>,System.Collections.Generic.HashSet`1<System.String>)
CalculateMaxDependencyDepth(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>)
GetDepth(NexusLabs.Needlr.Generators.Models.DiscoveredType,System.Collections.Generic.Dictionary`2<System.String,NexusLabs.Needlr.Generators.Models.DiscoveredType>,System.Collections.Generic.Dictionary`2<System.String,System.Int32>,System.Collections.Generic.HashSet`1<System.String>)
CalculateHubServices(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>,System.Int32)
GenerateOptionsSummaryMarkdown(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.Collections.Generic.HashSet`1<System.String>)
GetValidatorDescription(NexusLabs.Needlr.Generators.Models.DiscoveredOptions)
FilterOptions(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredOptions>,System.Collections.Generic.HashSet`1<System.String>)
IsUnfiltered(System.Collections.Generic.HashSet`1<System.String>)
MatchesTypeName(System.Collections.Generic.HashSet`1<System.String>,System.String)
MatchesAnyTypeName(System.Collections.Generic.HashSet`1<System.String>,System.Collections.Generic.IEnumerable`1<System.String>)