< Summary

Information
Class: NexusLabs.Needlr.Generators.CodeGen.FactoryCodeGenerator
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/CodeGen/FactoryCodeGenerator.cs
Line coverage
96%
Covered lines: 147
Uncovered lines: 6
Coverable lines: 153
Total lines: 295
Line coverage: 96%
Branch coverage
91%
Covered branches: 44
Total branches: 48
Branch coverage: 91.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
GetParameterBaseName(...)25%10428.57%
GetParameterIdentifierName(...)100%11100%
GetInjectableNamesByType(...)100%66100%
GenerateFactoryInterface(...)100%66100%
GenerateFactoryImplementation(...)100%1010100%
GenerateFuncRegistration(...)90%101095%
GenerateFactoriesSource(...)100%1212100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/CodeGen/FactoryCodeGenerator.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.CodeGen;
 12
 13/// <summary>
 14/// Generates factory interfaces and implementations for [GenerateFactory] attributed types.
 15/// </summary>
 16internal static class FactoryCodeGenerator
 17{
 18    private static string GetParameterBaseName(TypeDiscoveryHelper.ConstructorParameterInfo parameter)
 19    {
 37120        if (!string.IsNullOrWhiteSpace(parameter.ParameterName))
 37121            return parameter.ParameterName!;
 22
 023        var typeName = GeneratorHelpers.GetShortTypeName(parameter.TypeName);
 024        var genericStart = typeName.IndexOf('<');
 025        if (genericStart >= 0)
 026            typeName = typeName.Substring(0, genericStart);
 27
 028        return GeneratorHelpers.ToCamelCase(typeName);
 29    }
 30
 31    private static string GetParameterIdentifierName(TypeDiscoveryHelper.ConstructorParameterInfo parameter)
 32    {
 32633        return GeneratorHelpers.EscapeIdentifier(GetParameterBaseName(parameter));
 34    }
 35
 36    private static Dictionary<string, string> GetInjectableNamesByType(
 37        IEnumerable<TypeDiscoveryHelper.ConstructorParameterInfo> parameters)
 38    {
 3739        var namesByType = new Dictionary<string, string>(StringComparer.Ordinal);
 3740        var usedNames = new HashSet<string>(StringComparer.Ordinal);
 41
 15042        foreach (var parameter in parameters)
 43        {
 3844            if (namesByType.ContainsKey(parameter.TypeName))
 45                continue;
 46
 3847            var baseName = GetParameterBaseName(parameter);
 3848            var uniqueName = baseName;
 8049            for (var suffix = 2; !usedNames.Add(uniqueName); suffix++)
 250                uniqueName = baseName + suffix;
 51
 3852            namesByType.Add(parameter.TypeName, uniqueName);
 53        }
 54
 3755        return namesByType;
 56    }
 57
 58    internal static void GenerateFactoryInterface(StringBuilder builder, DiscoveredFactory factory, BreadcrumbWriter bre
 59    {
 3760        var factoryName = $"I{factory.SimpleTypeName}Factory";
 61
 3762        builder.AppendLine("/// <summary>");
 3763        builder.AppendLine($"/// Factory interface for creating instances of <see cref=\"{factory.TypeName}\"/>.");
 3764        builder.AppendLine("/// </summary>");
 3765        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 3766        builder.AppendLine($"public interface {factoryName}");
 3767        builder.AppendLine("{");
 68
 69        // Generate Create method for each constructor
 15870        foreach (var ctor in factory.Constructors)
 71        {
 4272            var runtimeParamList = string.Join(", ", ctor.RuntimeParameters.Select(p =>
 9073                $"{p.TypeName} {GetParameterIdentifierName(p)}"));
 74
 4275            builder.AppendLine($"    /// <summary>Creates a new instance of {factory.SimpleTypeName}.</summary>");
 76
 77            // Add <param> tags for documented runtime parameters
 18078            foreach (var param in ctor.RuntimeParameters)
 79            {
 4880                if (!string.IsNullOrWhiteSpace(param.DocumentationComment))
 81                {
 782                    var paramName = GetParameterBaseName(param);
 783                    var escapedDoc = GeneratorHelpers.EscapeXmlContent(param.DocumentationComment!);
 784                    builder.AppendLine($"    /// <param name=\"{paramName}\">{escapedDoc}</param>");
 85                }
 86            }
 87
 4288            builder.AppendLine($"    {factory.ReturnTypeName} Create({runtimeParamList});");
 89        }
 90
 3791        builder.AppendLine("}");
 3792    }
 93
 94    internal static void GenerateFactoryImplementation(StringBuilder builder, DiscoveredFactory factory, BreadcrumbWrite
 95    {
 3796        var factoryInterfaceName = $"I{factory.SimpleTypeName}Factory";
 3797        var factoryImplName = $"{factory.SimpleTypeName}Factory";
 98
 3799        var allInjectableParams = factory.Constructors
 42100            .SelectMany(c => c.InjectableParameters)
 43101            .GroupBy(p => p.TypeName)
 38102            .Select(g => g.First())
 37103            .ToList();
 37104        var injectableNamesByType = GetInjectableNamesByType(allInjectableParams);
 105
 37106        builder.AppendLine("/// <summary>");
 37107        builder.AppendLine($"/// Factory implementation for creating instances of <see cref=\"{factory.TypeName}\"/>.");
 37108        builder.AppendLine("/// </summary>");
 37109        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 37110        builder.AppendLine($"internal sealed class {factoryImplName} : {factoryInterfaceName}");
 37111        builder.AppendLine("{");
 112
 113        // Fields for injectable dependencies
 150114        foreach (var param in allInjectableParams)
 115        {
 38116            var fieldName = "_" + injectableNamesByType[param.TypeName];
 38117            builder.AppendLine($"    private readonly {param.TypeName} {fieldName};");
 118        }
 119
 37120        builder.AppendLine();
 121
 122        // Constructor
 37123        var ctorParams = string.Join(
 37124            ", ",
 37125            allInjectableParams.Select(p =>
 75126                $"{p.TypeName} {GeneratorHelpers.EscapeIdentifier(injectableNamesByType[p.TypeName])}"));
 37127        builder.AppendLine($"    public {factoryImplName}({ctorParams})");
 37128        builder.AppendLine("    {");
 150129        foreach (var param in allInjectableParams)
 130        {
 38131            var parameterName = injectableNamesByType[param.TypeName];
 38132            var fieldName = "_" + parameterName;
 38133            var paramName = GeneratorHelpers.EscapeIdentifier(parameterName);
 38134            builder.AppendLine($"        {fieldName} = {paramName};");
 135        }
 37136        builder.AppendLine("    }");
 37137        builder.AppendLine();
 138
 139        // Create methods for each constructor
 158140        foreach (var ctor in factory.Constructors)
 141        {
 42142            var runtimeParamList = string.Join(", ", ctor.RuntimeParameters.Select(p =>
 90143                $"{p.TypeName} {GetParameterIdentifierName(p)}"));
 144
 42145            builder.AppendLine($"    public {factory.ReturnTypeName} Create({runtimeParamList})");
 42146            builder.AppendLine("    {");
 42147            builder.Append($"        return new {factory.TypeName}(");
 148
 149            // Build constructor arguments as named arguments (paramName: value) rather
 150            // than positionally. A generated constructor's parameter order follows field
 151            // declaration order and need not match this injectable-then-runtime grouping,
 152            // so binding by name is required for correctness regardless of which order
 153            // the target constructor was declared or generated in.
 42154            var allArgs = new List<string>();
 170155            foreach (var inj in ctor.InjectableParameters)
 156            {
 43157                var fieldName = "_" + injectableNamesByType[inj.TypeName];
 43158                var argName = GetParameterIdentifierName(inj);
 43159                allArgs.Add($"{argName}: {fieldName}");
 160            }
 180161            foreach (var rt in ctor.RuntimeParameters)
 162            {
 48163                var paramName = GetParameterIdentifierName(rt);
 48164                allArgs.Add($"{paramName}: {paramName}");
 165            }
 166
 42167            builder.Append(string.Join(", ", allArgs));
 42168            builder.AppendLine(");");
 42169            builder.AppendLine("    }");
 170        }
 171
 37172        builder.AppendLine("}");
 37173    }
 174
 175    internal static void GenerateFuncRegistration(StringBuilder builder, DiscoveredFactory factory, FactoryDiscoveryHelp
 176    {
 177        // Build Func<TRuntime..., TReturn> type - uses ReturnTypeName (interface if generic attribute used)
 90178        var runtimeTypes = string.Join(", ", ctor.RuntimeParameters.Select(p => p.TypeName));
 42179        var funcType = $"Func<{runtimeTypes}, {factory.ReturnTypeName}>";
 180
 181        // Build the lambda
 42182        var runtimeParams = string.Join(", ", ctor.RuntimeParameters.Select(GetParameterIdentifierName));
 183
 42184        builder.AppendLine($"{indent}services.AddSingleton<{funcType}>(sp =>");
 42185        builder.AppendLine($"{indent}    ({runtimeParams}) => new {factory.TypeName}(");
 186
 187        // Build constructor call arguments as named arguments — see the matching
 188        // comment in GenerateFactoryImplementation for why positional binding is unsafe
 189        // here.
 42190        var allArgs = new List<string>();
 170191        foreach (var inj in ctor.InjectableParameters)
 192        {
 43193            var argName = GetParameterIdentifierName(inj);
 43194            if (inj.IsKeyed)
 195            {
 0196                allArgs.Add($"{argName}: sp.GetRequiredKeyedService<{inj.TypeName}>(\"{GeneratorHelpers.EscapeStringLite
 197            }
 198            else
 199            {
 43200                allArgs.Add($"{argName}: sp.GetRequiredService<{inj.TypeName}>()");
 201            }
 202        }
 180203        foreach (var rt in ctor.RuntimeParameters)
 204        {
 48205            var paramName = GetParameterIdentifierName(rt);
 48206            allArgs.Add($"{paramName}: {paramName}");
 207        }
 208
 266209        for (int i = 0; i < allArgs.Count; i++)
 210        {
 91211            var arg = allArgs[i];
 91212            var isLast = i == allArgs.Count - 1;
 91213            builder.AppendLine($"{indent}        {arg}{(isLast ? ")" : ",")}");
 214        }
 42215        builder.AppendLine($"{indent});");
 42216    }
 217
 218    /// <summary>
 219    /// Generates the complete Factories.g.cs source file containing factory
 220    /// interfaces, implementations, and the FactoryRegistrations helper.
 221    /// </summary>
 222    internal static string GenerateFactoriesSource(IReadOnlyList<DiscoveredFactory> factories, string assemblyName, Brea
 223    {
 38224        var builder = new StringBuilder();
 38225        var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName);
 226
 38227        breadcrumbs.WriteFileHeader(builder, assemblyName, "Needlr Generated Factories");
 38228        builder.AppendLine("#nullable enable");
 38229        builder.AppendLine();
 38230        builder.AppendLine("using System;");
 38231        builder.AppendLine();
 38232        builder.AppendLine("using Microsoft.Extensions.DependencyInjection;");
 38233        builder.AppendLine();
 38234        builder.AppendLine($"namespace {safeAssemblyName}.Generated;");
 38235        builder.AppendLine();
 236
 237        // Generate factory interfaces and implementations for each type
 152238        foreach (var factory in factories)
 239        {
 38240            if (factory.GenerateInterface)
 241            {
 37242                GenerateFactoryInterface(builder, factory, breadcrumbs, projectDirectory);
 37243                builder.AppendLine();
 37244                GenerateFactoryImplementation(builder, factory, breadcrumbs, projectDirectory);
 37245                builder.AppendLine();
 246            }
 247        }
 248
 249        // Generate the registration helper
 38250        builder.AppendLine("/// <summary>");
 38251        builder.AppendLine("/// Helper class for registering factory types.");
 38252        builder.AppendLine("/// </summary>");
 38253        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 38254        builder.AppendLine("public static class FactoryRegistrations");
 38255        builder.AppendLine("{");
 38256        builder.AppendLine("    /// <summary>");
 38257        builder.AppendLine("    /// Registers all generated factories.");
 38258        builder.AppendLine("    /// </summary>");
 38259        builder.AppendLine("    /// <param name=\"services\">The service collection to register to.</param>");
 38260        builder.AppendLine("    public static void RegisterFactories(IServiceCollection services)");
 38261        builder.AppendLine("    {");
 262
 152263        foreach (var factory in factories)
 264        {
 38265            breadcrumbs.WriteInlineComment(builder, "        ", $"Factory for {factory.SimpleTypeName}");
 266
 267            // Register Func<> for each constructor
 38268            if (factory.GenerateFunc)
 269            {
 158270                foreach (var ctor in factory.Constructors)
 271                {
 42272                    GenerateFuncRegistration(builder, factory, ctor, "        ");
 273                }
 274            }
 275
 276            // Register interface factory
 38277            if (factory.GenerateInterface)
 278            {
 37279                var factoryInterfaceName = $"I{factory.SimpleTypeName}Factory";
 37280                var factoryImplName = $"{factory.SimpleTypeName}Factory";
 37281                builder.AppendLine($"        services.AddSingleton<global::{safeAssemblyName}.Generated.{factoryInterfac
 282            }
 283        }
 284
 38285        builder.AppendLine("    }");
 38286        builder.AppendLine();
 38287        builder.AppendLine("    /// <summary>");
 38288        builder.AppendLine("    /// Gets the number of factory types generated at compile time.");
 38289        builder.AppendLine("    /// </summary>");
 38290        builder.AppendLine($"    public static int Count => {factories.Count};");
 38291        builder.AppendLine("}");
 292
 38293        return builder.ToString();
 294    }
 295}