Remove SourceGenerator.Foundations, ship analyzers under Roslyn-versioned folders for VS2022 and VS2026, and validate the packages - #4120
Conversation
…022/VS2026
The two OPC UA source generators were built on SourceGenerator.Foundations,
whose only job was getting the generator's dependency closure loaded inside the
compiler. It did so with a process-wide, first-generator-wins
AppDomain.AssemblyResolve hook backed by a non-concurrent Dictionary mutated
from AssemblyLoad callbacks, which raced on parallel clean builds and failed
with CS8784 ("Generator 'StackSourceGeneratorHoist' failed to initialize ...
Could not load file or assembly 'SourceGenerator.Foundations.Contracts'").
The repository already solves that problem deterministically: the NuGet
packages ship the closure under analyzers/dotnet/.../cs and Directory.Build.targets
registers the same closure as Analyzer items for project-reference consumers.
SourceGenerator.Foundations is therefore removed and both generators are plain
Roslyn IIncrementalGenerator implementations again. Its implicit crash handling
is replaced by an explicit SourceGenerator.Guard that reports MODELGEN003 /
STACKGEN003 and rethrows cancellation.
Analyzers and generators are now built once per supported Roslyn API version so
they load on both Visual Studio 2022 and Visual Studio 2026. tools/RoslynVariants.props
is the single source of truth for the versions; each analyzer has a primary
project (Roslyn 5.0), a *.Roslyn4_8 sibling that glob-links the same sources
(Roslyn 4.8), and - for the source generators - a *.Pack project that ships both
under analyzers/dotnet/roslyn<major>.<minor>/cs. Opc.Ua.MigrationAnalyzer,
.CodeFixer and .Generator get the same treatment.
Both delivery paths discover the payload by globbing the generator's output
directory, so adding a package to a generator's dependency tree needs no project
file, nuspec or validation script change.
Also fixed while migrating:
- Microsoft.CodeAnalysis.CSharp declares an exact [version] range on
Microsoft.CodeAnalysis.Common, so with central transitive pinning the pins must
move in lockstep or restore fails with NU1109. The central pins now derive from
the Visual Studio 2026 Roslyn version.
- LanguageVersion.CSharp13 does not exist in Roslyn 4.8; the stack generator now
uses CompilationOptions.IsCSharp13OrLater.
- MigrationGeneratorTests created its generator driver without parse options,
which silently matched under Roslyn 4.14 but throws "Inconsistent language
versions" under 5.0.
- build/OPCFoundation.Opc.Ua.SourceGeneration.props was packed under a name that
did not match the package id, so NuGet never auto-imported it. It is now packed
as build/$(PackageId).props.
- The migration analyzer props injected Analyzer items by hand from
analyzers/dotnet/cs. NuGet registers analyzers itself and selects the matching
Roslyn folder, so the injection was obsolete and would now force the wrong
variant to load alongside the selected one.
- GeneratePackageOnBuild is disabled for the generators; packing happens in CI or
on an explicit dotnet pack.
Because the repository's own projects consume the Visual Studio 2026 variant,
building this repository now requires a Roslyn 5.x host (the .NET 10 SDK or
Visual Studio 2026), which is already the documented prerequisite.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed53262-5cfa-429a-820e-bf6146a00d8b
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Removes SourceGenerator.Foundations from the OPC UA source generators and restructures analyzers/generators to ship/build per supported Roslyn API version (VS2022 + VS2026), including updated packaging and in-repo consumption to avoid CS8784 clean-build races.
Changes:
- Introduces shared MSBuild targets/props for Roslyn-variant builds and multi-folder analyzer packaging (
roslyn4.8/roslyn5.0). - Refactors generators back to plain Roslyn
IIncrementalGeneratorwith explicit exception-to-diagnostic guarding. - Updates MigrationAnalyzer + docs/tests/pipelines to the new multi-Roslyn layout and dependency-flow model.
Reviewed changes
Copilot reviewed 56 out of 56 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tools/SourceGeneratorVariant.targets | Shared build settings for generator variant projects (per Roslyn API version). |
| tools/SourceGeneratorPack.targets | Shared packing logic to ship multiple Roslyn variant analyzer folders. |
| tools/SourceGeneration.slnx | Adds Roslyn4_8 + Pack projects to the source generation solution. |
| tools/RoslynVariants.props | Single source of truth for supported Roslyn API versions + folder names. |
| tools/Opc.Ua.SourceGeneration/SourceGeneratorTelemetry.cs | Removes SGF logging bridge; keeps only Debug sink. |
| tools/Opc.Ua.SourceGeneration/SourceGeneratorPackaging.targets | Removes legacy packaging target (single-folder analyzers/dotnet/cs flow). |
| tools/Opc.Ua.SourceGeneration/SourceGeneratorHelpers.cs | Adds shared SourceGenerator.Guard + debugger attach helper. |
| tools/Opc.Ua.SourceGeneration/SourceGenerator.cs | Makes SourceGenerator partial to share helper implementations. |
| tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.csproj | Converts to primary (VS2026) variant project importing shared sources/targets. |
| tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.Sources.targets | Centralizes identity/sources for variant projects + imports variant settings. |
| tools/Opc.Ua.SourceGeneration/NugetREADME.md | Documents supported hosts by Roslyn API. |
| tools/Opc.Ua.SourceGeneration/ModelSourceGenerator.cs | Drops SGF base; uses Roslyn generator + guarded callbacks. |
| tools/Opc.Ua.SourceGeneration/ModelCompilation.cs | Removes SGF logger/context types; telemetry now uses Roslyn context. |
| tools/Opc.Ua.SourceGeneration/DataTypeCompilation.cs | Removes SGF context alias. |
| tools/Opc.Ua.SourceGeneration/CompilationOptions.cs | Adds IsCSharp13OrLater helper (no LanguageVersion.CSharp13 dependency). |
| tools/Opc.Ua.SourceGeneration.Stack/StackSourceGenerator.cs | Drops SGF base; uses Roslyn generator + guarded callback. |
| tools/Opc.Ua.SourceGeneration.Stack/StackGeneration.cs | Removes SGF logger/context types; updates C# version check helper usage. |
| tools/Opc.Ua.SourceGeneration.Stack/SourceGenerator.cs | Makes SourceGenerator partial to share helper implementations. |
| tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.csproj | Converts to primary (VS2026) variant project importing shared sources/targets. |
| tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.Sources.targets | Centralizes stack generator identity/sources + imports variant settings. |
| tools/Opc.Ua.SourceGeneration.Stack/NugetREADME.md | Documents supported hosts by Roslyn API. |
| tools/Opc.Ua.SourceGeneration.Stack.Roslyn4_8/Opc.Ua.SourceGeneration.Stack.Roslyn4_8.csproj | Adds VS2022 (Roslyn 4.8) stack variant project. |
| tools/Opc.Ua.SourceGeneration.Stack.Pack/Opc.Ua.SourceGeneration.Stack.Pack.csproj | Adds pack project to ship both stack variants. |
| tools/Opc.Ua.SourceGeneration.Roslyn4_8/Opc.Ua.SourceGeneration.Roslyn4_8.csproj | Adds VS2022 (Roslyn 4.8) model variant project. |
| tools/Opc.Ua.SourceGeneration.Pack/Opc.Ua.SourceGeneration.Pack.csproj | Adds pack project to ship both model variants + renamed auto-import props. |
| tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.nuspec | Ships analyzer/codefix/generator into per-Roslyn folders. |
| tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.csproj | Pins to Roslyn variant infra; builds both variants before pack; sets nuspec props. |
| tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.Sources.targets | Centralizes migration analyzer identity/sources + imports variant settings. |
| tools/Opc.Ua.MigrationAnalyzer/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props | Removes manual <Analyzer> injection to avoid double-loading variants. |
| tools/Opc.Ua.MigrationAnalyzer/NugetREADME.md | Updates packaging notes to per-Roslyn folder layout. |
| tools/Opc.Ua.MigrationAnalyzer.Roslyn4_8/Opc.Ua.MigrationAnalyzer.Roslyn4_8.csproj | Adds VS2022 (Roslyn 4.8) migration analyzer variant. |
| tools/Opc.Ua.MigrationAnalyzer.Generator/Opc.Ua.MigrationAnalyzer.Generator.csproj | Converts generator to variant infra via shared sources/targets. |
| tools/Opc.Ua.MigrationAnalyzer.Generator/Opc.Ua.MigrationAnalyzer.Generator.Sources.targets | Centralizes generator identity/sources + imports variant settings. |
| tools/Opc.Ua.MigrationAnalyzer.Generator.Roslyn4_8/Opc.Ua.MigrationAnalyzer.Generator.Roslyn4_8.csproj | Adds VS2022 (Roslyn 4.8) migration generator variant. |
| tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.csproj | Converts code fixer to variant infra via shared sources/targets. |
| tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.Sources.targets | Centralizes codefix identity, linked sources, and Roslyn package overrides. |
| tools/Opc.Ua.MigrationAnalyzer.CodeFixer.Roslyn4_8/Opc.Ua.MigrationAnalyzer.CodeFixer.Roslyn4_8.csproj | Adds VS2022 (Roslyn 4.8) migration code fixer variant. |
| tools/MigrationAnalyzerVariant.targets | Shared build settings for migration-analyzer variant projects. |
| tools/MigrationAnalyzer.slnx | Adds Roslyn4_8 and pack projects to the migration analyzer solution. |
| tests/Opc.Ua.SourceGeneration.Tests/TypeGeneratorTests.cs | Removes SGF hoist usage; uses generator directly and parse options. |
| tests/Opc.Ua.SourceGeneration.Tests/NodesetMethodArgumentTests.cs | Removes SGF hoist usage; uses generator directly. |
| tests/Opc.Ua.SourceGeneration.Tests/NodesetIdentifierSidecarTests.cs | Removes SGF hoist usage; uses generator directly. |
| tests/Opc.Ua.SourceGeneration.Tests/NodesetEventRecordTests.cs | Removes SGF hoist usage; uses generator directly. |
| tests/Opc.Ua.SourceGeneration.Tests/ModelGeneratorTests.cs | Removes SGF hoist usage; uses generator directly. |
| tests/Opc.Ua.SourceGeneration.Tests/ModelDependencyScannerTests.cs | Removes SGF hoist usage; uses generator directly. |
| tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGeneratorTests.cs | Removes SGF hoist usage; uses generator directly. |
| tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGenerationAssemblyTests.cs | Removes SGF hoist usage; uses generator directly. |
| tests/Opc.Ua.SourceGeneration.Stack.Tests/SourceGeneratorDiagnosticTests.cs | Adds tests for SourceGenerator.Guard behavior. |
| tests/Opc.Ua.MigrationAnalyzer.Tests/Generators/MigrationGeneratorTests.cs | Fixes driver creation to pass parse options for Roslyn 5.x consistency. |
| docs/migrate/2.0.x/packages.md | Removes SGF from migration package list. |
| docs/DeveloperGuide.md | Documents new per-Roslyn analyzer/generator build & packaging model. |
| UA.slnx | Adds Roslyn4_8 and pack projects to main solution layout. |
| Directory.Packages.props | Central Roslyn package pins derive from RoslynApiVersionVS2026; removes SGF pin. |
| Directory.Build.targets | Adds target to include generator runtime closure as Analyzer items for project-ref usage. |
| .azurepipelines/validate-source-generator-packages.ps1 | Validates multi-Roslyn analyzer folders and forbids SGF/Microsoft.CodeAnalysis shipping. |
| .azurepipelines/preview.yml | Updates pipeline comments for new nuspec property names. |
Comments suppressed due to low confidence (4)
tools/SourceGeneratorPack.targets:1
- The property name appears to be misspelled (
NoRoslynatorAnalzers). If the repo/tooling expectsNoRoslynatorAnalyzers, this typo will prevent the intended setting from taking effect. Rename the property to the correct spelling to ensure consistent analyzer suppression.
tools/SourceGeneratorPack.targets:1 - Setting
PackageLicenseFileto an empty value can cause NuGet packing metadata warnings/misconfiguration. If you’re usingPackageLicenseExpression, removePackageLicenseFileentirely, or set it to an actual file that is packed into the package.
tools/Opc.Ua.SourceGeneration/SourceGeneratorTelemetry.cs:1 - The
exceptionargument is currently not included in the output, which drops the most useful debugging context when logging failures. Consider appendingexception(e.g.,exception.ToString()) to theDebug.WriteLineoutput when it is non-null.
tools/Opc.Ua.SourceGeneration/SourceGeneratorHelpers.cs:1 - Reporting only
ex.Message+ex.StackTracecan produce diagnostics that lack the exception type and inner-exception chain, andStackTracemay be null. Consider reportingex.ToString()(or includingex.GetType().FullName+ex.ToString()) as a single diagnostic argument (optionally truncated) so the diagnostic is reliably actionable across exception types.
Audit of the packages preview.yml produces, plus the CI parallelism that the SourceGenerator.Foundations removal unblocks. Package audit: moving the two generator packages onto dedicated *.Pack projects leaves the shipped set unchanged. Evaluating PackageId/IsPackable across every project in preview-pack.slnx yields the same 47 package ids on master and on this branch, and a full Release build + `dotnet pack --no-build` produces exactly those 47 packages. Only the owning project changed, which is invisible to consumers. To keep it that way, .azurepipelines/expected-packages.txt pins the expected set and validate-source-generator-packages.ps1 fails when the packed output does not match, so adding, removing or renaming a shipped package has to happen deliberately in the same pull request. The script gains two further checks: - The model generator's props file has to be named build/<PackageId>.props. NuGet only auto-imports that exact name, and the file declares every CompilerVisibleProperty and CompilerVisibleItemMetadata the generator relies on. Until this branch it was packed as build/OPCFoundation.Opc.Ua.SourceGeneration.props, which does not match the package id OPCFoundation.NetStandard.Opc.Ua.SourceGeneration, so no NuGet consumer ever imported it and every ModelSourceGenerator* property and AdditionalFiles metadata was silently invisible to the generator. In-repo projects were unaffected because they import the props by relative path. - A standalone consumer that actually drives the generator. The existing clean consumer only proves the analyzer loads; this one feeds a real NodeSet2 in as an AdditionalFile, pins a ModelSourceGeneratorPrefix that cannot be derived from the model itself, and then references the emitted identifier tables and NodeState type from hand-written code. A generator that does not run, or runs without seeing its options, becomes a compile error. Deliberately does not import the props by path, so the package's auto-import is what is under test. Both new checks were verified to fail against a package rebuilt with the historical misnamed props. CI: remove /p:UseSharedCompilation=false and -maxcpucount:1 from buildandtest.yml. `dotnet build` defaults to all logical processors, so -maxcpucount:1 actively disabled parallelism. Both flags were added silently in unrelated pull requests (f358078, 208c601) and suppress exactly the conditions the SourceGenerator. Foundations resolver race needed; that race is gone, and disabling the compiler server now costs more than before because the analyzer closure registered per compilation grew to 19 assemblies. Compilation is roughly 72 percent of what the workflow executes (233 of 323 job-minutes in run 29916768976), so the Build step durations there are the baseline to compare against. codeql-analysis.yml keeps the flag because the CodeQL tracer needs it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed53262-5cfa-429a-820e-bf6146a00d8b
…ostics Copilot's review left no inline threads, only four low-confidence notes. Two were valid and are fixed; two describe load-bearing settings and are now documented so they are not "fixed" into breakage later. Fixed: - SourceGeneratorTelemetry dropped the exception when a log entry had no matching diagnostic descriptor. The SGF-based code passed it to the external sink, so removing SGF silently lost it. The debug fallback now appends the exception. Uses a literal separator rather than Environment.NewLine, which RS1035 bans in analyzers. - Exception diagnostics (MODELGEN003 / STACKGEN003) reported ex.StackTrace, which is null for an exception that never left its throw site and carries neither the exception type nor the inner-exception chain. All three call sites - SourceGenerator.Guard, StackGeneration.Emit and ModelCompilation.Emit - now report ex.ToString() so the detail is identical wherever the failure originates. Documented, not changed: - NoRoslynatorAnalzers is misspelled, but common.props is the only reader and it uses the same spelling, so the setting does take effect. Renaming only here would silently disable it. - PackageLicenseFile must stay empty. common.props sets it to LICENSE.txt for every packable project, and NuGet rejects a package declaring both a license expression and a license file (NU5035). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed53262-5cfa-429a-820e-bf6146a00d8b
Code review flagged, and a run against a real Roslyn 4.8 compiler confirmed, that the roslyn4.8 analyzer payload does not work at all. Directory.Packages.props pins System.Collections.Immutable and System.Reflection.Metadata centrally with transitive pinning on, so the whole analyzer closure - the generator, Opc.Ua.SourceGeneration.Core and Opc.Ua.Types - references 10.0.0.0. Roslyn 4.8 supplies 7.0.0.0. Running csc from Microsoft.Net.Compilers.Toolset 4.8.0 over the packed roslyn4.8 folder gives: warning CS8784: Generator 'ModelSourceGenerator' failed to initialize ... MissingMethodException: Method not found: 'IncrementalValueProvider`1<ImmutableArray`1<!!0>> IncrementalValueProviderExtensions.Collect(IncrementalValuesProvider`1<!!0>)' because the shipped System.Collections.Immutable 10.x binds a second ImmutableArray<T> identity. Dropping that copy from the payload only changes the failure to FileNotFoundException for 10.0.0.0. Both surface as a warning, so the consumer silently gets no generated code. Fixing it properly means building the entire closure, Opc.Ua.Types included, against the band's package versions - a change to a core shipping library that does not belong in this pull request. Note master had the same defect: it was pinned to Roslyn 4.14 while referencing System.Collections.Immutable 10.0.0.0, so no 4.x host ever actually worked. Shipping only roslyn5.0 is therefore not a regression, it is an accurate statement of what has ever worked, and the versioned folder means an older host now skips the analyzer instead of failing inside it. - Remove the five *.Roslyn4_8 variant projects, their solution entries, and the 4.8 half of the pack projects and the MigrationAnalyzer nuspec. - Never ship Microsoft.CodeAnalysis*, System.Collections.Immutable or System.Reflection.Metadata: excluded from both the package payload (SourceGeneratorPack.targets) and the in-repo analyzer closure (Directory.Build.targets), and asserted in validate-source-generator-packages.ps1 so the trap cannot come back. - Document in RoslynVariants.props and DeveloperGuide.md exactly what adding a down-level band requires. Also from the review: the pack guard tested the flattened assembly list, so it only fired when *no* variant produced output - with N variants declared it would silently ship a package missing a band. It now checks each declared variant contributed assemblies (verified by hiding one variant's output and confirming the error fires). And the migration skill doc still described the old analyzers/dotnet/cs layout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed53262-5cfa-429a-820e-bf6146a00d8b
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #4120 +/- ##
==========================================
- Coverage 80.23% 79.91% -0.33%
==========================================
Files 1515 1525 +10
Lines 209980 210637 +657
Branches 36213 36331 +118
==========================================
- Hits 168479 168324 -155
- Misses 28867 29657 +790
- Partials 12634 12656 +22
🚀 New features to boost your workflow:
|
…ntime
Two fixes.
Docker build (refserver job on run 30560951546) failed with
NuGet.targets(198,5): error : Invalid framework identifier ''.
[/src/samples/ConsoleReferenceServer/ConsoleReferenceServer.csproj]
The Dockerfile restores from a hand-listed set of csproj files plus `COPY *.*`
for the repository root. The generator projects now import shared MSBuild logic
that was in neither list, so their TargetFramework evaluated empty and every
project referencing them failed to restore. Reproduced by replaying the
Dockerfile's copy list into a scratch tree, and confirmed by deleting the three
files again afterwards.
- Copy tools/SourceGeneratorVariant.targets and both *.Sources.targets into the
restore layer.
- Move tools/RoslynVariants.props to the repository root as roslyn.props.
Directory.Packages.props imports it, so keeping it under tools/ meant any
partial-copy context that takes only the root files - which is exactly what the
sample Dockerfiles do - could not restore. It now sits with common.props,
targets.props and version.props where the other shared props live.
Second, the roslyn5.0 payload only worked by accident. Microsoft.CodeAnalysis
5.0.0 depends on System.Collections.Immutable 9.0.0, but the central pin put
10.0.0.0 into the whole analyzer closure - the generator,
Opc.Ua.SourceGeneration.Core and Opc.Ua.Types. .NET satisfies a reference from a
higher assembly version but never from a lower one, so the band worked only
because every host that exists today happens to carry 10.x: the .NET 10 SDK
supplies it from the shared framework (it is not in Roslyn/bincore) and Visual
Studio 2026's csc carries 10.0.0.1. A host on the band's declared floor would
have failed the same way the removed roslyn4.8 payload did.
The pin now derives from RoslynRuntimeVersionVS2026 in roslyn.props, so the
closure is 9.0.0.0 throughout and matches what Microsoft.CodeAnalysis 5.0.0
itself references. Verified by scanning assembly references in the packed
payload. Deriving the pin from the band rather than leaving a loose literal is
what stops the closure drifting above the host floor again.
Opc.Ua.Core.Schema.Tests and Opc.Ua.PubSub.Schema.Tests pull
System.Collections.Immutable >= 10.0.5 transitively through JsonSchema.Net; they
are not part of the analyzer closure, so they override the pin locally.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed53262-5cfa-429a-820e-bf6146a00d8b
The generator and migration-analyzer packages shipped only `analyzers/dotnet/roslyn5.0/cs`. The .NET SDK loads the highest band its compiler supports and silently skips anything above it, so every consumer on Visual Studio 2022 / .NET 9 SDK got no diagnostics and no generated code, with only warning CS9057 to show for it. Ship a second band built against Microsoft.CodeAnalysis 4.14. That version depends on the same System.Collections.Immutable 9.0.0 as 5.0.0 and its csc runs on net9.0, so the two bands share $(RoslynRuntimeVersion) and only the five Roslyn-referencing assemblies need a second build - Opc.Ua.Types and Opc.Ua.SourceGeneration.Core are compiled once and serve both. Nothing else in the repository exercises the down-level payload, and every way it can be wrong is reported as a compiler *warning*, so a broken band would ship silently. validate-source-generator-packages.ps1 therefore now runs the packed roslyn4.14 payload through a real Microsoft.Net.Compilers.Toolset 4.14.0 csc and asserts both that no CS9057/CS8784/CS8032/CS8034 is emitted and that /reportanalyzer lists the generator as having executed - absence of complaints alone would also pass if the generator were never handed to the compiler. The migration analyzer, whose payload comes from a hand-written nuspec rather than SourceGeneratorPack.targets, gets the same treatment plus a band/assembly manifest check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ed53262-5cfa-429a-820e-bf6146a00d8b
Carries master's #4120 up the stack. The generator tests this change adds still used the SourceGenerator.Foundations ModelSourceGeneratorHoist wrapper, which #4120 removed, so they now drive CSharpGeneratorDriver with the generator directly like the rest of the suite. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
Description
The two OPC UA source generators were built on
SourceGenerator.Foundations(SGF), whose only job was getting the generator's dependency closure loaded inside the compiler. It does so with a process-wide, first-generator-winsAppDomain.AssemblyResolvehook backed by a non-concurrentDictionarymutated fromAssemblyLoadcallbacks. Concurrent compilations in a sharedVBCSCompilerrace it, which is the root cause of the intermittent clean-build failure:The repository already solves that problem deterministically — #4041 made the NuGet packages ship the full closure under
analyzers/dotnet/.../cs, andDirectory.Build.targetsregisters the same closure asAnalyzeritems for in-repoProjectReferenceconsumers. SGF was therefore pure liability and is removed.What changed
1. SourceGenerator.Foundations removed.
ModelSourceGeneratorandStackSourceGeneratorare plain[Generator(LanguageNames.CSharp)] IIncrementalGeneratorimplementations again. SGF's implicit crash handling is replaced by an explicitSourceGenerator.Guardthat reportsMODELGEN003/STACKGEN003and rethrows cancellation. Its logger sink was already dead code and is dropped.Opc.Ua.SourceGeneration.Stack.dllshrank from 18 MB (embedded closure) to 29 KB.2. Analyzers now ship under Roslyn-versioned folders.
analyzers/dotnet/roslyn4.14/csandanalyzers/dotnet/roslyn5.0/csinstead ofanalyzers/dotnet/cs, driven byroslyn.propsat the repository root, with new*.Packprojects owning the package identity. The versioned folder matters: a host older than the band skips the analyzer instead of loading it and failing inside generator initialization.3. Dependencies flow automatically. Both delivery paths discover the payload by globbing the generator's output directory, so adding a package to a generator's dependency tree requires no
.csproj,.targets,.nuspecor validation-script edit. Verified by temporarily adding aPackageReferenceand watching it appear in the payload with no other file touched.Two Roslyn bands: VS2022 and VS2026
Analyzers ship twice, once per band. The .NET SDK loads the highest band its compiler supports and silently skips anything above it, so a single-band package leaves every consumer below that band with no diagnostics and no generated code.
roslyn4.14Microsoft.CodeAnalysis.CSharp 4.14.0roslyn5.0Microsoft.CodeAnalysis.CSharp 5.0.0Getting this wrong is invisible, which is most of why it took three attempts. The rule:
Every failure mode is a compiler warning:
CS9057CS8784MissingMethodException- a shipped S.C.I bound a secondImmutableArray<T>CS8032This PR first shipped a
roslyn4.8band that never loaded - confirmed against a real Roslyn 4.8 compiler:That band was removed, and this is not a regression: master had the same defect, pinned to Roslyn 4.14 while referencing
System.Collections.Immutable 10.0.0.0, so no 4.x host ever worked there either.The band that landed instead is
4.14, and it is nearly free:Microsoft.CodeAnalysis 4.14depends on the same S.C.I 9.0.0 as 5.0.0 and itscscruns onnet9.0. Both bands therefore share$(RoslynRuntimeVersion), soOpc.Ua.TypesandOpc.Ua.SourceGeneration.Coreare compiled once and serve both; only the five Roslyn-referencing assemblies get a second build, each from a 12-line project that glob-links the primary sources and overridesRoslynApiVersion. Going lower still - 4.8 wants S.C.I 7.x - would mean rebuilding that whole closure,Opc.Ua.Typesincluded, and remains out of scope. (System.Collections.Immutable 7.0.0also does not resolve in this repository:NU1603, 8.0.0 substituted.)Nothing else in the repository exercises the down-level payload, and since every failure is a warning, a broken band would ship silently - exactly as the 4.8 one did. So
validate-source-generator-packages.ps1now restores a realMicrosoft.Net.Compilers.Toolset 4.14.0, runs itscsc.dllover the packedroslyn4.14payload, and asserts both that none ofCS9057/CS8784/CS8032/CS8034is emitted and that/reportanalyzerlists the generator as having executed - silence alone would also pass if the generator were never handed to the compiler. The migration analyzer, whose payload comes from a hand-written nuspec rather thanSourceGeneratorPack.targets, gets the same check plus a band/assembly manifest assertion.To stop the trap recurring,
Microsoft.CodeAnalysis*,System.Collections.ImmutableandSystem.Reflection.Metadataare excluded from both the package payload and the in-repo analyzer closure, and asserted against in the validation script.roslyn.propsanddocs/DeveloperGuide.mddocument what adding a band requires.The shipped package set is unchanged
Evaluating
PackageId/IsPackableacross every project inpreview-pack.slnxgives the same 47 package ids onmasterand on this branch, and a full Release build plusdotnet pack --no-buildproduces exactly those 47. Only the owning project changed:…Opc.Ua.SourceGenerationOpc.Ua.SourceGeneration.csprojOpc.Ua.SourceGeneration.Pack.csproj…Opc.Ua.SourceGeneration.StackOpc.Ua.SourceGeneration.Stack.csprojOpc.Ua.SourceGeneration.Stack.Pack.csproj.azurepipelines/expected-packages.txtpins the expected set and the validation script fails when the packed output drifts.Two new package checks
build/<PackageId>.propsnaming. NuGet only auto-imports that exact name, and the file declares everyCompilerVisibleProperty/CompilerVisibleItemMetadatathe model generator relies on. It was previously packed asbuild/OPCFoundation.Opc.Ua.SourceGeneration.props, which does not match the package id — so no NuGet consumer ever imported it, and everyModelSourceGenerator*property andAdditionalFilesmetadata was silently invisible. In-repo projects were unaffected because they import it by relative path.An end-to-end consumer that actually drives the generator. The pre-existing clean-consumer check only proved the analyzer loads.
Test-SourceGeneratingConsumerbuilds a standalone project that feeds a real NodeSet2 in as anAdditionalFile, pins aModelSourceGeneratorPrefixthat cannot be derived from the model, and references the emitted types from hand-written code. It deliberately does not import the props by path, so the package's auto-import is what is under test. Both checks were verified to fail against a package rebuilt with the misnamed props.CI: parallelism re-enabled
buildandtest.ymlpinned threedotnet buildinvocations to/p:UseSharedCompilation=falseand-maxcpucount:1.dotnet builddefaults to all logical processors, so-maxcpucount:1actively disabled parallelism. Both flags were introduced silently in unrelated PRs (f358078b1,208c601b0) and suppress exactly the conditions the SGF race needed — now structurally gone. Compilation is ~72 % of what that workflow executes (233 of 323 job-minutes in run29916768976), so the queue-independentBuildstep durations are the baseline:Buildstep, 26test-*jobsbuild-linux-all-tfmbuild-windows-all-tfmcodeql-analysis.ymlkeeps the flag — the CodeQL tracer requires it.Incidental fixes
Microsoft.CodeAnalysis.CSharpdeclares an exact[version]range onMicrosoft.CodeAnalysis.Common, so withCentralPackageTransitivePinningEnabledthe central pins must move in lockstep or restore fails withNU1109. They now derive from$(RoslynApiVersionVS2026).MigrationGeneratorTestscreated its generator driver without parse options — silently fine under Roslyn 4.14,Inconsistent language versionsunder 5.0.<Analyzer>items by hand fromanalyzers/dotnet/cs; NuGet registers analyzers itself and selects the matching folder, so the injection was obsolete and would now force the wrong assembly to load.ex.StackTrace(null for an exception that never left its throw site, and carrying neither type nor inner chain) — nowex.ToString()at all three call sites.GeneratePackageOnBuildis disabled for the generators; packing happens in CI or on an explicitdotnet pack.Note for reviewers
Building this repository requires a Roslyn 5.x host (the .NET 10 SDK or Visual Studio 2026). That is already the documented prerequisite and every CI leg installs
10.0.xlast, but it is now a hard requirement.Related Issues
No tracking issue: this is a follow-up to the CS8784 clean-build failure reported against #4041, which fixed the packaged delivery path but left the in-repo project-reference path relying on the SGF resolver.
Checklist
Put an
xin the boxes that apply. You can complete these step by step after opening the PR.Verification detail
dotnet build UA.slnx(clean, and after merging master)CA1873inOpc.Ua.ClientOpc.Ua.SourceGeneration.TestsOpc.Ua.SourceGeneration.Stack.TestsOpc.Ua.SourceGeneration.Core.TestsOpc.Ua.MigrationAnalyzer.Testsdotnet pack --no-buildroslyn4.14androslyn5.0present; no host-provided assemblies shippedvalidate-source-generator-packages.ps1roslyn4.14→ realCS9057; non-running generator name → execution assertion fires; expecting aroslyn4.8band → manifest check fires