From c93d2322ecee09982eabe92ebd6b04423a659568 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 8 Oct 2026 18:36:40 +0100 Subject: [PATCH 1/2] chore: take Purview.BuildSdk 1.0.4 Fixes the single-TFM floor that left a project declaring TargetFrameworks building one target only, and adds the PurviewTrackedMirroredFile report. Untracks the 9 .agents files the SDK mirrors from NuGet packages. Git only ignores untracked paths, so while they were tracked the generated .agents/.gitignore could not cover them and every upgrade that changed one showed up as a local modification. They stay on disk and every build rewrites them. --- ...source-generator-framework-writer.agent.md | 79 ---- .agents/agents/test-author-writer.agent.md | 50 --- ...odernize-test-to-codequery-tunit.prompt.md | 48 --- ...r-source-generator-to-codewriter.prompt.md | 52 --- .../sdk-configuration-reference/SKILL.md | 204 ---------- .../SKILL.md | 214 ---------- .../SKILL.md | 324 ---------------- .../skills/source-generator-testing/SKILL.md | 364 ------------------ .agents/skills/tunit-test-authoring/SKILL.md | 214 ---------- global.json | 2 +- 10 files changed, 1 insertion(+), 1550 deletions(-) delete mode 100644 .agents/agents/source-generator-framework-writer.agent.md delete mode 100644 .agents/agents/test-author-writer.agent.md delete mode 100644 .agents/prompts/modernize-test-to-codequery-tunit.prompt.md delete mode 100644 .agents/prompts/refactor-source-generator-to-codewriter.prompt.md delete mode 100644 .agents/skills/sdk-configuration-reference/SKILL.md delete mode 100644 .agents/skills/sdk-project-behavior-and-detection/SKILL.md delete mode 100644 .agents/skills/source-generator-codewriter-modernization/SKILL.md delete mode 100644 .agents/skills/source-generator-testing/SKILL.md delete mode 100644 .agents/skills/tunit-test-authoring/SKILL.md diff --git a/.agents/agents/source-generator-framework-writer.agent.md b/.agents/agents/source-generator-framework-writer.agent.md deleted file mode 100644 index 65f5bd03..00000000 --- a/.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/.agents/agents/test-author-writer.agent.md b/.agents/agents/test-author-writer.agent.md deleted file mode 100644 index fb5fb87d..00000000 --- a/.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/.agents/prompts/modernize-test-to-codequery-tunit.prompt.md b/.agents/prompts/modernize-test-to-codequery-tunit.prompt.md deleted file mode 100644 index cf463ceb..00000000 --- a/.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/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md b/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md deleted file mode 100644 index e230d224..00000000 --- a/.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/.agents/skills/sdk-configuration-reference/SKILL.md b/.agents/skills/sdk-configuration-reference/SKILL.md deleted file mode 100644 index 5dd68c61..00000000 --- a/.agents/skills/sdk-configuration-reference/SKILL.md +++ /dev/null @@ -1,204 +0,0 @@ ---- -name: sdk-configuration-reference -description: "Use when configuring Purview.BuildSdk through Directory.Build.props or a .csproj, especially for NamespacePrefix, version detection, testing framework selection, telemetry, repo bootstrapping, and embedded agent-skill settings." ---- - -# Purview.BuildSdk configuration reference - -Use this skill when a task asks what can be configured in `Purview.BuildSdk`, where a property must be set, or which defaults the SDK applies automatically. - -## First rule: know where a property must be set - -Set repo-wide bootstrap properties **before** importing the SDK in `Directory.Build.props` when the value must affect `Sdk.props` evaluation. - -Common pre-import properties: - -- `NamespacePrefix` -- `UsePackageJsonVersion` -- `RootPackageJson` -- Repo-wide testing framework selection properties when you want every project to inherit them - -If a property changes behavior in `Sdk.targets` instead, it can usually be set later (for example in a project file), but prefer repo-wide defaults in `Directory.Build.props` unless the scenario is intentionally project-specific. - -## Version detection settings - -These properties control package/app version resolution from `package.json`: - -- `UsePackageJsonVersion` — default `true`; supported values: `true`, `false`, `Strict` -- `RootPackageJson` — explicit path to the `package.json` to read -- `EnableVersionDetectionCache` — default `true`; enables local caching of resolved version data -- `VersionDetectionCacheFile` — optional explicit cache file path -- `VersionDetectionLogEnabled` — default `false`; set to `true` to log the detected package version - -Behavior rules: - -1. If `RootPackageJson` is set, the SDK uses that path. -2. Otherwise it tries to discover the repo root from CI variables, `.git`, or a nearby `package.json`. -3. When version detection succeeds, both `Version` and `PackageVersion` are set from the `version` field. -4. `UsePackageJsonVersion=Strict` should be treated as “fail if discovery/resolution cannot succeed”. - -## Core identity and build settings - -These are the most important configurable properties exposed by the SDK: - -- `NamespacePrefix` — required unless `DisableNamespacePrefixCheck=true` -- `DisableNamespacePrefixCheck` — default `false` -- `DisablePurviewStylePolicyValidation` — default `false`; set to `true` to stop the build failing (`PRSGD0006`-`PRSGD0009`) when the repository `.editorconfig` overrides the modifier policy (`dotnet_style_require_accessibility_modifiers` other than `omit_if_default`), hides `IDE0040`/`IDE1006`, disables the Style category in bulk, weakens the `_camelCase` private instance field naming rule, or adds the accessibility rules to `NoWarn`. Entries the SDK injects itself (the test-context rule set for test/shared-testing projects, `CA1515` for Aspire hosts and CLI apps) are ignored -- `TargetFramework` — defaults to `net10.0` when neither `TargetFramework` nor `TargetFrameworks` is set; projects explicitly declaring `IsRoslynComponent=true` default to `netstandard2.0` -- `IsRoslynComponent` — when explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, no dependency file, compiler-generated output under the framework-specific intermediate directory, telemetry exclusion, and `PrivateAssets=all` applied to `Microsoft.CodeAnalysis.*` / `Microsoft.CodeAnalysis.Analyzers` references. Packable Roslyn components automatically pack the built analyzer assembly and PDB into `analyzers/dotnet/cs/`; a pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true` -- `IsRoslynComponentOnly` — defaults to `true` for Roslyn components and creates an analyzer-only package: it sets `IncludeBuildOutput=false`, `IncludeSymbols=false`, and packages the portable PDB alongside the analyzer under `analyzers/dotnet/cs/`. Set it to `false` for a dual-role Roslyn component that uses normal library symbol packaging. -- `PackProjectReferencedSourceGenerators` — default `true`; packable projects automatically include analyzer `ProjectReference` outputs and runtime dependencies under `analyzers/dotnet/cs/`. Set it to `false` globally or use `Pack="false"` on one analyzer reference to opt out. -- `EnableAssemblyNameGeneration` — default `true`; when `true`, `AssemblyName` and default `PackageId` follow the fully evaluated `RootNamespace` (or the full logical project name when suffix-stripping removed a segment, e.g. `Shared`/`ServiceDefaults`). Set `false` before the SDK import to use the standard project-name behaviour -- `PurviewSharedTestingOutputType` — default `Library`; forced onto `IsSharedTestingProject` projects (with `IsTestProject`/`IsTestingPlatformApplication` cleared), because the test packages otherwise flip them into an executable test host. Set it to `Exe` before the SDK import to keep that package-driven shape -- `PurviewTestContextNoWarn` — default `CA1002;CA1012;CA1034;CA1047;CA1050;CA1051;CA1062;CA1064;CA1515;CA1707`; the production API-surface rules exempted in test and shared-testing projects. Test projects keep the strict style contract (`IDE0040`, field naming, formatting, `IDE1006`), but are context aware: public test classes/fixtures, `Method_Scenario_Expectation` names, exposed fields and unvalidated helper parameters are allowed. Override before the SDK import to narrow or extend the set -- `DisablePurviewTestContextRuleSet` — default `false`; set to `true` to make test and shared-testing projects enforce the production API-surface rules as well -- `DisableProjectFileNamingConventionCheck` — default `false`; disables the directory-name/file-name match validation -- `DisableGenerateAssemblyInfoClass` — default `false`; disables generated `AssemblyInfo` -- `DisableAutoInternalsVisibleTo` — default `false`; disables automatic friend assembly generation -- `AutoIncludeUsings` — default `true`; controls SDK-added global usings -- `SourceLinkPackageName` — default `Microsoft.SourceLink.GitHub` -- `DisableSourceLink` — default `false` - -## Telemetry and package-related settings - -- `ExcludePurviewTelemetry` — default `false`; removes `Purview.Telemetry.SourceGenerator` -- `ExcludeMSTelemetryExtension` — default `false`; removes `Microsoft.Extensions.Telemetry.Abstractions`. Only relevant when `ExcludePurviewTelemetry` is also `false` — when `ExcludePurviewTelemetry=true` the whole telemetry group is skipped anyway -- `IsPackable` — defaults to `false` if not set elsewhere -- `PackageTags`, `IncludeSource`, `IncludeSymbols`, `PublishRepositoryUrl`, `SymbolPackageFormat` — standard pack-related settings the SDK participates in for packable projects -- Packable-project defaults (only applied when the consuming project has not supplied a value): `GenerateDocumentationFile=true`, `IncludeSymbols=true`, `SymbolPackageFormat=snupkg`, `PublishRepositoryUrl=true`, `EmbedUntrackedSources=true`, `DebugType=portable`. Portable PDBs are delivered through the `.snupkg`; the normal `.nupkg` does not receive PDB files unless the project opts in explicitly. Roslyn-component-only packages default `IncludeSymbols=false` and ship their PDB inside `analyzers/dotnet/cs/` instead -- If the repo root is discoverable, the repository-root `README.md` is packed automatically (and registered via `PackageReadmeFile`) when the file exists and `PackageReadmeFile` was not configured explicitly - -## Test framework settings - -The SDK supports opinionated testing defaults and validation. - -Primary settings: - -- `TestingFramework` — default `TUnit`; supported values: `TUnit`, `Xunit`, `None` -- `SubstituteFramework` — default `TUnitMocks`; supported values: `TUnitMocks`, `NSubstitute`, `None` -- `TestDataFramework` — default `Bogus`; supported values: `Bogus`, `None` - -Default outcome for standard test projects: - -- `TUnit` -- `TUnit.Mocks` -- `Bogus` -- Microsoft.Testing.Platform integration - -Specialised packages such as `TUnit.Aspire` and `Testcontainers` are not automatic defaults; they remain -explicit choices based on the project's purpose. - -Related toggles and derived settings: - -- `CollectCoverage` — defaults to `true` for detected test projects -- `EnableStaticNativeInstrumentation` — defaults to `false` for test projects -- `EnableDynamicNativeInstrumentation` — defaults to `false` for test projects -- `TestingPlatformDotnetTestSupport`, `UseMicrosoftTestingPlatformRunner`, `EnableMicrosoftTestingPlatform` — enabled automatically for TUnit test projects - -## Repo bootstrap and developer-experience settings - -These settings control the SDK’s repo-level helper file bootstrapping: - -- `DisableAutoCopySdkFiles` — default `false`; master switch for SDK-managed repo file copying -- `BootstrapEditorConfigToRepoRoot` — default `true` -- `RepositoryEditorConfigFilePath` — optional override for the destination `.editorconfig` -- `BootstrapGlobalJsonToRepoRoot` — default `true` -- `RepositoryGlobalJsonFilePath` — optional override for the destination `global.json` -- `PurviewBuildSdkVersionForGlobalJson` — defaults to detected SDK package version, fallback `1.0.0` -- `PurviewAutoSdkPack` — default `true`; when `true`, automatically packs the `Sdk/` folder contents into the NuGet package with the correct root-level paths -- `EnableAgentFolderInPackage` — default `true`; mirrors the bundled `.agents/**` folder from the SDK NuGet package into the consuming repo’s `.agents/` -- `AgentPackDestinationFolder` — default `.agents`; repo-relative destination folder that receives mirrored agent content as `$(AgentPackDestinationFolder)/**` -- `PurviewAgentFolderSourcePath` — overrides the folder that provides the bundled `.agents` content (defaults to the package-level `.agents` folder beside `Sdk/`) -- `PurviewAgentFolderCopyRetries` — default `3`; copy attempts per file before a failure is reported -- `PurviewAgentFolderCopyRetryDelayMilliseconds` — default `500`; base delay between copy attempts -- `PurviewAgentFolderCopyFailureAsError` — default `true`; when `false`, a copy that still fails after every retry is a warning instead of an error -- `PurviewAgentSyncManifestPath` — overrides the change-detection manifest (default `/.purview/agent-sync.cache`) used to skip unchanged agent content -- `PurviewSuppressCopyRetryWarnings` — default `true`; demotes built-in copy task retry notices (`MSB3026`) to messages. Set to `false` to see every retry attempt - -## Shared repository copy behaviour - -`.agents` content, `.editorconfig` and `global.json` live in one repository-wide location but are written -by every project, so parallel builds race for the same destinations. The SDK therefore: - -1. Skips unchanged content using the `.purview/agent-sync.cache` manifest (fingerprint + content hash per file), so repeat builds touch nothing. This also detects an in-place package republish that keeps the same version. -2. Stages every write into a temporary file in the destination folder and renames it into place, so readers never see partial content and writers cannot interleave. -3. Retries quietly — retry attempts are low-importance messages, `MSB3026` notices are demoted — and only reports a copy that still fails after `Purview*CopyRetries` attempts, as an error by default. -4. Treats "another project already wrote identical content" as success, so a lost race is a no-op instead of a failure. - -`PurviewRepoBootstrapMode` (`IfMissing` default, or `Always`/`WarnOnDrift`/`Never`) controls whether -existing `.editorconfig`/`global.json` files may be overwritten or reported as drifted. - -**Hard requirement:** This SDK must pack the contents of `Sdk/` into the NuGet package so that downstream consumers of `Purview.BuildSdk` receive the same `Sdk/**` files. The `PurviewAutoSdkPack` feature (default `true`) is the mechanism that delivers this for standard consuming projects. When a project is packable, the SDK automatically adds `Sdk/**/*` as package content with the correct root-level paths: - -- `Sdk/.agents/**` → `.agents/**` -- `Sdk/.github/**` → `.github/**` -- `Sdk/build/**` → `build/**` -- `Sdk/buildTransitive/**` → `buildTransitive/**` -- `Sdk/buildMultiTargeting/**` → `buildMultiTargeting/**` -- `Sdk/*.md`, `Sdk/*.png`, `Sdk/*.jpg`, etc. → package root -- everything else under `Sdk/` → `Sdk/` - -The SDK injects a `.gitignore` file into each second-level folder under `Sdk/.agents` during packaging with the following content: - -```text[.gitignore] -# Ignore all files -* - -# Don't ignore directories, so Git can traverse them -!*/ - -# Keep this file -!.gitignore -``` - -This lets consuming repos keep the agent folder structure discoverable while ignoring the copied content in Git. - -## Important derived properties you can inspect - -When explaining SDK behavior, prefer these derived values over guessing: - -- `PurviewLogicalProjectName` -- `PurviewNamespacePrefix` -- `PurviewProjectShortName` -- `PurviewTestType` -- `PurviewSharedTestingOutputType` (default `Library`; `Exe` keeps the test packages' executable/test-host shape) -- `PurviewTestContextNoWarn` (production API-surface rules exempted in test/shared-testing projects) -- `PurviewPolicyExemptNoWarn` (SDK-injected `NoWarn` entries that `ValidatePurviewStylePolicy` accepts) -- `RootNamespace` -- `AssemblyName` -- `PackageVersion` -- `TestingType` -- `TargetProjectName` -- `RepoRoot` -- `RootPackageJson` - -## Compiler-visible properties - -The SDK exports many properties for analyzers and source generators through `build_property.`. When authoring analyzers or generators, prefer those exported properties instead of re-deriving SDK behavior manually. - -Especially relevant exported properties include: - -- `UsePackageJsonVersion`, `RootPackageJson`, `RepoRoot`, `Version`, `PackageVersion` -- `NamespacePrefix`, `DisableNamespacePrefixCheck` -- `TestingFramework`, `SubstituteFramework`, `TestDataFramework` -- `ExcludePurviewTelemetry`, `ExcludeMSTelemetryExtension` -- `EnableAssemblyNameGeneration`, `DisableAutoInternalsVisibleTo`, `DisableGenerateAssemblyInfoClass` -- `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsSharedProject` -- `TestingType`, `TargetProjectName` -- `IsContainerProject`, `IsSdkProject`, `SdkProjectName`, `IsWebProject`, `IsWebSdkProject`, `IsWorkerSdkProject`, `IsAspireHostProject`, `IsCLIProject` -- `EditorConfigFilePath`, `RepositoryEditorConfigFilePath`, `BootstrapEditorConfigToRepoRoot` -- `RepositoryGlobalJsonFilePath`, `BootstrapGlobalJsonToRepoRoot`, `DisableAutoCopySdkFiles` -- `PurviewRepoBootstrapMode`, `PurviewRepoBootstrapCopyRetries`, `PurviewRepoBootstrapCopyRetryDelayMilliseconds`, `PurviewRepoBootstrapCopyFailureAsError` -- `PurviewAgentFolderSourcePath`, `PurviewAgentFolderCopyRetries`, `PurviewAgentFolderCopyRetryDelayMilliseconds`, `PurviewAgentFolderCopyFailureAsError`, `PurviewAgentSyncManifestPath`, `PurviewSuppressCopyRetryWarnings` -- `PurviewBuildSdkVersionForGlobalJson`, `CurrentYear`, `AutoGeneratedAssemblyInfoFile` - -## Guidance for edits - -When changing SDK configuration: - -1. Preserve existing defaults unless the task explicitly changes product behavior. -2. Keep README, SDK property declarations, validation, and any shipped skills aligned. -3. If you add a new user-facing property, update both the configuration docs and the bundled skills. -4. If the property affects import-time behavior, document that it must be set before the SDK import. -5. Keep repository policy guidance aligned with the engineering-principles documentation, and keep low-level - property explanations aligned with the wiki reference pages. diff --git a/.agents/skills/sdk-project-behavior-and-detection/SKILL.md b/.agents/skills/sdk-project-behavior-and-detection/SKILL.md deleted file mode 100644 index 9961d001..00000000 --- a/.agents/skills/sdk-project-behavior-and-detection/SKILL.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -name: sdk-project-behavior-and-detection -description: "Use when explaining why Purview.BuildSdk classified a project as test, shared, CLI, web, Aspire host, or container, or when reasoning about auto-added packages, project references, namespaces, and naming conventions." ---- - -# Purview.BuildSdk project behavior and detection - -Use this skill when a task asks **why** the SDK applied a behavior automatically, or when adding/moving projects in a repo that relies on the SDK's naming and project-type inference. - -## Project-type detection rules - -The SDK infers behavior from project names, project contents, and SDK declarations. - -### Test detection - -A project is treated as a test project when its name ends with `*Test` or `*Tests` and the suffix before `Test(s)` matches a supported testing type such as: - -- `Unit` -- `Integration` -- `E2E` -- `EndToEnd` -- `Acceptance` -- `Functional` -- `Performance` -- `Load` -- `Smoke` -- `Stress` -- `Regression` -- `Security` -- `Chaos` -- `Scenario` -- `System` -- `Threat` -- `BlackBox` -- `WhiteBox` -- `Accessibility` -- `Interactive` -- `Environment` -- `Architecture` -- `Contract` - -Derived properties: - -- `IsTestProject=true` -- `TestingType=` -- `PurviewTestType=Tests` -- `TargetProjectName=` - -### Shared project detection - -The SDK recognizes shared project names exactly. These are not generic substring matches. - -Shared project names: - -- `Shared` -- `SharedFramework` -- `SharedInfrastructure` -- `SharedInfra` -- `SharedUtilities` -- `SharedUtils` -- `SharedLibrary` -- `SharedLib` -- `SharedHelpers` - -Shared testing project names: - -- `SharedTestingFramework` -- `SharedTestingInfrastructure` -- `SharedTestingInfra` -- `SharedTestingUtilities` -- `SharedTestingUtils` -- `SharedTestingLibrary` -- `SharedTestingLib` -- `SharedTestingHelpers` - -Derived flags: - -- `IsSharedProject` -- `IsSharedTestingProject` - -### SDK/content-based detection - -- `IsSdkProject` / `SdkProjectName` come from parsing the project/import `Sdk="..."` declaration -- `IsWebSdkProject=true` for `Microsoft.NET.Sdk.Web` -- `IsWorkerSdkProject=true` for `Microsoft.NET.Sdk.Worker` -- `IsAspireHostProject=true` when the SDK starts with `Aspire.Sdk.Host` or `Aspire.AppHost.Sdk` -- `IsContainerProject=true` when `Dockerfile`, `dockerfile`, or `Dockerfile.dev` exists in the project directory -- `IsCLIProject=true` when the project name ends with `CLI`, `Console`, `CommandLine`, `QuickStart`, or `QuickStarts` - -## Namespace and identity behavior - -The SDK derives the project identity from `NamespacePrefix` and the project name. - -Key behavior: - -1. `PurviewLogicalProjectName` is built from `NamespacePrefix` plus the project name, with deduplication when the project name already starts with the namespace tail. -2. `RootNamespace` defaults to `PurviewLogicalProjectName`. -3. Known suffixes are stripped from `RootNamespace`, including shared/shared-testing names and common segments like `Core`, `EF`, `Shared`, `ClientShared`, and `ServiceDefaults`. -4. Test suffixes are removed from `RootNamespace`, so `Acme.Api.UnitTests` still maps back to `Acme.Api`. -5. `AssemblyName` and `PackageId` default to the fully evaluated `RootNamespace` (the canonical default public name) — except when suffix-stripping removed a segment of the logical project name (e.g. `Shared` or `ServiceDefaults`), in which case they use the full `PurviewLogicalProjectName` so those assemblies/packages stay distinct from their parent. Test/shared-testing projects keep their detected suffix in `AssemblyName`/`PackageId` so test assemblies stay distinct. Explicit `AssemblyName`/`PackageId`/`RootNamespace` values always win. -6. The naming defaults are applied during `Sdk.props` evaluation (before the Microsoft SDK computes `TargetName`), so the compiled output name always matches `AssemblyName`. -7. The SDK ships `Purview.BuildSdk.Analyzers` and adds it as an `` item to every C# project, so its rules (PDS0002 Extensions namespace, PDS0003 explicit types with target-typed `new()`, PDS0004 correct acronym capitalization) surface in both command-line builds and Visual Studio. The code-fix assembly ships beside it and is referenced as an `` item inside Visual Studio so the IDE discovers its code fixes. `PDS0004` follows .NET naming guidance for well-known framework spellings (`Sql`, `Guid`, `Uuid`, `Url`, `Dns`, `Tcp`, `Http`, `Xml`, `Db`, ...) and exempts them by default — `Db` is exempt so `DbContext`/`DbConnection`/`DbSet` are never flagged (re-enable via `acronym_map = Db:DB`); a small set — `Api`, `Ai`, `Ui`, `Io`, `Os`, `Cpu`, `Gpu`, `Cli`, `Gui`, `Ram`, `Ssh` — is still renamed to uppercase. Members mandated by a contract (interface implementations, base-class overrides) are never renamed. Customise via `dotnet_analyzer_configuration.pds0004.allowed_words` (exempt segments), `.acronym_map` (segment renames, e.g. `Sql:SQL`), and `.allowed_identifiers` (brand names exempted by exact name or word-boundary prefix, first match wins — e.g. `CosmosDb` covers `CosmosDbServer`). All three merge with and override the shipped defaults. The shipped `.editorconfig` treats generated content (`Migrations/`, `*.g.cs`, `Generated/`, `*.Designer.cs`, `obj/`/`bin/`) as `generated_code = true`, and suppresses namespace-conflict diagnostics under `Extensions/` so no `#pragma` suppressions are needed there. A VS code refactoring (`Split extensions class into one class per receiver type`) is offered on static extensions classes that target multiple receiver types: it splits them into one `Extensions` class per receiver (a generic `this TBuilder where TBuilder : IHostApplicationBuilder` receiver becomes `HostApplicationBuilderExtensions`) and places each under `Extensions//` so the PDS0002 convention stays satisfied. A companion refactoring (`Move extensions class to conventional location`) is offered on single-receiver extensions classes that are misplaced: it re-paths the file to `Extensions//`, fixes the namespace to the receiver's namespace, and updates `using` directives in other referencing documents so the move compiles. - -Do not hand-author alternate namespace conventions unless the repository explicitly opts out of the SDK defaults. - -## Automatic project references - -The SDK adds project references based on layout conventions. - -### Non-test projects - -For ordinary non-test, non-shared projects, it automatically looks for sibling shared projects: - -- `../Shared*/Shared*.csproj` - -It also removes accidental self/shared-testing matches. - -### Test projects - -For detected test projects, it attempts these target-project paths in order when they exist: - -- `../$(TargetProjectName)/$(TargetProjectName).csproj` -- `../../$(TargetProjectName)/$(TargetProjectName).csproj` -- `../src/$(TargetProjectName)/$(TargetProjectName).csproj` -- `../../src/$(TargetProjectName)/$(TargetProjectName).csproj` - -It also adds sibling shared-testing project references via: - -- `../SharedTesting*/SharedTesting*.csproj` - -This is why consistent naming and placement matter so much in repos that use the SDK. - -## Automatic framework/package behavior - -### For non-test C# projects - -- Adds SourceLink unless `DisableSourceLink=true` -- Adds Purview telemetry packages unless `ExcludePurviewTelemetry=true` -- Generates documentation files (`GenerateDocumentationFile=true`) unless explicitly disabled -- Generates `InternalsVisibleTo` attributes unless `DisableAutoInternalsVisibleTo=true` - -### For packable projects - -- Defaults `GenerateDocumentationFile`, `IncludeSymbols`, `SymbolPackageFormat=snupkg`, `PublishRepositoryUrl`, `EmbedUntrackedSources`, `IncludeSource`, and `DebugType=portable` — only when the consuming project has not supplied a value -- Delivers portable PDBs via the `.snupkg`; the normal `.nupkg` does not receive PDBs unless the project opts in explicitly -- Packs the repository-root `README.md` (registered via `PackageReadmeFile`) when the file exists and `PackageReadmeFile` is unset; skips when a README is already being packed -- Non-packable projects (including web apps) default `WarnOnPackingNonPackableProject=false` so solution-wide pack operations skip them silently - -### For Roslyn component (analyzer/source-generator) projects - -- Defaults a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, and SourceLink with `EmbedUntrackedSources=true` -- `IsRoslynComponentOnly` defaults to `true`, excluding normal build output (`IncludeBuildOutput=false`) and setting `IncludeSymbols=false`; no `.symbols.nupkg` or `.snupkg` is produced and the analyzer PDB ships inside the main `.nupkg` under `analyzers/dotnet/cs/` beside the analyzer assembly (`PurviewPackAnalyzerPdb=true`). Set it to `false` for a dual-role component that uses normal library symbol packaging. -- Packable Roslyn components automatically pack the built analyzer assembly (and PDB) into `analyzers/dotnet/cs/`; `SymbolPackageFormat` defaults to the modern `snupkg` if symbols are explicitly opted into -- `Microsoft.CodeAnalysis.*` and `Microsoft.CodeAnalysis.Analyzers` references are defaulted to `PrivateAssets=all` (development-only dependencies) so they never leak into the packed nuspec -- A pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack of a packable Roslyn component if `LangVersion`, `Nullable`, `TreatWarningsAsErrors`, or `EnforceExtendedAnalyzerRules` is missing; opt out with `DisableRoslynCompilerDefaultsValidation=true` -- `ContinuousIntegrationBuild` is set only by real CI environment variables — packability alone never forces SourceLink's CI-mode dirty-repository checks - -### For test and shared-testing projects - -- Applies test-friendly `NoWarn` defaults -- Marks projects as not packable/publishable -- Adds substitute/test-data/testing packages based on `SubstituteFramework`, `TestDataFramework`, and `TestingFramework` -- For TUnit test projects, enables Microsoft.Testing.Platform integration properties automatically -- For shared-testing projects, skips the runnable test package and marks them with a skip/category pattern appropriate to the selected test framework - -For the default configuration, standard test projects receive: - -- `TUnit` -- `TUnit.Mocks` -- `Bogus` -- Microsoft.Testing.Platform integration - -Specialized testing dependencies such as `TUnit.Aspire` and `Testcontainers` are still explicit additions by -project purpose. - -### For special project types - -- CLI projects default to `OutputType=Exe` and include `appsettings*.json` as content -- Container projects enable `InvariantGlobalization`, `PublishAot`, Linux Docker defaults, and container tooling package references -- Web SDK projects get `Microsoft.AspNetCore.OpenApi.Generated` added to `InterceptorsNamespaces` unless marked as a separate web-project mode -- Aspire host projects default to `OutputType=Exe` - -## How to reason about surprising behavior - -If the SDK “did something unexpected”, inspect these values first: - -- `MSBuildProjectName` -- `NamespacePrefix` -- `PurviewLogicalProjectName` -- `RootNamespace` -- `TestingType` -- `TargetProjectName` -- `SdkProjectName` -- `IsTestProject` -- `IsSharedProject` -- `IsSharedTestingProject` -- `IsContainerProject` -- `IsCLIProject` -- `IsWebSdkProject` -- `IsAspireHostProject` - -Prefer explaining behavior from these computed properties rather than from assumptions about folder names alone. - -## Guidance for structural changes - -When adding or moving projects in a repo using this SDK: - -1. Keep the `.csproj` filename equal to its containing directory name unless the repo explicitly disables that validation. -2. Preserve established `src/` and `tests/`-style layouts whenever possible. -3. Use test project suffixes intentionally so auto-detection and auto-references work. -4. Keep shared helpers in exact shared/shared-testing names if you want the corresponding SDK behavior. -5. If you change a naming rule in the SDK, update the README and the shipped skills together. -6. If the question is really about repository policy rather than one computed property, point the user to the - engineering-principles documentation first, then explain the specific SDK mechanics. diff --git a/.agents/skills/source-generator-codewriter-modernization/SKILL.md b/.agents/skills/source-generator-codewriter-modernization/SKILL.md deleted file mode 100644 index 8501ca40..00000000 --- a/.agents/skills/source-generator-codewriter-modernization/SKILL.md +++ /dev/null @@ -1,324 +0,0 @@ ---- -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." ---- - -# 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. - -## Required implementation pattern - -When creating source output, favor this shape: - -1. Build immutable, value-equatable pipeline values first. -2. Register source output. -3. Inside the callback: `var writer = generationContext.CreateCodeWriter();` -4. Write header/usings/namespace. -5. Write structured types and members using declaration options records. -6. Add source once per output artifact. - -### CodeWriter lifetime: created in the output callback, never in pipeline state - -The line between "fine" and "forbidden" is the **incremental-cache boundary**, not the act of -creating a writer or handing it to a helper. - -**Fine — output-scoped use inside a `RegisterSourceOutput` callback.** Create the writer inside the -callback and pass it to any emitter/helper methods called from that same callback. It may be held in -local variables, passed as a parameter, or wrapped in a short-lived output context: - -```csharp -context.RegisterSourceOutput( - targets.CombineWithContext(contextProvider), - static (spc, pair) => - { - var (model, generationContext) = pair; - var writer = generationContext.CreateCodeWriter(); - EmitHeader(writer, model); - EmitType(writer, model); - spc.AddSource($"{model.Name}.g.cs", writer.ToString()); - } -); -``` - -**Forbidden — persisting the writer across the incremental-cache boundary.** Do not create it earlier -in the pipeline and pass it down, and do not store it anywhere Roslyn caches or another callback can -observe it: - -- Do not create a `CodeWriter` in a provider stage and carry it through the pipeline. -- Do not store a `CodeWriter` as a property or field on `GenerationContext` or a custom context. -- Do not return a `CodeWriter` (or an object holding one) from an incremental provider. -- Do not cache or reuse a writer for a later callback or a different output. - -A cached writer can retain previously written source, mix output from concurrently running -callbacks, and defeat scope tracking. A writer created in the callback and used only within that -callback is safe and expected. - -## Source generator & analyser best practices - -Apply the following rules to every generator, analyser, and refactor. - -### 1. Core principles - -- **Analyser for validation; generator for generation.** -- **Syntax for syntax, symbols for declarations, operations for executable semantics.** -- **Use `ForAttributeWithMetadataName` for attribute-driven generators.** -- **Remove Roslyn objects from the incremental pipeline as early as possible.** -- **Every value crossing a pipeline boundary must have meaningful value equality.** -- **Prefer many small incremental stages over one large transform.** -- **Keep broad inputs such as `Compilation` away from downstream generation.** -- **Generate deterministic output.** -- **Compile against the oldest Roslyn API version containing the functionality you need.** -- **Test caching, not just generated text.** - -The guiding principle for an incremental generator is: - -> Extract semantic information once, convert it into a small value model, and make everything downstream operate only on that value model. - -### 2. Analyser vs source generator - -Use a `DiagnosticAnalyser` when the question is: - -> Is the source code valid according to this library's rules? - -Use an `IIncrementalGenerator` when the question is: - -> Given valid source code, what source should be generated? - -| Requirement | Prefer | -| --- | --- | -| Require a class to be `partial` | Analyser | -| Require an attribute on a declaration | Analyser | -| Validate a method signature | Analyser | -| Reject unsupported property types | Analyser | -| Detect invalid attribute arguments | Analyser | -| Detect unsupported API usage | Analyser | -| Offer an automatic fix | Analyser + `CodeFixProvider` | -| Generate members for a marked class | Incremental generator | -| Generate serializers/validators/mappers | Incremental generator | -| Generate a registry from discovered types | Incremental generator | -| Read a schema file and generate C# | Incremental generator | -| Internal generation failure | Generator diagnostic | - -### 3. Choosing an analyser action - -Use the narrowest API that represents the concept being analysed: - -- `RegisterSyntaxNodeAction` — exact source syntax (e.g., modifier presence). -- `RegisterSymbolAction` — declaration semantics (e.g., attributes, interfaces, accessibility). -- `RegisterOperationAction` — executable behaviour (e.g., invocation, assignment, object creation). -- `RegisterOperationBlockStart/EndAction` — stateful method analysis. -- `RegisterSymbolStart/EndAction` — type-wide analysis across members. -- `RegisterCompilationStartAction` — resolve known framework symbols once. -- `RegisterAdditionalFileAction` — analyse `AdditionalFiles`. -- Avoid `RegisterSyntaxTreeAction`, `RegisterSemanticModelAction`, and compilation-end actions unless genuinely necessary. - -### 4. Syntax vs symbol vs operation - -Decision tree: - -1. Does exact source spelling/structure matter? → **Syntax** -2. Otherwise, is it a declaration? → **Symbol** -3. Otherwise, is it executable behaviour? → **Operation** - -Use `SymbolEqualityComparer.Default.Equals(...)` when comparing symbols. - -### 5. Analyser best practices - -- Enable concurrent execution with `context.EnableConcurrentExecution()`. -- Explicitly configure generated-code analysis with `context.ConfigureGeneratedCodeAnalysis(...)`. -- Resolve known framework/library symbols once in a `RegisterCompilationStartAction`. -- Prefer narrow registrations over scanning entire syntax trees or compilations. -- Treat diagnostic IDs as public contracts and maintain release tracking files when publishing public diagnostics. - -### 6. Incremental generator golden rules - -Implement `IIncrementalGenerator`. Simply implementing it is not enough; the pipeline must be incremental. - -> **Pipeline values must be immutable and value-equatable.** - -Never keep these in persistent pipeline models: - -| Type | Verdict | -| --- | --- | -| `ISymbol` / `INamedTypeSymbol` / `IMethodSymbol` / `IPropertySymbol` | Never retain | -| `Compilation` | Do not propagate | -| `SemanticModel` | Do not propagate | -| `IOperation` | Do not propagate | -| `SyntaxTree` | Do not propagate | -| `SyntaxNode` | Remove ASAP | -| `Location` | Remove ASAP | -| `AdditionalText` | Project immediately | -| `T[]` / `List` | Avoid | -| `ImmutableArray` | Wrap with sequence equality | - -Use immutable records and `EquatableArray` (sequence equality) for collection members. - -### 7. Designing the pipeline - -Pipeline shape: - -```text -Roslyn Input - ↓ -Cheap discovery - ↓ -Semantic extraction - ↓ -Small equatable model - ↓ -Validation/transformation - ↓ -Generation model - ↓ -Source output -``` - -Guidelines: - -- Project the semantic transform as the boundary where Roslyn objects disappear. -- Prefer `static` callbacks to avoid capturing generator state. -- Honour cancellation tokens. -- Split transformations into many small stages. -- Keep syntax predicates cheap. -- Avoid indirect discovery (every interface implementation, every subclass, entire-compilation scans). - -### 8. Syntax discovery - -- Prefer `context.SyntaxProvider.ForAttributeWithMetadataName(...)` for attribute-driven generators. -- Use `context.SyntaxProvider.CreateSyntaxProvider(...)` only when syntax itself is the trigger and there is no marker attribute. -- The predicate must be cheap; do not walk the tree or do semantic work in it. - -### 9. `Collect`, `Combine`, and invalidation - -- `Collect()` turns per-item outputs into one aggregate. Changing any item invalidates the aggregate. -- Use `Collect()` only for genuinely global output: registries, lookups, duplicate detection, aggregate switches. -- Prefer per-item `RegisterSourceOutput`. -- `Combine()` is correct when the output depends on two providers. -- Avoid `models.Combine(context.CompilationProvider)` — project the compilation to a tiny capability fact first. -- Use `.WithComparer(...)` only when logical equality differs from the default. - -### 10. Diagnostics - -- Prefer a separate `DiagnosticAnalyser` for normal user validation. -- Use generator diagnostics only for malformed additional files, generator-only configuration, conflicting output, or failures that cannot be expressed by an analyser. -- Report diagnostics on the most useful user-authored `Location`. -- Do not keep `Location` in long-lived pipeline models. - -### 11. Output generation - -- Output must be deterministic: no timestamps, random GUIDs, process IDs, machine paths, culture-dependent output, or unordered dictionary output. -- Hint names must be deterministic, unique, and stable. -- Prefer text generation or `CodeWriter` over building Roslyn syntax trees just to stringify them. -- Use `RegisterPostInitializationOutput` for constant source such as marker attributes. -- Add generated source once per output artifact. - -### 12. Testing incrementally - -- Snapshot-testing generated source is not enough. -- Test first execution, cached second execution, unrelated changes remaining cached, per-target invalidation, deletion, renaming, global options, additional files, and global registry invalidation. -- Use `GeneratorDriverOptions` with `trackIncrementalGeneratorSteps: true` and inspect reasons: `New`, `Modified`, `Unchanged`, `Cached`, `Removed`. - -### 13. Roslyn version compatibility and packaging - -- The `Microsoft.CodeAnalysis.*` version used to compile the analyser/generator sets the minimum compiler-host requirement. -- The consumer's `TargetFramework` does not determine analyser compatibility. -- Choose the oldest Roslyn version that contains the APIs you need. -- Common baselines: Roslyn 4.8 for VS 17.8 / .NET 8, 4.12 for VS 17.12 / .NET 9, 5.0 for VS 2026 18.0 / .NET 10. -- Ship one `netstandard2.0` analyser/generator binary unless you have a deliberate multi-version strategy. -- Do not mistake multi-targeting for automatic analyser asset selection. -- Use `PrivateAssets="all"` for Roslyn development dependencies. -- Enable `EnforceExtendedAnalyzerRules` and investigate `RSxxxx` diagnostics before suppressing them. - -### 14. Recommended project configuration - -A generator project should normally include: - -```xml - - netstandard2.0 - latest - enable - true - false - true - true - - - - - - - - - - -``` - -## Preferred API map for CodeWriter - -### File and namespace - -- `AutoGeneratedHeader(...)` -- `Using(...)` -- `FileScopedNamespace(...)` or `BlockNamespace(...)` -- `OpenPragmasScope(...)` for warning suppression scopes - -### Types and members - -- Types: `Class`, `Struct`, `RecordClass`, `RecordStruct`, `Interface`, `Enum`, `Delegate` -- Members: `Method`, `MethodScope`, `Property`, `Field`, `Constructor` -- Attributes: `AttributeDeclarationOptions`, `AttributeArgumentOptions` -- Type syntax: `TypeReference` (nullable/generic/array/pointer-safe composition) - -### XML documentation - -Use `XmlCommentWriter` extension methods on `CodeWriter`: - -- `XmlSummary(...)`, `XmlParam(...)`, `XmlTypeParam(...)`, `XmlReturn(...)`, `XmlRemarks(...)`, `XmlExample(...)` -- `XmlCode(...)` / `XmlCodeBlock(...)` -- `XmlList(...)`, `XmlSeeAlso(...)`, `XmlException(...)` - -Static helpers: `CodeWriter.XmlInlineCode(...)`, `CodeWriter.XmlSee(...)`, `CodeWriter.XmlParamRef(...)`, `CodeWriter.XmlText(...)`. - -## Refactoring guide: string/StringBuilder -> CodeWriter - -Apply this checklist in order: - -1. **Move emission boundaries** — replace giant string assembly with phases: header, namespace, type, members. -2. **Replace manual braces/indentation** — use `using` scopes (`ClassScope`, `MethodScope`, `OpenBlockScope`, `IndentedScope`). -3. **Replace handwritten signatures** — use declaration option records. -4. **Replace raw XML lines** — use XML extension methods (`XmlSummary`, `XmlParam`, etc.). -5. **Normalize type strings** — use `TypeReference`. -6. **Preserve semantics and ordering** — generated members and diagnostics must remain equivalent. -7. **Validate scope safety** — keep or enable `PurviewSourceGeneratorFrameworkValidateCodeWriterScopes` for tests/dev. - -## Anti-patterns to remove during refactors - -- `StringBuilder.AppendLine("public class ...")` for declarations that can be structured. -- Manually writing `{` / `}` around methods and types where scope APIs exist. -- Hard-coded nullable type suffixes and generic syntax in arbitrary strings when `TypeReference` is available. -- Raw XML tag string composition when XML extension methods can enforce consistency. -- Creating a `CodeWriter` before the output callback and passing it through the pipeline. -- Storing a `CodeWriter` on `GenerationContext`, a custom context, or any incremental pipeline model. -- Sharing one `CodeWriter` across multiple generated outputs or callbacks. -- Keeping Roslyn objects, `CodeWriter`, or mutable state in incremental pipeline models. - -## Review checklist for pull requests - -- Generated declarations use structured APIs for types and members. -- XML docs use XML extension methods rather than raw `///` fragments. -- `CodeWriter` is created inside each output callback and never persists in pipeline state; passing it - to helper methods within that callback is expected. -- Header and generated attributes are deterministic and consistent. -- Existing diagnostics, generated member names, and public behavior are preserved. -- Roslyn objects are removed from pipeline models; `EquatableArray` is used for collections. -- Per-target output is preferred over collected global output unless global knowledge is required. -- `CompilationProvider` is not casually combined into output. -- Deterministic hint names and source text are used. -- Incremental caching behavior is tested, not just generated text. - -## See also - -- `agents/source-generator-framework-writer.agent.md` — specialist agent for `Purview.SourceGeneratorFramework` emitter authoring. -- `prompts/refactor-source-generator-to-codewriter.prompt.md` — prompt template for legacy-emitter refactor tasks. diff --git a/.agents/skills/source-generator-testing/SKILL.md b/.agents/skills/source-generator-testing/SKILL.md deleted file mode 100644 index 2a565445..00000000 --- a/.agents/skills/source-generator-testing/SKILL.md +++ /dev/null @@ -1,364 +0,0 @@ ---- -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." ---- - -# Testing source generators, analyzers, code fixes and refactorings - -Use this skill whenever a task involves authoring, fixing, or modernising tests for Roslyn components -(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. - -## Picking the right runner - -| Roslyn type | Runner | -|---|---| -| `IIncrementalGenerator` / `ISourceGenerator` | `SourceGeneratorTestRunner` | -| `DiagnosticAnalyzer` | `DiagnosticAnalyzerTestRunner` | -| `CodeFixProvider` | `CodeFixTestRunner` (single) | -| — | `CodeFixTestRunner.RunFixAllAsync` (project-wide) | -| `CodeRefactoringProvider` | `RefactoringTestRunner` | - -TUnit projects should prefer the matching base class instead (see `tunit-test-authoring`): -`TUnitSourceGeneratorTestBase`, `TUnitDiagnosticAnalyzerTestBase`, `TUnitCodeFixTestBase`, -`TUnitRefactoringTestBase`. - -## Result types - -- `DriverRunResult` (generator) — `DriverResult`, `AllSyntaxTrees`/`PrimarySyntaxTrees`, - `CompilationResult.Compilation`, `GetGeneratedTree`, `GetSource`, `GetTypeByMetadataName`, `LogEntries`. -- `AnalyzerTestResult` — `Diagnostics`, `Compilation`. -- `CodeFixTestResult` — `Diagnostics`, `CodeActions`, `FixedSource`, `Compilation`, `ChangedSolution`. -- `CodeFixFixAllResult` — `Diagnostics`, `CodeActions`, `FixedSources`, `ChangedSolution`. -- `RefactorTestResult` — `CodeActions`, `FixedSources`, `ChangedSolution`, `Compilation`. - -Use `DriverRunResultExtensions` (`AssertNoCompilationErrors`, `AssertNoGenerationExceptions`, -`AssertSingleGeneratedSource`, `AssertGeneratedSourceContains`, …) for quick checks, but prefer -`CodeQuery` for structural assertions. - -## Querying produced code with `CodeQuery` - -Every result exposes a `CodeQuery` via extensions in `CodeQueryResultExtensions`: - -```csharp -result.Generated() // DriverRunResult: generated trees (default, generated-first) -result.Output() // DriverRunResult: entire output compilation (user + generated) -analyzerResult.Code() // AnalyzerTestResult: input compilation -codeFixResult.Code() // CodeFixTestResult: input compilation -codeFixResult.FixedCode() // CodeFixTestResult: parsed fixed source (or post-fix solution) -fixAllResult.FixedCode() // CodeFixFixAllResult: post-fix documents -refactorResult.FixedCode() // RefactorTestResult: post-refactor documents -``` - -Every `Get` has an accompanying `Has` (bool) and `TryGet` (out): `GetMethod`/`HasMethod`/`TryGetMethod`, -`GetClass`, `GetStruct`, `GetInterface`, `GetEnum`, `GetDelegate`, `GetRecord`, `GetProperty`, -`GetField`, `GetConstructor`, `GetNamespace`, `GetTypeDeclaration`, plus generic `Get`/`Has` -and `GetSyntaxTree`/`HasSyntaxTree`. `Get` throws `SyntaxNotFoundException` when nothing matches. - -Type lookups accept an optional generic arity — `GetClass(name, arity)` / `HasClass(name, arity)` — and the -`TypeReference`/`TypeIdentity` overloads match arity automatically from the identity, so -`new TypeIdentity("ResourceDefinition", ns, arity: 1)` finds `ResourceDefinition` without matching the -non-generic `ResourceDefinition`. - -Every `Get` returns a `CodeQueryResult` — the matched syntax node (`Node`) plus a query scoped to it -(`Query`), with implicit conversions to both the node and the scoped query. Use `.Node` for direct syntax -access, or chain member queries (the originating query is carried by the result, so it is not passed again). - -Types can be matched against `TypeReference`/`TypeIdentity`, resolved through the compilation's semantic -model (nullable value types are significant, so `int?` never matches `int`): - -```csharp -result.Generated().HasMethod("DoWork", TypeReference.Create(), TypeReference.Create().Nullable(), complexType); -result.Generated().HasReturnType("Compute", TypeReference.Create()); -result.Generated().GetMethod("Format").HasParameters(TypeReference.Create(), objectReference); -``` - -When a test expects a nullable annotation, prefer the test-only `query.MakeNullable(type)` extension (on a -`CodeQuery`, accepting a `TypeReference` or `TypeIdentity`). It resolves the annotation against the query's -compilation and, unlike `TypeReference.Nullable()`/`TypeIdentity.MakeNullable()`, does not trigger the -`PSGFR16` context-overload suggestion (tests have no generation context to pass). - -Member chaining from a type declaration (`MemberQueryExtensions`): - -```csharp -var service = result.Generated().GetClass("ServiceCollectionExtensions"); // or GetClass(name, "Namespace") -service.HasProperty("Count", intType); -service.HasIndexer(stringType, intType); -service.HasMethod("Add", intType, complexType); -service.HasMethodReturnType("Add", stringType); -service.HasConstructor(stringType); -service.HasAttribute("SomeAttribute"); -``` - -Node-inspection checks on scoped results: - -```csharp -result.Generated().GetClass("Service").HasAccessibility(Accessibility.Public); // resolves C# defaults -result.Generated().GetClass("Service").GetProperty("Name").HasSetterAccessibility(Accessibility.Private); -result.Generated().GetClass("ResourceDefinition", 1).HasGenericTypeParameters("TResourceType"); -result.Generated().GetClass("ResourceDefinition", 1).HasBaseType(new TypeIdentity("ResourceDefinition", ns)); -result.Generated().GetClass("Service").HasNestedType("Builder"); // GetNestedType(...) to fetch -result.Generated().GetClass("Service").IsInNamespace("Example.Models"); // IsInGlobalNamespace() for no namespace -result.Generated().IsInNamespace(cls.Node, "Example.Models"); // or on the query directly -``` - -## Configuring options and a reusable starting point - -`SourceGeneratorTestOptions` is the base record. Common knobs: - -- `AdditionalNamespaces` / `IncludeDefaultNamespaces` — namespaces prepended to test source. -- `AdditionalAssemblyTypes` / `AdditionalReferences` — assemblies referenced by the test compilation - (use `AdditionalAssemblyTypes = [typeof(SomeType)]` to pull in a whole assembly). -- `AdditionalSources` — extra source files added to every run. -- `AnalyzerConfigOptions` — `build_property.*` values; keys without the prefix are also exposed as MSBuild - properties. -- `DisableSourceGeneratorPropertyName` / `DisableSourceGeneratorValue` — generator disable toggle. -- `NullableContextOptions`, `OutputKind`, `LanguageVersion`, `CompileToAssembly`. -- `ValidateCodeWriterScopes`, `EnableLogging`. -- `ExcludeGeneratedSourceHintNames` — hides generated marker trees from `PrimarySyntaxTrees`. - -**Easy starting point recipe.** Derive an options record that seeds the namespaces and assemblies your -generator needs, so every test gets a working compilation with no boilerplate: - -```csharp -public sealed record MyGeneratorTestOptions : SourceGeneratorTestOptions -{ - public MyGeneratorTestOptions() - { - AdditionalNamespaces = AdditionalNamespaces.Add("My.Namespace"); - AdditionalAssemblyTypes = AdditionalAssemblyTypes.AddRange( - typeof(SomeDependencyType), - typeof(TypeIdentity) // the framework's Shared assembly, when needed - ); - DisableSourceGeneratorPropertyName = PropertyLibrary.DisableMyGenerator; - } -} -``` - -`Compile()` returns a copy with `CompileToAssembly = true`, preserving the derived options type. Use the -base class hooks `OnBeforeRun`/`OnBeforeRunAsync`/`OnAfterRun` to mutate sources/options per run (for -example to append a marker attribute source via `WithAdditionalSources`). - -When `CompileToAssembly` is enabled, emission is fully in-memory. On .NET 8+ the assembly loads into a -collectible `AssemblyLoadContext`, so the result is `IDisposable` — `using var result = ...` unloads it and -keeps repeated runs from polluting the default context. `result.CompilationResult.Assembly` is the runnable -assembly (generated code can execute); `result.CompilationResult.Metadata` / `.MetadataAssembly` give a -metadata-only reflection view (types, members, attributes) over the emitted assembly without executing code. - -## Best practices - -- **Deterministic output**: `WriteAutoGeneratedHeader` is timestamp-free; assert with - `ContainsGeneratedCode`/`GeneratesCode` (whitespace-flattened) or `CodeQuery`, never with timestamps. -- **Generator references**: to use a generated type in the test project AND pass the generator type to a - runner, reference the generator project twice — once `OutputItemType="Analyzer"` and once as a normal - reference. -- **Multi-target**: build generators against the Roslyn version that supports the test matrix; the - framework is built against Roslyn 5.0 (net8.0/net9.0 assets keep a .NET 8–10 matrix loading); keep - `System.Collections.Immutable` version pinned to the shared one. -- **Scope validation**: keep `PurviewSourceGeneratorFrameworkValidateCodeWriterScopes` enabled; it makes - undisposed `CodeWriter` scopes fail tests. -- **Prefer `CodeQuery` over string matching** for structural assertions (members, signatures, namespaces). - -## Incremental cache testing (`RunIncrementalAsync`) - -To prove the pipeline caches correctly stage-by-stage, use `SourceGeneratorTestRunner.RunIncrementalAsync` -(or `GenerateIncrementalAsync` on the TUnit base). It runs a sequence of source sets over a **single shared -`GeneratorDriver`** and captures each run's `TrackedSteps`, keyed by tracking name. The canonical reference -implementation is `IncrementalPipelineCacheTests` in `SourceGeneratorShared.UnitTests`; the end-to-end -generator variant is `ServiceRegistrationCacheTests` in -`SourceGeneratorFramework.ExampleGenerator.UnitTests`. Copy the pattern into your own test project — do -not expect the source repo's files locally. - -### What is being asserted and why - -Roslyn reports one `IncrementalStepRunReason` per step output on each run: - -- `New` — the step ran for the first time. -- `Modified` — the step ran and produced a different value than the previous run. -- `Unchanged` — the step ran but produced the same value. -- `Cached` — the step was skipped and its previous result reused from the incremental cache. - -A pipeline is "caching correctly" when an unchanged input keeps every stage `Cached`/`Unchanged`, and a -targeted change marks **only** the stages whose inputs actually changed `Modified` while unrelated stages -stay `Cached`. If a generator accidentally leaks `Compilation`, `SemanticModel`, `ISymbol`, -`SyntaxNode`, or `Location` into a pipeline model, unrelated stages will report `Modified`/`New` on rerun — -these tests fail the build and catch the regression. - -### The four scenarios every cache test should cover - -1. **First run → all `New`.** Nothing can be cached on the first run; this confirms every stage is tracked - under the expected name. -2. **Identical rerun → all `Cached`/`Unchanged`.** `RunIncrementalAsync(sources, ...)` runs the same source - set twice for exactly this case. This is the strongest "it caches" proof. -3. **Source-only change → only the source/attribute stage `Modified`.** Changing an attributed class must - mark `ForAttribute_*` (and downstream output) `Modified` while property/config stages stay `Cached`. -4. **Property-only change → only the property/configuration stage `Modified`.** Toggling an MSBuild - property (via `IncrementalRunInput.AnalyzerConfig`) must mark `GetMSBuildPropertyValue_*` / - `GetGenerationConfiguration` / `GetGenerationContext_*` `Modified` while `ForAttribute_*` stays `Cached`. - -### How the runner makes this possible - -`RunIncrementalAsync` creates **one** driver, enables incremental step tracking, and **reuses the same -`Compilation` instance for identical source sets** (keyed by prepared source text). Without that reuse, -Roslyn would see a fresh compilation on the second run and report stages `Modified`/`New` even though the -sources are byte-identical — the "cached" assertion would fail. - -### The `StepReasons` helper - -`IncrementalCacheRun.Steps` is `ImmutableDictionary>` -keyed by tracking name. Flatten each step's `Outputs` into the reasons list so assertions read cleanly: - -```csharp -static ImmutableDictionary> StepReasons(IncrementalCacheRun run) -{ - var builder = ImmutableDictionary.CreateBuilder>(); - foreach (var pair in run.Steps) - builder[pair.Key] = [.. pair.Value.SelectMany(step => step.Outputs.Select(static output => output.Reason))]; - return builder.ToImmutable(); -} -``` - -### Framework pipeline stage names - -- `GetMSBuildPropertyValue_{Property}` — `IncrementalPipeline.PropertyValueProvider`. -- `GetGenerationConfiguration` — `IncrementalPipeline.GenerationContextValueProvider`. -- `GetGenerationContext_{Capabilities}` — e.g. `GetGenerationContext_EmptyCapabilities`. -- `ForAttribute_{AttributeType}` — `IncrementalPipeline.ForAttributeWithMetadataName`. - -The framework reference (`IncrementalPipelineCacheTests`, framework-agnostic runner) shows all four -scenarios against `TestGenerator`/`DiagnosticTestGenerator`: - -```csharp -using System.Collections.Immutable; -using Purview.SourceGeneratorFramework.TestGenerators; -using StepReason = Microsoft.CodeAnalysis.IncrementalStepRunReason; - -public class IncrementalPipelineCacheTests -{ - const string AttributedSource = """ - [TestAttribute] - public partial class MyClass { } - """; - const string ChangedAttributedSource = """ - [TestAttribute] - public partial class AnotherClass { } - """; - const string TestAttributeSource = """ - [System.AttributeUsage(System.AttributeTargets.Class)] - public sealed class TestAttribute : System.Attribute { } - """; - - static SourceGeneratorTestOptions CreateOptions() => - new SourceGeneratorTestOptions() - .WithAdditionalSources(TestAttributeSource) - .WithExcludeGeneratedSourceHintNames("TestAttribute"); - - static ImmutableDictionary> StepReasons(IncrementalCacheRun run) { /* as above */ } - - [Test] - public async Task FirstRun_AllStagesAreNew(CancellationToken cancellationToken) - { - var result = await new SourceGeneratorTestRunner().RunIncrementalAsync( - [new IncrementalRunInput([AttributedSource])], - CreateOptions(), - cancellationToken); - - var reasons = StepReasons(result.Runs[0]); - await Assert.That(reasons).IsNotEmpty(); - await Assert.That(reasons.Values.SelectMany(r => r).All(r => r == StepReason.New)).IsTrue(); - } - - [Test] - public async Task IdenticalRerun_AllStagesCached(CancellationToken cancellationToken) - { - var result = await new SourceGeneratorTestRunner() - .RunIncrementalAsync([AttributedSource], CreateOptions(), cancellationToken); - - var second = StepReasons(result.Runs[1]); - await Assert.That(second.Values.SelectMany(r => r).All(r => r is StepReason.Cached or StepReason.Unchanged)).IsTrue(); - } - - [Test] - public async Task SourceChange_MarksAttributeStageModified_PropertyStagesStayCached(CancellationToken cancellationToken) - { - var result = await new SourceGeneratorTestRunner().RunIncrementalAsync( - [new IncrementalRunInput([AttributedSource]), new IncrementalRunInput([ChangedAttributedSource])], - CreateOptions(), - cancellationToken); - - var second = StepReasons(result.Runs[1]); - await Assert.That(second["ForAttribute_TestAttribute"]).Contains(StepReason.Modified); - await Assert.That(second["GetMSBuildPropertyValue_DisableTestGenerator"].All(r => r == StepReason.Cached)).IsTrue(); - } - - [Test] - public async Task PropertyChange_MarksPropertyStageModified_AttributeStageStaysCached(CancellationToken cancellationToken) - { - var result = await new SourceGeneratorTestRunner().RunIncrementalAsync( - [ - new IncrementalRunInput([AttributedSource]), - new IncrementalRunInput([AttributedSource], [("build_property.DisableTestGenerator", "true")]), - ], - CreateOptions(), - cancellationToken); - - var second = StepReasons(result.Runs[1]); - await Assert.That(second["GetMSBuildPropertyValue_DisableTestGenerator"]).Contains(StepReason.Modified); - await Assert.That(second["ForAttribute_TestAttribute"].All(r => r == StepReason.Cached)).IsTrue(); - } -} -``` - -### End-to-end generator variant (`GenerateIncrementalAsync`) - -TUnit tests derive from the base class and call `GenerateIncrementalAsync` (which wires the -`OnBeforeRun`/`OnBeforeRunAsync` hooks and the derived options). `ServiceRegistrationCacheTests` in -`SourceGeneratorFramework.ExampleGenerator.UnitTests` is the reference: - -```csharp -public class ServiceRegistrationCacheTests - : TUnitSourceGeneratorTestBase -{ - const string Source = """ - namespace Test; - - [GenerateService] - public class MyService { } - """; - - [Test] - public async Task IdenticalRerun_AllStagesCached(CancellationToken cancellationToken) - { - var result = await GenerateIncrementalAsync([Source], cancellationToken: cancellationToken); - - var second = StepReasons(result.Runs[1]); - string[] frameworkStages = - [ - "GetMSBuildPropertyValue_EmitServiceRegistrationInfo", - "GetGenerationConfiguration", - "GetGenerationContext_EmptyCapabilities", - "ForAttribute_GenerateServiceAttribute", - ]; - await Assert.That( - frameworkStages.All(stage => - second.TryGetValue(stage, out var reasons) - && reasons.All(r => r is StepReason.Cached or StepReason.Unchanged))).IsTrue(); - } -} -``` - -**Why the example filters to `frameworkStages`:** `ServiceRegistrationGenerator` emits its own -`GenerateServiceAttribute` via post-initialization output, which is regenerated as a new `SyntaxTree` each -run. That makes Roslyn's *internal* `ForAttributeWithMetadataName` `Compilation` step legitimately report -`Modified` on an identical rerun. Asserting on the framework-named stages (which stay `Cached`/`Unchanged`) -is the meaningful check. If your generator does not depend on its own post-init output, the stricter -"every tracked step is `Cached`/`Unchanged`" assertion (as in the framework `IncrementalPipelineCacheTests`) -is correct. - -Per-run MSBuild-property changes are supplied with `new IncrementalRunInput(sources, [("build_property.X", "value")])`. - -## License - -This project is licensed under the MIT license. \ No newline at end of file diff --git a/.agents/skills/tunit-test-authoring/SKILL.md b/.agents/skills/tunit-test-authoring/SKILL.md deleted file mode 100644 index fa58cd8f..00000000 --- a/.agents/skills/tunit-test-authoring/SKILL.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -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." ---- - -# TUnit test authoring for Roslyn components - -Use this skill whenever a task involves authoring, fixing, or modernising **TUnit** tests for Roslyn -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. - -## Base class → method matrix - -| Roslyn type | Base class | Method to call | -|---|---|---| -| `IIncrementalGenerator` / `ISourceGenerator` | `TUnitSourceGeneratorTestBase` | `GenerateAsync(source, options, ct)` | -| `DiagnosticAnalyzer` | `TUnitDiagnosticAnalyzerTestBase` | `AnalyzeAsync(source, options, ct)` | -| `CodeFixProvider` (single fix) | `TUnitCodeFixTestBase` | `ApplyCodeFixAsync(source, options, ct)` | -| `CodeFixProvider` (fix-all) | `TUnitCodeFixTestBase` | `ApplyFixAllAsync(sources, options, ct)` | -| `CodeRefactoringProvider` | `TUnitRefactoringTestBase` | `RefactorAsync(source, options, ct)` | - -For cache tests, `TUnitSourceGeneratorTestBase` also exposes `GenerateIncrementalAsync(...)`. - -Framework-agnostic equivalents (no TUnit): `SourceGeneratorTestRunner`, `DiagnosticAnalyzerTestRunner`, -`CodeFixTestRunner`, `RefactoringTestRunner`. - -## Easy starting point: derive your options record - -Create a test-options record that seeds the namespaces and assemblies your component needs, then pass it -to every test via `new MyTestOptions()`: - -```csharp -public sealed record MyGeneratorTestOptions : SourceGeneratorTestOptions -{ - public MyGeneratorTestOptions() - { - AdditionalNamespaces = AdditionalNamespaces.Add("My.Namespace"); - AdditionalAssemblyTypes = AdditionalAssemblyTypes.AddRange( - typeof(SomeDependencyType), - typeof(TypeIdentity) // framework Shared assembly, when needed - ); - DisableSourceGeneratorPropertyName = "DisableMyGenerator"; - } -} - -public class MyGeneratorTests : TUnitSourceGeneratorTestBase -{ - [Test] - public async Task GeneratesExpectedSource(CancellationToken ct) => - await GenerateAsync("...source...", ct); -} -``` - -Use the base hooks to customise per-run: `OnBeforeRun`/`OnBeforeRunAsync` (mutate sources/options, e.g. -`options.WithAdditionalSources(markerAttributeSource)`) and `OnAfterRun`/`OnAfterRunAsync`. Use -`options.Compile()` to opt into `CompileToAssembly` while preserving the derived options type. - -For code fixes/refactorings, select a specific registered action via `CodeFixTestOptions.EquivalenceKey` -or `CodeActionIndex`, and `RefactorTestOptions.Span`/`NodeSelector` (e.g. -`NodeSelector = query => query.GetMethod("M")`). - -## TUnit assertion extensions - -All assertion extensions live under `Purview.SourceGeneratorFramework.Testing.TUnit.Assertions` -(globally imported by the package's props). `Assert.That(...)` calls are terminal and **return the value** -when awaited. - -- **`CodeQueryAssertions`** — return `CodeQueryResult` (the matched node via `.Node`, plus a query - scoped to it via `.Query`): `HasGeneratedMethod` (optionally with `TypeReference[]` parameter types), - `HasGeneratedMethodReturnType`, `HasGeneratedClass` (by name or `TypeReference`/`TypeIdentity` identity), - `HasGeneratedProperty`, `HasGeneratedField`, `HasGeneratedSyntaxTree`; `HasFixedMethod` for code-fix and - refactor results. The assertions operate on a `CodeQuery` directly, so pass any query — `result.Generated()` - (generated trees), `result.Output()` (whole compilation), or `result.FixedCode()` — or use the convenience - overloads on the test result types, which query the relevant code for you. -- **Scoped member chaining** — `HasPropertyOfType(name, type)`, `HasFieldOfType(name, type)`, - `HasMethodOfType(name, TypeReference[])`, `HasConstructorOfType(TypeReference[])`, and - `HasAttributeOfType(name)` chain from a scoped `CodeQueryResult` (e.g. the result of `HasGeneratedClass`) - and return the matched member. They are named to avoid colliding with the bool `Has*` predicates in - `MemberQueryExtensions`. -- **Fluent `.And` chains** — the node-producing assertions (`HasGeneratedClass`, `HasPropertyOfType`, - `HasMethodOfType`, `HasNestedType`) move the chain onto the matched node, so further assertions can be - appended with `.And`. Node-inspection assertions — `WithAccessibility`, `WithGetterAccessibility`, - `WithSetterAccessibility`, `WithBaseType`, `WithGenericTypeParameter(s)`, `IsInNamespace`, - `IsInGlobalNamespace` — keep the node on the chain: - ```csharp - var method = await Assert.That(query) - .HasGeneratedClass("Service") - .And.HasNestedType("Builder") - .And.WithAccessibility(Accessibility.Private) - .And.HasMethodOfType("Build", []); - ``` - Generic types are matched by arity — `HasGeneratedClass(name, arity)` or a `TypeIdentity` with arity — - so `new TypeIdentity("ResourceDefinition", ns, arity: 1)` finds `ResourceDefinition` - without matching the non-generic `ResourceDefinition`. -- **Nullable expected types in tests** — use the test-only `query.MakeNullable(type)` extension (on a - `CodeQuery`). It resolves the annotation against the query's compilation and, unlike - `TypeReference.Nullable()`/`TypeIdentity.MakeNullable()`, does not trip the `PSGFR16` context-overload - suggestion, since tests have no generation context to pass. - ```csharp - CodeQueryResult method = await Assert.That(result.Generated()).HasGeneratedMethod("DoWork", [intType, nullableInt]); - CodeQueryResult cls = await Assert.That(result.Generated()).HasGeneratedClass("Service"); - await Assert.That(cls.HasProperty("Count", intType)).IsTrue(); // chained member query - await Assert.That(cls.Node.Identifier.ValueText).IsEqualTo("Service"); // direct syntax access - await Assert.That(result.FixedCode()).HasFixedMethod("DoWork"); // code-fix / refactor results - - var query = result.Generated(); - var attributeClass = await Assert.That(query).HasGeneratedClass(hostKitAttribute); - await Assert.That(attributeClass).HasPropertyOfType("Name", query.MakeNullable(TypeLibrary.System.String)); - ``` -- **`DiagnosticAssertions`** — `HasDiagnostic(descriptor|id)`, `HasDiagnostics(count)`, - `DoesNotHaveDiagnostic`, `HasNoDiagnostics`, `HasNoErrorDiagnostics` on generator/analyzer/code-fix results. -- **`TypeIdentityAssertions`** — `HasSymbol(TypeIdentity)` / `HasSymbol("Namespace.Type")`. -- **`GeneratedCodeAssertionsExtensions`** — `GeneratesCode(expected)`, `ContainsGeneratedCode(expected)` - (whitespace-flattened string comparison). - -For structural assertions (members, signatures, namespaces) prefer `result.Generated()` + -`Get/Has/TryGet` from `CodeQuery` (see `source-generator-testing`). - -## Incremental cache tests (`GenerateIncrementalAsync`) - -`TUnitSourceGeneratorTestBase` exposes `GenerateIncrementalAsync`, which mirrors `RunIncrementalAsync` but -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. - -Why the tests look the way they do: - -- `GenerateIncrementalAsync([Source])` runs the **same source twice** on a single shared driver. The first - run reports every stage `New`; the second must report `Cached`/`Unchanged` for unchanged stages — that is - the core "it caches" proof. -- `GenerateIncrementalAsync([new IncrementalRunInput([Source]), new IncrementalRunInput([changed])])` runs two - **different** source sets, so a source-only change must mark `ForAttribute_*` `Modified` while - property/configuration stages stay `Cached`. -- `new IncrementalRunInput([Source], [("build_property.X", "value")])` toggles an MSBuild property for one - run only, so a property-only change must mark `GetMSBuildPropertyValue_*`/`GetGenerationConfiguration`/ - `GetGenerationContext_*` `Modified` while `ForAttribute_*` stays `Cached`. -- 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). -- 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`, - `GetGenerationConfiguration`, `GetGenerationContext_EmptyCapabilities`) rather than every tracked step. - -```csharp -public class ServiceRegistrationCacheTests - : TUnitSourceGeneratorTestBase -{ - const string Source = """ - namespace Test; - - [GenerateService] - public class MyService { } - """; - - [Test] - public async Task IdenticalRerun_AllStagesCached(CancellationToken cancellationToken) - { - var result = await GenerateIncrementalAsync([Source], cancellationToken: cancellationToken); - - var second = StepReasons(result.Runs[1]); - string[] frameworkStages = - [ - "GetMSBuildPropertyValue_EmitServiceRegistrationInfo", - "GetGenerationConfiguration", - "GetGenerationContext_EmptyCapabilities", - "ForAttribute_GenerateServiceAttribute", - ]; - await Assert.That( - frameworkStages.All(stage => - second.TryGetValue(stage, out var reasons) - && reasons.All(r => r is StepReason.Cached or StepReason.Unchanged))).IsTrue(); - } - - [Test] - public async Task PropertyChange_MarksPropertyStageModified_AttributeStageStaysCached(CancellationToken cancellationToken) - { - var result = await GenerateIncrementalAsync( - [ - new IncrementalRunInput([Source]), - new IncrementalRunInput([Source], [(PropertyLibrary.EmitServiceRegistrationInfo, "true")]), - ], - cancellationToken: cancellationToken); - - var second = StepReasons(result.Runs[1]); - await Assert.That(second["GetMSBuildPropertyValue_EmitServiceRegistrationInfo"]).Contains(StepReason.Modified); - await Assert.That(second["ForAttribute_GenerateServiceAttribute"].All(r => r is StepReason.Cached or StepReason.Unchanged)).IsTrue(); - } -} -``` - -Use `using StepReason = Microsoft.CodeAnalysis.IncrementalStepRunReason;` and your own stage names. - -## Modernising existing tests - -When converting legacy tests that do `result.GetGeneratedTree(...)` + `string.Contains(...)`: - -1. Replace tree-lookup + string matching with `result.Generated().GetClass/GetMethod/GetProperty(...)` and - the `Has*`/`TryGet*` family. -2. Replace signature string checks with `TypeReference` parameter/return-type matching. -3. Replace `Assert.That(text).Contains("...")` with the terminal assertion extensions that return nodes. -4. Verify options use a derived record (namespaces + assemblies) rather than repeating `AdditionalNamespaces` - 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" - section, swapping in your own generator and stage names. - -## License - -This project is licensed under the MIT license. \ No newline at end of file diff --git a/global.json b/global.json index c5bd1b1a..9ece9928 100644 --- a/global.json +++ b/global.json @@ -3,7 +3,7 @@ "allowPrerelease": false }, "msbuild-sdks": { - "Purview.BuildSdk": "1.0.3" + "Purview.BuildSdk": "1.0.4" }, "test": { "runner": "Microsoft.Testing.Platform" From c10b5db55adf0aa4838070f5cea7daea36c9b199 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 8 Oct 2026 20:08:56 +0100 Subject: [PATCH 2/2] chore: structure --- purview-build.json => .config/purview-build.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename purview-build.json => .config/purview-build.json (100%) diff --git a/purview-build.json b/.config/purview-build.json similarity index 100% rename from purview-build.json rename to .config/purview-build.json