From 710cf6dc2ab6921a241559e4c2771e3709b02045 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Wed, 16 Sep 2026 17:15:39 +0100 Subject: [PATCH] docs: updated urls in documentation --- AGENTS.md | 2 +- README.md | 10 +- docs/wiki/Analyzers.md | 115 ++++++++ docs/wiki/Attribute-Data-Models.md | 196 +++++++++++++ docs/{code-writer.md => wiki/Code-Writer.md} | 4 +- docs/wiki/Getting-Started.md | 183 ++++++++++++ docs/{guide.md => wiki/Guide.md} | 12 +- docs/wiki/Home.md | 80 ++++++ docs/wiki/Incremental-Pipeline.md | 268 +++++++++++++++++ docs/wiki/Packaging.md | 159 ++++++++++ docs/{performance.md => wiki/Performance.md} | 4 +- docs/wiki/Release-Flow.md | 81 ++++++ .../Step-Cache-Tests.md} | 0 docs/wiki/Testing-TUnit.md | 197 +++++++++++++ docs/wiki/Testing.md | 272 ++++++++++++++++++ .../{type-library.md => wiki/Type-Library.md} | 2 +- docs/wiki/_Sidebar.md | 14 + package.json | 14 +- src/Directory.Build.props | 8 +- src/Directory.Build.targets | 2 +- .../README.md | 2 +- .../README.md | 2 +- .../Sdk/README.md | 5 + .../Sdk/README.md | 7 +- .../SourceGeneratorFramework/Sdk/README.md | 5 + 25 files changed, 1616 insertions(+), 28 deletions(-) create mode 100644 docs/wiki/Analyzers.md create mode 100644 docs/wiki/Attribute-Data-Models.md rename docs/{code-writer.md => wiki/Code-Writer.md} (99%) create mode 100644 docs/wiki/Getting-Started.md rename docs/{guide.md => wiki/Guide.md} (99%) create mode 100644 docs/wiki/Home.md create mode 100644 docs/wiki/Incremental-Pipeline.md create mode 100644 docs/wiki/Packaging.md rename docs/{performance.md => wiki/Performance.md} (97%) create mode 100644 docs/wiki/Release-Flow.md rename docs/{step-cache-tests.md => wiki/Step-Cache-Tests.md} (100%) create mode 100644 docs/wiki/Testing-TUnit.md create mode 100644 docs/wiki/Testing.md rename docs/{type-library.md => wiki/Type-Library.md} (99%) create mode 100644 docs/wiki/_Sidebar.md diff --git a/AGENTS.md b/AGENTS.md index a39a963..19190e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,4 +8,4 @@ Do **not** suppress or mute compiler, analyser, or build warnings by adding ``). | +| `PSGFR16` | Prefer the nullable-context `Nullable()`/`MakeNullable()` overload so annotations honour the target compilation. | +| `PSGFR17` | Consume `CodeWriter` scope-returning methods (`...Scope`, `IndentedScope`) with `using`. | +| `PSGFR18` | Prefer structured declaration APIs (`Class`, `Method`, `Property`, `Field`) over raw declaration text. | +| `PSGFR19` | Prefer structured statement APIs (`Return`, `MethodCall`, `Throw`, `Assignment`, `Using`, `Comment`) over raw statement text. | +| `PSGFR20` | Prefer the minimal `CodeWriter` overloads over constructing `*DeclarationOptions` values manually. | +| `PSGFR21` | Prefer `HashDefines`/`HashDefinesScope` for `#if`/`#endif` conditional-compilation directives. | +| `PSGFR22` | Prefer `PragmaDisable`/`OpenPragmasScope` for `#pragma warning` directives. | +| `PSGFR23` | Prefer structured `IfBlock`/`ElseIf`/`Else` over raw `if` block text. | +| `PSGFR24` | `CodeFixProvider` is not marked `[ExportCodeFixProvider]`; Visual Studio will never discover it. | +| `PSGFR25` | `DiagnosticAnalyzer` is not marked `[DiagnosticAnalyzer]`; it will never run. | +| `PSGFR26` | A generator type is not marked `[Generator]`; it will never run. | +| `PSGFR27` | A Roslyn component type is not public; the compiler host cannot instantiate it. | +| `PSGFR28` | `FixableDiagnosticIds` references a diagnostic ID no analyzer in the compilation produces; the fix will never be shown. | +| `PSGFR29` | Do not embed a `CodeWriter` in a string; use `XmlCommentWriter.XmlInlineCode` instead. | +| `PSGFR30` | Prefer `static` lambdas in incremental pipeline methods so the compiler never allocates a closure on the per-item hot path. | +| `PSGFR31` | Prefer `GeneratorAttributeSyntaxContext.TargetSymbol` over `SemanticModel.GetDeclaredSymbol(ctx.TargetNode)`. | +| `PSGFR32` | Avoid `NormalizeWhitespace` when generating source; use an indented text writer such as `CodeWriter`. | +| `PSGFR33` | Pipeline models must not retain Roslyn objects (`ISymbol`, `SyntaxNode`, `Location`, ...); extract the information into value types. | +| `PSGFR34` | Prefer C# 14 `extension(Receiver)` blocks over classic static `this`-parameter extension methods. | +| `PSGFR35` | Extension class name must match the extended type (`{Receiver}Extensions`). | +| `PSGFR36` | Extension classes must be placed in the extended type's namespace under an `Extensions` folder. | +| `PSGFR37` | One extension class per receiver type; split classes that extend multiple types. | +| `PSGFR38` | Extension classes should carry `[EditorBrowsable(EditorBrowsableState.Never)]`. | + +## Type-library and attribute-model diagnostics + +The bundled generators carry their own diagnostic families, reported by the +`TypeLibraryValidationAnalyzer` (`TLB0001`–`TLB0019`) and the attribute-data-model validation +analyzers. These are documented on their feature pages: + +- [Type-Library.md](Type-Library.md#validation) +- [Attribute-Data-Models.md](Attribute-Data-Models.md) + +## Code fixes + +Code fix providers ship in the `Purview.SourceGeneratorFramework.CodeFixers` assembly and cover the +analyzer rules above, including: + +- `AddGeneratorAttributeCodeFixProvider` — adds the missing `[Generator]` attribute (`PSGFR26`). +- `AddDiagnosticAnalyzerAttributeCodeFixProvider` — adds `[DiagnosticAnalyzer]` (`PSGFR25`). +- `AddExportCodeFixProviderAttributeCodeFixProvider` — adds `[ExportCodeFixProvider]` (`PSGFR24`). +- `MakeRoslynComponentPublicCodeFixProvider` — makes the component type public (`PSGFR27`). +- `RemoveOrphanedFixableDiagnosticIdCodeFixProvider` — removes unused fixable diagnostic IDs (`PSGFR28`). +- `PreferTargetSymbolCodeFixProvider` — switches to `TargetSymbol` (`PSGFR31`). +- `PreferStaticLambdaCodeFixProvider` — makes pipeline lambdas `static` (`PSGFR30`). +- `PreferNullableContextOverloadCodeFixProvider` — adds the generation context to `Nullable()` / + `MakeNullable()` calls, including project-wide "Fix all" support (`PSGFR16`). +- `PipelineModelReferenceEqualityCollectionCodeFixProvider` — wraps collection members for sequence + equality (`PSGFR15`). +- `PreferStructuredCodeWriterIfBlockCodeFixProvider` — rewrites raw `if`/`else if`/`else` block text + to the structured `IfBlock`/`ElseIf`/`Else` APIs (`PSGFR23`). +- `CodeWriterToStringCodeFixProvider` — replaces embedded `CodeWriter` string interpolation (`PSGFR29`). +- `AttributeDataModelSymbolPropertyCodeFixProvider` — fixes attribute-data-model symbol properties. +- `ReorganizeExtensionClassCodeFixProvider` — renames (`PSGFR35`), splits multi-receiver classes + (`PSGFR37`), moves the class under `Extensions/{ReceiverNamespace}/`, and updates referencing files + (`PSGFR36`). +- `ConvertToExtensionBlockCodeFixProvider` — converts classic methods to C# 14 `extension` blocks + (`PSGFR34`). +- `AddExtensionClassMetadataCodeFixProvider` — adds `[EditorBrowsable(EditorBrowsableState.Never)]` + to extension classes (`PSGFR38`). +- Type-library fixes — `TypeLibraryMemberAccessibilityCodeFixProvider`, + `TypeLibraryMarkerDefaultInitializerCodeFixProvider`, `MakeTypeLibrarySpecPartialCodeFixProvider`, + `RenameTypeLibrarySpecCodeFixProvider`, and `TypeLibraryMemberTypeCodeFixProvider`. + +See [Guide.md](Guide.md#19-extension-class-conventions) for the extension-class conventions the +`PSGFR34`–`PSGFR38` rules enforce. + +## Roslyn component discovery + +The compiler host only loads a source generator, diagnostic analyzer, or code fix provider when three +conditions hold. Missing any one means the component is **silently ignored**: + +1. **The type is public** (`PSGFR27`). +2. **The type is decorated** — `[Generator]` (`PSGFR26`), `[DiagnosticAnalyzer]` (`PSGFR25`), or + `[ExportCodeFixProvider]` (`PSGFR24`). +3. **The assembly is loaded as an analyzer** — packed under `analyzers/dotnet/cs/` in a package, or + referenced with `OutputItemType="Analyzer"` in a project reference. + +A code fix provider also only appears when the diagnostic ID in `FixableDiagnosticIds` is actually +produced by an analyzer loaded alongside it (`PSGFR28`). Visual Studio MEF-composes fix providers when +the analyzer set loads, so after adding or updating a fixer assembly you must restart Visual Studio or +reload the project for the fixes to appear. + +## License + +This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project. \ No newline at end of file diff --git a/docs/wiki/Attribute-Data-Models.md b/docs/wiki/Attribute-Data-Models.md new file mode 100644 index 0000000..f97ea52 --- /dev/null +++ b/docs/wiki/Attribute-Data-Models.md @@ -0,0 +1,196 @@ +# Attribute Data Models + +`AttributeDataModelGenerator` generates `readonly record struct` parser models for .NET attributes. +Instead of hand-writing `FromAttributeData` methods for every attribute you inspect in a source +generator, declare a `readonly partial record struct` with `[Generate]` and the generator fills in the +`Empty` sentinel, `FromAttributeData` overloads, and property extraction logic. + +The generator is implemented in `Purview.SourceGeneratorFramework.Generators` and ships inside the +`Purview.SourceGeneratorFramework` package under `analyzers/dotnet/cs/`, so it runs automatically +when you reference the package. + +## Marker attributes + +The generator emits marker attributes into your compilation: + +| Attribute | Purpose | +| --- | --- | +| `[Generate(Type targetAttribute)]` | Placed on a `readonly partial record struct` to opt into generation. | +| `[Generate(string targetAttribute)]` | Resolves the attribute by fully-qualified name. Use when the attribute type is not available in the generator's compilation (e.g. `LengthAttribute` in .NET 8+ or a self-generated attribute). | +| `[Property]` | A record parameter is populated from a named attribute property (the property name is inferred from the parameter name unless overridden). | +| `[Property(string name)]` | Explicit named property source. | +| `[Property(..., DefaultValue = ...)]` | Fallback value when the named property is not present. | +| `[Argument]` | Populated from a constructor argument by parameter name. | +| `[Argument(int index)]` | Constructor argument by parameter index. | +| `[Argument(string name)]` | Constructor argument by parameter name. | +| `[Argument(..., DefaultValue = ...)]` | Fallback when the constructor argument is not present. | +| `[NestedModel]` | Populated by recursively calling `FromAttributeData` on a nested generated model. | +| `[Exclude]` | Skips auto-discovery for this parameter. | +| `[GenericTypeArgument]` | Populated from a generic type argument of the attribute class. | +| `[GenericTypeArgument(int index)]` | Generic type argument by position. | +| `[GenericTypeArgument(string name)]` | Generic type argument by type parameter name. | + +## Manual mapping + +```csharp +using Microsoft.CodeAnalysis; +using Purview.SourceGeneratorFramework.Generators; +using System.ComponentModel.DataAnnotations; + +namespace MySourceGenerator.Models; + +[Generate(typeof(RequiredAttribute))] +public readonly partial record struct RequiredAttributeData( + bool AllowEmptyStrings +); +``` + +Generated output: + +```csharp +readonly record struct RequiredAttributeData(bool Exists, bool AllowEmptyStrings) +{ + public static readonly RequiredAttributeData Empty = new(false, default(bool)); + + public static RequiredAttributeData FromAttributeData(ImmutableArray attributes) + { + // ... + } + + public static RequiredAttributeData FromAttributeData(AttributeData attributeData) + { + if (!TargetAttribute.Equals(attributeData.AttributeClass)) + return Empty; + + attributeData.TryGetNamedArgument("AllowEmptyStrings", out var allowEmptyStrings); + return new(true, allowEmptyStrings); + } +} +``` + +## String target names + +When the attribute type is not referenced in the generator project, pass the fully-qualified name as +a string. This is useful for attributes newer than the generator's target framework (e.g. +`LengthAttribute` in .NET 8+) or attributes that are generated by the same generator: + +```csharp +[Generate("System.ComponentModel.DataAnnotations.RequiredAttribute")] +public readonly partial record struct RequiredAttributeData( + bool AllowEmptyStrings +); +``` + +A plain type name (`"RequiredAttribute"`) can also be used, which matches an attribute in the global +namespace or in any namespace. `AutoDiscover` requires the real `Type` overload because it must +inspect the attribute's constructors and properties. + +## Constructor arguments + +```csharp +[Generate(typeof(LengthAttribute))] +public readonly partial record struct LengthAttributeData( + [Argument(0)] int MinimumLength, + [Argument(1)] int MaximumLength +); +``` + +Or by constructor parameter name: + +```csharp +[Generate(typeof(StringLengthAttribute))] +public readonly partial record struct StringLengthAttributeData( + [Argument("maximumLength", DefaultValue = 2147483647)] int MaximumLength, + int MinimumLength +); +``` + +## Nested models + +Any property whose type is itself annotated with `[Generate]` can be populated as a nested model. +This is useful for shared base attribute data, such as `ValidationAttribute` in +`System.ComponentModel.DataAnnotations`: + +```csharp +[Generate(typeof(ValidationAttribute), MatchByInheritance = true)] +public readonly partial record struct ValidationAttributeData( + [Property] string? ErrorMessage, + [Property] string? ErrorMessageResourceName, + [Property] ITypeSymbol? ErrorMessageResourceType +); + +[Generate(typeof(RequiredAttribute))] +public readonly partial record struct RequiredAttributeData( + bool AllowEmptyStrings, + [NestedModel] ValidationAttributeData ValidationAttribute +); +``` + +Because `ValidationAttributeData` uses `MatchByInheritance = true`, it matches any attribute that +derives from `ValidationAttribute`, including `RequiredAttribute`. + +## Generic type arguments + +If the attribute class is generic, a record parameter can be populated from the attribute's type +argument: + +```csharp +[Generate(typeof(MyGenericAttribute<>))] +public readonly partial record struct MyGenericAttributeData( + [GenericTypeArgument] T Value +); +``` + +Use `[GenericTypeArgument(0)]` or `[GenericTypeArgument("TValue")]` to disambiguate when the +attribute has multiple type parameters. + +## Auto-discovery + +For simple attributes you can let the generator discover all constructor parameters and public named +properties automatically: + +```csharp +[Generate(typeof(RequiredAttribute), AutoDiscover = true)] +public readonly partial record struct RequiredAttributeData; +``` + +This generates the same `RequiredAttributeData` as the manual example above. Nested models are not +auto-discovered; declare them explicitly if needed. + +## Default values + +`DefaultValue` provides a runtime fallback when the attribute does not contain the requested property +or argument. The `Empty` sentinel always uses `default(T)` for every property (including an `Exists` +field set to `false`): + +```csharp +[Generate(typeof(HostKitAttribute))] +public readonly partial record struct HostKitAttributeData( + [Argument("name", DefaultValue = "MyApp")] string Name, + [Argument("generateOptions", DefaultValue = true)] bool GenerateOptions +); +``` + +## Type library integration + +A `[TypeRef]` member declared with `GenerateFullNameConst` produces a `public const string +{Member}FullName`, which can be used as the `[Generate]` target of an attribute-data model instead of +a `typeof(...)` value — see [Type-Library.md](Type-Library.md#using-full-name-constants-as-attribute-data-model-targets) +for the full example. + +Because the `TypeLibrary` class is emitted through `TypeLibraryGenerator`'s main pipeline, its +constants are **not** present in the compilation that `AttributeDataModelGenerator`'s +`ForAttributeWithMetadataName` pipeline sees (only post-initialization output is shared between +generators in a single pass). `AttributeDataModelGenerator` therefore reassembles the target from the +argument's member-access expression — guarded so the root identifier must match a +`[GenerateTypeLibrary]` spec's `ClassName` — and resolves it against the compilation. + +For `[Argument]`/`[Property]` members marked `IsEnum = true`, a `DefaultValue` supplied as a **bare +member name** (for example `"Inherit"`) is expanded to the fully-qualified +`"{EnumFullName}.{Member}"` form using the enum type of the target attribute's matching constructor +parameter (for `[Argument]`) or property (for `[Property]`). Fully-qualified defaults and defaults +whose enum type cannot be resolved are emitted unchanged. + +## License + +This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project. \ No newline at end of file diff --git a/docs/code-writer.md b/docs/wiki/Code-Writer.md similarity index 99% rename from docs/code-writer.md rename to docs/wiki/Code-Writer.md index ff1d39a..fbcecf4 100644 --- a/docs/code-writer.md +++ b/docs/wiki/Code-Writer.md @@ -28,7 +28,7 @@ when it was opened: - Pass `throwOnUnclosedScopes: false` explicitly when a test intentionally materializes partial output. Scope tracking has a real cost — every scope open captures a `StackTrace` and allocates a per-scope -record — which is why production leaves it off (see [docs/performance.md](performance.md)). +record — which is why production leaves it off (see [Performance.md](Performance.md)). ## Primitives @@ -581,6 +581,6 @@ writer.Property("Name", TypeReference.Create(), TypeDeclarationAccessibi ## Samples -The [`SourceGeneratorFramework.ExampleGenerator`](../src/src/SourceGeneratorFramework.ExampleGenerator) +The [`SourceGeneratorFramework.ExampleGenerator`](../../src/src/SourceGeneratorFramework.ExampleGenerator) reference implementation demonstrates these APIs end-to-end, including the `CodeWriterSampleGenerator`, which compiles a best-practice sample class for every `[GenerateCodeWriterSample]` target. diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md new file mode 100644 index 0000000..d4c89b2 --- /dev/null +++ b/docs/wiki/Getting-Started.md @@ -0,0 +1,183 @@ +# Getting Started + +## Install + +```bash +dotnet add package Purview.SourceGeneratorFramework +``` + +Reference the package from a Roslyn source generator project: + +```xml + + + netstandard2.0 + true + true + + + + + + + + +``` + +## Referencing a generator project + +Roslyn must receive both a source-generator assembly and its framework runtime dependency as +analyzer inputs. Use an analyzer project reference: + +```xml + +``` + +The Purview SDK automatically invokes `GetSourceGeneratorAnalyzerFiles`, which returns both the +generator and its framework dependency without adding either file to the consuming application's +runtime references. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly remains +supported but is not required. + +### Referencing a generator from its test project + +A test project can need the source-generator project in two different roles at the same time: + +- as an analyzer, so the generator runs against the test project and its generated attributes and + other types can be used directly by test source files; and +- as a normal assembly reference, so the test code can name and instantiate the generator type + through `Purview.SourceGeneratorFramework.Testing`. + +Add two project references with deliberately different metadata: + +```xml + + + + + + + +``` + +Do not put `OutputItemType="Analyzer"` on the normal reference. The Purview SDK automatically +uses `GetSourceGeneratorAnalyzerFiles` for the analyzer reference and supplies the generator's +runtime dependencies to Roslyn. + +Because the second reference is a normal assembly reference, the generator's Roslyn dependencies +also become visible to the test compilation. For a multi-target test project, build the generator +against the Roslyn version that supports its API usage and is compatible with the oldest test target. +This framework is built against Roslyn 5.0 (C# 14 / .NET 10 generation), which ships `net8.0` and +`net9.0` package assets, so a `.NET 8`–`.NET 10` test matrix still loads it. Compiler hosts must be +Roslyn 5.0 or later (`.NET 10` SDK / Visual Studio 2026). Do not centrally pin +`System.Collections.Immutable` to a newer runtime version merely to make the generator load. + +## Write a generator + +Implement `IIncrementalGenerator` and use the framework helpers to build a pipeline: + +```csharp +using Microsoft.CodeAnalysis; +using Purview.SourceGeneratorFramework.Helpers; +using Purview.SourceGeneratorFramework.Models; + +[Generator] +public sealed class MyGenerator : IIncrementalGenerator +{ + static readonly TypeIdentity AttributeType = new("MyAttribute", "MyNamespace"); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var contextProvider = IncrementalPipeline.DefaultGenerationContextValueProvider(context); + + var targets = IncrementalPipeline.ForAttributeWithMetadataName( + context, + AttributeType, + static (ctx, ct) => ctx.TargetSymbol.Name + ); + + context.RegisterSourceOutput( + targets.CombineWithContext(contextProvider), + static (spc, pair) => + { + var (name, generationContext) = pair; + var writer = generationContext.CreateCodeWriter(); + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace("MyNamespace"); + writer.Class( + name, + TypeDeclarationAccessibility.Public, + options => options with { IsStatic = true }, + body => body.Comment("generated content") + ); + + spc.AddSource($"{name}.g.cs", writer.ToString()); + } + ); + } +} +``` + +See the [`SourceGeneratorFramework.ExampleGenerator`](../../src/src/SourceGeneratorFramework.ExampleGenerator) +reference implementation for a complete end-to-end sample, and +[`SourceGeneratorFramework.ExampleGenerator.CodeFixers`](../../src/src/SourceGeneratorFramework.ExampleGenerator.CodeFixers) +for a companion code-fix sample. + +## Test the generator + +Reference the testing package and run the generator against a snippet of C#: + +```bash +dotnet add package Purview.SourceGeneratorFramework.Testing +``` + +```csharp +using Purview.SourceGeneratorFramework.Testing; + +public class MyGeneratorTests +{ + [Test] + public async Task GeneratesExpectedSource() + { + var source = """ + [MyNamespace.MyAttribute] + public partial class MyClass { } + """; + + var runner = new SourceGeneratorTestRunner(); + var result = await runner.RunAsync(source); + + result.AssertNoCompilationErrors(); + var generated = result.AssertSingleGeneratedSource(); + } +} +``` + +Use the TUnit integration for ready-made test base classes and fluent assertions: + +```bash +dotnet add package Purview.SourceGeneratorFramework.Testing.TUnit +``` + +## Next pages + +- [Source Generator & Analyser Best Practices](Guide.md) +- [CodeWriter structured API reference](Code-Writer.md) +- [Incremental Pipeline](Incremental-Pipeline.md) +- [Testing](Testing.md) +- [Testing with TUnit](Testing-TUnit.md) +- [Step-Cache Tests](Step-Cache-Tests.md) +- [Packaging](Packaging.md) \ No newline at end of file diff --git a/docs/guide.md b/docs/wiki/Guide.md similarity index 99% rename from docs/guide.md rename to docs/wiki/Guide.md index c23c59d..0e43538 100644 --- a/docs/guide.md +++ b/docs/wiki/Guide.md @@ -1,13 +1,3 @@ ---- -created: 2026-08-29 -updated: 2026-08-29 -tags: - - source-generator - - analyser - - roslyn - - best-practices ---- - # Source Generator & Analyser Best Practices > Practical guidance for writing Roslyn analysers and incremental source generators that remain fast, deterministic, cache-friendly, IDE-compatible, and safe to distribute. @@ -1294,7 +1284,7 @@ await Assert.That(result.Runs[1]).StepIsModified("GetGenerationConfiguration"); Assertions on `IncrementalCacheRun` (`AllStepsNew`, `AllStepsCachedOrUnchanged`, `StepIsCached`, `StepIsModified`, `HasStepReason`) plus `GetStepReasons()` cover the golden matrix. See -[docs/step-cache-tests.md](step-cache-tests.md) for the full walkthrough and the canonical +[Step-Cache-Tests.md](Step-Cache-Tests.md) for the full walkthrough and the canonical `StepCacheTests.cs` sample in the ExampleGenerator unit tests. --- diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..ee2fd6b --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,80 @@ +# SourceGenerator Framework Wiki + +This wiki is the project documentation hub for **Purview.SourceGeneratorFramework** — a strongly typed +framework for building, testing, and maintaining incremental C# source generators with Roslyn. It +includes structured code generation (`CodeWriter`), incremental pipeline helpers, attribute data +models, type libraries, a step-cache test runner for verifying incremental behaviour, and bundled +analysers that guide generators back to best practice. + +## Start here + +- [Getting Started](Getting-Started.md) +- [Source Generator & Analyser Best Practices](Guide.md) +- [CodeWriter structured API reference](Code-Writer.md) +- [TypeLibraryGenerator](Type-Library.md) +- [Attribute Data Models](Attribute-Data-Models.md) +- [Incremental Pipeline](Incremental-Pipeline.md) +- [Analyzers](Analyzers.md) +- [Testing](Testing.md) +- [Testing with TUnit](Testing-TUnit.md) +- [Step-Cache Tests](Step-Cache-Tests.md) +- [Packaging](Packaging.md) +- [Performance](Performance.md) +- [Release Flow](Release-Flow.md) + +## Packages + +| Package | Description | Packable | +| --- | --- | --- | +| [`Purview.SourceGeneratorFramework`](../../src/src/SourceGeneratorFramework) | Core helpers, models, and MSBuild integration for writing incremental source generators. | Yes | +| [`Purview.SourceGeneratorFramework.Testing`](../../src/src/SourceGeneratorFramework.Testing) | Framework-agnostic test runner and assertions for source generator unit tests. | Yes | +| [`Purview.SourceGeneratorFramework.Testing.TUnit`](../../src/src/SourceGeneratorFramework.Testing.TUnit) | TUnit-specific test base classes and assertions for source generator tests. | Yes | +| [`Purview.SourceGeneratorFramework.Generators`](../../src/src/SourceGeneratorFramework.Generators) | Internal Roslyn source generator used by the framework package. | No | +| [`Purview.SourceGeneratorFramework.ExampleGenerator`](../../src/src/SourceGeneratorFramework.ExampleGenerator) | Reference implementation showing how to build a generator with the framework. | No | + +## Feature highlights + +- **`CodeWriter`** — an allocation-conscious writer for generated C# source with indentation, + namespace/type declarations, structured statements, XML documentation, conditional compilation + blocks, and deterministic output. Declarations and statements are structured values rather than raw + text; see [Code-Writer.md](Code-Writer.md). +- **`IncrementalPipeline`** — extension methods for composing `IncrementalValueProvider` / + `IncrementalValuesProvider` pipelines, including attribute-based discovery, generation-context + creation, and disable-property checks; see [Incremental-Pipeline.md](Incremental-Pipeline.md). +- **`GenerationContext`** — a base execution-services context carrying the Roslyn `Compilation`, + immutable generator settings, optional logging, and a factory for independently owned `CodeWriter` + instances. +- **`GeneratorResult`** — a value-or-diagnostics result type for incremental transforms, with + explicit per-diagnostic `IsBlocking` control over whether generation continues. +- **`AttributeDataModelGenerator`** — bundled generator that emits `readonly record struct` attribute + parser models from `[Generate]` declarations; see [Attribute-Data-Models.md](Attribute-Data-Models.md). +- **`TypeLibraryGenerator`** — generates a self-contained `public static partial` type library from a + small declarative spec; see [Type-Library.md](Type-Library.md). +- **Bundled analysers and code fixes** — `PSGFR11`–`PSGFR38` diagnostics for Roslyn best practice, + plus code fixes; see [Analyzers.md](Analyzers.md). +- **Testing framework** — `SourceGeneratorTestRunner`, `CodeQuery` syntax-node + assertions, refactoring tests, and incremental cache tests; see [Testing.md](Testing.md) and + [Testing-TUnit.md](Testing-TUnit.md). +- **Step-cache tests** — prove a generator caches correctly stage-by-stage; see + [Step-Cache-Tests.md](Step-Cache-Tests.md). + +## Requirements + +- .NET SDK 10.0 or later to build the framework. +- The framework is built against Roslyn 5.0 (`Microsoft.CodeAnalysis` 5.x), so compiler hosts that + load the generator, analyser, and testing assemblies must be Roslyn 5.0 or later (`.NET 10` SDK / + Visual Studio 2026 18.0). +- Source generators target `netstandard2.0`; test projects target `net8.0`, `net9.0`, and `net10.0`. + +## Repository layout + +- `src/src/SourceGeneratorFramework` — core framework package. +- `src/src/SourceGeneratorFramework.Generators` — bundled generators (attribute data models, type + library), shipped inside the core package. +- `src/src/SourceGeneratorFramework.Analyzers` — bundled analysers. +- `src/src/SourceGeneratorFramework.CodeFixers` — bundled code fix providers. +- `src/src/SourceGeneratorFramework.Testing` — framework-agnostic testing package. +- `src/src/SourceGeneratorFramework.Testing.TUnit` — TUnit testing integration. +- `src/src/SourceGeneratorFramework.ExampleGenerator` — reference generator implementation. +- `src/src/SourceGeneratorFramework.Benchmarks` — BenchmarkDotNet benchmarks (see + [Performance.md](Performance.md)). \ No newline at end of file diff --git a/docs/wiki/Incremental-Pipeline.md b/docs/wiki/Incremental-Pipeline.md new file mode 100644 index 0000000..76e15f2 --- /dev/null +++ b/docs/wiki/Incremental-Pipeline.md @@ -0,0 +1,268 @@ +# Incremental Pipeline + +`IncrementalPipeline` provides extension methods for composing `IncrementalValueProvider` and +`IncrementalValuesProvider` pipelines — attribute-based discovery, generation-context creation, +disable-property checks, and thin source-output registration. It is designed around the golden rule +from the [best-practices guide](Guide.md): **pipeline values must be immutable and value-equatable.** + +## GenerationContext + +`GenerationContext` is a base execution-services context that carries: + +- the Roslyn `Compilation`; +- immutable generator `GenerationSettings`; +- an optional `ISourceGenLogger`; and +- a factory for independently owned `CodeWriter` instances. + +```csharp +var contextProvider = IncrementalPipeline.DefaultGenerationContextValueProvider(context); +``` + +Create a fresh writer through the generation context so it inherits the configuration: + +```csharp +var writer = generationContext.CreateCodeWriter(); +``` + +`CreateCodeWriter()` returns a new, independently owned instance on every call. The writer is not +stored on `GenerationContext`; keep it scoped to the source-output operation that owns the generated +source. + +### Custom generation contexts + +Custom contexts do not need to accept or read build properties themselves: + +```csharp +public sealed class MyGenerationContext : GenerationContext +{ + public MyGenerationContext( + Compilation compilation, + GenerationSettings settings, + ISourceGenLogger? logger) + : base(compilation, settings, logger) + { + } +} +``` + +Use the ordinary context-provider overload. The framework combines the compiler-visible property +with the compilation and supplies the resulting immutable settings to the custom context factory: + +```csharp +var contextProvider = IncrementalPipeline.GenerationContextValueProvider( + context, + nameof(MyGenerator), + "1.0.0", + factory: static (compilation, settings, logger, cancellationToken) => + { + cancellationToken.ThrowIfCancellationRequested(); + return new MyGenerationContext(compilation, settings, logger); + }, + disablePropertyName: "MyGenerator_Disable" +); +``` + +The provider resolves scope validation, generator disabling, and test logging from analyzer-config +properties before invoking the factory. The supplied logger is created internally only when logging +is enabled and a sink is registered for that run. + +## Keep CodeWriter out of incremental contexts + +Treat `GenerationContext` values as cached incremental-pipeline state and each `CodeWriter` as +mutable, output-scoped execution state. Create the writer inside the registered source-output +callback, after the incremental cache boundary. Creating it in the callback and passing it to +emitter/helper methods called from that same callback is the intended pattern; the only thing that +is forbidden is persisting the writer in pipeline state, where Roslyn caches it: + +```csharp +IncrementalPipeline.RegisterSourceOutput( + context, + targets, + contextProvider, + static (spc, target, generationContext) => + { + var writer = generationContext.CreateCodeWriter(); + EmitTarget(generationContext, writer, target); + spc.AddSource($"{target.Name}.g.cs", writer.ToString()); + } +); +``` + +This separation is intentional: + +- Roslyn caches the complete value published by an incremental provider. It does not provide a way + to exclude one property of that value from caching. +- `CodeWriter` is mutable. Caching one can retain previously written source when the context is + reused for another output or generator run. +- Source-output callbacks may process independent targets concurrently. Sharing a writer can mix + their output and introduce data races. +- A fresh writer gives each generated source independent scope tracking and deterministic ownership. + +These rules also apply to custom contexts: **never add or assign a `CodeWriter` property or field on +a class derived from `GenerationContext`**. A custom context is still produced by an incremental +provider and cached as one complete value. Store only compilation-derived services and immutable +configuration there, and call `CreateCodeWriter()` in the output callback. + +When emitter methods need both logging/context services and writing, either pass the context and +output-scoped writer separately, or compose them into a short-lived output wrapper created inside +the callback. Such a wrapper must never be returned from an incremental provider: + +```csharp +public sealed class GenerationOutputContext : ISourceGenLogger + where TContext : GenerationContext +{ + public GenerationOutputContext(TContext generation) + { + Generation = generation; + Writer = generation.CreateCodeWriter(); + } + + public TContext Generation { get; } + public CodeWriter Writer { get; } + + public void Log( + SourceGenLogLevel level, + int indentation, + string message, + params object[] args) => + Generation.Log(level, indentation, message, args); +} +``` + +The wrapper reduces emitter parameter noise without extending the writer's lifetime into Roslyn's +incremental cache. + +## GeneratorResult and diagnostics that don't stop generation + +`IncrementalPipeline.RegisterSourceOutput` combines targets with the generation context, reports +diagnostics, and runs the generator callback only for successful results: + +```csharp +var targets = IncrementalPipeline.ForAttributeWithMetadataName( + context, + AttributeType, + static (ctx, ct) => + { + var symbol = ctx.TargetSymbol; + return symbol is null + ? GeneratorResult.Empty + : GeneratorResult.Create(symbol.Name); + } +); + +var contextProvider = IncrementalPipeline.DefaultGenerationContextValueProvider(context); + +IncrementalPipeline.RegisterSourceOutput( + context, + targets, + contextProvider, + static (spc, name, generationContext) => + { + var writer = generationContext.CreateCodeWriter(); + writer.Comment($"generated {name}"); + spc.AddSource($"{name}.g.cs", writer.ToString()); + } +); +``` + +The registered callback runs only when `GeneratorResult.ShouldProcess` is `true` — the result +carries a value and none of its carried diagnostics are blocking. + +`ReportableDiagnostic.IsBlocking` is an explicit, per-diagnostic decision, independent of the +diagnostic's severity. `GeneratorResult.ShouldProcess` is `true` when the result carries a value +and none of its diagnostics are blocking, so an `Error`-severity diagnostic can still allow +generation to continue. This is useful when the generated code helps the developer fix the problem — +for example, a generator that emits an abstract base class with methods the user must override can +report an error for each missing override while still emitting the base class, so the user can see +exactly what to implement: + +```csharp +static readonly DiagnosticDescriptor MissingOverride = new( + "MYGEN001", + "Missing override", + "Type '{0}' must override '{1}'", + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true +); + +var targets = IncrementalPipeline.ForAttributeWithMetadataName( + context, + AttributeType, + static (ctx, ct) => + { + var symbol = ctx.TargetSymbol; + var model = new BaseModel(symbol.Name); + + // An error-severity diagnostic that explicitly allows generation to continue: + // IsBlocking is false, so ShouldProcess stays true and the base class is emitted. + var diagnostic = ReportableDiagnostic.Create( + MissingOverride, + isBlocking: false, + symbol, + symbol.Name, + "Execute" + ); + + return GeneratorResult.Create(model, diagnostic); + } +); +``` + +Blocking diagnostics (`isBlocking: true`) stop generation for that target while still being reported. +`GeneratorResult.HasBlockingDiagnostics` reports whether any carried diagnostic blocked processing; +`HasErrorDiagnostics` reports the severity-based view (whether any diagnostic has an `Error` +`DefaultSeverity`). + +## Disabling a generator at build time + +Pass the generator's compiler-visible disable property to the context provider. Its resolved value is +included in `GenerationSettings` automatically: + +```xml + + true + +``` + +```csharp +var contextProvider = IncrementalPipeline.DefaultGenerationContextValueProvider( + context, + nameof(MyGenerator), + "1.0.0", + disablePropertyName: "MyGenerator_Disable" +); + +// In the output stage: +if (generationContext.Settings.IsSourceGeneratorDisabled) + return; +``` + +`IsDisabledValueProvider` remains available when expensive upstream transforms must be filtered +before they are combined with the generation context. + +## Scope validation + +The default generation-context provider reads the +`PurviewSourceGeneratorFrameworkValidateCodeWriterScopes` MSBuild property and threads it into +`GenerationSettings.ValidateCodeWriterScopes`. When enabled, `ToString()` throws +`CodeWriterScopeValidationException` if `OpenScopeCount` is not zero. See +[Code-Writer.md](Code-Writer.md#construction-and-scope-validation). + +## Test logging + +Framework logging is disabled in ordinary compiler runs. The testing integration enables it by +registering an isolated sink and supplying a per-run session ID through analyzer config. Context +providers create the internal logger automatically; generators do not implement a logging interface +and no logging-support source is generated. + +The sink registry stores callbacks only. It never buffers log entries. If logging is disabled, the +session ID is missing, or no matching sink is registered, the provider supplies no logger and log +calls are discarded without storing entries. Test sinks own any entries they choose to capture and +are removed when the test run completes. + +## Tracking names and step-cache tests + +The framework's pipeline helpers assign a tracking name to every stage so cache tests can assert +which stages were recomputed. See [Step-Cache-Tests.md](Step-Cache-Tests.md) for the tracking-name +table and the golden test matrix. \ No newline at end of file diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md new file mode 100644 index 0000000..e6ccbb6 --- /dev/null +++ b/docs/wiki/Packaging.md @@ -0,0 +1,159 @@ +# Packaging + +This page covers how to package a source generator that references +`Purview.SourceGeneratorFramework`, and how the framework package itself is assembled and validated. + +## How the framework packages are assembled + +`Purview.SourceGeneratorFramework` is dual-role: the built framework assembly ships in `lib/` so +consumers can compile generators against it, and the `analyzers/` folder carries the generator + +analyzer assemblies and their runtime dependencies. The bundled projects are: + +- `SourceGeneratorFramework.Generators` — `AttributeDataModelGenerator`, `TypeLibraryGenerator`; +- `SourceGeneratorFramework.Analyzers` — the `PSGFR*` and `TLB*` analyzers; +- `SourceGeneratorFramework.CodeFixers` — the code fix providers; +- `SourceGeneratorShared` — shared models and helpers, packed into the package as + `Purview.SourceGeneratorFramework.Shared.dll`. + +These projects are `IsRoslynComponent = true` and are **not** packable on their own; they are packed +into the main package by the `SourceGeneratorFramework` project via analyzer project references +(`OutputItemType="Analyzer"`). + +The repo's pack validation (`purview-build.json`) requires the `purview.sourcegeneratorframework` +package to contain, at minimum: + +- `lib/netstandard2.0/Purview.SourceGeneratorFramework.dll` and + `lib/netstandard2.0/Purview.SourceGeneratorFramework.Shared.dll`; +- `analyzers/dotnet/cs/` versions of the framework, generators, analyzers, code fixers, and shared + assemblies; +- `build/Purview.SourceGeneratorFramework.props` and `build/Purview.SourceGeneratorFramework.targets`; +- `README.md`, `LICENSE.md`, and `purview-logo.png`. + +PDBs are delivered only through the `.snupkg`; `*.pdb` files are forbidden inside the `.nupkg`. + +## Referencing a generator from a consuming project + +Use an analyzer project reference so Roslyn receives both the generator assembly and its framework +runtime dependency: + +```xml + +``` + +The Purview SDK automatically invokes `GetSourceGeneratorAnalyzerFiles`, which returns both the +generator and its framework dependency without adding either file to the consuming application's +runtime references. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly remains +supported but is not required. + +### Generators embedded in another package + +If the generator assembly is embedded in a different NuGet package, the outer package must make the +framework's compiler-visible properties visible to its consumers. Build assets from +`Purview.SourceGeneratorFramework` are not automatically copied into the outer package. Include a +`.props` file imported by the outer package that declares the property and its +`CompilerVisibleProperty` entry (see +[Code-Writer.md](Code-Writer.md#generators-embedded-in-another-package)), and pack it using the outer +package's ID so NuGet imports it automatically: + +```xml + +``` + +## Roslyn version compatibility + +The most important packaging rule is: + +> **The version of `Microsoft.CodeAnalysis.*` used to compile your analyzer/generator establishes a +> minimum compiler-host API requirement.** + +The consumer's `` does not determine analyzer compatibility. Analyzer/generator code +executes inside a compiler/IDE host. Microsoft's published baseline for the framework's Roslyn +generation is: + +| Roslyn package | Minimum Visual Studio | Language / .NET generation | +| ---: | --- | --- | +| 4.8 | VS 2022 17.8 | C# 12 / .NET 8 | +| 4.12 | VS 2022 17.12 | C# 13 / .NET 9 | +| 5.0 | VS 2026 18.0 | C# 14 / .NET 10 | + +> **This framework is built against Roslyn 5.0.** The generator, analyzer, and testing assemblies in +> `Purview.SourceGeneratorFramework*` are compiled against `Microsoft.CodeAnalysis` 5.x, so compiler +> hosts that load them must be Roslyn 5.0 or later (`.NET 10` SDK / Visual Studio 2026 18.0). The +> testing packages multi-target `net8.0`–`net10.0`; Roslyn 5.x ships `net8.0`/`net9.0` package assets, +> so those test targets still load the test runner. + +See [Guide.md](Guide.md) sections 14–18 for the full discussion of Roslyn versioning, multi-version +packaging strategies, and the recommended generator project configuration. + +## Recommended generator project configuration + +A broadly-compatible generator project might start with: + +```xml + + + + netstandard2.0 + + latest + enable + + true + false + + true + + true + + + + + + + + + + + + + + + + + +``` + +Then centrally define: + +```xml + + 4.8.0 + 5.9.0 + +``` + +The exact Roslyn baseline is a product-support decision. + +## License + +This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project. \ No newline at end of file diff --git a/docs/performance.md b/docs/wiki/Performance.md similarity index 97% rename from docs/performance.md rename to docs/wiki/Performance.md index f733ba3..d5e99ee 100644 --- a/docs/performance.md +++ b/docs/wiki/Performance.md @@ -1,7 +1,7 @@ # Performance Benchmark results are produced by the benchmarks project -([`SourceGeneratorFramework.Benchmarks`](../src/src/SourceGeneratorFramework.Benchmarks)) using +([`SourceGeneratorFramework.Benchmarks`](../../src/src/SourceGeneratorFramework.Benchmarks)) using [BenchmarkDotNet](https://benchmarkdotnet.org) and folded here for reference. ## What is measured @@ -11,7 +11,7 @@ All benchmarks measure the **production** code path: generator runs configure Scope tracking is a testing/debug feature and is excluded here because capturing an opening `StackTrace` per scope dominates both time and allocation (for 1000 small classes it inflates the writer benchmark from ~1.6 ms/2.3 MB to ~19 ms/22 MB). Tests opt into it so an unclosed `using` or -block fails fast; see [docs/code-writer.md](code-writer.md#construction-and-scope-validation). +block fails fast; see [Code-Writer.md](Code-Writer.md#construction-and-scope-validation). ## Environment diff --git a/docs/wiki/Release-Flow.md b/docs/wiki/Release-Flow.md new file mode 100644 index 0000000..fe21768 --- /dev/null +++ b/docs/wiki/Release-Flow.md @@ -0,0 +1,81 @@ +# Release Flow + +This page documents how the repository builds, tests, packs, and releases +`Purview.SourceGeneratorFramework`. + +## Versioning + +The current version lives in the repository-root `package.json`: + +```json +{ + "name": "purview-sourcegeneratorframework", + "version": "1.0.0-prerelease.42" +} +``` + +The version is read by the build tooling (for example `just version` runs +`bun -p "require('./package.json').version"`), and GitHub releases are tagged `v`, e.g. +`v1.0.0-prerelease.42`. + +## Workflows + +### Pull requests + +`.github/workflows/pr.yml` runs on `pull_request` to `main`. It calls the shared +`purview-dev/build` workflow (`purview-build.yml`) with `run-pack: true` and `validate-pack: true`, so +every PR restores, builds, lints, runs tests, packs, and validates the packages. + +### Releases + +`.github/workflows/release.yml` runs on `push` to `main`. It calls the shared +`purview-dev/build` workflow (`purview-release.yml`) with `release-mode: NuGet`, which builds, tests, +packs, validates, publishes to NuGet, and creates the GitHub release. + +## Local pipelines + +The `Justfile` wraps the shared `Purview.Build` pipeline (installed as a pinned dotnet tool to +`.tools/purview-build/purview-build`): + +| Recipe | Pipeline mode | Purpose | +| --- | --- | --- | +| `just pipeline-pr` | default | Restore, build, lint, tests. | +| `just pipeline-build` | `--Build:RunTests=false --Release:Mode=None` | Build-only pipeline. | +| `just pipeline-tests` | `--Build:RunTests=true --Release:Mode=None` | Build with tests. | +| `just pipeline-release` | `--Release:Mode=NuGet` | Full release: build, test, pack, publish. | +| `just pipeline-local-release` | `--Release:Mode=LocalNuGet` | Build, test, pack, and publish to a local NuGet feed. | + +Convenience recipes also exist for building (`just build`), testing (`just test`, `just test-unit`), +packing (`just pack`), benchmarking (`just benchmark`), linting (`just lint-check`/`just lint-fix`), +and cleaning (`just clean`, `just scrub`). + +## Pack validation + +`purview-build.json` configures pack validation: + +- `PackValidation.RequireSymbolPackage` and `RequireSymbolFiles` — every packable package must ship a + `.snupkg` with symbol files. +- `PackValidation.RequiredContent` — each package must contain its declared assets. For example, + `purview.sourcegeneratorframework` must contain the `lib/netstandard2.0/` framework assembly and + shared assembly, the `analyzers/dotnet/cs/` generator/analyzer/code-fixer/shared assemblies, the + `build/Purview.SourceGeneratorFramework.props` and `.targets` files, `README.md`, `LICENSE.md`, and + `purview-logo.png`. See [Packaging.md](Packaging.md) for details. +- `PackValidation.ForbiddenContent` — `*.pdb` files are forbidden inside the `.nupkg` (PDBs are + delivered only through the `.snupkg`). + +## Dependency management + +`Directory.Packages.props` centralises package versions: + +- `Microsoft.CodeAnalysis.CSharp` / `Microsoft.CodeAnalysis.CSharp.Workspaces` — Roslyn 5.x + (`RoslynCompilerVersion`, currently `[5.9.0,)`). +- `Microsoft.CodeAnalysis.Analyzers` — `RoslynAnalyzersVersion` `[5.9.0,)`. +- `TUnit` / `TUnit.Core` / `TUnit.Assertions` / `TUnit.Mocks` — `[1.67.0,)`. +- `System.Reflection.MetadataLoadContext` — used by the testing package for the metadata-only + `CompilationResult` view. + +Central Package Management is enabled with `CentralPackageTransitivePinningEnabled`. + +## License + +This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project. \ No newline at end of file diff --git a/docs/step-cache-tests.md b/docs/wiki/Step-Cache-Tests.md similarity index 100% rename from docs/step-cache-tests.md rename to docs/wiki/Step-Cache-Tests.md diff --git a/docs/wiki/Testing-TUnit.md b/docs/wiki/Testing-TUnit.md new file mode 100644 index 0000000..a2c2fbc --- /dev/null +++ b/docs/wiki/Testing-TUnit.md @@ -0,0 +1,197 @@ +# Testing with TUnit + +`Purview.SourceGeneratorFramework.Testing.TUnit` is the TUnit integration for testing incremental C# +source generators built with `Purview.SourceGeneratorFramework`. + +## Installation + +```bash +dotnet add package Purview.SourceGeneratorFramework.Testing.TUnit +``` + +## What's included + +- **`TUnitSourceGeneratorTestBase`** — ready-made base class for TUnit tests. It wires + generator log output to `TestContext.Current.OutputWriter`. +- **Custom TUnit assertions** for inspecting `DriverRunResult` instances directly in TUnit tests. +- **MSBuild `.props`** — automatically adds `global using` directives for + `Purview.SourceGeneratorFramework.Testing.TUnit` and + `Purview.SourceGeneratorFramework.Testing.TUnit.Assertions`. + +## Usage + +Reference the package from a TUnit test project: + +```xml + + + + +``` + +Derive your test class from `TUnitSourceGeneratorTestBase` and use the inherited +`GenerateAsync` method: + +```csharp +using Purview.SourceGeneratorFramework.Testing.TUnit; + +public class MyGeneratorTests : TUnitSourceGeneratorTestBase +{ + [Test] + public async Task GeneratesExpectedSource() + { + var source = """ + [MyNamespace.MyAttribute] + public partial class MyClass { } + """; + + var result = await GenerateAsync(source); + + result.AssertNoCompilationErrors(); + var generated = result.AssertSingleGeneratedSource(); + + await Assert.That(generated).Contains("public static partial class MyClass"); + } +} +``` + +The base class also provides access to the underlying `SourceGeneratorTestRunner` behavior +through `GenerateAsync`. + +## Using generated types in the TUnit project + +If test source files use generated attributes or other generated declarations while the tests also +derive from `TUnitSourceGeneratorTestBase`, reference the generator project both as an +analyzer and as a normal assembly: + +```xml + + + + + + + +``` + +For example, the analyzer reference allows a test fixture to use `[MyGeneratedAttribute]`, while the +normal reference allows the test class to derive from `TUnitSourceGeneratorTestBase`. Do +not add `OutputItemType="Analyzer"` to the normal reference. + +For multi-target TUnit projects, the normal reference means the generator's Roslyn dependencies +participate in reference resolution for every target. Build the generator against the Roslyn version +that supports its API usage; this framework is built against Roslyn 5.0, which ships `net8.0` and +`net9.0` package assets, so a .NET 8–10 test matrix still loads it. Compiler hosts that consume the +generator as an analyzer must be Roslyn 5.0 or later (`.NET 10` SDK / Visual Studio 2026). Do not +force a newer `System.Collections.Immutable` version through central package management. + +## Which base class and method + +| Roslyn type | Base class | Method | +|---|---|---| +| Generator | `TUnitSourceGeneratorTestBase` | `GenerateAsync(source, options, ct)` | +| Diagnostic analyzer | `TUnitDiagnosticAnalyzerTestBase` | `AnalyzeAsync(source, options, ct)` | +| Code fix (single) | `TUnitCodeFixTestBase` | `ApplyCodeFixAsync(source, options, ct)` | +| Code fix (fix-all) | `TUnitCodeFixTestBase` | `ApplyFixAllAsync(sources, options, ct)` | +| Refactoring | `TUnitRefactoringTestBase` | `RefactorAsync(source, options, ct)` | + +For cache tests, `TUnitSourceGeneratorTestBase` also exposes `GenerateIncrementalAsync(...)`. + +## Easy starting point: derived options + +Derive a `SourceGeneratorTestOptions` record that seeds namespaces and additional assemblies, then +pass it to every test: + +```csharp +public sealed record MyTestOptions : SourceGeneratorTestOptions +{ + public MyTestOptions() + { + AdditionalNamespaces = AdditionalNamespaces.Add("My.Namespace"); + AdditionalAssemblyTypes = AdditionalAssemblyTypes.AddRange(typeof(SomeDependencyType), typeof(TypeIdentity)); + DisableSourceGeneratorPropertyName = "DisableMyGenerator"; + } +} + +public class MyGeneratorTests : TUnitSourceGeneratorTestBase { ... } +``` + +Use `options.Compile()` for `CompileToAssembly`, and the +`OnBeforeRun`/`OnBeforeRunAsync`/`OnAfterRun` hooks for per-run customisation. Code-fix/refactoring +tests select actions with `EquivalenceKey` or `CodeActionIndex` (and +`RefactorTestOptions.NodeSelector`/`Span`). + +## Assertion extensions + +All assertion extensions are under `Purview.SourceGeneratorFramework.Testing.TUnit.Assertions` +(globally imported). `await Assert.That(...)` is terminal and returns the value: + +- `HasGeneratedMethod` / `HasGeneratedMethodReturnType` / `HasGeneratedClass` / `HasGeneratedProperty` / + `HasGeneratedField` / `HasGeneratedSyntaxTree` — return the syntax node; + `HasGeneratedMethod(name, TypeReference[])` matches parameter types. `HasGeneratedClass(name, arity)` + (or a `TypeIdentity` with arity) matches a generic type by its type-parameter count, so + `new TypeIdentity("ResourceDefinition", ns, arity: 1)` finds `ResourceDefinition` without matching + the non-generic `ResourceDefinition`. +- `HasFixedMethod` — same for code-fix and refactoring results. +- `HasPropertyOfType` / `HasFieldOfType` / `HasMethodOfType` / `HasConstructorOfType` / + `HasAttributeOfType` / `HasNestedType` — chain from a scoped `CodeQueryResult` (for example the + result of `HasGeneratedClass`) and return the matched member. The node-producing assertions move the + chain onto the matched node, so you can append node-inspection assertions with `.And`: + ```csharp + var method = await Assert.That(query) + .HasGeneratedClass("Service") + .And.HasNestedType("Builder") + .And.WithAccessibility(Accessibility.Private) + .And.HasMethodOfType("Build", []); + ``` +- `WithAccessibility` / `WithGetterAccessibility` / `WithSetterAccessibility` / `WithBaseType` / + `WithGenericTypeParameter(s)` / `IsInNamespace` / `IsInGlobalNamespace` — node-inspection assertions + that keep the matched node on the chain. Accessibility resolves C# defaults (an unmodified nested + type is `Private`, a top-level type `Internal`, interface/enum members `Public`, and an accessor with + no modifier inherits its property's accessibility). +- `HasDiagnostic` / `HasDiagnostics` / `HasNoDiagnostics` / `DoesNotHaveDiagnostic` / + `HasNoErrorDiagnostics`. +- `HasSymbol(TypeIdentity)` / `HasSymbol("Namespace.Type")`. +- `GeneratesCode(expected)` / `ContainsGeneratedCode(expected)` (whitespace-flattened). + +The `CodeQuery` assertions operate on a `CodeQuery` directly, so they accept a query from any test +result — `result.Generated()` for generated code, `result.Output()` for the whole compilation, or +`result.FixedCode()` for fixed/refactored code. Convenience overloads on the test result types query +the generated (or fixed) code for you. + +To assert a nullable expected type, use the test-only `query.MakeNullable(...)` extension: it resolves +the annotation against the query's compilation and, unlike `TypeReference.Nullable()` / +`TypeIdentity.MakeNullable()`, does not trigger the `PSGFR16` context-overload suggestion (tests have +no generation context to pass). + +```csharp +var query = result.Generated(); +MethodDeclarationSyntax method = await Assert.That(query).HasGeneratedMethod("DoWork", [intType, nullableInt]); +await Assert.That(query).HasGeneratedSyntaxTree("Service.g.cs"); +await Assert.That(result.FixedCode()).HasFixedMethod("DoWork"); // code-fix / refactor results + +// Scoped member chaining: +CodeQueryResult attributeClass = await Assert.That(query).HasGeneratedClass(hostKitAttribute); +await Assert.That(attributeClass).HasPropertyOfType("Name", query.MakeNullable(TypeLibrary.System.String)); +``` + +## Incremental cache tests + +`GenerateIncrementalAsync` proves the pipeline caches stage-by-stage (first run `New`, identical rerun +`Cached`/`Unchanged`, targeted changes mark only the affected stage `Modified`). A reference +implementation (`ServiceRegistrationCacheTests`) lives in the `Purview.SourceGeneratorFramework` source +repository's example generator tests; replicate it in your own project with your own stage names. See +[Step-Cache-Tests.md](Step-Cache-Tests.md) for the full walkthrough. + +## License + +This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project. \ No newline at end of file diff --git a/docs/wiki/Testing.md b/docs/wiki/Testing.md new file mode 100644 index 0000000..20fd893 --- /dev/null +++ b/docs/wiki/Testing.md @@ -0,0 +1,272 @@ +# Testing + +`Purview.SourceGeneratorFramework.Testing` is the framework-agnostic test runner and assertion +library for unit testing incremental C# source generators. + +## Installation + +```bash +dotnet add package Purview.SourceGeneratorFramework.Testing +``` + +## What's included + +- **`SourceGeneratorTestRunner`** — compiles a snippet of C# source, runs the generator, + automatically registers an isolated framework logging sink, and returns a `DriverRunResult` with + generated syntax trees, the output compilation, and captured log entries. +- **`SourceGeneratorTestBase`** — abstract base class that accepts an `ITestOutput` + instance for framework-specific logging integration. +- **`SourceGeneratorTestOptions`** — options for configuring references, namespaces, analyzer-config + values, output kind, and whether to emit the output compilation to an assembly. +- **`DriverRunResult`** — wrapper around `GeneratorDriverRunResult` that exposes generated trees, the + output compilation, emitted assembly, and log entries. +- **`DriverRunResultExtensions`** — assertion helpers such as `AssertNoCompilationErrors`, + `AssertNoGenerationExceptions`, `AssertSingleGeneratedSource`, `AssertGeneratedSourceContains`, and + more. +- **`ITestOutput`** / **`NullTestOutput`** — abstraction for capturing generator log output during + tests. + +## Usage + +Reference the package from a test project and write a test using the runner directly: + +```xml + + + + +``` + +```csharp +using Purview.SourceGeneratorFramework.Testing; + +public class MyGeneratorTests +{ + [Test] + public async Task GeneratesExpectedSource() + { + var source = """ + [MyNamespace.MyAttribute] + public partial class MyClass { } + """; + + var runner = new SourceGeneratorTestRunner(); + var result = await runner.RunAsync(source); + + result.AssertNoCompilationErrors(); + var generated = result.AssertSingleGeneratedSource(); + } +} +``` + +Or derive from `SourceGeneratorTestBase` and plug in your own `ITestOutput` +implementation. + +## Running the generator in the test project + +Sometimes the test project's own source uses types produced by the generator — for example, an +integration test may attach a generated marker attribute to a fixture class while also passing the +generator type to `SourceGeneratorTestRunner`. + +Reference the generator project twice, once in each role: + +```xml + + + + + + + +``` + +The analyzer reference makes generated declarations available to the test project's compilation. The +normal reference makes the generator's CLR type available to the testing API. These are separate from +the in-memory compilation created by `SourceGeneratorTestRunner`; source supplied to the runner is +still compiled and generated independently. + +The normal reference also exposes the generator's assembly dependencies to every target framework of +the test project. This framework is built against Roslyn 5.0, which ships `net8.0` and `net9.0` +package assets, so tests targeting .NET 8, .NET 9, and .NET 10 can all load the test runner. The +Roslyn version used to compile a generator establishes the minimum compiler-host requirement for +projects that consume it as an analyzer — Roslyn 5.0 means `.NET 10` SDK / Visual Studio 2026 or +later. Do not centrally pin `System.Collections.Immutable` to a newer runtime version merely to make +the generator load. + +## Options + +Configure a test run with `SourceGeneratorTestOptions`: + +```csharp +var options = new SourceGeneratorTestOptions +{ + IncludeDefaultNamespaces = true, + AdditionalNamespaces = ["MyNamespace"], + AdditionalAssemblyTypes = [typeof(SomeExternalType)], + EnableLogging = true, + AnalyzerConfigOptions = { ["MyGenerator_Disable"] = "true" } +}; + +// Emitting the output to an assembly is opt-in because it is expensive. +var result = await runner.RunAsync(source, options.Compile()); +``` + +`Compile()` is an extension method that preserves the concrete options type. A derived options record +that wants a typed default must hide the inherited `SourceGeneratorTestOptions.Default` with a typed +static, otherwise `Default.Compile()` returns the base type: + +```csharp +public record MyTestOptions : SourceGeneratorTestOptions +{ + public static new MyTestOptions Default => new(); +} + +// Returns MyTestOptions with CompileToAssembly enabled. +var result = await runner.RunAsync(source, MyTestOptions.Default.Compile()); +``` + +### Compiled output + +Emission is fully in-memory (no files are written). On .NET 8+ the emitted assembly is loaded into a +fresh **collectible `AssemblyLoadContext`**, so the result is `IDisposable` and the assembly can be +unloaded when you are done with it — keeping repeated `CompileToAssembly` runs from accumulating +assemblies in the process-wide default context: + +```csharp +using var result = await runner.RunAsync(source, options.Compile()); + +result.CompilationResult.Assembly; // runnable assembly (may execute generated code) +result.CompilationResult.Metadata; // metadata-only MetadataLoadContext (never executes) +result.CompilationResult.MetadataAssembly; // emitted assembly reflected within that context +``` + +`CompilationResult.Metadata` / `MetadataAssembly` provide a metadata-only reflection view over the +emitted assembly: inspect types, members and attributes without loading it into the runtime or +executing any code. They are created lazily on first access. Dispose the result (or its +`DriverRunResult`) to unload the collectible context and release the metadata view. + +Analyzer options are preserved under their supplied keys. Keys without the Roslyn `build_property.` +prefix are additionally exposed as compiler-visible MSBuild properties, so either `MyGenerator_Disable` +or `build_property.MyGenerator_Disable` can be used in tests. + +## Querying produced code with `CodeQuery` + +Every result type exposes a `CodeQuery` so tests can locate syntax nodes in the produced code: + +```csharp +result.Generated() // DriverRunResult: generated trees (generated-first default) +result.Output() // DriverRunResult: whole output compilation +analyzerResult.Code() // AnalyzerTestResult / CodeFixTestResult: input compilation +codeFixResult.FixedCode() // CodeFixTestResult: fixed source +fixAllResult.FixedCode() // CodeFixFixAllResult / RefactorTestResult: changed documents +``` + +`CodeQuery` provides a `Get`/`Has`/`TryGet` family for declarations and members, generic +`Get`/`Has`, syntax-tree lookup, and type-aware matching against `TypeReference`. Every `Get` +returns a `CodeQueryResult` — the matched node (`Node`) plus a query scoped to it (`Query`) — with +implicit conversions to both the node and the scoped query, so member queries chain without +re-passing the query: + +```csharp +var query = result.Generated(); +query.GetClass("ServiceCollectionExtensions").HasMethod("Add", TypeReference.Create()); +query.GetClass("Service").GetProperty("Count", TypeReference.Create()); // property + type +query.GetClass("Service").GetMethod("DoWork").HasParameters(intType, nullableInt, complexType); +query.GetClass("Widget", "Example.Models"); // namespace-scoped lookup +query.HasClass(new TypeReference(new TypeIdentity("Widget", "Example.Models"))); // type-identity lookup +query.GetClass(TypeIdentity.Create()); // a TypeIdentity is implicitly castable to TypeReference +query.GetClass("ResourceDefinition", 1); // generic lookup by type-parameter count + +ClassDeclarationSyntax cls = query.GetClass("Service"); // implicit conversion to the node +query.GetClass("Service").Node.Members; // or use .Node for direct syntax access +``` + +`Get` throws `SyntaxNotFoundException` when nothing matches; `Has` returns `bool`. + +Type lookups accept an optional generic arity — `GetClass(name, arity)` / `HasClass(name, arity)` — +and the `TypeReference`/`TypeIdentity` overloads match arity automatically from the identity, so +`new TypeIdentity("ResourceDefinition", ns, arity: 1)` finds `ResourceDefinition` without matching +the non-generic `ResourceDefinition`. + +Scoped results also expose node-inspection checks through `MemberQueryExtensions`: +`HasAccessibility` (resolves C# defaults), `HasGetterAccessibility` / `HasSetterAccessibility`, +`HasBaseType`, `HasGenericTypeParameter(s)`, `GetNestedType` / `HasNestedType`, `IsInNamespace` / +`IsInGlobalNamespace`, and `GetDeclaredNamespace` on the query itself. + +### Nullable expected types in tests + +Tests asserting a nullable expected type can use the test-only `query.MakeNullable(type)` extension on +a `CodeQuery` (it accepts a `TypeReference` or `TypeIdentity`). It resolves the annotation against the +query's compilation and, unlike `TypeReference.Nullable()`/`TypeIdentity.MakeNullable()`, does not +trigger the `PSGFR16` context-overload suggestion — tests have no generation context to pass. + +```csharp +var query = result.Generated(); +query.GetClass("Service").HasProperty("Name", query.MakeNullable(TypeReference.Create())); +``` + +## Refactoring tests + +`RefactoringTestRunner` runs a `CodeRefactoringProvider` against a test document: + +```csharp +var runner = new RefactoringTestRunner(); +var result = await runner.RunAsync( + source, + new RefactorTestOptions + { + NodeSelector = query => query.GetMethod("M"), + EquivalenceKey = MyRefactoringProvider.EquivalenceKey, + }); + +result.FixedCode().HasMethod("M"); // query the refactored output +``` + +The trigger is a `Span` or a `NodeSelector` (which runs against a `CodeQuery` of the input +compilation). + +## Incremental cache testing + +`SourceGeneratorTestRunner.RunIncrementalAsync` runs the generator over a sequence of source sets +using a single shared driver and captures each run's tracked incremental steps, so tests can prove +each pipeline stage caches correctly: + +```csharp +var result = await runner.RunIncrementalAsync([firstSources, secondSources], options); + +var reasons = result.Runs[1].Steps["ForAttribute_MyAttribute"] + .SelectMany(step => step.Outputs.Select(output => output.Reason)); +``` + +`IncrementalCacheRunExtensions.GetStepReasons()` flattens a run's steps into an +`ImmutableDictionary>`, and the TUnit assertions +`AllStepsNew`, `AllStepsCachedOrUnchanged`, `StepIsCached`, `StepIsModified`, and `HasStepReason` make +the checks fluent: + +```csharp +await Assert.That(result.Runs[0]).AllStepsNew(); +await Assert.That(result.Runs[1]).StepIsModified("ForAttribute_MyAttribute"); +await Assert.That(result.Runs[1]).StepIsCached("GetGenerationConfiguration"); +``` + +`RunIncrementalAsync(sources, options, ct)` runs the same source set twice (the common "unchanged +rerun is cached" case). Per-run MSBuild-property changes use `new IncrementalRunInput(sources, [...])`. +Reference cache tests live in the `Purview.SourceGeneratorFramework` source repository — +`SourceGeneratorShared.UnitTests/IncrementalPipelineCacheTests` (framework stages), +`SourceGeneratorFramework.ExampleGenerator.UnitTests/StepCacheTests` (the canonical golden-matrix +sample), and `.../ServiceRegistrationCacheTests` (an end-to-end generator) — and should be replicated +into your own test project rather than copied from the package. See +[Step-Cache-Tests.md](Step-Cache-Tests.md) for the full walkthrough. + +## License + +This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project. \ No newline at end of file diff --git a/docs/type-library.md b/docs/wiki/Type-Library.md similarity index 99% rename from docs/type-library.md rename to docs/wiki/Type-Library.md index 6448a72..b8e1f73 100644 --- a/docs/type-library.md +++ b/docs/wiki/Type-Library.md @@ -300,4 +300,4 @@ The generator carries these same diagnostics on its `GeneratorResult` and gates `ShouldProcess`. Most are blocking (`IsBlocking: true`) and stop generation, but the non-blocking rules — `TLB0010` (marker without `= default`) and `TLB0013` (warning) — allow generation to continue, so a spec with those issues still produces the type library. See -[`GeneratorResult` diagnostics that don't stop generation](../src/src/SourceGeneratorFramework/Sdk/README.md#diagnostics-that-dont-stop-generation). \ No newline at end of file +[`GeneratorResult` diagnostics that don't stop generation](../../src/src/SourceGeneratorFramework/Sdk/README.md#diagnostics-that-dont-stop-generation). \ No newline at end of file diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md new file mode 100644 index 0000000..d23f6e3 --- /dev/null +++ b/docs/wiki/_Sidebar.md @@ -0,0 +1,14 @@ +- [Home](Home.md) +- [Getting Started](Getting-Started.md) +- [Source Generator & Analyser Best Practices](Guide.md) +- [CodeWriter](Code-Writer.md) +- [TypeLibraryGenerator](Type-Library.md) +- [Attribute Data Models](Attribute-Data-Models.md) +- [Incremental Pipeline](Incremental-Pipeline.md) +- [Analyzers](Analyzers.md) +- [Testing](Testing.md) +- [Testing with TUnit](Testing-TUnit.md) +- [Step-Cache Tests](Step-Cache-Tests.md) +- [Packaging](Packaging.md) +- [Performance](Performance.md) +- [Release Flow](Release-Flow.md) \ No newline at end of file diff --git a/package.json b/package.json index 398903c..370ab7f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,17 @@ { "name": "purview-sourcegeneratorframework", "version": "1.0.0-prerelease.42", - "private": true + "license": "MIT", + "author": { + "name": "Kieron Lanning", + "url": "https://kieronlanning.dev/" + }, + "homepage": "https://purview.dev/projects/sourcegeneratorframework/", + "bugs": { + "url": "https://github.com/purview-dev/sourcegeneratorframework/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/purview-dev/sourcegeneratorframework.git" + } } \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 36710dd..c9dc46d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -31,10 +31,16 @@ $(TestingTargetFrameworks) + + https://purview.dev/ + $(PurviewHomepage)projects/sourcegeneratorframework/ + $(PurviewHomepage)docs/sourcegeneratorframework/ + + Purview Contributors true - https://github.com/purview-dev/sourcegeneratorframework + $(PurviewProjectUrl) https://github.com/purview-dev/sourcegeneratorframework README.md LICENSE.md diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 234502d..e3f28f5 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -2,7 +2,7 @@ Purview Contributors Purview SourceGeneratorFramework libraries for building and testing incremental C# source generators. - https://github.com/purview-dev/sourcegeneratorframework + $(PurviewProjectUrl) https://github.com/purview-dev/sourcegeneratorframework README.md purview-logo.png diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/README.md b/src/src/SourceGeneratorFramework.ExampleGenerator/README.md index 341eac3..222d57a 100644 --- a/src/src/SourceGeneratorFramework.ExampleGenerator/README.md +++ b/src/src/SourceGeneratorFramework.ExampleGenerator/README.md @@ -27,7 +27,7 @@ Reference implementation of an incremental C# source generator built with `Purvi - Incremental pipeline with `IncrementalPipeline.ForAttributeWithMetadataName`. - Attribute-data model (`GenerateServiceAttributeData`) generated by `AttributeDataModelGenerator` using `[Argument]` for a constructor parameter and `[Property]` for a named property. - Enum string extraction from `TypedConstant` values (`IsEnum = true`). -- `CodeWriter` usage for all output, including post-initialization sources and attribute/enum declarations. See the [CodeWriter API reference](../../docs/code-writer.md) for the structured API and best practices. +- `CodeWriter` usage for all output, including post-initialization sources and attribute/enum declarations. See the [CodeWriter API reference](../../docs/wiki/Code-Writer.md) for the structured API and best practices. - A dedicated `CodeWriterSampleGenerator` that emits a best-practice sample class for every `[GenerateCodeWriterSample]` target, demonstrating minimal overloads, structured statements, scope usage, and `NetConditionalReturn`. - `TypeIdentity` / `TypeReference` / `TypeLibrary` helpers for safe type/namespace references. - Diagnostic reporting for invalid inputs (e.g. interfaces, static classes, nested classes, abstract classes). diff --git a/src/src/SourceGeneratorFramework.Generators/README.md b/src/src/SourceGeneratorFramework.Generators/README.md index 50faed4..e43fbe6 100644 --- a/src/src/SourceGeneratorFramework.Generators/README.md +++ b/src/src/SourceGeneratorFramework.Generators/README.md @@ -198,7 +198,7 @@ Consumers then reference `MyTypeLibrary.GenerateMyAttribute`, `MyTypeLibrary.Microsoft.Extensions.Logging.ILogger`, and `MyTypeLibrary.System.Collections.Generic.Items` directly. -See [docs/type-library.md](../../docs/type-library.md) for the full DSL and the member-accessibility rules. +See [docs/wiki/Type-Library.md](../../docs/wiki/Type-Library.md) for the full DSL and the member-accessibility rules. ## License diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/README.md b/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/README.md index de5ab60..8b76ab4 100644 --- a/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/README.md +++ b/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/README.md @@ -182,6 +182,11 @@ await Assert.That(attributeClass).HasPropertyOfType("Name", query.MakeNullable(T implementation (`ServiceRegistrationCacheTests`) lives in the `Purview.SourceGeneratorFramework` source repository's example generator tests; replicate it in your own project with your own stage names. +## Documentation + +- [Homepage](https://purview.dev/projects/sourcegeneratorframework/) +- [Documentation](https://purview.dev/docs/sourcegeneratorframework/) + ## License This project is licensed under the MIT license. diff --git a/src/src/SourceGeneratorFramework.Testing/Sdk/README.md b/src/src/SourceGeneratorFramework.Testing/Sdk/README.md index 5f14711..16f97c2 100644 --- a/src/src/SourceGeneratorFramework.Testing/Sdk/README.md +++ b/src/src/SourceGeneratorFramework.Testing/Sdk/README.md @@ -258,7 +258,12 @@ cache tests live in the `Purview.SourceGeneratorFramework` source repository — `SourceGeneratorShared.UnitTests/IncrementalPipelineCacheTests` (framework stages), `SourceGeneratorFramework.ExampleGenerator.UnitTests/StepCacheTests` (the canonical golden-matrix sample), and `.../ServiceRegistrationCacheTests` (an end-to-end generator) — and should be replicated into your own -test project rather than copied from the package. See `docs/step-cache-tests.md` for the full walkthrough. +test project rather than copied from the package. See `docs/wiki/Step-Cache-Tests.md` for the full walkthrough. + +## Documentation + +- [Homepage](https://purview.dev/projects/sourcegeneratorframework/) +- [Documentation](https://purview.dev/docs/sourcegeneratorframework/) ## License diff --git a/src/src/SourceGeneratorFramework/Sdk/README.md b/src/src/SourceGeneratorFramework/Sdk/README.md index e8247eb..eb8014b 100644 --- a/src/src/SourceGeneratorFramework/Sdk/README.md +++ b/src/src/SourceGeneratorFramework/Sdk/README.md @@ -983,6 +983,11 @@ The `Purview.SourceGeneratorFramework` package includes the `Purview.SourceGener | `PSGFR37` | One extension class per receiver type; split classes that extend multiple types. | | `PSGFR38` | Extension classes should carry `[EditorBrowsable(EditorBrowsableState.Never)]`. | +## Documentation + +- [Homepage](https://purview.dev/projects/sourcegeneratorframework/) +- [Documentation](https://purview.dev/docs/sourcegeneratorframework/) + ## License This project is licensed under the MIT license.