< Summary

Information
Class: NexusLabs.Needlr.Generators.CodeGen.DecoratorsCodeGenerator
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/CodeGen/DecoratorsCodeGenerator.cs
Line coverage
86%
Covered lines: 95
Uncovered lines: 15
Coverable lines: 110
Total lines: 238
Line coverage: 86.3%
Branch coverage
84%
Covered branches: 39
Total branches: 46
Branch coverage: 84.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
GenerateApplyDecoratorsMethod(...)80.76%422671.15%
GenerateRegisterHostedServicesMethod(...)75%44100%
ExpandOpenDecorators(...)100%88100%
GenerateRegisterProvidersMethod(...)87.5%88100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/CodeGen/DecoratorsCodeGenerator.cs

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System.Collections.Generic;
 5using System.Linq;
 6using System.Text;
 7
 8using Microsoft.CodeAnalysis;
 9
 10using NexusLabs.Needlr.Generators.Models;
 11
 12namespace NexusLabs.Needlr.Generators.CodeGen;
 13
 14/// <summary>
 15/// Generates decorator registration, hosted service registration,
 16/// provider registration, and open-decorator expansion code.
 17/// </summary>
 18internal static class DecoratorsCodeGenerator
 19{
 20    /// <summary>
 21    /// Emits the <c>ApplyDecorators(IServiceCollection)</c> method body into the TypeRegistry class.
 22    /// </summary>
 23    internal static void GenerateApplyDecoratorsMethod(StringBuilder builder, IReadOnlyList<DiscoveredDecorator> decorat
 24    {
 59025        builder.AppendLine("    /// <summary>");
 59026        builder.AppendLine("    /// Applies all discovered decorators, interceptors, and hosted services to the service 
 59027        builder.AppendLine("    /// Decorators are applied in order, with lower Order values applied first (closer to th
 59028        builder.AppendLine("    /// </summary>");
 59029        builder.AppendLine("    /// <param name=\"services\">The service collection to apply decorators to.</param>");
 59030        builder.AppendLine("    public static void ApplyDecorators(IServiceCollection services)");
 59031        builder.AppendLine("    {");
 32
 33        // Register ServiceCatalog first
 59034        breadcrumbs.WriteInlineComment(builder, "        ", "Register service catalog for DI resolution");
 59035        builder.AppendLine($"        services.AddSingleton<global::NexusLabs.Needlr.Catalog.IServiceCatalog, global::{sa
 59036        builder.AppendLine();
 37
 38        // Register composed (closed-over-implementations) registrations before decorators apply.
 59039        if (hasComposedRegistrations)
 40        {
 4341            breadcrumbs.WriteInlineComment(builder, "        ", "Register composed closed-generic types");
 4342            builder.AppendLine("        RegisterComposedTypes(services);");
 4343            builder.AppendLine();
 44        }
 45
 46        // Register hosted services first (before decorators apply)
 59047        if (hasHostedServices)
 48        {
 849            breadcrumbs.WriteInlineComment(builder, "        ", "Register hosted services");
 850            builder.AppendLine("        RegisterHostedServices(services);");
 851            if (decorators.Count > 0 || hasInterceptors)
 52            {
 153                builder.AppendLine();
 54            }
 55        }
 56
 59057        if (decorators.Count == 0 && !hasInterceptors)
 58        {
 54759            if (!hasHostedServices)
 60            {
 54061                breadcrumbs.WriteInlineComment(builder, "        ", "No decorators, interceptors, or hosted services dis
 62            }
 63        }
 64        else
 65        {
 4366            if (decorators.Count > 0)
 67            {
 68                // Group decorators by service type and order by Order property
 2969                var decoratorsByService = decorators
 4770                    .GroupBy(d => d.ServiceTypeName)
 6171                    .OrderBy(g => g.Key, StringComparer.Ordinal);
 72
 12273                foreach (var serviceGroup in decoratorsByService)
 74                {
 75                    // Write verbose breadcrumb for decorator chain
 3276                    if (breadcrumbs.Level == BreadcrumbLevel.Verbose)
 77                    {
 078                        var chainItems = serviceGroup.OrderBy(d => d.Order).ToList();
 079                        var lines = new List<string>
 080                        {
 081                            "Resolution order (outer → inner → target):"
 082                        };
 083                        for (int i = 0; i < chainItems.Count; i++)
 84                        {
 085                            var dec = chainItems[i];
 086                            var sourcePath = dec.SourceFilePath != null
 087                                ? BreadcrumbWriter.GetRelativeSourcePath(dec.SourceFilePath, projectDirectory)
 088                                : $"[{dec.AssemblyName}]";
 089                            lines.Add($"  {GeneratorHelpers.Literal(i + 1)}. {dec.DecoratorTypeName.Split('.').Last()} (
 90                        }
 091                        lines.Add($"Triggered by: [DecoratorFor<{serviceGroup.Key.Split('.').Last()}>] attributes");
 92
 093                        breadcrumbs.WriteVerboseBox(builder, "        ",
 094                            $"Decorator Chain: {serviceGroup.Key.Split('.').Last()}",
 095                            lines.ToArray());
 96                    }
 97                    else
 98                    {
 3299                        breadcrumbs.WriteInlineComment(builder, "        ", $"Decorators for {serviceGroup.Key}");
 100                    }
 101
 205102                    foreach (var decorator in serviceGroup.OrderBy(d => d.Order))
 103                    {
 47104                        builder.AppendLine($"        services.AddDecorator<{decorator.ServiceTypeName}, {decorator.Decor
 105                    }
 106                }
 107            }
 108
 43109            if (hasInterceptors)
 110            {
 15111                builder.AppendLine();
 15112                breadcrumbs.WriteInlineComment(builder, "        ", "Register intercepted services with their proxies");
 15113                builder.AppendLine($"        global::{safeAssemblyName}.Generated.InterceptorRegistrations.RegisterInter
 114            }
 115        }
 116
 590117        builder.AppendLine("    }");
 590118    }
 119
 120    /// <summary>
 121    /// Emits the <c>RegisterHostedServices(IServiceCollection)</c> method body.
 122    /// </summary>
 123    internal static void GenerateRegisterHostedServicesMethod(StringBuilder builder, IReadOnlyList<DiscoveredHostedServi
 124    {
 8125        builder.AppendLine("    /// <summary>");
 8126        builder.AppendLine("    /// Registers all discovered hosted services (BackgroundService and IHostedService imple
 8127        builder.AppendLine("    /// Each service is registered as singleton and also as IHostedService for the host to d
 8128        builder.AppendLine("    /// </summary>");
 8129        builder.AppendLine("    /// <param name=\"services\">The service collection to register to.</param>");
 8130        builder.AppendLine("    private static void RegisterHostedServices(IServiceCollection services)");
 8131        builder.AppendLine("    {");
 132
 34133        foreach (var hostedService in hostedServices)
 134        {
 9135            var typeName = hostedService.TypeName;
 9136            var shortName = typeName.Split('.').Last();
 9137            var sourcePath = hostedService.SourceFilePath != null
 9138                ? BreadcrumbWriter.GetRelativeSourcePath(hostedService.SourceFilePath, projectDirectory)
 9139                : $"[{hostedService.AssemblyName}]";
 140
 9141            breadcrumbs.WriteInlineComment(builder, "        ", $"Hosted service: {shortName} ← {sourcePath}");
 142
 143            // Register the concrete type as singleton
 9144            builder.AppendLine($"        services.AddSingleton<{typeName}>();");
 145
 146            // Register as IHostedService that forwards to the concrete type
 9147            builder.AppendLine($"        services.AddSingleton<global::Microsoft.Extensions.Hosting.IHostedService>(sp =
 148        }
 149
 8150        builder.AppendLine("    }");
 8151    }
 152
 153    /// <summary>
 154    /// Expands open generic decorators into concrete decorator registrations
 155    /// for each discovered closed implementation of the open generic interface.
 156    /// </summary>
 157    internal static void ExpandOpenDecorators(
 158        IReadOnlyList<DiscoveredType> injectableTypes,
 159        IReadOnlyList<DiscoveredOpenDecorator> openDecorators,
 160        List<DiscoveredDecorator> decorators)
 161    {
 40162        foreach (var discoveredType in injectableTypes)
 163        {
 164            // We need to check each interface this type implements to see if it's a closed version of an open generic
 60165            foreach (var openDecorator in openDecorators)
 166            {
 167                // Check if this type implements the open generic interface
 46168                foreach (var interfaceName in discoveredType.InterfaceNames)
 169                {
 170                    // This is string-based matching - we need to match the interface name pattern
 171                    // For example, if open generic is IHandler<> and the interface is IHandler<Order>, we should match
 7172                    var openGenericName = TypeDiscoveryHelper.GetFullyQualifiedName(openDecorator.OpenGenericInterface);
 173
 174                    // Extract the base name (before the <>)
 7175                    var openGenericBaseName = GeneratorHelpers.GetGenericBaseName(openGenericName);
 7176                    var interfaceBaseName = GeneratorHelpers.GetGenericBaseName(interfaceName);
 177
 7178                    if (openGenericBaseName == interfaceBaseName)
 179                    {
 180                        // This interface is a closed version of the open generic
 181                        // Create a closed decorator registration
 7182                        var closedDecoratorTypeName = GeneratorHelpers.CreateClosedGenericType(
 7183                            TypeDiscoveryHelper.GetFullyQualifiedName(openDecorator.DecoratorType),
 7184                            interfaceName,
 7185                            openGenericName);
 186
 7187                        decorators.Add(new DiscoveredDecorator(
 7188                            closedDecoratorTypeName,
 7189                            interfaceName,
 7190                            openDecorator.Order,
 7191                            openDecorator.AssemblyName,
 7192                            openDecorator.SourceFilePath));
 193                    }
 194                }
 195            }
 196        }
 6197    }
 198
 199    /// <summary>
 200    /// Emits the <c>RegisterProviders(IServiceCollection)</c> method body.
 201    /// </summary>
 202    internal static void GenerateRegisterProvidersMethod(StringBuilder builder, IReadOnlyList<DiscoveredProvider> provid
 203    {
 18204        builder.AppendLine("    /// <summary>");
 18205        builder.AppendLine("    /// Registers all generated providers as Singletons.");
 18206        builder.AppendLine("    /// Providers are strongly-typed service locators that expose services via typed propert
 18207        builder.AppendLine("    /// </summary>");
 18208        builder.AppendLine("    /// <param name=\"services\">The service collection to register to.</param>");
 18209        builder.AppendLine("    public static void RegisterProviders(IServiceCollection services)");
 18210        builder.AppendLine("    {");
 211
 74212        foreach (var provider in providers)
 213        {
 19214            var shortName = provider.SimpleTypeName;
 19215            var sourcePath = provider.SourceFilePath != null
 19216                ? BreadcrumbWriter.GetRelativeSourcePath(provider.SourceFilePath, projectDirectory)
 19217                : $"[{provider.AssemblyName}]";
 218
 19219            breadcrumbs.WriteInlineComment(builder, "        ", $"Provider: {shortName} ← {sourcePath}");
 220
 19221            if (provider.IsInterface)
 222            {
 223                // Interface mode: register the generated implementation
 13224                var implName = provider.ImplementationTypeName;
 13225                builder.AppendLine($"        services.AddSingleton<{provider.TypeName}, global::{safeAssemblyName}.Gener
 226            }
 6227            else if (provider.IsPartial)
 228            {
 229                // Shorthand class mode: register the partial class as its generated interface
 6230                var interfaceName = provider.InterfaceTypeName;
 6231                var providerNamespace = GeneratorHelpers.GetNamespaceFromTypeName(provider.TypeName);
 6232                builder.AppendLine($"        services.AddSingleton<global::{providerNamespace}.{interfaceName}, {provide
 233            }
 234        }
 235
 18236        builder.AppendLine("    }");
 18237    }
 238}