< 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: 329
Uncovered lines: 2
Coverable lines: 331
Total lines: 629
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    /// Placeholder emitted in place of a wall-clock timestamp so generated source stays
 16    /// byte-identical across builds. It is a schema-valid RFC 3339 value, so the embedded
 17    /// JSON still satisfies <c>needlr-graph-v1.schema.json</c>. The generated
 18    /// <c>WriteGraphToFile</c> substitutes the real UTC time when the graph is written.
 19    /// </summary>
 20    internal const string GeneratedAtSentinel = "0001-01-01T00:00:00.0000000+00:00";
 21
 22    /// <summary>
 23    /// Generates the needlr-graph.json content from the discovery result.
 24    /// </summary>
 25    public static string GenerateGraphJson(
 26        DiscoveryResult discoveryResult,
 27        string assemblyName,
 28        string? projectPath,
 29        IReadOnlyList<CollectedDiagnostic>? diagnostics = null,
 30        IReadOnlyDictionary<string, IReadOnlyList<DiscoveredType>>? referencedAssemblyTypes = null)
 31    {
 3332        var graph = BuildGraph(discoveryResult, assemblyName, projectPath, diagnostics, referencedAssemblyTypes);
 3333        return SerializeToJson(graph);
 34    }
 35
 36    private static NeedlrGraph BuildGraph(
 37        DiscoveryResult discoveryResult,
 38        string assemblyName,
 39        string? projectPath,
 40        IReadOnlyList<CollectedDiagnostic>? diagnostics,
 41        IReadOnlyDictionary<string, IReadOnlyList<DiscoveredType>>? referencedAssemblyTypes)
 42    {
 3343        var graph = new NeedlrGraph
 3344        {
 3345            SchemaVersion = "1.0",
 3346            GeneratedAt = GeneratedAtSentinel,
 3347            ProjectPath = projectPath,
 3348            AssemblyName = assemblyName
 3349        };
 50
 51        // Build type lookup for resolving dependencies (include referenced assembly types)
 3352        var typeLookup = BuildTypeLookup(discoveryResult, referencedAssemblyTypes);
 53
 54        // Map injectable types from current assembly to graph services
 2313655        foreach (var type in discoveryResult.InjectableTypes)
 56        {
 1153557            var service = MapToGraphService(type, assemblyName, typeLookup, discoveryResult);
 1153558            graph.Services.Add(service);
 59        }
 60
 61        // Add types from referenced assemblies with [GenerateTypeRegistry]
 3362        if (referencedAssemblyTypes != null)
 63        {
 5664            foreach (var kvp in referencedAssemblyTypes)
 65            {
 566                var refAssemblyName = kvp.Key;
 567                var types = kvp.Value;
 2268                foreach (var type in types)
 69                {
 670                    var service = MapToGraphService(type, refAssemblyName, typeLookup, discoveryResult);
 671                    graph.Services.Add(service);
 72                }
 73            }
 74        }
 75
 76        // Add diagnostics if provided
 3377        if (diagnostics != null)
 78        {
 2079            foreach (var diag in diagnostics)
 80            {
 681                graph.Diagnostics.Add(new GraphDiagnostic
 682                {
 683                    Id = diag.Id,
 684                    Severity = diag.Severity,
 685                    Message = diag.Message,
 686                    Location = diag.FilePath != null ? new GraphLocation
 687                    {
 688                        FilePath = diag.FilePath,
 689                        Line = diag.Line,
 690                        Column = 0
 691                    } : null,
 692                    RelatedServices = diag.RelatedServices?.ToList() ?? new List<string>()
 693                });
 94            }
 95        }
 96
 97        // Compute statistics (include referenced assembly types in count)
 3398        graph.Statistics = ComputeStatistics(discoveryResult, referencedAssemblyTypes);
 99
 33100        return graph;
 101    }
 102
 103    private static Dictionary<string, DiscoveredType> BuildTypeLookup(
 104        DiscoveryResult discoveryResult,
 105        IReadOnlyDictionary<string, IReadOnlyList<DiscoveredType>>? referencedAssemblyTypes)
 106    {
 33107        var lookup = new Dictionary<string, DiscoveredType>();
 108
 109        // Add types from current assembly
 23136110        foreach (var type in discoveryResult.InjectableTypes)
 111        {
 11535112            lookup[type.TypeName] = type;
 23134113            foreach (var iface in type.InterfaceNames)
 114            {
 32115                if (!lookup.ContainsKey(iface))
 116                {
 31117                    lookup[iface] = type;
 118                }
 119            }
 120        }
 121
 122        // Add types from referenced assemblies for dependency resolution
 33123        if (referencedAssemblyTypes != null)
 124        {
 56125            foreach (var kvp in referencedAssemblyTypes)
 126            {
 22127                foreach (var type in kvp.Value)
 128                {
 6129                    if (!lookup.ContainsKey(type.TypeName))
 130                    {
 5131                        lookup[type.TypeName] = type;
 132                    }
 20133                    foreach (var iface in type.InterfaceNames)
 134                    {
 4135                        if (!lookup.ContainsKey(iface))
 136                        {
 3137                            lookup[iface] = type;
 138                        }
 139                    }
 140                }
 141            }
 142        }
 143
 33144        return lookup;
 145    }
 146
 147    private static GraphService MapToGraphService(
 148        DiscoveredType type,
 149        string assemblyName,
 150        Dictionary<string, DiscoveredType> typeLookup,
 151        DiscoveryResult discoveryResult)
 152    {
 11541153        var service = new GraphService
 11541154        {
 11541155            Id = type.TypeName,
 11541156            TypeName = GetSimpleTypeName(type.TypeName),
 11541157            FullTypeName = type.TypeName,
 11541158            AssemblyName = assemblyName,
 11541159            Lifetime = type.Lifetime.ToString(),
 11541160            Location = type.SourceFilePath != null ? new GraphLocation
 11541161            {
 11541162                FilePath = type.SourceFilePath,
 11541163                Line = type.SourceLine,
 11541164                Column = 0
 11541165            } : null,
 11541166            ServiceKeys = type.ServiceKeys.ToList(),
 11541167            Metadata = new GraphServiceMetadata
 11541168            {
 11541169                IsDisposable = type.IsDisposable,
 2170                HasFactory = discoveryResult.Factories.Any(f => f.TypeName == type.TypeName),
 2171                HasOptions = discoveryResult.Options.Any(o => o.TypeName == type.TypeName),
 2172                IsHostedService = discoveryResult.HostedServices.Any(h => h.TypeName == type.TypeName),
 47780173                IsPlugin = discoveryResult.PluginTypes.Any(p => p.TypeName == type.TypeName)
 11541174            }
 11541175        };
 176
 177        // Map interfaces with locations
 23136178        foreach (var ifaceInfo in type.InterfaceInfos)
 179        {
 27180            service.Interfaces.Add(new GraphInterface
 27181            {
 27182                Name = GetSimpleTypeName(ifaceInfo.FullName),
 27183                FullName = ifaceInfo.FullName,
 27184                Location = ifaceInfo.HasLocation ? new GraphLocation
 27185                {
 27186                    FilePath = ifaceInfo.SourceFilePath!,
 27187                    Line = ifaceInfo.SourceLine,
 27188                    Column = 0
 27189                } : null
 27190            });
 191        }
 192        // Fall back to InterfaceNames if no InterfaceInfos (for backwards compat)
 11541193        if (type.InterfaceInfos.Length == 0)
 194        {
 23046195            foreach (var iface in type.InterfaceNames)
 196            {
 9197                service.Interfaces.Add(new GraphInterface
 9198                {
 9199                    Name = GetSimpleTypeName(iface),
 9200                    FullName = iface
 9201                });
 202            }
 203        }
 204
 205        // Map dependencies from constructor parameters
 32580206        foreach (var param in type.ConstructorParameters)
 207        {
 4749208            var dependency = new GraphDependency
 4749209            {
 4749210                ParameterName = param.ParameterName ?? string.Empty,
 4749211                TypeName = GetSimpleTypeName(param.TypeName),
 4749212                FullTypeName = param.TypeName,
 4749213                IsKeyed = param.IsKeyed,
 4749214                ServiceKey = param.ServiceKey
 4749215            };
 216
 217            // Try to resolve the dependency
 4749218            if (typeLookup.TryGetValue(param.TypeName, out var resolved))
 219            {
 1569220                dependency.ResolvedTo = resolved.TypeName;
 1569221                dependency.ResolvedLifetime = resolved.Lifetime.ToString();
 222            }
 223
 4749224            service.Dependencies.Add(dependency);
 225        }
 226
 227        // Map decorators
 11541228        var decorators = discoveryResult.Decorators
 12747229            .Where(d => type.InterfaceNames.Contains(d.ServiceTypeName))
 11564230            .OrderBy(d => d.Order);
 231
 23128232        foreach (var decorator in decorators)
 233        {
 23234            service.Decorators.Add(new GraphDecorator
 23235            {
 23236                TypeName = decorator.DecoratorTypeName,
 23237                Order = decorator.Order
 23238            });
 239        }
 240
 241        // Map interceptors
 11541242        var intercepted = discoveryResult.InterceptedServices
 11542243            .FirstOrDefault(i => i.TypeName == type.TypeName);
 244
 11541245        if (intercepted.TypeName != null)
 246        {
 1247            service.Interceptors = intercepted.AllInterceptorTypeNames.ToList();
 248        }
 249
 250        // Collect attributes
 11541251        service.Attributes.Add(type.Lifetime.ToString());
 11541252        if (type.IsKeyed)
 253        {
 2254            service.Attributes.Add("Keyed");
 255        }
 256
 11541257        return service;
 258    }
 259
 260    private static GraphStatistics ComputeStatistics(
 261        DiscoveryResult discoveryResult,
 262        IReadOnlyDictionary<string, IReadOnlyList<DiscoveredType>>? referencedAssemblyTypes)
 263    {
 264        // Get all types for statistics - current assembly + referenced assemblies
 33265        var allTypes = new List<DiscoveredType>(discoveryResult.InjectableTypes);
 33266        if (referencedAssemblyTypes != null)
 267        {
 56268            foreach (var kvp in referencedAssemblyTypes)
 269            {
 5270                allTypes.AddRange(kvp.Value);
 271            }
 272        }
 273
 33274        return new GraphStatistics
 33275        {
 33276            TotalServices = allTypes.Count,
 11541277            Singletons = allTypes.Count(t => t.Lifetime == GeneratorLifetime.Singleton),
 11541278            Scoped = allTypes.Count(t => t.Lifetime == GeneratorLifetime.Scoped),
 11541279            Transient = allTypes.Count(t => t.Lifetime == GeneratorLifetime.Transient),
 33280            Decorators = discoveryResult.Decorators.Count,
 33281            Interceptors = discoveryResult.InterceptedServices.Count,
 33282            Factories = discoveryResult.Factories.Count,
 33283            Options = discoveryResult.Options.Count,
 33284            HostedServices = discoveryResult.HostedServices.Count,
 33285            Plugins = discoveryResult.PluginTypes.Count
 33286        };
 287    }
 288
 289    private static string GetSimpleTypeName(string fullTypeName)
 290    {
 291        // Remove global:: prefix
 16767292        var name = fullTypeName;
 16767293        if (name.StartsWith("global::"))
 294        {
 16140295            name = name.Substring(8);
 296        }
 297
 298        // Handle generic types like Lazy<T> or IReadOnlyList<Assembly>
 299        // We want to preserve the generic structure but simplify inner types
 16767300        var genericStart = name.IndexOf('<');
 16767301        if (genericStart >= 0)
 302        {
 303            // Get the outer type name (before generic params)
 364304            var outerPart = name.Substring(0, genericStart);
 364305            var lastDot = outerPart.LastIndexOf('.');
 364306            var simpleOuter = lastDot >= 0 ? outerPart.Substring(lastDot + 1) : outerPart;
 307
 308            // Get the generic parameters and simplify them recursively
 364309            var genericEnd = name.LastIndexOf('>');
 364310            if (genericEnd > genericStart)
 311            {
 364312                var genericParams = name.Substring(genericStart + 1, genericEnd - genericStart - 1);
 313                // Simplify each generic parameter (split by comma, handle nested generics)
 364314                var simplifiedParams = SimplifyGenericParameters(genericParams);
 364315                return $"{simpleOuter}<{simplifiedParams}>";
 316            }
 317
 0318            return simpleOuter;
 319        }
 320
 321        // Get just the type name (after last dot)
 16403322        var idx = name.LastIndexOf('.');
 16403323        return idx >= 0 ? name.Substring(idx + 1) : name;
 324    }
 325
 326    private static string SimplifyGenericParameters(string genericParams)
 327    {
 328        // Handle nested generics by tracking depth
 364329        var result = new StringBuilder();
 364330        var depth = 0;
 364331        var currentParam = new StringBuilder();
 332
 23358333        foreach (var c in genericParams)
 334        {
 11315335            if (c == '<')
 336            {
 58337                depth++;
 58338                currentParam.Append(c);
 339            }
 11257340            else if (c == '>')
 341            {
 58342                depth--;
 58343                currentParam.Append(c);
 344            }
 11199345            else if (c == ',' && depth == 0)
 346            {
 347                // End of parameter at top level
 77348                if (result.Length > 0)
 349                {
 0350                    result.Append(", ");
 351                }
 77352                result.Append(GetSimpleTypeName(currentParam.ToString().Trim()));
 77353                currentParam.Clear();
 354            }
 355            else
 356            {
 11122357                currentParam.Append(c);
 358            }
 359        }
 360
 361        // Add last parameter
 364362        if (currentParam.Length > 0)
 363        {
 364364            if (result.Length > 0)
 365            {
 77366                result.Append(", ");
 367            }
 364368            result.Append(GetSimpleTypeName(currentParam.ToString().Trim()));
 369        }
 370
 364371        return result.ToString();
 372    }
 373
 374    /// <summary>
 375    /// Serializes the graph to JSON without using System.Text.Json (not available in all targets).
 376    /// Uses simple string building for source generator compatibility.
 377    /// </summary>
 378    private static string SerializeToJson(NeedlrGraph graph)
 379    {
 33380        var sb = new StringBuilder();
 33381        sb.AppendLine("{");
 33382        sb.AppendLine($"  \"schemaVersion\": \"{Escape(graph.SchemaVersion)}\",");
 33383        sb.AppendLine($"  \"generatedAt\": \"{Escape(graph.GeneratedAt)}\",");
 33384        sb.AppendLine($"  \"projectPath\": {NullableString(graph.ProjectPath)},");
 33385        sb.AppendLine($"  \"assemblyName\": {NullableString(graph.AssemblyName)},");
 386
 387        // Services array
 33388        sb.AppendLine("  \"services\": [");
 23148389        for (int i = 0; i < graph.Services.Count; i++)
 390        {
 11541391            SerializeService(sb, graph.Services[i], i == graph.Services.Count - 1);
 392        }
 33393        sb.AppendLine("  ],");
 394
 395        // Diagnostics array
 33396        sb.AppendLine("  \"diagnostics\": [");
 78397        for (int i = 0; i < graph.Diagnostics.Count; i++)
 398        {
 6399            SerializeDiagnostic(sb, graph.Diagnostics[i], i == graph.Diagnostics.Count - 1);
 400        }
 33401        sb.AppendLine("  ],");
 402
 403        // Statistics object
 33404        sb.AppendLine("  \"statistics\": {");
 33405        sb.AppendLine($"    \"totalServices\": {GeneratorHelpers.Literal(graph.Statistics.TotalServices)},");
 33406        sb.AppendLine($"    \"singletons\": {GeneratorHelpers.Literal(graph.Statistics.Singletons)},");
 33407        sb.AppendLine($"    \"scoped\": {GeneratorHelpers.Literal(graph.Statistics.Scoped)},");
 33408        sb.AppendLine($"    \"transient\": {GeneratorHelpers.Literal(graph.Statistics.Transient)},");
 33409        sb.AppendLine($"    \"decorators\": {GeneratorHelpers.Literal(graph.Statistics.Decorators)},");
 33410        sb.AppendLine($"    \"interceptors\": {GeneratorHelpers.Literal(graph.Statistics.Interceptors)},");
 33411        sb.AppendLine($"    \"factories\": {GeneratorHelpers.Literal(graph.Statistics.Factories)},");
 33412        sb.AppendLine($"    \"options\": {GeneratorHelpers.Literal(graph.Statistics.Options)},");
 33413        sb.AppendLine($"    \"hostedServices\": {GeneratorHelpers.Literal(graph.Statistics.HostedServices)},");
 33414        sb.AppendLine($"    \"plugins\": {GeneratorHelpers.Literal(graph.Statistics.Plugins)}");
 33415        sb.AppendLine("  }");
 416
 33417        sb.AppendLine("}");
 33418        return sb.ToString();
 419    }
 420
 421    private static void SerializeService(StringBuilder sb, GraphService service, bool isLast)
 422    {
 11541423        sb.AppendLine("    {");
 11541424        sb.AppendLine($"      \"id\": \"{Escape(service.Id)}\",");
 11541425        sb.AppendLine($"      \"typeName\": \"{Escape(service.TypeName)}\",");
 11541426        sb.AppendLine($"      \"fullTypeName\": \"{Escape(service.FullTypeName)}\",");
 11541427        sb.AppendLine($"      \"assemblyName\": {NullableString(service.AssemblyName)},");
 428
 429        // Interfaces
 11541430        sb.AppendLine("      \"interfaces\": [");
 23154431        for (int i = 0; i < service.Interfaces.Count; i++)
 432        {
 36433            var iface = service.Interfaces[i];
 36434            var comma = i < service.Interfaces.Count - 1 ? "," : "";
 36435            sb.AppendLine("        {");
 36436            sb.AppendLine($"          \"name\": \"{Escape(iface.Name)}\",");
 36437            sb.AppendLine($"          \"fullName\": \"{Escape(iface.FullName)}\",");
 36438            if (iface.Location != null)
 439            {
 26440                sb.AppendLine("          \"location\": {");
 26441                sb.AppendLine($"            \"filePath\": {NullableString(iface.Location.FilePath)},");
 26442                sb.AppendLine($"            \"line\": {GeneratorHelpers.Literal(iface.Location.Line)},");
 26443                sb.AppendLine($"            \"column\": {GeneratorHelpers.Literal(iface.Location.Column)}");
 26444                sb.AppendLine("          }");
 445            }
 446            else
 447            {
 10448                sb.AppendLine("          \"location\": null");
 449            }
 36450            sb.AppendLine($"        }}{comma}");
 451        }
 11541452        sb.AppendLine("      ],");
 453
 11541454        sb.AppendLine($"      \"lifetime\": \"{Escape(service.Lifetime)}\",");
 455
 456        // Location
 11541457        if (service.Location != null)
 458        {
 48459            sb.AppendLine("      \"location\": {");
 48460            sb.AppendLine($"        \"filePath\": {NullableString(service.Location.FilePath)},");
 48461            sb.AppendLine($"        \"line\": {GeneratorHelpers.Literal(service.Location.Line)},");
 48462            sb.AppendLine($"        \"column\": {GeneratorHelpers.Literal(service.Location.Column)}");
 48463            sb.AppendLine("      },");
 464        }
 465        else
 466        {
 11493467            sb.AppendLine("      \"location\": null,");
 468        }
 469
 470        // Dependencies
 11541471        sb.AppendLine("      \"dependencies\": [");
 32580472        for (int i = 0; i < service.Dependencies.Count; i++)
 473        {
 4749474            var dep = service.Dependencies[i];
 4749475            var comma = i < service.Dependencies.Count - 1 ? "," : "";
 4749476            sb.AppendLine("        {");
 4749477            sb.AppendLine($"          \"parameterName\": \"{Escape(dep.ParameterName)}\",");
 4749478            sb.AppendLine($"          \"typeName\": \"{Escape(dep.TypeName)}\",");
 4749479            sb.AppendLine($"          \"fullTypeName\": \"{Escape(dep.FullTypeName)}\",");
 4749480            sb.AppendLine($"          \"resolvedTo\": {NullableString(dep.ResolvedTo)},");
 4749481            sb.AppendLine($"          \"resolvedLifetime\": {NullableString(dep.ResolvedLifetime)},");
 4749482            sb.AppendLine($"          \"isKeyed\": {dep.IsKeyed.ToString().ToLowerInvariant()},");
 4749483            sb.AppendLine($"          \"serviceKey\": {NullableString(dep.ServiceKey)}");
 4749484            sb.AppendLine($"        }}{comma}");
 485        }
 11541486        sb.AppendLine("      ],");
 487
 488        // Decorators
 11541489        sb.AppendLine("      \"decorators\": [");
 23128490        for (int i = 0; i < service.Decorators.Count; i++)
 491        {
 23492            var dec = service.Decorators[i];
 23493            var comma = i < service.Decorators.Count - 1 ? "," : "";
 23494            sb.AppendLine($"        {{ \"typeName\": \"{Escape(dec.TypeName)}\", \"order\": {GeneratorHelpers.Literal(de
 495        }
 11541496        sb.AppendLine("      ],");
 497
 498        // Interceptors
 11541499        sb.Append("      \"interceptors\": [");
 11543500        sb.Append(string.Join(", ", service.Interceptors.Select(i => $"\"{Escape(i)}\"")));
 11541501        sb.AppendLine("],");
 502
 503        // Attributes
 11541504        sb.Append("      \"attributes\": [");
 23084505        sb.Append(string.Join(", ", service.Attributes.Select(a => $"\"{Escape(a)}\"")));
 11541506        sb.AppendLine("],");
 507
 508        // Service keys
 11541509        sb.Append("      \"serviceKeys\": [");
 11544510        sb.Append(string.Join(", ", service.ServiceKeys.Select(k => $"\"{Escape(k)}\"")));
 11541511        sb.AppendLine("],");
 512
 513        // Metadata
 11541514        sb.AppendLine("      \"metadata\": {");
 11541515        sb.AppendLine($"        \"hasFactory\": {service.Metadata.HasFactory.ToString().ToLowerInvariant()},");
 11541516        sb.AppendLine($"        \"hasOptions\": {service.Metadata.HasOptions.ToString().ToLowerInvariant()},");
 11541517        sb.AppendLine($"        \"isHostedService\": {service.Metadata.IsHostedService.ToString().ToLowerInvariant()},")
 11541518        sb.AppendLine($"        \"isDisposable\": {service.Metadata.IsDisposable.ToString().ToLowerInvariant()},");
 11541519        sb.AppendLine($"        \"isPlugin\": {service.Metadata.IsPlugin.ToString().ToLowerInvariant()}");
 11541520        sb.AppendLine("      }");
 521
 11541522        sb.AppendLine(isLast ? "    }" : "    },");
 11541523    }
 524
 525    private static void SerializeDiagnostic(StringBuilder sb, GraphDiagnostic diagnostic, bool isLast)
 526    {
 6527        sb.AppendLine("    {");
 6528        sb.AppendLine($"      \"id\": \"{Escape(diagnostic.Id)}\",");
 6529        sb.AppendLine($"      \"severity\": \"{Escape(diagnostic.Severity)}\",");
 6530        sb.AppendLine($"      \"message\": \"{Escape(diagnostic.Message)}\",");
 531
 6532        if (diagnostic.Location != null)
 533        {
 5534            sb.AppendLine("      \"location\": {");
 5535            sb.AppendLine($"        \"filePath\": {NullableString(diagnostic.Location.FilePath)},");
 5536            sb.AppendLine($"        \"line\": {GeneratorHelpers.Literal(diagnostic.Location.Line)},");
 5537            sb.AppendLine($"        \"column\": {GeneratorHelpers.Literal(diagnostic.Location.Column)}");
 5538            sb.AppendLine("      },");
 539        }
 540        else
 541        {
 1542            sb.AppendLine("      \"location\": null,");
 543        }
 544
 6545        sb.Append("      \"relatedServices\": [");
 11546        sb.Append(string.Join(", ", diagnostic.RelatedServices.Select(s => $"\"{Escape(s)}\"")));
 6547        sb.AppendLine("]");
 548
 6549        sb.AppendLine(isLast ? "    }" : "    },");
 6550    }
 551
 552    private static string Escape(string value)
 553    {
 86938554        if (string.IsNullOrEmpty(value))
 4803555            return value;
 556
 82135557        return value
 82135558            .Replace("\\", "\\\\")
 82135559            .Replace("\"", "\\\"")
 82135560            .Replace("\n", "\\n")
 82135561            .Replace("\r", "\\r")
 82135562            .Replace("\t", "\\t");
 563    }
 564
 565    private static string NullableString(string? value)
 566    {
 25933567        return value == null ? "null" : $"\"{Escape(value)}\"";
 568    }
 569
 570    /// <summary>
 571    /// Generates the NeedlrGraph.g.cs source file that embeds the graph JSON
 572    /// in a generated class for IDE tooling.
 573    /// </summary>
 574    internal static string GenerateGraphExportSource(string graphJson, string assemblyName, BreadcrumbWriter breadcrumbs
 575    {
 19576        var sb = new StringBuilder();
 577
 19578        breadcrumbs.WriteFileHeader(sb, assemblyName, "Needlr IDE Graph Export");
 579
 19580        sb.AppendLine("using System;");
 19581        sb.AppendLine("using System.IO;");
 19582        sb.AppendLine();
 19583        sb.AppendLine($"namespace {assemblyName}.Generated");
 19584        sb.AppendLine("{");
 19585        sb.AppendLine("    /// <summary>");
 19586        sb.AppendLine("    /// Provides the Needlr dependency graph for IDE tooling.");
 19587        sb.AppendLine("    /// </summary>");
 19588        sb.AppendLine("    internal static class NeedlrGraphExport");
 19589        sb.AppendLine("    {");
 19590        sb.AppendLine("        /// <summary>");
 19591        sb.AppendLine("        /// Gets the dependency graph JSON.");
 19592        sb.AppendLine("        /// </summary>");
 19593        sb.AppendLine("        public static string GraphJson => GraphJsonContent;");
 19594        sb.AppendLine();
 19595        sb.AppendLine("        private const string GraphJsonContent = @\"");
 596
 597        // Escape the JSON for C# verbatim string (double quotes only)
 19598        var escapedJson = graphJson.Replace("\"", "\"\"");
 19599        sb.Append(escapedJson);
 600
 19601        sb.AppendLine("\";");
 19602        sb.AppendLine();
 19603        sb.AppendLine("        /// <summary>");
 19604        sb.AppendLine("        /// Writes the graph to the specified path, stamping the current UTC time.");
 19605        sb.AppendLine("        /// </summary>");
 19606        sb.AppendLine("        public static void WriteGraphToFile(string path)");
 19607        sb.AppendLine("        {");
 19608        sb.AppendLine($"            var stamped = GraphJson.Replace(\"{GeneratedAtSentinel}\", DateTime.UtcNow.ToString(
 19609        sb.AppendLine("            File.WriteAllText(path, stamped);");
 19610        sb.AppendLine("        }");
 19611        sb.AppendLine("    }");
 19612        sb.AppendLine("}");
 613
 19614        return sb.ToString();
 615    }
 616}
 617
 618/// <summary>
 619/// Represents a diagnostic collected during generation for inclusion in the graph.
 620/// </summary>
 621internal sealed class CollectedDiagnostic
 622{
 623    public string Id { get; set; } = string.Empty;
 624    public string Severity { get; set; } = string.Empty;
 625    public string Message { get; set; } = string.Empty;
 626    public string? FilePath { get; set; }
 627    public int Line { get; set; }
 628    public IReadOnlyList<string>? RelatedServices { get; set; }
 629}

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)