Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions .agents/prompts/sdk-packable-project-analyzer-defaults.prompt.md

This file was deleted.

4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand Down
3 changes: 0 additions & 3 deletions .config/purview-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -38,15 +37,13 @@
"tools/*/System.IO.Hashing.dll"
],
"purview.sourcegeneratorframework.testing": [
".agents/**/*",
"build/Purview.SourceGeneratorFramework.Testing.props",
"lib/*/Purview.SourceGeneratorFramework.Testing.dll",
"lib/*/Purview.SourceGeneratorFramework.Testing.xml",
"purview-logo-light.png",
"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",
Expand Down
46 changes: 46 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,49 @@ Do **not** suppress or mute compiler, analyser, or build warnings by adding `<No
## Documentation and Samples

Any API change must update the corresponding documentation, including XML doc comments and `docs/wiki/` pages such as `docs/wiki/Code-Writer.md`. Samples (the `SourceGeneratorFramework.ExampleGenerator` reference implementation and benchmarks) must use the current best-practice APIs: the minimal-parameter `CodeWriter` overloads, structured statements (`MethodCall`, `Return`, `Assignment`, `Throw`, `Comment`, `NetConditionalReturn`) rather than raw text, and the current method names. Add or update a sample whenever an API addition or change warrants a demonstrable example, and cover it with unit tests.

## Agent skills

Authored skills live in the repository-root `skills/` folder. **Nothing packs them into a NuGet
package.** `Purview.BuildSdk` 1.0.6 removed skill packaging outright — the skills CLI clones the
*git* repository and only discovers a root `skills/` folder, so that is the one delivery route.
`PurviewSkillsToPack`, the `IncludeRepositorySkillsInPackage` shim in `src/Directory.Build.targets`,
and the `.agents/**/*` entries in `.config/purview-build.json` went with it.

`.agents/skills/` is the CLI's install location for skills from **other** repositories, and is
gitignored. This repository's own skills are never installed into it — that would leave two copies to
drift apart. Read them at their authored path, `skills/<name>/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/<name>/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/`.
8 changes: 6 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<RoslynCompilerVersion Condition="'$(RoslynCompilerVersion)' == ''">[5.9.0,)</RoslynCompilerVersion>
<RoslynAnalyzersVersion Condition="'$(RoslynAnalyzersVersion)' == ''">[5.9.0,)</RoslynAnalyzersVersion>
<TUnitVersion>[1.67.0,)</TUnitVersion>
<!-- Floor raised to 1.75.0 because Purview.Skills.Testing 1.0.6 depends on TUnit.Assertions/TUnit.Core
1.75.0, and central transitive pinning would otherwise pin them back down. -->
<TUnitVersion>[1.75.0,)</TUnitVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Bogus" Version="[35.6.5,)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[8.0.0,)" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[10.0.401,)" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[10.0.402,)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynCompilerVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(RoslynCompilerVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="$(RoslynAnalyzersVersion)" />
Expand All @@ -23,5 +25,7 @@
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="ILRepack.Lib" Version="2.0.48" />
<PackageVersion Include="Mono.Cecil" Version="0.11.6" />
<!-- The Agent Skills conformance rule set, shipped alongside Purview.BuildSdk. -->
<PackageVersion Include="Purview.Skills.Testing" Version="1.0.6" />
</ItemGroup>
</Project>
8 changes: 6 additions & 2 deletions docs/wiki/Analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<Name>`. |
| `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`).
Expand Down Expand Up @@ -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 (`<c>Type</c>`) 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

Expand Down
5 changes: 4 additions & 1 deletion docs/wiki/Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"allowPrerelease": false
},
"msbuild-sdks": {
"Purview.BuildSdk": "1.0.5"
"Purview.BuildSdk": "1.0.6"
},
"test": {
"runner": "Microsoft.Testing.Platform"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sourcegenerator-framework",
"version": "1.1.0",
"version": "1.1.1",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading