< Summary

Information
Class: NexusLabs.Needlr.Roslyn.Shared.GeneratedConstructorEligibility
Assembly: NexusLabs.Needlr.Generators
File(s): /_work/nick-alienware-needlr-1-1784953859-3e8997/needlr/needlr/src/NexusLabs.Needlr.Roslyn.Shared/GeneratedConstructorEligibility.cs
Line coverage
92%
Covered lines: 99
Uncovered lines: 8
Coverable lines: 107
Total lines: 395
Line coverage: 92.5%
Branch coverage
88%
Covered branches: 119
Total branches: 134
Branch coverage: 88.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/_work/nick-alienware-needlr-1-1784953859-3e8997/needlr/needlr/src/NexusLabs.Needlr.Roslyn.Shared/GeneratedConstructorEligibility.cs

#LineLine coverage
 1using System.Collections.Generic;
 2using System.Linq;
 3
 4using Microsoft.CodeAnalysis;
 5using Microsoft.CodeAnalysis.CSharp;
 6using Microsoft.CodeAnalysis.CSharp.Syntax;
 7
 8namespace NexusLabs.Needlr.Roslyn.Shared;
 9
 10/// <summary>
 11/// Shared, symbol-level facts about Needlr's generated-constructor feature
 12/// (<c>[GenerateConstructor]</c> and field-level constructor guard attributes), used by
 13/// every Roslyn component that must agree on whether a type's effective constructor is
 14/// produced by the <c>GeneratedConstructorGenerator</c> pass rather than by a
 15/// hand-written constructor.
 16/// </summary>
 17/// <remarks>
 18/// This class intentionally exposes only stable symbol-level facts (attribute identity,
 19/// field eligibility, trigger detection, and overall eligibility) and not the main
 20/// generator's emission model -- guard resolution, parameter-name normalization, and
 21/// source text generation remain specific to <c>NexusLabs.Needlr.Generators</c>. A
 22/// consuming component that also needs the fully resolved model (parameter names,
 23/// resolved guard calls) should combine these facts with its own emission logic, the
 24/// way <c>ConstructorGenerationDiscoveryHelper</c> does.
 25/// </remarks>
 26internal static class GeneratedConstructorEligibility
 27{
 28    private const string NeedlrGeneratorsNamespace = "NexusLabs.Needlr.Generators";
 29    private const string GenerateConstructorAttributeName = "GenerateConstructorAttribute";
 30    private const string ConstructorGuardAttributeName = "ConstructorGuardAttribute";
 31    private const string ConstructorIgnoreAttributeName = "ConstructorIgnoreAttribute";
 32    private const string ConstructorGuardDefinitionAttributeName = "ConstructorGuardDefinitionAttribute";
 33
 34    /// <summary>
 35    /// The exact suffix <c>GeneratedConstructorGenerator</c> uses for every hint name it
 36    /// passes to <c>AddSource</c> (see <c>GeneratedConstructorGenerator.BuildHintName</c>).
 37    /// Matching this precise suffix -- rather than the repository-wide <c>.g.cs</c>
 38    /// generated-file convention shared by every other source generator -- is required so
 39    /// a hand-authored constructor in some unrelated generated-looking file (e.g. a
 40    /// checked-in <c>Service.g.cs</c>) is never mistaken for this feature's own
 41    /// generated constructor.
 42    /// </summary>
 43    private const string GeneratedConstructorFileSuffix = ".GeneratedConstructor.g.cs";
 44
 45    /// <summary>
 46    /// Matches a Needlr attribute class by BOTH simple name and containing namespace, per
 47    /// the discovery-helper convention. This deliberately avoids comparing a full
 48    /// display-string name, which would incorrectly match an unrelated third-party or
 49    /// application-defined attribute that merely shares the same simple name.
 50    /// </summary>
 51    internal static bool IsNeedlrGeneratorsAttribute(INamedTypeSymbol attributeClass, string simpleName)
 52    {
 360803853        return attributeClass.Name == simpleName &&
 360803854            attributeClass.ContainingNamespace?.ToDisplayString() == NeedlrGeneratorsNamespace;
 55    }
 56
 57    /// <summary>
 58    /// True when the type carries a <c>[GenerateConstructor]</c> class-level attribute,
 59    /// regardless of its configured guard mode.
 60    /// </summary>
 61    internal static bool HasGenerateConstructorAttribute(INamedTypeSymbol typeSymbol)
 62    {
 771363        foreach (var attribute in typeSymbol.GetAttributes())
 64        {
 176365            if (attribute.AttributeClass is { } attrClass && IsNeedlrGeneratorsAttribute(attrClass, GenerateConstructorA
 3366                return true;
 67        }
 68
 207769        return false;
 70    }
 71
 72    /// <summary>
 73    /// True when the field carries <c>[ConstructorIgnore]</c>, excluding it from
 74    /// generated-constructor parameters even though it would otherwise be eligible.
 75    /// </summary>
 76    internal static bool HasConstructorIgnoreAttribute(IFieldSymbol field)
 77    {
 253978        foreach (var attribute in field.GetAttributes())
 79        {
 28280            if (attribute.AttributeClass is { } attrClass && IsNeedlrGeneratorsAttribute(attrClass, ConstructorIgnoreAtt
 781                return true;
 82        }
 83
 98484        return false;
 85    }
 86
 87    /// <summary>
 88    /// True when the field carries a guard attribute that positively triggers
 89    /// generated-constructor generation on its own (i.e. without a class-level
 90    /// <c>[GenerateConstructor]</c> attribute): a <c>[ConstructorGuard]</c> with a
 91    /// built-in kind other than <c>None</c>, a <c>[ConstructorGuard]</c> selecting a
 92    /// custom guard type, or any alias attribute meta-annotated with
 93    /// <c>[ConstructorGuardDefinition]</c>. Exclusion-only shapes (a bare
 94    /// <c>[ConstructorGuard(ConstructorGuardKind.None)]</c>, or no guard attribute at
 95    /// all) are not positive triggers.
 96    /// </summary>
 97    internal static bool HasPositiveConstructorGuardTrigger(IFieldSymbol field)
 98    {
 114899        foreach (var attribute in field.GetAttributes())
 100        {
 127101            var attrClass = attribute.AttributeClass;
 127102            if (attrClass is null)
 103                continue;
 104
 127105            if (IsNeedlrGeneratorsAttribute(attrClass, ConstructorGuardAttributeName))
 106            {
 69107                if (IsPositiveConstructorGuardAttribute(attribute))
 65108                    return true;
 109
 110                continue;
 111            }
 112
 58113            if (HasConstructorGuardDefinitionMetaAttribute(attrClass))
 55114                return true;
 115        }
 116
 387117        return false;
 118    }
 119
 120    /// <summary>
 121    /// True when any eligible constructor field on the type has a positive guard
 122    /// trigger. See <see cref="HasPositiveConstructorGuardTrigger(IFieldSymbol)"/>.
 123    /// </summary>
 124    internal static bool HasPositiveFieldGuardTrigger(INamedTypeSymbol typeSymbol)
 125    {
 3943234126        foreach (var field in GetEligibleConstructorFields(typeSymbol))
 127        {
 507128            if (HasPositiveConstructorGuardTrigger(field))
 120129                return true;
 130        }
 131
 1971050132        return false;
 120133    }
 134
 135    /// <summary>
 136    /// Returns the private, readonly, non-initialized instance fields of
 137    /// <paramref name="typeSymbol"/> that are eligible to become generated-constructor
 138    /// parameters (excluding any field carrying <c>[ConstructorIgnore]</c>), in
 139    /// deterministic declaration order. This is the exact field set and order that
 140    /// <c>GeneratedConstructorGenerator</c> emits as the constructor's parameter list,
 141    /// so callers that need to match the generated constructor's actual signature
 142    /// (parameter order, parameter types) must use this method rather than re-deriving
 143    /// their own field filter.
 144    /// </summary>
 145    internal static IReadOnlyList<IFieldSymbol> GetEligibleConstructorFields(INamedTypeSymbol typeSymbol)
 146    {
 3940367147        var result = new List<IFieldSymbol>();
 148
 8129954149        foreach (var field in GetOrderedInstanceFields(typeSymbol))
 150        {
 124610151            if (!IsEligibleField(field) || HasConstructorIgnoreAttribute(field))
 152                continue;
 153
 984154            result.Add(field);
 155        }
 156
 3940367157        return result;
 158    }
 159
 160    /// <summary>
 161    /// True when <paramref name="typeSymbol"/> is eligible for generated-constructor
 162    /// generation, i.e. <c>GeneratedConstructorGenerator</c> will emit a public
 163    /// constructor for it (or would, once the type also satisfies parameter-name
 164    /// uniqueness after normalization -- a rare edge case this method does not evaluate,
 165    /// since it requires the main generator's parameter-name normalization). Callers
 166    /// that only need to know whether a type's <em>effective</em> constructor requires
 167    /// arguments (as opposed to the implicit parameterless constructor still visible to
 168    /// a sibling generator/analyzer pass within the same compilation) can rely on this
 169    /// method instead of duplicating the same shape checks.
 170    /// </summary>
 171    internal static bool IsEligibleForGeneratedConstructor(INamedTypeSymbol typeSymbol)
 172    {
 39173        if (typeSymbol.TypeKind != TypeKind.Class || typeSymbol.IsRecord)
 0174            return false;
 175
 176        // Nested types are unsupported: a nested type's enclosing instance (when the
 177        // outer type is non-static) is not something a generated constructor knows how
 178        // to obtain, so this feature is restricted to top-level types.
 39179        if (typeSymbol.ContainingType != null)
 0180            return false;
 181
 182        // Base types requiring constructor arguments are unsupported. A base type
 183        // with an accessible parameterless constructor (including the common case of no
 184        // explicit base constructors at all, e.g. BackgroundService) is fully supported
 185        // since the generated constructor relies on the implicit `: base()` call.
 39186        if (typeSymbol.BaseType != null && typeSymbol.BaseType.SpecialType != SpecialType.System_Object &&
 39187            !HasAccessibleParameterlessConstructor(typeSymbol.BaseType))
 188        {
 0189            return false;
 190        }
 191
 39192        if (GetEligibleConstructorFields(typeSymbol).Count == 0)
 31193            return false;
 194
 8195        if (!HasGenerateConstructorAttribute(typeSymbol) && !HasPositiveFieldGuardTrigger(typeSymbol))
 0196            return false;
 197
 8198        if (!IsDeclaredPartial(typeSymbol))
 0199            return false;
 200
 8201        if (HasExplicitInstanceConstructor(typeSymbol))
 0202            return false;
 203
 8204        return true;
 205    }
 206
 207    /// <summary>
 208    /// True when <paramref name="baseType"/> has an accessible parameterless
 209    /// constructor (public, protected, or protected-internal), including the implicit
 210    /// case where no constructor is declared at all.
 211    /// </summary>
 212    internal static bool HasAccessibleParameterlessConstructor(INamedTypeSymbol baseType)
 213    {
 1187107214        if (baseType.InstanceConstructors.Length == 0)
 127260215            return true;
 216
 3575059217        foreach (var ctor in baseType.InstanceConstructors)
 218        {
 1192763219            if (ctor.Parameters.Length != 0)
 220                continue;
 221
 930161222            if (ctor.DeclaredAccessibility == Accessibility.Public ||
 930161223                ctor.DeclaredAccessibility == Accessibility.Protected ||
 930161224                ctor.DeclaredAccessibility == Accessibility.ProtectedOrInternal)
 225            {
 930161226                return true;
 227            }
 228        }
 229
 129686230        return false;
 231    }
 232
 233    /// <summary>
 234    /// True when any part of <paramref name="typeSymbol"/>'s declaration uses the
 235    /// <c>partial</c> modifier. A generated constructor can only be contributed to a
 236    /// partial type. Checked against <see cref="TypeDeclarationSyntax"/> generally
 237    /// (rather than only <see cref="ClassDeclarationSyntax"/>) so an analyzer can also
 238    /// evaluate this rule against a record declaration before reporting the separate
 239    /// unsupported-shape diagnostic for that record.
 240    /// </summary>
 241    internal static bool IsDeclaredPartial(INamedTypeSymbol typeSymbol)
 242    {
 782243        foreach (var syntaxRef in typeSymbol.DeclaringSyntaxReferences)
 244        {
 259245            if (syntaxRef.GetSyntax() is TypeDeclarationSyntax typeDeclaration &&
 259246                typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
 247            {
 254248                return true;
 249            }
 250        }
 251
 5252        return false;
 253    }
 254
 255    /// <summary>
 256    /// Determines whether a declaration is the canonical source declaration for its
 257    /// type, ordered by file path and source position.
 258    /// </summary>
 259    internal static bool IsCanonicalDeclaration(
 260        INamedTypeSymbol typeSymbol,
 261        TypeDeclarationSyntax typeDeclaration)
 262    {
 2957263        var canonical = typeSymbol.DeclaringSyntaxReferences
 2974264            .OrderBy(reference => reference.SyntaxTree.FilePath, System.StringComparer.Ordinal)
 2968265            .ThenBy(reference => reference.Span.Start)
 2957266            .FirstOrDefault();
 267
 2957268        return canonical is not null &&
 2957269            canonical.SyntaxTree == typeDeclaration.SyntaxTree &&
 2957270            canonical.Span == typeDeclaration.Span;
 271    }
 272
 273    /// <summary>
 274    /// True when <paramref name="typeSymbol"/> already declares an explicit,
 275    /// hand-written instance constructor. Generated-constructor generation is skipped
 276    /// entirely for such a type, to preserve one unambiguous constructor per the
 277    /// feature's contract.
 278    /// </summary>
 279    /// <remarks>
 280    /// A constructor declared in a file whose path ends with the exact
 281    /// <see cref="GeneratedConstructorFileSuffix"/> hint-name suffix
 282    /// <c>GeneratedConstructorGenerator</c> uses for its own output is never treated as
 283    /// a conflicting hand-written constructor. This method is used both by the source
 284    /// generator itself (which only ever observes a pre-generation compilation and so
 285    /// never sees its own output) and by diagnostic analyzers packaged alongside it
 286    /// (which see the fully generator-augmented compilation, where the type's own
 287    /// already-generated constructor would otherwise be indistinguishable from a real,
 288    /// hand-written one). Matching only this precise suffix -- rather than the
 289    /// repository-wide <c>.g.cs</c> generated-file convention shared by every source
 290    /// generator -- ensures a hand-authored constructor checked into some unrelated
 291    /// <c>*.g.cs</c>-suffixed file still correctly conflicts.
 292    /// </remarks>
 293    internal static bool HasExplicitInstanceConstructor(INamedTypeSymbol typeSymbol)
 294    {
 849295        foreach (var ctor in typeSymbol.InstanceConstructors)
 296        {
 215297            if (!ctor.IsImplicitlyDeclared && !IsDeclaredInGeneratedConstructorFile(ctor))
 7298                return true;
 299        }
 300
 206301        return false;
 302    }
 303
 304    private static bool IsDeclaredInGeneratedConstructorFile(ISymbol symbol)
 305    {
 31306        foreach (var location in symbol.Locations)
 307        {
 8308            var filePath = location.SourceTree?.FilePath;
 8309            if (filePath != null && filePath.EndsWith(GeneratedConstructorFileSuffix, System.StringComparison.Ordinal))
 1310                return true;
 311        }
 312
 7313        return false;
 314    }
 315
 316    private static bool IsPositiveConstructorGuardAttribute(AttributeData attribute)
 317    {
 69318        if (attribute.ConstructorArguments.Length == 0)
 0319            return false;
 320
 69321        var first = attribute.ConstructorArguments[0];
 322
 323        // Kind-based guard: None (0) is the only non-positive built-in kind.
 69324        if (first.Kind == TypedConstantKind.Enum && first.Value is int enumValue)
 33325            return enumValue != 0;
 326
 327        // A resolved custom guard type reference is always positive.
 36328        return first.Value is ITypeSymbol;
 329    }
 330
 331    private static bool HasConstructorGuardDefinitionMetaAttribute(INamedTypeSymbol attributeClass)
 332    {
 175333        foreach (var metaAttribute in attributeClass.GetAttributes())
 334        {
 57335            if (metaAttribute.AttributeClass is { } metaClass && IsNeedlrGeneratorsAttribute(metaClass, ConstructorGuard
 55336                return true;
 337        }
 338
 3339        return false;
 340    }
 341
 342    private static List<IFieldSymbol> GetOrderedInstanceFields(INamedTypeSymbol typeSymbol)
 343    {
 3940367344        return typeSymbol.GetMembers()
 3940367345            .OfType<IFieldSymbol>()
 1692523346            .Where(f => !f.IsStatic && !f.IsConst && !f.IsImplicitlyDeclared)
 96386347            .OrderBy(f => f.Locations.FirstOrDefault()?.SourceTree?.FilePath ?? string.Empty, System.StringComparer.Ordi
 96386348            .ThenBy(f => f.Locations.FirstOrDefault()?.SourceSpan.Start ?? 0)
 3940367349            .ToList();
 350    }
 351
 352    private static bool IsEligibleField(IFieldSymbol field)
 353    {
 124610354        return GetFieldIneligibilityReason(field) is null;
 355    }
 356
 357    /// <summary>
 358    /// Returns a short, human-readable reason why <paramref name="field"/> cannot
 359    /// participate in generated-constructor generation, or <see langword="null"/> when
 360    /// the field's shape is eligible (a private, instance, readonly field without an
 361    /// initializer). Used by analyzers to report a targeted diagnostic when a
 362    /// constructor guard attribute is applied to a field that generation would
 363    /// otherwise silently skip.
 364    /// </summary>
 365    internal static string? GetFieldIneligibilityReason(IFieldSymbol field)
 366    {
 124844367        if (field.IsImplicitlyDeclared)
 0368            return "compiler-generated";
 369
 124844370        if (field.IsStatic)
 4371            return "static";
 372
 124840373        if (field.DeclaredAccessibility != Accessibility.Private)
 123616374            return "not private";
 375
 1224376        if (!field.IsReadOnly)
 4377            return "not readonly";
 378
 1220379        if (HasInitializer(field))
 14380            return "initialized with a field initializer";
 381
 1206382        return null;
 383    }
 384
 385    private static bool HasInitializer(IFieldSymbol field)
 386    {
 4866387        foreach (var syntaxRef in field.DeclaringSyntaxReferences)
 388        {
 1220389            if (syntaxRef.GetSyntax() is VariableDeclaratorSyntax { Initializer: not null })
 14390                return true;
 391        }
 392
 1206393        return false;
 394    }
 395}