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/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/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..b483b9e 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 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
@@ -74,8 +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 to Roslyn.
+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
@@ -180,4 +187,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..61e43ed 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. 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 `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
+
+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 | 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 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. `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
+
+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 +254,11 @@ A broadly-compatible generator project might start with:
Version="$(RoslynAnalyserVersion)"
PrivateAssets="all" />
+
+
@@ -150,6 +274,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 +289,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/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.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)
+
+
diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AnalyzerTestHelpers.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AnalyzerTestHelpers.cs
index 11750ce..86f0917 100644
--- a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AnalyzerTestHelpers.cs
+++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/AnalyzerTestHelpers.cs
@@ -1,3 +1,4 @@
+using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
@@ -10,24 +11,90 @@ public static async Task> GetAnalyzerDiagnosticsAsync(
this DiagnosticAnalyzer analyzer,
string source,
CancellationToken cancellationToken = default
+ ) => await GetAnalyzerDiagnosticsAsync(analyzer, source, null, false, cancellationToken);
+
+ ///
+ /// Runs the analyzer against the supplied source, optionally exposing build_property.*
+ /// values through the analyzer config options and adding a reference to
+ /// Purview.SourceGeneratorFramework.dll.
+ ///
+ public static async Task> GetAnalyzerDiagnosticsAsync(
+ this DiagnosticAnalyzer analyzer,
+ string source,
+ IReadOnlyDictionary? buildProperties,
+ bool referenceSourceGeneratorFramework,
+ CancellationToken cancellationToken = default
)
{
- var syntaxTree = CSharpSyntaxTree.ParseText(source, cancellationToken: cancellationToken);
- var references = new[]
+ var compilation = CreateTestCompilation(source, referenceSourceGeneratorFramework);
+
+ var analyzerOptions = buildProperties is null
+ ? null
+ : TestAnalyzerConfigOptions.CreateAnalyzerOptions(buildProperties);
+
+ var compilationWithAnalyzers = analyzerOptions is null
+ ? compilation.WithAnalyzers([analyzer])
+ : compilation.WithAnalyzers([analyzer], analyzerOptions);
+
+ return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(cancellationToken);
+ }
+
+ static CSharpCompilation CreateTestCompilation(string source, bool referenceSourceGeneratorFramework)
+ {
+ var syntaxTree = CSharpSyntaxTree.ParseText(source);
+ List references = new()
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location),
MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location),
};
- var compilation = CSharpCompilation.Create(
- "TestAssembly",
- [syntaxTree],
- references,
- new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
- );
+ if (referenceSourceGeneratorFramework)
+ {
+ references.Add(MetadataReference.CreateFromFile(typeof(CodeWriter).Assembly.Location));
+ }
- var compilationWithAnalyzers = compilation.WithAnalyzers([analyzer]);
+ CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary);
- return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(cancellationToken);
+ return CSharpCompilation.Create("TestAssembly", [syntaxTree], references, compilationOptions);
+ }
+}
+
+///
+/// Minimal that exposes a fixed set of
+/// build_property.* values to analyzers in an in-memory test compilation.
+///
+static class TestAnalyzerConfigOptions
+{
+ public static AnalyzerOptions CreateAnalyzerOptions(IReadOnlyDictionary buildProperties) =>
+ new(ImmutableArray.Empty, CreateProvider(buildProperties));
+
+ public static AnalyzerConfigOptionsProvider CreateProvider(IReadOnlyDictionary buildProperties) =>
+ new Provider(new Options(ImmutableDictionary.CreateRange(StringComparer.Ordinal, buildProperties)));
+
+ sealed class Provider(AnalyzerConfigOptions options) : AnalyzerConfigOptionsProvider
+ {
+ public override AnalyzerConfigOptions GlobalOptions => options;
+
+ public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => options;
+
+ public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => options;
+ }
+
+ sealed class Options(ImmutableDictionary values) : AnalyzerConfigOptions
+ {
+ public override bool TryGetValue(
+ string key,
+ [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? value
+ )
+ {
+ if (values.TryGetValue(key, out var resolved))
+ {
+ value = resolved;
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
}
}
diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/SelfContainedGeneratorAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/SelfContainedGeneratorAnalyzerTests.cs
new file mode 100644
index 0000000..ab4ed14
--- /dev/null
+++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/SelfContainedGeneratorAnalyzerTests.cs
@@ -0,0 +1,150 @@
+namespace Purview.SourceGeneratorFramework.Analyzers;
+
+public sealed class SelfContainedGeneratorAnalyzerTests
+{
+ static readonly Dictionary DefaultProperties = new()
+ {
+ ["build_property.IsRoslynComponent"] = "true",
+ ["build_property.IsPackable"] = "false",
+ ["build_property.PurviewEmbedSourceGeneratorFramework"] = "true",
+ ["build_property.PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles"] = "false",
+ ["build_property.PurviewSourceGeneratorFrameworkAnalyzerValidation"] = "true",
+ };
+
+ [Test]
+ public async Task GivenNonSelfContainedRoslynComponent_ReportsError(CancellationToken cancellationToken)
+ {
+ // Arrange
+ SelfContainedGeneratorAnalyzer analyzer = new();
+
+ // Act
+ var diagnostics = await analyzer.GetAnalyzerDiagnosticsAsync(
+ "class C { }",
+ DefaultProperties,
+ referenceSourceGeneratorFramework: true,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert
+ .That(diagnostics.Select(static d => d.Id).ToArray())
+ .Contains(SelfContainedGeneratorAnalyzer.DiagnosticId);
+ }
+
+ [Test]
+ public async Task GivenNotRoslynComponent_DoesNotReport(CancellationToken cancellationToken)
+ {
+ // Arrange
+ Dictionary properties = new(DefaultProperties)
+ {
+ ["build_property.IsRoslynComponent"] = "false",
+ };
+
+ // Act
+ var diagnostics = await new SelfContainedGeneratorAnalyzer().GetAnalyzerDiagnosticsAsync(
+ "class C { }",
+ properties,
+ referenceSourceGeneratorFramework: true,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(diagnostics.Any(static d => d.Id == SelfContainedGeneratorAnalyzer.DiagnosticId)).IsFalse();
+ }
+
+ [Test]
+ public async Task GivenNoFrameworkReference_DoesNotReport(CancellationToken cancellationToken)
+ {
+ // Act
+ var diagnostics = await new SelfContainedGeneratorAnalyzer().GetAnalyzerDiagnosticsAsync(
+ "class C { }",
+ DefaultProperties,
+ referenceSourceGeneratorFramework: false,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(diagnostics.Any(static d => d.Id == SelfContainedGeneratorAnalyzer.DiagnosticId)).IsFalse();
+ }
+
+ [Test]
+ public async Task GivenPackableGenerator_DoesNotReport(CancellationToken cancellationToken)
+ {
+ // Arrange
+ Dictionary properties = new(DefaultProperties) { ["build_property.IsPackable"] = "true" };
+
+ // Act
+ var diagnostics = await new SelfContainedGeneratorAnalyzer().GetAnalyzerDiagnosticsAsync(
+ "class C { }",
+ properties,
+ referenceSourceGeneratorFramework: true,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(diagnostics.Any(static d => d.Id == SelfContainedGeneratorAnalyzer.DiagnosticId)).IsFalse();
+ }
+
+ [Test]
+ public async Task GivenMergedAnalyzerFiles_DoesNotReport(CancellationToken cancellationToken)
+ {
+ // Arrange
+ Dictionary properties = new(DefaultProperties)
+ {
+ ["build_property.PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles"] = "true",
+ };
+
+ // Act
+ var diagnostics = await new SelfContainedGeneratorAnalyzer().GetAnalyzerDiagnosticsAsync(
+ "class C { }",
+ properties,
+ referenceSourceGeneratorFramework: true,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(diagnostics.Any(static d => d.Id == SelfContainedGeneratorAnalyzer.DiagnosticId)).IsFalse();
+ }
+
+ [Test]
+ public async Task GivenValidationOptOut_DoesNotReport(CancellationToken cancellationToken)
+ {
+ // Arrange
+ Dictionary properties = new(DefaultProperties)
+ {
+ ["build_property.PurviewSourceGeneratorFrameworkAnalyzerValidation"] = "false",
+ };
+
+ // Act
+ var diagnostics = await new SelfContainedGeneratorAnalyzer().GetAnalyzerDiagnosticsAsync(
+ "class C { }",
+ properties,
+ referenceSourceGeneratorFramework: true,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(diagnostics.Any(static d => d.Id == SelfContainedGeneratorAnalyzer.DiagnosticId)).IsFalse();
+ }
+
+ [Test]
+ public async Task GivenEmbeddingDisabled_DoesNotReport(CancellationToken cancellationToken)
+ {
+ // Arrange
+ Dictionary properties = new(DefaultProperties)
+ {
+ ["build_property.PurviewEmbedSourceGeneratorFramework"] = "false",
+ };
+
+ // Act
+ var diagnostics = await new SelfContainedGeneratorAnalyzer().GetAnalyzerDiagnosticsAsync(
+ "class C { }",
+ properties,
+ referenceSourceGeneratorFramework: true,
+ cancellationToken
+ );
+
+ // Assert
+ await Assert.That(diagnostics.Any(static d => d.Id == SelfContainedGeneratorAnalyzer.DiagnosticId)).IsFalse();
+ }
+}