< Summary

Information
Class: NexusLabs.Needlr.Generators.InterceptorDiscoveryHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/InterceptorDiscoveryHelper.cs
Line coverage
86%
Covered lines: 116
Uncovered lines: 18
Coverable lines: 134
Total lines: 342
Line coverage: 86.5%
Branch coverage
80%
Covered branches: 68
Total branches: 85
Branch coverage: 80%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_InterceptorTypeName()100%11100%
get_Order()100%11100%
get_IsMethodLevel()100%210%
get_MethodName()100%210%
GetInterceptAttributes(...)100%44100%
GetMethodLevelInterceptAttributes(...)80%191054.54%
HasInterceptAttributes(...)91.66%121288.88%
GetInterceptedMethods(...)85.71%141490.9%
TryGetInterceptorType(...)90%202092.85%
GetInterceptorOrder(...)100%66100%
GetMethodReturnType(...)100%11100%
GetMethodParameters(...)100%22100%
IsTaskOrValueTask(...)100%22100%
IsSystemInterface(...)50%66100%
.ctor(...)100%11100%
get_Name()100%11100%
get_TypeName()100%11100%
get_RefKind()100%11100%
GetDeclaration()20%7555.55%
.ctor(...)100%11100%
get_Name()100%11100%
get_ReturnType()100%11100%
get_Parameters()100%11100%
get_InterfaceTypeName()100%11100%
get_IsAsync()100%11100%
get_IsVoid()100%11100%
get_InterceptorTypeNames()100%11100%
GetParameterList()100%11100%
GetArgumentList()25%4472.72%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/InterceptorDiscoveryHelper.cs

#LineLine coverage
 1using Microsoft.CodeAnalysis;
 2
 3namespace NexusLabs.Needlr.Generators;
 4
 5/// <summary>
 6/// Helper for discovering interceptor attributes from Roslyn symbols.
 7/// </summary>
 8internal static class InterceptorDiscoveryHelper
 9{
 10    private const string InterceptAttributePrefix = "NexusLabs.Needlr.InterceptAttribute";
 11
 12    /// <summary>
 13    /// Result of interceptor discovery for a type.
 14    /// </summary>
 15    public readonly struct InterceptorInfo
 16    {
 17        public InterceptorInfo(
 18            string interceptorTypeName,
 19            int order,
 20            bool isMethodLevel,
 21            string? methodName)
 22        {
 1723            InterceptorTypeName = interceptorTypeName;
 1724            Order = order;
 1725            IsMethodLevel = isMethodLevel;
 1726            MethodName = methodName;
 1727        }
 28
 29        /// <summary>Fully qualified name of the interceptor type.</summary>
 3430        public string InterceptorTypeName { get; }
 31
 32        /// <summary>Order in which interceptor executes (lower = first).</summary>
 1733        public int Order { get; }
 34
 35        /// <summary>True if this interceptor is applied at method level, false for class level.</summary>
 036        public bool IsMethodLevel { get; }
 37
 38        /// <summary>Method name if IsMethodLevel is true, null otherwise.</summary>
 039        public string? MethodName { get; }
 40    }
 41
 42    /// <summary>
 43    /// Gets all Intercept attributes applied to a type (class-level only).
 44    /// </summary>
 45    /// <param name="typeSymbol">The type symbol to check.</param>
 46    /// <returns>A list of interceptor info for each Intercept attribute found.</returns>
 47    public static IReadOnlyList<InterceptorInfo> GetInterceptAttributes(INamedTypeSymbol typeSymbol)
 48    {
 1449        var result = new List<InterceptorInfo>();
 50
 8451        foreach (var attribute in typeSymbol.GetAttributes())
 52        {
 2853            var interceptorType = TryGetInterceptorType(attribute);
 2854            if (interceptorType is null)
 55                continue;
 56
 1757            var order = GetInterceptorOrder(attribute);
 1758            var interceptorTypeName = TypeDiscoveryHelper.GetFullyQualifiedName(interceptorType);
 59
 1760            result.Add(new InterceptorInfo(interceptorTypeName, order, isMethodLevel: false, methodName: null));
 61        }
 62
 1463        return result;
 64    }
 65
 66    /// <summary>
 67    /// Gets all Intercept attributes applied to methods of a type.
 68    /// </summary>
 69    /// <param name="typeSymbol">The type symbol to check.</param>
 70    /// <returns>A list of interceptor info for each method-level Intercept attribute found.</returns>
 71    public static IReadOnlyList<InterceptorInfo> GetMethodLevelInterceptAttributes(INamedTypeSymbol typeSymbol)
 72    {
 1473        var result = new List<InterceptorInfo>();
 74
 8475        foreach (var member in typeSymbol.GetMembers())
 76        {
 2877            if (member is not IMethodSymbol methodSymbol)
 78                continue;
 79
 2880            if (methodSymbol.MethodKind != MethodKind.Ordinary)
 81                continue;
 82
 2883            foreach (var attribute in methodSymbol.GetAttributes())
 84            {
 085                var interceptorType = TryGetInterceptorType(attribute);
 086                if (interceptorType is null)
 87                    continue;
 88
 089                var order = GetInterceptorOrder(attribute);
 090                var interceptorTypeName = TypeDiscoveryHelper.GetFullyQualifiedName(interceptorType);
 91
 092                result.Add(new InterceptorInfo(interceptorTypeName, order, isMethodLevel: true, methodName: methodSymbol
 93            }
 94        }
 95
 1496        return result;
 97    }
 98
 99    /// <summary>
 100    /// Checks if a type has any Intercept attributes (class or method level).
 101    /// </summary>
 102    /// <param name="typeSymbol">The type symbol to check.</param>
 103    /// <returns>True if the type has at least one Intercept attribute.</returns>
 104    public static bool HasInterceptAttributes(INamedTypeSymbol typeSymbol)
 105    {
 106        // Check class-level
 7002842107        foreach (var attribute in typeSymbol.GetAttributes())
 108        {
 2044105109            if (TryGetInterceptorType(attribute) is not null)
 18110                return true;
 111        }
 112
 113        // Check method-level
 55513218114        foreach (var member in typeSymbol.GetMembers())
 115        {
 26299302116            if (member is not IMethodSymbol methodSymbol)
 117                continue;
 118
 48999786119            foreach (var attribute in methodSymbol.GetAttributes())
 120            {
 4452482121                if (TryGetInterceptorType(attribute) is not null)
 0122                    return true;
 123            }
 124        }
 125
 1457307126        return false;
 127    }
 128
 129    /// <summary>
 130    /// Gets the interface methods that need to be proxied for interception.
 131    /// </summary>
 132    /// <param name="typeSymbol">The type symbol implementing the interface(s).</param>
 133    /// <param name="classLevelInterceptors">Interceptors applied at class level.</param>
 134    /// <param name="methodLevelInterceptors">Interceptors applied at method level.</param>
 135    /// <returns>A list of method infos that need to be generated in the proxy.</returns>
 136    public static IReadOnlyList<InterceptedMethodInfo> GetInterceptedMethods(
 137        INamedTypeSymbol typeSymbol,
 138        IReadOnlyList<InterceptorInfo> classLevelInterceptors,
 139        IReadOnlyList<InterceptorInfo> methodLevelInterceptors)
 140    {
 14141        var result = new List<InterceptedMethodInfo>();
 14142        var methodNameToInterceptors = methodLevelInterceptors
 0143            .GroupBy(i => i.MethodName!)
 14144            .ToDictionary(g => g.Key, g => g.ToList());
 145
 56146        foreach (var iface in typeSymbol.AllInterfaces)
 147        {
 14148            if (IsSystemInterface(iface))
 149                continue;
 150
 56151            foreach (var member in iface.GetMembers())
 152            {
 14153                if (member is not IMethodSymbol methodSymbol)
 154                    continue;
 155
 14156                if (methodSymbol.MethodKind != MethodKind.Ordinary)
 157                    continue;
 158
 159                // Combine class-level and method-level interceptors for this method
 14160                var interceptors = new List<InterceptorInfo>(classLevelInterceptors);
 14161                if (methodNameToInterceptors.TryGetValue(methodSymbol.Name, out var methodInterceptors))
 162                {
 0163                    interceptors.AddRange(methodInterceptors);
 164                }
 165
 166                // Always include the method - even if no interceptors, the proxy needs to forward the call
 14167                var methodInfo = new InterceptedMethodInfo(
 14168                    methodSymbol.Name,
 14169                    GetMethodReturnType(methodSymbol),
 14170                    GetMethodParameters(methodSymbol),
 14171                    TypeDiscoveryHelper.GetFullyQualifiedName(iface),
 14172                    methodSymbol.IsAsync || IsTaskOrValueTask(methodSymbol.ReturnType),
 14173                    methodSymbol.ReturnsVoid,
 48174                    interceptors.OrderBy(i => i.Order).Select(i => i.InterceptorTypeName).ToArray());
 175
 14176                result.Add(methodInfo);
 177            }
 178        }
 179
 14180        return result;
 181    }
 182
 183    private static INamedTypeSymbol? TryGetInterceptorType(AttributeData attribute)
 184    {
 6496615185        var attrClass = attribute.AttributeClass;
 6496615186        if (attrClass is null)
 0187            return null;
 188
 189        // Check generic InterceptAttribute<T>
 6496615190        if (attrClass.IsGenericType)
 191        {
 54192            var unboundTypeName = attrClass.ConstructedFrom?.ToDisplayString();
 54193            if (unboundTypeName is not null && unboundTypeName.StartsWith(InterceptAttributePrefix, StringComparison.Ord
 194            {
 31195                if (attrClass.TypeArguments.Length == 1 && attrClass.TypeArguments[0] is INamedTypeSymbol interceptorTyp
 196                {
 31197                    return interceptorType;
 198                }
 199            }
 200        }
 201
 202        // Check non-generic InterceptAttribute(typeof(T))
 6496584203        var attrClassName = attrClass.ToDisplayString();
 6496584204        if (attrClassName == InterceptAttributePrefix)
 205        {
 206            // Get from constructor argument
 4207            if (attribute.ConstructorArguments.Length > 0 &&
 4208                attribute.ConstructorArguments[0].Value is INamedTypeSymbol ctorInterceptorType)
 209            {
 4210                return ctorInterceptorType;
 211            }
 212        }
 213
 6496580214        return null;
 215    }
 216
 217    private static int GetInterceptorOrder(AttributeData attribute)
 218    {
 39219        foreach (var namedArg in attribute.NamedArguments)
 220        {
 5221            if (namedArg.Key == "Order" && namedArg.Value.Value is int orderValue)
 222            {
 5223                return orderValue;
 224            }
 225        }
 12226        return 0;
 227    }
 228
 229    private static string GetMethodReturnType(IMethodSymbol methodSymbol)
 230    {
 14231        return methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 232    }
 233
 234    private static IReadOnlyList<MethodParameterInfo> GetMethodParameters(IMethodSymbol methodSymbol)
 235    {
 14236        var result = new List<MethodParameterInfo>();
 52237        foreach (var param in methodSymbol.Parameters)
 238        {
 12239            result.Add(new MethodParameterInfo(
 12240                param.Name,
 12241                param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
 12242                param.RefKind));
 243        }
 14244        return result;
 245    }
 246
 247    private static bool IsTaskOrValueTask(ITypeSymbol typeSymbol)
 248    {
 14249        var name = typeSymbol.ToDisplayString();
 14250        return name.StartsWith("System.Threading.Tasks.Task", StringComparison.Ordinal) ||
 14251               name.StartsWith("System.Threading.Tasks.ValueTask", StringComparison.Ordinal);
 252    }
 253
 254    private static bool IsSystemInterface(INamedTypeSymbol interfaceSymbol)
 255    {
 14256        var ns = interfaceSymbol.ContainingNamespace?.ToDisplayString();
 14257        return ns is not null && (ns.StartsWith("System", StringComparison.Ordinal) ||
 14258                                  ns.StartsWith("Microsoft", StringComparison.Ordinal));
 259    }
 260
 261    /// <summary>
 262    /// Represents a method parameter for code generation.
 263    /// </summary>
 264    public readonly struct MethodParameterInfo
 265    {
 266        public MethodParameterInfo(string name, string typeName, RefKind refKind)
 267        {
 12268            Name = name;
 12269            TypeName = typeName;
 12270            RefKind = refKind;
 12271        }
 272
 36273        public string Name { get; }
 12274        public string TypeName { get; }
 24275        public RefKind RefKind { get; }
 276
 277        public string GetDeclaration()
 278        {
 12279            var refPrefix = RefKind switch
 12280            {
 0281                RefKind.Ref => "ref ",
 0282                RefKind.Out => "out ",
 0283                RefKind.In => "in ",
 0284                RefKind.RefReadOnlyParameter => "ref readonly ",
 12285                _ => ""
 12286            };
 12287            return $"{refPrefix}{TypeName} {Name}";
 288        }
 289    }
 290
 291    /// <summary>
 292    /// Represents a method that needs to be intercepted.
 293    /// </summary>
 294    public readonly struct InterceptedMethodInfo
 295    {
 296        public InterceptedMethodInfo(
 297            string name,
 298            string returnType,
 299            IReadOnlyList<MethodParameterInfo> parameters,
 300            string interfaceTypeName,
 301            bool isAsync,
 302            bool isVoid,
 303            string[] interceptorTypeNames)
 304        {
 14305            Name = name;
 14306            ReturnType = returnType;
 14307            Parameters = parameters;
 14308            InterfaceTypeName = interfaceTypeName;
 14309            IsAsync = isAsync;
 14310            IsVoid = isVoid;
 14311            InterceptorTypeNames = interceptorTypeNames;
 14312        }
 313
 59314        public string Name { get; }
 31315        public string ReturnType { get; }
 54316        public IReadOnlyList<MethodParameterInfo> Parameters { get; }
 28317        public string InterfaceTypeName { get; }
 24318        public bool IsAsync { get; }
 28319        public bool IsVoid { get; }
 79320        public string[] InterceptorTypeNames { get; }
 321
 322        public string GetParameterList()
 323        {
 26324            return string.Join(", ", Parameters.Select(p => p.GetDeclaration()));
 325        }
 326
 327        public string GetArgumentList()
 328        {
 14329            return string.Join(", ", Parameters.Select(p =>
 14330            {
 12331                var prefix = p.RefKind switch
 12332                {
 0333                    RefKind.Ref => "ref ",
 0334                    RefKind.Out => "out ",
 0335                    RefKind.In => "in ",
 12336                    _ => ""
 12337                };
 12338                return prefix + p.Name;
 14339            }));
 340        }
 341    }
 342}