< Summary

Information
Class: NexusLabs.Needlr.Generators.Export.GraphExporter
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/Export/GraphExporter.cs
Line coverage
99%
Covered lines: 328
Uncovered lines: 2
Coverable lines: 330
Total lines: 620
Line coverage: 99.3%
Branch coverage
97%
Covered branches: 115
Total branches: 118
Branch coverage: 97.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
GenerateGraphJson(...)100%11100%
BuildGraph(...)100%1818100%
BuildTypeLookup(...)100%1818100%
MapToGraphService(...)100%2222100%
ComputeStatistics(...)100%44100%
GetSimpleTypeName(...)80%101093.75%
SimplifyGenericParameters(...)93.75%161695.23%
SerializeToJson(...)100%44100%
SerializeService(...)100%1818100%
SerializeDiagnostic(...)100%44100%
Escape(...)100%22100%
NullableString(...)100%22100%
GenerateGraphExportSource(...)100%11100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/Export/GraphExporter.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Text;
 5using NexusLabs.Needlr.Generators.Models;
 6
 7namespace NexusLabs.Needlr.Generators.Export;
 8
 9/// <summary>
 10/// Generates the Needlr dependency graph JSON for IDE tooling.
 11/// </summary>
 12internal static class GraphExporter
 13{
 14    /// <summary>
 15    /// Generates the needlr-graph.json content from the discovery result.
 16    /// </summary>
 17    public static string GenerateGraphJson(
 18        DiscoveryResult discoveryResult,
 19        string assemblyName,
 20        string? projectPath,
 21        IReadOnlyList<CollectedDiagnostic>? diagnostics = null,
 22        IReadOnlyDictionary<string, IReadOnlyList<DiscoveredType>>? referencedAssemblyTypes = null)
 23    {
 1924        var graph = BuildGraph(discoveryResult, assemblyName, projectPath, diagnostics, referencedAssemblyTypes);
 1925        return SerializeToJson(graph);
 26    }
 27
 28    private static NeedlrGraph BuildGraph(
 29        DiscoveryResult discoveryResult,
 30        string assemblyName,
 31        string? projectPath,
 32        IReadOnlyList<CollectedDiagnostic>? diagnostics,
 33        IReadOnlyDictionary<string, IReadOnlyList<DiscoveredType>>? referencedAssemblyTypes)
 34    {
 1935        var graph = new NeedlrGraph
 1936        {
 1937            SchemaVersion = "1.0",
 1938            GeneratedAt = DateTime.UtcNow.ToString("O"),
 1939            ProjectPath = projectPath,
 1940            AssemblyName = assemblyName
 1941        };
 42
 43        // Build type lookup for resolving dependencies (include referenced assembly types)
 1944        var typeLookup = BuildTypeLookup(discoveryResult, referencedAssemblyTypes);
 45
 46        // Map injectable types from current assembly to graph services
 612847        foreach (var type in discoveryResult.InjectableTypes)
 48        {
 304549            var service = MapToGraphService(type, assemblyName, typeLookup, discoveryResult);
 304550            graph.Services.Add(service);
 51        }
 52
 53        // Add types from referenced assemblies with [GenerateTypeRegistry]
 1954        if (referencedAssemblyTypes != null)
 55        {
 2856            foreach (var kvp in referencedAssemblyTypes)
 57            {
 558                var refAssemblyName = kvp.Key;
 559                var types = kvp.Value;
 2260                foreach (var type in types)
 61                {
 662                    var service = MapToGraphService(type, refAssemblyName, typeLookup, discoveryResult);
 663                    graph.Services.Add(service);
 64                }
 65            }
 66        }
 67
 68        // Add diagnostics if provided
 1969        if (diagnostics != null)
 70        {
 2071            foreach (var diag in diagnostics)
 72            {
 673                graph.Diagnostics.Add(new GraphDiagnostic
 674                {
 675                    Id = diag.Id,
 676                    Severity = diag.Severity,
 677                    Message = diag.Message,
 678                    Location = diag.FilePath != null ? new GraphLocation
 679                    {
 680                        FilePath = diag.FilePath,
 681                        Line = diag.Line,
 682                        Column = 0
 683                    } : null,
 684                    RelatedServices = diag.RelatedServices?.ToList() ?? new List<string>()
 685                });
 86            }
 87        }
 88
 89        // Compute statistics (include referenced assembly types in count)
 1990        graph.Statistics = ComputeStatistics(discoveryResult, referencedAssemblyTypes);
 91
 1992        return graph;
 93    }
 94
 95    private static Dictionary<string, DiscoveredType> BuildTypeLookup(
 96        DiscoveryResult discoveryResult,
 97        IReadOnlyDictionary<string, IReadOnlyList<DiscoveredType>>? referencedAssemblyTypes)
 98    {
 1999        var lookup = new Dictionary<string, DiscoveredType>();
 100
 101        // Add types from current assembly
 6128102        foreach (var type in discoveryResult.InjectableTypes)
 103        {
 3045104            lookup[type.TypeName] = type;
 6126105            foreach (var iface in type.InterfaceNames)
 106            {
 18107                if (!lookup.ContainsKey(iface))
 108                {
 17109                    lookup[iface] = type;
 110                }
 111            }
 112        }
 113
 114        // Add types from referenced assemblies for dependency resolution
 19115        if (referencedAssemblyTypes != null)
 116        {
 28117            foreach (var kvp in referencedAssemblyTypes)
 118            {
 22119                foreach (var type in kvp.Value)
 120                {
 6121                    if (!lookup.ContainsKey(type.TypeName))
 122                    {
 5123                        lookup[type.TypeName] = type;
 124                    }
 20125                    foreach (var iface in type.InterfaceNames)
 126                    {
 4127                        if (!lookup.ContainsKey(iface))
 128                        {
 3129                            lookup[iface] = type;
 130                        }
 131                    }
 132                }
 133            }
 134        }
 135
 19136        return lookup;
 137    }
 138
 139    private static GraphService MapToGraphService(
 140        DiscoveredType type,
 141        string assemblyName,
 142        Dictionary<string, DiscoveredType> typeLookup,
 143        DiscoveryResult discoveryResult)
 144    {
 3051145        var service = new GraphService
 3051146        {
 3051147            Id = type.TypeName,
 3051148            TypeName = GetSimpleTypeName(type.TypeName),
 3051149            FullTypeName = type.TypeName,
 3051150            AssemblyName = assemblyName,
 3051151            Lifetime = type.Lifetime.ToString(),
 3051152            Location = type.SourceFilePath != null ? new GraphLocation
 3051153            {
 3051154                FilePath = type.SourceFilePath,
 3051155                Line = type.SourceLine,
 3051156                Column = 0
 3051157            } : null,
 3051158            ServiceKeys = type.ServiceKeys.ToList(),
 3051159            Metadata = new GraphServiceMetadata
 3051160            {
 3051161                IsDisposable = type.IsDisposable,
 2162                HasFactory = discoveryResult.Factories.Any(f => f.TypeName == type.TypeName),
 2163                HasOptions = discoveryResult.Options.Any(o => o.TypeName == type.TypeName),
 2164                IsHostedService = discoveryResult.HostedServices.Any(h => h.TypeName == type.TypeName),
 13904165                IsPlugin = discoveryResult.PluginTypes.Any(p => p.TypeName == type.TypeName)
 3051166            }
 3051167        };
 168
 169        // Map interfaces with locations
 6128170        foreach (var ifaceInfo in type.InterfaceInfos)
 171        {
 13172            service.Interfaces.Add(new GraphInterface
 13173            {
 13174                Name = GetSimpleTypeName(ifaceInfo.FullName),
 13175                FullName = ifaceInfo.FullName,
 13176                Location = ifaceInfo.HasLocation ? new GraphLocation
 13177                {
 13178                    FilePath = ifaceInfo.SourceFilePath!,
 13179                    Line = ifaceInfo.SourceLine,
 13180                    Column = 0
 13181                } : null
 13182            });
 183        }
 184        // Fall back to InterfaceNames if no InterfaceInfos (for backwards compat)
 3051185        if (type.InterfaceInfos.Length == 0)
 186        {
 6094187            foreach (var iface in type.InterfaceNames)
 188            {
 9189                service.Interfaces.Add(new GraphInterface
 9190                {
 9191                    Name = GetSimpleTypeName(iface),
 9192                    FullName = iface
 9193                });
 194            }
 195        }
 196
 197        // Map dependencies from constructor parameters
 8616198        foreach (var param in type.ConstructorParameters)
 199        {
 1257200            var dependency = new GraphDependency
 1257201            {
 1257202                ParameterName = param.ParameterName ?? string.Empty,
 1257203                TypeName = GetSimpleTypeName(param.TypeName),
 1257204                FullTypeName = param.TypeName,
 1257205                IsKeyed = param.IsKeyed,
 1257206                ServiceKey = param.ServiceKey
 1257207            };
 208
 209            // Try to resolve the dependency
 1257210            if (typeLookup.TryGetValue(param.TypeName, out var resolved))
 211            {
 415212                dependency.ResolvedTo = resolved.TypeName;
 415213                dependency.ResolvedLifetime = resolved.Lifetime.ToString();
 214            }
 215
 1257216            service.Dependencies.Add(dependency);
 217        }
 218
 219        // Map decorators
 3051220        var decorators = discoveryResult.Decorators
 609221            .Where(d => type.InterfaceNames.Contains(d.ServiceTypeName))
 3054222            .OrderBy(d => d.Order);
 223
 6108224        foreach (var decorator in decorators)
 225        {
 3226            service.Decorators.Add(new GraphDecorator
 3227            {
 3228                TypeName = decorator.DecoratorTypeName,
 3229                Order = decorator.Order
 3230            });
 231        }
 232
 233        // Map interceptors
 3051234        var intercepted = discoveryResult.InterceptedServices
 3052235            .FirstOrDefault(i => i.TypeName == type.TypeName);
 236
 3051237        if (intercepted.TypeName != null)
 238        {
 1239            service.Interceptors = intercepted.AllInterceptorTypeNames.ToList();
 240        }
 241
 242        // Collect attributes
 3051243        service.Attributes.Add(type.Lifetime.ToString());
 3051244        if (type.IsKeyed)
 245        {
 2246            service.Attributes.Add("Keyed");
 247        }
 248
 3051249        return service;
 250    }
 251
 252    private static GraphStatistics ComputeStatistics(
 253        DiscoveryResult discoveryResult,
 254        IReadOnlyDictionary<string, IReadOnlyList<DiscoveredType>>? referencedAssemblyTypes)
 255    {
 256        // Get all types for statistics - current assembly + referenced assemblies
 19257        var allTypes = new List<DiscoveredType>(discoveryResult.InjectableTypes);
 19258        if (referencedAssemblyTypes != null)
 259        {
 28260            foreach (var kvp in referencedAssemblyTypes)
 261            {
 5262                allTypes.AddRange(kvp.Value);
 263            }
 264        }
 265
 19266        return new GraphStatistics
 19267        {
 19268            TotalServices = allTypes.Count,
 3051269            Singletons = allTypes.Count(t => t.Lifetime == GeneratorLifetime.Singleton),
 3051270            Scoped = allTypes.Count(t => t.Lifetime == GeneratorLifetime.Scoped),
 3051271            Transient = allTypes.Count(t => t.Lifetime == GeneratorLifetime.Transient),
 19272            Decorators = discoveryResult.Decorators.Count,
 19273            Interceptors = discoveryResult.InterceptedServices.Count,
 19274            Factories = discoveryResult.Factories.Count,
 19275            Options = discoveryResult.Options.Count,
 19276            HostedServices = discoveryResult.HostedServices.Count,
 19277            Plugins = discoveryResult.PluginTypes.Count
 19278        };
 279    }
 280
 281    private static string GetSimpleTypeName(string fullTypeName)
 282    {
 283        // Remove global:: prefix
 4449284        var name = fullTypeName;
 4449285        if (name.StartsWith("global::"))
 286        {
 4284287            name = name.Substring(8);
 288        }
 289
 290        // Handle generic types like Lazy<T> or IReadOnlyList<Assembly>
 291        // We want to preserve the generic structure but simplify inner types
 4449292        var genericStart = name.IndexOf('<');
 4449293        if (genericStart >= 0)
 294        {
 295            // Get the outer type name (before generic params)
 98296            var outerPart = name.Substring(0, genericStart);
 98297            var lastDot = outerPart.LastIndexOf('.');
 98298            var simpleOuter = lastDot >= 0 ? outerPart.Substring(lastDot + 1) : outerPart;
 299
 300            // Get the generic parameters and simplify them recursively
 98301            var genericEnd = name.LastIndexOf('>');
 98302            if (genericEnd > genericStart)
 303            {
 98304                var genericParams = name.Substring(genericStart + 1, genericEnd - genericStart - 1);
 305                // Simplify each generic parameter (split by comma, handle nested generics)
 98306                var simplifiedParams = SimplifyGenericParameters(genericParams);
 98307                return $"{simpleOuter}<{simplifiedParams}>";
 308            }
 309
 0310            return simpleOuter;
 311        }
 312
 313        // Get just the type name (after last dot)
 4351314        var idx = name.LastIndexOf('.');
 4351315        return idx >= 0 ? name.Substring(idx + 1) : name;
 316    }
 317
 318    private static string SimplifyGenericParameters(string genericParams)
 319    {
 320        // Handle nested generics by tracking depth
 98321        var result = new StringBuilder();
 98322        var depth = 0;
 98323        var currentParam = new StringBuilder();
 324
 6334325        foreach (var c in genericParams)
 326        {
 3069327            if (c == '<')
 328            {
 16329                depth++;
 16330                currentParam.Append(c);
 331            }
 3053332            else if (c == '>')
 333            {
 16334                depth--;
 16335                currentParam.Append(c);
 336            }
 3037337            else if (c == ',' && depth == 0)
 338            {
 339                // End of parameter at top level
 21340                if (result.Length > 0)
 341                {
 0342                    result.Append(", ");
 343                }
 21344                result.Append(GetSimpleTypeName(currentParam.ToString().Trim()));
 21345                currentParam.Clear();
 346            }
 347            else
 348            {
 3016349                currentParam.Append(c);
 350            }
 351        }
 352
 353        // Add last parameter
 98354        if (currentParam.Length > 0)
 355        {
 98356            if (result.Length > 0)
 357            {
 21358                result.Append(", ");
 359            }
 98360            result.Append(GetSimpleTypeName(currentParam.ToString().Trim()));
 361        }
 362
 98363        return result.ToString();
 364    }
 365
 366    /// <summary>
 367    /// Serializes the graph to JSON without using System.Text.Json (not available in all targets).
 368    /// Uses simple string building for source generator compatibility.
 369    /// </summary>
 370    private static string SerializeToJson(NeedlrGraph graph)
 371    {
 19372        var sb = new StringBuilder();
 19373        sb.AppendLine("{");
 19374        sb.AppendLine($"  \"schemaVersion\": \"{Escape(graph.SchemaVersion)}\",");
 19375        sb.AppendLine($"  \"generatedAt\": \"{Escape(graph.GeneratedAt)}\",");
 19376        sb.AppendLine($"  \"projectPath\": {NullableString(graph.ProjectPath)},");
 19377        sb.AppendLine($"  \"assemblyName\": {NullableString(graph.AssemblyName)},");
 378
 379        // Services array
 19380        sb.AppendLine("  \"services\": [");
 6140381        for (int i = 0; i < graph.Services.Count; i++)
 382        {
 3051383            SerializeService(sb, graph.Services[i], i == graph.Services.Count - 1);
 384        }
 19385        sb.AppendLine("  ],");
 386
 387        // Diagnostics array
 19388        sb.AppendLine("  \"diagnostics\": [");
 50389        for (int i = 0; i < graph.Diagnostics.Count; i++)
 390        {
 6391            SerializeDiagnostic(sb, graph.Diagnostics[i], i == graph.Diagnostics.Count - 1);
 392        }
 19393        sb.AppendLine("  ],");
 394
 395        // Statistics object
 19396        sb.AppendLine("  \"statistics\": {");
 19397        sb.AppendLine($"    \"totalServices\": {graph.Statistics.TotalServices},");
 19398        sb.AppendLine($"    \"singletons\": {graph.Statistics.Singletons},");
 19399        sb.AppendLine($"    \"scoped\": {graph.Statistics.Scoped},");
 19400        sb.AppendLine($"    \"transient\": {graph.Statistics.Transient},");
 19401        sb.AppendLine($"    \"decorators\": {graph.Statistics.Decorators},");
 19402        sb.AppendLine($"    \"interceptors\": {graph.Statistics.Interceptors},");
 19403        sb.AppendLine($"    \"factories\": {graph.Statistics.Factories},");
 19404        sb.AppendLine($"    \"options\": {graph.Statistics.Options},");
 19405        sb.AppendLine($"    \"hostedServices\": {graph.Statistics.HostedServices},");
 19406        sb.AppendLine($"    \"plugins\": {graph.Statistics.Plugins}");
 19407        sb.AppendLine("  }");
 408
 19409        sb.AppendLine("}");
 19410        return sb.ToString();
 411    }
 412
 413    private static void SerializeService(StringBuilder sb, GraphService service, bool isLast)
 414    {
 3051415        sb.AppendLine("    {");
 3051416        sb.AppendLine($"      \"id\": \"{Escape(service.Id)}\",");
 3051417        sb.AppendLine($"      \"typeName\": \"{Escape(service.TypeName)}\",");
 3051418        sb.AppendLine($"      \"fullTypeName\": \"{Escape(service.FullTypeName)}\",");
 3051419        sb.AppendLine($"      \"assemblyName\": {NullableString(service.AssemblyName)},");
 420
 421        // Interfaces
 3051422        sb.AppendLine("      \"interfaces\": [");
 6146423        for (int i = 0; i < service.Interfaces.Count; i++)
 424        {
 22425            var iface = service.Interfaces[i];
 22426            var comma = i < service.Interfaces.Count - 1 ? "," : "";
 22427            sb.AppendLine("        {");
 22428            sb.AppendLine($"          \"name\": \"{Escape(iface.Name)}\",");
 22429            sb.AppendLine($"          \"fullName\": \"{Escape(iface.FullName)}\",");
 22430            if (iface.Location != null)
 431            {
 12432                sb.AppendLine("          \"location\": {");
 12433                sb.AppendLine($"            \"filePath\": {NullableString(iface.Location.FilePath)},");
 12434                sb.AppendLine($"            \"line\": {iface.Location.Line},");
 12435                sb.AppendLine($"            \"column\": {iface.Location.Column}");
 12436                sb.AppendLine("          }");
 437            }
 438            else
 439            {
 10440                sb.AppendLine("          \"location\": null");
 441            }
 22442            sb.AppendLine($"        }}{comma}");
 443        }
 3051444        sb.AppendLine("      ],");
 445
 3051446        sb.AppendLine($"      \"lifetime\": \"{Escape(service.Lifetime)}\",");
 447
 448        // Location
 3051449        if (service.Location != null)
 450        {
 14451            sb.AppendLine("      \"location\": {");
 14452            sb.AppendLine($"        \"filePath\": {NullableString(service.Location.FilePath)},");
 14453            sb.AppendLine($"        \"line\": {service.Location.Line},");
 14454            sb.AppendLine($"        \"column\": {service.Location.Column}");
 14455            sb.AppendLine("      },");
 456        }
 457        else
 458        {
 3037459            sb.AppendLine("      \"location\": null,");
 460        }
 461
 462        // Dependencies
 3051463        sb.AppendLine("      \"dependencies\": [");
 8616464        for (int i = 0; i < service.Dependencies.Count; i++)
 465        {
 1257466            var dep = service.Dependencies[i];
 1257467            var comma = i < service.Dependencies.Count - 1 ? "," : "";
 1257468            sb.AppendLine("        {");
 1257469            sb.AppendLine($"          \"parameterName\": \"{Escape(dep.ParameterName)}\",");
 1257470            sb.AppendLine($"          \"typeName\": \"{Escape(dep.TypeName)}\",");
 1257471            sb.AppendLine($"          \"fullTypeName\": \"{Escape(dep.FullTypeName)}\",");
 1257472            sb.AppendLine($"          \"resolvedTo\": {NullableString(dep.ResolvedTo)},");
 1257473            sb.AppendLine($"          \"resolvedLifetime\": {NullableString(dep.ResolvedLifetime)},");
 1257474            sb.AppendLine($"          \"isKeyed\": {dep.IsKeyed.ToString().ToLowerInvariant()},");
 1257475            sb.AppendLine($"          \"serviceKey\": {NullableString(dep.ServiceKey)}");
 1257476            sb.AppendLine($"        }}{comma}");
 477        }
 3051478        sb.AppendLine("      ],");
 479
 480        // Decorators
 3051481        sb.AppendLine("      \"decorators\": [");
 6108482        for (int i = 0; i < service.Decorators.Count; i++)
 483        {
 3484            var dec = service.Decorators[i];
 3485            var comma = i < service.Decorators.Count - 1 ? "," : "";
 3486            sb.AppendLine($"        {{ \"typeName\": \"{Escape(dec.TypeName)}\", \"order\": {dec.Order} }}{comma}");
 487        }
 3051488        sb.AppendLine("      ],");
 489
 490        // Interceptors
 3051491        sb.Append("      \"interceptors\": [");
 3053492        sb.Append(string.Join(", ", service.Interceptors.Select(i => $"\"{Escape(i)}\"")));
 3051493        sb.AppendLine("],");
 494
 495        // Attributes
 3051496        sb.Append("      \"attributes\": [");
 6104497        sb.Append(string.Join(", ", service.Attributes.Select(a => $"\"{Escape(a)}\"")));
 3051498        sb.AppendLine("],");
 499
 500        // Service keys
 3051501        sb.Append("      \"serviceKeys\": [");
 3054502        sb.Append(string.Join(", ", service.ServiceKeys.Select(k => $"\"{Escape(k)}\"")));
 3051503        sb.AppendLine("],");
 504
 505        // Metadata
 3051506        sb.AppendLine("      \"metadata\": {");
 3051507        sb.AppendLine($"        \"hasFactory\": {service.Metadata.HasFactory.ToString().ToLowerInvariant()},");
 3051508        sb.AppendLine($"        \"hasOptions\": {service.Metadata.HasOptions.ToString().ToLowerInvariant()},");
 3051509        sb.AppendLine($"        \"isHostedService\": {service.Metadata.IsHostedService.ToString().ToLowerInvariant()},")
 3051510        sb.AppendLine($"        \"isDisposable\": {service.Metadata.IsDisposable.ToString().ToLowerInvariant()},");
 3051511        sb.AppendLine($"        \"isPlugin\": {service.Metadata.IsPlugin.ToString().ToLowerInvariant()}");
 3051512        sb.AppendLine("      }");
 513
 3051514        sb.AppendLine(isLast ? "    }" : "    },");
 3051515    }
 516
 517    private static void SerializeDiagnostic(StringBuilder sb, GraphDiagnostic diagnostic, bool isLast)
 518    {
 6519        sb.AppendLine("    {");
 6520        sb.AppendLine($"      \"id\": \"{Escape(diagnostic.Id)}\",");
 6521        sb.AppendLine($"      \"severity\": \"{Escape(diagnostic.Severity)}\",");
 6522        sb.AppendLine($"      \"message\": \"{Escape(diagnostic.Message)}\",");
 523
 6524        if (diagnostic.Location != null)
 525        {
 5526            sb.AppendLine("      \"location\": {");
 5527            sb.AppendLine($"        \"filePath\": {NullableString(diagnostic.Location.FilePath)},");
 5528            sb.AppendLine($"        \"line\": {diagnostic.Location.Line},");
 5529            sb.AppendLine($"        \"column\": {diagnostic.Location.Column}");
 5530            sb.AppendLine("      },");
 531        }
 532        else
 533        {
 1534            sb.AppendLine("      \"location\": null,");
 535        }
 536
 6537        sb.Append("      \"relatedServices\": [");
 11538        sb.Append(string.Join(", ", diagnostic.RelatedServices.Select(s => $"\"{Escape(s)}\"")));
 6539        sb.AppendLine("]");
 540
 6541        sb.AppendLine(isLast ? "    }" : "    },");
 6542    }
 543
 544    private static string Escape(string value)
 545    {
 23076546        if (string.IsNullOrEmpty(value))
 1263547            return value;
 548
 21813549        return value
 21813550            .Replace("\\", "\\\\")
 21813551            .Replace("\"", "\\\"")
 21813552            .Replace("\n", "\\n")
 21813553            .Replace("\r", "\\r")
 21813554            .Replace("\t", "\\t");
 555    }
 556
 557    private static string NullableString(string? value)
 558    {
 6891559        return value == null ? "null" : $"\"{Escape(value)}\"";
 560    }
 561
 562    /// <summary>
 563    /// Generates the NeedlrGraph.g.cs source file that embeds the graph JSON
 564    /// in a generated class for IDE tooling.
 565    /// </summary>
 566    internal static string GenerateGraphExportSource(string graphJson, string assemblyName, BreadcrumbWriter breadcrumbs
 567    {
 5568        var sb = new StringBuilder();
 569
 5570        breadcrumbs.WriteFileHeader(sb, assemblyName, "Needlr IDE Graph Export");
 571
 5572        sb.AppendLine("using System;");
 5573        sb.AppendLine("using System.IO;");
 5574        sb.AppendLine();
 5575        sb.AppendLine($"namespace {assemblyName}.Generated");
 5576        sb.AppendLine("{");
 5577        sb.AppendLine("    /// <summary>");
 5578        sb.AppendLine("    /// Provides the Needlr dependency graph for IDE tooling.");
 5579        sb.AppendLine("    /// </summary>");
 5580        sb.AppendLine("    internal static class NeedlrGraphExport");
 5581        sb.AppendLine("    {");
 5582        sb.AppendLine("        /// <summary>");
 5583        sb.AppendLine("        /// Gets the dependency graph JSON.");
 5584        sb.AppendLine("        /// </summary>");
 5585        sb.AppendLine("        public static string GraphJson => GraphJsonContent;");
 5586        sb.AppendLine();
 5587        sb.AppendLine("        private const string GraphJsonContent = @\"");
 588
 589        // Escape the JSON for C# verbatim string (double quotes only)
 5590        var escapedJson = graphJson.Replace("\"", "\"\"");
 5591        sb.Append(escapedJson);
 592
 5593        sb.AppendLine("\";");
 5594        sb.AppendLine();
 5595        sb.AppendLine("        /// <summary>");
 5596        sb.AppendLine("        /// Writes the graph to the specified path.");
 5597        sb.AppendLine("        /// </summary>");
 5598        sb.AppendLine("        public static void WriteGraphToFile(string path)");
 5599        sb.AppendLine("        {");
 5600        sb.AppendLine("            File.WriteAllText(path, GraphJson);");
 5601        sb.AppendLine("        }");
 5602        sb.AppendLine("    }");
 5603        sb.AppendLine("}");
 604
 5605        return sb.ToString();
 606    }
 607}
 608
 609/// <summary>
 610/// Represents a diagnostic collected during generation for inclusion in the graph.
 611/// </summary>
 612internal sealed class CollectedDiagnostic
 613{
 614    public string Id { get; set; } = string.Empty;
 615    public string Severity { get; set; } = string.Empty;
 616    public string Message { get; set; } = string.Empty;
 617    public string? FilePath { get; set; }
 618    public int Line { get; set; }
 619    public IReadOnlyList<string>? RelatedServices { get; set; }
 620}

Methods/Properties

GenerateGraphJson(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Export.CollectedDiagnostic>,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>>)
BuildGraph(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.String,System.String,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Export.CollectedDiagnostic>,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>>)
BuildTypeLookup(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>>)
MapToGraphService(NexusLabs.Needlr.Generators.Models.DiscoveredType,System.String,System.Collections.Generic.Dictionary`2<System.String,NexusLabs.Needlr.Generators.Models.DiscoveredType>,NexusLabs.Needlr.Generators.Models.DiscoveryResult)
ComputeStatistics(NexusLabs.Needlr.Generators.Models.DiscoveryResult,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredType>>)
GetSimpleTypeName(System.String)
SimplifyGenericParameters(System.String)
SerializeToJson(NexusLabs.Needlr.Generators.Export.NeedlrGraph)
SerializeService(System.Text.StringBuilder,NexusLabs.Needlr.Generators.Export.GraphService,System.Boolean)
SerializeDiagnostic(System.Text.StringBuilder,NexusLabs.Needlr.Generators.Export.GraphDiagnostic,System.Boolean)
Escape(System.String)
NullableString(System.String)
GenerateGraphExportSource(System.String,System.String,NexusLabs.Needlr.Generators.BreadcrumbWriter,System.String)