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 @@
[](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