< Summary

Information
Class: NexusLabs.Needlr.Generators.ConstructorGenerationDiscoveryHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /_work/nick-alienware-needlr-1-1784953859-3e8997/needlr/needlr/src/NexusLabs.Needlr.Generators/ConstructorGenerationDiscoveryHelper.cs
Line coverage
93%
Covered lines: 100
Uncovered lines: 7
Coverable lines: 107
Total lines: 299
Line coverage: 93.4%
Branch coverage
88%
Covered branches: 81
Total branches: 92
Branch coverage: 88%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
TryGetEffectiveConstructorParameters(...)100%88100%
TryGetModel(...)85%404095.55%
IsCandidateClassDeclaration(...)100%22100%
TryCreateCanonicalModel(...)75%4483.33%
HasFieldMember(...)100%44100%
IsCanonicalFieldBearingDeclaration(...)92.85%141492.3%
ComparePosition(...)100%22100%
GetGenerateConstructorAttributeMode(...)90%101090.9%
GetParameterName(...)75%9875%

File(s)

/_work/nick-alienware-needlr-1-1784953859-3e8997/needlr/needlr/src/NexusLabs.Needlr.Generators/ConstructorGenerationDiscoveryHelper.cs

#LineLine coverage
 1using System.Collections.Generic;
 2using System.Linq;
 3
 4using Microsoft.CodeAnalysis;
 5using Microsoft.CodeAnalysis.CSharp;
 6using Microsoft.CodeAnalysis.CSharp.Syntax;
 7
 8using NexusLabs.Needlr.Generators.Models;
 9using NexusLabs.Needlr.Roslyn.Shared;
 10
 11namespace NexusLabs.Needlr.Generators;
 12
 13/// <summary>
 14/// Discovers whether a type is eligible for generated-constructor generation
 15/// (<c>[GenerateConstructor]</c> or a positive field-level constructor guard trigger)
 16/// and builds the shared <see cref="GeneratedConstructorModel"/> that both source
 17/// emission and Needlr's type-registry constructor discovery consume.
 18/// </summary>
 19/// <remarks>
 20/// Symbol-level eligibility facts (attribute identity, eligible field set, positive
 21/// field-guard trigger, overall eligibility) are delegated to
 22/// <see cref="GeneratedConstructorEligibility"/> so that this generator, the SignalR
 23/// generator, and Needlr's analyzers agree on the same rules. This class owns only the
 24/// main-generator-specific emission model: guard resolution, parameter-name
 25/// normalization, and the <see cref="GeneratedConstructorModel"/> shape.
 26/// </remarks>
 27internal static class ConstructorGenerationDiscoveryHelper
 28{
 29    private const string GenerateConstructorAttributeName = "GenerateConstructorAttribute";
 30
 131    internal static readonly SymbolDisplayFormat NullableAwareFormat = SymbolDisplayFormat.FullyQualifiedFormat.WithMisc
 132        SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions | SymbolDisplayMiscellaneousOptions.IncludeNullabl
 33
 34    /// <summary>
 35    /// Builds the effective constructor-parameter list for a type that is eligible
 36    /// for generated-constructor generation, for use by Needlr's type-registry
 37    /// discovery. Returns <see langword="null"/> when the type is not eligible, in
 38    /// which case callers should fall back to symbol-based constructor discovery
 39    /// (which is correct both for types with a hand-written constructor and for
 40    /// referenced-assembly types whose generated constructor is already compiled).
 41    /// </summary>
 42    internal static IReadOnlyList<TypeDiscoveryHelper.ConstructorParameterInfo>? TryGetEffectiveConstructorParameters(IN
 43    {
 67751844        var model = TryGetModel(typeSymbol);
 67751845        if (model is null)
 67750046            return null;
 47
 48        // Only usable for Needlr's automatic DI discovery when every parameter is a
 49        // container-resolvable service type -- the same rule applied to hand-written
 50        // constructors via TypeDiscoveryHelper.IsInjectableParameterType. A
 51        // field-derived constructor with a string/value-type parameter (e.g. a
 52        // field-triggered guard on a plain string) is still a perfectly valid
 53        // generated constructor; it just isn't eligible for automatic registration
 54        // without a factory or explicit registration.
 8355        foreach (var field in GeneratedConstructorEligibility.GetEligibleConstructorFields(typeSymbol))
 56        {
 2557            if (!TypeDiscoveryHelper.IsInjectableParameterType(field.Type))
 358                return null;
 59        }
 60
 1561        var fields = model.Value.Fields;
 1562        var result = new TypeDiscoveryHelper.ConstructorParameterInfo[fields.Length];
 6863        for (var i = 0; i < fields.Length; i++)
 64        {
 1965            result[i] = new TypeDiscoveryHelper.ConstructorParameterInfo(
 1966                fields[i].ParameterTypeName,
 1967                serviceKey: null,
 1968                parameterName: fields[i].ParameterName);
 69        }
 70
 1571        return result;
 372    }
 73
 74    /// <summary>
 75    /// Builds the full generated-constructor model for a type, or <see langword="null"/>
 76    /// when the type is not eligible for constructor generation.
 77    /// </summary>
 78    internal static GeneratedConstructorModel? TryGetModel(INamedTypeSymbol typeSymbol)
 79    {
 210200880        if (typeSymbol.TypeKind != TypeKind.Class || typeSymbol.IsRecord)
 328781            return null;
 82
 83        // Nested types are unsupported: a nested type's enclosing instance (when the
 84        // outer type is non-static) is not something a generated constructor knows how
 85        // to obtain, so this feature is restricted to top-level types.
 209872186        if (typeSymbol.ContainingType != null)
 187            return null;
 88
 89        // Base types requiring constructor arguments are unsupported. A base type
 90        // with an accessible parameterless constructor (including the common case of no
 91        // explicit base constructors at all, e.g. BackgroundService) is fully supported
 92        // since the generated constructor relies on the implicit `: base()` call.
 209872093        if (typeSymbol.BaseType != null && typeSymbol.BaseType.SpecialType != SpecialType.System_Object &&
 209872094            !GeneratedConstructorEligibility.HasAccessibleParameterlessConstructor(typeSymbol.BaseType))
 95        {
 12968496            return null;
 97        }
 98
 196903699        var candidates = new List<(IFieldSymbol Field, ConstructorGuardModel[] Guards)>();
 3938712100        foreach (var field in GeneratedConstructorEligibility.GetEligibleConstructorFields(typeSymbol))
 101        {
 320102            candidates.Add((field, ConstructorGuardDiscoveryHelper.GetExplicitGuards(field)));
 103        }
 104
 1969036105        var hasPositiveFieldTrigger = GeneratedConstructorEligibility.HasPositiveFieldGuardTrigger(typeSymbol);
 1969036106        var classNullGuardMode = GetGenerateConstructorAttributeMode(typeSymbol);
 107
 1969036108        if (classNullGuardMode is null && !hasPositiveFieldTrigger)
 1968921109            return null;
 110
 115111        if (!GeneratedConstructorEligibility.IsDeclaredPartial(typeSymbol))
 1112            return null;
 113
 114114        if (GeneratedConstructorEligibility.HasExplicitInstanceConstructor(typeSymbol))
 1115            return null;
 116
 113117        if (candidates.Count == 0)
 0118            return null;
 119
 113120        var parameterNames = new HashSet<string>(System.StringComparer.Ordinal);
 113121        var fields = new EligibleConstructorField[candidates.Count];
 518122        for (var i = 0; i < candidates.Count; i++)
 123        {
 146124            var (field, guards) = candidates[i];
 146125            var parameterName = GetParameterName(field.Name);
 126
 127            // Parameter-name collisions after normalization are unsupported; skip
 128            // generation entirely rather than emit a broken constructor.
 146129            if (!parameterNames.Add(parameterName))
 0130                return null;
 131
 146132            var parameterTypeName = field.Type.ToDisplayString(NullableAwareFormat);
 146133            var isNonNullableReferenceType = field.Type.IsReferenceType && field.Type.NullableAnnotation == NullableAnno
 134
 146135            fields[i] = new EligibleConstructorField(field.Name, parameterName, parameterTypeName, isNonNullableReferenc
 136        }
 137
 113138        var typeParameterList = typeSymbol.TypeParameters.Length == 0
 113139            ? string.Empty
 119140            : "<" + string.Join(", ", typeSymbol.TypeParameters.Select(tp => tp.Name)) + ">";
 141
 113142        var containingNamespace = typeSymbol.ContainingNamespace is { IsGlobalNamespace: false }
 113143            ? typeSymbol.ContainingNamespace.ToDisplayString()
 113144            : string.Empty;
 145
 226146        var sourceFilePath = typeSymbol.Locations.FirstOrDefault(l => l.IsInSource)?.SourceTree?.FilePath;
 147
 113148        return new GeneratedConstructorModel(
 113149            containingNamespace,
 113150            typeSymbol.Name,
 113151            typeParameterList,
 113152            typeSymbol.TypeParameters.Length,
 113153            classNullGuardMode ?? GeneratedConstructorNullGuardMode.None,
 113154            fields,
 113155            sourceFilePath);
 156    }
 157
 158    /// <summary>
 159    /// The syntax-level predicate for the generator's per-type incremental pipeline:
 160    /// a cheap, semantic-model-free check for a top-level class declaration with at
 161    /// least one field member. Any class without a field can never be eligible (see
 162    /// <see cref="TryGetModel"/>), so this filters the overwhelming majority of
 163    /// classes in a typical compilation before a semantic model is ever touched.
 164    /// </summary>
 165    internal static bool IsCandidateClassDeclaration(SyntaxNode node)
 166    {
 2858167        return node is ClassDeclarationSyntax classDeclaration && HasFieldMember(classDeclaration);
 168    }
 169
 170    /// <summary>
 171    /// The transform for the generator's per-type incremental pipeline. Builds the
 172    /// full <see cref="GeneratedConstructorModel"/> for the syntax node's type,
 173    /// immediately converting the resolved <see cref="INamedTypeSymbol"/> into an
 174    /// equatable value model rather than passing the symbol itself further down the
 175    /// pipeline (symbols compare by reference across compilations and would defeat
 176    /// incremental caching).
 177    /// </summary>
 178    /// <remarks>
 179    /// For a partial type declared across multiple field-bearing declarations, this
 180    /// produces a model for exactly one of them -- the declaration earliest in
 181    /// deterministic (file path, then position) order -- and <see langword="null"/> for
 182    /// every other declaration of the same type, so the generator emits exactly one
 183    /// source file per type regardless of how many partial declarations carry fields.
 184    /// </remarks>
 185    internal static GeneratedConstructorModel? TryCreateCanonicalModel(GeneratorSyntaxContext context)
 186    {
 78187        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 188
 78189        if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol typeSymbol)
 0190            return null;
 191
 78192        if (!IsCanonicalFieldBearingDeclaration(typeSymbol, classDeclaration))
 2193            return null;
 194
 76195        return TryGetModel(typeSymbol);
 196    }
 197
 198    private static bool HasFieldMember(ClassDeclarationSyntax classDeclaration)
 199    {
 731200        foreach (var member in classDeclaration.Members)
 201        {
 217202            if (member is FieldDeclarationSyntax)
 159203                return true;
 204        }
 205
 69206        return false;
 207    }
 208
 209    /// <summary>
 210    /// True when <paramref name="classDeclaration"/> is, among every field-bearing
 211    /// partial declaration of <paramref name="typeSymbol"/>, the one earliest in
 212    /// deterministic (file path, then span start) order. Comparing file path and span
 213    /// rather than syntax-node identity keeps this correct even when a declaration is
 214    /// re-parsed into a new (but structurally equivalent) node instance.
 215    /// </summary>
 216    private static bool IsCanonicalFieldBearingDeclaration(INamedTypeSymbol typeSymbol, ClassDeclarationSyntax classDecl
 217    {
 78218        string? canonicalFilePath = null;
 78219        var canonicalStart = 0;
 220
 320221        foreach (var syntaxRef in typeSymbol.DeclaringSyntaxReferences)
 222        {
 82223            if (syntaxRef.GetSyntax() is not ClassDeclarationSyntax candidate || !HasFieldMember(candidate))
 224                continue;
 225
 82226            var filePath = candidate.SyntaxTree.FilePath;
 82227            var start = candidate.SpanStart;
 228
 82229            if (canonicalFilePath is null ||
 82230                ComparePosition(filePath, start, canonicalFilePath, canonicalStart) < 0)
 231            {
 80232                canonicalFilePath = filePath;
 80233                canonicalStart = start;
 234            }
 235        }
 236
 78237        if (canonicalFilePath is null)
 0238            return false;
 239
 78240        return canonicalFilePath == classDeclaration.SyntaxTree.FilePath && canonicalStart == classDeclaration.SpanStart
 241    }
 242
 243    private static int ComparePosition(string filePathA, int startA, string filePathB, int startB)
 244    {
 4245        var pathCompare = string.CompareOrdinal(filePathA, filePathB);
 4246        return pathCompare != 0 ? pathCompare : startA.CompareTo(startB);
 247    }
 248
 249    private static GeneratedConstructorNullGuardMode? GetGenerateConstructorAttributeMode(INamedTypeSymbol typeSymbol)
 250    {
 11144878251        foreach (var attribute in typeSymbol.GetAttributes())
 252        {
 3603442253            var attrClass = attribute.AttributeClass;
 3603442254            if (attrClass is null)
 255                continue;
 256
 3603442257            if (!GeneratedConstructorEligibility.IsNeedlrGeneratorsAttribute(attrClass, GenerateConstructorAttributeName
 258                continue;
 259
 78260            if (attribute.ConstructorArguments.Length == 0)
 68261                return GeneratedConstructorNullGuardMode.None;
 262
 10263            var arg = attribute.ConstructorArguments[0];
 10264            if (arg.Value is int modeValue)
 10265                return (GeneratedConstructorNullGuardMode)modeValue;
 266
 0267            return GeneratedConstructorNullGuardMode.None;
 268        }
 269
 1968958270        return null;
 271    }
 272
 273    /// <summary>
 274    /// Normalizes a field name into a constructor parameter name (e.g. <c>_repository</c>
 275    /// -> <c>repository</c>), escaping C# keywords with <c>@</c>. Internal so
 276    /// <see cref="FactoryDiscoveryHelper"/> can derive the exact same parameter names
 277    /// for a generated constructor's fields when building factory call sites.
 278    /// </summary>
 279    internal static string GetParameterName(string fieldName)
 280    {
 238281        var name = fieldName;
 282
 238283        if (name.Length > 1 && name[0] == '_')
 284        {
 236285            name = name.Substring(1);
 286        }
 287
 238288        if (name.Length == 0)
 289        {
 0290            name = "value";
 291        }
 238292        else if (char.IsUpper(name[0]))
 293        {
 0294            name = char.ToLowerInvariant(name[0]) + name.Substring(1);
 295        }
 296
 238297        return GeneratorHelpers.EscapeIdentifier(name);
 298    }
 299}