< Summary

Information
Class: NexusLabs.Needlr.Generators.CodeGen.InterceptorCodeGenerator
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/CodeGen/InterceptorCodeGenerator.cs
Line coverage
91%
Covered lines: 168
Uncovered lines: 16
Coverable lines: 184
Total lines: 342
Line coverage: 91.3%
Branch coverage
71%
Covered branches: 40
Total branches: 56
Branch coverage: 71.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
GenerateInterceptorProxyClass(...)78.57%141498.33%
GenerateInterceptedMethod(...)63.33%373080.3%
GenerateInterceptorProxiesSource(...)83.33%121296.55%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/CodeGen/InterceptorCodeGenerator.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 interceptor proxy classes for AOP-style method interception.
 15/// </summary>
 16internal static class InterceptorCodeGenerator
 17{
 18    internal static void GenerateInterceptorProxyClass(StringBuilder builder, DiscoveredInterceptedService service, Brea
 19    {
 1520        var proxyTypeName = GeneratorHelpers.GetProxyTypeName(service.TypeName);
 1521        var shortTypeName = GeneratorHelpers.GetShortTypeName(service.TypeName);
 22
 23        // Write verbose breadcrumb for interceptor proxy
 1524        if (breadcrumbs.Level == BreadcrumbLevel.Verbose)
 25        {
 326            var sourcePath = service.SourceFilePath != null
 327                ? BreadcrumbWriter.GetRelativeSourcePath(service.SourceFilePath, projectDirectory)
 328                : $"[{service.AssemblyName}]";
 29
 330            var interceptorsList = service.AllInterceptorTypeNames
 531                .Select((t, i) => $"  {i + 1}. {t.Split('.').Last()}")
 332                .ToArray();
 33
 334            var proxiedMethods = service.Methods
 335                .Where(m => m.InterceptorTypeNames.Length > 0)
 336                .Select(m => m.Name)
 337                .ToArray();
 338            var forwardedMethods = service.Methods
 339                .Where(m => m.InterceptorTypeNames.Length == 0)
 040                .Select(m => m.Name)
 341                .ToArray();
 42
 343            var lines = new List<string>
 344            {
 345                $"Source: {sourcePath}",
 346                $"Target Interface: {string.Join(", ", service.InterfaceNames.Select(i => i.Split('.').Last()))}",
 347                "Interceptors (execution order):"
 348            };
 349            lines.AddRange(interceptorsList);
 350            lines.Add($"Methods proxied: {(proxiedMethods.Length > 0 ? string.Join(", ", proxiedMethods) : "none")}");
 351            lines.Add($"Methods forwarded: {(forwardedMethods.Length > 0 ? string.Join(", ", forwardedMethods) : "none")
 52
 353            breadcrumbs.WriteVerboseBox(builder, "",
 354                $"Interceptor Proxy: {shortTypeName}",
 355                lines.ToArray());
 56        }
 57
 1558        builder.AppendLine("/// <summary>");
 1559        builder.AppendLine($"/// Interceptor proxy for {service.TypeName}.");
 1560        builder.AppendLine("/// Routes method calls through configured interceptors.");
 1561        builder.AppendLine("/// </summary>");
 1562        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 63
 64        // Implement all interfaces
 1565        builder.Append($"internal sealed class {proxyTypeName}");
 1566        if (service.InterfaceNames.Length > 0)
 67        {
 1568            builder.Append(" : ");
 1569            builder.Append(string.Join(", ", service.InterfaceNames));
 70        }
 1571        builder.AppendLine();
 1572        builder.AppendLine("{");
 73
 74        // Fields
 1575        builder.AppendLine($"    private readonly {service.TypeName} _target;");
 1576        builder.AppendLine("    private readonly IServiceProvider _serviceProvider;");
 1577        builder.AppendLine();
 78
 79        // Static MethodInfo fields for each method
 1580        var methodIndex = 0;
 6081        foreach (var method in service.Methods)
 82        {
 1583            builder.AppendLine($"    internal static readonly MethodInfo _method{methodIndex} = typeof({method.Interface
 1584            methodIndex++;
 85        }
 1586        builder.AppendLine();
 87
 88        // Constructor
 1589        builder.AppendLine($"    public {proxyTypeName}({service.TypeName} target, IServiceProvider serviceProvider)");
 1590        builder.AppendLine("    {");
 1591        builder.AppendLine("        _target = target ?? throw new ArgumentNullException(nameof(target));");
 1592        builder.AppendLine("        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(service
 1593        builder.AppendLine("    }");
 1594        builder.AppendLine();
 95
 96        // Generate each method
 1597        methodIndex = 0;
 6098        foreach (var method in service.Methods)
 99        {
 15100            GenerateInterceptedMethod(builder, method, methodIndex, service.TypeName, breadcrumbs);
 15101            methodIndex++;
 102        }
 103
 15104        builder.AppendLine("}");
 15105    }
 106
 107    internal static void GenerateInterceptedMethod(StringBuilder builder, InterceptorDiscoveryHelper.InterceptedMethodIn
 108    {
 15109        var parameterList = method.GetParameterList();
 15110        var argumentList = method.GetArgumentList();
 111
 112        // Write breadcrumb for method
 15113        if (method.InterceptorTypeNames.Length > 0)
 114        {
 33115            var interceptorNames = string.Join(" → ", method.InterceptorTypeNames.Select(t => t.Split('.').Last()));
 15116            breadcrumbs.WriteInlineComment(builder, "    ", $"{method.Name}: {interceptorNames}");
 117        }
 118        else
 119        {
 0120            breadcrumbs.WriteInlineComment(builder, "    ", $"{method.Name}: direct forward (no interceptors)");
 121        }
 122
 15123        builder.AppendLine($"    public {method.ReturnType} {method.Name}({parameterList})");
 15124        builder.AppendLine("    {");
 125
 126        // If no interceptors, just forward directly to target
 15127        if (method.InterceptorTypeNames.Length == 0)
 128        {
 0129            if (method.IsVoid)
 130            {
 0131                builder.AppendLine($"        _target.{method.Name}({argumentList});");
 132            }
 133            else
 134            {
 0135                builder.AppendLine($"        return _target.{method.Name}({argumentList});");
 136            }
 0137            builder.AppendLine("    }");
 0138            builder.AppendLine();
 0139            return;
 140        }
 141
 142        // Build interceptor chain
 15143        var interceptorCount = method.InterceptorTypeNames.Length;
 15144        builder.AppendLine($"        var interceptors = new IMethodInterceptor[{interceptorCount}];");
 66145        for (var i = 0; i < interceptorCount; i++)
 146        {
 18147            builder.AppendLine($"        interceptors[{i}] = _serviceProvider.GetRequiredService<{method.InterceptorType
 148        }
 15149        builder.AppendLine();
 150
 151        // Create arguments array
 15152        if (method.Parameters.Count > 0)
 153        {
 24154            builder.AppendLine($"        var args = new object?[] {{ {string.Join(", ", method.Parameters.Select(p => p.
 155        }
 156        else
 157        {
 3158            builder.AppendLine("        var args = Array.Empty<object?>();");
 159        }
 15160        builder.AppendLine();
 161
 162        // Build the proceed chain - start from innermost (actual call) and wrap outward
 15163        builder.AppendLine("        // Build the interceptor chain from inside out");
 15164        builder.AppendLine("        Func<ValueTask<object?>> proceed = async () =>");
 15165        builder.AppendLine("        {");
 166
 15167        if (method.IsVoid)
 168        {
 3169            builder.AppendLine($"            _target.{method.Name}({argumentList});");
 3170            builder.AppendLine("            return null;");
 171        }
 12172        else if (method.IsAsync)
 173        {
 174            // Check if the return type is Task<T> or ValueTask<T> (has a result)
 1175            var hasResult = !method.ReturnType.Equals("global::System.Threading.Tasks.Task", StringComparison.Ordinal) &
 1176                           !method.ReturnType.Equals("global::System.Threading.Tasks.ValueTask", StringComparison.Ordina
 177
 1178            if (hasResult)
 179            {
 1180                builder.AppendLine($"            var result = await _target.{method.Name}({argumentList});");
 1181                builder.AppendLine("            return result;");
 182            }
 183            else
 184            {
 0185                builder.AppendLine($"            await _target.{method.Name}({argumentList});");
 0186                builder.AppendLine("            return null;");
 187            }
 188        }
 189        else
 190        {
 11191            builder.AppendLine($"            var result = _target.{method.Name}({argumentList});");
 11192            builder.AppendLine("            return result;");
 193        }
 194
 15195        builder.AppendLine("        };");
 15196        builder.AppendLine();
 197
 198        // Wrap each interceptor, from last to first (so first interceptor is outermost)
 15199        builder.AppendLine("        for (var i = interceptors.Length - 1; i >= 0; i--)");
 15200        builder.AppendLine("        {");
 15201        builder.AppendLine("            var interceptor = interceptors[i];");
 15202        builder.AppendLine("            var nextProceed = proceed;");
 15203        builder.AppendLine($"            proceed = () => interceptor.InterceptAsync(new MethodInvocation(_target, _metho
 15204        builder.AppendLine("        }");
 15205        builder.AppendLine();
 206
 207        // Invoke the chain and return the result
 15208        if (method.IsVoid)
 209        {
 3210            builder.AppendLine("        proceed().AsTask().GetAwaiter().GetResult();");
 211        }
 12212        else if (method.IsAsync)
 213        {
 1214            var hasResult = !method.ReturnType.Equals("global::System.Threading.Tasks.Task", StringComparison.Ordinal) &
 1215                           !method.ReturnType.Equals("global::System.Threading.Tasks.ValueTask", StringComparison.Ordina
 216
 1217            if (hasResult)
 218            {
 219                // Extract the inner type from Task<T> or ValueTask<T>
 1220                var innerType = GeneratorHelpers.ExtractGenericTypeArgument(method.ReturnType);
 1221                if (method.ReturnType.StartsWith("global::System.Threading.Tasks.ValueTask<", StringComparison.Ordinal))
 222                {
 0223                    builder.AppendLine($"        return new {method.ReturnType}(proceed().AsTask().ContinueWith(t => ({i
 224                }
 225                else
 226                {
 227                    // Task<T>
 1228                    builder.AppendLine($"        return proceed().AsTask().ContinueWith(t => ({innerType})t.Result!);");
 229                }
 230            }
 231            else
 232            {
 233                // Task or ValueTask without result
 0234                if (method.ReturnType.StartsWith("global::System.Threading.Tasks.ValueTask", StringComparison.Ordinal))
 235                {
 0236                    builder.AppendLine("        return new global::System.Threading.Tasks.ValueTask(proceed().AsTask());
 237                }
 238                else
 239                {
 0240                    builder.AppendLine("        return proceed().AsTask();");
 241                }
 242            }
 243        }
 244        else
 245        {
 246            // Synchronous with return value
 11247            builder.AppendLine($"        return ({method.ReturnType})proceed().AsTask().GetAwaiter().GetResult()!;");
 248        }
 249
 15250        builder.AppendLine("    }");
 15251        builder.AppendLine();
 15252    }
 253
 254    /// <summary>
 255    /// Generates the complete InterceptorProxies.g.cs source file containing
 256    /// proxy classes and the InterceptorRegistrations helper.
 257    /// </summary>
 258    internal static string GenerateInterceptorProxiesSource(IReadOnlyList<DiscoveredInterceptedService> interceptedServi
 259    {
 15260        var builder = new StringBuilder();
 15261        var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName);
 262
 15263        breadcrumbs.WriteFileHeader(builder, assemblyName, "Needlr Interceptor Proxies");
 15264        builder.AppendLine("#nullable enable");
 15265        builder.AppendLine();
 15266        builder.AppendLine("using System;");
 15267        builder.AppendLine("using System.Reflection;");
 15268        builder.AppendLine("using System.Threading.Tasks;");
 15269        builder.AppendLine();
 15270        builder.AppendLine("using Microsoft.Extensions.DependencyInjection;");
 15271        builder.AppendLine();
 15272        builder.AppendLine("using NexusLabs.Needlr;");
 15273        builder.AppendLine();
 15274        builder.AppendLine($"namespace {safeAssemblyName}.Generated;");
 15275        builder.AppendLine();
 276
 277        // Generate each proxy class
 60278        foreach (var service in interceptedServices)
 279        {
 15280            GenerateInterceptorProxyClass(builder, service, breadcrumbs, projectDirectory);
 15281            builder.AppendLine();
 282        }
 283
 284        // Generate the registration helper
 15285        builder.AppendLine("/// <summary>");
 15286        builder.AppendLine("/// Helper class for registering intercepted services.");
 15287        builder.AppendLine("/// </summary>");
 15288        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 15289        builder.AppendLine("public static class InterceptorRegistrations");
 15290        builder.AppendLine("{");
 15291        builder.AppendLine("    /// <summary>");
 15292        builder.AppendLine("    /// Registers all intercepted services and their proxies.");
 15293        builder.AppendLine("    /// </summary>");
 15294        builder.AppendLine("    /// <param name=\"services\">The service collection to register to.</param>");
 15295        builder.AppendLine("    public static void RegisterInterceptedServices(IServiceCollection services)");
 15296        builder.AppendLine("    {");
 297
 60298        foreach (var service in interceptedServices)
 299        {
 15300            var proxyTypeName = GeneratorHelpers.GetProxyTypeName(service.TypeName);
 15301            var lifetime = service.Lifetime switch
 15302            {
 4303                GeneratorLifetime.Singleton => "Singleton",
 11304                GeneratorLifetime.Scoped => "Scoped",
 0305                GeneratorLifetime.Transient => "Transient",
 0306                _ => "Scoped"
 15307            };
 308
 309            // Register all interceptor types
 66310            foreach (var interceptorType in service.AllInterceptorTypeNames)
 311            {
 18312                breadcrumbs.WriteInlineComment(builder, "        ", $"Register interceptor: {interceptorType.Split('.').
 18313                builder.AppendLine($"        if (!services.Any(d => d.ServiceType == typeof({interceptorType})))");
 18314                builder.AppendLine($"            services.Add{lifetime}<{interceptorType}>();");
 315            }
 316
 317            // Register the actual implementation type
 15318            builder.AppendLine($"        // Register actual implementation");
 15319            builder.AppendLine($"        services.Add{lifetime}<{service.TypeName}>();");
 320
 321            // Register proxy for each interface
 60322            foreach (var iface in service.InterfaceNames)
 323            {
 15324                builder.AppendLine($"        // Register proxy for {iface}");
 15325                builder.AppendLine($"        services.Add{lifetime}<{iface}>(sp => new {proxyTypeName}(");
 15326                builder.AppendLine($"            sp.GetRequiredService<{service.TypeName}>(),");
 15327                builder.AppendLine($"            sp));");
 328            }
 329        }
 330
 15331        builder.AppendLine("    }");
 15332        builder.AppendLine();
 15333        builder.AppendLine("    /// <summary>");
 15334        builder.AppendLine("    /// Gets the number of intercepted services discovered at compile time.");
 15335        builder.AppendLine("    /// </summary>");
 15336        builder.AppendLine($"    public static int Count => {GeneratorHelpers.Literal(interceptedServices.Count)};");
 15337        builder.AppendLine("}");
 338
 15339        return builder.ToString();
 340    }
 341}
 342