< 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    {
 692069520        foreach (var attribute in typeSymbol.GetAttributes())
 21        {
 202091122            var attributeClass = attribute.AttributeClass;
 202091123            if (attributeClass == null)
 24                continue;
 25
 26            // Check for non-generic GenerateFactoryAttribute
 202091127            var name = attributeClass.Name;
 202091128            if (name == GenerateFactoryAttributeName)
 2729                return true;
 30
 202088431            var fullName = attributeClass.ToDisplayString();
 202088432            if (fullName == GenerateFactoryAttributeFullName)
 033                return true;
 34
 35            // Check for generic GenerateFactoryAttribute<T>
 202088436            if (attributeClass.IsGenericType)
 37            {
 3738                var originalDef = attributeClass.OriginalDefinition;
 3739                if (originalDef.Name == GenerateFactoryAttributeName ||
 3740                    originalDef.ToDisplayString().StartsWith(GenerateFactoryAttributeFullName + "<"))
 041                    return true;
 42            }
 43        }
 44
 143942345        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    {
 9055        foreach (var attribute in typeSymbol.GetAttributes())
 56        {
 2357            var attributeClass = attribute.AttributeClass;
 2358            if (attributeClass == null)
 59                continue;
 60
 2361            var name = attributeClass.Name;
 2362            var fullName = attributeClass.ToDisplayString();
 63
 2364            bool isFactoryAttribute = name == GenerateFactoryAttributeName ||
 2365                                      fullName == GenerateFactoryAttributeFullName ||
 2366                                      (attributeClass.IsGenericType &&
 2367                                       attributeClass.OriginalDefinition.Name == GenerateFactoryAttributeName);
 68
 2369            if (isFactoryAttribute)
 70            {
 71                // Check named argument "Mode"
 4872                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
 2182        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    {
 9092        foreach (var attribute in typeSymbol.GetAttributes())
 93        {
 2394            var attributeClass = attribute.AttributeClass;
 2395            if (attributeClass == null)
 96                continue;
 97
 98            // Check for generic GenerateFactoryAttribute<T>
 2399            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
 21114        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    {
 24124        var result = new List<FactoryConstructorInfo>();
 125
 102126        foreach (var ctor in typeSymbol.InstanceConstructors)
 127        {
 27128            if (ctor.IsStatic)
 129                continue;
 130
 27131            if (ctor.DeclaredAccessibility != Accessibility.Public)
 132                continue;
 133
 134            // Extract XML documentation for parameters
 27135            var paramDocs = GetConstructorParameterDocumentation(ctor);
 136
 27137            var injectableParams = new List<TypeDiscoveryHelper.ConstructorParameterInfo>();
 27138            var runtimeParams = new List<TypeDiscoveryHelper.ConstructorParameterInfo>();
 139
 168140            foreach (var param in ctor.Parameters)
 141            {
 57142                var typeName = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 57143                paramDocs.TryGetValue(param.Name, out var docComment);
 144
 57145                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                {
 32154                    var paramInfo = new TypeDiscoveryHelper.ConstructorParameterInfo(typeName, null, param.Name, docComm
 32155                    runtimeParams.Add(paramInfo);
 156                }
 157            }
 158
 159            // Only include constructors that have at least one runtime parameter
 160            // (otherwise normal registration works fine)
 27161            if (runtimeParams.Count > 0)
 162            {
 26163                result.Add(new FactoryConstructorInfo(
 26164                    injectableParams.ToArray(),
 26165                    runtimeParams.ToArray()));
 166            }
 167        }
 168
 24169        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    {
 27177        var result = new Dictionary<string, string>(StringComparer.Ordinal);
 178
 27179        var xmlDoc = constructor.GetDocumentationCommentXml();
 27180        if (string.IsNullOrWhiteSpace(xmlDoc))
 22181            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
 57242        if (typeSymbol.TypeKind == TypeKind.Interface)
 25243            return true;
 244
 32245        if (typeSymbol is INamedTypeSymbol namedType && namedType.IsAbstract)
 0246            return true;
 247
 248        // Common framework types are injectable
 32249        var fullName = typeSymbol.ToDisplayString();
 32250        if (fullName.StartsWith("Microsoft.Extensions.", StringComparison.Ordinal))
 0251            return true;
 252
 253        // Classes with [Singleton], [Scoped], or [Transient] are injectable
 32254        if (typeSymbol is INamedTypeSymbol classType)
 255        {
 220256            foreach (var attr in classType.GetAttributes())
 257            {
 78258                var attrName = attr.AttributeClass?.Name;
 78259                if (attrName is "SingletonAttribute" or "ScopedAttribute" or "TransientAttribute")
 0260                    return true;
 261            }
 262        }
 263
 32264        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        {
 26276            InjectableParameters = injectableParameters;
 26277            RuntimeParameters = runtimeParameters;
 26278        }
 279
 280        /// <summary>Parameters that can be resolved from the service provider.</summary>
 75281        public TypeDiscoveryHelper.ConstructorParameterInfo[] InjectableParameters { get; }
 282
 283        /// <summary>Parameters that must be provided at factory call time.</summary>
 175284        public TypeDiscoveryHelper.ConstructorParameterInfo[] RuntimeParameters { get; }
 285    }
 286}