< 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: 1332
Line coverage: 86.2%
Branch coverage
83%
Covered branches: 363
Total branches: 436
Branch coverage: 83.2%
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(...)90%202089.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)
 143939848        => 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
 23115460        var registerAsInterfaces = GetRegisterAsInterfaces(typeSymbol);
 23115461        if (registerAsInterfaces.Count > 0)
 62        {
 063            return registerAsInterfaces;
 64        }
 65
 23115466        var result = new List<INamedTypeSymbol>();
 67
 68        // Get all constructor parameter types to detect decorator pattern
 23115469        var constructorParamTypes = GetConstructorParameterTypes(typeSymbol);
 70
 88315871        foreach (var iface in typeSymbol.AllInterfaces)
 72        {
 21042573            if (iface.IsUnboundGenericType)
 74                continue;
 75
 21042576            if (IsSystemInterface(iface))
 77                continue;
 78
 31579            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
 31385            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
 28690            if (IsHostedServiceInterface(iface))
 91                continue;
 92
 27993            result.Add(iface);
 94        }
 95
 23115496        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    {
 231154106        var result = new List<INamedTypeSymbol>();
 107
 1305698108        foreach (var attribute in typeSymbol.GetAttributes())
 109        {
 421695110            var attrClass = attribute.AttributeClass;
 421695111            if (attrClass == null)
 112                continue;
 113
 114            // Check for RegisterAsAttribute<T>
 421695115            var attrFullName = attrClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 421695116            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
 231154129        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    {
 231154137        var paramTypes = new HashSet<string>(StringComparer.Ordinal);
 138
 1489982139        foreach (var ctor in typeSymbol.InstanceConstructors)
 140        {
 513837141            if (ctor.IsStatic)
 142                continue;
 143
 2206754144            foreach (var param in ctor.Parameters)
 145            {
 589540146                var paramTypeName = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 589540147                paramTypes.Add(paramTypeName);
 148            }
 149        }
 150
 231154151        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    {
 313159        var ifaceName = iface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 313160        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<>
 4187309175        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)
 406529180            var hasUnresolvedTypeParameters = typeSymbol.TypeArguments.Any(ta => ta.TypeKind == TypeKind.TypeParameter);
 181
 200205182            if (hasUnresolvedTypeParameters)
 183            {
 184                // Build the open generic name manually
 60372185                var containingNamespace = typeSymbol.ContainingNamespace?.ToDisplayString();
 60372186                var typeName = typeSymbol.Name;
 60372187                var arity = typeSymbol.TypeParameters.Length;
 188
 189                // Create the open generic syntax: MyClass<,> for 2 type params, MyClass<> for 1
 60372190                var commas = arity > 1 ? new string(',', arity - 1) : string.Empty;
 60372191                var openGenericPart = $"<{commas}>";
 192
 60372193                if (string.IsNullOrEmpty(containingNamespace) || containingNamespace == "<global namespace>")
 194                {
 0195                    return $"global::{typeName}{openGenericPart}";
 196                }
 197
 60372198                return $"global::{containingNamespace}.{typeName}{openGenericPart}";
 199            }
 200        }
 201
 4126937202        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    {
 1744163213        if (namespacePrefixes == null || namespacePrefixes.Count == 0)
 1511619214            return true;
 215
 232544216        var typeNamespace = typeSymbol.ContainingNamespace?.ToDisplayString() ?? string.Empty;
 217
 218        // Check if type is in the global namespace
 232544219        var isGlobalNamespace = typeSymbol.ContainingNamespace?.IsGlobalNamespace == true;
 220
 937603221        foreach (var prefix in namespacePrefixes)
 222        {
 223            // Empty string prefix matches global namespace types
 236526224            if (string.IsNullOrEmpty(prefix))
 225            {
 7963226                if (isGlobalNamespace)
 348227                    return true;
 228                continue;
 229            }
 230
 228563231            if (typeNamespace.StartsWith(prefix, StringComparison.Ordinal))
 189232                return true;
 233        }
 234
 232007235        return false;
 537236    }
 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    {
 10402152245        foreach (var member in namespaceSymbol.GetMembers())
 246        {
 4269601247            if (member is INamedTypeSymbol typeSymbol)
 248            {
 3487146249                yield return typeSymbol;
 250            }
 782455251            else if (member is INamespaceSymbol nestedNamespace)
 252            {
 19347410253                foreach (var nestedType in GetAllTypes(nestedNamespace))
 254                {
 8891250255                    yield return nestedType;
 256                }
 257            }
 258        }
 931475259    }
 260
 261    private static bool HasDoNotAutoRegisterAttribute(INamedTypeSymbol typeSymbol)
 3836262        => SharedHelper.HasDoNotAutoRegisterAttribute(typeSymbol);
 263
 264    private static bool HasDoNotAutoRegisterAttributeDirect(INamedTypeSymbol typeSymbol)
 762389265        => SharedHelper.HasDoNotAutoRegisterAttributeDirect(typeSymbol);
 266
 267    private static bool IsCompilerGenerated(INamedTypeSymbol typeSymbol)
 833485268        => SharedHelper.IsCompilerGenerated(typeSymbol);
 269
 270    private static bool InheritsFrom(INamedTypeSymbol typeSymbol, string baseTypeName)
 9200271        => SharedHelper.InheritsFrom(typeSymbol, baseTypeName);
 272
 273    private static bool IsSystemInterface(INamedTypeSymbol interfaceSymbol)
 504472274        => SharedHelper.IsSystemType(interfaceSymbol);
 275
 276    private static bool IsHostedServiceInterface(INamedTypeSymbol interfaceSymbol)
 277    {
 286278        var fullName = GetFullyQualifiedName(interfaceSymbol);
 286279        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
 1439381291        if (typeSymbol.IsAbstract || typeSymbol.TypeKind != TypeKind.Class)
 677307292            return false;
 293
 294        // Check accessibility
 762074295        if (!isCurrentAssembly && IsInternalOrLessAccessible(typeSymbol))
 0296            return false;
 297
 298        // Skip if marked with [DoNotAutoRegister]
 762074299        if (HasDoNotAutoRegisterAttributeDirect(typeSymbol))
 2300            return false;
 301
 302        // Skip compiler-generated types
 762072303        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)
 762072308        if (IsDecoratorForHostedService(typeSymbol))
 0309            return false;
 310
 311        // Check if inherits from BackgroundService
 762072312        if (InheritsFromBackgroundService(typeSymbol))
 6313            return true;
 314
 315        // Check if directly implements IHostedService (not via BackgroundService)
 762066316        if (ImplementsIHostedService(typeSymbol))
 1317            return true;
 318
 762065319        return false;
 320    }
 321
 322    private static bool IsDecoratorForHostedService(INamedTypeSymbol typeSymbol)
 323    {
 4397132324        foreach (var attribute in typeSymbol.GetAttributes())
 325        {
 1436494326            var attrClass = attribute.AttributeClass;
 1436494327            if (attrClass == null)
 328                continue;
 329
 1436494330            var attrFullName = attrClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 331
 332            // Check for DecoratorForAttribute<IHostedService>
 1436494333            if (attrFullName.StartsWith("global::NexusLabs.Needlr.DecoratorForAttribute<", StringComparison.Ordinal))
 334            {
 335                // Get the type argument
 19336                if (attrClass.IsGenericType && attrClass.TypeArguments.Length == 1)
 337                {
 19338                    var typeArg = attrClass.TypeArguments[0];
 19339                    if (typeArg is INamedTypeSymbol namedTypeArg)
 340                    {
 19341                        var typeArgName = GetFullyQualifiedName(namedTypeArg);
 19342                        if (typeArgName == "global::Microsoft.Extensions.Hosting.IHostedService")
 0343                            return true;
 344                    }
 345                }
 346            }
 347        }
 762072348        return false;
 349    }
 350
 351    private static bool InheritsFromBackgroundService(INamedTypeSymbol typeSymbol)
 352    {
 1396455353        var baseType = typeSymbol.BaseType;
 4179551354        while (baseType != null)
 355        {
 2783109356            var fullName = GetFullyQualifiedName(baseType);
 2783109357            if (fullName == "global::Microsoft.Extensions.Hosting.BackgroundService")
 13358                return true;
 2783096359            baseType = baseType.BaseType;
 360        }
 1396442361        return false;
 362    }
 363
 364    private static bool ImplementsIHostedService(INamedTypeSymbol typeSymbol)
 365    {
 4756002366        foreach (var iface in typeSymbol.AllInterfaces)
 367        {
 981560368            var fullName = GetFullyQualifiedName(iface);
 981560369            if (fullName == "global::Microsoft.Extensions.Hosting.IHostedService")
 2370                return true;
 371        }
 1396440372        return false;
 373    }
 374
 375    private static bool IsSystemType(INamedTypeSymbol typeSymbol)
 404184376        => SharedHelper.IsSystemType(typeSymbol);
 377
 378    private static bool HasUnsatisfiedRequiredMembers(INamedTypeSymbol typeSymbol)
 400542379        => 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)
 899989390        => 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)
 72790401        if (typeSymbol.TypeKind != TypeKind.Class)
 14402            return false;
 403
 72776404        if (typeSymbol.IsAbstract)
 0405            return false;
 406
 72776407        if (typeSymbol.IsStatic)
 1160408            return false;
 409
 71616410        if (typeSymbol.IsUnboundGenericType)
 0411            return false;
 412
 413        // Exclude open generic types (type definitions with type parameters like MyClass<T>)
 71616414        if (typeSymbol.TypeParameters.Length > 0)
 203415            return false;
 416
 71413417        if (typeSymbol.ContainingType != null)
 0418            return false;
 419
 71413420        if (IsCompilerGenerated(typeSymbol))
 66813421            return false;
 422
 4600423        if (InheritsFrom(typeSymbol, "System.Exception"))
 0424            return false;
 425
 4600426        if (InheritsFrom(typeSymbol, "System.Attribute"))
 764427            return false;
 428
 3836429        if (typeSymbol.IsRecord)
 0430            return false;
 431
 3836432        if (HasDoNotAutoRegisterAttribute(typeSymbol))
 2433            return false;
 434
 435        // Must have a determinable lifetime to be injectable
 3834436        var lifetime = DetermineLifetime(typeSymbol);
 3834437        if (!lifetime.HasValue)
 1528438            return false;
 439
 2306440        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
 70502452        if (typeSymbol.TypeKind != TypeKind.Class)
 14453            return false;
 454
 70488455        if (typeSymbol.IsAbstract)
 0456            return false;
 457
 70488458        if (typeSymbol.IsStatic)
 1160459            return false;
 460
 69328461        if (typeSymbol.IsUnboundGenericType)
 0462            return false;
 463
 464        // Exclude open generic types (type definitions with type parameters like MyClass<T>)
 69328465        if (typeSymbol.TypeParameters.Length > 0)
 203466            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
 69125472        if (!HasParameterlessConstructor(typeSymbol))
 1518473            return false;
 474
 475        // Must have at least one plugin interface
 67607476        var pluginInterfaces = GetPluginInterfaces(typeSymbol);
 67607477        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
 4015554489        if (typeSymbol.DeclaredAccessibility != Accessibility.Public)
 156000490            return true;
 491
 492        // Check all containing types (for nested types)
 3859554493        var containingType = typeSymbol.ContainingType;
 3859554494        while (containingType != null)
 495        {
 0496            if (containingType.DeclaredAccessibility != Accessibility.Public)
 0497                return true;
 0498            containingType = containingType.ContainingType;
 499        }
 500
 3859554501        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    {
 774091511        foreach (var iface in typeSymbol.AllInterfaces)
 512        {
 185174513            var fullName = GetFullyQualifiedName(iface);
 185174514            if (fullName == "global::System.IDisposable" || fullName == "global::System.IAsyncDisposable")
 58455515                return true;
 516        }
 172644517        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    {
 170077542        foreach (var iface in typeSymbol.AllInterfaces)
 543        {
 1788544            var ifaceName = iface.ToDisplayString();
 32169545            foreach (var pluginInterface in NeedlrPluginInterfaceNames)
 546            {
 14297547                if (ifaceName == pluginInterface)
 1548                    return true;
 549            }
 550        }
 83250551        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
 9527136582        foreach (var attribute in assembly.GetAttributes())
 583        {
 4523975584            var attrClass = attribute.AttributeClass;
 4523975585            if (attrClass == null)
 586                continue;
 587
 4523975588            if (attrClass.ToDisplayString() == attributeName)
 68589                return true;
 590        }
 591
 239559592        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
 409367628        if (HasDoNotInjectAttribute(typeSymbol))
 1629            return null;
 630
 631        // Exclude open generic types (type definitions with type parameters like MyClass<T>)
 409366632        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)
 409365637        if (HasDeferToContainerAttribute(typeSymbol))
 6638            return GetExplicitLifetime(typeSymbol) ?? GeneratorLifetime.Singleton;
 639
 640        // Get all instance constructors
 409359641        var constructors = typeSymbol.InstanceConstructors;
 642
 1454833643        foreach (var ctor in constructors)
 644        {
 645            // Skip static constructors
 434773646            if (ctor.IsStatic)
 647                continue;
 648
 434773649            var parameters = ctor.Parameters;
 650
 651            // Parameterless constructor is always valid
 434773652            if (parameters.Length == 0)
 188683653                return GetExplicitLifetime(typeSymbol) ?? GeneratorLifetime.Singleton;
 654
 655            // Single parameter of same type (copy constructor) - not injectable
 246090656            if (parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(parameters[0].Type, typeSymbol))
 657                continue;
 658
 659            // Check if all parameters are injectable types
 238831660            if (AllParametersAreInjectable(parameters))
 44748661                return GetExplicitLifetime(typeSymbol) ?? GeneratorLifetime.Singleton;
 662        }
 663
 175928664        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    {
 1310154672        foreach (var attribute in typeSymbol.GetAttributes())
 673        {
 421672674            var attributeClass = attribute.AttributeClass;
 421672675            if (attributeClass == null)
 676                continue;
 677
 421672678            var name = attributeClass.Name;
 421672679            var fullName = attributeClass.ToDisplayString();
 680
 421672681            if (name == TransientAttributeName || fullName == TransientAttributeFullName)
 5682                return GeneratorLifetime.Transient;
 683
 421667684            if (name == ScopedAttributeName || fullName == ScopedAttributeFullName)
 36685                return GeneratorLifetime.Scoped;
 686
 421631687            if (name == SingletonAttributeName || fullName == SingletonAttributeFullName)
 23688                return GeneratorLifetime.Singleton;
 689        }
 690
 233373691        return null;
 692    }
 693
 694    private static bool AllParametersAreInjectable(System.Collections.Immutable.ImmutableArray<IParameterSymbol> paramet
 695    {
 1142022696        foreach (var param in parameters)
 697        {
 372633698            if (!IsInjectableParameterType(param.Type))
 214714699                return false;
 700        }
 91021701        return true;
 702    }
 703
 704    private static bool IsInjectableParameterType(ITypeSymbol typeSymbol)
 705    {
 706        // Must not be a delegate
 372633707        if (typeSymbol.TypeKind == TypeKind.Delegate)
 7260708            return false;
 709
 710        // Must not be a value type
 365373711        if (typeSymbol.IsValueType)
 96265712            return false;
 713
 714        // Must not be string
 269108715        if (typeSymbol.SpecialType == SpecialType.System_String)
 89013716            return false;
 717
 718        // Must be a class or interface
 180095719        if (typeSymbol.TypeKind != TypeKind.Class && typeSymbol.TypeKind != TypeKind.Interface)
 22176720            return false;
 721
 157919722        return true;
 723    }
 724
 725    private static bool HasDoNotInjectAttribute(INamedTypeSymbol typeSymbol)
 726    {
 2319911727        foreach (var attribute in typeSymbol.GetAttributes())
 728        {
 750589729            var attributeClass = attribute.AttributeClass;
 750589730            if (attributeClass == null)
 731                continue;
 732
 750589733            var name = attributeClass.Name;
 750589734            if (name == DoNotInjectAttributeName)
 1735                return true;
 736
 750588737            var fullName = attributeClass.ToDisplayString();
 750588738            if (fullName == DoNotInjectAttributeFullName)
 0739                return true;
 740        }
 741
 409366742        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
 1439387754        if (typeSymbol.TypeKind != TypeKind.Class)
 539398755            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
 899989760        if (!IsAccessibleFromGeneratedCode(typeSymbol, isCurrentAssembly))
 0761            return false;
 762
 899989763        if (typeSymbol.IsAbstract)
 137909764            return false;
 765
 762080766        if (typeSymbol.IsStatic)
 87743767            return false;
 768
 674337769        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
 674337774        if (typeSymbol.TypeParameters.Length > 0)
 39954775            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        // IMPORTANT: If the plugin type is emitted by a DIFFERENT source generator, TypeRegistryGenerator
 781        // cannot see it (Roslyn generators receive the original compilation in isolation). In that case,
 782        // the other generator should emit a [ModuleInitializer] that calls
 783        // NeedlrSourceGenBootstrap.RegisterPlugins(() => [...]) to contribute those types at runtime.
 784
 785        // Exclude hosted service types â€” they have their own dedicated registration path
 786        // (RegisterHostedServices) and must not be included in plugin types.
 634383787        if (InheritsFromBackgroundService(typeSymbol) || ImplementsIHostedService(typeSymbol))
 8788            return false;
 789
 790        // Must have a parameterless constructor
 634375791        if (!HasParameterlessConstructor(typeSymbol))
 233833792            return false;
 793
 794        // Exclude types with required members that can't be set via constructor
 795        // These would cause compilation errors: "Required member 'X' must be set"
 400542796        if (HasUnsatisfiedRequiredMembers(typeSymbol))
 2797            return false;
 798
 400540799        return true;
 800    }
 801
 802    /// <summary>
 803    /// Gets the plugin base types (interfaces and base classes) for a type.
 804    /// Plugin base types are non-System interfaces and non-System/non-object base classes.
 805    /// </summary>
 806    /// <param name="typeSymbol">The type symbol to check.</param>
 807    /// <returns>A list of plugin base type symbols (interfaces and base classes).</returns>
 808    public static IReadOnlyList<INamedTypeSymbol> GetPluginInterfaces(INamedTypeSymbol typeSymbol)
 809    {
 468144810        var result = new List<INamedTypeSymbol>();
 811
 812        // Add non-system interfaces
 1524382813        foreach (var iface in typeSymbol.AllInterfaces)
 814        {
 294047815            if (iface.IsUnboundGenericType)
 816                continue;
 817
 294047818            if (IsSystemInterface(iface))
 819                continue;
 820
 256821            result.Add(iface);
 822        }
 823
 824        // Add non-system base classes (walking up the hierarchy)
 468144825        var baseType = typeSymbol.BaseType;
 469291826        while (baseType != null)
 827        {
 828            // Stop at System.Object or System types
 404184829            if (IsSystemType(baseType))
 830                break;
 831
 832            // Skip abstract types that can't be instantiated directly
 833            // but include them as they represent the plugin contract
 1147834            result.Add(baseType);
 1147835            baseType = baseType.BaseType;
 836        }
 837
 468144838        return result;
 839    }
 840
 841    /// <summary>
 842    /// Checks if a type implements a specific interface by name.
 843    /// </summary>
 844    /// <param name="typeSymbol">The type symbol to check.</param>
 845    /// <param name="interfaceFullName">The full name of the interface.</param>
 846    /// <returns>True if the type implements the interface.</returns>
 847    public static bool ImplementsInterface(INamedTypeSymbol typeSymbol, string interfaceFullName)
 848    {
 0849        foreach (var iface in typeSymbol.AllInterfaces)
 850        {
 0851            if (iface.ToDisplayString() == interfaceFullName)
 0852                return true;
 853        }
 0854        return false;
 855    }
 856
 857    /// <summary>
 858    /// Checks if a type has a public parameterless constructor.
 859    /// </summary>
 860    /// <param name="typeSymbol">The type symbol to check.</param>
 861    /// <returns>True if the type has a parameterless constructor.</returns>
 862    public static bool HasParameterlessConstructor(INamedTypeSymbol typeSymbol)
 863    {
 2706129864        foreach (var ctor in typeSymbol.InstanceConstructors)
 865        {
 805717866            if (ctor.DeclaredAccessibility == Accessibility.Public &&
 805717867                ctor.Parameters.Length == 0)
 868            {
 312305869                return true;
 870            }
 871        }
 872
 873        // If no explicit constructors, the default constructor is available
 874        // (unless there are other constructors with parameters)
 391195875        if (typeSymbol.InstanceConstructors.Length == 0)
 155844876            return true;
 877
 235351878        return false;
 879    }
 880
 881    /// <summary>
 882    /// Gets the attribute types applied to a plugin type.
 883    /// </summary>
 884    /// <param name="typeSymbol">The type symbol to check.</param>
 885    /// <returns>A list of attribute type names (fully qualified).</returns>
 886    public static IReadOnlyList<string> GetPluginAttributes(INamedTypeSymbol typeSymbol)
 887    {
 1383888        var result = new List<string>();
 889
 2886890        foreach (var attribute in typeSymbol.GetAttributes())
 891        {
 60892            var attributeClass = attribute.AttributeClass;
 60893            if (attributeClass == null)
 894                continue;
 895
 896            // Skip system attributes and compiler-generated attributes
 60897            var ns = attributeClass.ContainingNamespace?.ToDisplayString() ?? string.Empty;
 60898            if (ns.StartsWith("System.Runtime.CompilerServices", StringComparison.Ordinal))
 899                continue;
 900
 901            // Include this attribute
 60902            var attributeName = GetFullyQualifiedName(attributeClass);
 60903            if (!result.Contains(attributeName))
 904            {
 60905                result.Add(attributeName);
 906            }
 907        }
 908
 909        // Also check for inherited attributes from base types
 1383910        var baseType = typeSymbol.BaseType;
 5968911        while (baseType != null && baseType.SpecialType != SpecialType.System_Object)
 912        {
 9170913            foreach (var attribute in baseType.GetAttributes())
 914            {
 0915                var attributeClass = attribute.AttributeClass;
 0916                if (attributeClass == null)
 917                    continue;
 918
 919                // Check if attribute is inherited
 0920                if (!IsInheritedAttribute(attributeClass))
 921                    continue;
 922
 0923                var ns = attributeClass.ContainingNamespace?.ToDisplayString() ?? string.Empty;
 0924                if (ns.StartsWith("System.Runtime.CompilerServices", StringComparison.Ordinal))
 925                    continue;
 926
 0927                var attributeName = GetFullyQualifiedName(attributeClass);
 0928                if (!result.Contains(attributeName))
 929                {
 0930                    result.Add(attributeName);
 931                }
 932            }
 933
 4585934            baseType = baseType.BaseType;
 935        }
 936
 1383937        return result;
 938    }
 939
 940    /// <summary>
 941    /// Checks if an attribute type has [AttributeUsage(Inherited = true)].
 942    /// </summary>
 943    private static bool IsInheritedAttribute(INamedTypeSymbol attributeClass)
 944    {
 0945        foreach (var attr in attributeClass.GetAttributes())
 946        {
 0947            if (attr.AttributeClass?.ToDisplayString() != "System.AttributeUsageAttribute")
 948                continue;
 949
 0950            foreach (var namedArg in attr.NamedArguments)
 951            {
 0952                if (namedArg.Key == "Inherited" && namedArg.Value.Value is bool inherited)
 953                {
 0954                    return inherited;
 955                }
 956            }
 957
 958            // Default for AttributeUsage is Inherited = true
 0959            return true;
 960        }
 961
 962        // Default is Inherited = true
 0963        return true;
 964    }
 965
 966    /// <summary>
 967    /// Gets the parameters of the best injectable constructor for a type.
 968    /// Returns the first constructor where all parameters are injectable types.
 969    /// </summary>
 970    /// <param name="typeSymbol">The type symbol to analyze.</param>
 971    /// <returns>
 972    /// A list of fully qualified parameter type names, or null if no injectable constructor was found.
 973    /// </returns>
 974    public static IReadOnlyList<string>? GetBestConstructorParameters(INamedTypeSymbol typeSymbol)
 975    {
 57976        foreach (var ctor in typeSymbol.InstanceConstructors)
 977        {
 18978            if (ctor.IsStatic)
 979                continue;
 980
 18981            if (ctor.DeclaredAccessibility != Accessibility.Public)
 982                continue;
 983
 18984            var parameters = ctor.Parameters;
 985
 986            // Parameterless constructor - no parameters needed
 18987            if (parameters.Length == 0)
 13988                return Array.Empty<string>();
 989
 990            // Single parameter of same type (copy constructor) - skip
 5991            if (parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(parameters[0].Type, typeSymbol))
 992                continue;
 993
 994            // Check if all parameters are injectable
 5995            if (!AllParametersAreInjectable(parameters))
 996                continue;
 997
 998            // This constructor is good - collect parameter type names
 2999            var parameterTypes = new string[parameters.Length];
 81000            for (int i = 0; i < parameters.Length; i++)
 1001            {
 21002                parameterTypes[i] = GetFullyQualifiedNameForType(parameters[i].Type);
 1003            }
 21004            return parameterTypes;
 1005        }
 1006
 31007        return null;
 1008    }
 1009
 1010    /// <summary>
 1011    /// Gets the fully qualified name for any type symbol (including generics like Lazy&lt;T&gt;).
 1012    /// </summary>
 1013    private static string GetFullyQualifiedNameForType(ITypeSymbol typeSymbol)
 1014    {
 546811015        return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 1016    }
 1017
 1018    // NOTE: HasKernelFunctions was moved to NexusLabs.Needlr.SemanticKernel.Generators
 1019    // NOTE: HubRegistrationInfo was moved to NexusLabs.Needlr.SignalR.Generators
 1020
 1021    /// <summary>
 1022    /// Represents a constructor parameter with optional keyed service information.
 1023    /// </summary>
 1024    public readonly struct ConstructorParameterInfo
 1025    {
 1026        public ConstructorParameterInfo(string typeName, string? serviceKey = null, string? parameterName = null, string
 1027        {
 547411028            TypeName = typeName;
 547411029            ServiceKey = serviceKey;
 547411030            ParameterName = parameterName;
 547411031            DocumentationComment = documentationComment;
 547411032        }
 1033
 1034        /// <summary>
 1035        /// The fully qualified type name of the parameter.
 1036        /// </summary>
 2744731037        public string TypeName { get; }
 1038
 1039        /// <summary>
 1040        /// The service key from [FromKeyedServices] attribute, or null if not a keyed service.
 1041        /// </summary>
 1652251042        public string? ServiceKey { get; }
 1043
 1044        /// <summary>
 1045        /// The original parameter name from the constructor (used for factory generation).
 1046        /// </summary>
 554221047        public string? ParameterName { get; }
 1048
 1049        /// <summary>
 1050        /// XML documentation comment for this parameter, extracted from the constructor's XML docs.
 1051        /// </summary>
 381052        public string? DocumentationComment { get; }
 1053
 1054        /// <summary>
 1055        /// True if this parameter should be resolved as a keyed service.
 1056        /// </summary>
 1099651057        public bool IsKeyed => ServiceKey is not null;
 1058    }
 1059
 1060    /// <summary>
 1061    /// Gets the parameters of the best injectable constructor for a type, including keyed service info.
 1062    /// Returns the first constructor where all parameters are injectable types.
 1063    /// </summary>
 1064    /// <param name="typeSymbol">The type symbol to analyze.</param>
 1065    /// <returns>
 1066    /// A list of constructor parameter info, or null if no injectable constructor was found.
 1067    /// </returns>
 1068    public static IReadOnlyList<ConstructorParameterInfo>? GetBestConstructorParametersWithKeys(INamedTypeSymbol typeSym
 1069    {
 1070        const string FromKeyedServicesAttributeName = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribu
 1071
 7570971072        foreach (var ctor in typeSymbol.InstanceConstructors)
 1073        {
 2582231074            if (ctor.IsStatic)
 1075                continue;
 1076
 2582231077            if (ctor.DeclaredAccessibility != Accessibility.Public)
 1078                continue;
 1079
 2437071080            var parameters = ctor.Parameters;
 1081
 1082            // Parameterless constructor - no parameters needed
 2437071083            if (parameters.Length == 0)
 1752801084                return Array.Empty<ConstructorParameterInfo>();
 1085
 1086            // Single parameter of same type (copy constructor) - skip
 684271087            if (parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(parameters[0].Type, typeSymbol))
 1088                continue;
 1089
 1090            // Check if all parameters are injectable
 668991091            if (!AllParametersAreInjectable(parameters))
 1092                continue;
 1093
 1094            // This constructor is good - collect parameter info with keyed service keys
 462711095            var parameterInfos = new ConstructorParameterInfo[parameters.Length];
 2019001096            for (int i = 0; i < parameters.Length; i++)
 1097            {
 546791098                var param = parameters[i];
 546791099                var typeName = GetFullyQualifiedNameForType(param.Type);
 546791100                string? serviceKey = null;
 1101
 1102                // Check for [FromKeyedServices("key")] attribute
 1162341103                foreach (var attr in param.GetAttributes())
 1104                {
 34381105                    var attrClass = attr.AttributeClass;
 34381106                    if (attrClass is null)
 1107                        continue;
 1108
 34381109                    var attrFullName = attrClass.ToDisplayString();
 34381110                    if (attrFullName == FromKeyedServicesAttributeName)
 1111                    {
 1112                        // Extract the key from the constructor argument
 01113                        if (attr.ConstructorArguments.Length > 0)
 1114                        {
 01115                            var keyArg = attr.ConstructorArguments[0];
 01116                            if (keyArg.Value is string keyValue)
 1117                            {
 01118                                serviceKey = keyValue;
 1119                            }
 1120                        }
 01121                        break;
 1122                    }
 1123                }
 1124
 546791125                parameterInfos[i] = new ConstructorParameterInfo(typeName, serviceKey);
 1126            }
 462711127            return parameterInfos;
 1128        }
 1129
 95501130        return null;
 1131    }
 1132
 1133    /// <summary>
 1134    /// Gets the service keys from [Keyed] attributes on a type.
 1135    /// </summary>
 1136    /// <param name="typeSymbol">The type symbol to check.</param>
 1137    /// <returns>Array of service keys, or empty array if no [Keyed] attributes found.</returns>
 1138    public static string[] GetKeyedServiceKeys(INamedTypeSymbol typeSymbol)
 1139    {
 2311171140        var keys = new List<string>();
 1141
 13055241142        foreach (var attribute in typeSymbol.GetAttributes())
 1143        {
 4216451144            var attributeClass = attribute.AttributeClass;
 4216451145            if (attributeClass == null)
 1146                continue;
 1147
 4216451148            var name = attributeClass.Name;
 4216451149            var fullName = attributeClass.ToDisplayString();
 1150
 4216451151            if (name == KeyedAttributeName || fullName == KeyedAttributeFullName)
 1152            {
 1153                // Extract the key from the constructor argument
 51154                if (attribute.ConstructorArguments.Length > 0)
 1155                {
 51156                    var keyArg = attribute.ConstructorArguments[0];
 51157                    if (keyArg.Value is string keyValue)
 1158                    {
 51159                        keys.Add(keyValue);
 1160                    }
 1161                }
 1162            }
 1163        }
 1164
 2311171165        return keys.ToArray();
 1166    }
 1167
 1168    // NOTE: TryGetHubRegistrationInfo, TryGetPropertyStringValue, TryGetPropertyTypeValue
 1169    // were moved to NexusLabs.Needlr.SignalR.Generators
 1170
 1171    /// <summary>
 1172    /// Checks if a type has the <c>[DeferToContainer]</c> attribute.
 1173    /// </summary>
 1174    /// <param name="typeSymbol">The type symbol to check.</param>
 1175    /// <returns>True if the type has the DeferToContainer attribute.</returns>
 1176    public static bool HasDeferToContainerAttribute(INamedTypeSymbol typeSymbol)
 1177    {
 23199051178        foreach (var attribute in typeSymbol.GetAttributes())
 1179        {
 7505891180            var attrClass = attribute.AttributeClass;
 7505891181            if (attrClass is null)
 1182                continue;
 1183
 7505891184            var name = attrClass.Name;
 7505891185            var fullName = attrClass.ToDisplayString();
 1186
 7505891187            if (name == DeferToContainerAttributeName || fullName == DeferToContainerAttributeFullName)
 71188                return true;
 1189        }
 1190
 4093601191        return false;
 1192    }
 1193
 1194    /// <summary>
 1195    /// Gets the constructor parameter types declared in the <c>[DeferToContainer]</c> attribute.
 1196    /// </summary>
 1197    /// <param name="typeSymbol">The type symbol to check.</param>
 1198    /// <returns>
 1199    /// A list of fully qualified parameter type names from the attribute,
 1200    /// or null if the attribute is not present.
 1201    /// </returns>
 1202    public static IReadOnlyList<string>? GetDeferToContainerParameterTypes(INamedTypeSymbol typeSymbol)
 1203    {
 13054791204        foreach (var attribute in typeSymbol.GetAttributes())
 1205        {
 4216401206            var attrClass = attribute.AttributeClass;
 4216401207            if (attrClass is null)
 1208                continue;
 1209
 4216401210            var name = attrClass.Name;
 4216401211            var fullName = attrClass.ToDisplayString();
 1212
 4216401213            if (name != DeferToContainerAttributeName && fullName != DeferToContainerAttributeFullName)
 1214                continue;
 1215
 1216            // The attribute has a params Type[] constructor parameter
 1217            // Check constructor arguments
 91218            if (attribute.ConstructorArguments.Length == 0)
 01219                return Array.Empty<string>();
 1220
 91221            var arg = attribute.ConstructorArguments[0];
 1222
 1223            // params array is passed as a single array argument
 91224            if (arg.Kind == TypedConstantKind.Array)
 1225            {
 91226                var types = new List<string>();
 401227                foreach (var element in arg.Values)
 1228                {
 111229                    if (element.Value is INamedTypeSymbol namedType)
 1230                    {
 111231                        types.Add(GetFullyQualifiedName(namedType));
 1232                    }
 1233                }
 91234                return types;
 1235            }
 1236        }
 1237
 2310951238        return null;
 1239    }
 1240
 1241    /// <summary>
 1242    /// Result of decorator discovery.
 1243    /// </summary>
 1244    public readonly struct DecoratorInfo
 1245    {
 1246        public DecoratorInfo(string decoratorTypeName, string serviceTypeName, int order)
 1247        {
 191248            DecoratorTypeName = decoratorTypeName;
 191249            ServiceTypeName = serviceTypeName;
 191250            Order = order;
 191251        }
 1252
 191253        public string DecoratorTypeName { get; }
 191254        public string ServiceTypeName { get; }
 191255        public int Order { get; }
 1256    }
 1257
 1258    /// <summary>
 1259    /// Gets all DecoratorFor&lt;T&gt; attributes applied to a type.
 1260    /// </summary>
 1261    /// <param name="typeSymbol">The type symbol to check.</param>
 1262    /// <returns>A list of decorator info for each DecoratorFor attribute found.</returns>
 1263    public static IReadOnlyList<DecoratorInfo> GetDecoratorForAttributes(INamedTypeSymbol typeSymbol)
 1264    {
 14393811265        var result = new List<DecoratorInfo>();
 1266
 69205201267        foreach (var attribute in typeSymbol.GetAttributes())
 1268        {
 20208791269            var attrClass = attribute.AttributeClass;
 20208791270            if (attrClass is null)
 1271                continue;
 1272
 1273            // Check if this is a generic DecoratorForAttribute<T>
 20208791274            if (!attrClass.IsGenericType)
 1275                continue;
 1276
 331277            var unboundTypeName = attrClass.ConstructedFrom?.ToDisplayString();
 331278            if (unboundTypeName is null || !unboundTypeName.StartsWith(DecoratorForAttributePrefix, StringComparison.Ord
 1279                continue;
 1280
 1281            // Get the service type from the generic type argument
 191282            if (attrClass.TypeArguments.Length != 1)
 1283                continue;
 1284
 191285            var serviceType = attrClass.TypeArguments[0] as INamedTypeSymbol;
 191286            if (serviceType is null)
 1287                continue;
 1288
 191289            var serviceTypeName = GetFullyQualifiedName(serviceType);
 191290            var decoratorTypeName = GetFullyQualifiedName(typeSymbol);
 1291
 1292            // Get the Order property value
 191293            int order = 0;
 571294            foreach (var namedArg in attribute.NamedArguments)
 1295            {
 191296                if (namedArg.Key == "Order" && namedArg.Value.Value is int orderValue)
 1297                {
 191298                    order = orderValue;
 191299                    break;
 1300                }
 1301            }
 1302
 191303            result.Add(new DecoratorInfo(decoratorTypeName, serviceTypeName, order));
 1304        }
 1305
 14393811306        return result;
 1307    }
 1308
 1309    /// <summary>
 1310    /// Checks if a type has any DecoratorFor&lt;T&gt; attributes.
 1311    /// </summary>
 1312    /// <param name="typeSymbol">The type symbol to check.</param>
 1313    /// <returns>True if the type has at least one DecoratorFor attribute.</returns>
 1314    public static bool HasDecoratorForAttribute(INamedTypeSymbol typeSymbol)
 1315    {
 521316        foreach (var attribute in typeSymbol.GetAttributes())
 1317        {
 91318            var attrClass = attribute.AttributeClass;
 91319            if (attrClass is null)
 1320                continue;
 1321
 91322            if (!attrClass.IsGenericType)
 1323                continue;
 1324
 41325            var unboundTypeName = attrClass.ConstructedFrom?.ToDisplayString();
 41326            if (unboundTypeName is not null && unboundTypeName.StartsWith(DecoratorForAttributePrefix, StringComparison.
 21327                return true;
 1328        }
 1329
 161330        return false;
 1331    }
 1332}

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)