< Summary

Information
Class: NexusLabs.Needlr.Generators.ComposedRegistrationDiscoveryHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/ComposedRegistrationDiscoveryHelper.cs
Line coverage
99%
Covered lines: 146
Uncovered lines: 1
Coverable lines: 147
Total lines: 376
Line coverage: 99.3%
Branch coverage
95%
Covered branches: 136
Total branches: 142
Branch coverage: 95.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
get_CompositionType()100%11100%
get_SourceOpenGenericInterface()100%11100%
get_AsServiceType()100%11100%
get_Lifetime()100%11100%
GetComposedMarkers(...)96.42%2828100%
Expand(...)100%1616100%
FindClosedSourceInterfaces(...)100%1414100%
SelectConstructor(...)90%1010100%
BuildResolutionExpression(...)100%11100%
BuildResolutionExpression(...)100%22100%
GetFromKeyedServicesKey(...)90%1010100%
SatisfiesConstraints(...)83.33%6685.71%
SatisfiesConstraint(...)100%3232100%
IsAssignableTo(...)100%88100%
SatisfiesNewConstraint(...)100%1010100%
IsNullableValueType(...)50%22100%
ContainsTypeParameter(...)75%44100%

File(s)

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

#LineLine coverage
 1using System.Collections.Generic;
 2using System.Linq;
 3
 4using Microsoft.CodeAnalysis;
 5
 6using NexusLabs.Needlr.Generators.Models;
 7using NexusLabs.Needlr.Roslyn.Shared;
 8
 9namespace NexusLabs.Needlr.Generators;
 10
 11/// <summary>
 12/// Discovers <c>[RegisterClosedOverImplementationsOf]</c> markers and expands each into concrete closed
 13/// registrations — one per discovered concrete closed implementation of the designated open generic
 14/// interface — by closing the composition type over the same type argument(s) via Roslyn
 15/// <see cref="INamedTypeSymbol.Construct(ITypeSymbol[])"/> and resolving its constructor dependencies.
 16/// </summary>
 17internal static class ComposedRegistrationDiscoveryHelper
 18{
 19    private const string AttributeName = "RegisterClosedOverImplementationsOfAttribute";
 20    private const string AttributeNamespace = "NexusLabs.Needlr.Generators";
 21    private const string FromKeyedServicesAttributeFullName = "Microsoft.Extensions.DependencyInjection.FromKeyedService
 22
 123    private static readonly SymbolDisplayFormat FullyQualified = SymbolDisplayFormat.FullyQualifiedFormat;
 24
 25    /// <summary>
 26    /// A single discovered marker (before assembly/source metadata is attached by the caller).
 27    /// </summary>
 28    public readonly struct ComposedMarkerInfo
 29    {
 30        public ComposedMarkerInfo(
 31            INamedTypeSymbol compositionType,
 32            INamedTypeSymbol sourceOpenGenericInterface,
 33            INamedTypeSymbol? asServiceType,
 34            GeneratorLifetime lifetime)
 35        {
 6836            CompositionType = compositionType;
 6837            SourceOpenGenericInterface = sourceOpenGenericInterface;
 6838            AsServiceType = asServiceType;
 6839            Lifetime = lifetime;
 6840        }
 41
 6842        public INamedTypeSymbol CompositionType { get; }
 6843        public INamedTypeSymbol SourceOpenGenericInterface { get; }
 6844        public INamedTypeSymbol? AsServiceType { get; }
 6845        public GeneratorLifetime Lifetime { get; }
 46    }
 47
 48    /// <summary>
 49    /// Reads all valid <c>[RegisterClosedOverImplementationsOf]</c> attributes from a type.
 50    /// Invalid shapes (non-open-generic source interface, missing facade) are skipped here and surfaced
 51    /// to the user by the companion analyzer.
 52    /// </summary>
 53    public static IReadOnlyList<ComposedMarkerInfo> GetComposedMarkers(INamedTypeSymbol typeSymbol)
 54    {
 158186555        var result = new List<ComposedMarkerInfo>();
 56
 760089057        foreach (var attribute in typeSymbol.GetAttributes())
 58        {
 221858059            var attrClass = attribute.AttributeClass;
 221858060            if (attrClass is null)
 61                continue;
 62
 221858063            if (attrClass.Name != AttributeName)
 64                continue;
 65
 6866            if (attrClass.ContainingNamespace?.ToDisplayString() != AttributeNamespace)
 67                continue;
 68
 6869            if (attribute.ConstructorArguments.Length < 1)
 70                continue;
 71
 6872            if (attribute.ConstructorArguments[0].Value is not INamedTypeSymbol sourceInterface)
 73                continue;
 74
 75            // Only open generic interfaces drive discovery; the analyzer reports other shapes.
 6876            if (!sourceInterface.IsUnboundGenericType || sourceInterface.TypeKind != TypeKind.Interface)
 77                continue;
 78
 6879            INamedTypeSymbol? asServiceType = null;
 6880            var lifetime = GeneratorLifetime.Singleton;
 81
 27682            foreach (var namedArg in attribute.NamedArguments)
 83            {
 7084                if (namedArg.Key == "As" && namedArg.Value.Value is INamedTypeSymbol asSymbol)
 85                {
 6886                    asServiceType = asSymbol;
 87                }
 288                else if (namedArg.Key == "Lifetime" && namedArg.Value.Value is int lifetimeValue)
 89                {
 290                    lifetime = (GeneratorLifetime)lifetimeValue;
 91                }
 92            }
 93
 6894            result.Add(new ComposedMarkerInfo(typeSymbol, sourceInterface, asServiceType, lifetime));
 95        }
 96
 158186597        return result;
 98    }
 99
 100    /// <summary>
 101    /// Expands each marker into closed registrations using the discovered candidate implementation types,
 102    /// appending resolvable registrations to <paramref name="registrations"/> and constraint violations
 103    /// to <paramref name="violations"/>.
 104    /// </summary>
 105    public static void Expand(
 106        IReadOnlyList<DiscoveredComposedMarker> markers,
 107        IReadOnlyList<INamedTypeSymbol> candidateTypes,
 108        List<DiscoveredComposedRegistration> registrations,
 109        List<ComposedConstraintViolation> violations)
 110    {
 270111        foreach (var marker in markers)
 112        {
 113            // The facade is required; absence is reported by the analyzer, skip emission here.
 68114            if (marker.AsServiceType is null)
 115                continue;
 116
 68117            var facadeTypeName = marker.AsServiceType.ToDisplayString(FullyQualified);
 118
 119            // Distinct closed implementations of the source interface, ordered for deterministic output.
 68120            var closedInterfaces = FindClosedSourceInterfaces(marker.SourceOpenGenericInterface, candidateTypes)
 38121                .OrderBy(i => i.ToDisplayString(FullyQualified), System.StringComparer.Ordinal)
 68122                .ToList();
 123
 312124            foreach (var closedInterface in closedInterfaces)
 125            {
 88126                var typeArguments = closedInterface.TypeArguments;
 127
 128                // Only fully closed implementations (concrete type arguments) participate.
 183129                if (typeArguments.Any(t => t.TypeKind == TypeKind.TypeParameter))
 130                    continue;
 131
 132                // Arity between the source interface and the composition must align to close the composition.
 88133                if (typeArguments.Length != marker.CompositionType.TypeParameters.Length)
 134                    continue;
 135
 88136                if (!SatisfiesConstraints(marker.CompositionType, typeArguments))
 137                {
 34138                    violations.Add(new ComposedConstraintViolation(
 34139                        marker.CompositionType.ToDisplayString(FullyQualified),
 36140                        string.Join(", ", typeArguments.Select(t => t.ToDisplayString(FullyQualified))),
 34141                        marker.SourceOpenGenericInterface.ToDisplayString(FullyQualified),
 34142                        marker.SourceFilePath));
 34143                    continue;
 144                }
 145
 54146                var closedComposition = marker.CompositionType.Construct(typeArguments.ToArray());
 147
 148                // A composition type using [GenerateConstructor]/field-triggered generation has
 149                // its effective constructor derived from the shared field model instead of
 150                // Roslyn's InstanceConstructors, which would otherwise only see the implicit
 151                // parameterless constructor visible before the sibling GeneratedConstructorGenerator
 152                // pass emits the real one within this compilation.
 54153                if (GeneratedConstructorEligibility.IsEligibleForGeneratedConstructor(closedComposition))
 154                {
 3155                    var generatedFields = GeneratedConstructorEligibility.GetEligibleConstructorFields(closedComposition
 3156                    var generatedArguments = generatedFields
 4157                        .Select(f => BuildResolutionExpression(f.Type, serviceKey: null))
 3158                        .ToList();
 159
 3160                    registrations.Add(new DiscoveredComposedRegistration(
 3161                        facadeTypeName,
 3162                        closedComposition.ToDisplayString(FullyQualified),
 3163                        generatedArguments,
 3164                        marker.Lifetime,
 3165                        marker.AssemblyName,
 3166                        marker.SourceFilePath));
 3167                    continue;
 168                }
 169
 51170                var constructor = SelectConstructor(closedComposition);
 51171                if (constructor is null)
 172                    continue;
 173
 51174                var arguments = constructor.Parameters
 51175                    .Select(BuildResolutionExpression)
 51176                    .ToList();
 177
 51178                registrations.Add(new DiscoveredComposedRegistration(
 51179                    facadeTypeName,
 51180                    closedComposition.ToDisplayString(FullyQualified),
 51181                    arguments,
 51182                    marker.Lifetime,
 51183                    marker.AssemblyName,
 51184                    marker.SourceFilePath));
 185            }
 186        }
 67187    }
 188
 189    private static List<INamedTypeSymbol> FindClosedSourceInterfaces(
 190        INamedTypeSymbol sourceOpenGenericInterface,
 191        IReadOnlyList<INamedTypeSymbol> candidateTypes)
 192    {
 68193        var sourceDefinition = sourceOpenGenericInterface.OriginalDefinition;
 68194        var result = new List<INamedTypeSymbol>();
 68195        var seen = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
 196
 450197        foreach (var candidate in candidateTypes)
 198        {
 157199            if (candidate.IsAbstract || candidate.TypeKind != TypeKind.Class)
 200                continue;
 201
 518202            foreach (var iface in candidate.AllInterfaces)
 203            {
 102204                if (!iface.IsGenericType)
 205                    continue;
 206
 94207                if (!SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, sourceDefinition))
 208                    continue;
 209
 88210                if (seen.Add(iface))
 88211                    result.Add(iface);
 212            }
 213        }
 214
 68215        return result;
 216    }
 217
 218    // A composition type is now handled above, before this method is reached, when it is
 219    // eligible for generated-constructor generation (see
 220    // GeneratedConstructorEligibility.IsEligibleForGeneratedConstructor in Expand). This
 221    // method remains the resolution path for a composition type with a hand-written
 222    // constructor.
 223    private static IMethodSymbol? SelectConstructor(INamedTypeSymbol closedComposition)
 224    {
 51225        IMethodSymbol? best = null;
 226
 204227        foreach (var ctor in closedComposition.InstanceConstructors)
 228        {
 51229            if (ctor.IsStatic)
 230                continue;
 231
 51232            if (ctor.DeclaredAccessibility != Accessibility.Public)
 233                continue;
 234
 51235            if (best is null || ctor.Parameters.Length > best.Parameters.Length)
 51236                best = ctor;
 237        }
 238
 51239        return best;
 240    }
 241
 242    private static string BuildResolutionExpression(IParameterSymbol parameter)
 243    {
 57244        return BuildResolutionExpression(parameter.Type, GetFromKeyedServicesKey(parameter));
 245    }
 246
 247    /// <summary>
 248    /// Builds a <c>sp.GetRequiredService&lt;T&gt;()</c> (or keyed-service) resolution
 249    /// expression for a type, independent of whether it came from a hand-written
 250    /// constructor's <see cref="IParameterSymbol"/> or a generated constructor's
 251    /// <see cref="IFieldSymbol"/>.
 252    /// </summary>
 253    private static string BuildResolutionExpression(ITypeSymbol type, string? serviceKey)
 254    {
 61255        var typeName = type.ToDisplayString(FullyQualified);
 256
 61257        return serviceKey is null
 61258            ? $"sp.GetRequiredService<{typeName}>()"
 61259            : $"sp.GetRequiredKeyedService<{typeName}>(\"{GeneratorHelpers.EscapeStringLiteral(serviceKey)}\")";
 260    }
 261
 262    private static string? GetFromKeyedServicesKey(IParameterSymbol parameter)
 263    {
 116264        foreach (var attr in parameter.GetAttributes())
 265        {
 2266            if (attr.AttributeClass?.ToDisplayString() != FromKeyedServicesAttributeFullName)
 267                continue;
 268
 2269            if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string keyValue)
 2270                return keyValue;
 271        }
 272
 55273        return null;
 274    }
 275
 276    private static bool SatisfiesConstraints(
 277        INamedTypeSymbol composition,
 278        System.Collections.Immutable.ImmutableArray<ITypeSymbol> typeArguments)
 279    {
 88280        var typeParameters = composition.TypeParameters;
 88281        if (typeParameters.Length != typeArguments.Length)
 0282            return false;
 283
 298284        for (var i = 0; i < typeParameters.Length; i++)
 285        {
 95286            if (!SatisfiesConstraint(typeParameters[i], typeArguments[i]))
 34287                return false;
 288        }
 289
 54290        return true;
 291    }
 292
 293    private static bool SatisfiesConstraint(
 294        ITypeParameterSymbol typeParameter,
 295        ITypeSymbol typeArgument)
 296    {
 95297        if (typeParameter.HasReferenceTypeConstraint && !typeArgument.IsReferenceType)
 6298            return false;
 299
 89300        if (typeParameter.HasValueTypeConstraint &&
 89301            (!typeArgument.IsValueType || IsNullableValueType(typeArgument)))
 6302            return false;
 303
 83304        if (typeParameter.HasNotNullConstraint && IsNullableValueType(typeArgument))
 4305            return false;
 306
 79307        if (typeParameter.HasUnmanagedTypeConstraint && !typeArgument.IsUnmanagedType)
 2308            return false;
 309
 77310        if (typeParameter.HasConstructorConstraint && !SatisfiesNewConstraint(typeArgument))
 10311            return false;
 312
 176313        foreach (var constraintType in typeParameter.ConstraintTypes)
 314        {
 315            // Only non-generic constraint types are validated here: exact-match assignability is
 316            // variance-immune and reliable for them. Generic constraint types — whether self-referential
 317            // (where T : IComparable<T>), variant (where T : IProducer<Animal>), or invariant — are
 318            // deferred to the C# compiler so a variance/substitution subtlety can never skip a valid
 319            // registration. A bare type-parameter constraint (where T : U) is likewise deferred.
 24320            if (constraintType is INamedTypeSymbol { IsGenericType: true } || ContainsTypeParameter(constraintType))
 321                continue;
 322
 16323            if (!IsAssignableTo(typeArgument, constraintType))
 6324                return false;
 325        }
 326
 61327        return true;
 328    }
 329
 330    private static bool IsAssignableTo(ITypeSymbol type, ITypeSymbol target)
 331    {
 16332        if (SymbolEqualityComparer.Default.Equals(type, target))
 2333            return true;
 334
 14335        if (target.TypeKind == TypeKind.Interface)
 16336            return type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, target));
 337
 12338        for (var baseType = type.BaseType; baseType is not null; baseType = baseType.BaseType)
 339        {
 4340            if (SymbolEqualityComparer.Default.Equals(baseType, target))
 2341                return true;
 342        }
 343
 2344        return false;
 345    }
 346
 347    private static bool SatisfiesNewConstraint(ITypeSymbol typeArgument)
 348    {
 18349        if (typeArgument.IsValueType)
 2350            return true;
 351
 16352        if (typeArgument is not INamedTypeSymbol named)
 2353            return false;
 354
 14355        if (named.IsAbstract)
 2356            return false;
 357
 12358        return named.InstanceConstructors.Any(c =>
 24359            !c.IsStatic &&
 24360            c.Parameters.Length == 0 &&
 24361            c.DeclaredAccessibility == Accessibility.Public);
 362    }
 363
 364    private static bool IsNullableValueType(ITypeSymbol typeArgument) =>
 20365        typeArgument is INamedTypeSymbol named &&
 20366        named.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
 367
 368    private static bool ContainsTypeParameter(ITypeSymbol type)
 369    {
 18370        if (type.TypeKind == TypeKind.TypeParameter)
 2371            return true;
 372
 16373        return type is INamedTypeSymbol named &&
 16374            named.TypeArguments.Any(ContainsTypeParameter);
 375    }
 376}

Methods/Properties

.cctor()
.ctor(Microsoft.CodeAnalysis.INamedTypeSymbol,Microsoft.CodeAnalysis.INamedTypeSymbol,Microsoft.CodeAnalysis.INamedTypeSymbol,NexusLabs.Needlr.Generators.GeneratorLifetime)
get_CompositionType()
get_SourceOpenGenericInterface()
get_AsServiceType()
get_Lifetime()
GetComposedMarkers(Microsoft.CodeAnalysis.INamedTypeSymbol)
Expand(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.Models.DiscoveredComposedMarker>,System.Collections.Generic.IReadOnlyList`1<Microsoft.CodeAnalysis.INamedTypeSymbol>,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.DiscoveredComposedRegistration>,System.Collections.Generic.List`1<NexusLabs.Needlr.Generators.Models.ComposedConstraintViolation>)
FindClosedSourceInterfaces(Microsoft.CodeAnalysis.INamedTypeSymbol,System.Collections.Generic.IReadOnlyList`1<Microsoft.CodeAnalysis.INamedTypeSymbol>)
SelectConstructor(Microsoft.CodeAnalysis.INamedTypeSymbol)
BuildResolutionExpression(Microsoft.CodeAnalysis.IParameterSymbol)
BuildResolutionExpression(Microsoft.CodeAnalysis.ITypeSymbol,System.String)
GetFromKeyedServicesKey(Microsoft.CodeAnalysis.IParameterSymbol)
SatisfiesConstraints(Microsoft.CodeAnalysis.INamedTypeSymbol,System.Collections.Immutable.ImmutableArray`1<Microsoft.CodeAnalysis.ITypeSymbol>)
SatisfiesConstraint(Microsoft.CodeAnalysis.ITypeParameterSymbol,Microsoft.CodeAnalysis.ITypeSymbol)
IsAssignableTo(Microsoft.CodeAnalysis.ITypeSymbol,Microsoft.CodeAnalysis.ITypeSymbol)
SatisfiesNewConstraint(Microsoft.CodeAnalysis.ITypeSymbol)
IsNullableValueType(Microsoft.CodeAnalysis.ITypeSymbol)
ContainsTypeParameter(Microsoft.CodeAnalysis.ITypeSymbol)