< Summary

Information
Class: NexusLabs.Needlr.Generators.CodeGen.DecoratorsCodeGenerator
Assembly: NexusLabs.Needlr.Generators
File(s): /actions-runner/_work/needlr/needlr/src/NexusLabs.Needlr.Generators/CodeGen/DecoratorsCodeGenerator.cs
Line coverage
85%
Covered lines: 94
Uncovered lines: 16
Coverable lines: 110
Total lines: 238
Line coverage: 85.4%
Branch coverage
82%
Covered branches: 38
Total branches: 46
Branch coverage: 82.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

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

File(s)

/actions-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    {
 54925        builder.AppendLine("    /// <summary>");
 54926        builder.AppendLine("    /// Applies all discovered decorators, interceptors, and hosted services to the service 
 54927        builder.AppendLine("    /// Decorators are applied in order, with lower Order values applied first (closer to th
 54928        builder.AppendLine("    /// </summary>");
 54929        builder.AppendLine("    /// <param name=\"services\">The service collection to apply decorators to.</param>");
 54930        builder.AppendLine("    public static void ApplyDecorators(IServiceCollection services)");
 54931        builder.AppendLine("    {");
 32
 33        // Register ServiceCatalog first
 54934        breadcrumbs.WriteInlineComment(builder, "        ", "Register service catalog for DI resolution");
 54935        builder.AppendLine($"        services.AddSingleton<global::NexusLabs.Needlr.Catalog.IServiceCatalog, global::{sa
 54936        builder.AppendLine();
 37
 38        // Register composed (closed-over-implementations) registrations before decorators apply.
 54939        if (hasComposedRegistrations)
 40        {
 4241            breadcrumbs.WriteInlineComment(builder, "        ", "Register composed closed-generic types");
 4242            builder.AppendLine("        RegisterComposedTypes(services);");
 4243            builder.AppendLine();
 44        }
 45
 46        // Register hosted services first (before decorators apply)
 54947        if (hasHostedServices)
 48        {
 749            breadcrumbs.WriteInlineComment(builder, "        ", "Register hosted services");
 750            builder.AppendLine("        RegisterHostedServices(services);");
 751            if (decorators.Count > 0 || hasInterceptors)
 52            {
 053                builder.AppendLine();
 54            }
 55        }
 56
 54957        if (decorators.Count == 0 && !hasInterceptors)
 58        {
 51859            if (!hasHostedServices)
 60            {
 51161                breadcrumbs.WriteInlineComment(builder, "        ", "No decorators, interceptors, or hosted services dis
 62            }
 63        }
 64        else
 65        {
 3166            if (decorators.Count > 0)
 67            {
 68                // Group decorators by service type and order by Order property
 1769                var decoratorsByService = decorators
 2670                    .GroupBy(d => d.ServiceTypeName)
 3771                    .OrderBy(g => g.Key);
 72
 7473                foreach (var serviceGroup in decoratorsByService)
 74                {
 75                    // Write verbose breadcrumb for decorator chain
 2076                    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($"  {i + 1}. {dec.DecoratorTypeName.Split('.').Last()} (Order={dec.Order}) ← {sour
 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                    {
 2099                        breadcrumbs.WriteInlineComment(builder, "        ", $"Decorators for {serviceGroup.Key}");
 100                    }
 101
 118102                    foreach (var decorator in serviceGroup.OrderBy(d => d.Order))
 103                    {
 26104                        builder.AppendLine($"        services.AddDecorator<{decorator.ServiceTypeName}, {decorator.Decor
 105                    }
 106                }
 107            }
 108
 31109            if (hasInterceptors)
 110            {
 14111                builder.AppendLine();
 14112                breadcrumbs.WriteInlineComment(builder, "        ", "Register intercepted services with their proxies");
 14113                builder.AppendLine($"        global::{safeAssemblyName}.Generated.InterceptorRegistrations.RegisterInter
 114            }
 115        }
 116
 549117        builder.AppendLine("    }");
 549118    }
 119
 120    /// <summary>
 121    /// Emits the <c>RegisterHostedServices(IServiceCollection)</c> method body.
 122    /// </summary>
 123    internal static void GenerateRegisterHostedServicesMethod(StringBuilder builder, IReadOnlyList<DiscoveredHostedServi
 124    {
 7125        builder.AppendLine("    /// <summary>");
 7126        builder.AppendLine("    /// Registers all discovered hosted services (BackgroundService and IHostedService imple
 7127        builder.AppendLine("    /// Each service is registered as singleton and also as IHostedService for the host to d
 7128        builder.AppendLine("    /// </summary>");
 7129        builder.AppendLine("    /// <param name=\"services\">The service collection to register to.</param>");
 7130        builder.AppendLine("    private static void RegisterHostedServices(IServiceCollection services)");
 7131        builder.AppendLine("    {");
 132
 30133        foreach (var hostedService in hostedServices)
 134        {
 8135            var typeName = hostedService.TypeName;
 8136            var shortName = typeName.Split('.').Last();
 8137            var sourcePath = hostedService.SourceFilePath != null
 8138                ? BreadcrumbWriter.GetRelativeSourcePath(hostedService.SourceFilePath, projectDirectory)
 8139                : $"[{hostedService.AssemblyName}]";
 140
 8141            breadcrumbs.WriteInlineComment(builder, "        ", $"Hosted service: {shortName} ← {sourcePath}");
 142
 143            // Register the concrete type as singleton
 8144            builder.AppendLine($"        services.AddSingleton<{typeName}>();");
 145
 146            // Register as IHostedService that forwards to the concrete type
 8147            builder.AppendLine($"        services.AddSingleton<global::Microsoft.Extensions.Hosting.IHostedService>(sp =
 148        }
 149
 7150        builder.AppendLine("    }");
 7151    }
 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    {
 17204        builder.AppendLine("    /// <summary>");
 17205        builder.AppendLine("    /// Registers all generated providers as Singletons.");
 17206        builder.AppendLine("    /// Providers are strongly-typed service locators that expose services via typed propert
 17207        builder.AppendLine("    /// </summary>");
 17208        builder.AppendLine("    /// <param name=\"services\">The service collection to register to.</param>");
 17209        builder.AppendLine("    public static void RegisterProviders(IServiceCollection services)");
 17210        builder.AppendLine("    {");
 211
 70212        foreach (var provider in providers)
 213        {
 18214            var shortName = provider.SimpleTypeName;
 18215            var sourcePath = provider.SourceFilePath != null
 18216                ? BreadcrumbWriter.GetRelativeSourcePath(provider.SourceFilePath, projectDirectory)
 18217                : $"[{provider.AssemblyName}]";
 218
 18219            breadcrumbs.WriteInlineComment(builder, "        ", $"Provider: {shortName} ← {sourcePath}");
 220
 18221            if (provider.IsInterface)
 222            {
 223                // Interface mode: register the generated implementation
 12224                var implName = provider.ImplementationTypeName;
 12225                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
 17236        builder.AppendLine("    }");
 17237    }
 238}