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
4 changes: 2 additions & 2 deletions docs/wiki/Analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The analyzers enforce two families of rules:
| `PSGFR37` | One extension class per receiver type; split classes that extend multiple types. |
| `PSGFR38` | Extension classes should carry `[EditorBrowsable(EditorBrowsableState.Never)]`. |
| `PSGFR39` | A non-packable Roslyn component that explicitly opts out of the default self-contained analyzer output (`PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=false`) while embedding the framework, otherwise the package embeds the loose framework DLL under `analyzers/`. |
| `PSGFR40` | In Roslyn components (`IsRoslynComponent=true`), qualify XML doc `cref` references to SGF public types with `global::Purview.SourceGeneratorFramework...`. |
| `PSGFR40` | In Roslyn components (`IsRoslynComponent=true`), reference SGF types as inline code (`<c>Type</c>`) instead of a `cref`: copied documentation must not depend on cref resolution. |

## Type-library and attribute-model diagnostics

Expand Down Expand Up @@ -81,7 +81,7 @@ analyzer rules above, including:
- `PreferStructuredCodeWriterIfBlockCodeFixProvider` — rewrites raw `if`/`else if`/`else` block text
to the structured `IfBlock`/`ElseIf`/`Else` APIs (`PSGFR23`).
- `CodeWriterToStringCodeFixProvider` — replaces embedded `CodeWriter` string interpolation (`PSGFR29`).
- `QualifyFrameworkCrefCodeFixProvider` — rewrites SGF XML doc `cref` targets to fully qualified `global::Purview.SourceGeneratorFramework...` names (`PSGFR40`).
- `PreferInlineCodeForFrameworkCrefCodeFixProvider` — rewrites SGF XML doc `cref` targets to inline code (`<c>Type</c>`) (`PSGFR40`).
- `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
Expand Down
47 changes: 43 additions & 4 deletions docs/wiki/Packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,40 @@ substituting the merged path into `TargetPathWithTargetPlatformMoniker` immediat
runs, leaving `GetTargetPath` — which resolves assembly references — pointing at the unmerged bin.
This is what keeps the in-repo test harness working while shipped assemblies stay self-contained.

### Self-contained analyzer validation (PSGFR39)
### Framework type internalization in merged components

ILRepack's `Internalize` is best-effort: a framework type that reaches the merged component's public
API surface stays public, and the types the framework's own generators emit into the component (the
`Purview.SourceGeneratorFramework.Generators` attribute set, generated type libraries, marker
attributes) are not part of the merged framework assembly at all, so ILRepack never sees them. Either
gap leaks framework types out of what must be a self-contained analyzer, and any project that loads
that analyzer alongside the real framework assembly fails with `CS0433` ambiguity for every leaked
type.

The merge tool therefore runs a deterministic internalization pass over the merged output
(`FrameworkTypeInternalizer`) after `ILRepack` finishes:

- every type in a framework-owned namespace (`Purview.SourceGeneratorFramework` and its children,
including the generated `Generators` attribute set) becomes non-public;
- `Microsoft.CodeAnalysis.EmbeddedAttribute` (the framework-emitted marker) becomes non-public;
- **Roslyn component entry points are never internalized** — a generator, analyzer, code fix
provider, or refactoring provider that is reachable from the framework namespace stays public,
because Roslyn only instantiates public components (PSGFR27). This is what keeps the framework's
own bundled analyzers working after the pass;
- the pass reports the merge result: leftover public framework types fail the merge (exit code `5`),
and public component members whose signature exposes a framework type are logged as warnings so the
component author can make them (or their declaring type) non-public.

The component's own generated types (the type library, attribute data models) keep their accessibility
in the component assembly: TLB0015 requires a hand-written partial to be declared
`public static partial` so it can merge with the generated library, and in-repo consumers such as code
fixers and sibling assemblies compile against it. Self-containment is therefore enforced at the merge
boundary rather than by rewriting generated accessibility. See [Type-Library.md](Type-Library.md).

> A merged component is an analyzer artifact and must never be referenced as a compile-time
> dependency. Tests that need to run a *packaged* generator load it out of band — see
> [Testing-TUnit.md](Testing-TUnit.md).


The bundled `SelfContainedGeneratorAnalyzer` (PSGFR39) runs on every project that references the
framework and errors when a **non-packable** Roslyn component explicitly opts out of the default
Expand Down Expand Up @@ -303,9 +336,15 @@ The exact Roslyn baseline is a product-support decision.

The following checks are the acceptance criteria for the self-contained packaging:

1. **No assembly reference** — every shipped Roslyn component DLL
(`analyzers/dotnet/cs/*.dll`) has no assembly reference to `Purview.SourceGeneratorFramework`.
Inspect the metadata directly; do not rely on "the sample compiled".
1. **No assembly reference and no public framework types** — every shipped Roslyn component DLL
(`analyzers/dotnet/cs/*.dll`) has no assembly reference to `Purview.SourceGeneratorFramework` and
exposes **no public type** in a framework-owned namespace (`Purview.SourceGeneratorFramework` and
its children, including the generated `Generators` attribute set, plus the framework-emitted
`Microsoft.CodeAnalysis.EmbeddedAttribute`). The only exception is the component's own Roslyn
component entry points, which must stay public because Roslyn only instantiates public components.
Inspect the metadata directly; do not rely on "the sample compiled". The merge tool fails with exit
code `5` when a merge leaves public framework types behind, and logs a warning naming any public
member that still exposes a framework type.
2. **No loose framework DLL in packages** — no `.nupkg` contains
`Purview.SourceGeneratorFramework.dll` under `analyzers/`, and `*.pdb` files are forbidden in the
`.nupkg` (symbols ship only through the `.snupkg`).
Expand Down
47 changes: 47 additions & 0 deletions docs/wiki/Testing-TUnit.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,53 @@ that supports its API usage; this framework is built against Roslyn 5.0, which s
generator as an analyzer must be Roslyn 5.0 or later (`.NET 10` SDK / Visual Studio 2026). Do not
force a newer `System.Collections.Immutable` version through central package management.

## Running a packaged (merged) generator in tests

A component that ships as a self-contained analyzer must **not** be added as a compile-time
`<Reference>`. Its assembly carries the framework implementation merged into itself, and — because
ILRepack cannot internalize a framework type that reaches the component's public API surface — some
packages expose framework types publicly. Referencing such an assembly from a test project that also
loads the real `Purview.SourceGeneratorFramework.dll` (which the Testing packages do) makes every
framework type ambiguous (`CS0433`). From framework `1.0.0-prerelease.51` the merge tool guarantees a
merged component exposes no framework types apart from its own Roslyn entry points, but a merged
component remains an analyzer artifact and should still be consumed out of band.

To register a packaged generator as an additional generator/analyzer:

1. copy the analyzer DLL from the package beside the test binaries, without referencing it:

```xml
<PackageReference Include="My.Generator.Package" GeneratePathProperty="true" />

<ItemGroup>
<None
Include="$(PkgMy_Generator_Package)\analyzers\dotnet\cs\My.Generator.dll"
Link="My.Generator.dll"
CopyToOutputDirectory="PreserveNewest"
Visible="false" />
</ItemGroup>
```

2. resolve the types out of band and pass them through the options:

```csharp
var assembly = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "My.Generator.dll"));

options.AdditionalGeneratorTypes =
[.. options.AdditionalGeneratorTypes, assembly.GetType("My.Namespace.MyGenerator", throwOnError: true)!];
options.AnalyzerTypes = [assembly.GetType("My.Namespace.MyAnalyzer", throwOnError: true)!];
```

`SourceGeneratorTestRunner` instantiates the supplied types with `Activator.CreateInstance`, so this is
equivalent to `typeof(...)` without the compile-time reference. Two consequences: the loaded
generator's framework copy owns its own logging registry and CodeWriter scope validation (do not
assert on its `LogEntries`, and leave `ValidateCodeWriterScopes` off for that run), and the loaded
assembly must be built against a Roslyn version compatible with the test host.

For a component in the same repository, prefer a project reference to the component project: it
resolves the **unmerged** bin output plus the loose framework DLL, so framework types keep a single
identity and `typeof(...)`, `InternalsVisibleTo`, and every assertion API keep working.

## Which base class and method

| Roslyn type | Base class | Method |
Expand Down
15 changes: 14 additions & 1 deletion docs/wiki/Type-Library.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ generated type library) whose nested `public static partial` classes mirror the
members. Every class — the root and each nested namespace class — exposes a `public const string Namespace`
and the leaf classes expose the members as `public static readonly` fields:

The generated type library is always `public` in the component's own assembly so a hand-written
partial can merge with it (TLB0015 enforces the matching `public static partial` declaration) and so
in-repo consumers such as code fixers and sibling assemblies can compile against it. When the
component is packaged, the merge tool internalizes every framework-owned type in the shipped analyzer
(including the `TypeIdentity`/`TypeReference`/`PurviewTypeLibrary` members this library exposes), so
nothing leaks out of the package; see [Packaging.md](Packaging.md).

Author documentation is copied into the generated library. A `cref` that targets a framework type is
rendered as inline code (`<c>TypeReference</c>`) while it is copied, because the generated file's
namespace and using set differ from the author's source and an unresolvable cref would produce
`CS1574`. `PSGFR40` reports the same pattern in the editor and its code fix applies the same
rewrite.

```csharp
namespace MyGenerator;

Expand All @@ -32,7 +45,7 @@ public static partial class TypeLibrary
```

No `extension(...)` blocks are emitted. The generated types are `public static partial` so you can expand
them with your own methods in a separate partial file — but the extension partial must be declared
them with your own methods in a separate partial file — the extension partial must be declared
`public static partial` in the **same** namespace as the generated type. The generated type is emitted in
the namespace given by the `Namespace` argument, or the **global namespace** when it is omitted, so a
partial declared inside your project namespace will not merge with it (it silently shadows the generated
Expand Down
6 changes: 6 additions & 0 deletions docs/wiki/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Source Generator Framework

Reusable foundations for building, testing, packaging, and optimizing Roslyn source generators.

[Documentation overview](Home.md){ .md-button .md-button--primary }
[Get started](Getting-Started.md){ .md-button }
21 changes: 21 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
site_name: Source Generator Framework
site_description: Developer documentation for the Purview Source Generator Framework
repo_url: https://github.com/purview-dev/sourcegenerator-framework
edit_uri: edit/main/docs/wiki/
docs_dir: docs/wiki

theme:
name: material
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
accent: cyan
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
accent: cyan
features: [navigation.instant, navigation.sections, navigation.top, search.highlight, search.suggest, content.code.copy]

plugins: [search, techdocs-core]
markdown_extensions: [admonition, attr_list, md_in_html, pymdownx.details, pymdownx.superfences]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "purview-sourcegenerator-framework",
"version": "1.0.0-prerelease.50",
"version": "1.0.0-prerelease.51",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
Expand Down
3 changes: 3 additions & 0 deletions purview-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
"purview-logo-light.png",
"README.md"
]
},
"ForbiddenContent": {
"*": ["analyzers/**/Purview.SourceGeneratorFramework.dll"]
}
},
"Release": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ PSGFR36 | Purview.SourceGeneratorFramework | Warning | Extension class is not pl
PSGFR37 | Purview.SourceGeneratorFramework | Warning | Extension class extends multiple receiver types
PSGFR38 | Purview.SourceGeneratorFramework | Warning | Extension class is missing EditorBrowsable
PSGFR39 | Purview.SourceGeneratorFramework | Error | Roslyn component must produce a self-contained analyzer
PSGFR40 | Purview.SourceGeneratorFramework | Warning | Unqualified SGF cref is ambiguous
PSGFR40 | Purview.SourceGeneratorFramework | Warning | SGF cref should be inline code
TLB0014 | TypeLibrary | Warning | Type library partial extension is declared in a different namespace
TLB0015 | TypeLibrary | Info | Type library partial extension must be declared 'public static partial'
TLB0016 | TypeLibrary | Error | Enum value member type must be TypeIdentity or EnumValueDefinition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,32 @@
namespace Purview.SourceGeneratorFramework.Analyzers;

/// <summary>
/// Flags unqualified XML documentation cref references to SGF public types in Roslyn components.
/// Qualifying these cref targets avoids the duplicate-framework ambiguity that can arise when the
/// same SGF type is visible through multiple assembly identities.
/// Flags unqualified XML documentation cref references to SGF public types in Roslyn components, and
/// points at inline code (<c>&lt;c&gt;</c>) instead of a cref.
/// <para>
/// A cref has to resolve in every compilation that contains the documentation. Component documentation
/// is copied into generated code (<c>TypeLibraryGenerator</c> re-emits the spec, member, and enum-value
/// docs) whose namespace and using set differ from the author's source, so a cref to an SGF type can
/// end up unresolvable (CS1574) or resolve through a second assembly identity — the duplicate-framework
/// ambiguity this rule was originally added for. Inline code carries the type as text, which never
/// depends on a symbol, a namespace, or an assembly identity.
/// </para>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AmbiguousFrameworkCrefAnalyzer : DiagnosticAnalyzer
public sealed class UnqualifiedFrameworkCrefAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "PSGFR40";
internal const string QualifiedTypePropertyName = "QualifiedTypeName";

const string FrameworkAssemblyName = "Purview.SourceGeneratorFramework";

static readonly DiagnosticDescriptor Rule = new(
DiagnosticId,
"Qualify SGF cref with global::",
"XML documentation cref '{0}' refers to SGF type '{1}'; qualify the cref with 'global::'",
"Reference SGF types as inline code",
"XML documentation cref '{0}' refers to SGF type '{1}'; use <c>{1}</c> so the documentation stays resolvable when it is copied into generated code",
"Purview.SourceGeneratorFramework",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Detects unqualified XML documentation cref references to Purview.SourceGeneratorFramework public types in Roslyn components so they can be rewritten to a fully qualified global:: name."
description: "Detects XML documentation cref references to Purview.SourceGeneratorFramework public types in Roslyn components so they can be rewritten to inline code, which stays valid wherever the documentation is emitted."
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
Expand Down Expand Up @@ -90,16 +97,8 @@ ImmutableDictionary<string, INamedTypeSymbol> publicFrameworkTypes
if (!ReferencesFrameworkType(semanticModel, cref.Cref, frameworkType, context.CancellationToken))
continue;

var qualifiedTypeName = frameworkType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var diagnostic = Diagnostic.Create(
Rule,
cref.GetLocation(),
ImmutableDictionary<string, string?>.Empty.Add(QualifiedTypePropertyName, qualifiedTypeName),
cref.Cref.ToString(),
qualifiedTypeName.Substring("global::".Length)
);

context.ReportDiagnostic(diagnostic);
var crefText = cref.Cref.ToString();
context.ReportDiagnostic(Diagnostic.Create(Rule, cref.GetLocation(), crefText, crefText));
}
}
}
Expand Down
Loading
Loading