diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..1efc2a9 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,54 @@ +# Internal Agent Instructions + +Purpose: Helper source files (embedded) for .NET incremental generators; keep output deterministic and public API stable. + +## 1. Scope +Chat = terse (summary + reason + minimal sample). Repo docs = rich (rationale, examples, edge cases, perf). Additive = new files/members only; no deletions, renames, or moves unless explicitly requested. + +## 2. Platform & Stability +netstandard2.0 / C# 7.3. No newer BCL APIs. Preserve public type & member names. + +## 3. Guards +Each helper file can be excluded from the build using a guard. +Nest or document dependencies using guards, depending on how closely related the helper file is to the dependency. +Closely related examples: +``` +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYEXTENSIONS // Feature: EquatableImmutableArrayExtensions +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY // Dependency: EquatableImmutableArray +// contents +#endif // !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#endif // !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYEXTENSIONS +``` +Not closely related examples: +``` +#if !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA // Feature: AttributeContextAndData +#if DATACUTE_EXCLUDE_TYPECONTEXT +#error AttributeContextAndData requires TypeContext (remove DATACUTE_EXCLUDE_TYPECONTEXT or also exclude DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA) +#endif +#if DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#error AttributeContextAndData requires EquatableImmutableArray (remove DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY or also exclude DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA) +#endif +// contents +#endif // !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA +``` + +## 4. Editing & Samples +Show only example code snippets (no diffs) with ≤3 lines of surrounding context when needed. Fenced blocks for multi‑line examples. Use code samples to illustrate changes; do not use diff syntax in chat. Use language tags: csharp for C# code, xml for XML docs, and text for plain text. Short reason (perf / determinism / clarity / bug fix). Keep commentary in the chat. Code changes must omit commentary. Resulting code must look like it was always written that way. + +When showing a change, prefer Before/After paired samples: +- Before: the minimal original snippet required for context. +- After: the revised snippet in full. + +## 5. Public API Doc Comments +For every public type or member emit an XML doc summary (no placeholders). Include // when present; only add if non-trivial behaviour (threading, allocation, ordering, invariants). + +## 6. Documentation +UK English. XML doc first sentence = summary; follow with behaviour / perf / pitfalls if useful. READMEs may include: Overview, Why, Key APIs, Examples, Performance, Dependencies, Exclusion Flags, Instrumentation. Document public surface only (internal/private only if cross‑file contract). Chat brevity does not apply to repo docs. + +## 7. Chat Response Format +1. Summary (1–2 lines) +2. Example code snippet(s) only (no diff format) +3. Optional next steps (label “Optional”). + +## 8. Assumptions +If unspecified, state one reversible assumption in the chat and proceed. diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 0000000..1efc2a9 --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,54 @@ +# Internal Agent Instructions + +Purpose: Helper source files (embedded) for .NET incremental generators; keep output deterministic and public API stable. + +## 1. Scope +Chat = terse (summary + reason + minimal sample). Repo docs = rich (rationale, examples, edge cases, perf). Additive = new files/members only; no deletions, renames, or moves unless explicitly requested. + +## 2. Platform & Stability +netstandard2.0 / C# 7.3. No newer BCL APIs. Preserve public type & member names. + +## 3. Guards +Each helper file can be excluded from the build using a guard. +Nest or document dependencies using guards, depending on how closely related the helper file is to the dependency. +Closely related examples: +``` +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYEXTENSIONS // Feature: EquatableImmutableArrayExtensions +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY // Dependency: EquatableImmutableArray +// contents +#endif // !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#endif // !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYEXTENSIONS +``` +Not closely related examples: +``` +#if !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA // Feature: AttributeContextAndData +#if DATACUTE_EXCLUDE_TYPECONTEXT +#error AttributeContextAndData requires TypeContext (remove DATACUTE_EXCLUDE_TYPECONTEXT or also exclude DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA) +#endif +#if DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#error AttributeContextAndData requires EquatableImmutableArray (remove DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY or also exclude DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA) +#endif +// contents +#endif // !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA +``` + +## 4. Editing & Samples +Show only example code snippets (no diffs) with ≤3 lines of surrounding context when needed. Fenced blocks for multi‑line examples. Use code samples to illustrate changes; do not use diff syntax in chat. Use language tags: csharp for C# code, xml for XML docs, and text for plain text. Short reason (perf / determinism / clarity / bug fix). Keep commentary in the chat. Code changes must omit commentary. Resulting code must look like it was always written that way. + +When showing a change, prefer Before/After paired samples: +- Before: the minimal original snippet required for context. +- After: the revised snippet in full. + +## 5. Public API Doc Comments +For every public type or member emit an XML doc summary (no placeholders). Include // when present; only add if non-trivial behaviour (threading, allocation, ordering, invariants). + +## 6. Documentation +UK English. XML doc first sentence = summary; follow with behaviour / perf / pitfalls if useful. READMEs may include: Overview, Why, Key APIs, Examples, Performance, Dependencies, Exclusion Flags, Instrumentation. Document public surface only (internal/private only if cross‑file contract). Chat brevity does not apply to repo docs. + +## 7. Chat Response Format +1. Summary (1–2 lines) +2. Example code snippet(s) only (no diff format) +3. Optional next steps (label “Optional”). + +## 8. Assumptions +If unspecified, state one reversible assumption in the chat and proceed. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8bbb3..1edd70f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.6] - 2025-08-16 + +### Added + +- Lots more documentation. + +### Changed + +- Include version in auto-generated comments added by this generator. + ## [1.0.5] - 2025-08-03 ### Fixed @@ -96,7 +106,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -[Unreleased]: https://github.com/datacute/IncrementalGeneratorExtensions/compare/1.0.5...develop +[Unreleased]: https://github.com/datacute/IncrementalGeneratorExtensions/compare/1.0.6...develop +[1.0.6]: https://github.com/datacute/IncrementalGeneratorExtensions/releases/tag/1.0.6 [1.0.5]: https://github.com/datacute/IncrementalGeneratorExtensions/releases/tag/1.0.5 [1.0.4]: https://github.com/datacute/IncrementalGeneratorExtensions/releases/tag/1.0.4 [1.0.3]: https://github.com/datacute/IncrementalGeneratorExtensions/releases/tag/1.0.3 diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/AttributeContextAndData.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/AttributeContextAndData.cs index 39becd5..55872e0 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/AttributeContextAndData.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/AttributeContextAndData.cs @@ -1,16 +1,18 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA && !DATACUTE_EXCLUDE_TYPECONTEXT +#if !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA // Feature: AttributeContextAndData +#if DATACUTE_EXCLUDE_TYPECONTEXT +#error AttributeContextAndData requires TypeContext (remove DATACUTE_EXCLUDE_TYPECONTEXT or also exclude DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA) +#endif +#if DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#error AttributeContextAndData requires EquatableImmutableArray (remove DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY or also exclude DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA) +#endif +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Text; using System.Threading; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Datacute.IncrementalGeneratorExtensions { @@ -78,16 +80,15 @@ public AttributeContextAndData( } /// - public bool Equals(AttributeContextAndData other) - { - return Context.Equals(other.Context) && Equals(ContainingTypes, other.ContainingTypes) && AttributeData.Equals(other.AttributeData); - } + public bool Equals(AttributeContextAndData other) => + Context.Equals(other.Context) && + Equals(ContainingTypes, other.ContainingTypes) && + EqualityComparer.Default.Equals(AttributeData, other.AttributeData) && + IsInFileScopedNamespace == other.IsInFileScopedNamespace; /// - public override bool Equals(object obj) - { - return obj is AttributeContextAndData other && Equals(other); - } + public override bool Equals(object obj) => + obj is AttributeContextAndData other && Equals(other); /// public override int GetHashCode() @@ -95,8 +96,9 @@ public override int GetHashCode() unchecked { var hashCode = Context.GetHashCode(); - hashCode = (hashCode * 397) ^ (ContainingTypes != null ? ContainingTypes.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (AttributeData != null ? AttributeData.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ ContainingTypes.GetHashCode(); + hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(AttributeData); + hashCode = (hashCode * 397) ^ (IsInFileScopedNamespace ? 1 : 0); return hashCode; } } @@ -154,11 +156,11 @@ public static bool Predicate(SyntaxNode syntaxNode, CancellationToken token) LightweightTrace.IncrementCount(GeneratorStage.ForAttributeWithMetadataNamePredicate); #endif // We are only interested in partial type declarations - var typeDeclaration = syntaxNode as TypeDeclarationSyntax; - if (typeDeclaration == null || !typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) - { - return false; - } + if (!(syntaxNode is TypeDeclarationSyntax typeDeclaration)) + return false; // Not a type declaration + + if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + return false; // Must be partial // Now, ensure all containing types are also partial. // This is necessary to be able to generate code for partial types correctly. @@ -212,19 +214,8 @@ public static AttributeContextAndData Transform( var attributeTargetSymbol = (ITypeSymbol)generatorAttributeSyntaxContext.TargetSymbol; var typeDeclaration = (TypeDeclarationSyntax)generatorAttributeSyntaxContext.TargetNode; - var isInFileScopedNamespace = false; - if (typeDeclaration.SyntaxTree.GetRoot(token) is CompilationUnitSyntax compilationUnit) - { - foreach (var member in compilationUnit.Members) - { - if (member.GetType().Name == "FileScopedNamespaceDeclarationSyntax") - { - isInFileScopedNamespace = true; - break; - } - } - } - + var isInFileScopedNamespace = HasFileScopedNamespace(typeDeclaration.SyntaxTree, token); + var isNullableContextEnabled = GetIsNullableContextEnabled(generatorAttributeSyntaxContext.SemanticModel, typeDeclaration.SpanStart); EquatableImmutableArray typeParameterNames; @@ -240,16 +231,7 @@ public static AttributeContextAndData Transform( typeParameterNames = EquatableImmutableArray.Empty; } - var typeContext = new TypeContext( - TypeContext.GetNamespaceDisplayString(attributeTargetSymbol.ContainingNamespace), - attributeTargetSymbol.Name, - attributeTargetSymbol.IsStatic, - isPartial: true, // Known to be true because of the predicate - attributeTargetSymbol.IsAbstract, - attributeTargetSymbol.IsSealed, - attributeTargetSymbol.DeclaredAccessibility, - TypeContext.GetTypeDeclarationKeyword(attributeTargetSymbol), - typeParameterNames); + var typeContext = CreateTypeContext(attributeTargetSymbol, isPartial: true, typeParameterNames); // Parse parent classes from symbol's containing types var parentClassCount = 0; @@ -274,18 +256,9 @@ public static AttributeContextAndData Transform( : EquatableImmutableArray.Empty; // The predicate has already confirmed that this type is partial. - var containingTypeIsPartial = true; - - containingTypesImmutableArrayBuilder.Insert(0, new TypeContext( - TypeContext.GetNamespaceDisplayString(containingType.ContainingNamespace), - containingType.Name, - containingType.IsStatic, - containingTypeIsPartial, - containingType.IsAbstract, - containingType.IsSealed, - containingType.DeclaredAccessibility, - TypeContext.GetTypeDeclarationKeyword(containingType), - containingTypeTypeParameterNames)); + const bool containingTypeIsPartial = true; + var containingTypeContext = CreateTypeContext(containingType, containingTypeIsPartial, containingTypeTypeParameterNames); + containingTypesImmutableArrayBuilder.Insert(0, containingTypeContext); containingType = containingType.ContainingType; } @@ -296,19 +269,48 @@ public static AttributeContextAndData Transform( containingTypes = EquatableImmutableArray.Empty; } - var attributeContextAndData = new AttributeContextAndData( + return new AttributeContextAndData( typeContext, containingTypes, attributeData, isInFileScopedNamespace, isNullableContextEnabled); + } + private static TypeContext CreateTypeContext( + ITypeSymbol symbol, + bool isPartial, + EquatableImmutableArray typeParameterNames) + { + return new TypeContext( + TypeContext.GetNamespaceDisplayString(symbol.ContainingNamespace), + symbol.Name, + symbol.IsStatic, + isPartial, + symbol.IsAbstract, + symbol.IsSealed, + symbol.DeclaredAccessibility, + TypeContext.GetTypeDeclarationKeyword(symbol), + typeParameterNames); + } - return attributeContextAndData; + private static bool HasFileScopedNamespace(SyntaxTree syntaxTree, CancellationToken token) + { + if (!(syntaxTree.GetRoot(token) is CompilationUnitSyntax root)) + return false; + + foreach (var member in root.Members) + { + if (member.GetType().Name == "FileScopedNamespaceDeclarationSyntax") + return true; + } + + return false; } - - + // This delegate is initialized to point to the bootstrap method. // After the first run, it will point to the final, efficient implementation. + // ReSharper disable once StaticMemberInGenericType There's typically only one instance. + // ReSharper disable once InconsistentNaming purposely named like a method private static Func GetIsNullableContextEnabled = BootstrapGetIsNullableContextEnabled; private static bool BootstrapGetIsNullableContextEnabled(SemanticModel semanticModel, int position) @@ -338,4 +340,4 @@ private static bool BootstrapGetIsNullableContextEnabled(SemanticModel semanticM } } } -#endif \ No newline at end of file +#endif // !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA \ No newline at end of file diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/AttributeContextAndDataExtensions.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/AttributeContextAndDataExtensions.cs index a3aea57..5b79e93 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/AttributeContextAndDataExtensions.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/AttributeContextAndDataExtensions.cs @@ -1,14 +1,13 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATAEXTENSIONS && !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA && !DATACUTE_EXCLUDE_TYPECONTEXT +#if !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATAEXTENSIONS // Feature: AttributeContextAndDataExtensions +#if !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA // Dependency: AttributeContextAndData using System; using Microsoft.CodeAnalysis; namespace Datacute.IncrementalGeneratorExtensions { + /// + /// Extension methods for wiring up collection into an incremental generator pipeline. + /// public static class AttributeContextAndDataExtensions { /// @@ -42,11 +41,12 @@ public static IncrementalValuesProvider> SelectAttrib predicate: AttributeContextAndData.Predicate, transform: (syntaxContext, token) => AttributeContextAndData.Transform(syntaxContext, attributeDataCollector, token)) -#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACEEXTENSIONS && !DATACUTE_EXCLUDE_LIGHTWEIGHT && !DATACUTE_EXCLUDE_GENERATORSTAGE +#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACEEXTENSIONS && !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE && !DATACUTE_EXCLUDE_GENERATORSTAGE .WithTrackingName(GeneratorStage.ForAttributeWithMetadataName); #else ; #endif } } -#endif \ No newline at end of file +#endif // !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA +#endif // !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATAEXTENSIONS diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArray.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArray.cs index 8b16d85..145b66b 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArray.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArray.cs @@ -1,12 +1,6 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -20,32 +14,75 @@ namespace Datacute.IncrementalGeneratorExtensions public sealed class EquatableImmutableArray : IEquatable>, IReadOnlyList where T : IEquatable { + /// + /// Gets a shared empty instance of . + /// public static EquatableImmutableArray Empty { get; } = new EquatableImmutableArray(ImmutableArray.Empty, 0); // Static factory method with singleton handling - public static EquatableImmutableArray Create(ImmutableArray values, CancellationToken cancellationToken = default) - => EquatableImmutableArrayInstanceCache.GetOrCreate(values, cancellationToken); + /// + /// Creates an for the provided immutable array, reusing a cached instance when possible. + /// + /// The immutable array backing the equatable wrapper. + /// A cancellation token. + /// An instance representing the supplied values (possibly a cached singleton for empty). + public static EquatableImmutableArray Create(ImmutableArray values, CancellationToken cancellationToken = default) + { + if (values.IsEmpty) + return Empty; +#if DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYINSTANCECACHE + // Cache disabled: compute hash directly and return a new instance (no reuse / instrumentation events) + var comparer = EqualityComparer.Default; + int hash = CalculateHashCode(values, comparer, 0, 0); + return new EquatableImmutableArray(values, hash); +#else + return EquatableImmutableArrayInstanceCache.GetOrCreate(values, cancellationToken); +#endif + } private readonly ImmutableArray _values; private readonly int _hashCode; - private readonly int _length; + /// + /// Gets the element at the specified index. + /// + /// Zero-based index. public T this[int index] => _values[index]; - public int Count => _length; - + /// + /// Gets the number of elements in the array. + /// + public int Count => _values.Length; + // Properties Duplicated from ImmutableArray - public int Length => _length; - public bool IsEmpty => _length == 0; + + /// + /// Gets the number of elements in the array (alias of ). + /// + public int Length => _values.Length; + /// + /// True if the underlying array has length 0. + /// + public bool IsEmpty => _values.Length == 0; + /// + /// True if the underlying immutable array is in its default (uninitialised) state. + /// public bool IsDefault => _values.IsDefault; + /// + /// True if the underlying immutable array is either default or empty. + /// public bool IsDefaultOrEmpty => _values.IsDefaultOrEmpty; internal EquatableImmutableArray(ImmutableArray values, int hashCode) { _values = values; - _length = values.Length; _hashCode = hashCode; } + /// + /// Determines value equality with another . + /// + /// The other instance. + /// True if both contain the same sequence of values; otherwise false. public bool Equals(EquatableImmutableArray other) { // Fast reference equality check @@ -57,17 +94,18 @@ public bool Equals(EquatableImmutableArray other) if (_hashCode != other._hashCode) return false; // We're really unlikely to get here, as we're using an instance cache - // so we've probably encountered a hash collision - + // so we've probably encountered a hash collision, + // or the instance cache is disabled. + // Compare array lengths - if (_length != other._length) return false; + if (_values.Length != other._values.Length) return false; // If both are empty, they're equal - if (_length == 0) return true; + if (_values.Length == 0) return true; // Element-by-element comparison var comparer = EqualityComparer.Default; - for (int i = 0; i < _length; i++) + for (int i = 0; i < _values.Length; i++) { if (!comparer.Equals(_values[i], other._values[i])) return false; @@ -76,12 +114,30 @@ public bool Equals(EquatableImmutableArray other) return true; } + /// public override bool Equals(object obj) => obj is EquatableImmutableArray other && Equals(other); + /// public override int GetHashCode() => _hashCode; IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator(); + + internal static int CalculateHashCode(ImmutableArray values, EqualityComparer comparer, int currentHash, int startIndex) + { + int hash = currentHash; + for (var index = startIndex; index < values.Length; index++) + { + var value = values[index]; + hash = HashHelpers_Combine(hash, value == null ? 0 : comparer.GetHashCode(value)); + } + return hash; + } + internal static int HashHelpers_Combine(int h1, int h2) + { + uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); + return ((int)rol5 + h1) ^ h2; + } } } #endif \ No newline at end of file diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayExtensions.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayExtensions.cs index fd137c4..411ccb6 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayExtensions.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayExtensions.cs @@ -1,9 +1,5 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYEXTENSIONS +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYEXTENSIONS // Feature: EquatableImmutableArrayExtensions +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY // Dependency: EquatableImmutableArray using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -150,6 +146,12 @@ public static EquatableImmutableArray ToEquatableImmutableArray(th where TRight : IEquatable => provider1.Combine(provider2.CollectEquatable()); + /// + /// Collects an and converts the to an . + /// + /// The incremental values provider to collect. + /// The type of the elements in the provider, which must implement . + /// An producing the collected values as an . public static IncrementalValueProvider> CollectEquatable( this IncrementalValuesProvider provider) where T : IEquatable @@ -157,4 +159,5 @@ public static IncrementalValueProvider> CollectEquata } } -#endif \ No newline at end of file +#endif // !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#endif // !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYEXTENSIONS \ No newline at end of file diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayInstanceCache.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayInstanceCache.cs index 1c3e2eb..91beb5c 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayInstanceCache.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/EquatableImmutableArrayInstanceCache.cs @@ -1,9 +1,5 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYINSTANCECACHE // Feature: EquatableImmutableArrayInstanceCache (optional) +#if !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY // Dependency: EquatableImmutableArray using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -114,7 +110,7 @@ public static EquatableImmutableArray GetOrCreate(ImmutableArray values, C } // No match found, calculate hash and create new instance - var hash = CalculateHashCode(values, comparer, firstElementHash, 1); + var hash = EquatableImmutableArray.CalculateHashCode(values, comparer, firstElementHash, 1); #if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE && !DATACUTE_EXCLUDE_GENERATORSTAGE // Record a histogram of the array sizes we are being asked to create LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayCacheMiss, values.Length); @@ -125,25 +121,7 @@ public static EquatableImmutableArray GetOrCreate(ImmutableArray values, C return newResult; } } - - private static int CalculateHashCode(ImmutableArray values, EqualityComparer comparer, int currentHash, int hashedValues) - { - int hash = currentHash; - for (var index = hashedValues; index < values.Length; index++) - { - var value = values[index]; - hash = HashHelpers_Combine(hash, value == null ? 0 : comparer.GetHashCode(value)); - } - return hash; - } - - private static int HashHelpers_Combine(int h1, int h2) - { - // RyuJIT optimizes this to use the ROL instruction - // Related GitHub pull request: https://github.com/dotnet/coreclr/pull/1830 - uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); - return ((int)rol5 + h1) ^ h2; - } } } -#endif \ No newline at end of file +#endif // !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#endif // !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYINSTANCECACHE diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStage.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStage.cs index cadb8c9..bade826 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStage.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStage.cs @@ -1,9 +1,4 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_GENERATORSTAGE +#if !DATACUTE_EXCLUDE_GENERATORSTAGE namespace Datacute.IncrementalGeneratorExtensions { /// diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStageDescriptions.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStageDescriptions.cs index b766aef..b6e89de 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStageDescriptions.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/GeneratorStageDescriptions.cs @@ -1,9 +1,5 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_GENERATORSTAGEDESCRIPTIONS && !DATACUTE_EXCLUDE_GENERATORSTAGE +#if !DATACUTE_EXCLUDE_GENERATORSTAGEDESCRIPTIONS // Feature: GeneratorStageDescriptions +#if !DATACUTE_EXCLUDE_GENERATORSTAGE // Dependency: GeneratorStage using System.Collections.Generic; namespace Datacute.IncrementalGeneratorExtensions @@ -74,4 +70,5 @@ public static class GeneratorStageDescriptions }; } } -#endif \ No newline at end of file +#endif // !DATACUTE_EXCLUDE_GENERATORSTAGE +#endif // !DATACUTE_EXCLUDE_GENERATORSTAGEDESCRIPTIONS \ No newline at end of file diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/IndentingLineAppender.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/IndentingLineAppender.cs index 56bf7c6..f3ac0f7 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/IndentingLineAppender.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/IndentingLineAppender.cs @@ -1,9 +1,4 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_INDENTINGLINEAPPENDER +#if !DATACUTE_EXCLUDE_INDENTINGLINEAPPENDER using System; using System.Collections.Generic; using System.IO; @@ -30,8 +25,8 @@ public class IndentingLineAppender private string _currentIndentString = string.Empty; private readonly StringBuilder _buffer; - private char _indentationCharacter; - private int _indentationCharacterRepetition; + private readonly char _indentationCharacter; + private readonly int _indentationCharacterRepetition; private readonly string _blockStart; private readonly string _blockEnd; private readonly Dictionary _indentationCache = new Dictionary(); @@ -54,6 +49,9 @@ public int IndentLevel } } + private void IncreaseIndent() { IndentLevel = _indentLevel + 1; } + private void DecreaseIndent() { IndentLevel = _indentLevel - 1; } + /// /// Initializes a new instance of the class with a default . /// @@ -100,6 +98,8 @@ public IndentingLineAppender( _blockEnd = blockEnd; SingleIndent = new string(_indentationCharacter, _indentationCharacterRepetition); + _indentationCache.Add(0, string.Empty); + _indentationCache.Add(1, SingleIndent); } /// @@ -133,8 +133,7 @@ public string StringForIndent(int indent) public IndentingLineAppender Clear() { _buffer.Clear(); - _indentLevel = 0; - _currentIndentString = string.Empty; + IndentLevel = 0; return this; } @@ -206,8 +205,7 @@ public IndentingLineAppender Append(char c) public IndentingLineAppender AppendStartBlock() { AppendLine(_blockStart); - _indentLevel++; - UpdateCurrentIndent(); + IncreaseIndent(); return this; } @@ -225,8 +223,7 @@ public IndentingLineAppender AppendStartBlock() /// Returns the current instance for method chaining. public IndentingLineAppender AppendEndBlock() { - _indentLevel--; - UpdateCurrentIndent(); + DecreaseIndent(); AppendLine(_blockEnd); return this; } diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/LightweightTrace.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/LightweightTrace.cs index 8e3aaa6..b3114b0 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/LightweightTrace.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/LightweightTrace.cs @@ -1,9 +1,4 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE +#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -15,14 +10,36 @@ namespace Datacute.IncrementalGeneratorExtensions { /// - /// A lightweight tracing utility that allows for efficient logging of events and counters. + /// Zero-allocation instrumentation core for incremental source generators. + /// Features: ring-buffer timestamped event trace; composite-key counters (id + value + mapping flag) for histograms & categorical counts; automatic method-call frequency counting; method entry/exit tagging; single-int key encoding to minimize memory & dictionary churn; unified AppendDiagnosticsComment output (counters + trace) embeddable in generated code. + /// Goal: fast, in-process behavioral visibility without external profilers or large allocations. /// public static class LightweightTrace { private const int Capacity = 1024; - private const int KeyValueShift = 1 << 10; - private const int MapValue = 1 << 28; + /// + /// Size of the contiguous ID range (also the multiplier for packing values): + /// compositeKey = id + (value * CompositeValueShift) (+ MapValueFlag). + /// id must be < CompositeValueShift; value is shifted by this amount when encoded. + /// + public const int CompositeValueShift = 1 << ValueShift; // 1024 id range (value bucket multiplier) + + /// + /// A flag bit used in composite keys to indicate that the encoded value should be mapped via eventNameMap. + /// This lets values represent enum-like categories instead of plain numbers when formatting output. + /// + public const int MapValueFlag = 1 << 28; + + // Bit layout (little endian within int): + // bits 0..9 : id (0..1023) + // bits 10..27 : value (0..(2^18 - 1)) + // bit 28 : map flag (categorical value lookup) + // bits 29..31 : currently unused (reserved) + private const int IdMask = CompositeValueShift - 1; // 0x3FF mask for id bits (bits 0..ValueShift-1) + private const int ValueShift = 10; // Number of bits reserved for the id (0..(2^ValueShift - 1)) + private const int ValueBits = 18; // Number of bits reserved for the value component (bits ValueShift .. ValueShift+ValueBits-1) + private const int ValueMask = ((1 << ValueBits) - 1) << ValueShift; // covers bits 10..27 private static readonly DateTime StartTime = DateTime.UtcNow; private static readonly Stopwatch Stopwatch = Stopwatch.StartNew(); @@ -36,6 +53,7 @@ public static class LightweightTrace /// The ID of the event to log. /// The type of the event ID, which must be an enum, either , or your own. public static void Add(TEnum eventId) where TEnum : Enum => Add(Convert.ToInt32(eventId)); + /// /// Adds an event to the trace log with the specified event ID and numeric value. /// @@ -43,6 +61,7 @@ public static class LightweightTrace /// The value associated with the event, which can be used for additional context or categorization. /// The type of the event ID, which must be an enum, either , or your own. public static void Add(TEnum eventId, int value) where TEnum : Enum => Add(Convert.ToInt32(eventId), value); + /// /// Adds an event to the trace log with the specified event ID and enum value. /// @@ -51,13 +70,15 @@ public static class LightweightTrace /// The type of the event ID, which must be an enum, either , or your own. /// The type of the value, which must be an enum, either , or your own. public static void Add(TEnumKey eventId, TEnumValue value) where TEnumKey : Enum where TEnumValue : Enum => Add(Convert.ToInt32(eventId), Convert.ToInt32(value), true); + /// /// Adds an event to the trace log with the specified numeric event ID and value. /// /// The ID of the event to log. /// The value associated with the event, which can be used for additional context or categorization. /// If true, the value is treated as a mapped value when generating the diagnostic log. - public static void Add(int eventId, int value, bool mapValue = false) => Add(eventId + value * KeyValueShift, mapValue); + public static void Add(int eventId, int value, bool mapValue = false) => Add(EncodeKey(eventId, value, mapValue)); + /// /// Adds an event to the trace log with the specified numeric event ID. /// @@ -66,14 +87,14 @@ public static class LightweightTrace public static void Add(int eventId, bool mapValue = false) { var index = Interlocked.Increment(ref _index) % Capacity; - Events[index] = (Stopwatch.ElapsedTicks, eventId | (mapValue ? MapValue : 0)); + Events[index] = (Stopwatch.ElapsedTicks, eventId | (mapValue ? MapValueFlag : 0)); #if !DATACUTE_EXCLUDE_GENERATORSTAGE - if ((eventId / KeyValueShift) != Convert.ToInt32(GeneratorStage.MethodExit)) + if ((eventId / CompositeValueShift) != Convert.ToInt32(GeneratorStage.MethodExit)) { - IncrementCount(GeneratorStage.MethodCall, eventId % KeyValueShift, true); + IncrementCount(GeneratorStage.MethodCall, eventId % CompositeValueShift, true); } #else - IncrementCount(eventId % KeyValueShift); + IncrementCount(eventId % CompositeValueShift); #endif } @@ -84,6 +105,7 @@ public static void Add(int eventId, bool mapValue = false) /// The ID of the event to log. /// The type of the event ID, which must be an enum, either , or your own. public static void MethodEntry(TEnum eventId) where TEnum : Enum => Add(Convert.ToInt32(eventId), Convert.ToInt32(GeneratorStage.MethodEntry), true); + /// /// Adds a method exit event to the trace log with the specified event ID. /// @@ -111,10 +133,10 @@ public static void AppendTrace(this StringBuilder stringBuilder, Dictionary 0) { - var textAndValue = GetTextAndValue(eventNameMap, eventId); + var textAndValue = FormatEventKey(eventNameMap, eventId); stringBuilder.AppendFormat("{0:o} [{1:000}] {2}", StartTime.AddTicks(timestamp), - eventId % KeyValueShift, + eventId % CompositeValueShift, textAndValue) .AppendLine(); } @@ -129,6 +151,7 @@ public static void AppendTrace(this StringBuilder stringBuilder, DictionaryThe ID of the counter to increment. /// The type of the counter ID, which must be an enum, either , or your own. public static void IncrementCount(TEnum counterId) where TEnum : Enum => IncrementCount(Convert.ToInt32(counterId)); + /// /// Increments the value of a given key[value] combination by 1. /// @@ -150,6 +173,7 @@ public static void AppendTrace(this StringBuilder stringBuilder, DictionaryThe ID of the counter to decrement. /// The type of the counter ID, which must be an enum, either , or your own. public static void DecrementCount(TEnum counterId) where TEnum : Enum => DecrementCount(Convert.ToInt32(counterId)); + /// /// Decrements the value of a given key[value] combination by 1. /// @@ -164,26 +188,28 @@ public static void AppendTrace(this StringBuilder stringBuilder, Dictionary /// The ID of the counter to increment. public static void IncrementCount(int counterId) => Counters.AddOrUpdate(counterId, 1, (_, count) => count + 1); + /// /// Increments the value of a given key[value] combination by 1. /// /// The ID of the counter to increment. /// The value associated with the counter, which can be used for additional context or categorization. /// If true, the value is treated as a mapped value when generating the diagnostic log. - public static void IncrementCount(int counterId, int value, bool mapValue = false) => Counters.AddOrUpdate(counterId + value * KeyValueShift + (mapValue ? MapValue : 0), 1, (_, count) => count + 1); + public static void IncrementCount(int counterId, int value, bool mapValue = false) => Counters.AddOrUpdate(EncodeKey(counterId, value, mapValue), 1, (_, count) => count + 1); /// /// Decrements the value of a given key by 1. /// /// The ID of the counter to increment. public static void DecrementCount(int counterId) => Counters.AddOrUpdate(counterId, -1, (_, count) => count - 1); + /// /// Decrements the value of a given key[value] combination by 1. /// /// The ID of the counter to decrement. /// The value associated with the counter, which can be used for additional context or categorization. /// If true, the value is treated as a mapped value when generating the diagnostic log. - public static void DecrementCount(int counterId, int value, bool mapValue = false) => Counters.AddOrUpdate(counterId + value * KeyValueShift + (mapValue ? MapValue : 0), -1, (_, count) => count - 1); + public static void DecrementCount(int counterId, int value, bool mapValue = false) => Counters.AddOrUpdate(EncodeKey(counterId, value, mapValue), -1, (_, count) => count - 1); /// /// Gets a string with the current cache performance metrics. @@ -199,53 +225,74 @@ public static void AppendCounts(this StringBuilder stringBuilder, Dictionary kvp.Key % KeyValueShift).ThenBy(kvp => kvp.Key)) + foreach (var kvp in Counters.OrderBy(kvp => kvp.Key % CompositeValueShift).ThenBy(kvp => kvp.Key)) { int counterId = kvp.Key; long count = kvp.Value; - var textAndValue = GetTextAndValue(eventNameMap, counterId); + var textAndValue = FormatEventKey(eventNameMap, counterId); stringBuilder.AppendFormat( "[{0:000}] {1}: {2}", - counterId % KeyValueShift, textAndValue, count) + counterId % CompositeValueShift, textAndValue, count) .AppendLine(); } } - private static string GetTextAndValue(Dictionary eventNameMap, int key) + /// + /// Encodes a composite key combining an ID and a value into a single int. + /// Set when the value represents a categorical/enum mapping rather than a numeric measurement. + /// + /// The base ID (0..CompositeValueShift-1). + /// The associated value bucket or enum ordinal. + /// True to mark the value as mapped (name lookup) instead of numeric. + public static int EncodeKey(int id, int value, bool mapValue = false) => + id + (value * CompositeValueShift) + (mapValue ? MapValueFlag : 0); + + /// + /// Decodes a composite key into its ID, value, and mapped-value flag. + /// + /// The composite key previously produced by or any API that accepts (id,value). + /// The extracted base ID. + /// The extracted associated value. + /// True if the value should be mapped by name for display. + public static void DecodeKey(int key, out int id, out int value, out bool isMappedValue) { - int id = key % KeyValueShift; - int value = key / KeyValueShift; + isMappedValue = (key & MapValueFlag) != 0; + id = key & IdMask; + value = (key & ValueMask) >> ValueShift; + } - if ((key & MapValue) != 0) - { - value = (key & ~MapValue) / KeyValueShift; - } + private static string FormatEventKey(Dictionary eventNameMap, int key) + { + DecodeKey(key, out var id, out var value, out var mapped); - string text = null; + string idText = null; if (eventNameMap != null) { - eventNameMap.TryGetValue(id, out text); + eventNameMap.TryGetValue(id, out idText); } - if (text == null) + if (idText == null) { - text = string.Empty; + idText = string.Empty; + } + + if (!mapped && value == 0 && key < CompositeValueShift) + { + return idText; } string valueText = null; - if (key >= KeyValueShift) + if (mapped && eventNameMap != null) { - if (eventNameMap != null && (key & MapValue) != 0) - { - eventNameMap.TryGetValue(value, out valueText); - } - if (valueText == null) - { - valueText = $"{value}"; - } + eventNameMap.TryGetValue(value, out valueText); + } + + if (valueText == null) + { + valueText = value.ToString(); } - return (key >= KeyValueShift) ? $"{text} ({valueText})" : text; + return idText.Length == 0 ? valueText : $"{idText} ({valueText})"; } /// diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/LightweightTraceExtensions.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/LightweightTraceExtensions.cs index cb265b9..8557e77 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/LightweightTraceExtensions.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/LightweightTraceExtensions.cs @@ -1,9 +1,5 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACEEXTENSIONS && !DATACUTE_EXCLUDE_LIGHTWEIGHT +#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACEEXTENSIONS // Feature: LightweightTraceExtensions +#if !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE // Dependency: LightweightTrace using System; using System.Threading; using Microsoft.CodeAnalysis; @@ -11,12 +7,10 @@ namespace Datacute.IncrementalGeneratorExtensions { /// - /// Extensions for and to add tracing capabilities. + /// Integration layer: injects LightweightTrace instrumentation (event logging, counters, value buckets, enum mapping) into / pipelines and adds cancellation helpers that both log and throw. /// /// - /// Also includes methods to trace and throw if a is cancelled. - /// It is important to check for cancellation regularly within generators, - /// as the user may have pressed a key, changing the source, and requiring the generation to restart. + /// Encourages regular cancellation checks (logged with a tagged event) so long-running pipelines restart quickly on source edits while still producing a coherent diagnostics block. /// public static class LightweightTraceExtensions { @@ -231,4 +225,5 @@ public static void ThrowIfCancellationRequested( #endif } } -#endif \ No newline at end of file +#endif // !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACE +#endif // !DATACUTE_EXCLUDE_LIGHTWEIGHTTRACEEXTENSIONS \ No newline at end of file diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/SourceTextGeneratorBase.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/SourceTextGeneratorBase.cs index 64d8d3f..17f75b5 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/SourceTextGeneratorBase.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/SourceTextGeneratorBase.cs @@ -1,9 +1,16 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_SOURCETEXTGENERATORBASE && !DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA && !DATACUTE_EXCLUDE_TYPECONTEXT && !DATACUTE_EXCLUDE_INDENTINGLINEAPPENDER +#if !DATACUTE_EXCLUDE_SOURCETEXTGENERATORBASE // Feature: SourceTextGeneratorBase +#if DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA +#error SourceTextGeneratorBase requires AttributeContextAndData (remove DATACUTE_EXCLUDE_ATTRIBUTECONTEXTANDDATA or also exclude DATACUTE_EXCLUDE_SOURCETEXTGENERATORBASE) +#endif +#if DATACUTE_EXCLUDE_TYPECONTEXT +#error SourceTextGeneratorBase requires TypeContext (remove DATACUTE_EXCLUDE_TYPECONTEXT or also exclude DATACUTE_EXCLUDE_SOURCETEXTGENERATORBASE) +#endif +#if DATACUTE_EXCLUDE_INDENTINGLINEAPPENDER +#error SourceTextGeneratorBase requires IndentingLineAppender (remove DATACUTE_EXCLUDE_INDENTINGLINEAPPENDER or also exclude DATACUTE_EXCLUDE_SOURCETEXTGENERATORBASE) +#endif +#if DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#error SourceTextGeneratorBase requires EquatableImmutableArray (remove DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY or also exclude DATACUTE_EXCLUDE_SOURCETEXTGENERATORBASE) +#endif using System; using System.Text; using System.Threading; @@ -313,4 +320,4 @@ protected virtual void AppendDiagnosticLogs() } } } -#endif \ No newline at end of file +#endif // !DATACUTE_EXCLUDE_SOURCETEXTGENERATORBASE \ No newline at end of file diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/TabIndentingLineAppender.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/TabIndentingLineAppender.cs index 24c2495..871fe55 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/TabIndentingLineAppender.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/TabIndentingLineAppender.cs @@ -1,9 +1,7 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_TABINDENTINGLINEAPPENDER && !DATACUTE_EXCLUDE_INDENTINGLINEAPPENDER +#if !DATACUTE_EXCLUDE_TABINDENTINGLINEAPPENDER // Feature: TabIndentingLineAppender +#if DATACUTE_EXCLUDE_INDENTINGLINEAPPENDER +#error TabIndentingLineAppender requires IndentingLineAppender (remove DATACUTE_EXCLUDE_INDENTINGLINEAPPENDER or also exclude DATACUTE_EXCLUDE_TABINDENTINGLINEAPPENDER) +#endif using System.Text; namespace Datacute.IncrementalGeneratorExtensions @@ -46,4 +44,5 @@ public TabIndentingLineAppender( } } } -#endif \ No newline at end of file +#endif // !DATACUTE_EXCLUDE_TABINDENTINGLINEAPPENDER + diff --git a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/TypeContext.cs b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/TypeContext.cs index 8ea3264..73b0447 100644 --- a/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/TypeContext.cs +++ b/IncrementalGeneratorExtensions.Content/Datacute/IncrementalGeneratorExtensions/TypeContext.cs @@ -1,9 +1,7 @@ -// -// This file is part of the Datacute.IncrementalGeneratorExtensions package. -// It is included as a source file and should not be modified. -// - -#if !DATACUTE_EXCLUDE_TYPECONTEXT && !DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#if !DATACUTE_EXCLUDE_TYPECONTEXT // Feature: TypeContext +#if DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY +#error TypeContext requires EquatableImmutableArray (remove DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAY or also exclude DATACUTE_EXCLUDE_TYPECONTEXT) +#endif using System; using Microsoft.CodeAnalysis; @@ -298,4 +296,5 @@ public static string GetNameWithTypeParametersForHint(string name, EquatableImmu } } } -#endif \ No newline at end of file +#endif // !DATACUTE_EXCLUDE_TYPECONTEXT + diff --git a/IncrementalGeneratorExtensions.Content/IncrementalGeneratorExtensions.Content.csproj b/IncrementalGeneratorExtensions.Content/IncrementalGeneratorExtensions.Content.csproj index bca5cc2..e9f3fe1 100644 --- a/IncrementalGeneratorExtensions.Content/IncrementalGeneratorExtensions.Content.csproj +++ b/IncrementalGeneratorExtensions.Content/IncrementalGeneratorExtensions.Content.csproj @@ -6,6 +6,7 @@ netstandard2.0 7.3 false + diff --git a/IncrementalGeneratorExtensions.sln b/IncrementalGeneratorExtensions.sln index e887383..1ef52d7 100644 --- a/IncrementalGeneratorExtensions.sln +++ b/IncrementalGeneratorExtensions.sln @@ -10,6 +10,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution versionsuffix.ci.props = versionsuffix.ci.props versionsuffix.release.props = versionsuffix.release.props PACKAGE_README.md = PACKAGE_README.md + .github\copilot-instructions.md = .github\copilot-instructions.md + global.json = global.json + .junie\guidelines.md = .junie\guidelines.md EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IncrementalGeneratorExtensions", "IncrementalGeneratorExtensions\IncrementalGeneratorExtensions.csproj", "{2DDE40FD-08D6-469C-948A-2AED9E0B2B49}" diff --git a/IncrementalGeneratorExtensions/Generator.cs b/IncrementalGeneratorExtensions/Generator.cs index c3639cc..16f5a73 100644 --- a/IncrementalGeneratorExtensions/Generator.cs +++ b/IncrementalGeneratorExtensions/Generator.cs @@ -7,8 +7,14 @@ namespace Datacute.IncrementalGeneratorExtensions { [Generator] + /// + /// Incremental generator that injects embedded helper source files into the consuming compilation. + /// public class Generator : IIncrementalGenerator { + /// + /// Registers the post-initialisation callback that loads embedded .cs resources and adds them via . + /// public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(ProcessEmbeddedResources); @@ -18,6 +24,17 @@ private void ProcessEmbeddedResources(IncrementalGeneratorPostInitializationCont { // Get the assembly where this generator is defined var assembly = typeof(Generator).Assembly; + var assemblyName = assembly.GetName(); + var generatorName = assemblyName?.Name ?? "a source code generator"; + var version = assemblyName?.Version != null ? assemblyName.Version.ToString(fieldCount: 3) : "unknown"; + + var header = + $"//------------------------------------------------------------------------------{Environment.NewLine}" + + $"// {Environment.NewLine}" + + $"// This code was generated by {generatorName}.{Environment.NewLine}" + + $"// Version: {version}{Environment.NewLine}" + + $"// {Environment.NewLine}" + + $"//------------------------------------------------------------------------------{Environment.NewLine}{Environment.NewLine}"; // Retrieve all embedded resource names var resourceNames = assembly.GetManifestResourceNames(); @@ -39,8 +56,18 @@ private void ProcessEmbeddedResources(IncrementalGeneratorPostInitializationCont using (var reader = new StreamReader(stream)) { - var sourceCode = reader.ReadToEnd(); - var sourceText = SourceText.From(sourceCode, Encoding.UTF8); + // Rebuild text with platform newline to avoid mixed line endings + var sb = new StringBuilder(header.Length + (int)stream.Length + 32); + sb.Append(header); + + string line; + while ((line = reader.ReadLine()) != null) + { + sb.Append(line); + sb.Append(Environment.NewLine); + } + + var sourceText = SourceText.From(sb.ToString(), Encoding.UTF8); // Add the source to the compilation context.AddSource(fileName, sourceText); diff --git a/IncrementalGeneratorExtensions/content/Datacute.IncrementalGeneratorExtensions.README.md b/IncrementalGeneratorExtensions/content/Datacute.IncrementalGeneratorExtensions.README.md index 07c84e8..e182b19 100644 --- a/IncrementalGeneratorExtensions/content/Datacute.IncrementalGeneratorExtensions.README.md +++ b/IncrementalGeneratorExtensions/content/Datacute.IncrementalGeneratorExtensions.README.md @@ -73,6 +73,10 @@ making it easier to debug and understand the flow of your generator. _buffer.AppendDiagnosticsComment(GeneratorStageDescriptions.GeneratorStageNameMap); ``` +Composite key encoding (overview): +- ID and optional value are packed into a single int using a stride and a flag for value-name mapping. +- Use CompositeValueShift and MapValueFlag to understand the packing; EncodeKey/DecodeKey helpers are available. + # Additional Resources diff --git a/PACKAGE_README.md b/PACKAGE_README.md index 98e1720..74b722c 100644 --- a/PACKAGE_README.md +++ b/PACKAGE_README.md @@ -1,43 +1,70 @@ -Provides extension methods and helper classes designed to -simplify the development of .NET Incremental Source Generators. +Fast-start helper set for building .NET incremental source generators: drop-in source files (added to your compilation) that cover attribute data collection, value-equality wrappers, structured emission, indentation, and lightweight tracing. -It adds a directory of source files directly to your project, -included in your build, making it easier to package your -incremental source code generator. +## Features +* SourceTextGenerator base class +* EquatableImmutableArray +* Attribute Context and Data (with TypeContext) +* IndentingLineAppender (and tab variant) +* LightweightTrace & GeneratorStage enum -## Features Included +More details, examples and exclusion symbols: https://github.com/datacute/IncrementalGeneratorExtensions -**SourceTextGenerator Base Class** +## Quick Example (minimal, trimmed) +```csharp +[Generator] +public sealed class DemoGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var usages = context.SelectAttributeContexts( + "MyNamespace.GenerateSomethingAttribute", + GenerateSomethingData.Collect); -- Provides a base class for incremental source generators that handles the boilerplate - of generating a partial class (or similar) file for an instance of a marker attribute. + context.RegisterSourceOutput(usages, static (spc, usage) => + { + var gen = new GenerateSomethingSource(in usage, in spc.CancellationToken); + spc.AddSource(usage.CreateHintName("GenerateSomething"), gen.GetSourceText()); + }); + } +} +``` -**EquatableImmutableArray**: +### With a simple attribute: +```csharp +[AttributeUsage(AttributeTargets.Class)] +public sealed class GenerateSomethingAttribute : Attribute +{ + public GenerateSomethingAttribute(string name) => Name = name; + public string Name { get; } +} +``` -- Provides an `EquatableImmutableArray` type which enables value-based - equality comparison of array contents, rather than the reference equality - of the array instance itself, which is what `ImmutableArray` uses. -- Incremental source generators produce new `ImmutableArray` outputs within their - pipelines, and by converting these to `EquatableImmutableArray` instances, - the pipeline stages can be correctly identified as having no changes in their - output. +### Collecting attribute constructor arguments: +```csharp +sealed class GenerateSomethingData : IEquatable +{ + public GenerateSomethingData(string name) => Name = name; + public string Name { get; } -**Attribute Context and Data** + // Equals and GetHashCode not shown to keep the example brief -- Adds types and extension methods to simplify collecting data about each use of a marker attribute. -- `TypeContext` captures the type information. -- `AttributeContextAndData` captures the attribute data, which includes the `TypeContext` of the type marked by - the attribute, and the `TypeContext` of each of the containing types. -- `AttributeContextAndData` has a generic type argument which is your type that holds - information collected for the attribute, such as its positional and named arguments. + public static GenerateSomethingData Collect(GeneratorAttributeSyntaxContext c) + => new GenerateSomethingData((string)c.Attributes[0].ConstructorArguments[0].Value); +} +``` -**Indented StringBuilder**: -- Provides a customisable `IndentingLineAppender` class that wraps a `StringBuilder` and adds - auto-indentation support, making it easier to generate indented source code. +### Using the simplest source generator: +```csharp +sealed class GenerateSomethingSource : SourceTextGeneratorBase +{ + readonly GenerateSomethingData _data; -**Lightweight Tracing**: + public GenerateSomethingSource( + in AttributeContextAndData usage, + in CancellationToken token) + : base(in usage, in token) => _data = usage.AttributeData; -- A simple tracing mechanism that integrates with the incremental source generator's - `WithTrackingName` API, making it easier to diagnose and debug your generator's execution. -- Usage counters and timing logs can be included as a comment in the generated source. -- Provides an enum `GeneratorStage` with descriptions for common stages of the generator pipeline. + protected override void AppendCustomMembers() + => Buffer.AppendLine($"public static string GeneratedName => \"{_data.Name}\";"); +} +``` diff --git a/README.md b/README.md index 5348df2..9ce750b 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ [![Build](https://github.com/datacute/IncrementalGeneratorExtensions/actions/workflows/ci.yml/badge.svg)](https://github.com/datacute/IncrementalGeneratorExtensions/actions/workflows/ci.yml) -Source for the `Datacute.IncrementalGeneratorExtensions` NuGet package, -which adds source files that provide additional functionality -for incremental source generators in .NET. +> Source for the `Datacute.IncrementalGeneratorExtensions` NuGet package, + which adds source files that provide additional functionality + for incremental source generators in .NET. --- -Explanation of the above... +Breaking down the tagline... ## The NuGet package @@ -61,6 +61,66 @@ to help with the development of incremental source generators in .NET. - Provides an enum `GeneratorStage` with descriptions for each stage of the generator, which can be used to track the execution flow in the Lightweight Tracing methods. +## Minimal Example + +### Generator wiring +```csharp +[Generator] +public sealed class DemoGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var usages = context.SelectAttributeContexts( + "MyNamespace.GenerateSomethingAttribute", + GenerateSomethingData.Collect); + + context.RegisterSourceOutput(usages, static (spc, usage) => + { + var gen = new GenerateSomethingSource(in usage, in spc.CancellationToken); + spc.AddSource(usage.CreateHintName("GenerateSomething"), gen.GetSourceText()); + }); + } +} +``` + +### Simple marker attribute +```csharp +[AttributeUsage(AttributeTargets.Class)] +public sealed class GenerateSomethingAttribute : Attribute +{ + public GenerateSomethingAttribute(string name) => Name = name; + public string Name { get; } +} +``` + +### Collect projected payload +```csharp +sealed class GenerateSomethingData : IEquatable +{ + public GenerateSomethingData(string name) => Name = name; + public string Name { get; } + // Equals / GetHashCode omitted for brevity + public static GenerateSomethingData Collect(GeneratorAttributeSyntaxContext c) + => new GenerateSomethingData((string)c.Attributes[0].ConstructorArguments[0].Value); +} +``` + +### Emit using the base class +```csharp +sealed class GenerateSomethingSource : SourceTextGeneratorBase +{ + readonly GenerateSomethingData _data; + + public GenerateSomethingSource( + in AttributeContextAndData usage, + in CancellationToken token) + : base(in usage, in token) => _data = usage.AttributeData; + + protected override void AppendCustomMembers() + => Buffer.AppendLine($"public static string GeneratedName => \"{_data.Name}\";"); +} +``` + ## Customizing the experience ### Hiding the README file @@ -101,6 +161,24 @@ define the relevant constant in the consuming project's `.csproj` file: The file will still be generated, but it will not add anything to the compilation. +## Document Roles +Each README targets a specific audience: + +* `README.md` (this doc) + * Audience: GitHub viewers, contributors and evaluators. + * Purpose: Explain project scope, features, architecture links, build & contribution basics. +* `PACKAGE_README.md` + * Audience: Potential NuGet consumers browsing nuget.org packages. + * Purpose: Quick value summary, barest minimal example. +* `content/Datacute.IncrementalGeneratorExtensions.README.md` + * Audience: Existing users inside their IDE after install. + * Purpose: Fast in‑project reference: feature list, exclusion flags, how to hide file, doc links. +* `docs` directory + * Audience: Anyone needing more detail than the brief READMEs. + * Purpose: Online extended docs for each helper. + +Guideline: Depth lives in `docs/`; keep package README short; keep content README scannable. + # Thanks - Andrew Lock | .NET Escapades diff --git a/docs/AttributeContextAndData README.md b/docs/AttributeContextAndData README.md index 5adb9e6..0eb4e29 100644 --- a/docs/AttributeContextAndData README.md +++ b/docs/AttributeContextAndData README.md @@ -2,18 +2,92 @@ --- # AttributeContextAndData.cs, TypeContext.cs and AttributeContextAndDataExtensions.cs -***todo - expand on copied points below*** +Collecting rich, equality-stable information about each use of a marker attribute is a recurring pattern in incremental generators. These helpers standardise that pattern: +* `TypeContext` captures a type symbol plus its containing type chain (outermost → innermost) with stable value equality. +* `AttributeContextAndData` couples Roslyn attribute discovery with a strongly-typed payload you project once (constructor / named arguments, derived metadata, precomputed lookups, etc.). +* Extension methods wrap the low-level `ForAttributeWithMetadataName` plumbing so you focus on projecting meaningful data rather than repeatedly walking symbols. -- Adds types and extension methods to simplify collecting data about each use of a marker attribute. -- `TypeContext` captures the type information. -- `AttributeContextAndData` captures the attribute data, which includes the `TypeContext` of the type marked by - the attribute, and the `TypeContext` of each of the containing types. -- `AttributeContextAndData` has a generic type argument which is your type that holds - information collected for the attribute, such as its positional and named arguments. +### What You Get In `AttributeContextAndData` +* `AttributeData` (`TData`) – the payload: a minimal immutable projection of one attribute usage (raw Roslyn `AttributeData` is discarded after decoding). +* `Context` – the `TypeContext` for the directly attributed type. +* `ContainingTypes` – value‑equatable ordered chain of enclosing `TypeContext`s (outermost first). +* `CreateHintName(string generatorName)` – builds a stable, file‑safe hint including namespace, containing types and generic arity/type parameters. +* Convenience flags: `ContainingNamespaceIsGlobalNamespace`, `ContainingNamespaceDisplayString`, `HasContainingTypes`, `IsInFileScopedNamespace`, `IsNullableContextEnabled`. + +### Why It Matters +* Stable value equality (type + containing chain + payload) short‑circuits downstream pipeline work. +* Single pass attribute argument decoding; you persist only the payload you need. # Example Usage +```csharp +// 1. Marker attribute (in the consuming project) +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public sealed class GenerateSomethingAttribute : Attribute +{ + public GenerateSomethingAttribute(string name, int version = 1) + { + Name = name; Version = version; + } + public string Name { get; } + public int Version { get; } +} + +// 2. Strongly-typed payload for one attribute usage +public sealed class GenerateSomethingData : IEquatable +{ + public GenerateSomethingData(string name, int version) + { + Name = name; + Version = version; + } + + public string Name { get; } + public int Version { get; } + public bool Equals(GenerateSomethingData other) => other != null && Name == other.Name && Version == other.Version; + public override bool Equals(object o) => Equals(o as GenerateSomethingData); + public override int GetHashCode() => (Name ?? "").GetHashCode() ^ Version; + + // Factory used by SelectAttributeContexts (keeps generator focused on the pipeline) + public static GenerateSomethingData Collect(Microsoft.CodeAnalysis.GeneratorAttributeSyntaxContext syntaxCtx) + { + var ad = syntaxCtx.Attributes[0]; + var ctorArgs = ad.ConstructorArguments; + var name = (string)ctorArgs[0].Value; + var version = ctorArgs.Length > 1 ? (int)ctorArgs[1].Value : 1; + return new GenerateSomethingData(name, version); + } +} + +// 3. In your IncrementalGenerator.Initialize +const string AttributeMetadataName = "MyNamespace.GenerateSomethingAttribute"; // fully qualified metadata name + +// Helper selects + transforms attribute usages into AttributeContextAndData +var attributeUsages = context.SelectAttributeContexts( + AttributeMetadataName, + GenerateSomethingData.Collect); -_**todo**_ +// 4. Emit code (IndentingLineAppender for tidy formatting) – per usage +context.RegisterSourceOutput(attributeUsages, static (spc, usage) => +{ + var ctx = usage.Context; // TypeContext of the attributed type + var data = usage.AttributeData; // Your projected payload (GenerateSomethingData) + var hintName = usage.CreateHintName("GenerateSomething"); // Namespace + containing types + generics safe + + var ap = new IndentingLineAppender(); + ap.AppendLine("// ") + .AppendLine("// demo") + .AppendLine("// ") + .AppendLine(); + + // Minimal illustration: emit into the existing partial type (ignores namespace / containing types for brevity) + ap.AppendLine(ctx.TypeDeclaration()) + .AppendStartBlock() + .AppendLine($"public const int GeneratedVersion = {data.Version};") + .AppendEndBlock(); + + spc.AddSource(hintName, ap.ToString()); +}); +``` # Excluding the source files @@ -30,8 +104,17 @@ define the relevant constant in the consuming project's `.csproj` file: The files will still appear in the project, but will not add anything to the compilation. -### Note: Dependency -`AttributeContextAndDataExtensions.cs` depends on `AttributeContextAndData.cs` which in turn depends on `TypeContext.cs` +### Note: Dependencies +Direct: +* `AttributeContextAndDataExtensions.cs` -> `AttributeContextAndData.cs` +* `AttributeContextAndData.cs` -> `TypeContext.cs` (uses the type) and `EquatableImmutableArray.cs` (fields of `EquatableImmutableArray`) + +Transitive (implicit, not re‑stated as guards in the extensions file): +* `AttributeContextAndDataExtensions.cs` -> `TypeContext.cs`, `EquatableImmutableArray.cs` (only via `AttributeContextAndData.cs`) + +Behavior: +* Core files (`AttributeContextAndData.cs`, `TypeContext.cs`) use fail‑fast `#error` directives if a required direct dependency is excluded. +* The extensions file only guards its direct dependency (the core `AttributeContextAndData`); if you exclude a transitive dependency the core file will produce the error and compilation will halt. --- diff --git a/docs/EquatableImmutableArray README.md b/docs/EquatableImmutableArray README.md index e6e83a5..3493654 100644 --- a/docs/EquatableImmutableArray README.md +++ b/docs/EquatableImmutableArray README.md @@ -2,19 +2,93 @@ --- # EquatableImmutableArray.cs and EquatableImmutableArrayExtensions.cs -***todo - expand on copied points below*** -- Provides an `EquatableImmutableArray` type which enable value-based - equality comparison of array contents, rather than the reference equality - of the array instance itself, which is what `ImmutableArray` uses. -- Incremental source generators produce `ImmutableArray` outputs within their - pipelines, and by converting these to `EquatableImmutableArray` instances, - the pipeline stages can be correctly identified as having no changes in their - output. +Efficient value-based collection equality for incremental generator pipelines. + +`ImmutableArray` compares by reference; two arrays with identical elements are +considered different. In a Roslyn incremental pipeline that causes unnecessary downstream +re-execution. `EquatableImmutableArray` wraps an `ImmutableArray` and implements +`IEquatable>` by structural comparison (fast‑path hash + instance cache). + +Benefits in generator graphs: +* Stable equality: identical logical sequences short‑circuit change detection, trimming downstream recomputation (CPU) and reallocation (GC). +* Small wrapper: only original array reference + cached hash + length; negligible overhead versus element storage. +* Optional instance cache: reuses wrappers for identical sequences so repeated projections or orderings avoid re-hashing and element scans. +* Tracing hooks (with LightweightTrace + GeneratorStage): surface cache hits/misses and typical array length distribution to guide optimisation. + +Extensions (`EquatableImmutableArrayExtensions`) integrate easily with Roslyn providers: `CollectEquatable`, `CombineEquatable`, `ToEquatableImmutableArray`. # Example Usage -_**todo**_ See the doc-comments in the source files for example usage. +### 1. Converting an existing ImmutableArray after building it +```csharp +var builder = ImmutableArray.CreateBuilder(count); +// ... populate builder ... +ParentInfos = builder.MoveToImmutable().ToEquatableImmutableArray(); +``` + +### 2. Projecting while converting (ImmutableArray -> EquatableImmutableArray) +```csharp +EquatableImmutableArray names = symbols.ToEquatableImmutableArray(s => s.Name); +``` + +### 3. Chaining inside an incremental pipeline +```csharp +var combined = leftProvider + .CombineEquatable(rightProvider) // right collected + converted + .Select(static tuple => Process(tuple.Left, tuple.Right)); +``` + +### 4. Collecting values and keeping value equality +```csharp +IncrementalValueProvider> allItems = sourceValues.CollectEquatable(); +``` + +### 5. Disabling the cache (compile constant) for debugging / deterministic profiling +```xml +$(DefineConstants);DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYINSTANCECACHE +``` +Now each `Create` call allocates a fresh wrapper; equality still works (hash computed once per wrapper) but no cache hit/miss counters are recorded. + +--- +### API Highlights +Factory: +* `EquatableImmutableArray.Empty` – canonical empty instance (never allocate another empty wrapper). +* `EquatableImmutableArray.Create(ImmutableArray, CancellationToken)` – convert + (optionally) cache; central entry point used by all extension helpers. + +Extensions (selected): +* `ImmutableArray.ToEquatableImmutableArray()` / projecting overload – wrap in value-equality immediately (optionally transform elements). +* `IEnumerable.ToEquatableImmutableArray()` – materialise + wrap in one call. +* `IncrementalValuesProvider.CollectEquatable()` – `Collect()` then wrap so downstream stages see stable equality. +* `IncrementalValuesProvider.CombineEquatable(IncrementalValuesProvider)` – pairs each left with an equatable collected right set. +* `IncrementalValueProvider.CombineEquatable(IncrementalValuesProvider)` – value + collected equatable values for fan-out scenarios. + +Equality Implementation Outline: +1. Instance cache (length -> first element hash -> weak list) narrows candidates. +2. If candidate hash matches cached hash, element compare only if needed. +3. Hash stored; subsequent comparisons often short‑circuit on hash & reference. + +When the cache is excluded the structural hash is computed once per wrapper; equality still uses hash then element compare. + +### Choosing When To Convert +Convert to `EquatableImmutableArray` every time an `ImmutableArray` value emerges in your pipeline or local code that could participate in equality-based change detection. Uniform conversion keeps semantics simple (arrays are always value-equal) and avoids accidental missed optimisation points. The wrapper + hash cost is tiny relative to typical generator work; consistency wins. + +### Performance Notes +* Hash combine uses cheap rotate‑and‑xor (similar to .NET HashHelpers) for low per-element cost. +* Cache reduces wrapper allocations & equality cost when identical sequences recur (common for symbol/type parameter lists). +* Weak references allow reclaimed wrappers; stale slots removed opportunistically during candidate scans. +* Optional cancellation checks (with tracing) keep long conversions responsive to rapid edit iterations. + +### Tracing (Optional) +With LightweightTrace + GeneratorStage included you may see counters: +* `EquatableImmutableArrayCacheHit` – structural reuse (fast path success). +* `EquatableImmutableArrayCacheMiss` – new distinct sequence added to cache. +* `EquatableImmutableArrayCacheWeakReferenceRemoved` – stale wrapper slot cleaned. +* `EquatableImmutableArrayLength` – length histogram to spot pathological sizes. +Disable by excluding `LightweightTrace` or `GeneratorStage`, or by disabling the cache (miss counters suppressed in non‑cache mode). + +### Memory Considerations +Only additional data beyond the wrapped `ImmutableArray` is a cached hash (`int`). # Excluding the source files @@ -30,9 +104,15 @@ define the relevant constant in the consuming project's `.csproj` file: The files will still appear in the project, but will not add anything to the compilation. -### Note: Dependency -`EquatableImmutableArrayExtensions.cs` depends on `EquatableImmutableArray.cs` and will **not work** when it -is excluded. (Unless you supply your own implementation of `EquatableImmutableArray`.) +### Note: Dependencies +Direct: +* `EquatableImmutableArrayExtensions.cs` -> `EquatableImmutableArray.cs` + +Internal Implementation Detail (optional performance): +* `EquatableImmutableArrayInstanceCache.cs` (used by factory; excluding it forfeits caching but core correctness remains). Define `DATACUTE_EXCLUDE_EQUATABLEIMMUTABLEARRAYINSTANCECACHE` to bypass caching; each Create() call allocates a fresh wrapper and no cache hit/miss counters are recorded. + +Behavior: +* Extensions file only guards the core type; no transitive dependencies. --- diff --git a/docs/GeneratorStage README.md b/docs/GeneratorStage README.md index de2a2fb..17f49a8 100644 --- a/docs/GeneratorStage README.md +++ b/docs/GeneratorStage README.md @@ -2,16 +2,45 @@ --- # GeneratorStage.cs and GeneratorStageDescriptions.cs -***todo - expand on copied points below*** +Purpose: provide the canonical event id set and readable name map that plug directly into `LightweightTrace` so you can capture, aggregate and render meaningful incremental generator instrumentation without inventing your own numbering scheme. You increment counts or add events with `GeneratorStage` values and later resolve names via `GeneratorStageDescriptions.GeneratorStageNameMap` (optionally merged with your custom ids ≥ 100). - Provides an enum `GeneratorStage` for each common stage of the generator pipeline, - for use with Lightweight Tracing to track the execution flow. + for use with `LightweightTrace` to track the execution flow. - The `GeneratorStageDescriptions` provides a mapping of the enum values to their descriptions, in English, which can be used when generating diagnostic logs and counters. # Example Usage +```csharp +// Merge built-in names (optionally append your own >= 100) +static readonly Dictionary EventNameMap = new(GeneratorStageDescriptions.GeneratorStageNameMap) +{ + { 100, "My Custom Aggregation" } +}; -_**todo**_ +// Track pipeline stage throughput and events: +var texts = context.AdditionalTextsProvider + .Select(SelectFileInfo) + .WithTrackingName(GeneratorStage.AdditionalTextsProviderSelect); +var options = context.AnalyzerConfigOptionsProvider + .Select(GeneratorOptions.Select) + .WithTrackingName(GeneratorStage.AnalyzerConfigOptionsProviderSelect); + +LightweightTrace.Add(GeneratorStage.SourceProductionContextAddSource); +context.AddSource(hintName, source); + +// Later: emit counts with readable names +buffer.AppendDiagnosticsComment(MyCustomExtendedDescriptions.EventNameMap); +``` + +### Enum Value Guidance +Numbers less than 100 are allocated to built-in lifecycle, provider projection, optional EquatableImmutableArray cache metrics, and generic method entry/exit / call markers. +Create your own enum using values between 100 and 1023 (to fit `LightweightTrace` expectations) and merge its names into a dictionary copied from `GeneratorStageDescriptions.GeneratorStageNameMap`. + +### Recommended Use +Instrument only while diagnosing hotspots (cache misses, excessive provider selects) – remove or exclude for zero cost later. + +### Performance +Each counter increment is a single array index increment inside `LightweightTrace`; overhead is negligible relative to typical Roslyn analysis. Excluding `GeneratorStage` (define `DATACUTE_EXCLUDE_GENERATORSTAGE`) compiles out all references in generated code that are guarded. # Excluding the source files @@ -28,8 +57,8 @@ define the relevant constant in the consuming project's `.csproj` file: The files will still appear in the project, but will not add anything to the compilation. ### Note: Dependency -`GeneratorStageDescriptions.cs` depends on `GeneratorStage.cs` and will **not work** when it -is excluded. +Direct: +* `GeneratorStageDescriptions.cs` -> `GeneratorStage.cs` --- diff --git a/docs/IndentingLineAppender README.md b/docs/IndentingLineAppender README.md index 01ab9ca..19b1574 100644 --- a/docs/IndentingLineAppender README.md +++ b/docs/IndentingLineAppender README.md @@ -2,16 +2,53 @@ --- # IndentingLineAppender.cs and TabIndentingLineAppender.cs -***todo - expand on copied points below*** +`IndentingLineAppender` is a focused helper for generating structured / indented source without littering your code with manual space concatenations. It wraps one `StringBuilder`, maintains an `IndentLevel`, and offers: +* Block helpers: `AppendStartBlock()` / `AppendEndBlock()` (writes delimiters and adjusts indent). +* Multi-line helpers: `AppendLines()` and `AppendFormatLines()` (each line indented, blank lines preserved). +* Fluent chaining so familiar `StringBuilder` patterns are usable. +* The `TabIndentingLineAppender` subclass is identical except it uses a single tab character per indentation level. -- Provides a customisable `IndentingLineAppender` class that wraps a `StringBuilder` and adds - auto-indentation support, making it easier to generate indented source code. -- Includes a `TabIndentingLineAppender` customisation that uses tabs for indentation. +## Example Usage +```csharp +var a = new IndentingLineAppender(); +a.AppendLine("namespace Demo") + .AppendStartBlock() // { + .AppendLine("internal static class C") + .AppendStartBlock() // { + .AppendLine("public static void M() {}"); +a.AppendEndBlock() // } + .AppendEndBlock(); // } -# Example Usage +string code = a.ToString(); -_**todo**_ +// Using tabs (identical API, different indentation style): +var tabs = new TabIndentingLineAppender(); +tabs.AppendLine("class T") + .AppendStartBlock() + .AppendLine("int x;") + .AppendEndBlock(); +``` + +Result (`code` variable) roughly: +```text +namespace Demo +{ + internal static class C + { + public static void M() {} + } +} +``` + +## Key APIs +* `AppendLine(string)` – append one line with current indent. +* `AppendStartBlock()` / `AppendEndBlock()` – write block delimiters and adjust `IndentLevel` automatically. +* `AppendLines(string)` – append multi-line content (each non-empty line prefixed with current indent). +* `AppendFormatLines(format, params object[])` – format then treat the result as multi-line. +* `IndentLevel` (get/set) – manual control (normally let block helpers change it). +* `Clear()` – reuse the same instance for multiple emission passes. +* `Direct` – access the underlying `StringBuilder` for an uncommon operation. # Excluding the source files @@ -28,8 +65,11 @@ define the relevant constant in the consuming project's `.csproj` file: The files will still appear in the project, but will not add anything to the compilation. ### Note: Dependency -`TabIndentingLineAppender.cs` depends on `IndentingLineAppender.cs` and will **not work** when it -is excluded. +Direct: +* `TabIndentingLineAppender.cs` -> `IndentingLineAppender.cs` + +Behavior: +* `TabIndentingLineAppender.cs` emits a fail‑fast `#error` if the base is excluded. --- diff --git a/docs/LightweightTrace README.md b/docs/LightweightTrace README.md index 81cbf7f..c6eb555 100644 --- a/docs/LightweightTrace README.md +++ b/docs/LightweightTrace README.md @@ -2,20 +2,174 @@ --- # LightweightTrace.cs and LightweightTraceExtensions.cs -***todo - expand on copied points below*** +LightweightTrace is a zero‑allocation instrumentation layer for incremental source generators: a timestamped ring buffer of events plus composite-key counters (id + optional value + mapping flag) that represent histograms, categorical buckets, and method-call frequencies. +It can emit a single embedded diagnostics comment (counters + trace) into generated source so you can understand pipeline behavior (frequency, timing patterns, entry/exit flow) without external tooling. LightweightTraceExtensions wires this into IncrementalValue/Values providers and adds cancellation logging helpers. +## Sample Diagnostics Output +```text +/* Diagnostics +Counters: +[011] ForAttributeWithMetadataName Predicate: 6 +[012] ForAttributeWithMetadataName Transform: 6 +[018] EquatableImmutableArray Cache Hit: 3 +[019] EquatableImmutableArray Cache Miss: 5 +[021] EquatableImmutableArray Length (1): 1 +[021] EquatableImmutableArray Length (2): 4 +[021] EquatableImmutableArray Length (6): 2 +[021] EquatableImmutableArray Length (8): 1 +[050] Method Call (Generator Initialize): 2 +[050] Method Call (Register Post Initialization Output): 2 +[050] Method Call (Register Source Output): 6 +[050] Method Call (Source Production Context Add Source): 5 +[050] Method Call (ForAttributeWithMetadataName Pipeline Output): 6 +[050] Method Call (AdditionalTextsProvider Select): 2 +[050] Method Call (AnalyzerConfigOptionsProvider Select): 2 +[050] Method Call (Combined Attributes and Options): 12 +[050] Method Call (Selected Attribute Glob Info (Path/Ext)): 12 +[050] Method Call (Selected Attribute Globs (Path/Ext)): 12 +[050] Method Call (Combined File Info and Resource Globs): 2 +[050] Method Call (Filtered Files Matching Globs): 2 +[050] Method Call (Generating Doc Comment): 2 +[050] Method Call (Extracted EmbeddedResource (with File/Glob info)): 2 +[050] Method Call (Combined Resource/File Data and All Attribute Glob Info): 2 +[050] Method Call (Selected Matching (AttributeContext, EmbeddedResource)): 8 +[050] Method Call (Grouped Resources by AttributeContext into Lookup): 1 +[050] Method Call (Prepared Final Generation Input (AttrContext, Resources, Options)): 6 -- Provides a lightweight tracing mechanism and provides an easy way to integrate - with the incremental source generator's `WithTrackingName` diagnostic mechanism. -- Usage counters and timing logs can be included as a comment in the generated source. -- Provides an enum `GeneratorStage` with descriptions for each stage of the generator, - which can be used to track the execution flow. -- The `GeneratorStageDescriptions` provides a mapping of the enum values to their descriptions, - in English, which can be used when generating diagnostic logs and counters. +Trace Log: +2025-07-26T05:01:43.6826743Z [000] Generator Initialize (Method Entry) +2025-07-26T05:01:43.7094197Z [000] Generator Initialize (Method Exit) +2025-07-26T05:01:43.7116005Z [002] Register Post Initialization Output (Method Entry) +2025-07-26T05:01:43.7168645Z [002] Register Post Initialization Output (Method Exit) +2025-07-26T05:01:44.3301246Z [010] ForAttributeWithMetadataName Pipeline Output +2025-07-26T05:01:44.3301294Z [010] ForAttributeWithMetadataName Pipeline Output +2025-07-26T05:01:44.3308954Z [130] Combined Attributes and Options (Method Entry) +2025-07-26T05:01:44.3313608Z [130] Combined Attributes and Options (Method Entry) +2025-07-26T05:01:44.3537540Z [130] Combined Attributes and Options (Method Exit) +2025-07-26T05:01:44.3539443Z [130] Combined Attributes and Options (Method Exit) +2025-07-26T05:01:44.3362945Z [016] AnalyzerConfigOptionsProvider Select (Method Entry) +2025-07-26T05:01:44.3420436Z [016] AnalyzerConfigOptionsProvider Select (Method Exit) +2025-07-26T05:01:44.4064968Z [141] Selected Attribute Glob Info (Path/Ext) (Method Entry) +2025-07-26T05:01:44.4065008Z [141] Selected Attribute Glob Info (Path/Ext) (Method Entry) +2025-07-26T05:01:44.4123922Z [141] Selected Attribute Glob Info (Path/Ext) (Method Exit) +2025-07-26T05:01:44.4125724Z [141] Selected Attribute Glob Info (Path/Ext) (Method Exit) +2025-07-26T05:01:44.4129990Z [142] Selected Attribute Globs (Path/Ext) (Method Entry) +2025-07-26T05:01:44.4130013Z [142] Selected Attribute Globs (Path/Ext) (Method Entry) +2025-07-26T05:01:44.4181419Z [142] Selected Attribute Globs (Path/Ext) (Method Exit) +2025-07-26T05:01:44.4183188Z [142] Selected Attribute Globs (Path/Ext) (Method Exit) +2025-07-26T05:01:44.4376394Z [143] Combined File Info and Resource Globs +2025-07-26T05:01:44.4459070Z [144] Filtered Files Matching Globs +2025-07-26T05:01:44.4469755Z [145] Generating Doc Comment (99) +2025-07-26T05:01:44.4494728Z [145] Generating Doc Comment (100) +2025-07-26T05:01:44.4530661Z [146] Extracted EmbeddedResource (with File/Glob info) +2025-07-26T05:01:44.4773410Z [147] Combined Resource/File Data and All Attribute Glob Info +2025-07-26T05:01:44.4886634Z [148] Selected Matching (AttributeContext, EmbeddedResource) +2025-07-26T05:01:44.4886679Z [148] Selected Matching (AttributeContext, EmbeddedResource) +2025-07-26T05:01:44.5108575Z [150] Grouped Resources by AttributeContext into Lookup +2025-07-26T05:01:44.5237805Z [160] Prepared Final Generation Input (AttrContext, Resources, Options) +2025-07-26T05:01:44.5247888Z [003] Register Source Output +2025-07-26T05:01:44.5417853Z [007] Source Production Context Add Source +2025-07-26T05:01:44.5418159Z [003] Register Source Output +2025-07-26T05:01:44.5419170Z [007] Source Production Context Add Source +*/ +``` +## Example Calls Producing Parts Of That Output +```csharp +// ForAttributeWithMetadataName Predicate / Transform (counters & pipeline outputs) +LightweightTrace.IncrementCount(GeneratorStage.ForAttributeWithMetadataNamePredicate); +LightweightTrace.Add(GeneratorStage.ForAttributeWithMetadataNamePipelineOutput); + +// EquatableImmutableArray Length histogram bucket +LightweightTrace.IncrementCount(GeneratorStage.EquatableImmutableArrayLength, values.Length); + +// Method call mapping (shows up as Method Call (...)) +LightweightTrace.Add(GeneratorStage.RegisterSourceOutput); +LightweightTrace.IncrementCount(GeneratorStage.MethodCall, (int)GeneratorStage.RegisterSourceOutput, mapValue: true); + +// Method Entry / Exit wrapping a stage +LightweightTrace.MethodEntry(GeneratorStage.Initialize); +LightweightTrace.MethodExit(GeneratorStage.Initialize); +``` +## Capabilities +- Single-int composite key packs id + optional value (+ flag) to avoid allocations. +- Counters (simple, histogram buckets, mapped enum categories). +- Time-stamped rolling trace (ring buffer) with method entry/exit tagging. +- Automatic method call counting (except MethodExit) for frequency insight. +- Name mapping for ids and (optionally flagged) values via built-in GeneratorStage / GeneratorStageDescriptions plus your own merged enum map. +- AppendDiagnosticsComment combines counters + trace into one embeddable block. +- Cancellation helpers record where cancellation was observed. +## Encoding (Brief) +Composite key: composite = id + (value * CompositeValueShift) + (mapped ? MapValueFlag : 0). Decode splits into id, value, mapped flag. This keeps storage dense and lookups cheap. + +## Extending GeneratorStage With Your Own Events +You get a built-in baseline enum (GeneratorStage) and a name map (GeneratorStageDescriptions.GeneratorStageNameMap). Extend by defining your own enum with distinct numeric values (gaps are fine) and merge its names into a lazy dictionary so both built-in and custom events share one lookup. + +Example enum (abbreviated, made-up stages): +```csharp +public enum MyPipelineStage +{ + FooParsed = 200, + FooAndBarCombined = 210, + BarFiltered = 220, + BazGrouped = 230, + OutputComposed = 240, + DiagnosticsEmitted = 250 +} +``` +Name map merging built-in GeneratorStage names with custom names: +```csharp +public static class MyPipelineStageDescriptions +{ + public static Dictionary EventNameMap => _lazy.Value; + private static readonly Lazy> _lazy = new Lazy>(Create); + private static Dictionary Create() + { + var map = new Dictionary(GeneratorStageDescriptions.GeneratorStageNameMap) + { + { (int)MyPipelineStage.FooParsed, "Foo Parsed" }, + { (int)MyPipelineStage.FooAndBarCombined, "Foo & Bar Combined" }, + { (int)MyPipelineStage.BarFiltered, "Bar Filtered" }, + { (int)MyPipelineStage.BazGrouped, "Baz Grouped" }, + { (int)MyPipelineStage.OutputComposed, "Output Composed" }, + { (int)MyPipelineStage.DiagnosticsEmitted, "Diagnostics Emitted" }, + }; + return map; + } +} +``` +Using custom events alongside GeneratorStage: +```csharp +LightweightTrace.Add(MyPipelineStage.FooParsed); // event +LightweightTrace.IncrementCount(MyPipelineStage.BarFiltered); // counter +LightweightTrace.MethodEntry(MyPipelineStage.OutputComposed); // entry +LightweightTrace.MethodExit(MyPipelineStage.OutputComposed); // exit +LightweightTrace.IncrementCount(GeneratorStage.MethodCall, + (int)MyPipelineStage.OutputComposed, mapValue: true); // categorical mapping +buffer.AppendDiagnosticsComment(MyPipelineStageDescriptions.EventNameMap); // merged names +``` +Result: diagnostics output shows both core GeneratorStage events and your custom stages with readable names. + +## Recommended Numeric ID Ranges +CompositeValueShift = 1024, so valid base event/counter IDs (the id portion) are 0–1023. + +| Range | Intended Use | +|-----------|-------------------------------------------------------------| +| 0–99 | Built-in / core GeneratorStage (leave gaps for expansion). | +| 100–1023 | Your custom stages (pick sparse numbers, avoid collisions). | -# Example Usage +Beyond 1023 (>= CompositeValueShift) cannot be used for the base id field unless you change CompositeValueShift (must remain a power of two). -_**todo**_ +Value (the second part of a composite key: id + value*CompositeValueShift [+ MapValueFlag]) occupies higher bits: +* Unmapped keys (flag off) must remain < MapValueFlag (1 << 28). This yields a maximum raw value of (MapValueFlag / CompositeValueShift) - 1 = 262,143 (2^18 - 1) before the flag bit would be set. +* Mapped keys add MapValueFlag, so their value portion can extend further; maximum safe mapped value ≈ floor((Int32.MaxValue - MapValueFlag) / CompositeValueShift) = 1,835,007. +Guidelines: +1. Keep base IDs < 1024; raise CompositeValueShift only if you truly need more distinct IDs. +2. Reserve low IDs for framework / shared semantics; start custom enums at 100 or above. +3. Leave numeric gaps for future stages (e.g., 100,110,120...) to insert later steps cleanly. +4. Bucket numbers (value argument) are independent of the base ID range; keep them small for readability even though large values are supported. +5. When mapping enum values (mapValue: true) ensure those enum members also have numeric values < CompositeValueShift so they fit the id naming space of the map. +6. Avoid redefining an existing numeric ID with a new meaning once shipped; allocate a new ID instead. + # Excluding the source files To disable the inclusion of a specific source file, @@ -32,7 +186,7 @@ The files will still appear in the project, but will not add anything to the com ### Note: Dependency `LightweightTraceExtensions.cs` depends on `LightweightTrace.cs` and will **not work** when it -is excluded. (Unless you supply your own implementation of `LightweightTrace`.) +is excluded. (Unless you supply your own implementation of `LightweightTrace`.) --- diff --git a/docs/SourceTextGeneratorBase README.md b/docs/SourceTextGeneratorBase README.md index a37f6db..6d0125f 100644 --- a/docs/SourceTextGeneratorBase README.md +++ b/docs/SourceTextGeneratorBase README.md @@ -2,14 +2,114 @@ --- # SourceTextGeneratorBase.cs -***todo - expand on copied points below*** -- Provides a base class for incremental source generators that handles the boilerplate - of generating a partial class (or similar) file for an instance of a marker attribute. +Base class that writes the routine wrapper code for a generated partial type: the auto‑generated comment, optional `#nullable enable`, namespace (file‑scoped or block), any containing types, the type declaration and braces. You just supply the inside members. -# Example Usage +## Overview +`SourceTextGeneratorBase` is created for each attribute usage (`AttributeContextAndData`). It: +* Stores basic context (namespace, containing types, nullable, file‑scoped flag). +* Emits a simple header you can override. +* Calls overridable steps in a fixed order, then your `AppendCustomMembers()`. +* Closes everything and optionally lets you write diagnostics at the end. -_**todo**_ +It does not keep the full attribute payload itself; your subclass copies only what it needs. + +## Typical Overrides +In practice you usually only override: +* `PrepareForGeneration()` – capture/copy payload data, clear or seed the buffer. +* `AppendDocComments()` – add XML docs before the type declaration (optional). +* `AppendCustomMembers()` – generate the body (fields / properties / methods). + +Everything else has sensible defaults and is rarely touched. + +## Execution Order (actual method body) +Below is the exact implementation from the base class (comments removed for brevity): + +```csharp +protected virtual void AppendSource() +{ + PrepareForGeneration(); // override + + AppendAutoGeneratedComment(); + AppendNullableEnable(); + AppendStartNamespace(); + AppendContainingTypes(); + AppendDocComments(); + AppendTypeDeclaration(); + AppendStartBlock(); + + AppendCustomMembers(); // override + + AppendEndBlock(); + AppendContainingTypesEndBlock(); + AppendEndNamespace(); + + AppendDiagnosticLogs(); // defaults to NOP +} +``` + +## Example +Assume a marker attribute and payload already collected via `SelectAttributeContexts` (see `AttributeContextAndData` README). + +```csharp +// Payload type +public sealed class GenerateSomethingData : IEquatable +{ + public GenerateSomethingData(string name, int version) { Name = name; Version = version; } + public string Name { get; } + public int Version { get; } + public bool Equals(GenerateSomethingData other) => other != null && Name == other.Name && Version == other.Version; + public override bool Equals(object o) => Equals(o as GenerateSomethingData); + public override int GetHashCode() => (Name ?? "").GetHashCode() ^ Version; + public static GenerateSomethingData Collect(GeneratorAttributeSyntaxContext ctx) + { + var ad = ctx.Attributes[0]; + var args = ad.ConstructorArguments; + return new GenerateSomethingData((string)args[0].Value, args.Length > 1 ? (int)args[1].Value : 1); + } +} + +// Concrete generator for one usage +sealed class GenerateSomethingSource : SourceTextGeneratorBase +{ + private readonly GenerateSomethingData _data; + public GenerateSomethingSource( + in AttributeContextAndData usage, + in CancellationToken token) + : base(in usage, in token) + { + _data = usage.AttributeData; + } + + protected override void AppendDocComments() + { + Buffer.AppendLine("/// ") + .AppendLine($"/// Generated members for '{_data.Name}'.") + .AppendLine("/// "); + } + + protected override void AppendCustomMembers() + { + Buffer.AppendLine($"public const int GeneratedVersion = {_data.Version};"); + } + + protected override void AppendDiagnosticLogs() + { + Buffer.AppendLine(); + Buffer.Direct.AppendDiagnosticsComment(GeneratorStageDescriptions.GeneratorStageNameMap); + } +} + +// In IncrementalGenerator.Initialize +const string AttributeMetadataName = "MyNamespace.GenerateSomethingAttribute"; +var usages = context.SelectAttributeContexts(AttributeMetadataName, GenerateSomethingData.Collect); + +context.RegisterSourceOutput(usages, static (spc, usage) => +{ + var gen = new GenerateSomethingSource(in usage, in spc.CancellationToken); + spc.AddSource(usage.CreateHintName("GenerateSomething"), gen.GetSourceText()); +}); +``` # Excluding the source files @@ -24,9 +124,16 @@ define the relevant constant in the consuming project's `.csproj` file: The files will still appear in the project, but will not add anything to the compilation. -### Note: Dependency -`SourceTextGeneratorBase.cs` depends on `AttributeContextAndData.cs` and `IndentingLineAppender.cs` -and will **not work** when either of them are excluded. +### Note: Dependencies +Direct: +* `SourceTextGeneratorBase.cs` -> `AttributeContextAndData.cs`, `TypeContext.cs`, `EquatableImmutableArray.cs`, `IndentingLineAppender.cs` + +Transitive: +* `AttributeContextAndData.cs` itself depends (directly) on `TypeContext.cs` & `EquatableImmutableArray.cs` (already listed as direct here, so no hidden transitive chain beyond these) + +Behavior: +* Fail‑fast `#error` directives are emitted if any direct dependency is excluded; this surfaces misconfiguration early. +* No additional guards for transitive items are needed since all are direct in this case. --- diff --git a/version.props b/version.props index f945316..2accf7f 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 1.0.5 + 1.0.6