< 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    {
 1420        var proxyTypeName = GeneratorHelpers.GetProxyTypeName(service.TypeName);
 1421        var shortTypeName = GeneratorHelpers.GetShortTypeName(service.TypeName);
 22
 23        // Write verbose breadcrumb for interceptor proxy
 1424        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
 1458        builder.AppendLine("/// <summary>");
 1459        builder.AppendLine($"/// Interceptor proxy for {service.TypeName}.");
 1460        builder.AppendLine("/// Routes method calls through configured interceptors.");
 1461        builder.AppendLine("/// </summary>");
 1462        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 63
 64        // Implement all interfaces
 1465        builder.Append($"internal sealed class {proxyTypeName}");
 1466        if (service.InterfaceNames.Length > 0)
 67        {
 1468            builder.Append(" : ");
 1469            builder.Append(string.Join(", ", service.InterfaceNames));
 70        }
 1471        builder.AppendLine();
 1472        builder.AppendLine("{");
 73
 74        // Fields
 1475        builder.AppendLine($"    private readonly {service.TypeName} _target;");
 1476        builder.AppendLine("    private readonly IServiceProvider _serviceProvider;");
 1477        builder.AppendLine();
 78
 79        // Static MethodInfo fields for each method
 1480        var methodIndex = 0;
 5681        foreach (var method in service.Methods)
 82        {
 1483            builder.AppendLine($"    internal static readonly MethodInfo _method{methodIndex} = typeof({method.Interface
 1484            methodIndex++;
 85        }
 1486        builder.AppendLine();
 87
 88        // Constructor
 1489        builder.AppendLine($"    public {proxyTypeName}({service.TypeName} target, IServiceProvider serviceProvider)");
 1490        builder.AppendLine("    {");
 1491        builder.AppendLine("        _target = target ?? throw new ArgumentNullException(nameof(target));");
 1492        builder.AppendLine("        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(service
 1493        builder.AppendLine("    }");
 1494        builder.AppendLine();
 95
 96        // Generate each method
 1497        methodIndex = 0;
 5698        foreach (var method in service.Methods)
 99        {
 14100            GenerateInterceptedMethod(builder, method, methodIndex, service.TypeName, breadcrumbs);
 14101            methodIndex++;
 102        }
 103
 14104        builder.AppendLine("}");
 14105    }
 106
 107    internal static void GenerateInterceptedMethod(StringBuilder builder, InterceptorDiscoveryHelper.InterceptedMethodIn
 108    {
 14109        var parameterList = method.GetParameterList();
 14110        var argumentList = method.GetArgumentList();
 111
 112        // Write breadcrumb for method
 14113        if (method.InterceptorTypeNames.Length > 0)
 114        {
 31115            var interceptorNames = string.Join(" → ", method.InterceptorTypeNames.Select(t => t.Split('.').Last()));
 14116            breadcrumbs.WriteInlineComment(builder, "    ", $"{method.Name}: {interceptorNames}");
 117        }
 118        else
 119        {
 0120            breadcrumbs.WriteInlineComment(builder, "    ", $"{method.Name}: direct forward (no interceptors)");
 121        }
 122
 14123        builder.AppendLine($"    public {method.ReturnType} {method.Name}({parameterList})");
 14124        builder.AppendLine("    {");
 125
 126        // If no interceptors, just forward directly to target
 14127        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
 14143        var interceptorCount = method.InterceptorTypeNames.Length;
 14144        builder.AppendLine($"        var interceptors = new IMethodInterceptor[{interceptorCount}];");
 62145        for (var i = 0; i < interceptorCount; i++)
 146        {
 17147            builder.AppendLine($"        interceptors[{i}] = _serviceProvider.GetRequiredService<{method.InterceptorType
 148        }
 14149        builder.AppendLine();
 150
 151        // Create arguments array
 14152        if (method.Parameters.Count > 0)
 153        {
 24154            builder.AppendLine($"        var args = new object?[] {{ {string.Join(", ", method.Parameters.Select(p => p.
 155        }
 156        else
 157        {
 2158            builder.AppendLine("        var args = Array.Empty<object?>();");
 159        }
 14160        builder.AppendLine();
 161
 162        // Build the proceed chain - start from innermost (actual call) and wrap outward
 14163        builder.AppendLine("        // Build the interceptor chain from inside out");
 14164        builder.AppendLine("        Func<ValueTask<object?>> proceed = async () =>");
 14165        builder.AppendLine("        {");
 166
 14167        if (method.IsVoid)
 168        {
 2169            builder.AppendLine($"            _target.{method.Name}({argumentList});");
 2170            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
 14195        builder.AppendLine("        };");
 14196        builder.AppendLine();
 197
 198        // Wrap each interceptor, from last to first (so first interceptor is outermost)
 14199        builder.AppendLine("        for (var i = interceptors.Length - 1; i >= 0; i--)");
 14200        builder.AppendLine("        {");
 14201        builder.AppendLine("            var interceptor = interceptors[i];");
 14202        builder.AppendLine("            var nextProceed = proceed;");
 14203        builder.AppendLine($"            proceed = () => interceptor.InterceptAsync(new MethodInvocation(_target, _metho
 14204        builder.AppendLine("        }");
 14205        builder.AppendLine();
 206
 207        // Invoke the chain and return the result
 14208        if (method.IsVoid)
 209        {
 2210            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
 14250        builder.AppendLine("    }");
 14251        builder.AppendLine();
 14252    }
 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    {
 14260        var builder = new StringBuilder();
 14261        var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName);
 262
 14263        breadcrumbs.WriteFileHeader(builder, assemblyName, "Needlr Interceptor Proxies");
 14264        builder.AppendLine("#nullable enable");
 14265        builder.AppendLine();
 14266        builder.AppendLine("using System;");
 14267        builder.AppendLine("using System.Reflection;");
 14268        builder.AppendLine("using System.Threading.Tasks;");
 14269        builder.AppendLine();
 14270        builder.AppendLine("using Microsoft.Extensions.DependencyInjection;");
 14271        builder.AppendLine();
 14272        builder.AppendLine("using NexusLabs.Needlr;");
 14273        builder.AppendLine();
 14274        builder.AppendLine($"namespace {safeAssemblyName}.Generated;");
 14275        builder.AppendLine();
 276
 277        // Generate each proxy class
 56278        foreach (var service in interceptedServices)
 279        {
 14280            GenerateInterceptorProxyClass(builder, service, breadcrumbs, projectDirectory);
 14281            builder.AppendLine();
 282        }
 283
 284        // Generate the registration helper
 14285        builder.AppendLine("/// <summary>");
 14286        builder.AppendLine("/// Helper class for registering intercepted services.");
 14287        builder.AppendLine("/// </summary>");
 14288        builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1
 14289        builder.AppendLine("public static class InterceptorRegistrations");
 14290        builder.AppendLine("{");
 14291        builder.AppendLine("    /// <summary>");
 14292        builder.AppendLine("    /// Registers all intercepted services and their proxies.");
 14293        builder.AppendLine("    /// </summary>");
 14294        builder.AppendLine("    /// <param name=\"services\">The service collection to register to.</param>");
 14295        builder.AppendLine("    public static void RegisterInterceptedServices(IServiceCollection services)");
 14296        builder.AppendLine("    {");
 297
 56298        foreach (var service in interceptedServices)
 299        {
 14300            var proxyTypeName = GeneratorHelpers.GetProxyTypeName(service.TypeName);
 14301            var lifetime = service.Lifetime switch
 14302            {
 3303                GeneratorLifetime.Singleton => "Singleton",
 11304                GeneratorLifetime.Scoped => "Scoped",
 0305                GeneratorLifetime.Transient => "Transient",
 0306                _ => "Scoped"
 14307            };
 308
 309            // Register all interceptor types
 62310            foreach (var interceptorType in service.AllInterceptorTypeNames)
 311            {
 17312                breadcrumbs.WriteInlineComment(builder, "        ", $"Register interceptor: {interceptorType.Split('.').
 17313                builder.AppendLine($"        if (!services.Any(d => d.ServiceType == typeof({interceptorType})))");
 17314                builder.AppendLine($"            services.Add{lifetime}<{interceptorType}>();");
 315            }
 316
 317            // Register the actual implementation type
 14318            builder.AppendLine($"        // Register actual implementation");
 14319            builder.AppendLine($"        services.Add{lifetime}<{service.TypeName}>();");
 320
 321            // Register proxy for each interface
 56322            foreach (var iface in service.InterfaceNames)
 323            {
 14324                builder.AppendLine($"        // Register proxy for {iface}");
 14325                builder.AppendLine($"        services.Add{lifetime}<{iface}>(sp => new {proxyTypeName}(");
 14326                builder.AppendLine($"            sp.GetRequiredService<{service.TypeName}>(),");
 14327                builder.AppendLine($"            sp));");
 328            }
 329        }
 330
 14331        builder.AppendLine("    }");
 14332        builder.AppendLine();
 14333        builder.AppendLine("    /// <summary>");
 14334        builder.AppendLine("    /// Gets the number of intercepted services discovered at compile time.");
 14335        builder.AppendLine("    /// </summary>");
 14336        builder.AppendLine($"    public static int Count => {interceptedServices.Count};");
 14337        builder.AppendLine("}");
 338
 14339        return builder.ToString();
 340    }
 341}
 342