From faca598f660de78bc494ce56f6ba0d44982623d2 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Tue, 22 Sep 2026 21:47:31 +0100 Subject: [PATCH 1/2] feat: using ILMerge to remove risk of version split --- Directory.Packages.props | 2 + docs/wiki/Code-Writer.md | 41 ++++ docs/wiki/Getting-Started.md | 17 +- docs/wiki/Packaging.md | 153 +++++++++++-- package.json | 4 +- purview-build.json | 4 +- src/SourceGeneratorFramework.slnx | 11 +- .../SourceGeneratorFramework.Analyzers.csproj | 21 +- .../TypeLibraryValidationAnalyzer.cs | 1 + ...SourceGeneratorFramework.CodeFixers.csproj | 20 +- .../CompilerServices/IsExternalInit.cs | 15 -- ...GeneratorFramework.ExampleGenerator.csproj | 8 + .../Helpers/TypeLibraryModelLibrary.cs | 2 + ...SourceGeneratorFramework.Generators.csproj | 20 +- .../Program.cs | 125 +++++++++++ .../SourceGeneratorFramework.MergeTool.csproj | 12 + .../CompilerServices/IsExternalInit.cs | 7 +- .../SourceGeneratorFramework/Sdk/README.md | 15 +- .../Purview.SourceGeneratorFramework.targets | 205 ++++++++++++++++-- .../SourceGeneratorFramework.csproj | 19 +- 20 files changed, 587 insertions(+), 115 deletions(-) delete mode 100644 src/src/SourceGeneratorFramework.ExampleGenerator/Extensions/System/Runtime/CompilerServices/IsExternalInit.cs create mode 100644 src/src/SourceGeneratorFramework.MergeTool/Program.cs create mode 100644 src/src/SourceGeneratorFramework.MergeTool/SourceGeneratorFramework.MergeTool.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 1000e80..7223b1a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,5 +20,7 @@ + + diff --git a/docs/wiki/Code-Writer.md b/docs/wiki/Code-Writer.md index 055751e..64d222a 100644 --- a/docs/wiki/Code-Writer.md +++ b/docs/wiki/Code-Writer.md @@ -583,6 +583,47 @@ writer.Property("Name", TypeReference.Create(), TypeDeclarationAccessibi - Keep every value emitted through the structured API so layout stays deterministic and the analyzers can guide callers back to the best practice. +## Generators embedded in another package + +When a generator built with this framework is embedded into a different NuGet package (rather than +shipped as its own package), the outer package must make the framework's compiler-visible properties +available to its consumers, because build assets from `Purview.SourceGeneratorFramework` are not +automatically copied into the outer package. + +Ship a `.props` file with the outer package that declares each property and its +`CompilerVisibleProperty` entry, and pack it under `buildTransitive/` using the outer package's ID so +NuGet imports it for consumers: + +```xml + + false + + + + + Throws when generated source is materialized while CodeWriter scopes remain undisposed. + + +``` + +```xml + +``` + +The framework's public compiler-visible properties are +`PurviewSourceGeneratorFrameworkValidateCodeWriterScopes`, +`PurviewSourceGeneratorFrameworkEnableLogging`, +`PurviewSourceGeneratorFrameworkLoggingSessionId`, and +`PurviewSourceGeneratorFrameworkLanguageVersion`. See [Packaging.md](Packaging.md) for the full +self-contained generator packaging guidance. + ## Samples The [`SourceGeneratorFramework.ExampleGenerator`](../../src/src/SourceGeneratorFramework.ExampleGenerator) diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index d4c89b2..4588b1f 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -38,10 +38,14 @@ analyzer inputs. Use an analyzer project reference: /> ``` -The Purview SDK automatically invokes `GetSourceGeneratorAnalyzerFiles`, which returns both the -generator and its framework dependency without adding either file to the consuming application's -runtime references. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly remains -supported but is not required. +The Purview SDK automatically invokes `GetSourceGeneratorAnalyzerFiles`, which returns the generator +assembly together with its framework dependency (the loose `Purview.SourceGeneratorFramework.dll`) +without adding either file to the consuming application's runtime references. The framework keeps the +generator's own output unmerged so its in-process test harness retains shared framework type identity. +Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly remains supported but is not +required. Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `true` to opt into a merged, +self-contained generator from `GetSourceGeneratorAnalyzerFiles` instead (see +[Packaging.md](Packaging.md)). ### Referencing a generator from its test project @@ -75,7 +79,8 @@ Add two project references with deliberately different metadata: Do not put `OutputItemType="Analyzer"` on the normal reference. The Purview SDK automatically uses `GetSourceGeneratorAnalyzerFiles` for the analyzer reference and supplies the generator's -runtime dependencies to Roslyn. +runtime dependencies (the loose `Purview.SourceGeneratorFramework.dll`) to Roslyn so the generator +loads correctly. Because the second reference is a normal assembly reference, the generator's Roslyn dependencies also become visible to the test compilation. For a multi-target test project, build the generator @@ -180,4 +185,4 @@ dotnet add package Purview.SourceGeneratorFramework.Testing.TUnit - [Testing](Testing.md) - [Testing with TUnit](Testing-TUnit.md) - [Step-Cache Tests](Step-Cache-Tests.md) -- [Packaging](Packaging.md) \ No newline at end of file +- [Packaging](Packaging.md) diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md index 466c22b..76e1782 100644 --- a/docs/wiki/Packaging.md +++ b/docs/wiki/Packaging.md @@ -6,13 +6,18 @@ This page covers how to package a source generator that references ## How the framework packages are assembled `Purview.SourceGeneratorFramework` is dual-role: the built framework assembly ships in `lib/` so -consumers can compile generators against it, and the `analyzers/` folder carries the generator + -analyzer assemblies and their runtime dependencies. The bundled projects are: +generator projects can compile against it, and the `analyzers/` folder carries self-contained +generator, analyzer, and code-fixer assemblies. The bundled projects are: - `SourceGeneratorFramework.Generators` — `AttributeDataModelGenerator`, `TypeLibraryGenerator`; - `SourceGeneratorFramework.Analyzers` — the `PSGFR*` and `TLB*` analyzers; - `SourceGeneratorFramework.CodeFixers` — the code fix providers. +Each Roslyn component has the framework implementation merged and internalized into its own assembly. +The package deliberately does **not** put `Purview.SourceGeneratorFramework.dll` under +`analyzers/dotnet/cs`. Consequently, generators built against different framework versions do not +ask Roslyn to load competing versions of a same-named runtime dependency. + The shared models and helpers that used to ship as a separate `Purview.SourceGeneratorFramework.Shared.dll` are compiled directly into the framework assembly (`SourceGeneratorFramework` links the `SourceGeneratorShared` sources via @@ -25,23 +30,40 @@ These projects are `IsRoslynComponent = true` and are **not** packable on their into the main package by the `SourceGeneratorFramework` project. They were previously consumed as analyzer project references, but since they now reference the framework assembly for the shared types (which would form a project-reference cycle), the `SourceGeneratorFramework` project builds them via -`GetSourceGeneratorAnalyzerFiles` and packs them under `analyzers/dotnet/cs/` in +`GetPurviewMergedAnalyzerFile` and packs them under `analyzers/dotnet/cs/` in `BuildAndPackBundledAnalyzerAssemblies`. The repo's pack validation (`purview-build.json`) requires the `purview.sourcegeneratorframework` package to contain, at minimum: - `lib/netstandard2.0/Purview.SourceGeneratorFramework.dll`; -- `analyzers/dotnet/cs/` versions of the framework, generators, analyzers, and code fixers; +- `analyzers/dotnet/cs/` versions of the self-contained generators, analyzers, and code fixers; - `build/Purview.SourceGeneratorFramework.props` and `build/Purview.SourceGeneratorFramework.targets`; +- `tools/net10.0/` versions of the framework-owned merge tool and its runtime files; - `README.md`, `LICENSE.md`, and `purview-logo-light.png`. PDBs are delivered only through the `.snupkg`; `*.pdb` files are forbidden inside the `.nupkg`. ## Referencing a generator from a consuming project -Use an analyzer project reference so Roslyn receives both the generator assembly and its framework -runtime dependency: +Reference the framework privately from a Roslyn component. When the generator project itself is +packed, the framework's build target replaces its output with a self-contained assembly at pack time: + +```xml + + true + + + + + +``` + +Use an analyzer project reference from a consuming project so Roslyn receives the generator +assembly: ```xml ``` -The Purview SDK automatically invokes `GetSourceGeneratorAnalyzerFiles`, which returns both the -generator and its framework dependency without adding either file to the consuming application's -runtime references. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly remains -supported but is not required. +The Purview SDK automatically invokes `GetSourceGeneratorAnalyzerFiles`, which returns the generator +assembly without adding it to the consuming application's runtime references. The framework returns +the generator unmerged together with the loose `Purview.SourceGeneratorFramework.dll` runtime +dependency so the compiler can load both, and so the generator's own in-process test harness keeps +its shared framework type identity. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly +remains supported but is not required. + +Set `PurviewEmbedSourceGeneratorFramework` to `false` only for a project that produces the framework +compile-time library itself. Published generator packages must not disable embedding. + +Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `true` to opt into a self-contained +**merged** generator from `GetSourceGeneratorAnalyzerFiles` instead of the unmerged assembly + loose +framework DLL. This also keeps GASF-based packages self-contained. Leave it unset (`false`) when the +generator's tests reference the generator assembly directly and rely on `InternalsVisibleTo` grants +or shared framework type identity. + +### Analyzer consumption contract + +A component that references the framework is consumed in three distinct ways, each producing a +different shape: + +| Path | Trigger | Output | +| --- | --- | --- | +| `GetSourceGeneratorAnalyzerFiles` (default) | A consuming project references the component as an analyzer | Unmerged component + the loose `Purview.SourceGeneratorFramework.dll` copied from the framework package `lib/`. Keeps the component's bin unmerged, so its in-process test harness retains shared framework type identity and `InternalsVisibleTo` access. | +| `GetSourceGeneratorAnalyzerFiles` (opt-in) | Same, with `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=true` | A **merged**, self-contained component. No loose framework DLL is needed. | +| `GetPurviewMergedAnalyzerFile` | The framework package's own bundled-component pack, or a third-party package embedding the generator | The **merged** component from the intermediate output; the component's bin is never overwritten. | +| `GenerateNuspec` (`EmbedPurviewSourceGeneratorFrameworkForPack`) | Packing a standalone, packable generator project | The generator's bin is replaced by the **merged** self-contained DLL and the loose framework DLL is deleted before the package is written. | + +In every path the loose framework DLL is declared as a `SourceGeneratorRuntimeDependency` (statically +from the framework package `lib/` for package consumers, with a target-time fallback for in-repo +`ProjectReference` components) so the SDK copies it beside the generator before Roslyn loads it. + +The merge itself (`_PurviewMergeSourceGeneratorFramework`) only writes to the component's +intermediate `purview-merged/` directory; the pack/analyzer targets copy that result where it is +needed. This is what keeps the in-repo test harness working while shipped assemblies stay +self-contained. + +### `IsExternalInit` contract + +The framework assembly defines `System.Runtime.CompilerServices.IsExternalInit` **publicly** so the +framework's own bundled generators can emit `init`-based attribute types into any consumer +compilation, and so the merge step has a single marker definition to internalize. Consumers +(generator projects) must **not** declare their own `IsExternalInit`: doing so produces a duplicate +type definition against the framework reference. ### Generators embedded in another package -If the generator assembly is embedded in a different NuGet package, the outer package must make the -framework's compiler-visible properties visible to its consumers. Build assets from -`Purview.SourceGeneratorFramework` are not automatically copied into the outer package. Include a -`.props` file imported by the outer package that declares the property and its +If the generator assembly is embedded in a different NuGet package, pack the generator's **merged, +self-contained** assembly so the outer package does not ship a loose `Purview.SourceGeneratorFramework.dll`. +Call the framework's `GetPurviewMergedAnalyzerFile` target on the generator project (which merges the +framework implementation into the generator's intermediate output without touching its bin) and add +the returned file under `analyzers/dotnet/cs`, disabling the SDK's default GASF-based analyzer packing: + +```xml + + false + $(TargetsForTfmSpecificContentInPackage);PackMyGenerator + + + + + + + + + analyzers/dotnet/cs/ + + + +``` + +If the generator assembly is also embedded at compile time for the outer package's consumers, the +outer package must make the framework's compiler-visible properties visible to those consumers. Build +assets from `Purview.SourceGeneratorFramework` are not automatically copied into the outer package. +Include a `.props` file imported by the outer package that declares the property and its `CompilerVisibleProperty` entry (see [Code-Writer.md](Code-Writer.md#generators-embedded-in-another-package)), and pack it using the outer package's ID so NuGet imports it automatically: @@ -135,6 +226,11 @@ A broadly-compatible generator project might start with: Version="$(RoslynAnalyserVersion)" PrivateAssets="all" /> + + @@ -150,6 +246,10 @@ A broadly-compatible generator project might start with: ``` +The resulting generator package contains the generator DLL under `analyzers/dotnet/cs`; it does not +contain a loose `Purview.SourceGeneratorFramework.dll`. Package validation should inspect both the +ZIP entries and the generator's assembly references to enforce that invariant. + Then centrally define: ```xml @@ -161,6 +261,29 @@ Then centrally define: The exact Roslyn baseline is a product-support decision. +## Release gates + +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". +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`). +3. **Merged entry points survive** — each merged DLL still exposes its `IIncrementalGenerator`, + `DiagnosticAnalyzer`, or `CodeFixProvider` implementations. +4. **Installed-package consumer test** — a generator project that references the framework package + by `PackageReference` (with the framework's `build/` targets auto-imported) packs a single + self-contained DLL under `analyzers/dotnet/cs`, and a consumer that installs that package builds + with the generator producing output. +5. **Two-version coexistence test** — build Generator A against the current framework and + Generator B against an intentionally binary-incompatible framework version (for example a v2 + that adds a `CodeWriter` member Generator B calls). Install both packages into one consumer and + build with both package-reference orders. Both generators must run, each against its own embedded + framework copy. Under the old shared-DLL model one generator fails with `MissingMethodException` + (load-order dependent); with self-contained packaging both succeed. + ## License -This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project. \ No newline at end of file +This documentation is part of the MIT-licensed `Purview.SourceGeneratorFramework` project. diff --git a/package.json b/package.json index 17c38cd..e25a004 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "purview-sourcegenerator-framework", - "version": "1.0.0-prerelease.47", + "version": "1.0.0-prerelease.48", "license": "MIT", "author": { "name": "Kieron Lanning", @@ -14,4 +14,4 @@ "type": "git", "url": "git+https://github.com/purview-dev/sourcegenerator-framework.git" } -} +} \ No newline at end of file diff --git a/purview-build.json b/purview-build.json index 374e2fd..b9765e0 100644 --- a/purview-build.json +++ b/purview-build.json @@ -11,12 +11,12 @@ "RequiredContent": { "purview.sourcegeneratorframework": [ "lib/netstandard2.0/Purview.SourceGeneratorFramework.dll", - "analyzers/dotnet/cs/Purview.SourceGeneratorFramework.dll", "analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Generators.dll", "analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Analyzers.dll", "analyzers/dotnet/cs/Purview.SourceGeneratorFramework.CodeFixers.dll", "build/Purview.SourceGeneratorFramework.props", "build/Purview.SourceGeneratorFramework.targets", + "tools/net10.0/Purview.SourceGeneratorFramework.MergeTool.dll", "README.md", "LICENSE.md", "purview-logo-light.png" @@ -45,4 +45,4 @@ "Release": { "Mode": "None" } -} \ No newline at end of file +} diff --git a/src/SourceGeneratorFramework.slnx b/src/SourceGeneratorFramework.slnx index 5a96ecf..189a73a 100644 --- a/src/SourceGeneratorFramework.slnx +++ b/src/SourceGeneratorFramework.slnx @@ -14,16 +14,21 @@ Path="src/SourceGeneratorFramework.CodeFixers/SourceGeneratorFramework.CodeFixers.csproj" Id="0911f5c7-139e-4ddc-9918-d4826fe0c20c" /> - - - + + + + + + + + diff --git a/src/src/SourceGeneratorFramework.Analyzers/SourceGeneratorFramework.Analyzers.csproj b/src/src/SourceGeneratorFramework.Analyzers/SourceGeneratorFramework.Analyzers.csproj index 02a0e3e..940b238 100644 --- a/src/src/SourceGeneratorFramework.Analyzers/SourceGeneratorFramework.Analyzers.csproj +++ b/src/src/SourceGeneratorFramework.Analyzers/SourceGeneratorFramework.Analyzers.csproj @@ -1,6 +1,7 @@  true + true - + + + <_PurviewSourceGeneratorFrameworkAssemblyFullPath>$([System.IO.Path]::GetFullPath('$(_PurviewSourceGeneratorFrameworkAssembly)')) + <_PurviewResolvedFrameworkAssembly Condition="Exists('$(_PurviewSourceGeneratorFrameworkAssemblyFullPath)')" + >$(_PurviewSourceGeneratorFrameworkAssemblyFullPath) + <_PurviewResolvedFrameworkAssembly + Condition="'$(_PurviewResolvedFrameworkAssembly)' == '' AND Exists('$(TargetDir)Purview.SourceGeneratorFramework.dll')" + >$(TargetDir)Purview.SourceGeneratorFramework.dll + + + <_PurviewSourceGeneratorFrameworkReference + Include="@(ReferenceCopyLocalPaths)" + Condition="'%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)' == 'Purview.SourceGeneratorFramework.dll'" + /> + + + <_PurviewResolvedFrameworkAssembly>@(_PurviewSourceGeneratorFrameworkReference->'%(FullPath)') + + + + + Purview.SourceGeneratorFramework.dll + + + + + + Purview.SourceGeneratorFramework.dll + + + - + - + + + + + + <_PurviewMergedComponentDirectory>$(IntermediateOutputPath)purview-merged\ + <_PurviewMergedComponent>$(_PurviewMergedComponentDirectory)$(TargetFileName) + <_PurviewMergedComponentPdb>$(_PurviewMergedComponentDirectory)$(TargetName).pdb + + + + + + + + + + + + + + + + + <_PurviewMergedComponentFullPath>$([System.IO.Path]::GetFullPath('$(_PurviewMergedComponent)')) + + + <_PurviewMergedAnalyzerFileOutput + Include="$(_PurviewMergedComponentFullPath)" + Condition="Exists('$(_PurviewMergedComponentFullPath)')" + /> + + + + + + + + + diff --git a/src/src/SourceGeneratorFramework/SourceGeneratorFramework.csproj b/src/src/SourceGeneratorFramework/SourceGeneratorFramework.csproj index 9bbbab6..aadca28 100644 --- a/src/src/SourceGeneratorFramework/SourceGeneratorFramework.csproj +++ b/src/src/SourceGeneratorFramework/SourceGeneratorFramework.csproj @@ -1,12 +1,12 @@  true - true + false + netstandard2.0 true @@ -15,6 +15,7 @@ false + false $(TargetsForTfmSpecificContentInPackage);BuildAndPackBundledAnalyzerAssemblies @@ -48,9 +49,9 @@ > @@ -61,6 +62,10 @@ > analyzers/dotnet/cs/ + <_PurviewMergeToolPackageFile Include="..\SourceGeneratorFramework.MergeTool\bin\$(Configuration)\net10.0\*.dll;..\SourceGeneratorFramework.MergeTool\bin\$(Configuration)\net10.0\*.json"> + tools/net10.0/%(_PurviewMergeToolPackageFile.Filename)%(_PurviewMergeToolPackageFile.Extension) + + From 85ab6ad05b18ef010a608da1ed060836ab5a7b2b Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Tue, 22 Sep 2026 23:38:21 +0100 Subject: [PATCH 2/2] feat: self-build + self-contained issues --- docs/wiki/Analyzers.md | 1 + docs/wiki/Getting-Started.md | 22 +-- docs/wiki/Packaging.md | 64 +++++--- .../AnalyzerReleases.Unshipped.md | 1 + .../SelfContainedGeneratorAnalyzer.cs | 100 ++++++++++++ .../SourceGeneratorFramework/Sdk/README.md | 22 +-- .../Purview.SourceGeneratorFramework.props | 15 ++ .../Purview.SourceGeneratorFramework.targets | 30 ++-- .../AnalyzerTestHelpers.cs | 87 ++++++++-- .../SelfContainedGeneratorAnalyzerTests.cs | 150 ++++++++++++++++++ 10 files changed, 432 insertions(+), 60 deletions(-) create mode 100644 src/src/SourceGeneratorFramework.Analyzers/SelfContainedGeneratorAnalyzer.cs create mode 100644 src/tests/SourceGeneratorFramework.Analyzers.UnitTests/SelfContainedGeneratorAnalyzerTests.cs diff --git a/docs/wiki/Analyzers.md b/docs/wiki/Analyzers.md index d90b37d..a27b86d 100644 --- a/docs/wiki/Analyzers.md +++ b/docs/wiki/Analyzers.md @@ -50,6 +50,7 @@ The analyzers enforce two families of rules: | `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)]`. | +| `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/`. | ## Type-library and attribute-model diagnostics diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index 4588b1f..b483b9e 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -39,13 +39,13 @@ analyzer inputs. Use an analyzer project reference: ``` The Purview SDK automatically invokes `GetSourceGeneratorAnalyzerFiles`, which returns the generator -assembly together with its framework dependency (the loose `Purview.SourceGeneratorFramework.dll`) -without adding either file to the consuming application's runtime references. The framework keeps the -generator's own output unmerged so its in-process test harness retains shared framework type identity. -Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly remains supported but is not -required. Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `true` to opt into a merged, -self-contained generator from `GetSourceGeneratorAnalyzerFiles` instead (see -[Packaging.md](Packaging.md)). +assembly without adding it to the consuming application's runtime references. By default the framework +returns a **merged, self-contained** generator from its intermediate output, so no loose +`Purview.SourceGeneratorFramework.dll` is needed and GASF-based packages stay self-contained. The +generator's own bin output stays unmerged, so its in-process test harness retains shared framework +type identity. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly remains supported but +is not required. Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `false` only when the +unmerged assembly + loose framework DLL shape is required (see [Packaging.md](Packaging.md)). ### Referencing a generator from its test project @@ -78,9 +78,11 @@ Add two project references with deliberately different metadata: ``` Do not put `OutputItemType="Analyzer"` on the normal reference. The Purview SDK automatically -uses `GetSourceGeneratorAnalyzerFiles` for the analyzer reference and supplies the generator's -runtime dependencies (the loose `Purview.SourceGeneratorFramework.dll`) to Roslyn so the generator -loads correctly. +uses `GetSourceGeneratorAnalyzerFiles` for the analyzer reference, which by default returns the +generator's **merged, self-contained** assembly from its intermediate output, so the generator loads +correctly with no extra runtime dependencies. The normal assembly reference resolves to the +generator's unmerged bin output, so the two roles do not interfere and the test compilation sees no +duplicate framework types. Because the second reference is a normal assembly reference, the generator's Roslyn dependencies also become visible to the test compilation. For a multi-target test project, build the generator diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md index 76e1782..61e43ed 100644 --- a/docs/wiki/Packaging.md +++ b/docs/wiki/Packaging.md @@ -75,20 +75,22 @@ assembly: ``` The Purview SDK automatically invokes `GetSourceGeneratorAnalyzerFiles`, which returns the generator -assembly without adding it to the consuming application's runtime references. The framework returns -the generator unmerged together with the loose `Purview.SourceGeneratorFramework.dll` runtime -dependency so the compiler can load both, and so the generator's own in-process test harness keeps -its shared framework type identity. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly -remains supported but is not required. +assembly without adding it to the consuming application's runtime references. By default +(`PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=true`) the framework returns the **merged, +self-contained** generator from the intermediate `purview-merged/` directory, so consuming projects +and any GASF-based package compile against a generator that carries its own framework implementation +and never needs the loose `Purview.SourceGeneratorFramework.dll`. The generator's bin output is left +unmerged, so a project that references the generator assembly directly (an in-process test harness) +keeps its shared framework type identity, `InternalsVisibleTo` access, and avoids `CS0433` +collisions with the framework library. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` +explicitly remains supported but is not required. Set `PurviewEmbedSourceGeneratorFramework` to `false` only for a project that produces the framework compile-time library itself. Published generator packages must not disable embedding. -Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `true` to opt into a self-contained -**merged** generator from `GetSourceGeneratorAnalyzerFiles` instead of the unmerged assembly + loose -framework DLL. This also keeps GASF-based packages self-contained. Leave it unset (`false`) when the -generator's tests reference the generator assembly directly and rely on `InternalsVisibleTo` grants -or shared framework type identity. +Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `false` only when the generator's +analyzer-files consumers must keep the unmerged assembly + loose framework DLL shape (for example a +generator shipped into a single compiler process alongside an incompatible framework version). ### Analyzer consumption contract @@ -97,19 +99,45 @@ different shape: | Path | Trigger | Output | | --- | --- | --- | -| `GetSourceGeneratorAnalyzerFiles` (default) | A consuming project references the component as an analyzer | Unmerged component + the loose `Purview.SourceGeneratorFramework.dll` copied from the framework package `lib/`. Keeps the component's bin unmerged, so its in-process test harness retains shared framework type identity and `InternalsVisibleTo` access. | -| `GetSourceGeneratorAnalyzerFiles` (opt-in) | Same, with `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=true` | A **merged**, self-contained component. No loose framework DLL is needed. | +| `GetSourceGeneratorAnalyzerFiles` (default) | A consuming project references the component as an analyzer | The **merged**, self-contained component, returned from the intermediate `purview-merged/` directory. The component's bin output stays unmerged, so its in-process test harness retains shared framework type identity and `InternalsVisibleTo` access without `CS0433` collisions. | +| `GetSourceGeneratorAnalyzerFiles` (opt-out) | Same, with `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=false` | Unmerged component + the loose `Purview.SourceGeneratorFramework.dll` copied from the framework package `lib/`. | | `GetPurviewMergedAnalyzerFile` | The framework package's own bundled-component pack, or a third-party package embedding the generator | The **merged** component from the intermediate output; the component's bin is never overwritten. | | `GenerateNuspec` (`EmbedPurviewSourceGeneratorFrameworkForPack`) | Packing a standalone, packable generator project | The generator's bin is replaced by the **merged** self-contained DLL and the loose framework DLL is deleted before the package is written. | -In every path the loose framework DLL is declared as a `SourceGeneratorRuntimeDependency` (statically -from the framework package `lib/` for package consumers, with a target-time fallback for in-repo -`ProjectReference` components) so the SDK copies it beside the generator before Roslyn loads it. +In the opt-out path the loose framework DLL is declared as a `SourceGeneratorRuntimeDependency` +(statically from the framework package `lib/` for package consumers, with a target-time fallback for +in-repo `ProjectReference` components) so the SDK copies it beside the generator before Roslyn loads +it. The merged paths never declare it. The merge itself (`_PurviewMergeSourceGeneratorFramework`) only writes to the component's -intermediate `purview-merged/` directory; the pack/analyzer targets copy that result where it is -needed. This is what keeps the in-repo test harness working while shipped assemblies stay -self-contained. +intermediate `purview-merged/` directory. `GetSourceGeneratorAnalyzerFiles` returns that result by +substituting the merged path into `TargetPathWithTargetPlatformMoniker` immediately before its body +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) + +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 +self-contained analyzer output. Such a component, if embedded into a package through the GASF-based +pack, forces the loose `Purview.SourceGeneratorFramework.dll` under `analyzers/`, reintroducing the +shared-version hazard. + +The analyzer reads the following compiler-visible properties: +`IsRoslynComponent`, `IsPackable`, `PurviewEmbedSourceGeneratorFramework`, +`PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles`, and +`PurviewSourceGeneratorFrameworkAnalyzerValidation`. It does not report when the component: + +- is not a Roslyn component, or does not reference the framework; +- disables embedding (`PurviewEmbedSourceGeneratorFramework=false`); +- is packable (`IsPackable=true`) — its own `GenerateNuspec` merge makes the package self-contained; +- keeps the default merged GASF output (`PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=true`); +- explicitly opts out (`PurviewSourceGeneratorFrameworkAnalyzerValidation=false`). + +Set `PurviewSourceGeneratorFrameworkAnalyzerValidation=false` on a component that is shipped +self-contained via `GetPurviewMergedAnalyzerFile`, or that is only consumed in-repo and never packed. +Packaging an embedded generator through the raw GASF path without one of the self-contained +arrangements is an error. ### `IsExternalInit` contract diff --git a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md index 8f89971..9ce7aaf 100644 --- a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md @@ -14,6 +14,7 @@ PSGFR35 | Purview.SourceGeneratorFramework | Warning | Extension class name does PSGFR36 | Purview.SourceGeneratorFramework | Warning | Extension class is not placed in the extended type's namespace/folder 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 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 diff --git a/src/src/SourceGeneratorFramework.Analyzers/SelfContainedGeneratorAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/SelfContainedGeneratorAnalyzer.cs new file mode 100644 index 0000000..5ebc218 --- /dev/null +++ b/src/src/SourceGeneratorFramework.Analyzers/SelfContainedGeneratorAnalyzer.cs @@ -0,0 +1,100 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +/// +/// Errors when a Roslyn component (source generator, diagnostic analyzer, or code fix provider) +/// references Purview.SourceGeneratorFramework but is not configured to produce a +/// self-contained analyzer output. Packing such a component under analyzers/dotnet/cs forces +/// the loose Purview.SourceGeneratorFramework.dll into the package, reintroducing the +/// shared-version hazard where two generators with different framework versions compete in the +/// compiler process. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class SelfContainedGeneratorAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "PSGFR39"; + + internal const string FrameworkAssemblyName = "Purview.SourceGeneratorFramework"; + internal const string EmbedProperty = "PurviewEmbedSourceGeneratorFramework"; + internal const string MergeAnalyzerFilesProperty = "PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles"; + internal const string AnalyzerValidationProperty = "PurviewSourceGeneratorFrameworkAnalyzerValidation"; + + static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + "Roslyn component must produce a self-contained analyzer", + "This Roslyn component references {0} but is not configured to produce a self-contained analyzer. If it is packed (directly or embedded in another package) the package will ship the loose {0}.dll under analyzers/, reintroducing the shared-version hazard. Set {1}=true, or pack it self-contained via GetPurviewMergedAnalyzerFile (see Packaging.md). If this component is only used in-repo or is shipped via GetPurviewMergedAnalyzerFile, set {2}=false.", + "Purview.SourceGeneratorFramework", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Detects Roslyn components that reference Purview.SourceGeneratorFramework without producing a self-contained analyzer output, which would force the loose framework DLL to be shipped under analyzers/ in any package that embeds them.", + customTags: ["CompilationEnd"] + ); + + public override ImmutableArray SupportedDiagnostics => [Rule]; + + public override void Initialize(AnalysisContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationAction(context => + { + var options = context.Options.AnalyzerConfigOptionsProvider; + var tree = context.Compilation.SyntaxTrees.FirstOrDefault(); + var analyzerOptions = tree is not null ? options.GetOptions(tree) : options.GlobalOptions; + + if (!IsExplicitlyTrue(analyzerOptions, "IsRoslynComponent")) + return; + + if (!ReferencesSourceGeneratorFramework(context.Compilation)) + return; + + // Merge disabled: only the framework compile-time library opts out of embedding. + if (IsExplicitlyFalse(analyzerOptions, EmbedProperty)) + return; + + // A packable generator merges at GenerateNuspec and deletes the loose DLL itself. + if (IsExplicitlyTrue(analyzerOptions, "IsPackable")) + return; + + // GetSourceGeneratorAnalyzerFiles returns a merged, self-contained component. + if (IsExplicitlyTrue(analyzerOptions, MergeAnalyzerFilesProperty)) + return; + + // Explicit acknowledgement that the component is never packed, or is shipped + // self-contained via GetPurviewMergedAnalyzerFile. + if (IsExplicitlyFalse(analyzerOptions, AnalyzerValidationProperty)) + return; + + context.ReportDiagnostic( + Diagnostic.Create( + Rule, + tree?.GetRoot().GetLocation() ?? Location.None, + FrameworkAssemblyName, + MergeAnalyzerFilesProperty, + AnalyzerValidationProperty + ) + ); + }); + } + + static bool ReferencesSourceGeneratorFramework(Compilation compilation) => + compilation + .References.Select(compilation.GetAssemblyOrModuleSymbol) + .OfType() + .Any(static assembly => + string.Equals(assembly.Identity.Name, FrameworkAssemblyName, StringComparison.OrdinalIgnoreCase) + ); + + static bool IsExplicitlyTrue(AnalyzerConfigOptions options, string propertyName) => + options.TryGetValue("build_property." + propertyName, out var value) + && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + + static bool IsExplicitlyFalse(AnalyzerConfigOptions options, string propertyName) => + options.TryGetValue("build_property." + propertyName, out var value) + && string.Equals(value, "false", StringComparison.OrdinalIgnoreCase); +} diff --git a/src/src/SourceGeneratorFramework/Sdk/README.md b/src/src/SourceGeneratorFramework/Sdk/README.md index 4224e30..f29faa3 100644 --- a/src/src/SourceGeneratorFramework/Sdk/README.md +++ b/src/src/SourceGeneratorFramework/Sdk/README.md @@ -17,13 +17,13 @@ analyzer inputs. Use an analyzer project reference: ``` The Purview SDK automatically invokes `GetSourceGeneratorAnalyzerFiles`, which returns the generator -assembly together with its framework dependency (the loose `Purview.SourceGeneratorFramework.dll`) -without adding either file to the consuming application's runtime references. The framework keeps the -generator's own output unmerged so its in-process test harness retains shared framework type identity. -Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly remains supported but is not -required. Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `true` to opt into a merged, -self-contained generator from `GetSourceGeneratorAnalyzerFiles` instead (see -[Packaging.md](Packaging.md)). +assembly without adding it to the consuming application's runtime references. By default the framework +returns a **merged, self-contained** generator from its intermediate output, so no loose +`Purview.SourceGeneratorFramework.dll` is needed and GASF-based packages stay self-contained. The +generator's own bin output stays unmerged, so its in-process test harness retains shared framework +type identity. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly remains supported but +is not required. Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `false` only when the +unmerged assembly + loose framework DLL shape is required (see [Packaging.md](Packaging.md)). ### Referencing a generator from its test project @@ -56,9 +56,11 @@ Add two project references with deliberately different metadata: ``` Do not put `OutputItemType="Analyzer"` on the normal reference. The Purview SDK automatically -uses `GetSourceGeneratorAnalyzerFiles` for the analyzer reference and supplies the generator's -runtime dependencies (the loose `Purview.SourceGeneratorFramework.dll`) to Roslyn so the generator -loads correctly. +uses `GetSourceGeneratorAnalyzerFiles` for the analyzer reference, which by default returns the +generator's **merged, self-contained** assembly from its intermediate output, so the generator loads +correctly with no extra runtime dependencies. The normal assembly reference resolves to the +generator's unmerged bin output, so the two roles do not interfere and the test compilation sees no +duplicate framework types. Because the second reference is a normal assembly reference, the generator's Roslyn dependencies also become visible to the test compilation. For a multi-target test project, build the generator diff --git a/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.props b/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.props index 388cc0d..15a5fa8 100644 --- a/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.props +++ b/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.props @@ -21,6 +21,21 @@ Carries the consuming project's LangVersion so generators can gate emitted features. + + True when the project is a Roslyn component (source generator, analyzer, or code fix). + + + True when the project produces its own NuGet package. + + + True when the framework implementation is merged into the Roslyn component. + + + True when GetSourceGeneratorAnalyzerFiles returns a merged, self-contained component. + + + False to opt out of PSGFR39 self-contained analyzer validation. + diff --git a/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.targets b/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.targets index 467fe05..e49f5f6 100644 --- a/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.targets +++ b/src/src/SourceGeneratorFramework/Sdk/build/Purview.SourceGeneratorFramework.targets @@ -9,7 +9,7 @@ >true false + >true true @@ -129,28 +129,34 @@ - - + + + +