< 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        {
 1823            InterceptorTypeName = interceptorTypeName;
 1824            Order = order;
 1825            IsMethodLevel = isMethodLevel;
 1826            MethodName = methodName;
 1827        }
 28
 29        /// <summary>Fully qualified name of the interceptor type.</summary>
 3630        public string InterceptorTypeName { get; }
 31
 32        /// <summary>Order in which interceptor executes (lower = first).</summary>
 1833        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    {
 1549        var result = new List<InterceptorInfo>();
 50
 9251        foreach (var attribute in typeSymbol.GetAttributes())
 52        {
 3153            var interceptorType = TryGetInterceptorType(attribute);
 3154            if (interceptorType is null)
 55                continue;
 56
 1857            var order = GetInterceptorOrder(attribute);
 1858            var interceptorTypeName = TypeDiscoveryHelper.GetFullyQualifiedName(interceptorType);
 59
 1860            result.Add(new InterceptorInfo(interceptorTypeName, order, isMethodLevel: false, methodName: null));
 61        }
 62
 1563        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    {
 1573        var result = new List<InterceptorInfo>();
 74
 9075        foreach (var member in typeSymbol.GetMembers())
 76        {
 3077            if (member is not IMethodSymbol methodSymbol)
 78                continue;
 79
 3080            if (methodSymbol.MethodKind != MethodKind.Ordinary)
 81                continue;
 82
 3083            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
 1596        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
 6828335107        foreach (var attribute in typeSymbol.GetAttributes())
 108        {
 1993831109            if (TryGetInterceptorType(attribute) is not null)
 19110                return true;
 111        }
 112
 113        // Check method-level
 54184780114        foreach (var member in typeSymbol.GetMembers())
 115        {
 25672063116            if (member is not IMethodSymbol methodSymbol)
 117                continue;
 118
 47820508119            foreach (var attribute in methodSymbol.GetAttributes())
 120            {
 4340138121                if (TryGetInterceptorType(attribute) is not null)
 0122                    return true;
 123            }
 124        }
 125
 1420327126        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    {
 15141        var result = new List<InterceptedMethodInfo>();
 15142        var methodNameToInterceptors = methodLevelInterceptors
 0143            .GroupBy(i => i.MethodName!)
 15144            .ToDictionary(g => g.Key, g => g.ToList());
 145
 60146        foreach (var iface in typeSymbol.AllInterfaces)
 147        {
 15148            if (IsSystemInterface(iface))
 149                continue;
 150
 60151            foreach (var member in iface.GetMembers())
 152            {
 15153                if (member is not IMethodSymbol methodSymbol)
 154                    continue;
 155
 15156                if (methodSymbol.MethodKind != MethodKind.Ordinary)
 157                    continue;
 158
 159                // Combine class-level and method-level interceptors for this method
 15160                var interceptors = new List<InterceptorInfo>(classLevelInterceptors);
 15161                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
 15167                var methodInfo = new InterceptedMethodInfo(
 15168                    methodSymbol.Name,
 15169                    GetMethodReturnType(methodSymbol),
 15170                    GetMethodParameters(methodSymbol),
 15171                    TypeDiscoveryHelper.GetFullyQualifiedName(iface),
 15172                    methodSymbol.IsAsync || IsTaskOrValueTask(methodSymbol.ReturnType),
 15173                    methodSymbol.ReturnsVoid,
 51174                    interceptors.OrderBy(i => i.Order).Select(i => i.InterceptorTypeName).ToArray());
 175
 15176                result.Add(methodInfo);
 177            }
 178        }
 179
 15180        return result;
 181    }
 182
 183    private static INamedTypeSymbol? TryGetInterceptorType(AttributeData attribute)
 184    {
 6334000185        var attrClass = attribute.AttributeClass;
 6334000186        if (attrClass is null)
 0187            return null;
 188
 189        // Check generic InterceptAttribute<T>
 6334000190        if (attrClass.IsGenericType)
 191        {
 57192            var unboundTypeName = attrClass.ConstructedFrom?.ToDisplayString();
 57193            if (unboundTypeName is not null && unboundTypeName.StartsWith(InterceptAttributePrefix, StringComparison.Ord
 194            {
 33195                if (attrClass.TypeArguments.Length == 1 && attrClass.TypeArguments[0] is INamedTypeSymbol interceptorTyp
 196                {
 33197                    return interceptorType;
 198                }
 199            }
 200        }
 201
 202        // Check non-generic InterceptAttribute(typeof(T))
 6333967203        var attrClassName = attrClass.ToDisplayString();
 6333967204        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
 6333963214        return null;
 215    }
 216
 217    private static int GetInterceptorOrder(AttributeData attribute)
 218    {
 41219        foreach (var namedArg in attribute.NamedArguments)
 220        {
 5221            if (namedArg.Key == "Order" && namedArg.Value.Value is int orderValue)
 222            {
 5223                return orderValue;
 224            }
 225        }
 13226        return 0;
 227    }
 228
 229    private static string GetMethodReturnType(IMethodSymbol methodSymbol)
 230    {
 15231        return methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 232    }
 233
 234    private static IReadOnlyList<MethodParameterInfo> GetMethodParameters(IMethodSymbol methodSymbol)
 235    {
 15236        var result = new List<MethodParameterInfo>();
 62237        foreach (var param in methodSymbol.Parameters)
 238        {
 16239            result.Add(new MethodParameterInfo(
 16240                param.Name,
 16241                param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
 16242                param.RefKind));
 243        }
 15244        return result;
 245    }
 246
 247    private static bool IsTaskOrValueTask(ITypeSymbol typeSymbol)
 248    {
 15249        var name = typeSymbol.ToDisplayString();
 15250        return name.StartsWith("System.Threading.Tasks.Task", StringComparison.Ordinal) ||
 15251               name.StartsWith("System.Threading.Tasks.ValueTask", StringComparison.Ordinal);
 252    }
 253
 254    private static bool IsSystemInterface(INamedTypeSymbol interfaceSymbol)
 255    {
 15256        var ns = interfaceSymbol.ContainingNamespace?.ToDisplayString();
 15257        return ns is not null && (ns.StartsWith("System", StringComparison.Ordinal) ||
 15258                                  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        {
 16268            Name = name;
 16269            TypeName = typeName;
 16270            RefKind = refKind;
 16271        }
 272
 48273        public string Name { get; }
 16274        public string TypeName { get; }
 32275        public RefKind RefKind { get; }
 276
 277        public string GetDeclaration()
 278        {
 16279            var refPrefix = RefKind switch
 16280            {
 0281                RefKind.Ref => "ref ",
 0282                RefKind.Out => "out ",
 0283                RefKind.In => "in ",
 0284                RefKind.RefReadOnlyParameter => "ref readonly ",
 16285                _ => ""
 16286            };
 16287            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        {
 15305            Name = name;
 15306            ReturnType = returnType;
 15307            Parameters = parameters;
 15308            InterfaceTypeName = interfaceTypeName;
 15309            IsAsync = isAsync;
 15310            IsVoid = isVoid;
 15311            InterceptorTypeNames = interceptorTypeNames;
 15312        }
 313
 63314        public string Name { get; }
 33315        public string ReturnType { get; }
 59316        public IReadOnlyList<MethodParameterInfo> Parameters { get; }
 30317        public string InterfaceTypeName { get; }
 26318        public bool IsAsync { get; }
 30319        public bool IsVoid { get; }
 84320        public string[] InterceptorTypeNames { get; }
 321
 322        public string GetParameterList()
 323        {
 31324            return string.Join(", ", Parameters.Select(p => p.GetDeclaration()));
 325        }
 326
 327        public string GetArgumentList()
 328        {
 15329            return string.Join(", ", Parameters.Select(p =>
 15330            {
 16331                var prefix = p.RefKind switch
 16332                {
 0333                    RefKind.Ref => "ref ",
 0334                    RefKind.Out => "out ",
 0335                    RefKind.In => "in ",
 16336                    _ => ""
 16337                };
 16338                return prefix + p.Name;
 15339            }));
 340        }
 341    }
 342}