From 4498b2fd34349a1bf137291d522791d355224afc Mon Sep 17 00:00:00 2001 From: Daniel Woodward <16451892+DrBarnabus@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:34:44 +0000 Subject: [PATCH 1/2] refactor: Introduce DiagnosticContext to replace Parser mutable state Replace the two mutable instance fields (_diagnostics and _location) on Parser with an explicit DiagnosticContext parameter, making diagnostic flow visible in method signatures instead of relying on closure capture. - Add DiagnosticContext class carrying CurrentLocation, Diagnostics list, and ReportDiagnostic method with existing fallback logic - Update Parser.Parse to return (GenerationSpec?, Diagnostics) tuple, eliminating mutable state from the Parser instance entirely - Pass DiagnosticContext through ParseTemplateStringIntoKeyParts and its local functions as an explicit parameter - Simplify SourceGenerator to use Parse return tuple directly - Add unit tests for DiagnosticContext covering location fallback, accumulation, and message arg passthrough --- .../DiagnosticContextTests.cs | 141 ++++++++++++++++++ .../DiagnosticContext.cs | 27 ++++ src/CompositeKey.SourceGeneration/Parser.cs | 78 +++++----- .../SourceGenerator.cs | 4 +- 4 files changed, 203 insertions(+), 47 deletions(-) create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/DiagnosticContextTests.cs create mode 100644 src/CompositeKey.SourceGeneration/DiagnosticContext.cs diff --git a/src/CompositeKey.SourceGeneration.UnitTests/DiagnosticContextTests.cs b/src/CompositeKey.SourceGeneration.UnitTests/DiagnosticContextTests.cs new file mode 100644 index 0000000..32a384a --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/DiagnosticContextTests.cs @@ -0,0 +1,141 @@ +using CompositeKey.SourceGeneration.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace CompositeKey.SourceGeneration.UnitTests; + +public static class DiagnosticContextTests +{ + #pragma warning disable RS2008 // Enable analyzer release tracking + private static readonly DiagnosticDescriptor TestDescriptor = new( + id: "TEST0001", + title: "Test Diagnostic", + messageFormat: "Test message: {0}", + category: "Test", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + #pragma warning restore RS2008 + + private static Compilation CreateTestCompilation() + { + return CompilationHelper.CreateCompilation("public class Placeholder { }"); + } + + private static Location CreateLocationInCompilation(Compilation compilation) + { + var tree = compilation.SyntaxTrees.First(); + return Location.Create(tree, TextSpan.FromBounds(0, 1)); + } + + private static Location CreateLocationOutsideCompilation() + { + var externalTree = CSharpSyntaxTree.ParseText("public class External { }"); + return Location.Create(externalTree, TextSpan.FromBounds(0, 1)); + } + + [Fact] + public static void ReportDiagnostic_WithValidLocation_UsesProvidedLocation() + { + var compilation = CreateTestCompilation(); + var context = new DiagnosticContext(compilation); + var currentLocation = CreateLocationInCompilation(compilation); + context.CurrentLocation = currentLocation; + + var providedLocation = CreateLocationInCompilation(compilation); + context.ReportDiagnostic(TestDescriptor, providedLocation, "arg1"); + + context.Diagnostics.Count.ShouldBe(1); + context.Diagnostics[0].Location.ShouldNotBeNull(); + context.Diagnostics[0].Location!.SourceSpan.ShouldBe(providedLocation.SourceSpan); + } + + [Fact] + public static void ReportDiagnostic_WithNullLocation_FallsBackToCurrentLocation() + { + var compilation = CreateTestCompilation(); + var context = new DiagnosticContext(compilation); + var currentLocation = CreateLocationInCompilation(compilation); + context.CurrentLocation = currentLocation; + + context.ReportDiagnostic(TestDescriptor, null, "arg1"); + + context.Diagnostics.Count.ShouldBe(1); + context.Diagnostics[0].Location.ShouldNotBeNull(); + context.Diagnostics[0].Location!.SourceSpan.ShouldBe(currentLocation.SourceSpan); + } + + [Fact] + public static void ReportDiagnostic_WithLocationFromUnknownTree_FallsBackToCurrentLocation() + { + var compilation = CreateTestCompilation(); + var context = new DiagnosticContext(compilation); + var currentLocation = CreateLocationInCompilation(compilation); + context.CurrentLocation = currentLocation; + + var externalLocation = CreateLocationOutsideCompilation(); + context.ReportDiagnostic(TestDescriptor, externalLocation, "arg1"); + + context.Diagnostics.Count.ShouldBe(1); + context.Diagnostics[0].Location.ShouldNotBeNull(); + context.Diagnostics[0].Location!.SourceSpan.ShouldBe(currentLocation.SourceSpan); + } + + [Fact] + public static void ReportDiagnostic_AccumulatesMultipleDiagnostics() + { + var compilation = CreateTestCompilation(); + var context = new DiagnosticContext(compilation); + context.CurrentLocation = CreateLocationInCompilation(compilation); + + context.ReportDiagnostic(TestDescriptor, null, "first"); + context.ReportDiagnostic(TestDescriptor, null, "second"); + context.ReportDiagnostic(TestDescriptor, null, "third"); + + context.Diagnostics.Count.ShouldBe(3); + } + + [Fact] + public static void ReportDiagnostic_PassesMessageArgsCorrectly() + { + var compilation = CreateTestCompilation(); + var context = new DiagnosticContext(compilation); + context.CurrentLocation = CreateLocationInCompilation(compilation); + + context.ReportDiagnostic(TestDescriptor, null, "test-value"); + + context.Diagnostics.Count.ShouldBe(1); + var diagnostic = context.Diagnostics[0].CreateDiagnostic(); + diagnostic.GetMessage().ShouldBe("Test message: test-value"); + } + + [Fact] + public static void Diagnostics_IsEmptyByDefault() + { + var compilation = CreateTestCompilation(); + var context = new DiagnosticContext(compilation); + + context.Diagnostics.Count.ShouldBe(0); + } + + [Fact] + public static void CurrentLocation_IsNullByDefault() + { + var compilation = CreateTestCompilation(); + var context = new DiagnosticContext(compilation); + + context.CurrentLocation.ShouldBeNull(); + } + + [Fact] + public static void CurrentLocation_CanBeSet() + { + var compilation = CreateTestCompilation(); + var context = new DiagnosticContext(compilation); + var location = CreateLocationInCompilation(compilation); + + context.CurrentLocation = location; + + context.CurrentLocation.ShouldBe(location); + } +} diff --git a/src/CompositeKey.SourceGeneration/DiagnosticContext.cs b/src/CompositeKey.SourceGeneration/DiagnosticContext.cs new file mode 100644 index 0000000..76ca9ee --- /dev/null +++ b/src/CompositeKey.SourceGeneration/DiagnosticContext.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using CompositeKey.SourceGeneration.Core; +using Microsoft.CodeAnalysis; + +namespace CompositeKey.SourceGeneration; + +internal sealed class DiagnosticContext(Compilation compilation) +{ + private readonly Compilation _compilation = compilation; + private readonly List _diagnostics = []; + + public Location? CurrentLocation { get; set; } + + public IReadOnlyList Diagnostics => _diagnostics; + + public void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location, params object?[]? messageArgs) + { + Debug.Assert(CurrentLocation != null); + + if (location is null || (location.SourceTree is not null && !_compilation.ContainsSyntaxTree(location.SourceTree))) + location = CurrentLocation; + + _diagnostics.Add(DiagnosticInfo.Create(descriptor, location, messageArgs)); + } + + public ImmutableEquatableArray ToImmutableDiagnostics() => _diagnostics.ToImmutableEquatableArray(); +} diff --git a/src/CompositeKey.SourceGeneration/Parser.cs b/src/CompositeKey.SourceGeneration/Parser.cs index 99bd883..9434aa0 100644 --- a/src/CompositeKey.SourceGeneration/Parser.cs +++ b/src/CompositeKey.SourceGeneration/Parser.cs @@ -19,36 +19,23 @@ internal sealed class Parser(KnownTypeSymbols knownTypeSymbols) private const LanguageVersion MinimumSupportedLanguageVersion = LanguageVersion.CSharp11; private readonly KnownTypeSymbols _knownTypeSymbols = knownTypeSymbols; - private readonly List _diagnostics = []; - private Location? _location; - - public ImmutableEquatableArray Diagnostics => _diagnostics.ToImmutableEquatableArray(); - - public void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location, params object?[]? messageArgs) - { - Debug.Assert(_location != null); - - if (location is null || (location.SourceTree is not null && !_knownTypeSymbols.Compilation.ContainsSyntaxTree(location.SourceTree))) - location = _location; - - _diagnostics.Add(DiagnosticInfo.Create(descriptor, location, messageArgs)); - } - - public GenerationSpec? Parse( + public (GenerationSpec? GenerationSpec, ImmutableEquatableArray Diagnostics) Parse( TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel semanticModel, CancellationToken cancellationToken) { + var diagnosticContext = new DiagnosticContext(_knownTypeSymbols.Compilation); + var targetTypeSymbol = semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, cancellationToken); Debug.Assert(targetTypeSymbol != null); - _location = targetTypeSymbol!.Locations.Length > 0 ? targetTypeSymbol.Locations[0] : null; - Debug.Assert(_location is not null); + diagnosticContext.CurrentLocation = targetTypeSymbol!.Locations.Length > 0 ? targetTypeSymbol.Locations[0] : null; + Debug.Assert(diagnosticContext.CurrentLocation is not null); var languageVersion = _knownTypeSymbols.Compilation is CSharpCompilation csc ? csc.LanguageVersion : (LanguageVersion?)null; if (languageVersion is null or < MinimumSupportedLanguageVersion) { - ReportDiagnostic(DiagnosticDescriptors.UnsupportedLanguageVersion, _location, languageVersion?.ToDisplayString(), MinimumSupportedLanguageVersion.ToDisplayString()); - return null; + diagnosticContext.ReportDiagnostic(DiagnosticDescriptors.UnsupportedLanguageVersion, diagnosticContext.CurrentLocation, languageVersion?.ToDisplayString(), MinimumSupportedLanguageVersion.ToDisplayString()); + return (null, diagnosticContext.ToImmutableDiagnostics()); } // Validate type structure using comprehensive shared validation @@ -61,8 +48,8 @@ public void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location if (!validationResult.IsSuccess) { - ReportDiagnostic(validationResult.Descriptor, _location, validationResult.MessageArgs); - return null; + diagnosticContext.ReportDiagnostic(validationResult.Descriptor, diagnosticContext.CurrentLocation, validationResult.MessageArgs); + return (null, diagnosticContext.ToImmutableDiagnostics()); } // Use validated data from the validation result (guaranteed non-null due to MemberNotNullWhen on IsSuccess) @@ -77,9 +64,9 @@ public void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location var propertyInitializers = ParsePropertyInitializers(constructorParameters, properties.Select(p => p.Spec).ToList(), ref constructionStrategy, constructorSetsRequiredMembers); var propertiesUsedInKey = new List<(PropertySpec Spec, ITypeSymbol TypeSymbol)>(); - var keyParts = ParseTemplateStringIntoKeyParts(compositeKeyAttributeValues!, properties, propertiesUsedInKey); + var keyParts = ParseTemplateStringIntoKeyParts(diagnosticContext, compositeKeyAttributeValues!, properties, propertiesUsedInKey); if (keyParts is null) - return null; // Should have already reported diagnostics by this point so just return null... + return (null, diagnosticContext.ToImmutableDiagnostics()); var primaryDelimiterKeyPart = keyParts.OfType().FirstOrDefault(); @@ -101,7 +88,7 @@ public void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location sortKeyParts.ToImmutableEquatableArray()); } - return new GenerationSpec( + var generationSpec = new GenerationSpec( new TargetTypeSpec( new TypeRef(targetTypeSymbol), targetTypeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null, @@ -111,6 +98,8 @@ public void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location (propertyInitializers?.Where(pi => propertiesUsedInKey.Any(p => p.Spec.Name == pi.Name))).ToImmutableEquatableArray(), constructionStrategy), key); + + return (generationSpec, diagnosticContext.ToImmutableDiagnostics()); } private static (List PartitionKeyParts, List SortKeyParts) SplitKeyPartsIntoPartitionAndSortKey(List keyParts) @@ -124,6 +113,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl } private List? ParseTemplateStringIntoKeyParts( + DiagnosticContext diagnosticContext, CompositeKeyAttributeValues compositeKeyAttributeValues, List<(PropertySpec Spec, ITypeSymbol TypeSymbol)> properties, List<(PropertySpec Spec, ITypeSymbol TypeSymbol)> propertiesUsedInKey) @@ -133,26 +123,26 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl var (tokenizationSuccessful, templateTokens) = TemplateValidation.TokenizeTemplateString(templateString, primaryKeySeparator); if (!tokenizationSuccessful) { - ReportDiagnostic(DiagnosticDescriptors.EmptyOrInvalidTemplateString, _location, templateString); + diagnosticContext.ReportDiagnostic(DiagnosticDescriptors.EmptyOrInvalidTemplateString, diagnosticContext.CurrentLocation, templateString); return null; } var separatorValidation = TemplateValidation.ValidatePrimaryKeySeparator(templateString, primaryKeySeparator, templateTokens); if (!separatorValidation.IsSuccess) { - ReportDiagnostic(separatorValidation.Descriptor, _location, separatorValidation.MessageArgs); + diagnosticContext.ReportDiagnostic(separatorValidation.Descriptor, diagnosticContext.CurrentLocation, separatorValidation.MessageArgs); return null; } if (!TemplateValidation.HasValidTemplateStructure(templateTokens)) { - ReportDiagnostic(DiagnosticDescriptors.EmptyOrInvalidTemplateString, _location, templateString); + diagnosticContext.ReportDiagnostic(DiagnosticDescriptors.EmptyOrInvalidTemplateString, diagnosticContext.CurrentLocation, templateString); return null; } if (primaryKeySeparator.HasValue && !TemplateValidation.ValidatePartitionAndSortKeyStructure(templateTokens, out _)) { - ReportDiagnostic(DiagnosticDescriptors.EmptyOrInvalidTemplateString, _location, templateString); + diagnosticContext.ReportDiagnostic(DiagnosticDescriptors.EmptyOrInvalidTemplateString, diagnosticContext.CurrentLocation, templateString); return null; } @@ -163,15 +153,15 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl { PrimaryDelimiterTemplateToken pd => new PrimaryDelimiterKeyPart(pd.Value) { LengthRequired = 1 }, DelimiterTemplateToken d => new DelimiterKeyPart(d.Value) { LengthRequired = 1 }, - PropertyTemplateToken p => ToPropertyKeyPart(p), - RepeatingPropertyTemplateToken rp => ToRepeatingPropertyKeyPart(rp), + PropertyTemplateToken p => ToPropertyKeyPart(diagnosticContext, p), + RepeatingPropertyTemplateToken rp => ToRepeatingPropertyKeyPart(diagnosticContext, rp), ConstantTemplateToken c => new ConstantKeyPart(c.Value) { LengthRequired = c.Value.Length }, _ => null }; if (keyPart is null) { - ReportDiagnostic(DiagnosticDescriptors.EmptyOrInvalidTemplateString, _location, templateString); + diagnosticContext.ReportDiagnostic(DiagnosticDescriptors.EmptyOrInvalidTemplateString, diagnosticContext.CurrentLocation, templateString); return null; } @@ -183,7 +173,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl { if (keyPart is PropertyKeyPart pkp && pkp.Property.CollectionType != CollectionType.None) { - ReportDiagnostic(DiagnosticDescriptors.RepeatingTypeMustUseRepeatingSyntax, _location, pkp.Property.Name); + diagnosticContext.ReportDiagnostic(DiagnosticDescriptors.RepeatingTypeMustUseRepeatingSyntax, diagnosticContext.CurrentLocation, pkp.Property.Name); return null; } } @@ -196,7 +186,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl if (valueParts.Any(kp => kp is RepeatingPropertyKeyPart)) { var repeatingPart = valueParts.First(kp => kp is RepeatingPropertyKeyPart) as RepeatingPropertyKeyPart; - ReportDiagnostic(DiagnosticDescriptors.RepeatingPropertyMustBeLastPart, _location, repeatingPart!.Property.Name); + diagnosticContext.ReportDiagnostic(DiagnosticDescriptors.RepeatingPropertyMustBeLastPart, diagnosticContext.CurrentLocation, repeatingPart!.Property.Name); return null; } } @@ -210,7 +200,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl if (partitionValueParts.Count > 0 && partitionValueParts.Any(kp => kp is RepeatingPropertyKeyPart) && partitionValueParts[^1] is not RepeatingPropertyKeyPart) { var repeatingPart = partitionValueParts.First(kp => kp is RepeatingPropertyKeyPart) as RepeatingPropertyKeyPart; - ReportDiagnostic(DiagnosticDescriptors.RepeatingPropertyMustBeLastPart, _location, repeatingPart!.Property.Name); + diagnosticContext.ReportDiagnostic(DiagnosticDescriptors.RepeatingPropertyMustBeLastPart, diagnosticContext.CurrentLocation, repeatingPart!.Property.Name); return null; } @@ -218,14 +208,14 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl if (sortValueParts.Count > 0 && sortValueParts.Any(kp => kp is RepeatingPropertyKeyPart) && sortValueParts[^1] is not RepeatingPropertyKeyPart) { var repeatingPart = sortValueParts.First(kp => kp is RepeatingPropertyKeyPart) as RepeatingPropertyKeyPart; - ReportDiagnostic(DiagnosticDescriptors.RepeatingPropertyMustBeLastPart, _location, repeatingPart!.Property.Name); + diagnosticContext.ReportDiagnostic(DiagnosticDescriptors.RepeatingPropertyMustBeLastPart, diagnosticContext.CurrentLocation, repeatingPart!.Property.Name); return null; } } return keyParts; - PropertyKeyPart? ToPropertyKeyPart(PropertyTemplateToken templateToken) + PropertyKeyPart? ToPropertyKeyPart(DiagnosticContext ctx, PropertyTemplateToken templateToken) { var availableProperties = properties .Select(p => new TemplateValidation.PropertyInfo(p.Spec.Name, p.Spec.HasGetter, p.Spec.HasSetter)) @@ -234,7 +224,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl var propertyValidation = TemplateValidation.ValidatePropertyReferences([templateToken], availableProperties); if (!propertyValidation.IsSuccess) { - ReportDiagnostic(propertyValidation.Descriptor, _location, propertyValidation.MessageArgs); + ctx.ReportDiagnostic(propertyValidation.Descriptor, ctx.CurrentLocation, propertyValidation.MessageArgs); return null; } @@ -246,7 +236,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl // Repeating type properties must use repeating syntax if (propertySpec.CollectionType != CollectionType.None) { - ReportDiagnostic(DiagnosticDescriptors.RepeatingTypeMustUseRepeatingSyntax, _location, propertySpec.Name); + ctx.ReportDiagnostic(DiagnosticDescriptors.RepeatingTypeMustUseRepeatingSyntax, ctx.CurrentLocation, propertySpec.Name); return null; } @@ -269,7 +259,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl if (!formatValidation.IsSuccess) { - ReportDiagnostic(formatValidation.Descriptor, _location, formatValidation.MessageArgs); + ctx.ReportDiagnostic(formatValidation.Descriptor, ctx.CurrentLocation, formatValidation.MessageArgs); return null; } @@ -321,7 +311,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl }; } - RepeatingPropertyKeyPart? ToRepeatingPropertyKeyPart(RepeatingPropertyTemplateToken templateToken) + RepeatingPropertyKeyPart? ToRepeatingPropertyKeyPart(DiagnosticContext ctx, RepeatingPropertyTemplateToken templateToken) { var availableProperties = properties .Select(p => new TemplateValidation.PropertyInfo(p.Spec.Name, p.Spec.HasGetter, p.Spec.HasSetter)) @@ -330,7 +320,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl var propertyValidation = TemplateValidation.ValidatePropertyReferences([templateToken], availableProperties); if (!propertyValidation.IsSuccess) { - ReportDiagnostic(propertyValidation.Descriptor, _location, propertyValidation.MessageArgs); + ctx.ReportDiagnostic(propertyValidation.Descriptor, ctx.CurrentLocation, propertyValidation.MessageArgs); return null; } @@ -340,7 +330,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl // Validate that the property is a collection type if (propertySpec.CollectionType == CollectionType.None) { - ReportDiagnostic(DiagnosticDescriptors.RepeatingPropertyMustUseCollectionType, _location, propertySpec.Name); + ctx.ReportDiagnostic(DiagnosticDescriptors.RepeatingPropertyMustUseCollectionType, ctx.CurrentLocation, propertySpec.Name); return null; } @@ -367,7 +357,7 @@ private static (List PartitionKeyParts, List SortKeyParts) Spl if (!formatValidation.IsSuccess) { - ReportDiagnostic(formatValidation.Descriptor, _location, formatValidation.MessageArgs); + ctx.ReportDiagnostic(formatValidation.Descriptor, ctx.CurrentLocation, formatValidation.MessageArgs); return null; } diff --git a/src/CompositeKey.SourceGeneration/SourceGenerator.cs b/src/CompositeKey.SourceGeneration/SourceGenerator.cs index 8f76c9b..b979241 100644 --- a/src/CompositeKey.SourceGeneration/SourceGenerator.cs +++ b/src/CompositeKey.SourceGeneration/SourceGenerator.cs @@ -24,9 +24,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select(static (tuple, cancellationToken) => { var parser = new Parser(tuple.Right); - var generationSpec = parser.Parse(tuple.Left.TypeDeclarationSyntax, tuple.Left.SemanticModel, cancellationToken); - - return (GenerationSpec: generationSpec, parser.Diagnostics); + return parser.Parse(tuple.Left.TypeDeclarationSyntax, tuple.Left.SemanticModel, cancellationToken); }) .WithTrackingName(GenerationSpecTrackingName); From a1a2d4185b6fedb0661c696d1d5a508127a73592 Mon Sep 17 00:00:00 2001 From: Daniel Woodward <16451892+DrBarnabus@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:42:57 +0000 Subject: [PATCH 2/2] docs: Update CLAUDE.md architecture section for extracted Parser/Emitter files --- CLAUDE.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9aa41cf..9cd5b73 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,10 +31,12 @@ The solution is split into four main projects plus corresponding test projects: **CompositeKey** (`src/CompositeKey/`) — Public API surface. Contains `[CompositeKey]` attribute, `[CompositeKeyConstructor]` attribute, and the `IPrimaryKey` / `ICompositePrimaryKey` interfaces. This is the NuGet package users install; analyzers and source generator ship embedded inside it. -**CompositeKey.SourceGeneration** (`src/CompositeKey.SourceGeneration/`) — Incremental source generator implementing `IIncrementalGenerator`. Three key phases: +**CompositeKey.SourceGeneration** (`src/CompositeKey.SourceGeneration/`) — Incremental source generator implementing `IIncrementalGenerator`. Key files: + - `SourceGenerator.cs` — Entry point; uses `ForAttributeWithMetadataName` for incremental pipeline -- `SourceGenerator.Parser.cs` — Extracts attribute data, validates types, builds `GenerationSpec` model -- `SourceGenerator.Emitter.cs` — Generates `ToString()`, `Parse()`, `TryParse()`, `ToPartitionKeyString()`, `ToSortKeyString()`, partial formatting methods, and `ISpanParsable` implementations +- `Parser.cs` — Extracts attribute data, validates types, builds `GenerationSpec` model; `Parse` returns a `(GenerationSpec?, Diagnostics)` tuple +- `DiagnosticContext.cs` — Carries `CurrentLocation`, accumulated diagnostics, and `ReportDiagnostic` with source-tree fallback logic; created per `Parse` invocation +- `Emitter.cs` — Generates `ToString()`, `Parse()`, `TryParse()`, `ToPartitionKeyString()`, `ToSortKeyString()`, partial formatting methods, and `ISpanParsable` implementations **CompositeKey.Analyzers.Common** (`src/CompositeKey.Analyzers.Common/`) — Shared validation logic used by both the source generator and IDE analyzers. Contains `TemplateStringTokenizer`, type/template/property validation, and all `DiagnosticDescriptor` definitions (COMPOSITE0001–COMPOSITE0008+).