< Summary

Information
Class: NexusLabs.Needlr.Generators.FactoryDiscoveryHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /actions-runner/_work/needlr/needlr/src/NexusLabs.Needlr.Generators/FactoryDiscoveryHelper.cs
Line coverage
87%
Covered lines: 107
Uncovered lines: 15
Coverable lines: 122
Total lines: 348
Line coverage: 87.7%
Branch coverage
84%
Covered branches: 91
Total branches: 108
Branch coverage: 84.2%
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(...)94.44%1818100%
GetFactoryReturnInterfaceType(...)100%1010100%
GetFactoryConstructors(...)100%1616100%
BuildGeneratedConstructorFactoryInfo(...)100%66100%
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)

/actions-runner/_work/needlr/needlr/src/NexusLabs.Needlr.Generators/FactoryDiscoveryHelper.cs

#LineLine coverage
 1using Microsoft.CodeAnalysis;
 2
 3using NexusLabs.Needlr.Generators.Models;
 4using NexusLabs.Needlr.Roslyn.Shared;
 5
 6namespace NexusLabs.Needlr.Generators;
 7
 8/// <summary>
 9/// Helper for discovering factory generation attributes from Roslyn symbols.
 10/// </summary>
 11internal static class FactoryDiscoveryHelper
 12{
 13    private const string GenerateFactoryAttributeName = "GenerateFactoryAttribute";
 14    private const string GenerateFactoryAttributeFullName = "NexusLabs.Needlr.GenerateFactoryAttribute";
 15
 16    /// <summary>
 17    /// Checks if a type has the [GenerateFactory] attribute.
 18    /// </summary>
 19    /// <param name="typeSymbol">The type symbol to check.</param>
 20    /// <returns>True if the type has [GenerateFactory]; otherwise, false.</returns>
 21    public static bool HasGenerateFactoryAttribute(INamedTypeSymbol typeSymbol)
 22    {
 741960723        foreach (var attribute in typeSymbol.GetAttributes())
 24        {
 216568825            var attributeClass = attribute.AttributeClass;
 216568826            if (attributeClass == null)
 27                continue;
 28
 29            // Check for non-generic GenerateFactoryAttribute
 216568830            var name = attributeClass.Name;
 216568831            if (name == GenerateFactoryAttributeName)
 3332                return true;
 33
 216565534            var fullName = attributeClass.ToDisplayString();
 216565535            if (fullName == GenerateFactoryAttributeFullName)
 036                return true;
 37
 38            // Check for generic GenerateFactoryAttribute<T>
 216565539            if (attributeClass.IsGenericType)
 40            {
 3841                var originalDef = attributeClass.OriginalDefinition;
 3842                if (originalDef.Name == GenerateFactoryAttributeName ||
 3843                    originalDef.ToDisplayString().StartsWith(GenerateFactoryAttributeFullName + "<"))
 044                    return true;
 45            }
 46        }
 47
 154409948        return false;
 49    }
 50
 51    /// <summary>
 52    /// Gets the factory generation mode from the [GenerateFactory] attribute.
 53    /// </summary>
 54    /// <param name="typeSymbol">The type symbol to check.</param>
 55    /// <returns>The Mode value (1=Func, 2=Interface, 3=All), or 3 (All) if not specified.</returns>
 56    public static int GetFactoryGenerationMode(INamedTypeSymbol typeSymbol)
 57    {
 11658        foreach (var attribute in typeSymbol.GetAttributes())
 59        {
 3160            var attributeClass = attribute.AttributeClass;
 3161            if (attributeClass == null)
 62                continue;
 63
 3164            var name = attributeClass.Name;
 3165            var fullName = attributeClass.ToDisplayString();
 66
 3167            bool isFactoryAttribute = name == GenerateFactoryAttributeName ||
 3168                                      fullName == GenerateFactoryAttributeFullName ||
 3169                                      (attributeClass.IsGenericType &&
 3170                                       attributeClass.OriginalDefinition.Name == GenerateFactoryAttributeName);
 71
 3172            if (isFactoryAttribute)
 73            {
 74                // Check named argument "Mode"
 5875                foreach (var namedArg in attribute.NamedArguments)
 76                {
 277                    if (namedArg.Key == "Mode" && namedArg.Value.Value is int modeValue)
 78                    {
 279                        return modeValue;
 80                    }
 81                }
 82            }
 83        }
 84
 2685        return 3; // Default is All (Func | Interface)
 86    }
 87
 88    /// <summary>
 89    /// Gets the interface type from a generic [GenerateFactory&lt;T&gt;] attribute, if present.
 90    /// </summary>
 91    /// <param name="typeSymbol">The type symbol to check.</param>
 92    /// <returns>The fully qualified interface type name, or null if non-generic attribute is used.</returns>
 93    public static string? GetFactoryReturnInterfaceType(INamedTypeSymbol typeSymbol)
 94    {
 11695        foreach (var attribute in typeSymbol.GetAttributes())
 96        {
 3197            var attributeClass = attribute.AttributeClass;
 3198            if (attributeClass == null)
 99                continue;
 100
 101            // Check for generic GenerateFactoryAttribute<T>
 31102            if (attributeClass.IsGenericType)
 103            {
 2104                var originalDef = attributeClass.OriginalDefinition;
 2105                if (originalDef.Name == GenerateFactoryAttributeName)
 106                {
 107                    // Extract the type argument
 2108                    var typeArg = attributeClass.TypeArguments.FirstOrDefault();
 2109                    if (typeArg != null)
 110                    {
 2111                        return $"global::{typeArg.ToDisplayString()}";
 112                    }
 113                }
 114            }
 115        }
 116
 26117        return null;
 118    }
 119
 120    /// <summary>
 121    /// Partitions constructor parameters into injectable (DI-resolvable) and runtime (must be provided).
 122    /// </summary>
 123    /// <param name="typeSymbol">The type symbol to analyze.</param>
 124    /// <returns>A list of factory constructor infos, one for each viable constructor.</returns>
 125    /// <remarks>
 126    /// A type eligible for generated-constructor generation (<c>[GenerateConstructor]</c>
 127    /// or a positive field-level constructor guard trigger) has its effective constructor
 128    /// derived from <see cref="ConstructorGenerationDiscoveryHelper.TryGetModel"/> instead
 129    /// of <see cref="INamedTypeSymbol.InstanceConstructors"/>, which would otherwise only
 130    /// see the implicit parameterless constructor visible before the sibling
 131    /// <c>GeneratedConstructorGenerator</c> pass emits the real one within this
 132    /// compilation. The field-derived parameter list is partitioned into injectable and
 133    /// runtime groups using the exact same rule as a hand-written constructor.
 134    /// </remarks>
 135    public static IReadOnlyList<FactoryConstructorInfo> GetFactoryConstructors(INamedTypeSymbol typeSymbol)
 136    {
 30137        var generatedModel = ConstructorGenerationDiscoveryHelper.TryGetModel(typeSymbol);
 30138        if (generatedModel is not null)
 139        {
 5140            var generatedCtor = BuildGeneratedConstructorFactoryInfo(typeSymbol, generatedModel.Value);
 5141            return generatedCtor is null
 5142                ? Array.Empty<FactoryConstructorInfo>()
 5143                : new[] { generatedCtor.Value };
 144        }
 145
 25146        var result = new List<FactoryConstructorInfo>();
 147
 106148        foreach (var ctor in typeSymbol.InstanceConstructors)
 149        {
 28150            if (ctor.IsStatic)
 151                continue;
 152
 28153            if (ctor.DeclaredAccessibility != Accessibility.Public)
 154                continue;
 155
 156            // Extract XML documentation for parameters
 28157            var paramDocs = GetConstructorParameterDocumentation(ctor);
 158
 28159            var injectableParams = new List<TypeDiscoveryHelper.ConstructorParameterInfo>();
 28160            var runtimeParams = new List<TypeDiscoveryHelper.ConstructorParameterInfo>();
 161
 174162            foreach (var param in ctor.Parameters)
 163            {
 59164                var typeName = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 59165                paramDocs.TryGetValue(param.Name, out var docComment);
 166
 59167                if (IsInjectableParameterType(param.Type))
 168                {
 169                    // Check for [FromKeyedServices] attribute
 26170                    var serviceKey = GetFromKeyedServicesKey(param);
 26171                    var paramInfo = new TypeDiscoveryHelper.ConstructorParameterInfo(typeName, serviceKey, param.Name, d
 26172                    injectableParams.Add(paramInfo);
 173                }
 174                else
 175                {
 33176                    var paramInfo = new TypeDiscoveryHelper.ConstructorParameterInfo(typeName, null, param.Name, docComm
 33177                    runtimeParams.Add(paramInfo);
 178                }
 179            }
 180
 181            // Only include constructors that have at least one runtime parameter
 182            // (otherwise normal registration works fine)
 28183            if (runtimeParams.Count > 0)
 184            {
 27185                result.Add(new FactoryConstructorInfo(
 27186                    injectableParams.ToArray(),
 27187                    runtimeParams.ToArray()));
 188            }
 189        }
 190
 25191        return result;
 192    }
 193
 194    /// <summary>
 195    /// Builds the single <see cref="FactoryConstructorInfo"/> for a generated
 196    /// constructor's field-derived parameter list, or <see langword="null"/> when no
 197    /// field is a runtime (non-injectable) parameter — matching the hand-written-constructor
 198    /// rule that a factory adds no value when every parameter is already container-resolvable.
 199    /// </summary>
 200    private static FactoryConstructorInfo? BuildGeneratedConstructorFactoryInfo(
 201        INamedTypeSymbol typeSymbol,
 202        GeneratedConstructorModel model)
 203    {
 204        // Paired by index with model.Fields: both are derived from the exact same
 205        // ordered, filtered field set (GeneratedConstructorEligibility.GetEligibleConstructorFields),
 206        // so the field's real ITypeSymbol (needed for injectability classification) lines
 207        // up with the model's already-normalized parameter name/type-name strings.
 5208        var fieldSymbols = GeneratedConstructorEligibility.GetEligibleConstructorFields(typeSymbol);
 209
 5210        var injectableParams = new List<TypeDiscoveryHelper.ConstructorParameterInfo>();
 5211        var runtimeParams = new List<TypeDiscoveryHelper.ConstructorParameterInfo>();
 212
 30213        for (var i = 0; i < model.Fields.Length; i++)
 214        {
 10215            var field = model.Fields[i];
 10216            var paramInfo = new TypeDiscoveryHelper.ConstructorParameterInfo(field.ParameterTypeName, serviceKey: null, 
 217
 10218            if (IsInjectableParameterType(fieldSymbols[i].Type))
 219            {
 6220                injectableParams.Add(paramInfo);
 221            }
 222            else
 223            {
 4224                runtimeParams.Add(paramInfo);
 225            }
 226        }
 227
 5228        if (runtimeParams.Count == 0)
 1229            return null;
 230
 4231        return new FactoryConstructorInfo(injectableParams.ToArray(), runtimeParams.ToArray());
 232    }
 233
 234    /// <summary>
 235    /// Extracts parameter documentation from a constructor's XML documentation comments.
 236    /// </summary>
 237    private static Dictionary<string, string> GetConstructorParameterDocumentation(IMethodSymbol constructor)
 238    {
 28239        var result = new Dictionary<string, string>(StringComparer.Ordinal);
 240
 28241        var xmlDoc = constructor.GetDocumentationCommentXml();
 28242        if (string.IsNullOrWhiteSpace(xmlDoc))
 23243            return result;
 244
 245        try
 246        {
 247            // Parse the XML documentation
 5248            var doc = System.Xml.Linq.XDocument.Parse(xmlDoc);
 5249            var paramElements = doc.Descendants("param");
 250
 32251            foreach (var param in paramElements)
 252            {
 11253                var nameAttr = param.Attribute("name");
 11254                if (nameAttr is null || string.IsNullOrWhiteSpace(nameAttr.Value))
 255                    continue;
 256
 257                // Get the inner text/content of the param element
 11258                var content = param.Value?.Trim();
 11259                if (!string.IsNullOrWhiteSpace(content))
 260                {
 11261                    result[nameAttr.Value] = content!;
 262                }
 263            }
 5264        }
 0265        catch
 266        {
 267            // If XML parsing fails, return empty dictionary
 0268        }
 269
 5270        return result;
 271    }
 272
 273    private static string? GetFromKeyedServicesKey(IParameterSymbol param)
 274    {
 275        const string FromKeyedServicesAttributeName = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribu
 276
 52277        foreach (var attr in param.GetAttributes())
 278        {
 0279            var attrClass = attr.AttributeClass;
 0280            if (attrClass is null)
 281                continue;
 282
 0283            var attrFullName = attrClass.ToDisplayString();
 0284            if (attrFullName == FromKeyedServicesAttributeName)
 285            {
 286                // Extract the key from the constructor argument
 0287                if (attr.ConstructorArguments.Length > 0)
 288                {
 0289                    var keyArg = attr.ConstructorArguments[0];
 0290                    if (keyArg.Value is string keyValue)
 291                    {
 0292                        return keyValue;
 293                    }
 294                }
 295            }
 296        }
 297
 26298        return null;
 299    }
 300
 301    private static bool IsInjectableParameterType(ITypeSymbol typeSymbol)
 302    {
 303        // Interfaces and abstract classes are typically injectable
 69304        if (typeSymbol.TypeKind == TypeKind.Interface)
 32305            return true;
 306
 37307        if (typeSymbol is INamedTypeSymbol namedType && namedType.IsAbstract)
 0308            return true;
 309
 310        // Common framework types are injectable
 37311        var fullName = typeSymbol.ToDisplayString();
 37312        if (fullName.StartsWith("Microsoft.Extensions.", StringComparison.Ordinal))
 0313            return true;
 314
 315        // Classes with [Singleton], [Scoped], or [Transient] are injectable
 37316        if (typeSymbol is INamedTypeSymbol classType)
 317        {
 260318            foreach (var attr in classType.GetAttributes())
 319            {
 93320                var attrName = attr.AttributeClass?.Name;
 93321                if (attrName is "SingletonAttribute" or "ScopedAttribute" or "TransientAttribute")
 0322                    return true;
 323            }
 324        }
 325
 37326        return false;
 327    }
 328
 329    /// <summary>
 330    /// Represents a constructor suitable for factory generation.
 331    /// </summary>
 332    public readonly struct FactoryConstructorInfo
 333    {
 334        public FactoryConstructorInfo(
 335            TypeDiscoveryHelper.ConstructorParameterInfo[] injectableParameters,
 336            TypeDiscoveryHelper.ConstructorParameterInfo[] runtimeParameters)
 337        {
 31338            InjectableParameters = injectableParameters;
 31339            RuntimeParameters = runtimeParameters;
 31340        }
 341
 342        /// <summary>Parameters that can be resolved from the service provider.</summary>
 90343        public TypeDiscoveryHelper.ConstructorParameterInfo[] InjectableParameters { get; }
 344
 345        /// <summary>Parameters that must be provided at factory call time.</summary>
 210346        public TypeDiscoveryHelper.ConstructorParameterInfo[] RuntimeParameters { get; }
 347    }
 348}