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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .csharpierignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
/src/tests/SourceGeneratorFramework.UnitTests/Resources/**
**/bin/
**/obj/
**/generated/**
**/Resources/**
5 changes: 4 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion build/PipelineCLI/Modules/PackModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules;

[ModuleCategory("Build")]
[DependsOn<BuildModule>]
[DependsOn<RunTestsModule>]
[DependsOn<VersionModule>]
public sealed class PackModule(IOptions<BuildSettings> settings, IOptions<ReleaseSettings> releaseSettings)
: Module<CommandResult>
Expand Down
5 changes: 4 additions & 1 deletion build/PipelineCLI/Modules/PublishLocalNuGetModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions build/PipelineCLI/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -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\\"
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "purview-sourcegeneratorframework",
"version": "1.0.0-prerelease.26",
"version": "1.0.0-prerelease.27",
"private": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ public static class SourceGeneratorTestOptionsExtensions
/// The concrete options type is preserved, so derived records such as
/// <see cref="AnalyzerTestOptions"/> and <see cref="CodeFixTestOptions"/> can opt into compiling
/// the output assembly without losing their derived properties.
/// <para>
/// The inherited <see cref="SourceGeneratorTestOptions.Default"/> is typed as the base
/// <see cref="SourceGeneratorTestOptions"/>, so calling <c>Compile()</c> on it returns the
/// base type. A derived record that wants a typed default should hide <c>Default</c> with its
/// own typed static property:
/// <code>
/// public record MyTestOptions : SourceGeneratorTestOptions
/// {
/// public static new MyTestOptions Default => new();
/// }
/// </code>
/// Calling <c>MyTestOptions.Default.Compile()</c> then returns a <c>MyTestOptions</c>.
/// </para>
/// </remarks>
public TOptions Compile() => options with { CompileToAssembly = true };
}
Expand Down
14 changes: 14 additions & 0 deletions src/src/SourceGeneratorFramework.Testing/Sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ public record SourceGeneratorTestOptions
/// <remarks>
/// 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.
/// <para>
/// This property is typed as <see cref="SourceGeneratorTestOptions"/>. A derived record that needs
/// a default of its own type should hide it with a typed static, for example
/// <c>public static new MyTestOptions Default => new();</c>, so fluent extensions such as
/// <c>Compile()</c> preserve the derived type.
/// </para>
/// </remarks>
public static SourceGeneratorTestOptions Default
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<T>` is used for collections.
Expand Down
4 changes: 3 additions & 1 deletion src/src/SourceGeneratorFramework/Sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand Down Expand Up @@ -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");
}
}