< 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    {
 37920        if (!string.IsNullOrWhiteSpace(parameter.ParameterName))
 37921            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    {
 33333        return GeneratorHelpers.EscapeIdentifier(GetParameterBaseName(parameter));
 34    }
 35
 36    private static Dictionary<string, string> GetInjectableNamesByType(
 37        IEnumerable<TypeDiscoveryHelper.ConstructorParameterInfo> parameters)
 38    {
 3839        var namesByType = new Dictionary<string, string>(StringComparer.Ordinal);
 3840        var usedNames = new HashSet<string>(StringComparer.Ordinal);
 41
 15442        foreach (var parameter in parameters)
 43        {
 3944            if (namesByType.ContainsKey(parameter.TypeName))
 45                continue;
 46
 3947            var baseName = GetParameterBaseName(parameter);
 3948            var uniqueName = baseName;
 8249            for (var suffix = 2; !usedNames.Add(uniqueName); suffix++)
 250                uniqueName = baseName + suffix;
 51
 3952            namesByType.Add(parameter.TypeName, uniqueName);
 53        }
 54
 3855        return namesByType;
 56    }
 57
 58    internal static void GenerateFactoryInterface(StringBuilder builder, DiscoveredFactory factory, BreadcrumbWriter bre
 59    {
 3860        var factoryName = $"I{factory.SimpleTypeName}Factory";
 61
 3862        builder.AppendLine("/// <summary>");
 3863        builder.AppendLine($"/// Factory interface for creating instances of <see cref=\"{factory.TypeName}\"/>.");
 3864        builder.AppendLine("/// </summary>");
 3865        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 3866        builder.AppendLine($"public interface {factoryName}");
 3867        builder.AppendLine("{");
 68
 69        // Generate Create method for each constructor
 16270        foreach (var ctor in factory.Constructors)
 71        {
 4372            var runtimeParamList = string.Join(", ", ctor.RuntimeParameters.Select(p =>
 9273                $"{p.TypeName} {GetParameterIdentifierName(p)}"));
 74
 4375            builder.AppendLine($"    /// <summary>Creates a new instance of {factory.SimpleTypeName}.</summary>");
 76
 77            // Add <param> tags for documented runtime parameters
 18478            foreach (var param in ctor.RuntimeParameters)
 79            {
 4980                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
 4388            builder.AppendLine($"    {factory.ReturnTypeName} Create({runtimeParamList});");
 89        }
 90
 3891        builder.AppendLine("}");
 3892    }
 93
 94    internal static void GenerateFactoryImplementation(StringBuilder builder, DiscoveredFactory factory, BreadcrumbWrite
 95    {
 3896        var factoryInterfaceName = $"I{factory.SimpleTypeName}Factory";
 3897        var factoryImplName = $"{factory.SimpleTypeName}Factory";
 98
 3899        var allInjectableParams = factory.Constructors
 43100            .SelectMany(c => c.InjectableParameters)
 44101            .GroupBy(p => p.TypeName)
 39102            .Select(g => g.First())
 38103            .ToList();
 38104        var injectableNamesByType = GetInjectableNamesByType(allInjectableParams);
 105
 38106        builder.AppendLine("/// <summary>");
 38107        builder.AppendLine($"/// Factory implementation for creating instances of <see cref=\"{factory.TypeName}\"/>.");
 38108        builder.AppendLine("/// </summary>");
 38109        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 38110        builder.AppendLine($"internal sealed class {factoryImplName} : {factoryInterfaceName}");
 38111        builder.AppendLine("{");
 112
 113        // Fields for injectable dependencies
 154114        foreach (var param in allInjectableParams)
 115        {
 39116            var fieldName = "_" + injectableNamesByType[param.TypeName];
 39117            builder.AppendLine($"    private readonly {param.TypeName} {fieldName};");
 118        }
 119
 38120        builder.AppendLine();
 121
 122        // Constructor
 38123        var ctorParams = string.Join(
 38124            ", ",
 38125            allInjectableParams.Select(p =>
 77126                $"{p.TypeName} {GeneratorHelpers.EscapeIdentifier(injectableNamesByType[p.TypeName])}"));
 38127        builder.AppendLine($"    public {factoryImplName}({ctorParams})");
 38128        builder.AppendLine("    {");
 154129        foreach (var param in allInjectableParams)
 130        {
 39131            var parameterName = injectableNamesByType[param.TypeName];
 39132            var fieldName = "_" + parameterName;
 39133            var paramName = GeneratorHelpers.EscapeIdentifier(parameterName);
 39134            builder.AppendLine($"        {fieldName} = {paramName};");
 135        }
 38136        builder.AppendLine("    }");
 38137        builder.AppendLine();
 138
 139        // Create methods for each constructor
 162140        foreach (var ctor in factory.Constructors)
 141        {
 43142            var runtimeParamList = string.Join(", ", ctor.RuntimeParameters.Select(p =>
 92143                $"{p.TypeName} {GetParameterIdentifierName(p)}"));
 144
 43145            builder.AppendLine($"    public {factory.ReturnTypeName} Create({runtimeParamList})");
 43146            builder.AppendLine("    {");
 43147            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.
 43154            var allArgs = new List<string>();
 174155            foreach (var inj in ctor.InjectableParameters)
 156            {
 44157                var fieldName = "_" + injectableNamesByType[inj.TypeName];
 44158                var argName = GetParameterIdentifierName(inj);
 44159                allArgs.Add($"{argName}: {fieldName}");
 160            }
 184161            foreach (var rt in ctor.RuntimeParameters)
 162            {
 49163                var paramName = GetParameterIdentifierName(rt);
 49164                allArgs.Add($"{paramName}: {paramName}");
 165            }
 166
 43167            builder.Append(string.Join(", ", allArgs));
 43168            builder.AppendLine(");");
 43169            builder.AppendLine("    }");
 170        }
 171
 38172        builder.AppendLine("}");
 38173    }
 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)
 92178        var runtimeTypes = string.Join(", ", ctor.RuntimeParameters.Select(p => p.TypeName));
 43179        var funcType = $"Func<{runtimeTypes}, {factory.ReturnTypeName}>";
 180
 181        // Build the lambda
 43182        var runtimeParams = string.Join(", ", ctor.RuntimeParameters.Select(GetParameterIdentifierName));
 183
 43184        builder.AppendLine($"{indent}services.AddSingleton<{funcType}>(sp =>");
 43185        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.
 43190        var allArgs = new List<string>();
 174191        foreach (var inj in ctor.InjectableParameters)
 192        {
 44193            var argName = GetParameterIdentifierName(inj);
 44194            if (inj.IsKeyed)
 195            {
 0196                allArgs.Add($"{argName}: sp.GetRequiredKeyedService<{inj.TypeName}>(\"{GeneratorHelpers.EscapeStringLite
 197            }
 198            else
 199            {
 44200                allArgs.Add($"{argName}: sp.GetRequiredService<{inj.TypeName}>()");
 201            }
 202        }
 184203        foreach (var rt in ctor.RuntimeParameters)
 204        {
 49205            var paramName = GetParameterIdentifierName(rt);
 49206            allArgs.Add($"{paramName}: {paramName}");
 207        }
 208
 272209        for (int i = 0; i < allArgs.Count; i++)
 210        {
 93211            var arg = allArgs[i];
 93212            var isLast = i == allArgs.Count - 1;
 93213            builder.AppendLine($"{indent}        {arg}{(isLast ? ")" : ",")}");
 214        }
 43215        builder.AppendLine($"{indent});");
 43216    }
 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    {
 39224        var builder = new StringBuilder();
 39225        var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName);
 226
 39227        breadcrumbs.WriteFileHeader(builder, assemblyName, "Needlr Generated Factories");
 39228        builder.AppendLine("#nullable enable");
 39229        builder.AppendLine();
 39230        builder.AppendLine("using System;");
 39231        builder.AppendLine();
 39232        builder.AppendLine("using Microsoft.Extensions.DependencyInjection;");
 39233        builder.AppendLine();
 39234        builder.AppendLine($"namespace {safeAssemblyName}.Generated;");
 39235        builder.AppendLine();
 236
 237        // Generate factory interfaces and implementations for each type
 156238        foreach (var factory in factories)
 239        {
 39240            if (factory.GenerateInterface)
 241            {
 38242                GenerateFactoryInterface(builder, factory, breadcrumbs, projectDirectory);
 38243                builder.AppendLine();
 38244                GenerateFactoryImplementation(builder, factory, breadcrumbs, projectDirectory);
 38245                builder.AppendLine();
 246            }
 247        }
 248
 249        // Generate the registration helper
 39250        builder.AppendLine("/// <summary>");
 39251        builder.AppendLine("/// Helper class for registering factory types.");
 39252        builder.AppendLine("/// </summary>");
 39253        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 39254        builder.AppendLine("public static class FactoryRegistrations");
 39255        builder.AppendLine("{");
 39256        builder.AppendLine("    /// <summary>");
 39257        builder.AppendLine("    /// Registers all generated factories.");
 39258        builder.AppendLine("    /// </summary>");
 39259        builder.AppendLine("    /// <param name=\"services\">The service collection to register to.</param>");
 39260        builder.AppendLine("    public static void RegisterFactories(IServiceCollection services)");
 39261        builder.AppendLine("    {");
 262
 156263        foreach (var factory in factories)
 264        {
 39265            breadcrumbs.WriteInlineComment(builder, "        ", $"Factory for {factory.SimpleTypeName}");
 266
 267            // Register Func<> for each constructor
 39268            if (factory.GenerateFunc)
 269            {
 162270                foreach (var ctor in factory.Constructors)
 271                {
 43272                    GenerateFuncRegistration(builder, factory, ctor, "        ");
 273                }
 274            }
 275
 276            // Register interface factory
 39277            if (factory.GenerateInterface)
 278            {
 38279                var factoryInterfaceName = $"I{factory.SimpleTypeName}Factory";
 38280                var factoryImplName = $"{factory.SimpleTypeName}Factory";
 38281                builder.AppendLine($"        services.AddSingleton<global::{safeAssemblyName}.Generated.{factoryInterfac
 282            }
 283        }
 284
 39285        builder.AppendLine("    }");
 39286        builder.AppendLine();
 39287        builder.AppendLine("    /// <summary>");
 39288        builder.AppendLine("    /// Gets the number of factory types generated at compile time.");
 39289        builder.AppendLine("    /// </summary>");
 39290        builder.AppendLine($"    public static int Count => {GeneratorHelpers.Literal(factories.Count)};");
 39291        builder.AppendLine("}");
 292
 39293        return builder.ToString();
 294    }
 295}