| | | 1 | | using System.Collections.Generic; |
| | | 2 | | using System.Linq; |
| | | 3 | | |
| | | 4 | | using Microsoft.CodeAnalysis; |
| | | 5 | | using Microsoft.CodeAnalysis.CSharp; |
| | | 6 | | using Microsoft.CodeAnalysis.CSharp.Syntax; |
| | | 7 | | |
| | | 8 | | namespace 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> |
| | | 26 | | internal 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 | | { |
| | 10 | 53 | | return attributeClass.Name == simpleName && |
| | 10 | 54 | | 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 | | { |
| | 16 | 63 | | foreach (var attribute in typeSymbol.GetAttributes()) |
| | | 64 | | { |
| | 4 | 65 | | if (attribute.AttributeClass is { } attrClass && IsNeedlrGeneratorsAttribute(attrClass, GenerateConstructorA |
| | 4 | 66 | | return true; |
| | | 67 | | } |
| | | 68 | | |
| | 2 | 69 | | 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 | | { |
| | 24 | 78 | | foreach (var attribute in field.GetAttributes()) |
| | | 79 | | { |
| | 4 | 80 | | if (attribute.AttributeClass is { } attrClass && IsNeedlrGeneratorsAttribute(attrClass, ConstructorIgnoreAtt |
| | 0 | 81 | | return true; |
| | | 82 | | } |
| | | 83 | | |
| | 8 | 84 | | 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 | | { |
| | 6 | 99 | | foreach (var attribute in field.GetAttributes()) |
| | | 100 | | { |
| | 2 | 101 | | var attrClass = attribute.AttributeClass; |
| | 2 | 102 | | if (attrClass is null) |
| | | 103 | | continue; |
| | | 104 | | |
| | 2 | 105 | | if (IsNeedlrGeneratorsAttribute(attrClass, ConstructorGuardAttributeName)) |
| | | 106 | | { |
| | 2 | 107 | | if (IsPositiveConstructorGuardAttribute(attribute)) |
| | 2 | 108 | | return true; |
| | | 109 | | |
| | | 110 | | continue; |
| | | 111 | | } |
| | | 112 | | |
| | 0 | 113 | | if (HasConstructorGuardDefinitionMetaAttribute(attrClass)) |
| | 0 | 114 | | return true; |
| | | 115 | | } |
| | | 116 | | |
| | 0 | 117 | | 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 | | { |
| | 6 | 126 | | foreach (var field in GetEligibleConstructorFields(typeSymbol)) |
| | | 127 | | { |
| | 2 | 128 | | if (HasPositiveConstructorGuardTrigger(field)) |
| | 2 | 129 | | return true; |
| | | 130 | | } |
| | | 131 | | |
| | 0 | 132 | | return false; |
| | 2 | 133 | | } |
| | | 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 | | { |
| | 9 | 147 | | var result = new List<IFieldSymbol>(); |
| | | 148 | | |
| | 34 | 149 | | foreach (var field in GetOrderedInstanceFields(typeSymbol)) |
| | | 150 | | { |
| | 8 | 151 | | if (!IsEligibleField(field) || HasConstructorIgnoreAttribute(field)) |
| | | 152 | | continue; |
| | | 153 | | |
| | 8 | 154 | | result.Add(field); |
| | | 155 | | } |
| | | 156 | | |
| | 9 | 157 | | 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 | | { |
| | 7 | 173 | | if (typeSymbol.TypeKind != TypeKind.Class || typeSymbol.IsRecord) |
| | 0 | 174 | | 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. |
| | 7 | 179 | | if (typeSymbol.ContainingType != null) |
| | 0 | 180 | | 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. |
| | 7 | 186 | | if (typeSymbol.BaseType != null && typeSymbol.BaseType.SpecialType != SpecialType.System_Object && |
| | 7 | 187 | | !HasAccessibleParameterlessConstructor(typeSymbol.BaseType)) |
| | | 188 | | { |
| | 0 | 189 | | return false; |
| | | 190 | | } |
| | | 191 | | |
| | 7 | 192 | | if (GetEligibleConstructorFields(typeSymbol).Count == 0) |
| | 1 | 193 | | return false; |
| | | 194 | | |
| | 6 | 195 | | if (!HasGenerateConstructorAttribute(typeSymbol) && !HasPositiveFieldGuardTrigger(typeSymbol)) |
| | 0 | 196 | | return false; |
| | | 197 | | |
| | 6 | 198 | | if (!IsDeclaredPartial(typeSymbol)) |
| | 0 | 199 | | return false; |
| | | 200 | | |
| | 6 | 201 | | if (HasExplicitInstanceConstructor(typeSymbol)) |
| | 0 | 202 | | return false; |
| | | 203 | | |
| | 6 | 204 | | 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 | | { |
| | 0 | 214 | | if (baseType.InstanceConstructors.Length == 0) |
| | 0 | 215 | | return true; |
| | | 216 | | |
| | 0 | 217 | | foreach (var ctor in baseType.InstanceConstructors) |
| | | 218 | | { |
| | 0 | 219 | | if (ctor.Parameters.Length != 0) |
| | | 220 | | continue; |
| | | 221 | | |
| | 0 | 222 | | if (ctor.DeclaredAccessibility == Accessibility.Public || |
| | 0 | 223 | | ctor.DeclaredAccessibility == Accessibility.Protected || |
| | 0 | 224 | | ctor.DeclaredAccessibility == Accessibility.ProtectedOrInternal) |
| | | 225 | | { |
| | 0 | 226 | | return true; |
| | | 227 | | } |
| | | 228 | | } |
| | | 229 | | |
| | 0 | 230 | | 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 | | { |
| | 18 | 243 | | foreach (var syntaxRef in typeSymbol.DeclaringSyntaxReferences) |
| | | 244 | | { |
| | 6 | 245 | | if (syntaxRef.GetSyntax() is TypeDeclarationSyntax typeDeclaration && |
| | 6 | 246 | | typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) |
| | | 247 | | { |
| | 6 | 248 | | return true; |
| | | 249 | | } |
| | | 250 | | } |
| | | 251 | | |
| | 0 | 252 | | 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 | | { |
| | 29 | 263 | | var canonical = typeSymbol.DeclaringSyntaxReferences |
| | 33 | 264 | | .OrderBy(reference => reference.SyntaxTree.FilePath, System.StringComparer.Ordinal) |
| | 29 | 265 | | .ThenBy(reference => reference.Span.Start) |
| | 29 | 266 | | .FirstOrDefault(); |
| | | 267 | | |
| | 29 | 268 | | return canonical is not null && |
| | 29 | 269 | | canonical.SyntaxTree == typeDeclaration.SyntaxTree && |
| | 29 | 270 | | 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 | | { |
| | 24 | 295 | | foreach (var ctor in typeSymbol.InstanceConstructors) |
| | | 296 | | { |
| | 6 | 297 | | if (!ctor.IsImplicitlyDeclared && !IsDeclaredInGeneratedConstructorFile(ctor)) |
| | 0 | 298 | | return true; |
| | | 299 | | } |
| | | 300 | | |
| | 6 | 301 | | return false; |
| | | 302 | | } |
| | | 303 | | |
| | | 304 | | private static bool IsDeclaredInGeneratedConstructorFile(ISymbol symbol) |
| | | 305 | | { |
| | 0 | 306 | | foreach (var location in symbol.Locations) |
| | | 307 | | { |
| | 0 | 308 | | var filePath = location.SourceTree?.FilePath; |
| | 0 | 309 | | if (filePath != null && filePath.EndsWith(GeneratedConstructorFileSuffix, System.StringComparison.Ordinal)) |
| | 0 | 310 | | return true; |
| | | 311 | | } |
| | | 312 | | |
| | 0 | 313 | | return false; |
| | | 314 | | } |
| | | 315 | | |
| | | 316 | | private static bool IsPositiveConstructorGuardAttribute(AttributeData attribute) |
| | | 317 | | { |
| | 2 | 318 | | if (attribute.ConstructorArguments.Length == 0) |
| | 0 | 319 | | return false; |
| | | 320 | | |
| | 2 | 321 | | var first = attribute.ConstructorArguments[0]; |
| | | 322 | | |
| | | 323 | | // Kind-based guard: None (0) is the only non-positive built-in kind. |
| | 2 | 324 | | if (first.Kind == TypedConstantKind.Enum && first.Value is int enumValue) |
| | 2 | 325 | | return enumValue != 0; |
| | | 326 | | |
| | | 327 | | // A resolved custom guard type reference is always positive. |
| | 0 | 328 | | return first.Value is ITypeSymbol; |
| | | 329 | | } |
| | | 330 | | |
| | | 331 | | private static bool HasConstructorGuardDefinitionMetaAttribute(INamedTypeSymbol attributeClass) |
| | | 332 | | { |
| | 0 | 333 | | foreach (var metaAttribute in attributeClass.GetAttributes()) |
| | | 334 | | { |
| | 0 | 335 | | if (metaAttribute.AttributeClass is { } metaClass && IsNeedlrGeneratorsAttribute(metaClass, ConstructorGuard |
| | 0 | 336 | | return true; |
| | | 337 | | } |
| | | 338 | | |
| | 0 | 339 | | return false; |
| | | 340 | | } |
| | | 341 | | |
| | | 342 | | private static List<IFieldSymbol> GetOrderedInstanceFields(INamedTypeSymbol typeSymbol) |
| | | 343 | | { |
| | 9 | 344 | | return typeSymbol.GetMembers() |
| | 9 | 345 | | .OfType<IFieldSymbol>() |
| | 8 | 346 | | .Where(f => !f.IsStatic && !f.IsConst && !f.IsImplicitlyDeclared) |
| | 0 | 347 | | .OrderBy(f => f.Locations.FirstOrDefault()?.SourceTree?.FilePath ?? string.Empty, System.StringComparer.Ordi |
| | 0 | 348 | | .ThenBy(f => f.Locations.FirstOrDefault()?.SourceSpan.Start ?? 0) |
| | 9 | 349 | | .ToList(); |
| | | 350 | | } |
| | | 351 | | |
| | | 352 | | private static bool IsEligibleField(IFieldSymbol field) |
| | | 353 | | { |
| | 8 | 354 | | 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 | | { |
| | 8 | 367 | | if (field.IsImplicitlyDeclared) |
| | 0 | 368 | | return "compiler-generated"; |
| | | 369 | | |
| | 8 | 370 | | if (field.IsStatic) |
| | 0 | 371 | | return "static"; |
| | | 372 | | |
| | 8 | 373 | | if (field.DeclaredAccessibility != Accessibility.Private) |
| | 0 | 374 | | return "not private"; |
| | | 375 | | |
| | 8 | 376 | | if (!field.IsReadOnly) |
| | 0 | 377 | | return "not readonly"; |
| | | 378 | | |
| | 8 | 379 | | if (HasInitializer(field)) |
| | 0 | 380 | | return "initialized with a field initializer"; |
| | | 381 | | |
| | 8 | 382 | | return null; |
| | | 383 | | } |
| | | 384 | | |
| | | 385 | | private static bool HasInitializer(IFieldSymbol field) |
| | | 386 | | { |
| | 32 | 387 | | foreach (var syntaxRef in field.DeclaringSyntaxReferences) |
| | | 388 | | { |
| | 8 | 389 | | if (syntaxRef.GetSyntax() is VariableDeclaratorSyntax { Initializer: not null }) |
| | 0 | 390 | | return true; |
| | | 391 | | } |
| | | 392 | | |
| | 8 | 393 | | return false; |
| | | 394 | | } |
| | | 395 | | } |