diff --git a/.csharpierignore b/.csharpierignore index eb7edce..7040451 100644 --- a/.csharpierignore +++ b/.csharpierignore @@ -1 +1,4 @@ -/src/tests/SourceGeneratorFramework.UnitTests/Resources/** \ No newline at end of file +**/bin/ +**/obj/ +**/generated/** +**/Resources/** \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 9c40525..95d30c0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ charset = utf-8 csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_primary_constructors = true:suggestion csharp_style_prefer_top_level_statements = true:silent -end_of_line = crlf +end_of_line = lf indent_size = 4 indent_style = tab insert_final_newline = true @@ -2664,5 +2664,8 @@ dotnet_diagnostic.RCS1205.severity = none dotnet_diagnostic.IDE0130.severity = none dotnet_diagnostic.CA1034.severity = none +[**/{Extension,Extensions}.{cs,vb}] +dotnet_diagnostic.CA1034.severity = none + [**/Generated/**/*.{cs,vb}] dotnet_diagnostic.CS8602.severity = none diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a92ee18..c2f96fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,5 +46,5 @@ jobs: if: steps.version.outputs.should_publish == 'true' env: Release__ShouldPublish: true - NuGet__ApiKey: ${{ secrets.NUGET_API_KEY }} + NuGet__ApiKey: ${{ secrets.NUGET__APIKEY }} run: dotnet run --project build/PipelineCLI/PipelineCLI.csproj --configuration Release diff --git a/build/PipelineCLI/Modules/PackModule.cs b/build/PipelineCLI/Modules/PackModule.cs index 8313cd8..af4b875 100644 --- a/build/PipelineCLI/Modules/PackModule.cs +++ b/build/PipelineCLI/Modules/PackModule.cs @@ -9,7 +9,7 @@ namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; [ModuleCategory("Build")] -[DependsOn] +[DependsOn] [DependsOn] public sealed class PackModule(IOptions settings, IOptions releaseSettings) : Module diff --git a/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs b/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs index 3dc4978..1eddee2 100644 --- a/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs +++ b/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs @@ -67,7 +67,10 @@ CancellationToken cancellationToken if (!Directory.Exists(fullLocalFeedPath)) Directory.CreateDirectory(fullLocalFeedPath); - var packages = Directory.GetFiles(buildSettings.Value.ArtifactsFolder, "*.s*nupkg").ToArray(); + var packages = Directory + .GetFiles(buildSettings.Value.ArtifactsFolder, "*.nupkg") + .Concat(Directory.GetFiles(buildSettings.Value.ArtifactsFolder, "*.snupkg")) + .ToArray(); if (packages.Length == 0) { throw new InvalidOperationException( diff --git a/build/PipelineCLI/Properties/launchSettings.json b/build/PipelineCLI/Properties/launchSettings.json index 2679915..d5bbebe 100644 --- a/build/PipelineCLI/Properties/launchSettings.json +++ b/build/PipelineCLI/Properties/launchSettings.json @@ -1,5 +1,8 @@ { "profiles": { + "Run": { + "commandName": "Project" + }, "Local-NuGet": { "commandName": "Project", "commandLineArgs": "--Release:Mode=LocalNuGet\r\n--PublishLocalNuGet:LocalFeedPath=p:\\_sync-projects\\.local-nuget\\" diff --git a/global.json b/global.json index 932522c..df658f9 100644 --- a/global.json +++ b/global.json @@ -5,7 +5,7 @@ "allowPrerelease": false }, "msbuild-sdks": { - "Purview.DotNetProjectSdk": "1.0.0-prerelease.42" + "Purview.DotNetProjectSdk": "1.0.0-prerelease.43" }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/package.json b/package.json index cc7d9e4..68bdd48 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name": "purview-sourcegeneratorframework", - "version": "1.0.0-prerelease.26", + "version": "1.0.0-prerelease.27", "private": true } diff --git a/src/src/SourceGeneratorFramework.Testing/Extensions/SourceGeneratorTestOptionsExtensions.cs b/src/src/SourceGeneratorFramework.Testing/Extensions/SourceGeneratorTestOptionsExtensions.cs index c69af23..54256cf 100644 --- a/src/src/SourceGeneratorFramework.Testing/Extensions/SourceGeneratorTestOptionsExtensions.cs +++ b/src/src/SourceGeneratorFramework.Testing/Extensions/SourceGeneratorTestOptionsExtensions.cs @@ -16,6 +16,19 @@ public static class SourceGeneratorTestOptionsExtensions /// The concrete options type is preserved, so derived records such as /// and can opt into compiling /// the output assembly without losing their derived properties. + /// + /// The inherited is typed as the base + /// , so calling Compile() on it returns the + /// base type. A derived record that wants a typed default should hide Default with its + /// own typed static property: + /// + /// public record MyTestOptions : SourceGeneratorTestOptions + /// { + /// public static new MyTestOptions Default => new(); + /// } + /// + /// Calling MyTestOptions.Default.Compile() then returns a MyTestOptions. + /// /// public TOptions Compile() => options with { CompileToAssembly = true }; } diff --git a/src/src/SourceGeneratorFramework.Testing/Sdk/README.md b/src/src/SourceGeneratorFramework.Testing/Sdk/README.md index 9b1eeb5..f12c0dc 100644 --- a/src/src/SourceGeneratorFramework.Testing/Sdk/README.md +++ b/src/src/SourceGeneratorFramework.Testing/Sdk/README.md @@ -112,6 +112,20 @@ var options = new SourceGeneratorTestOptions var result = await runner.RunAsync(source, options.Compile()); ``` +`Compile()` is an extension method that preserves the concrete options type. A derived options record +that wants a typed default must hide the inherited `SourceGeneratorTestOptions.Default` with a typed +static, otherwise `Default.Compile()` returns the base type: + +```csharp +public record MyTestOptions : SourceGeneratorTestOptions +{ + public static new MyTestOptions Default => new(); +} + +// Returns MyTestOptions with CompileToAssembly enabled. +var result = await runner.RunAsync(source, MyTestOptions.Default.Compile()); +``` + Analyzer options are preserved under their supplied keys. Keys without the Roslyn `build_property.` prefix are additionally exposed as compiler-visible MSBuild properties, so either `MyGenerator_Disable` or `build_property.MyGenerator_Disable` can be used in tests. diff --git a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs index 7df59c4..a3a72dc 100644 --- a/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs +++ b/src/src/SourceGeneratorFramework.Testing/SourceGeneratorTestOptions.cs @@ -24,6 +24,12 @@ public record SourceGeneratorTestOptions /// /// Configure this once during test-assembly initialization, before tests execute in parallel. /// Existing options instances are snapshots and are not changed when this property is updated. + /// + /// This property is typed as . A derived record that needs + /// a default of its own type should hide it with a typed static, for example + /// public static new MyTestOptions Default => new();, so fluent extensions such as + /// Compile() preserve the derived type. + /// /// public static SourceGeneratorTestOptions Default { diff --git a/src/src/SourceGeneratorFramework/Sdk/.agents/agents/source-generator-framework-writer.agent.md b/src/src/SourceGeneratorFramework/Sdk/.agents/agents/source-generator-framework-writer.agent.md index 7710ee3..65f5bd0 100644 --- a/src/src/SourceGeneratorFramework/Sdk/.agents/agents/source-generator-framework-writer.agent.md +++ b/src/src/SourceGeneratorFramework/Sdk/.agents/agents/source-generator-framework-writer.agent.md @@ -41,7 +41,7 @@ The most important rules are: - **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.** -- **Keep `CodeWriter` instances output-scoped; never cache them in incremental provider state or custom contexts.** +- **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 @@ -53,7 +53,7 @@ The most important rules are: 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. Keep `CodeWriter` instances output-scoped; never cache in incremental provider state or custom contexts. +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. diff --git a/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md b/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md index 26ae901..635486e 100644 --- a/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md +++ b/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md @@ -18,7 +18,41 @@ When creating source output, favor this shape: 5. Write structured types and members using declaration options records. 6. Add source once per output artifact. -Never store `CodeWriter` in `GenerationContext` or custom contexts. +### 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 @@ -265,14 +299,17 @@ Apply this checklist in order: - Manually writing `{` / `}` around methods and types where scope APIs exist. - Hard-coded nullable type suffixes and generic syntax in arbitrary strings when `TypeReferenceOptions` is available. - Raw XML tag string composition when XML extension methods can enforce consistency. -- Sharing one `CodeWriter` across multiple generated outputs. +- 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 per output callback and not cached. +- `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. diff --git a/src/src/SourceGeneratorFramework/Sdk/README.md b/src/src/SourceGeneratorFramework/Sdk/README.md index eac3951..6e97960 100644 --- a/src/src/SourceGeneratorFramework/Sdk/README.md +++ b/src/src/SourceGeneratorFramework/Sdk/README.md @@ -202,7 +202,9 @@ IncrementalPipeline.RegisterSourceOutput( Treat `GenerationContext` values as cached incremental-pipeline state and each `CodeWriter` as mutable, output-scoped execution state. Create the writer inside the registered source-output -callback, after the incremental cache boundary: +callback, after the incremental cache boundary. Creating it in the callback and passing it to +emitter/helper methods called from that same callback is the intended pattern; the only thing that +is forbidden is persisting the writer in pipeline state, where Roslyn caches it: ```csharp IncrementalPipeline.RegisterSourceOutput( diff --git a/src/tests/SourceGeneratorFramework.UnitTests/TestingFrameworkTests.cs b/src/tests/SourceGeneratorFramework.UnitTests/TestingFrameworkTests.cs index c90cc40..dbab701 100644 --- a/src/tests/SourceGeneratorFramework.UnitTests/TestingFrameworkTests.cs +++ b/src/tests/SourceGeneratorFramework.UnitTests/TestingFrameworkTests.cs @@ -8,6 +8,8 @@ public class TestingFrameworkTests sealed record CustomSourceGeneratorTestOptions : SourceGeneratorTestOptions { public string CustomValue { get; init; } = "custom"; + + public static new CustomSourceGeneratorTestOptions Default => new(); } const string GenerateAttributeSource = """ @@ -342,4 +344,16 @@ public async Task Compile_OnCustomDerivedOptions_PreservesConcreteTypeAndPropert await Assert.That(result.GetType()).IsEqualTo(typeof(CustomSourceGeneratorTestOptions)); await Assert.That(result.CustomValue).IsEqualTo("custom"); } + + [Test] + public async Task Compile_OnTypedStaticDefault_PreservesConcreteTypeAndProperties() + { + var options = CustomSourceGeneratorTestOptions.Default; + + var result = options.Compile(); + + await Assert.That(result.CompileToAssembly).IsTrue(); + await Assert.That(result.GetType()).IsEqualTo(typeof(CustomSourceGeneratorTestOptions)); + await Assert.That(result.CustomValue).IsEqualTo("custom"); + } }