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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageVersion Include="TUnit.Mocks" Version="$(TUnitVersion)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[8.0.0,)" />
<PackageVersion Include="System.Collections.Immutable" Version="[10.0.1,)" />
<PackageVersion Include="System.Reflection.Metadata" Version="[10.0.1,)" />
<PackageVersion Include="System.Reflection.MetadataLoadContext" Version="10.0.1" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="ILRepack.Lib" Version="2.0.48" />
Expand Down
14 changes: 14 additions & 0 deletions docs/wiki/Analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ The analyzers enforce two families of rules:
- **C# 14 extension-member conventions** — `PSGFR34`–`PSGFR38`, plus the associated
`ReorganizeExtensionClassCodeFixProvider` and `ConvertToExtensionBlockCodeFixProvider`.

## Build-time validation diagnostics

These are MSBuild diagnostics rather than compiler analyzers, so they are not tracked in
`AnalyzerReleases.*.md`:

| Code | Raised by | Summary |
|------|-----------|---------|
| `PSGF0001` | `Purview.BuildSdk` | A Roslyn component did not produce (or did not declare) a source-generator analyzer file. |
| `PSGF0003` | `Purview.SourceGeneratorFramework` | A `PurviewGeneratorVisibleProperty` is not compiler-visible in the declaring project or its `Sdk/build`/`Sdk/buildTransitive` assets, so consumers cannot read `build_property.<Name>`. |
| `PRSGD0005` | `Purview.BuildSdk` | A file in the returned analyzer closure references an assembly that is neither part of the closure nor a compiler-host assembly. |

Opt out with `PurviewSourceGeneratorFrameworkAnalyzerValidation=false` (`PRSGD0005`) or
`PurviewSourceGeneratorFrameworkGeneratorPropertyValidation=false` (`PSGF0003`).

## Rule reference

| Rule | Summary |
Expand Down
15 changes: 15 additions & 0 deletions docs/wiki/Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ type identity. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly
is not required. Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `false` only when the
unmerged assembly + loose framework DLL shape is required (see [Packaging.md](Packaging.md)).

### A component that references another component

A code-fix component can reference the generator component normally (`ProjectReference`,
`ReferenceOutputAssembly` not `false`) when it needs the generator's internal diagnostic identity.
The generator's **analyzer artifact** is the merged, self-contained assembly, while its **bin output**
stays unmerged; the framework assembly is copied beside that bin output and flows transitively
through `ProjectReference`, so the dependent component's bin folder is self-sufficient. The dependent
component's analyzer closure includes the referenced component's merged artifact — it is never
IL-merged a second time, which would duplicate its types. See
[Packaging.md](Packaging.md#a-component-that-references-another-component).

Declare each MSBuild property the generator reads with
`<PurviewGeneratorVisibleProperty Include="MyGenerator_Disable" />`; `PSGF0003` fails the build when
the property is not compiler-visible.

### Referencing a generator from its test project

A test project can need the source-generator project in two different roles at the same time:
Expand Down
98 changes: 92 additions & 6 deletions docs/wiki/Packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,107 @@ 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` (default) | A consuming project references the component as an analyzer | The **merged**, self-contained component, returned from the content-addressed intermediate `purview-merged/<contentTag>/` 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. |
| `GetPurviewMergedAnalyzerFileForPack` (pack-time, via `TargetsForTfmSpecificContentInPackage`) | Packing a standalone, packable generator project | The **merged**, self-contained DLL (and its PDB) is contributed directly to `analyzers/dotnet/cs`. The component's bin output is never mutated, so its on-disk shape does not depend on whether `build` or `pack` ran last. |

### A component that references another component

A code-fix (or analyzer) component can reference the generator component normally
(`ProjectReference`, `ReferenceOutputAssembly` not `false`) when it needs the generator's internal
diagnostic identity. Both components are `IsRoslynComponent`; only the generator references the
framework.

- The generator component merges at build time. Its **analyzer artifact** is the merged,
self-contained assembly from `obj/.../purview-merged/<contentTag>/`; its **bin output** stays unmerged and
references `Purview.SourceGeneratorFramework`.
- Because a package consumer's framework `lib/` asset is not copied to output, the framework
assembly is declared as copy-to-output content beside the generator's bin output. Content with
`CopyToOutputDirectory` flows transitively through `ProjectReference`, so the dependent code-fix
component's bin folder is self-sufficient too — that is what lets Visual Studio load the code-fix
provider from its own bin without a `FileNotFoundException` for the framework assembly.
- The dependent component's **analyzer closure** (what `GetSourceGeneratorAnalyzerFiles` returns)
includes the referenced component's analyzer artifact. The referenced component is returned as its
own merged assembly and is **never** IL-merged into the dependent component, which would duplicate
its types (including `InternalsVisibleTo`-visible internals) inside a second analyzer in the same
host.
- Visual Studio's project system resolves an `OutputItemType=Analyzer` project reference to the
referenced project's default target path — the component's **unmerged** bin assembly — and adds it
to the compiler's analyzers, which the command-line build does not. `Purview.BuildSdk` removes that
item before adding the resolved closure, so Roslyn only ever receives the merged artifact.
Otherwise the unmerged copy drags `Purview.SourceGeneratorFramework` into the compiler host
(`CS8784 FileNotFoundException: Could not load file or assembly 'Purview.SourceGeneratorFramework'`)
and the generators are registered twice.

### Build-time analyzer-closure validation

`Purview.BuildSdk` validates the whole closure returned by `GetSourceGeneratorAnalyzerFiles`: every
file's PE `AssemblyRef` must resolve to another file in the returned set or to a compiler-host
assembly (`Microsoft.CodeAnalysis*`, `System.Composition.*`, `System.*`, `netstandard`, …). A missing
component artifact fails the build with `PRSGD0005`. Extend the permitted set with
`<PurviewAnalyzerClosurePermittedReference Include="..." />` (or the
`PurviewAnalyzerClosurePermittedReferences` property). Opt out with
`PurviewSourceGeneratorFrameworkAnalyzerValidation=false`.

### Generator-read MSBuild properties

A generator that reads `build_property.<Name>` is only correct if `<Name>` is a
`CompilerVisibleProperty` wherever the generator runs. Declare the properties a component reads:

```xml
<ItemGroup>
<PurviewGeneratorVisibleProperty Include="MyGenerator_Disable" />
</ItemGroup>
```

`PSGF0003` fails the build when a declared property is neither a `CompilerVisibleProperty` in the
project nor declared by the project's own `Sdk/build` or `Sdk/buildTransitive` assets (which is what
consumers receive). Opt out with
`PurviewSourceGeneratorFrameworkGeneratorPropertyValidation=false`.

NuGet imports `buildTransitive` assets for **PackageReference** consumers only. While developing the
package repository itself every project uses `ProjectReference`, so
`Purview.BuildSdk`'s `ImportProjectReferencedBuildTransitiveAssets` target registers the referenced
project's `Sdk/buildTransitive/*.props|*.targets` `CompilerVisibleProperty` items for in-repo
consumers. Opt out with `PurviewImportProjectReferenceBuildTransitive=false`.

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.
intermediate output. `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.

The merged artifact is **content-addressed**: it lives in
`$(IntermediateOutputPath)purview-merged/<contentTag>/$(TargetFileName)`, where `<contentTag>` is a
SHA-256 of the component, the framework assembly and the merge tool (the merge tool computes it via
its `--tag` mode). The tag is in the *directory*, never the file name, because ILRepack derives the
merged assembly's simple name from the output file name and an analyzer must keep
`<AssemblyName>.dll`.

That gives two properties the earlier fixed-name output could not:

- a rebuild with identical inputs reuses the existing file and **skips the merge**, keeping
`_PurviewMergeSourceGeneratorFramework` idempotent across the per-consumer (and potentially
parallel) invocations of `GetSourceGeneratorAnalyzerFiles`; and
- changed inputs select a **new directory**, so the merge never overwrites a merged assembly that a
compiler host (csc/`VBCSCompiler`/Visual Studio) already has loaded. Overwriting such a file fails
on Windows with a sharing violation, which surfaced as `MSB3073` (merge tool exit code
`-532462766`) and left consumers loading a stale or partially written analyzer — the root cause of
`CS8784 FileNotFoundException: Could not load file or assembly 'Purview.SourceGeneratorFramework'`.

The merge tool merges into a per-process `.staging-<pid>` directory and publishes it with a
non-overwriting directory rename; a concurrent invocation that loses the race simply observes the
published artifact instead of failing.

Packaging needs the stable `<AssemblyName>.dll` name, so the pack paths
(`GetPurviewMergedAnalyzerFile`, `GetPurviewMergedAnalyzerFileForPack`) copy the content-addressed
artifact into `$(IntermediateOutputPath)purview-pack/` and pack that plainly named staging copy.

### Framework type internalization in merged components

Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"allowPrerelease": false
},
"msbuild-sdks": {
"Purview.BuildSdk": "1.0.0-prerelease.57"
"Purview.BuildSdk": "1.0.0-prerelease.60"
},
"test": {
"runner": "Microsoft.Testing.Platform"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "purview-sourcegenerator-framework",
"version": "1.0.0-prerelease.51",
"version": "1.0.0-prerelease.52",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
Expand Down
7 changes: 6 additions & 1 deletion purview-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
"analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Generators.dll",
"build/Purview.SourceGeneratorFramework.props",
"build/Purview.SourceGeneratorFramework.targets",
"content/Packaging.md",
"contentFiles/any/netstandard2.0/Packaging.md",
"lib/netstandard2.0/Purview.SourceGeneratorFramework.dll",
"lib/netstandard2.0/Purview.SourceGeneratorFramework.xml",
"packaging.md",
"purview-logo-light.png",
"README.md",
"tools/*/ILRepack.dll",
Expand Down Expand Up @@ -48,7 +51,9 @@
]
},
"ForbiddenContent": {
"*": ["analyzers/**/Purview.SourceGeneratorFramework.dll"]
"*": [
"analyzers/**/Purview.SourceGeneratorFramework.dll"
]
}
},
"Release": {
Expand Down
1 change: 1 addition & 0 deletions src/SourceGeneratorFramework.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<Project Path="tests/SourceGeneratorFramework.MergeTool.UnitTests/SourceGeneratorFramework.MergeTool.UnitTests.csproj" />
<Project Path="tests/SourceGeneratorFramework.UnitTests/SourceGeneratorFramework.UnitTests.csproj" />
<Project Path="tests/SourceGeneratorShared.UnitTests/SourceGeneratorShared.UnitTests.csproj" />
<Project Path="tests/SourceGeneratorFramework.BuildIntegrationTests/SourceGeneratorFramework.BuildIntegrationTests.csproj" />
</Folder>
<Folder Name="/tests/examples/">
<Project Path="tests/SourceGeneratorFramework.ExampleGenerator.CodeFixers.UnitTests/SourceGeneratorFramework.ExampleGenerator.CodeFixers.UnitTests.csproj" />
Expand Down
Loading
Loading