< Summary

Information
Class: NexusLabs.Needlr.Generators.TypeDiscoveryHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/TypeDiscoveryHelper.cs
Line coverage
86%
Covered lines: 383
Uncovered lines: 61
Coverable lines: 444
Total lines: 1330
Line coverage: 86.2%
Branch coverage
83%
Covered branches: 361
Total branches: 434
Branch coverage: 83.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
IsInjectableType(...)100%11100%
GetRegisterableInterfaces(...)92.85%141492.3%
GetRegisterAsInterfaces(...)66.66%161270%
GetConstructorParameterTypes(...)100%66100%
IsDecoratorInterface(...)100%11100%
GetFullyQualifiedName(...)78.57%141491.66%
MatchesNamespacePrefix(...)83.33%1818100%
GetAllTypes()100%88100%
HasDoNotAutoRegisterAttribute(...)100%11100%
HasDoNotAutoRegisterAttributeDirect(...)100%11100%
IsCompilerGenerated(...)100%11100%
InheritsFrom(...)100%11100%
IsSystemInterface(...)100%11100%
IsHostedServiceInterface(...)100%11100%
IsHostedServiceType(...)83.33%211880%
IsDecoratorForHostedService(...)92.85%141491.66%
InheritsFromBackgroundService(...)100%44100%
ImplementsIHostedService(...)100%44100%
IsSystemType(...)100%11100%
HasUnsatisfiedRequiredMembers(...)100%11100%
IsAccessibleFromGeneratedCode(...)100%11100%
WouldBeInjectableIgnoringAccessibility(...)79.16%282480.76%
WouldBePluginIgnoringAccessibility(...)83.33%121285.71%
IsInternalOrLessAccessible(...)50%8662.5%
IsDisposableType(...)100%66100%
.cctor()100%11100%
ImplementsNeedlrPluginInterface(...)100%66100%
GetNeedlrPluginInterfaceName(...)0%4260%
HasGenerateTypeRegistryAttribute(...)100%66100%
IsPubliclyAccessible(...)0%4260%
DetermineLifetime(...)100%1818100%
GetExplicitLifetime(...)100%1616100%
AllParametersAreInjectable(...)100%44100%
IsInjectableParameterType(...)100%1010100%
HasDoNotInjectAttribute(...)87.5%8890%
IsPluginType(...)88.88%181889.47%
GetPluginInterfaces(...)100%1010100%
ImplementsInterface(...)0%2040%
HasParameterlessConstructor(...)100%88100%
GetPluginAttributes(...)63.33%733063.63%
IsInheritedAttribute(...)0%156120%
GetBestConstructorParameters(...)100%1616100%
GetFullyQualifiedNameForType(...)100%11100%
.ctor(...)100%11100%
get_TypeName()100%11100%
get_ServiceKey()100%11100%
get_ParameterName()100%11100%
get_DocumentationComment()100%11100%
get_IsKeyed()100%11100%
GetBestConstructorParametersWithKeys(...)88.46%312680.76%
GetKeyedServiceKeys(...)100%1212100%
HasDeferToContainerAttribute(...)100%88100%
GetDeferToContainerParameterTypes(...)93.75%161693.75%
.ctor(...)100%11100%
get_DecoratorTypeName()100%11100%
get_ServiceTypeName()100%11100%
get_Order()100%11100%
GetDecoratorForAttributes(...)95.45%2222100%
HasDecoratorForAttribute(...)91.66%1212100%

File(s)

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

#LineLine coverage
 1using Microsoft.CodeAnalysis;
 2using SharedHelper = NexusLabs.Needlr.Roslyn.Shared.TypeDiscoveryHelper;
 3
 4namespace NexusLabs.Needlr.Generators;
 5
 6/// <summary>
 7/// Internal lifetime enum used by the generator to avoid runtime dependency on Attributes assembly.
 8/// Maps 1:1 with InjectableLifetime in the Attributes package.
 9/// </summary>
 10internal enum GeneratorLifetime
 11{
 12    Singleton = 0,
 13    Scoped = 1,
 14    Transient = 2
 15}
 16
 17/// <summary>
 18/// Helper utilities for discovering injectable types from Roslyn symbols.
 19/// </summary>
 20internal static class TypeDiscoveryHelper
 21{
 22    private const string DoNotInjectAttributeName = "DoNotInjectAttribute";
 23    private const string DoNotInjectAttributeFullName = "NexusLabs.Needlr.DoNotInjectAttribute";
 24    private const string DeferToContainerAttributeName = "DeferToContainerAttribute";
 25    private const string DeferToContainerAttributeFullName = "NexusLabs.Needlr.DeferToContainerAttribute";
 26    private const string DecoratorForAttributePrefix = "NexusLabs.Needlr.DecoratorForAttribute";
 27    private const string KeyedAttributeName = "KeyedAttribute";
 28    private const string KeyedAttributeFullName = "NexusLabs.Needlr.KeyedAttribute";
 29    private const string SingletonAttributeName = "SingletonAttribute";
 30    private const string SingletonAttributeFullName = "NexusLabs.Needlr.SingletonAttribute";
 31    private const string ScopedAttributeName = "ScopedAttribute";
 32    private const string ScopedAttributeFullName = "NexusLabs.Needlr.ScopedAttribute";
 33    private const string TransientAttributeName = "TransientAttribute";
 34    private const string TransientAttributeFullName = "NexusLabs.Needlr.TransientAttribute";
 35    private const string GenerateFactoryAttributeName = "GenerateFactoryAttribute";
 36    private const string GenerateFactoryAttributeFullName = "NexusLabs.Needlr.Generators.GenerateFactoryAttribute";
 37    private const string OptionsAttributeName = "OptionsAttribute";
 38    private const string OptionsAttributeFullName = "NexusLabs.Needlr.Generators.OptionsAttribute";
 39
 40    /// <summary>
 41    /// Determines whether a type symbol represents a concrete injectable type.
 42    /// Delegates to the shared library for consistency with analyzers.
 43    /// </summary>
 44    /// <param name="typeSymbol">The type symbol to check.</param>
 45    /// <param name="isCurrentAssembly">True if the type is from the current compilation's assembly (allows internal typ
 46    /// <returns>True if the type is a valid injectable type; otherwise, false.</returns>
 47    public static bool IsInjectableType(INamedTypeSymbol typeSymbol, bool isCurrentAssembly = false)
 142027148        => SharedHelper.IsInjectableType(typeSymbol, isCurrentAssembly);
 49
 50    private const string RegisterAsAttributePrefix = "NexusLabs.Needlr.RegisterAsAttribute";
 51
 52    /// <summary>
 53    /// Gets the interfaces that should be registered for a type.
 54    /// </summary>
 55    /// <param name="typeSymbol">The type symbol to get interfaces for.</param>
 56    /// <returns>A list of interface symbols suitable for registration.</returns>
 57    public static IReadOnlyList<INamedTypeSymbol> GetRegisterableInterfaces(INamedTypeSymbol typeSymbol)
 58    {
 59        // Check for [RegisterAs<T>] attributes - if present, only register as those interfaces
 22814260        var registerAsInterfaces = GetRegisterAsInterfaces(typeSymbol);
 22814261        if (registerAsInterfaces.Count > 0)
 62        {
 063            return registerAsInterfaces;
 64        }
 65
 22814266        var result = new List<INamedTypeSymbol>();
 67
 68        // Get all constructor parameter types to detect decorator pattern
 22814269        var constructorParamTypes = GetConstructorParameterTypes(typeSymbol);
 70
 87164871        foreach (var iface in typeSymbol.AllInterfaces)
 72        {
 20768273            if (iface.IsUnboundGenericType)
 74                continue;
 75
 20768276            if (IsSystemInterface(iface))
 77                continue;
 78
 32279            if (HasDoNotAutoRegisterAttributeDirect(iface))
 80                continue;
 81
 82            // Skip interfaces that this type also takes as constructor parameters (decorator pattern)
 83            // A type that implements IFoo and takes IFoo in its constructor is likely a decorator
 84            // and should not be auto-registered as IFoo to avoid circular dependencies
 32085            if (IsDecoratorInterface(iface, constructorParamTypes))
 86                continue;
 87
 88            // Skip IHostedService - hosted services are registered separately via RegisterHostedServices()
 89            // to ensure proper concrete + interface forwarding pattern
 29290            if (IsHostedServiceInterface(iface))
 91                continue;
 92
 28593            result.Add(iface);
 94        }
 95
 22814296        return result;
 97    }
 98
 99    /// <summary>
 100    /// Gets interface types specified by [RegisterAs&lt;T&gt;] attributes on the type.
 101    /// </summary>
 102    /// <param name="typeSymbol">The type symbol to check.</param>
 103    /// <returns>A list of interface symbols from RegisterAs attributes.</returns>
 104    public static IReadOnlyList<INamedTypeSymbol> GetRegisterAsInterfaces(INamedTypeSymbol typeSymbol)
 105    {
 228142106        var result = new List<INamedTypeSymbol>();
 107
 1288638108        foreach (var attribute in typeSymbol.GetAttributes())
 109        {
 416177110            var attrClass = attribute.AttributeClass;
 416177111            if (attrClass == null)
 112                continue;
 113
 114            // Check for RegisterAsAttribute<T>
 416177115            var attrFullName = attrClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 416177116            if (!attrFullName.StartsWith("global::" + RegisterAsAttributePrefix, StringComparison.Ordinal))
 117                continue;
 118
 119            // Get the type argument
 0120            if (attrClass.IsGenericType && attrClass.TypeArguments.Length == 1)
 121            {
 0122                if (attrClass.TypeArguments[0] is INamedTypeSymbol interfaceType)
 123                {
 0124                    result.Add(interfaceType);
 125                }
 126            }
 127        }
 128
 228142129        return result;
 130    }
 131
 132    /// <summary>
 133    /// Gets all parameter types from all constructors of a type.
 134    /// </summary>
 135    private static HashSet<string> GetConstructorParameterTypes(INamedTypeSymbol typeSymbol)
 136    {
 228142137        var paramTypes = new HashSet<string>(StringComparer.Ordinal);
 138
 1470534139        foreach (var ctor in typeSymbol.InstanceConstructors)
 140        {
 507125141            if (ctor.IsStatic)
 142                continue;
 143
 2177902144            foreach (var param in ctor.Parameters)
 145            {
 581826146                var paramTypeName = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 581826147                paramTypes.Add(paramTypeName);
 148            }
 149        }
 150
 228142151        return paramTypes;
 152    }
 153
 154    /// <summary>
 155    /// Checks if an interface is a decorator interface (also taken as a constructor parameter).
 156    /// </summary>
 157    private static bool IsDecoratorInterface(INamedTypeSymbol iface, HashSet<string> constructorParamTypes)
 158    {
 320159        var ifaceName = iface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 320160        return constructorParamTypes.Contains(ifaceName);
 161    }
 162
 163    /// <summary>
 164    /// Gets the fully qualified name for a type symbol suitable for code generation.
 165    /// For generic type definitions (open generics), outputs open generic syntax (e.g., MyClass&lt;&gt;).
 166    /// For constructed generics with concrete type arguments, outputs the full type (e.g., MyClass&lt;int&gt;).
 167    /// </summary>
 168    /// <param name="typeSymbol">The type symbol.</param>
 169    /// <returns>The fully qualified type name with global:: prefix.</returns>
 170    public static string GetFullyQualifiedName(INamedTypeSymbol typeSymbol)
 171    {
 172        // Check if this is an open generic type definition (has type parameters, not type arguments)
 173        // e.g., JobScheduler<TJob> where TJob is a TypeParameter, not a concrete type
 174        // We need to convert these to open generic syntax: JobScheduler<>
 2409802175        if (typeSymbol.TypeParameters.Length > 0 && !typeSymbol.IsUnboundGenericType)
 176        {
 177            // Check if type arguments are still type parameters (meaning this is a generic definition)
 178            // For a closed generic like ILogger<MyService>, TypeArguments contains MyService (a NamedTypeSymbol)
 179            // For an open generic like JobScheduler<TJob>, TypeArguments contains TJob (a TypeParameterSymbol)
 291439180            var hasUnresolvedTypeParameters = typeSymbol.TypeArguments.Any(ta => ta.TypeKind == TypeKind.TypeParameter);
 181
 143832182            if (hasUnresolvedTypeParameters)
 183            {
 184                // Build the open generic name manually
 59582185                var containingNamespace = typeSymbol.ContainingNamespace?.ToDisplayString();
 59582186                var typeName = typeSymbol.Name;
 59582187                var arity = typeSymbol.TypeParameters.Length;
 188
 189                // Create the open generic syntax: MyClass<,> for 2 type params, MyClass<> for 1
 59582190                var commas = arity > 1 ? new string(',', arity - 1) : string.Empty;
 59582191                var openGenericPart = $"<{commas}>";
 192
 59582193                if (string.IsNullOrEmpty(containingNamespace) || containingNamespace == "<global namespace>")
 194                {
 0195                    return $"global::{typeName}{openGenericPart}";
 196                }
 197
 59582198                return $"global::{containingNamespace}.{typeName}{openGenericPart}";
 199            }
 200        }
 201
 2350220202        return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 203    }
 204
 205    /// <summary>
 206    /// Checks if a type matches any of the given namespace prefixes.
 207    /// </summary>
 208    /// <param name="typeSymbol">The type symbol to check.</param>
 209    /// <param name="namespacePrefixes">The namespace prefixes to match.</param>
 210    /// <returns>True if the type's namespace starts with any of the prefixes.</returns>
 211    public static bool MatchesNamespacePrefix(INamedTypeSymbol typeSymbol, IReadOnlyList<string>? namespacePrefixes)
 212    {
 1716198213        if (namespacePrefixes == null || namespacePrefixes.Count == 0)
 1491552214            return true;
 215
 224646216        var typeNamespace = typeSymbol.ContainingNamespace?.ToDisplayString() ?? string.Empty;
 217
 218        // Check if type is in the global namespace
 224646219        var isGlobalNamespace = typeSymbol.ContainingNamespace?.IsGlobalNamespace == true;
 220
 906011221        foreach (var prefix in namespacePrefixes)
 222        {
 223            // Empty string prefix matches global namespace types
 228627224            if (string.IsNullOrEmpty(prefix))
 225            {
 7961226                if (isGlobalNamespace)
 348227                    return true;
 228                continue;
 229            }
 230
 220666231            if (typeNamespace.StartsWith(prefix, StringComparison.Ordinal))
 187232                return true;
 233        }
 234
 224111235        return false;
 535236    }
 237
 238    /// <summary>
 239    /// Recursively iterates all named type symbols in a namespace.
 240    /// </summary>
 241    /// <param name="namespaceSymbol">The namespace to iterate.</param>
 242    /// <returns>All named type symbols in the namespace and nested namespaces.</returns>
 243    public static IEnumerable<INamedTypeSymbol> GetAllTypes(INamespaceSymbol namespaceSymbol)
 244    {
 10235834245        foreach (var member in namespaceSymbol.GetMembers())
 246        {
 4201214247            if (member is INamedTypeSymbol typeSymbol)
 248            {
 3431179249                yield return typeSymbol;
 250            }
 770035251            else if (member is INamespaceSymbol nestedNamespace)
 252            {
 19036860253                foreach (var nestedType in GetAllTypes(nestedNamespace))
 254                {
 8748395255                    yield return nestedType;
 256                }
 257            }
 258        }
 916703259    }
 260
 261    private static bool HasDoNotAutoRegisterAttribute(INamedTypeSymbol typeSymbol)
 3787262        => SharedHelper.HasDoNotAutoRegisterAttribute(typeSymbol);
 263
 264    private static bool HasDoNotAutoRegisterAttributeDirect(INamedTypeSymbol typeSymbol)
 1147446265        => SharedHelper.HasDoNotAutoRegisterAttributeDirect(typeSymbol);
 266
 267    private static bool IsCompilerGenerated(INamedTypeSymbol typeSymbol)
 822318268        => SharedHelper.IsCompilerGenerated(typeSymbol);
 269
 270    private static bool InheritsFrom(INamedTypeSymbol typeSymbol, string baseTypeName)
 9082271        => SharedHelper.InheritsFrom(typeSymbol, baseTypeName);
 272
 273    private static bool IsSystemInterface(INamedTypeSymbol interfaceSymbol)
 497890274        => SharedHelper.IsSystemType(interfaceSymbol);
 275
 276    private static bool IsHostedServiceInterface(INamedTypeSymbol interfaceSymbol)
 277    {
 292278        var fullName = GetFullyQualifiedName(interfaceSymbol);
 292279        return fullName == "global::Microsoft.Extensions.Hosting.IHostedService";
 280    }
 281
 282    /// <summary>
 283    /// Determines whether a type is a hosted service (implements IHostedService or inherits from BackgroundService).
 284    /// </summary>
 285    /// <param name="typeSymbol">The type symbol to check.</param>
 286    /// <param name="isCurrentAssembly">True if the type is from the current compilation's assembly.</param>
 287    /// <returns>True if the type is a hosted service.</returns>
 288    public static bool IsHostedServiceType(INamedTypeSymbol typeSymbol, bool isCurrentAssembly = false)
 289    {
 290        // Must be a concrete, non-abstract class
 1420254291        if (typeSymbol.IsAbstract || typeSymbol.TypeKind != TypeKind.Class)
 668427292            return false;
 293
 294        // Check accessibility
 751827295        if (!isCurrentAssembly && IsInternalOrLessAccessible(typeSymbol))
 0296            return false;
 297
 298        // Skip if marked with [DoNotAutoRegister]
 751827299        if (HasDoNotAutoRegisterAttributeDirect(typeSymbol))
 2300            return false;
 301
 302        // Skip compiler-generated types
 751825303        if (IsCompilerGenerated(typeSymbol))
 0304            return false;
 305
 306        // Skip decorators - types with [DecoratorFor<IHostedService>] should not be
 307        // registered as hosted services (they decorate hosted services, not are hosted services)
 751825308        if (IsDecoratorForHostedService(typeSymbol))
 0309            return false;
 310
 311        // Check if inherits from BackgroundService
 751825312        if (InheritsFromBackgroundService(typeSymbol))
 6313            return true;
 314
 315        // Check if directly implements IHostedService (not via BackgroundService)
 751819316        if (ImplementsIHostedService(typeSymbol))
 1317            return true;
 318
 751818319        return false;
 320    }
 321
 322    private static bool IsDecoratorForHostedService(INamedTypeSymbol typeSymbol)
 323    {
 4337906324        foreach (var attribute in typeSymbol.GetAttributes())
 325        {
 1417128326            var attrClass = attribute.AttributeClass;
 1417128327            if (attrClass == null)
 328                continue;
 329
 1417128330            var attrFullName = attrClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 331
 332            // Check for DecoratorForAttribute<IHostedService>
 1417128333            if (attrFullName.StartsWith("global::NexusLabs.Needlr.DecoratorForAttribute<", StringComparison.Ordinal))
 334            {
 335                // Get the type argument
 20336                if (attrClass.IsGenericType && attrClass.TypeArguments.Length == 1)
 337                {
 20338                    var typeArg = attrClass.TypeArguments[0];
 20339                    if (typeArg is INamedTypeSymbol namedTypeArg)
 340                    {
 20341                        var typeArgName = GetFullyQualifiedName(namedTypeArg);
 20342                        if (typeArgName == "global::Microsoft.Extensions.Hosting.IHostedService")
 0343                            return true;
 344                    }
 345                }
 346            }
 347        }
 751825348        return false;
 349    }
 350
 351    private static bool InheritsFromBackgroundService(INamedTypeSymbol typeSymbol)
 352    {
 751825353        var baseType = typeSymbol.BaseType;
 2197209354        while (baseType != null)
 355        {
 1445390356            var fullName = GetFullyQualifiedName(baseType);
 1445390357            if (fullName == "global::Microsoft.Extensions.Hosting.BackgroundService")
 6358                return true;
 1445384359            baseType = baseType.BaseType;
 360        }
 751819361        return false;
 362    }
 363
 364    private static bool ImplementsIHostedService(INamedTypeSymbol typeSymbol)
 365    {
 2598565366        foreach (var iface in typeSymbol.AllInterfaces)
 367        {
 547464368            var fullName = GetFullyQualifiedName(iface);
 547464369            if (fullName == "global::Microsoft.Extensions.Hosting.IHostedService")
 1370                return true;
 371        }
 751818372        return false;
 373    }
 374
 375    private static bool IsSystemType(INamedTypeSymbol typeSymbol)
 398896376        => SharedHelper.IsSystemType(typeSymbol);
 377
 378    private static bool HasUnsatisfiedRequiredMembers(INamedTypeSymbol typeSymbol)
 395299379        => SharedHelper.HasUnsatisfiedRequiredMembers(typeSymbol);
 380
 381    /// <summary>
 382    /// Checks if a type is accessible from generated code.
 383    /// For types in the current assembly, internal and public types are accessible.
 384    /// For types in referenced assemblies, only public types are accessible.
 385    /// </summary>
 386    /// <param name="typeSymbol">The type symbol to check.</param>
 387    /// <param name="isCurrentAssembly">True if the type is from the current compilation's assembly.</param>
 388    /// <returns>True if the type is accessible from generated code.</returns>
 389    private static bool IsAccessibleFromGeneratedCode(INamedTypeSymbol typeSymbol, bool isCurrentAssembly)
 887935390        => SharedHelper.IsAccessibleFromGeneratedCode(typeSymbol, isCurrentAssembly);
 391
 392    /// <summary>
 393    /// Checks if a type would be registerable as injectable, ignoring accessibility constraints.
 394    /// This is used to detect internal types that match namespace filters but cannot be included.
 395    /// </summary>
 396    /// <param name="typeSymbol">The type symbol to check.</param>
 397    /// <returns>True if the type would be injectable if it were accessible.</returns>
 398    public static bool WouldBeInjectableIgnoringAccessibility(INamedTypeSymbol typeSymbol)
 399    {
 400        // Must be a class (not interface, struct, enum, delegate)
 71845401        if (typeSymbol.TypeKind != TypeKind.Class)
 14402            return false;
 403
 71831404        if (typeSymbol.IsAbstract)
 0405            return false;
 406
 71831407        if (typeSymbol.IsStatic)
 1140408            return false;
 409
 70691410        if (typeSymbol.IsUnboundGenericType)
 0411            return false;
 412
 413        // Exclude open generic types (type definitions with type parameters like MyClass<T>)
 70691414        if (typeSymbol.TypeParameters.Length > 0)
 198415            return false;
 416
 70493417        if (typeSymbol.ContainingType != null)
 0418            return false;
 419
 70493420        if (IsCompilerGenerated(typeSymbol))
 65952421            return false;
 422
 4541423        if (InheritsFrom(typeSymbol, "System.Exception"))
 0424            return false;
 425
 4541426        if (InheritsFrom(typeSymbol, "System.Attribute"))
 754427            return false;
 428
 3787429        if (typeSymbol.IsRecord)
 0430            return false;
 431
 3787432        if (HasDoNotAutoRegisterAttribute(typeSymbol))
 2433            return false;
 434
 435        // Must have a determinable lifetime to be injectable
 3785436        var lifetime = DetermineLifetime(typeSymbol);
 3785437        if (!lifetime.HasValue)
 1508438            return false;
 439
 2277440        return true;
 441    }
 442
 443    /// <summary>
 444    /// Checks if a type would be registerable as a plugin, ignoring accessibility constraints.
 445    /// This is used to detect internal types that match namespace filters but cannot be included.
 446    /// </summary>
 447    /// <param name="typeSymbol">The type symbol to check.</param>
 448    /// <returns>True if the type would be a plugin if it were accessible.</returns>
 449    public static bool WouldBePluginIgnoringAccessibility(INamedTypeSymbol typeSymbol)
 450    {
 451        // Must be a concrete class
 69586452        if (typeSymbol.TypeKind != TypeKind.Class)
 14453            return false;
 454
 69572455        if (typeSymbol.IsAbstract)
 0456            return false;
 457
 69572458        if (typeSymbol.IsStatic)
 1140459            return false;
 460
 68432461        if (typeSymbol.IsUnboundGenericType)
 0462            return false;
 463
 464        // Exclude open generic types (type definitions with type parameters like MyClass<T>)
 68432465        if (typeSymbol.TypeParameters.Length > 0)
 198466            return false;
 467
 468        // NOTE: Records ARE allowed as plugins (they are classes with parameterless constructors).
 469        // Records are excluded from IsInjectableType (auto-registration) but not from plugin discovery.
 470
 471        // Must have a parameterless constructor
 68234472        if (!HasParameterlessConstructor(typeSymbol))
 1498473            return false;
 474
 475        // Must have at least one plugin interface
 66736476        var pluginInterfaces = GetPluginInterfaces(typeSymbol);
 66736477        return pluginInterfaces.Count > 0;
 478    }
 479
 480    /// <summary>
 481    /// Checks if a type is internal (not public) and would be inaccessible from generated code
 482    /// in a different assembly.
 483    /// </summary>
 484    /// <param name="typeSymbol">The type symbol to check.</param>
 485    /// <returns>True if the type is internal or less accessible.</returns>
 486    public static bool IsInternalOrLessAccessible(INamedTypeSymbol typeSymbol)
 487    {
 488        // Check the type itself
 3957272489        if (typeSymbol.DeclaredAccessibility != Accessibility.Public)
 153724490            return true;
 491
 492        // Check all containing types (for nested types)
 3803548493        var containingType = typeSymbol.ContainingType;
 3803548494        while (containingType != null)
 495        {
 0496            if (containingType.DeclaredAccessibility != Accessibility.Public)
 0497                return true;
 0498            containingType = containingType.ContainingType;
 499        }
 500
 3803548501        return false;
 502    }
 503
 504    /// <summary>
 505    /// Checks if a type implements IDisposable or IAsyncDisposable.
 506    /// </summary>
 507    /// <param name="typeSymbol">The type symbol to check.</param>
 508    /// <returns>True if the type implements IDisposable or IAsyncDisposable.</returns>
 509    public static bool IsDisposableType(INamedTypeSymbol typeSymbol)
 510    {
 763986511        foreach (var iface in typeSymbol.AllInterfaces)
 512        {
 182756513            var fullName = GetFullyQualifiedName(iface);
 182756514            if (fullName == "global::System.IDisposable" || fullName == "global::System.IAsyncDisposable")
 57690515                return true;
 516        }
 170392517        return false;
 518    }
 519
 520    /// <summary>
 521    /// Known Needlr plugin interface names that indicate a type is a plugin.
 522    /// </summary>
 1523    private static readonly string[] NeedlrPluginInterfaceNames =
 1524    [
 1525        "NexusLabs.Needlr.IServiceCollectionPlugin",
 1526        "NexusLabs.Needlr.IPostBuildServiceCollectionPlugin",
 1527        "NexusLabs.Needlr.AspNet.IWebApplicationPlugin",
 1528        "NexusLabs.Needlr.AspNet.IWebApplicationBuilderPlugin",
 1529        "NexusLabs.Needlr.SignalR.IHubRegistrationPlugin",
 1530        "NexusLabs.Needlr.SemanticKernel.IKernelBuilderPlugin",
 1531        "NexusLabs.Needlr.Hosting.IHostApplicationBuilderPlugin",
 1532        "NexusLabs.Needlr.Hosting.IHostPlugin"
 1533    ];
 534
 535    /// <summary>
 536    /// Checks if a type implements any known Needlr plugin interface.
 537    /// </summary>
 538    /// <param name="typeSymbol">The type symbol to check.</param>
 539    /// <returns>True if the type implements a Needlr plugin interface.</returns>
 540    public static bool ImplementsNeedlrPluginInterface(INamedTypeSymbol typeSymbol)
 541    {
 167319542        foreach (var iface in typeSymbol.AllInterfaces)
 543        {
 1740544            var ifaceName = iface.ToDisplayString();
 31305545            foreach (var pluginInterface in NeedlrPluginInterfaceNames)
 546            {
 13913547                if (ifaceName == pluginInterface)
 1548                    return true;
 549            }
 550        }
 81919551        return false;
 552    }
 553
 554    /// <summary>
 555    /// Gets the name of the first Needlr plugin interface implemented by the type.
 556    /// </summary>
 557    /// <param name="typeSymbol">The type symbol to check.</param>
 558    /// <returns>The interface name, or null if none found.</returns>
 559    public static string? GetNeedlrPluginInterfaceName(INamedTypeSymbol typeSymbol)
 560    {
 0561        foreach (var iface in typeSymbol.AllInterfaces)
 562        {
 0563            var ifaceName = iface.ToDisplayString();
 0564            foreach (var pluginInterface in NeedlrPluginInterfaceNames)
 565            {
 0566                if (ifaceName == pluginInterface)
 0567                    return ifaceName;
 568            }
 569        }
 0570        return null;
 571    }
 572
 573    /// <summary>
 574    /// Checks if an assembly has the [GenerateTypeRegistry] attribute.
 575    /// </summary>
 576    /// <param name="assembly">The assembly symbol to check.</param>
 577    /// <returns>True if the assembly has the attribute.</returns>
 578    public static bool HasGenerateTypeRegistryAttribute(IAssemblySymbol assembly)
 579    {
 580        const string attributeName = "NexusLabs.Needlr.Generators.GenerateTypeRegistryAttribute";
 581
 6452324582        foreach (var attribute in assembly.GetAttributes())
 583        {
 3063893584            var attrClass = attribute.AttributeClass;
 3063893585            if (attrClass == null)
 586                continue;
 587
 3063893588            if (attrClass.ToDisplayString() == attributeName)
 48589                return true;
 590        }
 591
 162245592        return false;
 593    }
 594
 595    /// <summary>
 596    /// Checks if a type is publicly accessible (can be referenced from generated code).
 597    /// A type is publicly accessible if it and all its containing types are public.
 598    /// </summary>
 599    /// <param name="typeSymbol">The type symbol to check.</param>
 600    /// <returns>True if the type is publicly accessible.</returns>
 601    private static bool IsPubliclyAccessible(INamedTypeSymbol typeSymbol)
 602    {
 603        // Check the type itself
 0604        if (typeSymbol.DeclaredAccessibility != Accessibility.Public)
 0605            return false;
 606
 607        // Check all containing types (for nested types)
 0608        var containingType = typeSymbol.ContainingType;
 0609        while (containingType != null)
 610        {
 0611            if (containingType.DeclaredAccessibility != Accessibility.Public)
 0612                return false;
 0613            containingType = containingType.ContainingType;
 614        }
 615
 0616        return true;
 617    }
 618
 619    /// <summary>
 620    /// Determines the injectable lifetime for a type by analyzing its attributes and constructors.
 621    /// Checks for explicit lifetime attributes first, then falls back to Singleton if injectable.
 622    /// </summary>
 623    /// <param name="typeSymbol">The type symbol to analyze.</param>
 624    /// <returns>The determined lifetime, or null if the type is not injectable.</returns>
 625    public static GeneratorLifetime? DetermineLifetime(INamedTypeSymbol typeSymbol)
 626    {
 627        // Check for DoNotInjectAttribute
 404018628        if (HasDoNotInjectAttribute(typeSymbol))
 1629            return null;
 630
 631        // Exclude open generic types (type definitions with type parameters like MyClass<T>)
 404017632        if (typeSymbol.TypeParameters.Length > 0)
 1633            return null;
 634
 635        // Types with [DeferToContainer] are always injectable as Singleton
 636        // (the attribute declares constructor params that will be added by another generator)
 404016637        if (HasDeferToContainerAttribute(typeSymbol))
 6638            return GetExplicitLifetime(typeSymbol) ?? GeneratorLifetime.Singleton;
 639
 640        // Get all instance constructors
 404010641        var constructors = typeSymbol.InstanceConstructors;
 642
 1435811643        foreach (var ctor in constructors)
 644        {
 645            // Skip static constructors
 429089646            if (ctor.IsStatic)
 647                continue;
 648
 429089649            var parameters = ctor.Parameters;
 650
 651            // Parameterless constructor is always valid
 429089652            if (parameters.Length == 0)
 186225653                return GetExplicitLifetime(typeSymbol) ?? GeneratorLifetime.Singleton;
 654
 655            // Single parameter of same type (copy constructor) - not injectable
 242864656            if (parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(parameters[0].Type, typeSymbol))
 657                continue;
 658
 659            // Check if all parameters are injectable types
 235700660            if (AllParametersAreInjectable(parameters))
 44162661                return GetExplicitLifetime(typeSymbol) ?? GeneratorLifetime.Singleton;
 662        }
 663
 173623664        return null;
 665    }
 666
 667    /// <summary>
 668    /// Gets the explicit lifetime from attributes if specified.
 669    /// </summary>
 670    private static GeneratorLifetime? GetExplicitLifetime(INamedTypeSymbol typeSymbol)
 671    {
 1293030672        foreach (var attribute in typeSymbol.GetAttributes())
 673        {
 416151674            var attributeClass = attribute.AttributeClass;
 416151675            if (attributeClass == null)
 676                continue;
 677
 416151678            var name = attributeClass.Name;
 416151679            var fullName = attributeClass.ToDisplayString();
 680
 416151681            if (name == TransientAttributeName || fullName == TransientAttributeFullName)
 4682                return GeneratorLifetime.Transient;
 683
 416147684            if (name == ScopedAttributeName || fullName == ScopedAttributeFullName)
 38685                return GeneratorLifetime.Scoped;
 686
 416109687            if (name == SingletonAttributeName || fullName == SingletonAttributeFullName)
 16688                return GeneratorLifetime.Singleton;
 689        }
 690
 230335691        return null;
 692    }
 693
 694    private static bool AllParametersAreInjectable(System.Collections.Immutable.ImmutableArray<IParameterSymbol> paramet
 695    {
 1127035696        foreach (var param in parameters)
 697        {
 367739698            if (!IsInjectableParameterType(param.Type))
 211899699                return false;
 700        }
 89829701        return true;
 702    }
 703
 704    private static bool IsInjectableParameterType(ITypeSymbol typeSymbol)
 705    {
 706        // Must not be a delegate
 367739707        if (typeSymbol.TypeKind == TypeKind.Delegate)
 7165708            return false;
 709
 710        // Must not be a value type
 360574711        if (typeSymbol.IsValueType)
 95005712            return false;
 713
 714        // Must not be string
 265569715        if (typeSymbol.SpecialType == SpecialType.System_String)
 87848716            return false;
 717
 718        // Must be a class or interface
 177721719        if (typeSymbol.TypeKind != TypeKind.Class && typeSymbol.TypeKind != TypeKind.Interface)
 21881720            return false;
 721
 155840722        return true;
 723    }
 724
 725    private static bool HasDoNotInjectAttribute(INamedTypeSymbol typeSymbol)
 726    {
 2289551727        foreach (var attribute in typeSymbol.GetAttributes())
 728        {
 740758729            var attributeClass = attribute.AttributeClass;
 740758730            if (attributeClass == null)
 731                continue;
 732
 740758733            var name = attributeClass.Name;
 740758734            if (name == DoNotInjectAttributeName)
 1735                return true;
 736
 740757737            var fullName = attributeClass.ToDisplayString();
 740757738            if (fullName == DoNotInjectAttributeFullName)
 0739                return true;
 740        }
 741
 404017742        return false;
 743    }
 744
 745    /// <summary>
 746    /// Determines if a type is a valid plugin type (concrete class with parameterless constructor).
 747    /// </summary>
 748    /// <param name="typeSymbol">The type symbol to check.</param>
 749    /// <param name="isCurrentAssembly">True if the type is from the current compilation's assembly (allows internal typ
 750    /// <returns>True if the type is a valid plugin type.</returns>
 751    public static bool IsPluginType(INamedTypeSymbol typeSymbol, bool isCurrentAssembly = false)
 752    {
 753        // Must be a concrete class
 1420258754        if (typeSymbol.TypeKind != TypeKind.Class)
 532323755            return false;
 756
 757        // Must be accessible from generated code
 758        // - Current assembly: internal and public types are accessible
 759        // - Referenced assemblies: only public types are accessible
 887935760        if (!IsAccessibleFromGeneratedCode(typeSymbol, isCurrentAssembly))
 0761            return false;
 762
 887935763        if (typeSymbol.IsAbstract)
 136104764            return false;
 765
 751831766        if (typeSymbol.IsStatic)
 86576767            return false;
 768
 665255769        if (typeSymbol.IsUnboundGenericType)
 0770            return false;
 771
 772        // Exclude open generic types (type definitions with type parameters like MyClass<T>)
 773        // These cannot be instantiated directly and would produce invalid typeof() expressions
 665255774        if (typeSymbol.TypeParameters.Length > 0)
 39424775            return false;
 776
 777        // NOTE: Records ARE allowed as plugins (they are classes with parameterless constructors).
 778        // Records are excluded from IsInjectableType (auto-registration) but not from plugin discovery.
 779        // Use case: CacheConfiguration records can be discovered via IPluginFactory.CreatePluginsFromAssemblies<T>()
 780
 781        // Must have a parameterless constructor
 625831782        if (!HasParameterlessConstructor(typeSymbol))
 230532783            return false;
 784
 785        // Exclude types with required members that can't be set via constructor
 786        // These would cause compilation errors: "Required member 'X' must be set"
 395299787        if (HasUnsatisfiedRequiredMembers(typeSymbol))
 2788            return false;
 789
 790        // Skip types with [DoNotAutoRegister] directly on the type itself
 791        // NOTE: We use the "Direct" version because [DoNotAutoRegister] on interfaces
 792        // (like IServiceCollectionPlugin) means "don't inject as DI service", not
 793        // "don't discover plugins implementing this interface"
 395297794        if (HasDoNotAutoRegisterAttributeDirect(typeSymbol))
 2795            return false;
 796
 395295797        return true;
 798    }
 799
 800    /// <summary>
 801    /// Gets the plugin base types (interfaces and base classes) for a type.
 802    /// Plugin base types are non-System interfaces and non-System/non-object base classes.
 803    /// </summary>
 804    /// <param name="typeSymbol">The type symbol to check.</param>
 805    /// <returns>A list of plugin base type symbols (interfaces and base classes).</returns>
 806    public static IReadOnlyList<INamedTypeSymbol> GetPluginInterfaces(INamedTypeSymbol typeSymbol)
 807    {
 462030808        var result = new List<INamedTypeSymbol>();
 809
 810        // Add non-system interfaces
 1504476811        foreach (var iface in typeSymbol.AllInterfaces)
 812        {
 290208813            if (iface.IsUnboundGenericType)
 814                continue;
 815
 290208816            if (IsSystemInterface(iface))
 817                continue;
 818
 267819            result.Add(iface);
 820        }
 821
 822        // Add non-system base classes (walking up the hierarchy)
 462030823        var baseType = typeSymbol.BaseType;
 463167824        while (baseType != null)
 825        {
 826            // Stop at System.Object or System types
 398896827            if (IsSystemType(baseType))
 828                break;
 829
 830            // Skip abstract types that can't be instantiated directly
 831            // but include them as they represent the plugin contract
 1137832            result.Add(baseType);
 1137833            baseType = baseType.BaseType;
 834        }
 835
 462030836        return result;
 837    }
 838
 839    /// <summary>
 840    /// Checks if a type implements a specific interface by name.
 841    /// </summary>
 842    /// <param name="typeSymbol">The type symbol to check.</param>
 843    /// <param name="interfaceFullName">The full name of the interface.</param>
 844    /// <returns>True if the type implements the interface.</returns>
 845    public static bool ImplementsInterface(INamedTypeSymbol typeSymbol, string interfaceFullName)
 846    {
 0847        foreach (var iface in typeSymbol.AllInterfaces)
 848        {
 0849            if (iface.ToDisplayString() == interfaceFullName)
 0850                return true;
 851        }
 0852        return false;
 853    }
 854
 855    /// <summary>
 856    /// Checks if a type has a public parameterless constructor.
 857    /// </summary>
 858    /// <param name="typeSymbol">The type symbol to check.</param>
 859    /// <returns>True if the type has a parameterless constructor.</returns>
 860    public static bool HasParameterlessConstructor(INamedTypeSymbol typeSymbol)
 861    {
 2669769862        foreach (var ctor in typeSymbol.InstanceConstructors)
 863        {
 794928864            if (ctor.DeclaredAccessibility == Accessibility.Public &&
 794928865                ctor.Parameters.Length == 0)
 866            {
 308217867                return true;
 868            }
 869        }
 870
 871        // If no explicit constructors, the default constructor is available
 872        // (unless there are other constructors with parameters)
 385848873        if (typeSymbol.InstanceConstructors.Length == 0)
 153818874            return true;
 875
 232030876        return false;
 877    }
 878
 879    /// <summary>
 880    /// Gets the attribute types applied to a plugin type.
 881    /// </summary>
 882    /// <param name="typeSymbol">The type symbol to check.</param>
 883    /// <returns>A list of attribute type names (fully qualified).</returns>
 884    public static IReadOnlyList<string> GetPluginAttributes(INamedTypeSymbol typeSymbol)
 885    {
 1379886        var result = new List<string>();
 887
 2870888        foreach (var attribute in typeSymbol.GetAttributes())
 889        {
 56890            var attributeClass = attribute.AttributeClass;
 56891            if (attributeClass == null)
 892                continue;
 893
 894            // Skip system attributes and compiler-generated attributes
 56895            var ns = attributeClass.ContainingNamespace?.ToDisplayString() ?? string.Empty;
 56896            if (ns.StartsWith("System.Runtime.CompilerServices", StringComparison.Ordinal))
 897                continue;
 898
 899            // Include this attribute
 56900            var attributeName = GetFullyQualifiedName(attributeClass);
 56901            if (!result.Contains(attributeName))
 902            {
 56903                result.Add(attributeName);
 904            }
 905        }
 906
 907        // Also check for inherited attributes from base types
 1379908        var baseType = typeSymbol.BaseType;
 5909909        while (baseType != null && baseType.SpecialType != SpecialType.System_Object)
 910        {
 9060911            foreach (var attribute in baseType.GetAttributes())
 912            {
 0913                var attributeClass = attribute.AttributeClass;
 0914                if (attributeClass == null)
 915                    continue;
 916
 917                // Check if attribute is inherited
 0918                if (!IsInheritedAttribute(attributeClass))
 919                    continue;
 920
 0921                var ns = attributeClass.ContainingNamespace?.ToDisplayString() ?? string.Empty;
 0922                if (ns.StartsWith("System.Runtime.CompilerServices", StringComparison.Ordinal))
 923                    continue;
 924
 0925                var attributeName = GetFullyQualifiedName(attributeClass);
 0926                if (!result.Contains(attributeName))
 927                {
 0928                    result.Add(attributeName);
 929                }
 930            }
 931
 4530932            baseType = baseType.BaseType;
 933        }
 934
 1379935        return result;
 936    }
 937
 938    /// <summary>
 939    /// Checks if an attribute type has [AttributeUsage(Inherited = true)].
 940    /// </summary>
 941    private static bool IsInheritedAttribute(INamedTypeSymbol attributeClass)
 942    {
 0943        foreach (var attr in attributeClass.GetAttributes())
 944        {
 0945            if (attr.AttributeClass?.ToDisplayString() != "System.AttributeUsageAttribute")
 946                continue;
 947
 0948            foreach (var namedArg in attr.NamedArguments)
 949            {
 0950                if (namedArg.Key == "Inherited" && namedArg.Value.Value is bool inherited)
 951                {
 0952                    return inherited;
 953                }
 954            }
 955
 956            // Default for AttributeUsage is Inherited = true
 0957            return true;
 958        }
 959
 960        // Default is Inherited = true
 0961        return true;
 962    }
 963
 964    /// <summary>
 965    /// Gets the parameters of the best injectable constructor for a type.
 966    /// Returns the first constructor where all parameters are injectable types.
 967    /// </summary>
 968    /// <param name="typeSymbol">The type symbol to analyze.</param>
 969    /// <returns>
 970    /// A list of fully qualified parameter type names, or null if no injectable constructor was found.
 971    /// </returns>
 972    public static IReadOnlyList<string>? GetBestConstructorParameters(INamedTypeSymbol typeSymbol)
 973    {
 57974        foreach (var ctor in typeSymbol.InstanceConstructors)
 975        {
 18976            if (ctor.IsStatic)
 977                continue;
 978
 18979            if (ctor.DeclaredAccessibility != Accessibility.Public)
 980                continue;
 981
 18982            var parameters = ctor.Parameters;
 983
 984            // Parameterless constructor - no parameters needed
 18985            if (parameters.Length == 0)
 13986                return Array.Empty<string>();
 987
 988            // Single parameter of same type (copy constructor) - skip
 5989            if (parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(parameters[0].Type, typeSymbol))
 990                continue;
 991
 992            // Check if all parameters are injectable
 5993            if (!AllParametersAreInjectable(parameters))
 994                continue;
 995
 996            // This constructor is good - collect parameter type names
 2997            var parameterTypes = new string[parameters.Length];
 8998            for (int i = 0; i < parameters.Length; i++)
 999            {
 21000                parameterTypes[i] = GetFullyQualifiedNameForType(parameters[i].Type);
 1001            }
 21002            return parameterTypes;
 1003        }
 1004
 31005        return null;
 1006    }
 1007
 1008    /// <summary>
 1009    /// Gets the fully qualified name for any type symbol (including generics like Lazy&lt;T&gt;).
 1010    /// </summary>
 1011    private static string GetFullyQualifiedNameForType(ITypeSymbol typeSymbol)
 1012    {
 539641013        return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 1014    }
 1015
 1016    // NOTE: HasKernelFunctions was moved to NexusLabs.Needlr.SemanticKernel.Generators
 1017    // NOTE: HubRegistrationInfo was moved to NexusLabs.Needlr.SignalR.Generators
 1018
 1019    /// <summary>
 1020    /// Represents a constructor parameter with optional keyed service information.
 1021    /// </summary>
 1022    public readonly struct ConstructorParameterInfo
 1023    {
 1024        public ConstructorParameterInfo(string typeName, string? serviceKey = null, string? parameterName = null, string
 1025        {
 540271026            TypeName = typeName;
 540271027            ServiceKey = serviceKey;
 540271028            ParameterName = parameterName;
 540271029            DocumentationComment = documentationComment;
 540271030        }
 1031
 1032        /// <summary>
 1033        /// The fully qualified type name of the parameter.
 1034        /// </summary>
 2698991035        public string TypeName { get; }
 1036
 1037        /// <summary>
 1038        /// The service key from [FromKeyedServices] attribute, or null if not a keyed service.
 1039        /// </summary>
 1619221040        public string? ServiceKey { get; }
 1041
 1042        /// <summary>
 1043        /// The original parameter name from the constructor (used for factory generation).
 1044        /// </summary>
 541441045        public string? ParameterName { get; }
 1046
 1047        /// <summary>
 1048        /// XML documentation comment for this parameter, extracted from the constructor's XML docs.
 1049        /// </summary>
 411050        public string? DocumentationComment { get; }
 1051
 1052        /// <summary>
 1053        /// True if this parameter should be resolved as a keyed service.
 1054        /// </summary>
 1079551055        public bool IsKeyed => ServiceKey is not null;
 1056    }
 1057
 1058    /// <summary>
 1059    /// Gets the parameters of the best injectable constructor for a type, including keyed service info.
 1060    /// Returns the first constructor where all parameters are injectable types.
 1061    /// </summary>
 1062    /// <param name="typeSymbol">The type symbol to analyze.</param>
 1063    /// <returns>
 1064    /// A list of constructor parameter info, or null if no injectable constructor was found.
 1065    /// </returns>
 1066    public static IReadOnlyList<ConstructorParameterInfo>? GetBestConstructorParametersWithKeys(INamedTypeSymbol typeSym
 1067    {
 1068        const string FromKeyedServicesAttributeName = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribu
 1069
 7472111070        foreach (var ctor in typeSymbol.InstanceConstructors)
 1071        {
 2548511072            if (ctor.IsStatic)
 1073                continue;
 1074
 2548511075            if (ctor.DeclaredAccessibility != Accessibility.Public)
 1076                continue;
 1077
 2405251078            var parameters = ctor.Parameters;
 1079
 1080            // Parameterless constructor - no parameters needed
 2405251081            if (parameters.Length == 0)
 1729941082                return Array.Empty<ConstructorParameterInfo>();
 1083
 1084            // Single parameter of same type (copy constructor) - skip
 675311085            if (parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(parameters[0].Type, typeSymbol))
 1086                continue;
 1087
 1088            // Check if all parameters are injectable
 660231089            if (!AllParametersAreInjectable(parameters))
 1090                continue;
 1091
 1092            // This constructor is good - collect parameter info with keyed service keys
 456651093            var parameterInfos = new ConstructorParameterInfo[parameters.Length];
 1992541094            for (int i = 0; i < parameters.Length; i++)
 1095            {
 539621096                var param = parameters[i];
 539621097                var typeName = GetFullyQualifiedNameForType(param.Type);
 539621098                string? serviceKey = null;
 1099
 1100                // Check for [FromKeyedServices("key")] attribute
 1147101101                foreach (var attr in param.GetAttributes())
 1102                {
 33931103                    var attrClass = attr.AttributeClass;
 33931104                    if (attrClass is null)
 1105                        continue;
 1106
 33931107                    var attrFullName = attrClass.ToDisplayString();
 33931108                    if (attrFullName == FromKeyedServicesAttributeName)
 1109                    {
 1110                        // Extract the key from the constructor argument
 01111                        if (attr.ConstructorArguments.Length > 0)
 1112                        {
 01113                            var keyArg = attr.ConstructorArguments[0];
 01114                            if (keyArg.Value is string keyValue)
 1115                            {
 01116                                serviceKey = keyValue;
 1117                            }
 1118                        }
 01119                        break;
 1120                    }
 1121                }
 1122
 539621123                parameterInfos[i] = new ConstructorParameterInfo(typeName, serviceKey);
 1124            }
 456651125            return parameterInfos;
 1126        }
 1127
 94251128        return null;
 1129    }
 1130
 1131    /// <summary>
 1132    /// Gets the service keys from [Keyed] attributes on a type.
 1133    /// </summary>
 1134    /// <param name="typeSymbol">The type symbol to check.</param>
 1135    /// <returns>Array of service keys, or empty array if no [Keyed] attributes found.</returns>
 1136    public static string[] GetKeyedServiceKeys(INamedTypeSymbol typeSymbol)
 1137    {
 2281001138        var keys = new List<string>();
 1139
 12884401140        foreach (var attribute in typeSymbol.GetAttributes())
 1141        {
 4161201142            var attributeClass = attribute.AttributeClass;
 4161201143            if (attributeClass == null)
 1144                continue;
 1145
 4161201146            var name = attributeClass.Name;
 4161201147            var fullName = attributeClass.ToDisplayString();
 1148
 4161201149            if (name == KeyedAttributeName || fullName == KeyedAttributeFullName)
 1150            {
 1151                // Extract the key from the constructor argument
 51152                if (attribute.ConstructorArguments.Length > 0)
 1153                {
 51154                    var keyArg = attribute.ConstructorArguments[0];
 51155                    if (keyArg.Value is string keyValue)
 1156                    {
 51157                        keys.Add(keyValue);
 1158                    }
 1159                }
 1160            }
 1161        }
 1162
 2281001163        return keys.ToArray();
 1164    }
 1165
 1166    // NOTE: TryGetHubRegistrationInfo, TryGetPropertyStringValue, TryGetPropertyTypeValue
 1167    // were moved to NexusLabs.Needlr.SignalR.Generators
 1168
 1169    /// <summary>
 1170    /// Checks if a type has the <c>[DeferToContainer]</c> attribute.
 1171    /// </summary>
 1172    /// <param name="typeSymbol">The type symbol to check.</param>
 1173    /// <returns>True if the type has the DeferToContainer attribute.</returns>
 1174    public static bool HasDeferToContainerAttribute(INamedTypeSymbol typeSymbol)
 1175    {
 22895451176        foreach (var attribute in typeSymbol.GetAttributes())
 1177        {
 7407581178            var attrClass = attribute.AttributeClass;
 7407581179            if (attrClass is null)
 1180                continue;
 1181
 7407581182            var name = attrClass.Name;
 7407581183            var fullName = attrClass.ToDisplayString();
 1184
 7407581185            if (name == DeferToContainerAttributeName || fullName == DeferToContainerAttributeFullName)
 71186                return true;
 1187        }
 1188
 4040111189        return false;
 1190    }
 1191
 1192    /// <summary>
 1193    /// Gets the constructor parameter types declared in the <c>[DeferToContainer]</c> attribute.
 1194    /// </summary>
 1195    /// <param name="typeSymbol">The type symbol to check.</param>
 1196    /// <returns>
 1197    /// A list of fully qualified parameter type names from the attribute,
 1198    /// or null if the attribute is not present.
 1199    /// </returns>
 1200    public static IReadOnlyList<string>? GetDeferToContainerParameterTypes(INamedTypeSymbol typeSymbol)
 1201    {
 12883951202        foreach (var attribute in typeSymbol.GetAttributes())
 1203        {
 4161151204            var attrClass = attribute.AttributeClass;
 4161151205            if (attrClass is null)
 1206                continue;
 1207
 4161151208            var name = attrClass.Name;
 4161151209            var fullName = attrClass.ToDisplayString();
 1210
 4161151211            if (name != DeferToContainerAttributeName && fullName != DeferToContainerAttributeFullName)
 1212                continue;
 1213
 1214            // The attribute has a params Type[] constructor parameter
 1215            // Check constructor arguments
 91216            if (attribute.ConstructorArguments.Length == 0)
 01217                return Array.Empty<string>();
 1218
 91219            var arg = attribute.ConstructorArguments[0];
 1220
 1221            // params array is passed as a single array argument
 91222            if (arg.Kind == TypedConstantKind.Array)
 1223            {
 91224                var types = new List<string>();
 401225                foreach (var element in arg.Values)
 1226                {
 111227                    if (element.Value is INamedTypeSymbol namedType)
 1228                    {
 111229                        types.Add(GetFullyQualifiedName(namedType));
 1230                    }
 1231                }
 91232                return types;
 1233            }
 1234        }
 1235
 2280781236        return null;
 1237    }
 1238
 1239    /// <summary>
 1240    /// Result of decorator discovery.
 1241    /// </summary>
 1242    public readonly struct DecoratorInfo
 1243    {
 1244        public DecoratorInfo(string decoratorTypeName, string serviceTypeName, int order)
 1245        {
 201246            DecoratorTypeName = decoratorTypeName;
 201247            ServiceTypeName = serviceTypeName;
 201248            Order = order;
 201249        }
 1250
 201251        public string DecoratorTypeName { get; }
 201252        public string ServiceTypeName { get; }
 201253        public int Order { get; }
 1254    }
 1255
 1256    /// <summary>
 1257    /// Gets all DecoratorFor&lt;T&gt; attributes applied to a type.
 1258    /// </summary>
 1259    /// <param name="typeSymbol">The type symbol to check.</param>
 1260    /// <returns>A list of decorator info for each DecoratorFor attribute found.</returns>
 1261    public static IReadOnlyList<DecoratorInfo> GetDecoratorForAttributes(INamedTypeSymbol typeSymbol)
 1262    {
 14202541263        var result = new List<DecoratorInfo>();
 1264
 68281741265        foreach (var attribute in typeSymbol.GetAttributes())
 1266        {
 19938331267            var attrClass = attribute.AttributeClass;
 19938331268            if (attrClass is null)
 1269                continue;
 1270
 1271            // Check if this is a generic DecoratorForAttribute<T>
 19938331272            if (!attrClass.IsGenericType)
 1273                continue;
 1274
 361275            var unboundTypeName = attrClass.ConstructedFrom?.ToDisplayString();
 361276            if (unboundTypeName is null || !unboundTypeName.StartsWith(DecoratorForAttributePrefix, StringComparison.Ord
 1277                continue;
 1278
 1279            // Get the service type from the generic type argument
 201280            if (attrClass.TypeArguments.Length != 1)
 1281                continue;
 1282
 201283            var serviceType = attrClass.TypeArguments[0] as INamedTypeSymbol;
 201284            if (serviceType is null)
 1285                continue;
 1286
 201287            var serviceTypeName = GetFullyQualifiedName(serviceType);
 201288            var decoratorTypeName = GetFullyQualifiedName(typeSymbol);
 1289
 1290            // Get the Order property value
 201291            int order = 0;
 581292            foreach (var namedArg in attribute.NamedArguments)
 1293            {
 181294                if (namedArg.Key == "Order" && namedArg.Value.Value is int orderValue)
 1295                {
 181296                    order = orderValue;
 181297                    break;
 1298                }
 1299            }
 1300
 201301            result.Add(new DecoratorInfo(decoratorTypeName, serviceTypeName, order));
 1302        }
 1303
 14202541304        return result;
 1305    }
 1306
 1307    /// <summary>
 1308    /// Checks if a type has any DecoratorFor&lt;T&gt; attributes.
 1309    /// </summary>
 1310    /// <param name="typeSymbol">The type symbol to check.</param>
 1311    /// <returns>True if the type has at least one DecoratorFor attribute.</returns>
 1312    public static bool HasDecoratorForAttribute(INamedTypeSymbol typeSymbol)
 1313    {
 521314        foreach (var attribute in typeSymbol.GetAttributes())
 1315        {
 91316            var attrClass = attribute.AttributeClass;
 91317            if (attrClass is null)
 1318                continue;
 1319
 91320            if (!attrClass.IsGenericType)
 1321                continue;
 1322
 41323            var unboundTypeName = attrClass.ConstructedFrom?.ToDisplayString();
 41324            if (unboundTypeName is not null && unboundTypeName.StartsWith(DecoratorForAttributePrefix, StringComparison.
 21325                return true;
 1326        }
 1327
 161328        return false;
 1329    }
 1330}

Methods/Properties

IsInjectableType(Microsoft.CodeAnalysis.INamedTypeSymbol,System.Boolean)
GetRegisterableInterfaces(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetRegisterAsInterfaces(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetConstructorParameterTypes(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsDecoratorInterface(Microsoft.CodeAnalysis.INamedTypeSymbol,System.Collections.Generic.HashSet`1<System.String>)
GetFullyQualifiedName(Microsoft.CodeAnalysis.INamedTypeSymbol)
MatchesNamespacePrefix(Microsoft.CodeAnalysis.INamedTypeSymbol,System.Collections.Generic.IReadOnlyList`1<System.String>)
GetAllTypes()
HasDoNotAutoRegisterAttribute(Microsoft.CodeAnalysis.INamedTypeSymbol)
HasDoNotAutoRegisterAttributeDirect(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsCompilerGenerated(Microsoft.CodeAnalysis.INamedTypeSymbol)
InheritsFrom(Microsoft.CodeAnalysis.INamedTypeSymbol,System.String)
IsSystemInterface(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsHostedServiceInterface(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsHostedServiceType(Microsoft.CodeAnalysis.INamedTypeSymbol,System.Boolean)
IsDecoratorForHostedService(Microsoft.CodeAnalysis.INamedTypeSymbol)
InheritsFromBackgroundService(Microsoft.CodeAnalysis.INamedTypeSymbol)
ImplementsIHostedService(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsSystemType(Microsoft.CodeAnalysis.INamedTypeSymbol)
HasUnsatisfiedRequiredMembers(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsAccessibleFromGeneratedCode(Microsoft.CodeAnalysis.INamedTypeSymbol,System.Boolean)
WouldBeInjectableIgnoringAccessibility(Microsoft.CodeAnalysis.INamedTypeSymbol)
WouldBePluginIgnoringAccessibility(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsInternalOrLessAccessible(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsDisposableType(Microsoft.CodeAnalysis.INamedTypeSymbol)
.cctor()
ImplementsNeedlrPluginInterface(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetNeedlrPluginInterfaceName(Microsoft.CodeAnalysis.INamedTypeSymbol)
HasGenerateTypeRegistryAttribute(Microsoft.CodeAnalysis.IAssemblySymbol)
IsPubliclyAccessible(Microsoft.CodeAnalysis.INamedTypeSymbol)
DetermineLifetime(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetExplicitLifetime(Microsoft.CodeAnalysis.INamedTypeSymbol)
AllParametersAreInjectable(System.Collections.Immutable.ImmutableArray`1<Microsoft.CodeAnalysis.IParameterSymbol>)
IsInjectableParameterType(Microsoft.CodeAnalysis.ITypeSymbol)
HasDoNotInjectAttribute(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsPluginType(Microsoft.CodeAnalysis.INamedTypeSymbol,System.Boolean)
GetPluginInterfaces(Microsoft.CodeAnalysis.INamedTypeSymbol)
ImplementsInterface(Microsoft.CodeAnalysis.INamedTypeSymbol,System.String)
HasParameterlessConstructor(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetPluginAttributes(Microsoft.CodeAnalysis.INamedTypeSymbol)
IsInheritedAttribute(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetBestConstructorParameters(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetFullyQualifiedNameForType(Microsoft.CodeAnalysis.ITypeSymbol)
.ctor(System.String,System.String,System.String,System.String)
get_TypeName()
get_ServiceKey()
get_ParameterName()
get_DocumentationComment()
get_IsKeyed()
GetBestConstructorParametersWithKeys(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetKeyedServiceKeys(Microsoft.CodeAnalysis.INamedTypeSymbol)
HasDeferToContainerAttribute(Microsoft.CodeAnalysis.INamedTypeSymbol)
GetDeferToContainerParameterTypes(Microsoft.CodeAnalysis.INamedTypeSymbol)
.ctor(System.String,System.String,System.Int32)
get_DecoratorTypeName()
get_ServiceTypeName()
get_Order()
GetDecoratorForAttributes(Microsoft.CodeAnalysis.INamedTypeSymbol)
HasDecoratorForAttribute(Microsoft.CodeAnalysis.INamedTypeSymbol)