diff --git a/.agents/prompts/sdk-packable-project-analyzer-defaults.prompt.md b/.agents/prompts/sdk-packable-project-analyzer-defaults.prompt.md
deleted file mode 100644
index e7cb345..0000000
--- a/.agents/prompts/sdk-packable-project-analyzer-defaults.prompt.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-agent: ask
-description: "Ensure the Purview.BuildSdk applies analyzer/source-generator best-practice defaults to packable projects, so consumers of Purview.SourceGeneratorFramework (and any Roslyn component) get correct packaging without per-project overrides."
----
-
-You are working on the **Purview.BuildSdk** project (the `Purview.BuildSdk` package that projects import via ``). Apply this checklist to its `Sdk.props` / `Sdk.targets` so that **packable** analyzer and source-generator projects get Roslyn best-practice defaults automatically.
-
-## Context
-
-The `Purview.SourceGeneratorFramework` guidance requires every analyzer/generator project to set these properties or the shipped analyzer asset is broken or unoptimised. Consumers should not have to set them per project. The SDK is the right place to default them.
-
-## Task
-
-For projects the SDK classifies as analyzers/source generators (`IsRoslynComponent`, or projects that produce `analyzers/dotnet/cs` assets) and that are packable, ensure the following are the **defaults** (still overridable by the project):
-
-1. `TargetFramework=netstandard2.0` unless the project explicitly overrides it.
-2. `LangVersion=latest` and `Nullable=enable`.
-3. `IncludeBuildOutput=false` (so the library isn't packed as a `lib/` asset) and the analyzer/generator DLL packed into `analyzers/dotnet/cs`:
- ```xml
-
- ```
-4. `EnforceExtendedAnalyzerRules=true` and `TreatWarningsAsErrors=true`.
-5. Roslyn development dependencies (`Microsoft.CodeAnalysis.*`, `Microsoft.CodeAnalysis.Analyzers`) referenced with `PrivateAssets="all"`.
-
-## Requirements
-
-- Do not break existing projects that already set these explicitly; the SDK defaults must be overridable.
-- Handle the distinction between packable and non-packable projects — only packable analyzer/generator projects get the analyzer-asset packaging.
-- Ensure the defaults apply at the right evaluation point in the SDK (props vs targets) so project files can still override with normal `PropertyGroup` values.
-- Verify with a consumer project that references a generator through the SDK that the packed `.nupkg` contains `analyzers/dotnet/cs/.dll` and the `lib/` output is empty.
-
-## Background
-
-See the `source-generator-codewriter-modernization` skill's "Recommended project configuration" for the canonical generator project shape this SDK should default to.
\ No newline at end of file
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 42b8dea..d51c9f0 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -10,14 +10,14 @@
"rollForward": false
},
"dotnet-inspect": {
- "version": "0.25.0",
+ "version": "0.26.0",
"commands": [
"dotnet-inspect"
],
"rollForward": false
},
"purview.build": {
- "version": "0.5.3",
+ "version": "0.5.4",
"commands": [
"purview-build"
],
diff --git a/.config/purview-build.json b/.config/purview-build.json
index a74f0ac..bc5db30 100644
--- a/.config/purview-build.json
+++ b/.config/purview-build.json
@@ -14,7 +14,6 @@
"RequireSymbolFiles": true,
"RequiredContent": {
"purview.sourcegeneratorframework": [
- ".agents/**/*",
"analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Analyzers.dll",
"analyzers/dotnet/cs/Purview.SourceGeneratorFramework.CodeFixers.dll",
"analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Generators.dll",
@@ -38,7 +37,6 @@
"tools/*/System.IO.Hashing.dll"
],
"purview.sourcegeneratorframework.testing": [
- ".agents/**/*",
"build/Purview.SourceGeneratorFramework.Testing.props",
"lib/*/Purview.SourceGeneratorFramework.Testing.dll",
"lib/*/Purview.SourceGeneratorFramework.Testing.xml",
@@ -46,7 +44,6 @@
"README.md"
],
"purview.sourcegeneratorframework.testing.tunit": [
- ".agents/**/*",
"build/Purview.SourceGeneratorFramework.Testing.TUnit.props",
"lib/*/Purview.SourceGeneratorFramework.Testing.TUnit.dll",
"lib/*/Purview.SourceGeneratorFramework.Testing.TUnit.xml",
diff --git a/AGENTS.md b/AGENTS.md
index 19190e3..32ede14 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -9,3 +9,49 @@ Do **not** suppress or mute compiler, analyser, or build warnings by adding `/SKILL.md`.
+
+Install them into your agent's own directories with:
+
+```bash
+npx skills add purview-dev/sourcegenerator-framework
+```
+
+| Skill | Answers |
+| --- | --- |
+| `skills/purview-sourcegenerator-codewriter/` | emitting code: `CodeWriter`, `XmlCommentWriter`, scope safety, migrating a legacy string emitter |
+| `skills/purview-sourcegenerator-pipeline/` | composing the incremental pipeline: `IncrementalPipeline`, `GenerationContext`, `GeneratorResult`, disable properties |
+| `skills/purview-sourcegenerator-testing/` | the runner layer, `CodeQuery`, options, stage-by-stage cache tests |
+| `skills/purview-sourcegenerator-tunit-tests/` | which TUnit base class and method, and the assertion extensions |
+| `skills/purview-sourcegenerator-packaging/` | the self-contained merged analyzer, embedding a generator in another package, Roslyn compatibility |
+| `skills/purview-sourcegenerator-diagnostics/` | what a `PSGFR`/`PSGF`/`PRSGD` diagnostic means, and why a component is silently not running |
+
+Every skill carries the `purview-sourcegenerator` prefix. Skills install into one flat directory per
+agent, shared with every skill a developer has installed from anywhere, so an unprefixed name —
+`source-generator-testing`, `tunit-test-authoring` — collides and the loser is whichever installed
+second.
+
+To add a skill: create `skills//SKILL.md` with spec-legal frontmatter, and nothing else. There
+is no per-project registration any more.
+
+`src/tests/Repository.UnitTests/SkillTests.cs` validates every skill on every test run, through
+`Purview.Skills.Testing`'s rule set (the Agent Skills specification: frontmatter fields, name and
+description constraints, bundled links, body size). The `[InheritsTests]` attribute on that class is
+load-bearing — TUnit does not walk base types during discovery, so without it the run is green and
+nothing is checked. That project pins `net10.0` rather than taking the repository's testing set,
+because it asserts something about the repository rather than about the runtime's reach.
+
+A relative link from one `SKILL.md` into another skill's folder is a rule violation: only a skill's
+own folder is installed with it, so such a link works here and nowhere else. Name the other skill in
+prose instead, and keep deep detail in the skill's own `references/`.
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 988fb0e..331a47f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,12 +3,14 @@
true
[5.9.0,)
[5.9.0,)
- [1.67.0,)
+
+ [1.75.0,)
-
+
@@ -23,5 +25,7 @@
+
+
diff --git a/docs/wiki/Analyzers.md b/docs/wiki/Analyzers.md
index 4cf5e7b..b436181 100644
--- a/docs/wiki/Analyzers.md
+++ b/docs/wiki/Analyzers.md
@@ -30,7 +30,11 @@ These are MSBuild diagnostics rather than compiler analyzers, so they are not tr
| `PSGF0003` | `Purview.SourceGeneratorFramework` | A `PurviewGeneratorVisibleProperty` is not compiler-visible in the declaring project or its `Sdk/build`/`Sdk/buildTransitive` assets, so consumers cannot read `build_property.`. |
| `PRSGD0005` | `Purview.BuildSdk` | A file in the returned analyzer closure references an assembly that is neither part of the closure nor a compiler-host assembly. |
| `PSGFR41` | `Purview.SourceGeneratorFramework` merge tool | A component's public surface exposes framework types that the merge internalizes. Raised by the compiler analyzer of the same id at design time and by the merge pass as a `message` (default), `warning` or `error`. |
-| `PSGFR42` | `Purview.SourceGeneratorFramework` merge tool | The merged analyzer still exposes a public framework type, so it is not self-contained. Always an error; the merge fails with exit code 5. |
+| `PSGFR43` | `Purview.SourceGeneratorFramework` merge tool | The merged analyzer still exposes a public framework type, so it is not self-contained. Always an error; the merge fails with exit code 5. |
+
+`PSGFR41` is deliberately shared with the compiler analyzer of the same id: the merge reports the same
+finding. The merge's blocked code is `PSGFR43`, not `PSGFR42`, because `PSGFR42` belongs to the
+compiler-analyzer family (the undisposed test-harness result rule).
Opt out with `PurviewSourceGeneratorFrameworkAnalyzerValidation=false` (`PRSGD0005`) or
`PurviewSourceGeneratorFrameworkGeneratorPropertyValidation=false` (`PSGF0003`).
@@ -69,7 +73,7 @@ Opt out with `PurviewSourceGeneratorFrameworkAnalyzerValidation=false` (`PRSGD00
| `PSGFR39` | A non-packable Roslyn component that explicitly opts out of the default self-contained analyzer output (`PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=false`) while embedding the framework, otherwise the package embeds the loose framework DLL under `analyzers/`. |
| `PSGFR40` | In Roslyn components (`IsRoslynComponent=true`), reference SGF types as inline code (`Type`) instead of a `cref`: copied documentation must not depend on cref resolution. |
| `PSGFR41` | In Roslyn components whose framework implementation is merged, a public member (or generic constraint) whose signature references an SGF type: the merge internalizes every SGF type, so the signature is left referring to an internal type. Make the member or its declaring type non-public. |
-| `PSGFR42` | A disposable Testing-harness result (`DriverRunResult`) is produced and never disposed, leaking the collectible `AssemblyLoadContext` and `MetadataLoadContext` of a compiled run. Reported as a `warning` when the run syntactically compiles to an assembly (a `Compile` options member or `CompileToAssembly = true`), otherwise as `info`. |
+| `PSGFR42` | A disposable Testing-harness result (`DriverRunResult`) is produced and never disposed, leaking the collectible `AssemblyLoadContext` and `MetadataLoadContext` of a compiled run. Reported as a `warning` when the run syntactically compiles to an assembly (a `Compile` options member or `CompileToAssembly = true`), otherwise as `info`. Awaiting an assertion, or calling a helper that is handed the result — an extension on it, or a method taking it as a parameter — is not a production: both hand the same instance back, so only a task-like await (the shape the harness runners return) or a genuine producer is reported. |
## Type-library and attribute-model diagnostics
diff --git a/docs/wiki/Testing.md b/docs/wiki/Testing.md
index 2a0deeb..489df8c 100644
--- a/docs/wiki/Testing.md
+++ b/docs/wiki/Testing.md
@@ -157,7 +157,10 @@ executing any code. They are created lazily on first access. Dispose the result
[`PSGFR42`](Analyzers.md#rule-reference) enforces that: a `DriverRunResult` that is produced and never
disposed — including the indirect case where a helper creates a result, projects a value out of it and
returns only the projection — is flagged, as a `warning` when the run syntactically compiles to an assembly
-and as `info` otherwise. `DisposeTestResultCodeFixProvider` inserts the `using`.
+and as `info` otherwise. `DisposeTestResultCodeFixProvider` inserts the `using`. Awaiting an assertion, or
+calling a helper that is handed the result — an extension on it, or a method taking it as a parameter — is
+not a production: both hand the same instance back, so neither `await Assert.That(result).SomeAssertion()`
+nor `result.AssertNoGenerationExceptions()` is flagged.
## Reference sharing and parallelism
diff --git a/global.json b/global.json
index 5ad4318..32f1a20 100644
--- a/global.json
+++ b/global.json
@@ -3,7 +3,7 @@
"allowPrerelease": false
},
"msbuild-sdks": {
- "Purview.BuildSdk": "1.0.5"
+ "Purview.BuildSdk": "1.0.6"
},
"test": {
"runner": "Microsoft.Testing.Platform"
diff --git a/package.json b/package.json
index a5db2d2..d43debd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sourcegenerator-framework",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
diff --git a/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md b/skills/purview-sourcegenerator-codewriter/SKILL.md
similarity index 95%
rename from src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md
rename to skills/purview-sourcegenerator-codewriter/SKILL.md
index 8501ca4..ee363c9 100644
--- a/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md
+++ b/skills/purview-sourcegenerator-codewriter/SKILL.md
@@ -1,12 +1,22 @@
---
-name: source-generator-codewriter-modernization
-description: "Use when implementing, reviewing, or refactoring C# source generators and analysers in Purview.SourceGeneratorFramework. Covers CodeWriter/XmlCommentWriter-style emission, incremental pipeline design, value equality, and Roslyn best practices."
+name: purview-sourcegenerator-codewriter
+description: >-
+ Use when implementing, reviewing or refactoring C# source generators and analysers built on
+ Purview.SourceGeneratorFramework. Covers CodeWriter and XmlCommentWriter emission, scope safety,
+ incremental pipeline design, value equality, deterministic output, and migrating a legacy
+ string/StringBuilder emitter to CodeWriter.
+metadata:
+ product: Purview.SourceGeneratorFramework
+ repository: purview-dev/sourcegenerator-framework
---
# Source generator CodeWriter modernization
Use this skill for any work involving C# source generators, analysers, or generated output in `Purview.SourceGeneratorFramework`. It combines CodeWriter/XmlCommentWriter emission guidance with the incremental-source-generator and analyser best practices that ship with the framework.
+Refactoring an existing emitter off `string`/`StringBuilder`? The step-by-step playbook is in
+[references/legacy-emitter-migration.md](references/legacy-emitter-migration.md).
+
## Required implementation pattern
When creating source output, favor this shape:
diff --git a/skills/purview-sourcegenerator-codewriter/references/legacy-emitter-migration.md b/skills/purview-sourcegenerator-codewriter/references/legacy-emitter-migration.md
new file mode 100644
index 0000000..54efd09
--- /dev/null
+++ b/skills/purview-sourcegenerator-codewriter/references/legacy-emitter-migration.md
@@ -0,0 +1,58 @@
+# Migrating a legacy emitter to CodeWriter
+
+A playbook for moving a source generator's emission from manual `string` / `StringBuilder`
+construction to `CodeWriter` plus the `XmlCommentWriter` documentation extensions, with behaviour
+parity.
+
+## Before you start, collect
+
+- The emitter file(s) being changed.
+- The generator type name, and the version string it stamps into generated attributes or headers.
+- Whether the output must stay **byte-identical**. If it must, every whitespace decision below is a
+ constraint rather than a preference, and the diff of the generated snapshots is the acceptance
+ test.
+
+## Requirements
+
+1. Use the structured declaration APIs rather than hand-written declaration text:
+ `Class` / `Struct` / `RecordClass` / `Interface` / `Enum`, and `Method`, `Property`, `Field`,
+ `Constructor`.
+2. Use the XML helper extensions instead of composing `///` lines: `XmlSummary`, `XmlParam`,
+ `XmlReturn`, `XmlRemarks`, `XmlCode` / `XmlCodeBlock`.
+3. Use `TypeReference` once the type text gets complex — nullability, generics, arrays.
+4. Keep the writer **output-scoped**: `generationContext.CreateCodeWriter()` *inside* the output
+ callback. Never create it earlier in the pipeline, and never store it in incremental provider
+ state or in a custom context.
+5. Preserve behaviour, diagnostics and generated names.
+6. Keep the change minimal. Do not reformat logic the migration does not touch.
+
+## Strategy
+
+Work phase by phase, because each phase has a different mechanical replacement:
+
+| Emitter phase | Replace with |
+| --- | --- |
+| header | the header helper, not a literal banner string |
+| namespace | the namespace scope API |
+| type declarations | declaration option records, not signature strings |
+| member declarations | `Method` / `Property` / `Field` / `Constructor` |
+| indentation and braces | scoped APIs — never manual `{`/`}` and `\t` |
+| `///` comment composition | `XmlSummary`, `XmlParam`, `XmlReturn`, `XmlRemarks`, `XmlCode` |
+
+Call out any intentional semantic delta explicitly; a migration that quietly changes emitted names or
+diagnostics is the failure mode this playbook exists to avoid.
+
+## Verification
+
+- Run the impacted tests.
+- Confirm the generated files still compile.
+- Confirm there are **no scope leaks**: `OpenScopeCount == 0` when the writer is materialized. Enable
+ CodeWriter scope validation in the test run.
+- Where byte-identical output was required, diff the generated snapshots.
+
+## Report
+
+1. Files changed.
+2. Why each change was necessary.
+3. Risks and behaviour differences, if any.
+4. The verification that actually ran.
diff --git a/skills/purview-sourcegenerator-diagnostics/SKILL.md b/skills/purview-sourcegenerator-diagnostics/SKILL.md
new file mode 100644
index 0000000..21d9522
--- /dev/null
+++ b/skills/purview-sourcegenerator-diagnostics/SKILL.md
@@ -0,0 +1,142 @@
+---
+name: purview-sourcegenerator-diagnostics
+description: >-
+ Use when a Purview.SourceGeneratorFramework build reports a PSGFR, PSGF, PRSGD or TLB diagnostic,
+ when choosing which code fix applies, or when a source generator, diagnostic analyzer or code fix
+ provider is silently not running at all.
+metadata:
+ product: Purview.SourceGeneratorFramework
+ repository: purview-dev/sourcegenerator-framework
+---
+
+# Purview.SourceGeneratorFramework diagnostics
+
+`Purview.SourceGeneratorFramework` ships `Purview.SourceGeneratorFramework.Analyzers` and
+`Purview.SourceGeneratorFramework.CodeFixers` as analyzer assets inside the package, under
+`analyzers/dotnet/cs/`. They are enabled automatically in any project that references the framework —
+there is no separate package to add.
+
+Two compiler-analyzer families, plus a set of MSBuild-time diagnostics:
+
+- **Incremental generator best practice** — `PSGFR11`–`PSGFR33`: pipeline design, `CodeWriter` usage,
+ Roslyn component discovery, extension-class conventions.
+- **C# 14 extension-member conventions** — `PSGFR34`–`PSGFR38`, with
+ `ReorganizeExtensionClassCodeFixProvider` and `ConvertToExtensionBlockCodeFixProvider`.
+- **Merge and packaging** — `PSGFR39`–`PSGFR43`, where `PSGFR43` is the merge tool's blocked
+ public-surface code.
+
+## Start here when nothing runs
+
+A compiler host loads a generator, analyzer or code fix provider only when **three** conditions hold.
+Missing any one means the component is **silently ignored** — no error, no output, a green build:
+
+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"` on a `ProjectReference`.
+
+A code fix provider additionally appears only when a diagnostic ID in its `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 before the fixes show up.
+
+## Build-time validation diagnostics
+
+These are MSBuild diagnostics, not compiler analyzers, so they are **not** tracked in
+`AnalyzerReleases.*.md`:
+
+| Code | Raised by | Summary |
+| --- | --- | --- |
+| `PSGF0001` | `Purview.BuildSdk` | A Roslyn component did not produce (or declare) a source-generator analyzer file. |
+| `PSGF0003` | `Purview.SourceGeneratorFramework` | A `PurviewGeneratorVisibleProperty` is not compiler-visible in the declaring project or its `Sdk/build`/`Sdk/buildTransitive` assets, so consumers cannot read `build_property.`. |
+| `PRSGD0005` | `Purview.BuildSdk` | A file in the returned analyzer closure references an assembly that is neither in the closure nor a compiler-host assembly. |
+| `PSGFR41` | merge tool | A component's public surface exposes framework types the merge internalizes. Reported by the analyzer at design time, and by the merge pass as `message` (default), `warning` or `error`. |
+| `PSGFR43` | merge tool | The merged analyzer still exposes a public framework type, so it is not self-contained. Always an error; the merge fails with exit code 5. |
+
+Opt out with `PurviewSourceGeneratorFrameworkAnalyzerValidation=false` (`PRSGD0005`) or
+`PurviewSourceGeneratorFrameworkGeneratorPropertyValidation=false` (`PSGF0003`).
+
+## Incremental pipeline rules
+
+| Rule | Summary |
+| --- | --- |
+| `PSGFR11` | Prefer `SyntaxProvider.ForAttributeWithMetadataName` over `CreateSyntaxProvider` for attribute-based detection. |
+| `PSGFR12` | Use `IIncrementalGenerator` / `RegisterSourceOutput` instead of `ISourceGenerator`. |
+| `PSGFR14` | Avoid `RegisterImplementationSourceOutput` unless implementation-only output is required. |
+| `PSGFR15` | Pipeline model collection members should use sequence equality (`EquatableArray`). |
+| `PSGFR30` | Prefer `static` lambdas in pipeline methods, so the compiler never allocates a closure on the per-item hot path. |
+| `PSGFR31` | Prefer `GeneratorAttributeSyntaxContext.TargetSymbol` over `SemanticModel.GetDeclaredSymbol(ctx.TargetNode)`. |
+| `PSGFR33` | Pipeline models must not retain Roslyn objects (`ISymbol`, `SyntaxNode`, `Location`, …); extract the information into value types. |
+
+## CodeWriter rules
+
+| Rule | Summary |
+| --- | --- |
+| `PSGFR16` | Prefer the nullable-context `Nullable()`/`MakeNullable()` overload, so annotations honour the target compilation. |
+| `PSGFR17` | Consume 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 hand-constructing `*DeclarationOptions` values. |
+| `PSGFR21` | Prefer `HashDefines`/`HashDefinesScope` for `#if`/`#endif`. |
+| `PSGFR22` | Prefer `PragmaDisable`/`OpenPragmasScope` for `#pragma warning`. |
+| `PSGFR23` | Prefer structured `IfBlock`/`ElseIf`/`Else` over raw `if` block text. |
+| `PSGFR29` | Do not embed a `CodeWriter` in a string; use `XmlCommentWriter.XmlInlineCode`. |
+| `PSGFR32` | Avoid `NormalizeWhitespace` when generating source; use an indented text writer such as `CodeWriter`. |
+
+## Component, extension and merge rules
+
+| Rule | Summary |
+| --- | --- |
+| `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` names a diagnostic no analyzer in the compilation produces; the fix will never be shown. |
+| `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 belong in the extended type's namespace, under an `Extensions` folder. |
+| `PSGFR37` | One extension class per receiver type; split a class that extends several. |
+| `PSGFR38` | Extension classes should carry `[EditorBrowsable(EditorBrowsableState.Never)]`. |
+| `PSGFR39` | A non-packable Roslyn component opted out of the default self-contained analyzer output (`PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=false`) while embedding the framework; otherwise the package embeds the loose framework DLL under `analyzers/`. |
+| `PSGFR40` | In a Roslyn component (`IsRoslynComponent=true`), reference SGF types as inline code (`Type`) rather than a `cref`: copied documentation must not depend on cref resolution. |
+| `PSGFR41` | A public member (or generic constraint) whose signature references an SGF type in a component whose framework implementation is merged. The merge internalizes every SGF type, so the signature ends up referring to an internal type. Make the member, or its declaring type, non-public. |
+| `PSGFR42` | A disposable Testing-harness result (`DriverRunResult`) is produced and never disposed, leaking the collectible `AssemblyLoadContext` and `MetadataLoadContext` of a compiled run. `warning` when the run syntactically compiles to an assembly (a `Compile` options member, or `CompileToAssembly = true`), otherwise `info`. Awaiting an assertion, or calling a helper handed the result — an extension on it, or a method taking it as a parameter — is not a production: both hand the same instance back, so only a task-like await or a genuine producer is reported. |
+
+## Type-library and attribute-model diagnostics
+
+The bundled generators carry their own families: `TypeLibraryValidationAnalyzer`
+(`TLB0001`–`TLB0019`, plus `TLB0021`) and the attribute-data-model validation analyzers. They are
+documented on their own wiki pages, `Type-Library.md` and `Attribute-Data-Models.md`.
+
+## Code fixes
+
+Most rules above have a fix in `Purview.SourceGeneratorFramework.CodeFixers`:
+
+| Fix provider | Rule |
+| --- | --- |
+| `AddGeneratorAttributeCodeFixProvider` | `PSGFR26` |
+| `AddDiagnosticAnalyzerAttributeCodeFixProvider` | `PSGFR25` |
+| `AddExportCodeFixProviderAttributeCodeFixProvider` | `PSGFR24` |
+| `MakeRoslynComponentPublicCodeFixProvider` | `PSGFR27` |
+| `RemoveOrphanedFixableDiagnosticIdCodeFixProvider` | `PSGFR28` |
+| `PreferTargetSymbolCodeFixProvider` | `PSGFR31` |
+| `PreferStaticLambdaCodeFixProvider` | `PSGFR30` |
+| `PreferNullableContextOverloadCodeFixProvider` (project-wide Fix All) | `PSGFR16` |
+| `PipelineModelReferenceEqualityCollectionCodeFixProvider` | `PSGFR15` |
+| `PreferStructuredCodeWriterIfBlockCodeFixProvider` | `PSGFR23` |
+| `CodeWriterToStringCodeFixProvider` | `PSGFR29` |
+| `PreferInlineCodeForFrameworkCrefCodeFixProvider` | `PSGFR40` |
+| `MakeComponentSurfaceNonPublicCodeFixProvider` | `PSGFR41` |
+| `DisposeTestResultCodeFixProvider` | `PSGFR42` |
+| `ReorganizeExtensionClassCodeFixProvider` (renames, splits, moves, updates referencing files) | `PSGFR35`, `PSGFR36`, `PSGFR37` |
+| `ConvertToExtensionBlockCodeFixProvider` | `PSGFR34` |
+| `AddExtensionClassMetadataCodeFixProvider` | `PSGFR38` |
+| `MakeTypeLibrarySpecNonPublicCodeFixProvider` | `TLB0021` |
+| `AttributeDataModelSymbolPropertyCodeFixProvider` | attribute-data-model symbol properties |
+| `TypeLibraryMemberAccessibilityCodeFixProvider`, `TypeLibraryMarkerDefaultInitializerCodeFixProvider`, `MakeTypeLibrarySpecPartialCodeFixProvider`, `RenameTypeLibrarySpecCodeFixProvider`, `TypeLibraryMemberTypeCodeFixProvider` | type-library rules |
+
+## Related skills
+
+`purview-sourcegenerator-codewriter` for the emission patterns the `CodeWriter` rules push you
+towards, and `purview-sourcegenerator-testing` for proving a pipeline actually caches.
diff --git a/skills/purview-sourcegenerator-packaging/SKILL.md b/skills/purview-sourcegenerator-packaging/SKILL.md
new file mode 100644
index 0000000..92350db
--- /dev/null
+++ b/skills/purview-sourcegenerator-packaging/SKILL.md
@@ -0,0 +1,245 @@
+---
+name: purview-sourcegenerator-packaging
+description: >-
+ Use when packaging or referencing a source generator built on Purview.SourceGeneratorFramework -
+ the self-contained merged analyzer, GetSourceGeneratorAnalyzerFiles, embedding a generator in
+ another package, PurviewGeneratorVisibleProperty, analyzer-closure validation, Roslyn version
+ compatibility, and errors such as CS0433, CS8784 or MSB3073 from a loaded analyzer.
+metadata:
+ product: Purview.SourceGeneratorFramework
+ repository: purview-dev/sourcegenerator-framework
+---
+
+# Packaging a source generator
+
+The rule everything else follows from: **a shipped Roslyn component carries its own copy of the
+framework and references no loose `Purview.SourceGeneratorFramework.dll`.** Two generators built
+against different framework versions must be able to load into one compiler host. A shared DLL under
+`analyzers/` cannot give you that — whichever version loads first wins, and the other fails with
+`MissingMethodException` or `CS0433`.
+
+The framework delivers this by IL-merging and internalizing its implementation into each component's
+**intermediate** output, leaving the component's `bin` unmerged.
+
+## Referencing the framework from a component
+
+```xml
+
+ true
+
+
+
+
+
+```
+
+And from a consuming project, as an analyzer:
+
+```xml
+
+```
+
+`Purview.BuildSdk` invokes `GetSourceGeneratorAnalyzerFiles` automatically — naming
+`Targets="GetSourceGeneratorAnalyzerFiles"` explicitly still works but is not required.
+
+## The three consumption paths
+
+| Path | Trigger | What you get |
+| --- | --- | --- |
+| `GetSourceGeneratorAnalyzerFiles` (default) | a consuming project references the component as an analyzer | the **merged** component, from the content-addressed `purview-merged//`. `bin` stays unmerged, so an in-process test harness keeps shared framework type identity and `InternalsVisibleTo` access without `CS0433`. |
+| `GetSourceGeneratorAnalyzerFiles` with `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=false` | opt-out | unmerged component **plus** the loose framework DLL from the package `lib/`. |
+| `GetPurviewMergedAnalyzerFile` | the framework's own bundled-component pack, or a third-party package embedding the generator | the merged component; `bin` is never overwritten. |
+| `GetPurviewMergedAnalyzerFileForPack` (pack-time, via `TargetsForTfmSpecificContentInPackage`) | packing a standalone packable generator | the merged DLL and PDB contributed straight to `analyzers/dotnet/cs`. `bin` is never mutated, so its shape does not depend on whether `build` or `pack` ran last. |
+
+Two properties exist and both should normally be left alone:
+
+- `PurviewEmbedSourceGeneratorFramework=false` — only for a project that *is* the framework
+ compile-time library. A published generator package must never disable embedding.
+- `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=false` — only when the analyzer-files
+ consumer must keep the unmerged + loose-DLL shape (a generator shipped into one compiler process
+ alongside an incompatible framework version).
+
+`PSGFR39` errors when a **non-packable** component opts out while embedding the framework, because
+that shape, packed through the GASF path, forces the loose DLL back under `analyzers/`.
+
+## A component that references another component
+
+A code-fix or analyzer component can reference the generator component normally (`ProjectReference`,
+`ReferenceOutputAssembly` **not** `false`) when it needs the generator's internal diagnostic
+identity. Both are `IsRoslynComponent`; only the generator references the framework.
+
+- The referenced component is returned as **its own merged assembly** and is never IL-merged into the
+ dependent one — that would duplicate its types, including `InternalsVisibleTo`-visible internals,
+ inside a second analyzer in the same host.
+- The framework assembly is declared as copy-to-output content beside the generator's `bin`, and
+ content with `CopyToOutputDirectory` flows transitively through `ProjectReference`. That is what
+ lets Visual Studio load the dependent code-fix provider from its own `bin` without a
+ `FileNotFoundException` for the framework assembly.
+- Visual Studio's project system resolves an `OutputItemType=Analyzer` project reference to the
+ referenced project's **unmerged** `bin` assembly and adds it to the compiler's analyzers — the
+ command-line build does not. `Purview.BuildSdk` removes that item before adding the resolved
+ closure, so Roslyn only ever receives the merged artifact. Without that removal the unmerged copy
+ drags `Purview.SourceGeneratorFramework` into the compiler host
+ (`CS8784 FileNotFoundException: Could not load file or assembly 'Purview.SourceGeneratorFramework'`)
+ and every generator registers twice.
+
+## Analyzer-closure validation
+
+`Purview.BuildSdk` validates the whole closure `GetSourceGeneratorAnalyzerFiles` returns: every
+file's PE `AssemblyRef` must resolve to another file in the returned set, or to a compiler-host
+assembly (`Microsoft.CodeAnalysis*`, `System.Composition.*`, `System.*`, `netstandard`, …). A missing
+component artifact fails the build with `PRSGD0005`.
+
+Extend the permitted set with `` or the
+`PurviewAnalyzerClosurePermittedReferences` property. Opt out with
+`PurviewSourceGeneratorFrameworkAnalyzerValidation=false`.
+
+## Generator-read MSBuild properties
+
+A generator reading `build_property.` is only correct if `` is a
+`CompilerVisibleProperty` **wherever the generator runs** — including in a consumer's project.
+Declare what the component reads:
+
+```xml
+
+
+
+```
+
+`PSGF0003` fails the build when a declared property is neither a `CompilerVisibleProperty` in the
+project nor declared by the project's own `Sdk/build` or `Sdk/buildTransitive` assets — which is what
+consumers actually receive. Opt out with
+`PurviewSourceGeneratorFrameworkGeneratorPropertyValidation=false`.
+
+NuGet imports `buildTransitive` assets for **PackageReference** consumers only. Inside the package
+repository every project uses `ProjectReference`, so `Purview.BuildSdk`'s
+`ImportProjectReferencedBuildTransitiveAssets` registers the referenced project's
+`Sdk/buildTransitive/*.props|*.targets` `CompilerVisibleProperty` items for in-repo consumers. Opt
+out with `PurviewImportProjectReferenceBuildTransitive=false`.
+
+## Embedding a generator in another package
+
+Pack the **merged** assembly, so the outer package ships no loose framework DLL:
+
+```xml
+
+ false
+ $(TargetsForTfmSpecificContentInPackage);PackMyGenerator
+
+
+
+
+
+
+
+
+ analyzers/dotnet/cs/
+
+
+
+```
+
+If the generator is also embedded at compile time for the outer package's consumers, the outer
+package must make the framework's compiler-visible properties visible to them — build assets from
+`Purview.SourceGeneratorFramework` are **not** copied into the outer package. Ship a `.props` that
+declares the property and its `CompilerVisibleProperty` entry, packed under the outer package's own
+ID so NuGet imports it:
+
+```xml
+
+```
+
+## `IsExternalInit`
+
+The framework assembly defines `System.Runtime.CompilerServices.IsExternalInit` **publicly**, so the
+bundled generators can emit `init`-based attribute types into any consumer compilation and the merge
+has a single marker definition to internalize.
+
+**Do not declare your own.** A generator-local marker gives calls to the framework's `init` setters a
+different required-custom-modifier identity from the setter definitions. The merge tool normalizes
+those modifiers in a temporary copy for compatibility with legacy tooling, but removing the redundant
+marker is the preferred configuration.
+
+## Roslyn version compatibility
+
+> The version of `Microsoft.CodeAnalysis.*` you compile the component against sets a **minimum
+> compiler-host API requirement**. The consumer's `` has nothing to do with it —
+> analyzer code runs inside the compiler/IDE host.
+
+| 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**, so a host loading it must be Roslyn 5.0 or later
+(.NET 10 SDK / Visual Studio 2026 18.0). The testing packages multi-target `net8.0`–`net10.0`, and
+Roslyn 5.x ships `net8.0`/`net9.0` assets, so those test targets still load the runner.
+
+## A generator project to start from
+
+```xml
+
+
+ netstandard2.0
+ latest
+ enable
+ true
+ false
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+`Purview.BuildSdk` already defaults most of this for a project it classifies as a Roslyn component,
+so in an SDK repository the project file is usually far shorter. The exact Roslyn baseline is a
+product-support decision.
+
+## Release gates
+
+Acceptance criteria for self-contained packaging — check metadata, never "the sample compiled":
+
+1. **No framework reference, no public framework types.** Every `analyzers/dotnet/cs/*.dll` has no
+ assembly reference to `Purview.SourceGeneratorFramework` and exposes no public type in a
+ framework-owned namespace (including the generated `Generators` attribute set and the
+ framework-emitted `Microsoft.CodeAnalysis.EmbeddedAttribute`). The one exception is the
+ component's own Roslyn entry points, which must stay public — Roslyn only instantiates public
+ components (`PSGFR27`).
+2. **No loose framework DLL in any package**, and no `*.pdb` in the `.nupkg` — symbols ship only
+ through the `.snupkg`.
+3. **Merged entry points survive**: each merged DLL still exposes its `IIncrementalGenerator`,
+ `DiagnosticAnalyzer` or `CodeFixProvider`.
+4. **Installed-package consumer test**: a generator project referencing the framework by
+ `PackageReference` packs one self-contained DLL under `analyzers/dotnet/cs`, and a consumer that
+ installs it builds with the generator producing output.
+5. **Two-version coexistence test**: build Generator A against the current framework and Generator B
+ against an intentionally binary-incompatible version, install both into one consumer, and build
+ with both package-reference orders. Both must run against their own embedded copy.
+
+## Going deeper
+
+The merge pass, the content-addressed artifact, framework type internalization and the properties
+that tune it are in [references/merge-internals.md](references/merge-internals.md) — read it when a
+merge fails, when a type leaks, or when you hit `MSB3073` / `CS0433` / `CS8784` from a loaded
+analyzer.
diff --git a/skills/purview-sourcegenerator-packaging/references/merge-internals.md b/skills/purview-sourcegenerator-packaging/references/merge-internals.md
new file mode 100644
index 0000000..5d8b6c9
--- /dev/null
+++ b/skills/purview-sourcegenerator-packaging/references/merge-internals.md
@@ -0,0 +1,139 @@
+# The merge pass and the self-contained analyzer
+
+## Where the merged artifact lives
+
+`_PurviewMergeSourceGeneratorFramework` only ever writes to the component's **intermediate** output.
+`GetSourceGeneratorAnalyzerFiles` returns that result by substituting the merged path into
+`TargetPathWithTargetPlatformMoniker` immediately before its body runs, leaving `GetTargetPath` —
+which resolves assembly references — pointing at the unmerged `bin`. That split is what keeps the
+in-repo test harness working while shipped assemblies stay self-contained.
+
+The artifact is **content-addressed**:
+
+```text
+$(IntermediateOutputPath)purview-merged//$(TargetFileName)
+```
+
+`` is a SHA-256 over the component, the framework assembly and the merge tool (the tool
+computes it in its `--tag` mode). The tag is in the **directory**, never the file name, because
+ILRepack derives the merged assembly's simple name from the output file name and an analyzer must
+keep `.dll`.
+
+Two properties follow, and both matter:
+
+- A rebuild with identical inputs reuses the existing file and **skips the merge**, so
+ `_PurviewMergeSourceGeneratorFramework` is idempotent across the per-consumer — and potentially
+ parallel — invocations of `GetSourceGeneratorAnalyzerFiles`.
+- Changed inputs select a **new directory**, so the merge never overwrites an assembly a compiler
+ host (csc, `VBCSCompiler`, Visual Studio) already has loaded. Overwriting such a file fails on
+ Windows with a sharing violation, which surfaced as `MSB3073` (merge tool exit code
+ `-532462766`) and left consumers loading a stale or partially written analyzer — the root cause of
+ `CS8784 FileNotFoundException: Could not load file or assembly 'Purview.SourceGeneratorFramework'`.
+
+The tool merges into a per-process `.staging-` directory and publishes it with a
+non-overwriting directory rename. A concurrent invocation that loses the race simply observes the
+published artifact rather than failing.
+
+Packaging needs the stable `.dll` name, so the pack paths
+(`GetPurviewMergedAnalyzerFile`, `GetPurviewMergedAnalyzerFileForPack`) copy the content-addressed
+artifact into `$(IntermediateOutputPath)purview-pack/` and pack that plainly named staging copy.
+
+## Framework type internalization
+
+ILRepack's `Internalize` is best-effort, and two gaps matter:
+
+- a framework type that reaches the merged component's public API surface stays public; and
+- the types the framework's own generators emit **into** the component — the
+ `Purview.SourceGeneratorFramework.Generators` attribute set, generated type libraries, marker
+ attributes — are not part of the merged framework assembly at all, so ILRepack never sees them.
+
+Either gap leaks framework types out of what must be a self-contained analyzer, and any project
+loading that analyzer alongside the real framework assembly fails with `CS0433` ambiguity for every
+leaked type.
+
+So the merge tool runs a deterministic internalization pass (`FrameworkTypeInternalizer`) after
+ILRepack finishes:
+
+- Every type in a framework-owned namespace (`Purview.SourceGeneratorFramework` and its children,
+ including the generated `Generators` attribute set) becomes non-public. Ownership is evaluated
+ through the **declaring chain**, because Mono.Cecil reports an empty namespace for a nested type:
+ the framework's nested containers — generated type-library namespace classes, nested operator/enum
+ groups, `CodeWriter` scopes — are only reachable through their declaring type.
+- Types the framework's generators emitted into the component, recognized by the tool name on their
+ `System.CodeDom.Compiler.GeneratedCodeAttribute` (`TypeLibraryGenerator`,
+ `AttributeDataModelGenerator`), become non-public too, so a generated type library can never leak a
+ `TypeIdentity`/`TypeReference` member as public API.
+- `Microsoft.CodeAnalysis.EmbeddedAttribute`, the framework-emitted marker, becomes non-public.
+- **Roslyn component entry points are never internalized.** A generator, analyzer, code fix provider
+ or refactoring provider reachable from the framework namespace stays public, because Roslyn only
+ instantiates public components (`PSGFR27`). This is what keeps the framework's own bundled
+ analyzers working after the pass.
+- The pass reports the result. Leftover public framework types fail the merge with **exit code 5**
+ (`PSGFR43`). Public component members whose signature exposes a framework type are logged
+ (`PSGFR41`) so the author can make them, or their declaring type, non-public. Visibility is
+ followed through the declaring chain here too: a public nested type inside an internal type — for
+ example the compiler-synthesised `$`/`$` extension containers emitted for an extension class
+ — is not reachable from outside the component, so it is not part of the public surface and is not
+ reported.
+
+Ownership is seeded with the framework assembly's own type identities, so a framework type living
+*outside* the framework namespace (`Microsoft.CodeAnalysis.*Extensions`, `System.StringExtensions`)
+is internalized as well. The artifact's assembly-level `InternalsVisibleTo` /
+`IgnoresAccessChecksTo` grants are removed, because a shipped analyzer is not the component's own
+assembly.
+
+## Tuning the pass
+
+| Property | Default | Effect |
+| --- | --- | --- |
+| `PurviewMergeOwnedNamespaces` | empty | Extra namespace prefixes (semicolon-separated) treated as framework-owned. |
+| `PurviewMergeOwnedTypeFullNames` | empty | Extra type full names (semicolon-separated) treated as framework-owned. |
+| `PurviewMergePublicSurfaceSeverity` | `message` | How a finding is reported: `message` (plain log text), `warning`/`error` (MSBuild diagnostics with code `PSGFR41`), or `none`. |
+| `PurviewMergePublicSurfaceValidation` | `true` | `false` sets the severity to `none`. |
+| `PurviewMergePublicSurfaceOrigin` | the merged component assembly | The origin reported with an MSBuild finding; point it at a source or project path for IDE navigation. |
+
+All five participate in the content tag, so changing one re-runs the merge — and re-emits its
+findings — rather than reusing a cached artifact. The compiler analyzer with the same id (`PSGFR41`)
+reports the same condition while you edit, so the surface is usually fixed before the merge runs.
+
+## Generated accessibility: bin versus merged artifact
+
+The component's own generated types — the type library, the attribute data models — keep their
+generated accessibility in the component's **bin output**. `TLB0015` requires a hand-written partial
+to be `public static partial` so it can merge with the generated library, and in-repo consumers such
+as code fixers and sibling assemblies compile against it.
+
+Only the **merged analyzer artifact** internalizes them. Self-containment is therefore enforced at
+the merge boundary without rewriting the accessibility an in-repo consumer compiles against, and that
+split is safe precisely because a merged artifact is never referenced as a compile-time dependency.
+
+> A merged component is an analyzer artifact. Never reference it as a compile-time dependency. Tests
+> that need to run a *packaged* generator load it out of band.
+
+## How the framework package itself is assembled
+
+`Purview.SourceGeneratorFramework` is dual-role: the framework assembly ships in `lib/` so generator
+projects compile against it, and `analyzers/` carries self-contained generator, analyzer and
+code-fixer assemblies. The bundled projects are:
+
+- `SourceGeneratorFramework.Generators` — `AttributeDataModelGenerator`, `TypeLibraryGenerator`;
+- `SourceGeneratorFramework.Analyzers` — the `PSGFR*` and `TLB*` analyzers;
+- `SourceGeneratorFramework.CodeFixers` — the code fix providers.
+
+Each has the framework implementation merged and internalized into it. The package deliberately does
+**not** put `Purview.SourceGeneratorFramework.dll` under `analyzers/dotnet/cs`, so generators built
+against different framework versions never ask Roslyn to load competing versions of a same-named
+dependency.
+
+The shared models and helpers that once shipped as a separate
+`Purview.SourceGeneratorFramework.Shared.dll` are compiled directly into the framework assembly
+(`SourceGeneratorShared.Link.targets` links the sources). Consumers get one
+`Purview.SourceGeneratorFramework.dll`, which removes the version-skew hazard a separately bundled
+Shared assembly caused — generator packages loading a different Shared version in-process failed with
+binary-incompatibility errors, such as removed `PurviewTypeLibrary` fields.
+
+Those projects are `IsRoslynComponent = true` and are **not** packable on their own. The
+`SourceGeneratorFramework` project builds them via `GetPurviewMergedAnalyzerFile` and packs them
+under `analyzers/dotnet/cs/` in `BuildAndPackBundledAnalyzerAssemblies`. They were previously
+consumed as analyzer project references, but they now reference the framework assembly for the shared
+types, which would form a project-reference cycle.
diff --git a/skills/purview-sourcegenerator-pipeline/SKILL.md b/skills/purview-sourcegenerator-pipeline/SKILL.md
new file mode 100644
index 0000000..373942f
--- /dev/null
+++ b/skills/purview-sourcegenerator-pipeline/SKILL.md
@@ -0,0 +1,231 @@
+---
+name: purview-sourcegenerator-pipeline
+description: >-
+ Use when composing an incremental pipeline with Purview.SourceGeneratorFramework -
+ IncrementalPipeline helpers, GenerationContext and custom contexts, CreateCodeWriter lifetime,
+ GeneratorResult and blocking versus non-blocking diagnostics, disabling a generator from MSBuild,
+ scope validation, test logging, and stage tracking names.
+metadata:
+ product: Purview.SourceGeneratorFramework
+ repository: purview-dev/sourcegenerator-framework
+---
+
+# Incremental pipeline composition
+
+`IncrementalPipeline` composes `IncrementalValueProvider` and `IncrementalValuesProvider`:
+attribute-based discovery, generation-context creation, disable-property checks, and thin
+source-output registration.
+
+Everything here follows from one rule: **pipeline values must be immutable and value-equatable.**
+Roslyn caches the complete value a provider publishes, and offers no way to exclude one property of
+it from caching.
+
+## `GenerationContext`
+
+A base execution-services context carrying the Roslyn `Compilation`, immutable `GenerationSettings`,
+an optional `ISourceGenLogger`, and a factory for independently owned `CodeWriter` instances.
+
+```csharp
+var contextProvider = IncrementalPipeline.DefaultGenerationContextValueProvider(context);
+```
+
+### Custom contexts
+
+A custom context does not need to accept or read build properties itself:
+
+```csharp
+public sealed class MyGenerationContext : GenerationContext
+{
+ public MyGenerationContext(Compilation compilation, GenerationSettings settings, ISourceGenLogger? logger)
+ : base(compilation, settings, logger)
+ {
+ }
+}
+
+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 logger is created internally only when logging is
+enabled and a sink is registered for that run.
+
+## `CodeWriter` lifetime — the rule that bites
+
+Treat `GenerationContext` as **cached incremental state** and each `CodeWriter` as **mutable,
+output-scoped execution state**. Create the writer inside the registered source-output callback,
+after the incremental cache boundary:
+
+```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());
+ }
+);
+```
+
+Creating it in the callback and passing it to emitter helpers called from that same callback is the
+intended pattern. The only forbidden thing is **persisting the writer in pipeline state**.
+
+Why, concretely:
+
+- Roslyn caches the whole published value; you cannot exempt a field.
+- `CodeWriter` is mutable, so a cached one retains previously written source when the context is
+ reused for another output or run.
+- Source-output callbacks may process independent targets **concurrently**; a shared writer mixes
+ their output and races.
+- A fresh writer gives each generated source independent scope tracking and deterministic ownership.
+
+`CreateCodeWriter()` returns a new, independently owned instance every call.
+
+**Never add a `CodeWriter` property or field to 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.
+
+When an emitter needs both services and writing, pass the context and the writer separately, or
+compose a short-lived wrapper created inside the callback — which must never be returned from a
+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);
+}
+```
+
+## `GeneratorResult` and diagnostics that do not stop generation
+
+`RegisterSourceOutput` combines targets with the generation context, reports diagnostics, and runs
+the 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);
+ }
+);
+```
+
+The callback runs only when `GeneratorResult.ShouldProcess` is `true` — the result carries a value
+**and** none of its diagnostics are blocking.
+
+`ReportableDiagnostic.IsBlocking` is an explicit per-diagnostic decision, **independent of
+severity**. An `Error`-severity diagnostic can still allow generation to continue, which is what you
+want when the generated code is what helps the developer fix the problem — a generator emitting an
+abstract base class can report an error per 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
+);
+
+// IsBlocking is false, so ShouldProcess stays true and the base class is still emitted.
+var diagnostic = ReportableDiagnostic.Create(MissingOverride, isBlocking: false, symbol, symbol.Name, "Execute");
+
+return GeneratorResult.Create(model, diagnostic);
+```
+
+`isBlocking: true` stops generation for that target while still reporting.
+`HasBlockingDiagnostics` reports whether any carried diagnostic blocked;
+`HasErrorDiagnostics` reports the severity-based view.
+
+## Disabling a generator at build time
+
+Pass the compiler-visible disable property to the context provider; its resolved value lands 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` is still there for when expensive upstream transforms must be filtered
+before being combined with the generation context.
+
+Declare the property with `PurviewGeneratorVisibleProperty` so it is compiler-visible wherever the
+generator runs — `PSGF0003` otherwise fails the build. See the `purview-sourcegenerator-packaging`
+skill.
+
+## Scope validation
+
+The default context provider reads
+`PurviewSourceGeneratorFrameworkValidateCodeWriterScopes` and threads it into
+`GenerationSettings.ValidateCodeWriterScopes`. When enabled, `ToString()` throws
+`CodeWriterScopeValidationException` if `OpenScopeCount` is not zero.
+
+## Test logging
+
+Framework logging is off 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 implement no logging interface and no logging-support
+source is generated.
+
+The sink registry stores **callbacks only**; it never buffers entries. With logging disabled, a
+missing session ID, or no matching sink, the provider supplies no logger and log calls are discarded.
+Test sinks own whatever they capture and are removed when the run completes.
+
+## Tracking names
+
+The pipeline helpers assign a tracking name to every stage, so a cache test can assert which stages
+were recomputed. The `purview-sourcegenerator-testing` skill covers the stage matrix and the four
+scenarios a cache test should assert.
+
+## Related skills
+
+`purview-sourcegenerator-codewriter` for emission, `purview-sourcegenerator-diagnostics` for the
+`PSGFR` rules that enforce most of the above, and `purview-sourcegenerator-testing` for proving the
+pipeline caches.
diff --git a/src/src/SourceGeneratorFramework.Testing/Sdk/.agents/skills/source-generator-testing/SKILL.md b/skills/purview-sourcegenerator-testing/SKILL.md
similarity index 95%
rename from src/src/SourceGeneratorFramework.Testing/Sdk/.agents/skills/source-generator-testing/SKILL.md
rename to skills/purview-sourcegenerator-testing/SKILL.md
index fdbfc45..aaeff09 100644
--- a/src/src/SourceGeneratorFramework.Testing/Sdk/.agents/skills/source-generator-testing/SKILL.md
+++ b/skills/purview-sourcegenerator-testing/SKILL.md
@@ -1,6 +1,13 @@
---
-name: source-generator-testing
-description: "Use when writing or fixing tests for source generators, diagnostic analyzers, code fixes, or refactorings in a Purview.SourceGeneratorFramework repository — picking the right runner/base, configuring options, querying produced code with CodeQuery, and asserting incremental caching."
+name: purview-sourcegenerator-testing
+description: >-
+ Use when writing or fixing tests for source generators, diagnostic analyzers, code fixes or
+ refactorings built on Purview.SourceGeneratorFramework - picking the right runner and base class,
+ configuring SourceGeneratorTestOptions, querying produced code with CodeQuery, and asserting
+ incremental pipeline caching stage by stage.
+metadata:
+ product: Purview.SourceGeneratorFramework
+ repository: purview-dev/sourcegenerator-framework
---
# Testing source generators, analyzers, code fixes and refactorings
@@ -9,7 +16,10 @@ Use this skill whenever a task involves authoring, fixing, or modernising tests
(generators, diagnostic analyzers, code fix providers, refactoring providers) built with
`Purview.SourceGeneratorFramework`. It covers the framework-agnostic test runner layer and the
`CodeQuery` syntax-lookup API. For the TUnit base classes and assertion extensions, also load the
-`sdk` package's `tunit-test-authoring` skill.
+`purview-sourcegenerator-tunit-tests` skill.
+
+Modernising an existing suite onto `CodeQuery` and adding a stage-by-stage cache test? The playbook
+is in [references/test-modernisation.md](references/test-modernisation.md).
## Picking the right runner
@@ -21,7 +31,7 @@ Use this skill whenever a task involves authoring, fixing, or modernising tests
| — | `CodeFixTestRunner.RunFixAllAsync` (project-wide) |
| `CodeRefactoringProvider` | `RefactoringTestRunner` |
-TUnit projects should prefer the matching base class instead (see `tunit-test-authoring`):
+TUnit projects should prefer the matching base class instead (see `purview-sourcegenerator-tunit-tests`):
`TUnitSourceGeneratorTestBase`, `TUnitDiagnosticAnalyzerTestBase`, `TUnitCodeFixTestBase`,
`TUnitRefactoringTestBase`.
@@ -152,7 +162,10 @@ declaration") flags a `DriverRunResult` that is produced and never disposed —
where a helper creates a result, projects a value out of it and returns only the projection. It is a
`warning` when the run syntactically compiles to an assembly (`.Compile()`, a `Compile` options member, or
`CompileToAssembly = true`) and `info` otherwise; the `DisposeTestResultCodeFixProvider` fix inserts the
-`using`. A result that is returned to the caller, or disposed explicitly, is not flagged.
+`using`. A result that is returned to the caller, or disposed explicitly, is not flagged. Neither is awaiting
+an assertion, nor calling a helper that is handed the result — an extension on it, or a method taking it as a
+parameter: both hand the same instance back, so `await Assert.That(result).SomeAssertion()` and
+`result.AssertNoGenerationExceptions()` are not flagged.
## Best practices
diff --git a/skills/purview-sourcegenerator-testing/references/test-modernisation.md b/skills/purview-sourcegenerator-testing/references/test-modernisation.md
new file mode 100644
index 0000000..6db4780
--- /dev/null
+++ b/skills/purview-sourcegenerator-testing/references/test-modernisation.md
@@ -0,0 +1,63 @@
+# Modernising a Roslyn test suite
+
+A playbook for moving an existing test suite onto `CodeQuery` plus the TUnit assertion extensions,
+and adding a stage-by-stage incremental cache test.
+
+## Before you start, collect
+
+- The test file(s) being changed.
+- The Roslyn component under test — generator, analyzer, code fix or refactor (infer it from the
+ base class if the task does not say).
+- The component's type name.
+
+## Requirements
+
+### 1. The right base class and method
+
+| Component | Base class | Method |
+| --- | --- | --- |
+| Generator | `TUnitSourceGeneratorTestBase` | `GenerateAsync` |
+| Analyzer | `TUnitDiagnosticAnalyzerTestBase` | `AnalyzeAsync` |
+| Code fix | `TUnitCodeFixTestBase` | `ApplyCodeFixAsync` / `ApplyFixAllAsync` |
+| Refactor | `TUnitRefactoringTestBase` | `RefactorAsync` |
+
+### 2. `CodeQuery` instead of string matching
+
+Replace `GetGeneratedTree(...)` + `string.Contains(...)` with `CodeQuery`
+(`result.Generated().Get/Has/TryGet…`) and the terminal assertion extensions —
+`await Assert.That(result).HasGeneratedMethod/Class/Property/Field/SyntaxTree(…)` — which return the
+node so a follow-up assertion has something to work with.
+
+### 3. `TypeReference` instead of signature strings
+
+Match parameter and return types structurally. A signature string breaks on a whitespace or
+nullability change that means nothing.
+
+### 4. Options in one place
+
+Derive a `SourceGeneratorTestOptions` record that seeds the required namespaces and additional
+assemblies, and remove the per-test duplication it replaces.
+
+### 5. A stage-by-stage cache test
+
+Use `RunIncrementalAsync`, or `GenerateIncrementalAsync` on the TUnit base, and assert the four
+scenarios. `ServiceRegistrationCacheTests` and `IncrementalPipelineCacheTests` are the reference
+pattern.
+
+| Scenario | Expected |
+| --- | --- |
+| First run | every framework stage `New` |
+| Identical rerun (`RunIncrementalAsync(sources, …)` runs the same source twice) | framework stages `Cached`/`Unchanged` |
+| Source-only change | `ForAttribute_*` `Modified`; property/config stages stay `Cached` |
+| Property-only change (`new IncrementalRunInput(sources, [("build_property.X", "value")])`) | `GetMSBuildPropertyValue_*`, `GetGenerationConfiguration`, `GetGenerationContext_*` `Modified`; `ForAttribute_*` stays `Cached` |
+
+Use the `StepReasons(IncrementalCacheRun)` flattening helper. If the generator depends on its own
+post-init output, assert on the framework-named stages rather than on every tracked step.
+
+### 6. Minimal change
+
+Keep behaviour equivalent, and do not reformat unrelated tests.
+
+## Verification
+
+Build the test project and run its suite before finishing.
diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/.agents/skills/tunit-test-authoring/SKILL.md b/skills/purview-sourcegenerator-tunit-tests/SKILL.md
similarity index 91%
rename from src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/.agents/skills/tunit-test-authoring/SKILL.md
rename to skills/purview-sourcegenerator-tunit-tests/SKILL.md
index cf38b75..bc711c5 100644
--- a/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/.agents/skills/tunit-test-authoring/SKILL.md
+++ b/skills/purview-sourcegenerator-tunit-tests/SKILL.md
@@ -1,6 +1,13 @@
---
-name: tunit-test-authoring
-description: "Use when writing TUnit tests for source generators, diagnostic analyzers, code fixes, or refactorings in a Purview.SourceGeneratorFramework repository — choosing the correct base class and method, customising options, using the assertion extensions, and modernising existing tests."
+name: purview-sourcegenerator-tunit-tests
+description: >-
+ Use when writing TUnit tests for source generators, diagnostic analyzers, code fixes or
+ refactorings built on Purview.SourceGeneratorFramework - choosing the correct TUnit base class and
+ method, customising SourceGeneratorTestOptions, using the TUnit assertion extensions, and
+ modernising existing tests.
+metadata:
+ product: Purview.SourceGeneratorFramework.Testing.TUnit
+ repository: purview-dev/sourcegenerator-framework
---
# TUnit test authoring for Roslyn components
@@ -9,7 +16,7 @@ Use this skill whenever a task involves authoring, fixing, or modernising **TUni
components built with `Purview.SourceGeneratorFramework`. It tells you which base class to derive from,
which method to call, how to customise options with an easy starting point, and how to use the TUnit
assertion extensions. For the framework-agnostic runner layer and the `CodeQuery` API, also load the
-`sdk` package's `source-generator-testing` skill.
+`purview-sourcegenerator-testing` skill.
## Base class → method matrix
@@ -60,7 +67,9 @@ Use the base hooks to customise per-run: `OnBeforeRun`/`OnBeforeRunAsync` (mutat
Write `using var result = await GenerateAsync(...)`: `DriverRunResult` is `IDisposable`, and `PSGFR42`
flags a result that is produced and never disposed (a `warning` once the run compiles to an assembly,
`info` otherwise). It also catches the indirect case — a helper that creates a result, projects a value out
-of it and returns only the projection.
+of it and returns only the projection. Awaiting an assertion, or calling a helper that is handed the result —
+an extension on it, or a method taking it as a parameter — is not a production: both hand the same instance
+back, so neither is flagged.
For code fixes/refactorings, select a specific registered action via `CodeFixTestOptions.EquivalenceKey`
or `CodeActionIndex`, and `RefactorTestOptions.Span`/`NodeSelector` (e.g.
@@ -121,7 +130,7 @@ when awaited.
(whitespace-flattened string comparison).
For structural assertions (members, signatures, namespaces) prefer `result.Generated()` +
-`Get/Has/TryGet` from `CodeQuery` (see `source-generator-testing`).
+`Get/Has/TryGet` from `CodeQuery` (see `purview-sourcegenerator-testing`).
## Incremental cache tests (`GenerateIncrementalAsync`)
@@ -129,7 +138,7 @@ For structural assertions (members, signatures, namespaces) prefer `result.Gener
also wires the base class hooks (`OnBeforeRun`/`OnBeforeRunAsync`) and your derived options record. Use it to
prove the pipeline caches stage-by-stage. The reference is `ServiceRegistrationCacheTests` in
`SourceGeneratorFramework.ExampleGenerator.UnitTests`; the framework-agnostic twin with a full walkthrough is
-in the `source-generator-testing` skill.
+in the `purview-sourcegenerator-testing` skill.
Why the tests look the way they do:
@@ -146,7 +155,7 @@ Why the tests look the way they do:
output step is cached can be asserted to have logged nothing. Empty when `EnableLogging` is disabled.
- The `StepReasons(IncrementalCacheRun)` helper flattens each tracked step's `Outputs` into a
`ImmutableDictionary>` so assertions can address a stage
- by name (see `source-generator-testing` for the helper body).
+ by name (see `purview-sourcegenerator-testing` for the helper body).
- If the generator's pipeline depends on its own post-initialization output (a self-referencing generated
attribute), Roslyn's internal `ForAttributeWithMetadataName` `Compilation` step is legitimately `Modified`
on rerun; assert on the framework-named stages (e.g. `ForAttribute_GenerateServiceAttribute`,
@@ -213,7 +222,7 @@ When converting legacy tests that do `result.GetGeneratedTree(...)` + `string.Co
per test.
5. Add a stage-by-stage cache test if the component has an incremental pipeline — first run `New`,
identical rerun `Cached`/`Unchanged`, and targeted changes mark only the affected stage `Modified`.
- Use the inlined examples in this skill and in `source-generator-testing`'s "Incremental cache testing"
+ Use the inlined examples in this skill and in `purview-sourcegenerator-testing`'s "Incremental cache testing"
section, swapping in your own generator and stage names.
## License
diff --git a/src/SourceGeneratorFramework.slnx b/src/SourceGeneratorFramework.slnx
index 7546cfb..da0c6b9 100644
--- a/src/SourceGeneratorFramework.slnx
+++ b/src/SourceGeneratorFramework.slnx
@@ -41,6 +41,7 @@
+
diff --git a/src/settings.VisualStudio.json b/src/settings.VisualStudio.json
new file mode 100644
index 0000000..194ba35
--- /dev/null
+++ b/src/settings.VisualStudio.json
@@ -0,0 +1,4 @@
+/* Visual Studio Settings File */
+{
+ "csharpier.general.runonsave": true
+}
diff --git a/src/src/SourceGeneratorFramework.Analyzers/UndisposedTestResultAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/UndisposedTestResultAnalyzer.cs
index b5598ad..83dfea0 100644
--- a/src/src/SourceGeneratorFramework.Analyzers/UndisposedTestResultAnalyzer.cs
+++ b/src/src/SourceGeneratorFramework.Analyzers/UndisposedTestResultAnalyzer.cs
@@ -67,6 +67,9 @@ static void AnalyzeLocalDeclaration(SyntaxNodeAnalysisContext context)
if (Unwrap(initializer) is not InvocationExpressionSyntax and not ObjectCreationExpressionSyntax)
continue;
+ if (!ProducesResult(initializer, context))
+ continue;
+
if (
context.SemanticModel.GetDeclaredSymbol(declarator, context.CancellationToken) is not ILocalSymbol local
)
@@ -112,6 +115,9 @@ expression is AssignmentExpressionSyntax
if (produced is not InvocationExpressionSyntax and not ObjectCreationExpressionSyntax)
return;
+ if (!ProducesResult(expression, context))
+ return;
+
var type = context.SemanticModel.GetTypeInfo(expression, context.CancellationToken).Type;
if (!IsDisposableTestingResult(type))
return;
@@ -137,6 +143,84 @@ static ExpressionSyntax Unwrap(ExpressionSyntax expression) =>
_ => expression,
};
+ ///
+ /// Determines whether the expression produces a result, rather than consuming one the caller already
+ /// owns or handing one back.
+ ///
+ ///
+ /// Two shapes consume rather than produce. An assertion that is awaited hands the subject that was
+ /// asserted on back to the caller (await Assert.That(result).SomeAssertion()), so only a
+ /// task-like await — the shape every harness runner returns — produces. A helper that is handed the
+ /// result — an extension on it (result.AssertNoGenerationExceptions()), or a method taking it as
+ /// a parameter — returns that same instance, so it produces nothing either.
+ ///
+ static bool ProducesResult(ExpressionSyntax expression, SyntaxNodeAnalysisContext context)
+ {
+ var awaited = false;
+ var current = expression;
+
+ while (true)
+ {
+ if (current is ParenthesizedExpressionSyntax parenthesized)
+ {
+ current = parenthesized.Expression;
+ continue;
+ }
+
+ if (current is AwaitExpressionSyntax awaitExpression)
+ {
+ awaited = true;
+ current = awaitExpression.Expression;
+ continue;
+ }
+
+ break;
+ }
+
+ if (awaited && !IsTaskLike(context.SemanticModel.GetTypeInfo(current, context.CancellationToken).Type))
+ return false;
+
+ return current is not InvocationExpressionSyntax invocation || !ConsumesResult(invocation, context);
+ }
+
+ ///
+ /// Determines whether the invocation is handed an existing harness result — as an extension receiver or
+ /// as an argument — and therefore consumes one instead of producing one.
+ ///
+ ///
+ /// A helper that is given a result hands that same instance back when it returns a result at all, so it
+ /// never creates something new to dispose. Only a call that produces a result itself is reported.
+ ///
+ static bool ConsumesResult(InvocationExpressionSyntax invocation, SyntaxNodeAnalysisContext context)
+ {
+ if (
+ context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol
+ is not IMethodSymbol method
+ )
+ return false;
+
+ // An extension method called with instance syntax binds to a reduced symbol whose parameter list
+ // omits the receiver, so the receiver and the unreduced definition are both inspected.
+ if (IsDisposableTestingResult(method.ReceiverType))
+ return true;
+
+ foreach (var parameter in (method.ReducedFrom ?? method).Parameters)
+ {
+ if (IsDisposableTestingResult(parameter.Type))
+ return true;
+ }
+
+ return false;
+ }
+
+ static bool IsTaskLike(ITypeSymbol? type) =>
+ type is INamedTypeSymbol named
+ && named.OriginalDefinition.ToDisplayString()
+ is "System.Threading.Tasks.Task"
+ or "System.Threading.Tasks.Task"
+ or "System.Threading.Tasks.ValueTask"
+ or "System.Threading.Tasks.ValueTask";
+
static bool IsDisposableTestingResult(ITypeSymbol? type)
{
if (type is not INamedTypeSymbol named)
@@ -152,6 +236,7 @@ @namespace is null
)
return false;
+ // The result type is disposable if it implements either IDisposable or IAsyncDisposable, which is the case for all of the test harness result types.
return named.AllInterfaces.Any(static i =>
i.ToDisplayString() is "System.IDisposable" or "System.IAsyncDisposable"
);
diff --git a/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
index 76be836..276f28b 100644
--- a/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
+++ b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
@@ -24,7 +24,11 @@ static class MergeToolRunner
///
/// Reported when the merged artifact still exposes framework types; the merge fails.
///
- const string PublicSurfaceBlockedCode = "PSGFR42";
+ ///
+ /// PSGFR43, not PSGFR42: the compiler-analyzer family owns PSGFR42 for the
+ /// undisposed test-harness result rule, so the merge's blocked code takes the next free number.
+ ///
+ const string PublicSurfaceBlockedCode = "PSGFR43";
const string DefaultDiagnosticOrigin = "Purview.SourceGeneratorFramework";
diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/.agents/agents/test-author-writer.agent.md b/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/.agents/agents/test-author-writer.agent.md
deleted file mode 100644
index fb5fb87..0000000
--- a/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/.agents/agents/test-author-writer.agent.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-name: Test Author Writer
-description: "Specialist for Purview.SourceGeneratorFramework test suites — writing, fixing, and modernising TUnit tests for generators, diagnostic analyzers, code fixes, and refactorings, and for adding stage-by-stage incremental cache tests."
-tools:
- [
- "search/codebase",
- "edit/editFiles",
- "search",
- "execute/getTerminalOutput",
- "execute/runInTerminal",
- "read/terminalLastCommand",
- "read/terminalSelection",
- "execute/createAndRunTask",
- "execute/runTask",
- "read/getTaskOutput",
- "vscodeTasks/createAndRunTask",
- "vscodeTasks/getTaskOutput",
- "vscodeTasks/runTask",
- ]
----
-
-You are a specialist for `Purview.SourceGeneratorFramework` test authoring.
-
-## Primary objective
-
-Produce correct, maintainable TUnit tests for source generators, diagnostic analyzers, code fix
-providers, and refactoring providers, and prove incremental pipelines cache correctly.
-
-## Background knowledge
-
-Before writing or changing any test, load and apply the `source-generator-testing` skill (runner layer,
-result types, `CodeQuery`, options, cache testing) and the `tunit-test-authoring` skill (base classes,
-methods, assertion extensions, modernisation checklist). For source-generator emission work, also load the
-`source-generator-codewriter-modernization` skill.
-
-Key rules:
-
-- Pick the base class by the Roslyn component type: generator → `TUnitSourceGeneratorTestBase` +
- `GenerateAsync`; analyzer → `TUnitDiagnosticAnalyzerTestBase` + `AnalyzeAsync`; code fix →
- `TUnitCodeFixTestBase` + `ApplyCodeFixAsync`/`ApplyFixAllAsync`; refactor →
- `TUnitRefactoringTestBase` + `RefactorAsync`.
-- Prefer `CodeQuery` (`result.Generated()` / `result.FixedCode()` with `Get/Has/TryGet`) over
- raw-string assertions.
-- Prefer the terminal assertion extensions (`HasGeneratedMethod`, `HasGeneratedClass`, …) that return
- syntax nodes.
-- Derive a `SourceGeneratorTestOptions` record that seeds namespaces and additional assemblies.
-- For incremental pipelines, add a stage-by-stage cache test with `RunIncrementalAsync` /
- `GenerateIncrementalAsync`, asserting `New` on first run and `Cached`/`Unchanged` on an identical rerun,
- and `Modified` only on the stages whose inputs changed.
-- Keep generated-output assertions deterministic (no timestamps); enable CodeWriter scope validation.
\ No newline at end of file
diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/.agents/prompts/modernize-test-to-codequery-tunit.prompt.md b/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/.agents/prompts/modernize-test-to-codequery-tunit.prompt.md
deleted file mode 100644
index cf463ce..0000000
--- a/src/src/SourceGeneratorFramework.Testing.TUnit/Sdk/.agents/prompts/modernize-test-to-codequery-tunit.prompt.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-agent: ask
-description: "Modernise a Roslyn test suite to use CodeQuery + TUnit assertion extensions, and add a stage-by-stage incremental cache test."
----
-
-You are modernising tests in this repository. Apply the guidance from the `source-generator-testing` and
-`tunit-test-authoring` skills for picking the right base class, querying generated code with `CodeQuery`,
-and asserting incremental caching.
-
-## Inputs
-
-- Target test file(s): `${input:targetFiles:Path(s) to test file(s)}`
-- Roslyn component under test: `${input:componentType:generator|analyzer|codefix|refactor}` (inferred if blank)
-- Generator/analyzer/code-fix/refactor type name: `${input:componentName:Component type name}`
-
-## Task
-
-Modernise each test so it uses the framework's `CodeQuery` syntax-lookup API and the TUnit assertion
-extensions, and add a stage-by-stage cache test proving each incremental pipeline layer caches correctly.
-
-### Requirements
-
-1. Choose the correct base class and method for the component type:
- - Generator → `TUnitSourceGeneratorTestBase` → `GenerateAsync`.
- - Analyzer → `TUnitDiagnosticAnalyzerTestBase` → `AnalyzeAsync`.
- - Code fix → `TUnitCodeFixTestBase` → `ApplyCodeFixAsync` / `ApplyFixAllAsync`.
- - Refactor → `TUnitRefactoringTestBase` → `RefactorAsync`.
-2. Replace `GetGeneratedTree(...)` + `string.Contains(...)` assertions with `CodeQuery`
- (`result.Generated().Get/Has/TryGet…`) and the terminal assertion extensions
- (`await Assert.That(result).HasGeneratedMethod/Class/Property/Field/SyntaxTree(…)`) that return the node.
-3. Replace signature string checks with `TypeReference` parameter/return-type matching.
-4. Ensure options come from a derived `SourceGeneratorTestOptions` record seeding the required namespaces
- and additional assemblies; remove per-test duplication.
-5. Add an incremental cache test using `RunIncrementalAsync` (or `GenerateIncrementalAsync` on the TUnit
- base) with the four scenarios from the skills' "Incremental cache testing" sections
- (`ServiceRegistrationCacheTests` / `IncrementalPipelineCacheTests` are the reference pattern):
- - first run → every framework stage `New`;
- - identical rerun (`RunIncrementalAsync(sources, …)` runs the same source twice) → framework stages
- `Cached`/`Unchanged`;
- - source-only change → `ForAttribute_*` `Modified`, property/config stages stay `Cached`;
- - property-only change (`new IncrementalRunInput(sources, [("build_property.X", "value")])`) →
- `GetMSBuildPropertyValue_*`/`GetGenerationConfiguration`/`GetGenerationContext_*` `Modified`,
- `ForAttribute_*` stays `Cached`.
- Use the `StepReasons(IncrementalCacheRun)` flattening helper; if the generator depends on its own
- post-init output, assert on the framework-named stages rather than every tracked step.
-6. Keep changes minimal and behavior equivalent; do not reformat unrelated tests.
-
-Verify by building the test project and running its suite before finishing.
\ No newline at end of file
diff --git a/src/src/SourceGeneratorFramework.Testing.TUnit/SourceGeneratorFramework.Testing.TUnit.csproj b/src/src/SourceGeneratorFramework.Testing.TUnit/SourceGeneratorFramework.Testing.TUnit.csproj
index e7e8414..c90a875 100644
--- a/src/src/SourceGeneratorFramework.Testing.TUnit/SourceGeneratorFramework.Testing.TUnit.csproj
+++ b/src/src/SourceGeneratorFramework.Testing.TUnit/SourceGeneratorFramework.Testing.TUnit.csproj
@@ -1,4 +1,4 @@
-
+
netstandard2.0;$(TestingTargetFrameworks)
true
diff --git a/src/src/SourceGeneratorFramework.Testing/Sdk/README.md b/src/src/SourceGeneratorFramework.Testing/Sdk/README.md
index d33eda6..838c629 100644
--- a/src/src/SourceGeneratorFramework.Testing/Sdk/README.md
+++ b/src/src/SourceGeneratorFramework.Testing/Sdk/README.md
@@ -153,7 +153,10 @@ a result, projects a value out of it and returns the projection leaks just as ba
helper is flagged too. The rule is a `warning` when the run syntactically compiles to an assembly (a
`Compile` options member, or `CompileToAssembly = true`), because that is when the leak actually holds an
assembly and its load contexts, and `info` otherwise. `DisposeTestResultCodeFixProvider` turns the
-declaration into a `using` declaration, with project-wide "Fix all" support.
+declaration into a `using` declaration, with project-wide "Fix all" support. Awaiting an assertion, or calling
+a helper that is handed the result — an extension on it, or a method taking it as a parameter — is not a
+production: both hand the same instance back, so neither `await Assert.That(result).SomeAssertion()` nor
+`result.AssertNoGenerationExceptions()` is flagged.
### Reference sharing and parallelism
diff --git a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorFramework.Testing.csproj b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorFramework.Testing.csproj
index b541d82..9f4d14a 100644
--- a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorFramework.Testing.csproj
+++ b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorFramework.Testing.csproj
@@ -1,4 +1,4 @@
-
+
netstandard2.0;$(TestingTargetFrameworks)
diff --git a/src/src/SourceGeneratorFramework/Extensions/System/StringExtensions.cs b/src/src/SourceGeneratorFramework/Extensions/System/StringExtensions.cs
index d695374..b771057 100644
--- a/src/src/SourceGeneratorFramework/Extensions/System/StringExtensions.cs
+++ b/src/src/SourceGeneratorFramework/Extensions/System/StringExtensions.cs
@@ -4,7 +4,7 @@
namespace System;
-[System.Diagnostics.CodeAnalysis.SuppressMessage(
+[Diagnostics.CodeAnalysis.SuppressMessage(
"Design",
"CA1034:Nested types should not be visible",
Justification = "The extension(...) block is lowered to an unnamed nested type; the language requires it to be nested in a static class, and PSGFR34 mandates extension blocks over classic 'this'-parameter methods."
diff --git a/src/src/SourceGeneratorFramework/Sdk/.agents/agents/source-generator-framework-writer.agent.md b/src/src/SourceGeneratorFramework/Sdk/.agents/agents/source-generator-framework-writer.agent.md
deleted file mode 100644
index 65f5bd0..0000000
--- a/src/src/SourceGeneratorFramework/Sdk/.agents/agents/source-generator-framework-writer.agent.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-name: Source Generator Framework Writer
-description: "Specialist for Purview.SourceGeneratorFramework generation code using CodeWriter and XmlCodeWriter-style XML doc extensions; ideal for creating or refactoring generator emitters."
-tools:
- [
- "search/codebase",
- "edit/editFiles",
- "search",
- "execute/getTerminalOutput",
- "execute/runInTerminal",
- "read/terminalLastCommand",
- "read/terminalSelection",
- "execute/createAndRunTask",
- "execute/runTask",
- "read/getTaskOutput",
- "vscodeTasks/createAndRunTask",
- "vscodeTasks/getTaskOutput",
- "vscodeTasks/runTask",
- ]
----
-
-You are a specialist for `Purview.SourceGeneratorFramework` emitter authoring.
-
-## Primary objective
-
-Produce clear, deterministic, maintainable source-generator emission code using `CodeWriter` and XML extension helpers from `XmlCommentWriter`.
-
-## Background knowledge
-
-Before changing any source generator, analyser, or CodeWriter-related code, load and apply the `source-generator-codewriter-modernization` skill. It contains the full source-generator, analyser, and CodeWriter best-practices guidance for this framework, including incremental pipeline design, value equality, deterministic output, and Roslyn version compatibility.
-
-The most important rules are:
-
-- **Analyser for validation; generator for generation.**
-- **Syntax for syntax, symbols for declarations, operations for executable semantics.**
-- **Use `ForAttributeWithMetadataName` whenever possible.**
-- **Remove `ISymbol`, `Compilation`, `SemanticModel`, `IOperation`, `SyntaxTree`, `SyntaxNode`, and `Location` from incremental pipeline models as early as possible.**
-- **Pipeline models must be immutable and value-equatable; use `EquatableArray` for collections.**
-- **Avoid `Collect()` until global knowledge is genuinely required.**
-- **Never combine `CompilationProvider` into the pipeline merely because it is convenient.**
-- **Generate deterministic output and stable hint names.**
-- **Test incrementally, not just generated text.**
-- **Compile against the oldest Roslyn API version containing the functionality you need.**
-- **Create `CodeWriter` inside the output callback and pass it to helpers within that callback; never create it earlier in the pipeline or store it in incremental provider state or custom contexts.**
-
-## Available resources
-
-- `skills/source-generator-codewriter-modernization/SKILL.md` — source-generator, analyser, and CodeWriter best practices for this framework.
-- `prompts/refactor-source-generator-to-codewriter.prompt.md` — prompt template for legacy-emitter refactor tasks.
-
-## Must-follow rules
-
-1. Load and apply the `source-generator-codewriter-modernization` skill.
-2. Prefer structured declaration APIs over handwritten declaration strings.
-3. Prefer XML helper extensions (`XmlSummary`, `XmlParam`, etc.) over raw `///` output.
-4. Create `CodeWriter` inside each output callback; never create it earlier in the pipeline or cache it in incremental provider state or custom contexts.
-5. Preserve semantic behavior while modernizing implementation style.
-6. Keep edits minimal and localized to emitter concerns.
-
-## Refactoring posture
-
-When modernizing legacy code:
-
-- Replace manual indentation/braces with scope APIs.
-- Replace signature text with declaration option records.
-- Replace ad-hoc XML tags with helper APIs.
-- Preserve diagnostics and emitted symbol names.
-
-## Quality gates
-
-- Build/tests pass for impacted projects.
-- No scope leaks when materializing generated source.
-- Generated artifacts remain deterministic and reviewable.
-
-## Skill routing
-
-When relevant, first load and apply:
-
-- `source-generator-codewriter-modernization`
diff --git a/src/src/SourceGeneratorFramework/Sdk/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md b/src/src/SourceGeneratorFramework/Sdk/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md
deleted file mode 100644
index e230d22..0000000
--- a/src/src/SourceGeneratorFramework/Sdk/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md
+++ /dev/null
@@ -1,52 +0,0 @@
----
-agent: ask
-description: "Refactor a legacy source generator emitter from string/StringBuilder to CodeWriter + XmlCodeWriter-style XML extensions with behavior parity."
----
-
-You are modernizing a source generator implementation in this repository. Apply the guidance from the `source-generator-codewriter-modernization` skill for incremental pipelines, value equality, deterministic output, and CodeWriter scope safety.
-
-## Inputs
-
-- Target file(s): `${input:targetFiles:Path(s) to emitter file(s)}`
-- Generator type name: `${input:generatorName:Generator class name}`
-- Generator version: `${input:generatorVersion:Version string (for generated attributes/header)}`
-- Keep output byte-identical where possible: `${input:preserveFormatting:true|false}`
-
-## Task
-
-Refactor the selected legacy emitter implementation from manual `string` / `StringBuilder` output construction to `CodeWriter` and XML documentation extension helpers from `XmlCommentWriter` (XmlCodeWriter-style API usage).
-
-### Requirements
-
-1. Use structured declaration APIs where applicable:
- - `Class/Struct/RecordClass/Interface/Enum`
- - `Method`, `Property`, `Field`, `Constructor`
-2. Use XML helper extensions instead of raw `///` composition:
- - `XmlSummary`, `XmlParam`, `XmlReturn`, `XmlRemarks`, `XmlCode` or `XmlCodeBlock`
-3. Use `TypeReference` when type text becomes complex (nullability, generics, arrays).
-4. Ensure writer lifetime is output-scoped (`generationContext.CreateCodeWriter()` inside callback).
-5. Preserve behavior, diagnostics, and generated names.
-6. Keep changes minimal and focused; do not reformat unrelated logic.
-
-### Migration strategy
-
-- Identify emitter phases: header, namespace, type declarations, member declarations.
-- Replace indentation/braces with scoped APIs.
-- Replace signature strings with declaration options.
-- Replace XML comments with XmlCommentWriter extension methods.
-- Keep semantic equivalence; call out any intentional deltas.
-
-### Verification
-
-- Run relevant tests.
-- Confirm generated files still compile.
-- Confirm no `CodeWriter` scope leaks (`OpenScopeCount == 0` when materialized).
-
-### Output format
-
-Return:
-
-1. Files changed
-2. Why each change was necessary
-3. Risks/behavior differences (if any)
-4. Verification performed
diff --git a/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.targets b/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.targets
index 30a9197..fc6eeae 100644
--- a/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.targets
+++ b/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.targets
@@ -15,7 +15,7 @@
artifact stays self-contained either way; a public signature over an internalized framework
type is simply unusable. 'message' (the default) keeps the historical plain-text log output,
'warning'/'error' emit MSBuild diagnostics (PSGFR41) attributed to the component, and 'none'
- drops the findings. A *leaked* public framework type always fails the merge (PSGFR42), because
+ drops the findings. A *leaked* public framework type always fails the merge (PSGFR43), because
that artifact is not self-contained.
-->
+
true
false
diff --git a/src/tests/Repository.UnitTests/Repository.UnitTests.csproj b/src/tests/Repository.UnitTests/Repository.UnitTests.csproj
new file mode 100644
index 0000000..c064629
--- /dev/null
+++ b/src/tests/Repository.UnitTests/Repository.UnitTests.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net10.0
+ false
+
+
+
+
+
+
+
diff --git a/src/tests/Repository.UnitTests/SkillTests.cs b/src/tests/Repository.UnitTests/SkillTests.cs
new file mode 100644
index 0000000..aa775f9
--- /dev/null
+++ b/src/tests/Repository.UnitTests/SkillTests.cs
@@ -0,0 +1,60 @@
+using Purview.Skills.Testing;
+
+namespace Purview.SourceGeneratorFramework.Repository;
+
+///
+/// Runs the Agent Skills conformance rule set against this repository's authored skills in
+/// skills/.
+///
+///
+///
+/// [InheritsTests] is load-bearing. TUnit does not walk base types when it discovers
+/// tests, so without it this class compiles, the run is green, and nothing is validated.
+///
+///
+/// There is no RepositoryRoot override: the default is the SDK's repository-root marker walk,
+/// the same resolution InstallPurviewSkills uses, so an ordinary test run exercises it.
+/// is what catches a walk that landed elsewhere -
+/// every inherited rule passes vacuously against a catalogue that was never found.
+///
+///
+[InheritsTests]
+public sealed class SkillTests : PurviewSkillTests
+{
+ [Test]
+ public async Task SkillsFolder_ResolvesToThisRepository()
+ {
+ var catalogue = Evaluator.Catalogue;
+
+ await Assert
+ .That(catalogue.FolderExists)
+ .IsTrue()
+ .Because($"the marker walk resolved '{catalogue.RepositoryRoot}', whose skills folder does not exist");
+
+ await Assert
+ .That(File.Exists(Path.Combine(catalogue.RepositoryRoot, "AGENTS.md")))
+ .IsTrue()
+ .Because($"'{catalogue.RepositoryRoot}' does not look like this repository");
+
+ await Assert.That(catalogue.Skills).IsNotEmpty();
+ }
+
+ [Test]
+ public async Task EverySkill_IsNamedForThisProduct()
+ {
+ // Skills install into one flat directory per agent, shared with every skill a developer has
+ // installed from anywhere. An unprefixed name - 'source-generator-testing', 'tunit-test-authoring' -
+ // collides with somebody else's, and the loser is whichever installed second.
+ var unprefixed = Evaluator
+ .Catalogue.Skills.Select(skill => skill.FolderName)
+ .Where(name => !name.StartsWith("purview-sourcegenerator", StringComparison.Ordinal))
+ .ToArray();
+
+ await Assert
+ .That(unprefixed)
+ .IsEmpty()
+ .Because(
+ "every skill this repository publishes must carry the product prefix: " + string.Join(", ", unprefixed)
+ );
+ }
+}
diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/UndisposedTestResultAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/UndisposedTestResultAnalyzerTests.cs
index 9980efa..6306007 100644
--- a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/UndisposedTestResultAnalyzerTests.cs
+++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/UndisposedTestResultAnalyzerTests.cs
@@ -9,12 +9,45 @@ public sealed class UndisposedTestResultAnalyzerTests : TUnitDiagnosticAnalyzerT
// A stand-in for the runner surface: the analyzer keys off the produced type, not the method name, so a
// consumer's own wrapper is covered in exactly the same way the shipped runners are.
const string Harness = """
+ using System.Threading.Tasks;
using Purview.SourceGeneratorFramework;
using Purview.SourceGeneratorFramework.Testing;
static class Runner
{
public static DriverRunResult Run(SourceGeneratorTestOptions options) => null!;
+
+ public static Task RunAsync(SourceGeneratorTestOptions options) => null!;
+ }
+ """;
+
+ // A stand-in for the testing package's assertion surface: 'Assert.That(x)' is terminal and returns the
+ // value it was handed when awaited, so awaiting one hands back the caller's own result instead of
+ // producing a new one. The real extensions are generated by TUnit's source generator, which does not run
+ // in an analyzer test compilation, so the shape is reproduced here.
+ const string AssertionHarness = """
+ static class Assert
+ {
+ public static AssertionSource That(DriverRunResult value) => null!;
+ }
+
+ sealed class AssertionSource
+ {
+ public Assertion DoesNotHaveDiagnostic(string expected) => null!;
+ }
+
+ sealed class Assertion
+ {
+ public Awaiter GetAwaiter() => default;
+ }
+
+ readonly struct Awaiter : System.Runtime.CompilerServices.INotifyCompletion
+ {
+ public bool IsCompleted => true;
+
+ public T GetResult() => default!;
+
+ public void OnCompleted(System.Action continuation) { }
}
""";
@@ -25,6 +58,21 @@ static class Runner
IncludeDefaultNamespaces = false,
};
+ ///
+ /// Asserts that the analysed source compiled, guarding the tests that expect no diagnostic: a source that
+ /// did not compile would leave the analyzer looking at error types and reporting nothing, so the test
+ /// would pass vacuously.
+ ///
+ static async Task AssertSourceCompilesAsync(AnalyzerTestResult result)
+ {
+ var errors = result
+ .Compilation.GetDiagnostics()
+ .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .ToArray();
+
+ await Assert.That(errors).IsEmpty();
+ }
+
[Test]
public async Task UsingDeclaration_DoesNotReportDiagnostic(CancellationToken cancellationToken)
{
@@ -256,4 +304,221 @@ public void Run()
// Assert
await Assert.That(result).HasNoDiagnostics();
}
+
+ [Test]
+ public async Task DiscardedAwaitedResult_ReportsDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange - a task-like await is a producer, so the discard is reported like the sync case.
+ const string source =
+ Harness
+ + """
+
+ class Tests
+ {
+ public async Task Run()
+ {
+ _ = await Runner.RunAsync(new SourceGeneratorTestOptions().Compile());
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(source, Options, cancellationToken);
+
+ // Assert
+ await Assert.That(result).HasDiagnostics(1);
+ await Assert.That(result).HasDiagnostic(UndisposedTestResultAnalyzer.DiagnosticId);
+ await Assert.That(result.Diagnostics.Single().Severity).IsEqualTo(DiagnosticSeverity.Warning);
+ }
+
+ [Test]
+ public async Task AwaitedAssertionReturningTheSubject_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange - awaiting an assertion returns the subject that was asserted on, so no result is produced.
+ const string source =
+ Harness
+ + AssertionHarness
+ + """
+
+ class Tests
+ {
+ public async Task Run(DriverRunResult result)
+ {
+ await Assert.That(result).DoesNotHaveDiagnostic("TSG3021");
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(source, Options, cancellationToken);
+
+ // Assert
+ await AssertSourceCompilesAsync(result);
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task AwaitedAssertionAssignedToLocal_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange - the same shape in a declaration: the local aliases the caller's own result.
+ const string source =
+ Harness
+ + AssertionHarness
+ + """
+
+ class Tests
+ {
+ public async Task Run(DriverRunResult result)
+ {
+ var checkedResult = await Assert.That(result).DoesNotHaveDiagnostic("TSG3021");
+ System.Console.WriteLine(checkedResult.GetSource());
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(source, Options, cancellationToken);
+
+ // Assert
+ await AssertSourceCompilesAsync(result);
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task SynchronousAssertionHelper_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange - the helper asserts on the result and returns it for chaining, so it produces nothing.
+ const string source =
+ Harness
+ + """
+
+ class Tests
+ {
+ public void Run(DriverRunResult result)
+ {
+ result.AssertNoGenerationExceptions();
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(source, Options, cancellationToken);
+
+ // Assert
+ await AssertSourceCompilesAsync(result);
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task SynchronousAssertionChain_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange - every link in the chain hands the same result on.
+ const string source =
+ Harness
+ + """
+
+ class Tests
+ {
+ public void Run(DriverRunResult result)
+ {
+ result.AssertNoGenerationExceptions().AssertNoLogErrors();
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(source, Options, cancellationToken);
+
+ // Assert
+ await AssertSourceCompilesAsync(result);
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task SynchronousAssertionHelperAssignedToLocal_DoesNotReportDiagnostic(
+ CancellationToken cancellationToken
+ )
+ {
+ // Arrange - the local aliases the caller's own result.
+ const string source =
+ Harness
+ + """
+
+ class Tests
+ {
+ public void Run(DriverRunResult result)
+ {
+ var checkedResult = result.AssertNoGenerationExceptions();
+ System.Console.WriteLine(checkedResult.GetSource());
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(source, Options, cancellationToken);
+
+ // Assert
+ await AssertSourceCompilesAsync(result);
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task HelperTakingTheResultAsAParameter_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange - a helper handed the result returns that same instance, so it produces nothing.
+ const string source =
+ Harness
+ + """
+
+ static class Helpers
+ {
+ public static DriverRunResult Check(DriverRunResult result) => result;
+ }
+
+ class Tests
+ {
+ public void Run(DriverRunResult result)
+ {
+ Helpers.Check(result);
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(source, Options, cancellationToken);
+
+ // Assert
+ await AssertSourceCompilesAsync(result);
+ await Assert.That(result).HasNoDiagnostics();
+ }
+
+ [Test]
+ public async Task GenericHelperOnTheResult_DoesNotReportDiagnostic(CancellationToken cancellationToken)
+ {
+ // Arrange - the receiver is the result even though the parameter is the type parameter.
+ const string source =
+ Harness
+ + """
+
+ static class Helpers
+ {
+ public static T Tap(this T value) => value;
+ }
+
+ class Tests
+ {
+ public void Run(DriverRunResult result)
+ {
+ result.Tap();
+ }
+ }
+ """;
+
+ // Act
+ var result = await AnalyzeAsync(source, Options, cancellationToken);
+
+ // Assert
+ await AssertSourceCompilesAsync(result);
+ await Assert.That(result).HasNoDiagnostics();
+ }
}