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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Do **not** suppress or mute compiler, analyser, or build warnings by adding `<No

## Documentation and Samples

Any API change must update the corresponding documentation, including XML doc comments and `docs/` pages such as `docs/code-writer.md`. Samples (the `SourceGeneratorFramework.ExampleGenerator` reference implementation and benchmarks) must use the current best-practice APIs: the minimal-parameter `CodeWriter` overloads, structured statements (`MethodCall`, `Return`, `Assignment`, `Throw`, `Comment`, `NetConditionalReturn`) rather than raw text, and the current method names. Add or update a sample whenever an API addition or change warrants a demonstrable example, and cover it with unit tests.
Any API change must update the corresponding documentation, including XML doc comments and `docs/wiki/` pages such as `docs/wiki/Code-Writer.md`. Samples (the `SourceGeneratorFramework.ExampleGenerator` reference implementation and benchmarks) must use the current best-practice APIs: the minimal-parameter `CodeWriter` overloads, structured statements (`MethodCall`, `Return`, `Assignment`, `Throw`, `Comment`, `NetConditionalReturn`) rather than raw text, and the current method names. Add or update a sample whenever an API addition or change warrants a demonstrable example, and cover it with unit tests.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ A set of libraries for building and testing incremental C# source generators usi

## Documentation

- [Source generator & analyser best practices](docs/guide.md)
- [CodeWriter structured API reference](docs/code-writer.md)
- [Step-cache tests for incremental generators](docs/step-cache-tests.md)
- [Benchmark results](docs/performance.md)
- [Source generator & analyser best practices](docs/wiki/Guide.md)
- [CodeWriter structured API reference](docs/wiki/Code-Writer.md)
- [Step-cache tests for incremental generators](docs/wiki/Step-Cache-Tests.md)
- [Benchmark results](docs/wiki/Performance.md)

## Packages

Expand Down Expand Up @@ -69,7 +69,7 @@ See the [`Purview.SourceGeneratorFramework.Generators` README](src/src/SourceGen

`TypeLibraryGenerator` removes the boilerplate of hand-writing the static type library that exposes the `TypeIdentity`/`TypeReference` values a generator needs. From a small declarative spec (`[GenerateTypeLibrary]` + `[TypeRef]` members), it emits a self-contained `public static partial` type library whose nested `public static partial` classes mirror the namespaces of the members, each with a `Namespace` constant and `public static readonly` fields.

See [docs/type-library.md](docs/type-library.md) for the DSL, the member-accessibility rules, and a runnable sample in [`SourceGeneratorFramework.ExampleGenerator`](src/src/SourceGeneratorFramework.ExampleGenerator/TypeLibrarySpec.cs).
See [docs/wiki/Type-Library.md](docs/wiki/Type-Library.md) for the DSL, the member-accessibility rules, and a runnable sample in [`SourceGeneratorFramework.ExampleGenerator`](src/src/SourceGeneratorFramework.ExampleGenerator/TypeLibrarySpec.cs).

## Packaging

Expand Down
115 changes: 115 additions & 0 deletions docs/wiki/Analyzers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Analyzers

The `Purview.SourceGeneratorFramework` package includes the
`Purview.SourceGeneratorFramework.Analyzers` assembly as an analyzer asset, together with the
`Purview.SourceGeneratorFramework.CodeFixers` code fix providers. The diagnostics are enabled
automatically when you reference `Purview.SourceGeneratorFramework` from a source generator project.

## How the analyzers are shipped

The analyzer and code-fix assemblies are built as Roslyn components
(`IsRoslynComponent = true`) and packed into the `Purview.SourceGeneratorFramework` package under
`analyzers/dotnet/cs/`. Because they are not separately packable NuGet packages, they are documented
here rather than in a standalone README.

The analyzers enforce two families of rules:

- **Incremental generator best practice** — `PSGFR11`–`PSGFR33`, covering pipeline design,
`CodeWriter` usage, Roslyn component discovery, and extension-class conventions.
- **C# 14 extension-member conventions** — `PSGFR34`–`PSGFR38`, plus the associated
`ReorganizeExtensionClassCodeFixProvider` and `ConvertToExtensionBlockCodeFixProvider`.

## Rule reference

| Rule | Summary |
|------|---------|
| `PSGFR11` | Prefer `SyntaxProvider.ForAttributeWithMetadataName` over `CreateSyntaxProvider` for attribute-based detection. |
| `PSGFR12` | Use `IIncrementalGenerator` / `RegisterSourceOutput` instead of `ISourceGenerator`. |
| `PSGFR14` | Avoid `RegisterImplementationSourceOutput` unless implementation-only output is required. |
| `PSGFR15` | Pipeline model collection members should use sequence equality (e.g. `EquatableArray<T>`). |
| `PSGFR16` | Prefer the nullable-context `Nullable()`/`MakeNullable()` overload so annotations honour the target compilation. |
| `PSGFR17` | Consume `CodeWriter` scope-returning methods (`...Scope`, `IndentedScope`) with `using`. |
| `PSGFR18` | Prefer structured declaration APIs (`Class`, `Method`, `Property`, `Field`) over raw declaration text. |
| `PSGFR19` | Prefer structured statement APIs (`Return`, `MethodCall`, `Throw`, `Assignment`, `Using`, `Comment`) over raw statement text. |
| `PSGFR20` | Prefer the minimal `CodeWriter` overloads over constructing `*DeclarationOptions` values manually. |
| `PSGFR21` | Prefer `HashDefines`/`HashDefinesScope` for `#if`/`#endif` conditional-compilation directives. |
| `PSGFR22` | Prefer `PragmaDisable`/`OpenPragmasScope` for `#pragma warning` directives. |
| `PSGFR23` | Prefer structured `IfBlock`/`ElseIf`/`Else` over raw `if` block text. |
| `PSGFR24` | `CodeFixProvider` is not marked `[ExportCodeFixProvider]`; Visual Studio will never discover it. |
| `PSGFR25` | `DiagnosticAnalyzer` is not marked `[DiagnosticAnalyzer]`; it will never run. |
| `PSGFR26` | A generator type is not marked `[Generator]`; it will never run. |
| `PSGFR27` | A Roslyn component type is not public; the compiler host cannot instantiate it. |
| `PSGFR28` | `FixableDiagnosticIds` references a diagnostic ID no analyzer in the compilation produces; the fix will never be shown. |
| `PSGFR29` | Do not embed a `CodeWriter` in a string; use `XmlCommentWriter.XmlInlineCode` instead. |
| `PSGFR30` | Prefer `static` lambdas in incremental pipeline methods so the compiler never allocates a closure on the per-item hot path. |
| `PSGFR31` | Prefer `GeneratorAttributeSyntaxContext.TargetSymbol` over `SemanticModel.GetDeclaredSymbol(ctx.TargetNode)`. |
| `PSGFR32` | Avoid `NormalizeWhitespace` when generating source; use an indented text writer such as `CodeWriter`. |
| `PSGFR33` | Pipeline models must not retain Roslyn objects (`ISymbol`, `SyntaxNode`, `Location`, ...); extract the information into value types. |
| `PSGFR34` | Prefer C# 14 `extension(Receiver)` blocks over classic static `this`-parameter extension methods. |
| `PSGFR35` | Extension class name must match the extended type (`{Receiver}Extensions`). |
| `PSGFR36` | Extension classes must be placed in the extended type's namespace under an `Extensions` folder. |
| `PSGFR37` | One extension class per receiver type; split classes that extend multiple types. |
| `PSGFR38` | Extension classes should carry `[EditorBrowsable(EditorBrowsableState.Never)]`. |

## Type-library and attribute-model diagnostics

The bundled generators carry their own diagnostic families, reported by the
`TypeLibraryValidationAnalyzer` (`TLB0001`–`TLB0019`) and the attribute-data-model validation
analyzers. These are documented on their feature pages:

- [Type-Library.md](Type-Library.md#validation)
- [Attribute-Data-Models.md](Attribute-Data-Models.md)

## Code fixes

Code fix providers ship in the `Purview.SourceGeneratorFramework.CodeFixers` assembly and cover the
analyzer rules above, including:

- `AddGeneratorAttributeCodeFixProvider` — adds the missing `[Generator]` attribute (`PSGFR26`).
- `AddDiagnosticAnalyzerAttributeCodeFixProvider` — adds `[DiagnosticAnalyzer]` (`PSGFR25`).
- `AddExportCodeFixProviderAttributeCodeFixProvider` — adds `[ExportCodeFixProvider]` (`PSGFR24`).
- `MakeRoslynComponentPublicCodeFixProvider` — makes the component type public (`PSGFR27`).
- `RemoveOrphanedFixableDiagnosticIdCodeFixProvider` — removes unused fixable diagnostic IDs (`PSGFR28`).
- `PreferTargetSymbolCodeFixProvider` — switches to `TargetSymbol` (`PSGFR31`).
- `PreferStaticLambdaCodeFixProvider` — makes pipeline lambdas `static` (`PSGFR30`).
- `PreferNullableContextOverloadCodeFixProvider` — adds the generation context to `Nullable()` /
`MakeNullable()` calls, including project-wide "Fix all" support (`PSGFR16`).
- `PipelineModelReferenceEqualityCollectionCodeFixProvider` — wraps collection members for sequence
equality (`PSGFR15`).
- `PreferStructuredCodeWriterIfBlockCodeFixProvider` — rewrites raw `if`/`else if`/`else` block text
to the structured `IfBlock`/`ElseIf`/`Else` APIs (`PSGFR23`).
- `CodeWriterToStringCodeFixProvider` — replaces embedded `CodeWriter` string interpolation (`PSGFR29`).
- `AttributeDataModelSymbolPropertyCodeFixProvider` — fixes attribute-data-model symbol properties.
- `ReorganizeExtensionClassCodeFixProvider` — renames (`PSGFR35`), splits multi-receiver classes
(`PSGFR37`), moves the class under `Extensions/{ReceiverNamespace}/`, and updates referencing files
(`PSGFR36`).
- `ConvertToExtensionBlockCodeFixProvider` — converts classic methods to C# 14 `extension` blocks
(`PSGFR34`).
- `AddExtensionClassMetadataCodeFixProvider` — adds `[EditorBrowsable(EditorBrowsableState.Never)]`
to extension classes (`PSGFR38`).
- Type-library fixes — `TypeLibraryMemberAccessibilityCodeFixProvider`,
`TypeLibraryMarkerDefaultInitializerCodeFixProvider`, `MakeTypeLibrarySpecPartialCodeFixProvider`,
`RenameTypeLibrarySpecCodeFixProvider`, and `TypeLibraryMemberTypeCodeFixProvider`.

See [Guide.md](Guide.md#19-extension-class-conventions) for the extension-class conventions the
`PSGFR34`–`PSGFR38` rules enforce.

## Roslyn component discovery

The compiler host only loads a source generator, diagnostic analyzer, or code fix provider when three
conditions hold. Missing any one means the component is **silently ignored**:

1. **The type is public** (`PSGFR27`).
2. **The type is decorated** — `[Generator]` (`PSGFR26`), `[DiagnosticAnalyzer]` (`PSGFR25`), or
`[ExportCodeFixProvider]` (`PSGFR24`).
3. **The assembly is loaded as an analyzer** — packed under `analyzers/dotnet/cs/` in a package, or
referenced with `OutputItemType="Analyzer"` in a project reference.

A code fix provider also only appears when the diagnostic ID in `FixableDiagnosticIds` is actually
produced by an analyzer loaded alongside it (`PSGFR28`). Visual Studio MEF-composes fix providers when
the analyzer set loads, so after adding or updating a fixer assembly you must restart Visual Studio or
reload the project for the fixes to appear.

## License

This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project.
196 changes: 196 additions & 0 deletions docs/wiki/Attribute-Data-Models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# Attribute Data Models

`AttributeDataModelGenerator` generates `readonly record struct` parser models for .NET attributes.
Instead of hand-writing `FromAttributeData` methods for every attribute you inspect in a source
generator, declare a `readonly partial record struct` with `[Generate]` and the generator fills in the
`Empty` sentinel, `FromAttributeData` overloads, and property extraction logic.

The generator is implemented in `Purview.SourceGeneratorFramework.Generators` and ships inside the
`Purview.SourceGeneratorFramework` package under `analyzers/dotnet/cs/`, so it runs automatically
when you reference the package.

## Marker attributes

The generator emits marker attributes into your compilation:

| Attribute | Purpose |
| --- | --- |
| `[Generate(Type targetAttribute)]` | Placed on a `readonly partial record struct` to opt into generation. |
| `[Generate(string targetAttribute)]` | Resolves the attribute by fully-qualified name. Use when the attribute type is not available in the generator's compilation (e.g. `LengthAttribute` in .NET 8+ or a self-generated attribute). |
| `[Property]` | A record parameter is populated from a named attribute property (the property name is inferred from the parameter name unless overridden). |
| `[Property(string name)]` | Explicit named property source. |
| `[Property(..., DefaultValue = ...)]` | Fallback value when the named property is not present. |
| `[Argument]` | Populated from a constructor argument by parameter name. |
| `[Argument(int index)]` | Constructor argument by parameter index. |
| `[Argument(string name)]` | Constructor argument by parameter name. |
| `[Argument(..., DefaultValue = ...)]` | Fallback when the constructor argument is not present. |
| `[NestedModel]` | Populated by recursively calling `FromAttributeData` on a nested generated model. |
| `[Exclude]` | Skips auto-discovery for this parameter. |
| `[GenericTypeArgument]` | Populated from a generic type argument of the attribute class. |
| `[GenericTypeArgument(int index)]` | Generic type argument by position. |
| `[GenericTypeArgument(string name)]` | Generic type argument by type parameter name. |

## Manual mapping

```csharp
using Microsoft.CodeAnalysis;
using Purview.SourceGeneratorFramework.Generators;
using System.ComponentModel.DataAnnotations;

namespace MySourceGenerator.Models;

[Generate(typeof(RequiredAttribute))]
public readonly partial record struct RequiredAttributeData(
bool AllowEmptyStrings
);
```

Generated output:

```csharp
readonly record struct RequiredAttributeData(bool Exists, bool AllowEmptyStrings)
{
public static readonly RequiredAttributeData Empty = new(false, default(bool));

public static RequiredAttributeData FromAttributeData(ImmutableArray<AttributeData> attributes)
{
// ...
}

public static RequiredAttributeData FromAttributeData(AttributeData attributeData)
{
if (!TargetAttribute.Equals(attributeData.AttributeClass))
return Empty;

attributeData.TryGetNamedArgument<bool>("AllowEmptyStrings", out var allowEmptyStrings);
return new(true, allowEmptyStrings);
}
}
```

## String target names

When the attribute type is not referenced in the generator project, pass the fully-qualified name as
a string. This is useful for attributes newer than the generator's target framework (e.g.
`LengthAttribute` in .NET 8+) or attributes that are generated by the same generator:

```csharp
[Generate("System.ComponentModel.DataAnnotations.RequiredAttribute")]
public readonly partial record struct RequiredAttributeData(
bool AllowEmptyStrings
);
```

A plain type name (`"RequiredAttribute"`) can also be used, which matches an attribute in the global
namespace or in any namespace. `AutoDiscover` requires the real `Type` overload because it must
inspect the attribute's constructors and properties.

## Constructor arguments

```csharp
[Generate(typeof(LengthAttribute))]
public readonly partial record struct LengthAttributeData(
[Argument(0)] int MinimumLength,
[Argument(1)] int MaximumLength
);
```

Or by constructor parameter name:

```csharp
[Generate(typeof(StringLengthAttribute))]
public readonly partial record struct StringLengthAttributeData(
[Argument("maximumLength", DefaultValue = 2147483647)] int MaximumLength,
int MinimumLength
);
```

## Nested models

Any property whose type is itself annotated with `[Generate]` can be populated as a nested model.
This is useful for shared base attribute data, such as `ValidationAttribute` in
`System.ComponentModel.DataAnnotations`:

```csharp
[Generate(typeof(ValidationAttribute), MatchByInheritance = true)]
public readonly partial record struct ValidationAttributeData(
[Property] string? ErrorMessage,
[Property] string? ErrorMessageResourceName,
[Property] ITypeSymbol? ErrorMessageResourceType
);

[Generate(typeof(RequiredAttribute))]
public readonly partial record struct RequiredAttributeData(
bool AllowEmptyStrings,
[NestedModel] ValidationAttributeData ValidationAttribute
);
```

Because `ValidationAttributeData` uses `MatchByInheritance = true`, it matches any attribute that
derives from `ValidationAttribute`, including `RequiredAttribute`.

## Generic type arguments

If the attribute class is generic, a record parameter can be populated from the attribute's type
argument:

```csharp
[Generate(typeof(MyGenericAttribute<>))]
public readonly partial record struct MyGenericAttributeData<T>(
[GenericTypeArgument] T Value
);
```

Use `[GenericTypeArgument(0)]` or `[GenericTypeArgument("TValue")]` to disambiguate when the
attribute has multiple type parameters.

## Auto-discovery

For simple attributes you can let the generator discover all constructor parameters and public named
properties automatically:

```csharp
[Generate(typeof(RequiredAttribute), AutoDiscover = true)]
public readonly partial record struct RequiredAttributeData;
```

This generates the same `RequiredAttributeData` as the manual example above. Nested models are not
auto-discovered; declare them explicitly if needed.

## Default values

`DefaultValue` provides a runtime fallback when the attribute does not contain the requested property
or argument. The `Empty` sentinel always uses `default(T)` for every property (including an `Exists`
field set to `false`):

```csharp
[Generate(typeof(HostKitAttribute))]
public readonly partial record struct HostKitAttributeData(
[Argument("name", DefaultValue = "MyApp")] string Name,
[Argument("generateOptions", DefaultValue = true)] bool GenerateOptions
);
```

## Type library integration

A `[TypeRef]` member declared with `GenerateFullNameConst` produces a `public const string
{Member}FullName`, which can be used as the `[Generate]` target of an attribute-data model instead of
a `typeof(...)` value — see [Type-Library.md](Type-Library.md#using-full-name-constants-as-attribute-data-model-targets)
for the full example.

Because the `TypeLibrary` class is emitted through `TypeLibraryGenerator`'s main pipeline, its
constants are **not** present in the compilation that `AttributeDataModelGenerator`'s
`ForAttributeWithMetadataName` pipeline sees (only post-initialization output is shared between
generators in a single pass). `AttributeDataModelGenerator` therefore reassembles the target from the
argument's member-access expression — guarded so the root identifier must match a
`[GenerateTypeLibrary]` spec's `ClassName` — and resolves it against the compilation.

For `[Argument]`/`[Property]` members marked `IsEnum = true`, a `DefaultValue` supplied as a **bare
member name** (for example `"Inherit"`) is expanded to the fully-qualified
`"{EnumFullName}.{Member}"` form using the enum type of the target attribute's matching constructor
parameter (for `[Argument]`) or property (for `[Property]`). Fully-qualified defaults and defaults
whose enum type cannot be resolved are emitted unchanged.

## License

This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project.
4 changes: 2 additions & 2 deletions docs/code-writer.md → docs/wiki/Code-Writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ when it was opened:
- Pass `throwOnUnclosedScopes: false` explicitly when a test intentionally materializes partial output.

Scope tracking has a real cost — every scope open captures a `StackTrace` and allocates a per-scope
record — which is why production leaves it off (see [docs/performance.md](performance.md)).
record — which is why production leaves it off (see [Performance.md](Performance.md)).

## Primitives

Expand Down Expand Up @@ -581,6 +581,6 @@ writer.Property("Name", TypeReference.Create<string>(), TypeDeclarationAccessibi

## Samples

The [`SourceGeneratorFramework.ExampleGenerator`](../src/src/SourceGeneratorFramework.ExampleGenerator)
The [`SourceGeneratorFramework.ExampleGenerator`](../../src/src/SourceGeneratorFramework.ExampleGenerator)
reference implementation demonstrates these APIs end-to-end, including the `CodeWriterSampleGenerator`,
which compiles a best-practice sample class for every `[GenerateCodeWriterSample]` target.
Loading