< Summary

Information
Class: NexusLabs.Needlr.Generators.FactoryDiscoveryHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/FactoryDiscoveryHelper.cs
Line coverage
85%
Covered lines: 89
Uncovered lines: 15
Coverable lines: 104
Total lines: 286
Line coverage: 85.5%
Branch coverage
79%
Covered branches: 78
Total branches: 98
Branch coverage: 79.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
HasGenerateFactoryAttribute(...)78.57%141486.66%
GetFactoryGenerationMode(...)77.77%1818100%
GetFactoryReturnInterfaceType(...)100%1010100%
GetFactoryConstructors(...)100%1212100%
GetConstructorParameterDocumentation(...)91.66%121287.5%
GetFromKeyedServicesKey(...)50%611020%
IsInjectableParameterType(...)68.18%282276.92%
.ctor(...)100%11100%
get_InjectableParameters()100%11100%
get_RuntimeParameters()100%11100%

File(s)

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

#LineLine coverage
 1using Microsoft.CodeAnalysis;
 2
 3namespace NexusLabs.Needlr.Generators;
 4
 5/// <summary>
 6/// Helper for discovering factory generation attributes from Roslyn symbols.
 7/// </summary>
 8internal static class FactoryDiscoveryHelper
 9{
 10    private const string GenerateFactoryAttributeName = "GenerateFactoryAttribute";
 11    private const string GenerateFactoryAttributeFullName = "NexusLabs.Needlr.GenerateFactoryAttribute";
 12
 13    /// <summary>
 14    /// Checks if a type has the [GenerateFactory] attribute.
 15    /// </summary>
 16    /// <param name="typeSymbol">The type symbol to check.</param>
 17    /// <returns>True if the type has [GenerateFactory]; otherwise, false.</returns>
 18    public static bool HasGenerateFactoryAttribute(INamedTypeSymbol typeSymbol)
 19    {
 682835820        foreach (var attribute in typeSymbol.GetAttributes())
 21        {
 199386822            var attributeClass = attribute.AttributeClass;
 199386823            if (attributeClass == null)
 24                continue;
 25
 26            // Check for non-generic GenerateFactoryAttribute
 199386827            var name = attributeClass.Name;
 199386828            if (name == GenerateFactoryAttributeName)
 3029                return true;
 30
 199383831            var fullName = attributeClass.ToDisplayString();
 199383832            if (fullName == GenerateFactoryAttributeFullName)
 033                return true;
 34
 35            // Check for generic GenerateFactoryAttribute<T>
 199383836            if (attributeClass.IsGenericType)
 37            {
 4038                var originalDef = attributeClass.OriginalDefinition;
 4039                if (originalDef.Name == GenerateFactoryAttributeName ||
 4040                    originalDef.ToDisplayString().StartsWith(GenerateFactoryAttributeFullName + "<"))
 041                    return true;
 42            }
 43        }
 44
 142029645        return false;
 46    }
 47
 48    /// <summary>
 49    /// Gets the factory generation mode from the [GenerateFactory] attribute.
 50    /// </summary>
 51    /// <param name="typeSymbol">The type symbol to check.</param>
 52    /// <returns>The Mode value (1=Func, 2=Interface, 3=All), or 3 (All) if not specified.</returns>
 53    public static int GetFactoryGenerationMode(INamedTypeSymbol typeSymbol)
 54    {
 10255        foreach (var attribute in typeSymbol.GetAttributes())
 56        {
 2657            var attributeClass = attribute.AttributeClass;
 2658            if (attributeClass == null)
 59                continue;
 60
 2661            var name = attributeClass.Name;
 2662            var fullName = attributeClass.ToDisplayString();
 63
 2664            bool isFactoryAttribute = name == GenerateFactoryAttributeName ||
 2665                                      fullName == GenerateFactoryAttributeFullName ||
 2666                                      (attributeClass.IsGenericType &&
 2667                                       attributeClass.OriginalDefinition.Name == GenerateFactoryAttributeName);
 68
 2669            if (isFactoryAttribute)
 70            {
 71                // Check named argument "Mode"
 5472                foreach (var namedArg in attribute.NamedArguments)
 73                {
 274                    if (namedArg.Key == "Mode" && namedArg.Value.Value is int modeValue)
 75                    {
 276                        return modeValue;
 77                    }
 78                }
 79            }
 80        }
 81
 2482        return 3; // Default is All (Func | Interface)
 83    }
 84
 85    /// <summary>
 86    /// Gets the interface type from a generic [GenerateFactory&lt;T&gt;] attribute, if present.
 87    /// </summary>
 88    /// <param name="typeSymbol">The type symbol to check.</param>
 89    /// <returns>The fully qualified interface type name, or null if non-generic attribute is used.</returns>
 90    public static string? GetFactoryReturnInterfaceType(INamedTypeSymbol typeSymbol)
 91    {
 10292        foreach (var attribute in typeSymbol.GetAttributes())
 93        {
 2694            var attributeClass = attribute.AttributeClass;
 2695            if (attributeClass == null)
 96                continue;
 97
 98            // Check for generic GenerateFactoryAttribute<T>
 2699            if (attributeClass.IsGenericType)
 100            {
 2101                var originalDef = attributeClass.OriginalDefinition;
 2102                if (originalDef.Name == GenerateFactoryAttributeName)
 103                {
 104                    // Extract the type argument
 2105                    var typeArg = attributeClass.TypeArguments.FirstOrDefault();
 2106                    if (typeArg != null)
 107                    {
 2108                        return $"global::{typeArg.ToDisplayString()}";
 109                    }
 110                }
 111            }
 112        }
 113
 24114        return null;
 115    }
 116
 117    /// <summary>
 118    /// Partitions constructor parameters into injectable (DI-resolvable) and runtime (must be provided).
 119    /// </summary>
 120    /// <param name="typeSymbol">The type symbol to analyze.</param>
 121    /// <returns>A list of factory constructor infos, one for each viable constructor.</returns>
 122    public static IReadOnlyList<FactoryConstructorInfo> GetFactoryConstructors(INamedTypeSymbol typeSymbol)
 123    {
 27124        var result = new List<FactoryConstructorInfo>();
 125
 114126        foreach (var ctor in typeSymbol.InstanceConstructors)
 127        {
 30128            if (ctor.IsStatic)
 129                continue;
 130
 30131            if (ctor.DeclaredAccessibility != Accessibility.Public)
 132                continue;
 133
 134            // Extract XML documentation for parameters
 30135            var paramDocs = GetConstructorParameterDocumentation(ctor);
 136
 30137            var injectableParams = new List<TypeDiscoveryHelper.ConstructorParameterInfo>();
 30138            var runtimeParams = new List<TypeDiscoveryHelper.ConstructorParameterInfo>();
 139
 180140            foreach (var param in ctor.Parameters)
 141            {
 60142                var typeName = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 60143                paramDocs.TryGetValue(param.Name, out var docComment);
 144
 60145                if (IsInjectableParameterType(param.Type))
 146                {
 147                    // Check for [FromKeyedServices] attribute
 25148                    var serviceKey = GetFromKeyedServicesKey(param);
 25149                    var paramInfo = new TypeDiscoveryHelper.ConstructorParameterInfo(typeName, serviceKey, param.Name, d
 25150                    injectableParams.Add(paramInfo);
 151                }
 152                else
 153                {
 35154                    var paramInfo = new TypeDiscoveryHelper.ConstructorParameterInfo(typeName, null, param.Name, docComm
 35155                    runtimeParams.Add(paramInfo);
 156                }
 157            }
 158
 159            // Only include constructors that have at least one runtime parameter
 160            // (otherwise normal registration works fine)
 30161            if (runtimeParams.Count > 0)
 162            {
 29163                result.Add(new FactoryConstructorInfo(
 29164                    injectableParams.ToArray(),
 29165                    runtimeParams.ToArray()));
 166            }
 167        }
 168
 27169        return result;
 170    }
 171
 172    /// <summary>
 173    /// Extracts parameter documentation from a constructor's XML documentation comments.
 174    /// </summary>
 175    private static Dictionary<string, string> GetConstructorParameterDocumentation(IMethodSymbol constructor)
 176    {
 30177        var result = new Dictionary<string, string>(StringComparer.Ordinal);
 178
 30179        var xmlDoc = constructor.GetDocumentationCommentXml();
 30180        if (string.IsNullOrWhiteSpace(xmlDoc))
 25181            return result;
 182
 183        try
 184        {
 185            // Parse the XML documentation
 5186            var doc = System.Xml.Linq.XDocument.Parse(xmlDoc);
 5187            var paramElements = doc.Descendants("param");
 188
 32189            foreach (var param in paramElements)
 190            {
 11191                var nameAttr = param.Attribute("name");
 11192                if (nameAttr is null || string.IsNullOrWhiteSpace(nameAttr.Value))
 193                    continue;
 194
 195                // Get the inner text/content of the param element
 11196                var content = param.Value?.Trim();
 11197                if (!string.IsNullOrWhiteSpace(content))
 198                {
 11199                    result[nameAttr.Value] = content!;
 200                }
 201            }
 5202        }
 0203        catch
 204        {
 205            // If XML parsing fails, return empty dictionary
 0206        }
 207
 5208        return result;
 209    }
 210
 211    private static string? GetFromKeyedServicesKey(IParameterSymbol param)
 212    {
 213        const string FromKeyedServicesAttributeName = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribu
 214
 50215        foreach (var attr in param.GetAttributes())
 216        {
 0217            var attrClass = attr.AttributeClass;
 0218            if (attrClass is null)
 219                continue;
 220
 0221            var attrFullName = attrClass.ToDisplayString();
 0222            if (attrFullName == FromKeyedServicesAttributeName)
 223            {
 224                // Extract the key from the constructor argument
 0225                if (attr.ConstructorArguments.Length > 0)
 226                {
 0227                    var keyArg = attr.ConstructorArguments[0];
 0228                    if (keyArg.Value is string keyValue)
 229                    {
 0230                        return keyValue;
 231                    }
 232                }
 233            }
 234        }
 235
 25236        return null;
 237    }
 238
 239    private static bool IsInjectableParameterType(ITypeSymbol typeSymbol)
 240    {
 241        // Interfaces and abstract classes are typically injectable
 60242        if (typeSymbol.TypeKind == TypeKind.Interface)
 25243            return true;
 244
 35245        if (typeSymbol is INamedTypeSymbol namedType && namedType.IsAbstract)
 0246            return true;
 247
 248        // Common framework types are injectable
 35249        var fullName = typeSymbol.ToDisplayString();
 35250        if (fullName.StartsWith("Microsoft.Extensions.", StringComparison.Ordinal))
 0251            return true;
 252
 253        // Classes with [Singleton], [Scoped], or [Transient] are injectable
 35254        if (typeSymbol is INamedTypeSymbol classType)
 255        {
 244256            foreach (var attr in classType.GetAttributes())
 257            {
 87258                var attrName = attr.AttributeClass?.Name;
 87259                if (attrName is "SingletonAttribute" or "ScopedAttribute" or "TransientAttribute")
 0260                    return true;
 261            }
 262        }
 263
 35264        return false;
 265    }
 266
 267    /// <summary>
 268    /// Represents a constructor suitable for factory generation.
 269    /// </summary>
 270    public readonly struct FactoryConstructorInfo
 271    {
 272        public FactoryConstructorInfo(
 273            TypeDiscoveryHelper.ConstructorParameterInfo[] injectableParameters,
 274            TypeDiscoveryHelper.ConstructorParameterInfo[] runtimeParameters)
 275        {
 29276            InjectableParameters = injectableParameters;
 29277            RuntimeParameters = runtimeParameters;
 29278        }
 279
 280        /// <summary>Parameters that can be resolved from the service provider.</summary>
 84281        public TypeDiscoveryHelper.ConstructorParameterInfo[] InjectableParameters { get; }
 282
 283        /// <summary>Parameters that must be provided at factory call time.</summary>
 196284        public TypeDiscoveryHelper.ConstructorParameterInfo[] RuntimeParameters { get; }
 285    }
 286}