diff --git a/.agents/skills/opcua-v20-migration/SKILL.md b/.agents/skills/opcua-v20-migration/SKILL.md index 3418d30c26..8d813806de 100644 --- a/.agents/skills/opcua-v20-migration/SKILL.md +++ b/.agents/skills/opcua-v20-migration/SKILL.md @@ -17,7 +17,8 @@ description: | errors after upgrading to v20". license: MIT compatibility: | - Requires .NET SDK 10.0.300+, a C# project, and resolvable access to the + Requires .NET SDK 9.0.100+ (10.0.300+ for the `dotnet format analyzers` + auto-fix pass), a C# project, and resolvable access to the OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer NuGet package (v2.0.*-*). IDE auto-fixes need a Workspaces-aware host (Visual Studio, Rider, or `dotnet format`). Generator + analyzers load in csc.exe too. @@ -166,9 +167,9 @@ The single `OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` NuGet contains | Component | Where | Loaded by | Purpose | |---|---|---|---| -| `Opc.Ua.MigrationAnalyzer.dll` | `analyzers/dotnet/cs/` | csc.exe and IDE | 19 `DiagnosticAnalyzer`s (UA0001–UA0022). Targets stable Roslyn 4.14 API, no `Workspaces` reference, csc-safe. | -| `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` | `analyzers/dotnet/cs/` | Workspaces-aware hosts only (Visual Studio, Rider, `dotnet format analyzers`) | 14 `CodeFixProvider`s. | -| `Opc.Ua.MigrationAnalyzer.Generator.dll` | `analyzers/dotnet/cs/` | csc.exe and IDE | `IIncrementalGenerator` that emits `internal sealed [Obsolete] class Collection : List` shims into the consumer compilation for every `Collection` reference that fails to bind. | +| `Opc.Ua.MigrationAnalyzer.dll` | `analyzers/dotnet/roslyn4.14/cs/` and `roslyn5.0/cs/` | csc.exe and IDE | 19 `DiagnosticAnalyzer`s (UA0001–UA0022). No `Workspaces` reference, csc-safe. | +| `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` | `analyzers/dotnet/roslyn4.14/cs/` and `roslyn5.0/cs/` | Workspaces-aware hosts only (Visual Studio, Rider, `dotnet format analyzers`) | 14 `CodeFixProvider`s. | +| `Opc.Ua.MigrationAnalyzer.Generator.dll` | `analyzers/dotnet/roslyn4.14/cs/` and `roslyn5.0/cs/` | csc.exe and IDE | `IIncrementalGenerator` that emits `internal sealed [Obsolete] class Collection : List` shims into the consumer compilation for every `Collection` reference that fails to bind. | | `Opc.Ua.MigrationAnalyzer.Core.dll` | `lib//` × 6 TFMs (`net472`, `net48`, `netstandard2.1`, `net8.0`, `net9.0`, `net10.0`) | Runtime | Re-supplies the obsolete extension surface 2.0 moved or removed so 1.5.378 call sites continue to compile with `[Obsolete]` warnings. | ### The 19 analyzer rules at a glance diff --git a/.agents/skills/opcua-v20-migration/references/compatibility-matrix.md b/.agents/skills/opcua-v20-migration/references/compatibility-matrix.md index f433a16e3c..e9ac561dc7 100644 --- a/.agents/skills/opcua-v20-migration/references/compatibility-matrix.md +++ b/.agents/skills/opcua-v20-migration/references/compatibility-matrix.md @@ -8,8 +8,8 @@ correctly. | Component | Minimum | Recommended | Notes | |---|---|---|---| -| .NET SDK | 10.0.300 | latest 10.x | Earlier SDKs ship older Roslyn that has known incremental-generator bugs | -| `dotnet format` | bundled with SDK 10.0.300+ | latest 10.x | The `analyzers` subcommand is what applies UA0002…UA0022 fixes | +| .NET SDK | 9.0.100 | latest 10.x | 9.0.100 ships Roslyn 4.14, which the package's `roslyn4.14` band targets. Earlier SDKs ship older Roslyn that has known incremental-generator bugs | +| `dotnet format` | bundled with SDK 10.0.300+ | latest 10.x | The `analyzers` subcommand is what applies UA0002…UA0022 fixes. Diagnostics alone need only SDK 9.0.100; the auto-fix pass needs 10.0.300+ | | C# language version | 13 | 14 (default in SDK 10) | Required for `extension` keyword the runtime shim uses | | Consumer project SDK | `Microsoft.NET.Sdk` (SDK-style) | same | Pre-SDK MSBuild XML projects (`xmlns="…/2003"`) cannot install the analyzer — see [`known-gaps.md` G1](known-gaps.md#g1--legacy-net-framework-winforms-projects-in-pre-sdk-msbuild-xml) | @@ -37,19 +37,27 @@ and SDK-style csproj rewrites. ## Roslyn API targeting (internal) -The migration package's Roslyn components are built against the **stable -analyzer API surface**: +Every Roslyn component ships twice, once per band, under +`analyzers/dotnet//cs/`. The .NET SDK picks the highest band its compiler +supports and ignores the rest, so a single package serves both hosts: -| DLL | Roslyn API target | Why | +| Band | Built against | Loaded by | |---|---|---| -| `Opc.Ua.MigrationAnalyzer.dll` | `Microsoft.CodeAnalysis.CSharp 4.14.0` | csc-safe (loads in `csc.exe`); Workspaces-free | -| `Opc.Ua.MigrationAnalyzer.Generator.dll` | `Microsoft.CodeAnalysis.CSharp 4.14.0` | csc-safe; needed for `IIncrementalGenerator` | -| `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` | `Microsoft.CodeAnalysis.CSharp 4.14.0` + `Microsoft.CodeAnalysis.CSharp.Workspaces 4.14.0` | Loaded only in Workspaces-aware hosts (Visual Studio, `dotnet format`) | - -> The repo's `Directory.Packages.props` pins all `Microsoft.CodeAnalysis.*` -> packages to `4.14.0`. This is the **stable analyzer API**, not the -> csc-internal version that the .NET SDK ships (which is `5.x` in SDK 10). -> Analyzers built against 5.x silently fail to load in csc.exe — see +| `roslyn4.14` | `Microsoft.CodeAnalysis.CSharp 4.14.0` | Visual Studio 2022 17.14+ / .NET 9 SDK | +| `roslyn5.0` | `Microsoft.CodeAnalysis.CSharp 5.0.0` | Visual Studio 2026 18.0+ / .NET 10 SDK | + +Within a band, the three components differ only in what else they reference: + +| DLL | Extra reference | Why | +|---|---|---| +| `Opc.Ua.MigrationAnalyzer.dll` | none | csc-safe (loads in `csc.exe`); Workspaces-free | +| `Opc.Ua.MigrationAnalyzer.Generator.dll` | none | csc-safe; needed for `IIncrementalGenerator` | +| `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` | `Microsoft.CodeAnalysis.CSharp.Workspaces` | Loaded only in Workspaces-aware hosts (Visual Studio, `dotnet format`) | + +> An analyzer built against a **newer** Roslyn than the host is skipped silently +> with warning `CS9057` — the consumer simply gets no diagnostics and no +> generated shims. That is why the package ships a band per supported host +> rather than a single `analyzers/dotnet/cs/` folder; see > [`known-gaps.md` G9](known-gaps.md#g9--analyzer-silently-doesnt-load-under-cscexe-historical-fixed). ## Verifying analyzer + generator loaded under csc.exe diff --git a/.agents/skills/opcua-v20-migration/references/known-gaps.md b/.agents/skills/opcua-v20-migration/references/known-gaps.md index 5c8883a151..624d52616e 100644 --- a/.agents/skills/opcua-v20-migration/references/known-gaps.md +++ b/.agents/skills/opcua-v20-migration/references/known-gaps.md @@ -148,8 +148,10 @@ code-fixers in one assembly, which transitively referenced swallowed the Workspaces load failure → zero diagnostics across all samples even though `/analyzer:` was on the csc command line. -**Status:** fixed (commit `861fa6ee1`). Analyzer split into two DLLs; analyzer -DLL is Workspaces-free + targets stable Roslyn 4.14 API. +**Status:** fixed (commit `861fa6ee1`). Analyzer split into two DLLs; the analyzer +DLL is Workspaces-free. Each DLL now ships once per Roslyn band (`roslyn4.14`, +`roslyn5.0`) so the host always loads a build it can run — see +[`compatibility-matrix.md`](compatibility-matrix.md#roslyn-api-targeting-internal). **Verification:** if you ever suspect the analyzer isn't firing, run with `/p:ReportAnalyzer=true` and confirm `Opc.Ua.MigrationAnalyzer` and diff --git a/.azurepipelines/expected-packages.txt b/.azurepipelines/expected-packages.txt new file mode 100644 index 0000000000..a26ac92070 --- /dev/null +++ b/.azurepipelines/expected-packages.txt @@ -0,0 +1,63 @@ +# Every NuGet package the preview pipeline is expected to produce from +# `dotnet pack preview-pack.slnx`, one package id per line. +# +# `validate-source-generator-packages.ps1` fails the build when the packed output +# does not match this list exactly. The point is that adding, removing or renaming +# a shipped package has to be a conscious, reviewed act - a package silently +# disappearing (or a private build-time project accidentally becoming packable) +# is otherwise invisible until consumers notice. +# +# Lines starting with '#' and blank lines are ignored. The list covers the modern +# `dotnet pack` output only; the hand-authored `nuget/*.nuspec` packages are packed +# in a later pipeline step and are not part of this check. +# +# To update: run `dotnet pack preview-pack.slnx` and reconcile the produced +# package ids with this file in the same pull request that changes them. + +OPCFoundation.NetStandard.Opc.Ua.Bindings.Https +OPCFoundation.NetStandard.Opc.Ua.Client +OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes +OPCFoundation.NetStandard.Opc.Ua.Configuration +OPCFoundation.NetStandard.Opc.Ua.Core +OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics +OPCFoundation.NetStandard.Opc.Ua.Core.Schema +OPCFoundation.NetStandard.Opc.Ua.Core.Types +OPCFoundation.NetStandard.Opc.Ua.Di +OPCFoundation.NetStandard.Opc.Ua.Di.Client +OPCFoundation.NetStandard.Opc.Ua.Di.Server +OPCFoundation.NetStandard.Opc.Ua.Gds.Client.Common +OPCFoundation.NetStandard.Opc.Ua.Gds.Common +OPCFoundation.NetStandard.Opc.Ua.Gds.Server.Common +OPCFoundation.NetStandard.Opc.Ua.ISA95 +OPCFoundation.NetStandard.Opc.Ua.ISA95.Client +OPCFoundation.NetStandard.Opc.Ua.ISA95.Server +OPCFoundation.NetStandard.Opc.Ua.Lds.Server +OPCFoundation.NetStandard.Opc.Ua.Mcp +OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer +OPCFoundation.NetStandard.Opc.Ua.PubSub +OPCFoundation.NetStandard.Opc.Ua.PubSub.Adapter +OPCFoundation.NetStandard.Opc.Ua.PubSub.Diagnostics +OPCFoundation.NetStandard.Opc.Ua.PubSub.Eth +OPCFoundation.NetStandard.Opc.Ua.PubSub.Kafka +OPCFoundation.NetStandard.Opc.Ua.PubSub.Mqtt +OPCFoundation.NetStandard.Opc.Ua.PubSub.Schema +OPCFoundation.NetStandard.Opc.Ua.PubSub.Server +OPCFoundation.NetStandard.Opc.Ua.PubSub.Udp +OPCFoundation.NetStandard.Opc.Ua.Quickstarts.Servers +OPCFoundation.NetStandard.Opc.Ua.Redundancy +OPCFoundation.NetStandard.Opc.Ua.Redundancy.Client +OPCFoundation.NetStandard.Opc.Ua.Redundancy.Kubernetes +OPCFoundation.NetStandard.Opc.Ua.Redundancy.PubSub +OPCFoundation.NetStandard.Opc.Ua.Redundancy.Server +OPCFoundation.NetStandard.Opc.Ua.Security.Certificates +OPCFoundation.NetStandard.Opc.Ua.Server +OPCFoundation.NetStandard.Opc.Ua.SourceGeneration +OPCFoundation.NetStandard.Opc.Ua.SourceGeneration.Core +OPCFoundation.NetStandard.Opc.Ua.SourceGeneration.Stack +OPCFoundation.NetStandard.Opc.Ua.Types +OPCFoundation.NetStandard.Opc.Ua.WotCon +OPCFoundation.NetStandard.Opc.Ua.WotCon.Client +OPCFoundation.NetStandard.Opc.Ua.WotCon.Server +OPCFoundation.NetStandard.Opc.Ua.XRegistry +OPCFoundation.NetStandard.Opc.Ua.XRegistry.Client +OPCFoundation.NetStandard.Opc.Ua.XRegistry.Server diff --git a/.azurepipelines/preview.yml b/.azurepipelines/preview.yml index 03f2ba5cfd..4c3dfb91fe 100644 --- a/.azurepipelines/preview.yml +++ b/.azurepipelines/preview.yml @@ -133,7 +133,7 @@ jobs: inputs: command: 'pack' # Scoped to nuget/ — the MigrationAnalyzer nuspec at tools/Opc.Ua.MigrationAnalyzer/ - # uses MSBuild $(...) substitutions ($(shimBin), $(repoRoot), $(analyzerDll)) + # uses MSBuild $(...) substitutions ($(shimBin), $(repoRoot), $(analyzerDllVS2026)) # that only `dotnet pack`'s _SetMigrationAnalyzerNuspecProperties target can # resolve. Standalone `nuget.exe pack` leaves them literal and crashes with # DirectoryNotFoundException on `D:\net472`. The MigrationAnalyzer package is diff --git a/.azurepipelines/validate-source-generator-packages.ps1 b/.azurepipelines/validate-source-generator-packages.ps1 index 7aa227db3b..c3dc5629bd 100644 --- a/.azurepipelines/validate-source-generator-packages.ps1 +++ b/.azurepipelines/validate-source-generator-packages.ps1 @@ -91,42 +91,463 @@ function Test-PackageContents [string] $GeneratorAssembly ) - $analyzerPath = "analyzers/dotnet/cs/" + $analyzerRoot = "analyzers/dotnet/" $requiredAssemblies = @( $GeneratorAssembly, "Opc.Ua.SourceGeneration.Core.dll", - "Opc.Ua.Types.dll", - "SourceGenerator.Foundations.Contracts.dll", - "SourceGenerator.Foundations.Windows.dll" + "Opc.Ua.Types.dll" ) - foreach ($assembly in $requiredAssemblies) - { - Assert-Condition ( - $Package.Entries -contains "$analyzerPath$assembly" - ) "Package '$($Package.Id)' is missing '$analyzerPath$assembly'." - } - $dllEntries = @($Package.Entries | Where-Object { $_.EndsWith(".dll", [StringComparison]::OrdinalIgnoreCase) }) Assert-Condition ($dllEntries.Count -gt 0) "Package '$($Package.Id)' contains no assemblies." Assert-Condition ( @($dllEntries | Where-Object { -not $_.StartsWith( - $analyzerPath, + $analyzerRoot, [StringComparison]::OrdinalIgnoreCase) }).Count -eq 0 - ) "Package '$($Package.Id)' contains assemblies outside '$analyzerPath'." + ) "Package '$($Package.Id)' contains assemblies outside '$analyzerRoot'." + + # One folder per supported Roslyn API version; the .NET SDK loads the + # highest one its compiler supports and ignores folders above it. + $roslynFolders = @($dllEntries | + ForEach-Object { $_.Substring($analyzerRoot.Length).Split("/")[0] } | + Sort-Object -Unique) + Assert-Condition ( + $roslynFolders.Count -ge 2 + ) ("Package '$($Package.Id)' must ship one analyzer folder per supported Roslyn " + + "band; found: $($roslynFolders -join ', ').") + Assert-Condition ( + @($roslynFolders | Where-Object { $_ -notmatch "^roslyn[0-9]+\.[0-9]+$" }).Count -eq 0 + ) ("Package '$($Package.Id)' analyzer folders must be named 'roslyn.'; " + + "found: $($roslynFolders -join ', ').") + + foreach ($roslynFolder in $roslynFolders) + { + $analyzerPath = "$analyzerRoot$roslynFolder/cs/" + foreach ($assembly in $requiredAssemblies) + { + Assert-Condition ( + $Package.Entries -contains "$analyzerPath$assembly" + ) "Package '$($Package.Id)' is missing '$analyzerPath$assembly'." + } + } + + # Assemblies the Roslyn host supplies itself must never be shipped alongside an + # analyzer. A second copy makes the analyzer load context bind a different + # identity for types that cross Roslyn's own API surface - most visibly + # ImmutableArray - so the generator dies on its first API call with + # MissingMethodException, surfaced only as warning CS8784. + $hostProvided = @( + "Microsoft.CodeAnalysis", + "System.Collections.Immutable", + "System.Reflection.Metadata" + ) + foreach ($forbidden in $hostProvided) + { + Assert-Condition ( + @($dllEntries | Where-Object { + [IO.Path]::GetFileName($_).StartsWith( + $forbidden, + [StringComparison]::OrdinalIgnoreCase) + }).Count -eq 0 + ) ("Package '$($Package.Id)' must not ship '$forbidden*' - the Roslyn host " + + "provides it, and a second copy breaks generator initialization.") + } Assert-Condition ( @($dllEntries | Where-Object { [IO.Path]::GetFileName($_).StartsWith( - "Microsoft.CodeAnalysis", + "SourceGenerator.Foundations", [StringComparison]::OrdinalIgnoreCase) }).Count -eq 0 - ) "Package '$($Package.Id)' must not ship Microsoft.CodeAnalysis host assemblies." + ) "Package '$($Package.Id)' must not ship SourceGenerator.Foundations assemblies." Assert-Condition ( $Package.Dependencies.Count -eq 0 ) "Package '$($Package.Id)' must carry its analyzer runtime closure privately." } +function Test-AnalyzerBands +{ + <# + .SYNOPSIS + Asserts a package ships the expected analyzer assemblies in every Roslyn band. + + .DESCRIPTION + For packages that Test-PackageContents cannot check because they also ship lib/ + assemblies and carry dependencies - the migration analyzer, whose payload comes + from a hand-written nuspec rather than SourceGeneratorPack.targets. A band added + to one half of that nuspec and not the other is invisible at pack time. + #> + param( + [Parameter(Mandatory = $true)] + [PSCustomObject] $Package, + + [Parameter(Mandatory = $true)] + [string[]] $ExpectedFolders, + + [Parameter(Mandatory = $true)] + [string[]] $ExpectedAssemblies + ) + + $analyzerRoot = "analyzers/dotnet/" + foreach ($folder in $ExpectedFolders) + { + foreach ($assembly in $ExpectedAssemblies) + { + $path = "$analyzerRoot$folder/cs/$assembly" + Assert-Condition ( + $Package.Entries -contains $path + ) "Package '$($Package.Id)' is missing '$path'." + } + } + + $actualFolders = @($Package.Entries | + Where-Object { + $_.StartsWith($analyzerRoot, [StringComparison]::OrdinalIgnoreCase) -and + $_.EndsWith(".dll", [StringComparison]::OrdinalIgnoreCase) + } | + ForEach-Object { $_.Substring($analyzerRoot.Length).Split("/")[0] } | + Sort-Object -Unique) + Assert-Condition ( + @(Compare-Object $actualFolders @($ExpectedFolders | Sort-Object -Unique)).Count -eq 0 + ) ("Package '$($Package.Id)' analyzer bands are '$($actualFolders -join ', ')' but " + + "'$(($ExpectedFolders | Sort-Object -Unique) -join ', ')' was expected.") +} + +function Test-PackageBuildProps +{ + <# + .SYNOPSIS + Asserts the auto-imported MSBuild props file is named after the package id. + + .DESCRIPTION + NuGet only auto-imports `build/.props`. The model generator's props + declares every CompilerVisibleProperty / CompilerVisibleItemMetadata the generator + needs, so a name that does not track the package id (including the `.Debug` + configuration suffix) silently strips every `ModelSourceGenerator*` setting from + package consumers without any build error. + #> + param( + [Parameter(Mandatory = $true)] + [PSCustomObject] $Package, + + [switch] $RequiresProps + ) + + $expected = "build/$($Package.Id).props" + if ($RequiresProps) + { + Assert-Condition ( + $Package.Entries -contains $expected + ) ("Package '$($Package.Id)' is missing '$expected'. NuGet only auto-imports " + + "build/.props, so any other name is never imported by consumers.") + } + + $strayBuildFiles = @($Package.Entries | + Where-Object { $_.StartsWith("build/", [StringComparison]::OrdinalIgnoreCase) } | + Where-Object { $_ -ne $expected }) + Assert-Condition ( + $strayBuildFiles.Count -eq 0 + ) ("Package '$($Package.Id)' ships unexpected build/ entries that NuGet will never " + + "auto-import: $($strayBuildFiles -join ', ').") +} + +function Test-ExpectedPackageSet +{ + <# + .SYNOPSIS + Asserts the packed output matches the checked-in expected package list. + #> + param( + [Parameter(Mandatory = $true)] + [string] $ManifestPath + ) + + Assert-Condition (Test-Path $ManifestPath) "Expected package manifest '$ManifestPath' not found." + + $expected = @(Get-Content $ManifestPath | + ForEach-Object { $_.Trim() } | + Where-Object { $_ -and -not $_.StartsWith("#") } | + Sort-Object -Unique) + + $actual = @(Get-ChildItem -Path $PackageDirectory -Filter "*.nupkg" -File -Recurse | + Where-Object { $_.Name -notlike "*.snupkg" } | + ForEach-Object { $_.Name -replace "\.[0-9].*\.nupkg$", "" } | + Sort-Object -Unique) + + $missing = @($expected | Where-Object { $actual -notcontains $_ }) + $unexpected = @($actual | Where-Object { $expected -notcontains $_ }) + + Assert-Condition ($missing.Count -eq 0) ( + "Packages listed in '$ManifestPath' were not produced: $($missing -join ', '). " + + "If a package was intentionally removed or renamed, update the manifest in the " + + "same pull request.") + Assert-Condition ($unexpected.Count -eq 0) ( + "Unexpected packages were produced: $($unexpected -join ', '). If a package was " + + "intentionally added, add it to '$ManifestPath'; otherwise a build-time-only " + + "project has become packable by accident.") + + Write-Host "Package set matches the expected manifest ($($expected.Count) packages)." +} + +function Test-SourceGeneratingConsumer +{ + <# + .SYNOPSIS + Builds a standalone project that actually drives the packaged model generator. + + .DESCRIPTION + Test-CleanConsumer only proves the analyzer loads. This test proves the packaged + generator *generates*: it feeds a real NodeSet2 in as an AdditionalFile and then + references the emitted types from hand-written code, so a generator that fails to + run, or runs with the wrong options, becomes a compile error. + + The consumer deliberately does NOT import the generator's props file by path the + way the in-repo projects do. It relies on NuGet auto-importing + `build/.props`, and it pins a custom `ModelSourceGeneratorPrefix` that + cannot be derived from the NodeSet itself. If that props file is ever misnamed + again the metadata becomes invisible to the compiler, the emitted namespace falls + back to the model-derived default, and the references below stop compiling. + #> + param( + [Parameter(Mandatory = $true)] + [PSCustomObject] $Package, + + [Parameter(Mandatory = $true)] + [string] $ValidationRoot, + + [Parameter(Mandatory = $true)] + [string] $RepoRoot + ) + + $nodeSet = Join-Path $RepoRoot "samples\MinimalBoilerServer\Model\Boiler.NodeSet2.xml" + Assert-Condition (Test-Path $nodeSet) "NodeSet '$nodeSet' used by the generator consumer test not found." + + $consumerDirectory = Join-Path $ValidationRoot "SourceGenerating" + New-Item -ItemType Directory -Path $consumerDirectory | Out-Null + $projectPath = Join-Path $consumerDirectory "SourceGenerating.csproj" + $generatedRoot = "generated" + # Namespace prefix that the generator can only learn from the AdditionalFiles + # metadata, i.e. only when build/.props was auto-imported. + $prefix = "PackagedGeneratorProbe" + $escapedNodeSet = [System.Security.SecurityElement]::Escape($nodeSet) + + @" + + + net10.0 + enable + true + true + $generatedRoot + v105 + + + + + + + + http://opcfoundation.org/UA/Boiler/ + Boiler + $prefix + + + +"@ | Set-Content -Path $projectPath -Encoding utf8 + + # Referencing the generated identifier tables and a generated NodeState type is the + # assertion: without a successful generation run under the requested prefix none of + # these resolve and the consumer does not compile. + @" +using $prefix; + +namespace SourceGeneratingConsumer; + +public static class GeneratedModelProbe +{ + public static uint BoilerTypeIdentifier => ObjectTypes.BoilerType; + + public static string BoilerTypeBrowseName => BrowseNames.BoilerType; + + public static System.Type BoilerStateType => typeof(BoilerState); +} +"@ | Set-Content -Path (Join-Path $consumerDirectory "GeneratedModelProbe.cs") -Encoding utf8 + + $nugetConfig = Join-Path $ValidationRoot "NuGet.WithUpstream.Config" + $packagesPath = Join-Path $ValidationRoot "packages" + Invoke-DotNet @( + "restore", + $projectPath, + "--configfile", + $nugetConfig, + "--packages", + $packagesPath, + "--nologo" + ) + Invoke-DotNet @( + "build", + $projectPath, + "--configuration", + "Release", + "--no-restore", + "--nologo" + ) + + $generatedFiles = @(Get-ChildItem -Path (Join-Path $consumerDirectory $generatedRoot) ` + -Filter "*.cs" -File -Recurse -ErrorAction SilentlyContinue) + Assert-Condition ( + $generatedFiles.Count -gt 0 + ) ("The packaged generator produced no source for '$($Package.Id)'. The consumer " + + "compiled, but nothing was emitted under '$generatedRoot'.") + + Write-Host ( + "Packaged model generator emitted $($generatedFiles.Count) file(s) for a " + + "standalone NodeSet consumer.") +} + +function Test-DownlevelAnalyzerHost +{ + <# + .SYNOPSIS + Loads the down-level analyzer payload in a real compiler of that Roslyn band. + + .DESCRIPTION + The repository builds against the newest band, so nothing else here ever executes + the down-level payload. Every way it can be wrong is reported by the compiler as a + *warning*, which means a broken band ships silently and the consumer simply gets no + generated code: + + CS9057 - built against a newer compiler than the host, so it is skipped entirely. + CS8784 - loaded but failed to initialize, e.g. MissingMethodException because a + shipped System.Collections.Immutable bound a second ImmutableArray. + CS8032 - the analyzer instance could not be created at all. + + So run the matching csc over the packed payload and fail on any of them. Absence of + diagnostics is necessary but not sufficient - a generator that is never handed to + the compiler also produces none - so /reportanalyzer is used to additionally assert + that the generator positively executed. + #> + param( + [Parameter(Mandatory = $true)] + [PSCustomObject] $Package, + + [Parameter(Mandatory = $true)] + [string] $ValidationRoot, + + [Parameter(Mandatory = $true)] + [string] $RoslynFolder, + + [Parameter(Mandatory = $true)] + [string] $CompilerToolsetVersion, + + [Parameter(Mandatory = $true)] + [string] $GeneratorAssembly + ) + + $root = Join-Path $ValidationRoot "downlevel-$RoslynFolder-$($Package.Id)" + $analyzerDirectory = Join-Path $root "analyzers" + # Per package, and emptied first: a shared directory would leak the previous + # package's assemblies into this compilation and the assertion below would pass on + # someone else's generator. + Remove-Item -Path $analyzerDirectory -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path $analyzerDirectory -Force | Out-Null + + $archive = [IO.Compression.ZipFile]::OpenRead($Package.Path) + try + { + $prefix = "analyzers/dotnet/$RoslynFolder/cs/" + $entries = @($archive.Entries | + Where-Object { $_.FullName.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase) } | + Where-Object { $_.FullName.EndsWith(".dll", [StringComparison]::OrdinalIgnoreCase) }) + Assert-Condition ( + $entries.Count -gt 0 + ) "Package '$($Package.Id)' has no assemblies under '$prefix'." + foreach ($entry in $entries) + { + [IO.Compression.ZipFileExtensions]::ExtractToFile( + $entry, (Join-Path $analyzerDirectory ([IO.Path]::GetFileName($entry.FullName))), $true) + } + } + finally + { + $archive.Dispose() + } + + # `dotnet tool`-free way to get a specific csc: restore the toolset package. + $toolsetProject = Join-Path $root "toolset.csproj" + @" + + + net10.0 + false + + + + + +"@ | Set-Content -Path $toolsetProject -Encoding utf8 + + $packagesPath = Join-Path $ValidationRoot "packages" + Invoke-DotNet @( + "restore", + $toolsetProject, + "--configfile", + (Join-Path $ValidationRoot "NuGet.WithUpstream.Config"), + "--packages", + $packagesPath, + "--nologo" + ) + + $csc = Join-Path $packagesPath ` + "microsoft.net.compilers.toolset/$CompilerToolsetVersion/tasks/netcore/bincore/csc.dll" + Assert-Condition (Test-Path $csc) "csc from Microsoft.Net.Compilers.Toolset $CompilerToolsetVersion not found at '$csc'." + + $sourceFile = Join-Path $root "Probe.cs" + "namespace DownlevelProbe { public class Marker { } }" | Set-Content -Path $sourceFile -Encoding utf8 + + $referenceDirectory = @(Get-ChildItem -Path ( + Join-Path $env:ProgramFiles "dotnet\packs\Microsoft.NETCore.App.Ref") -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + ForEach-Object { Get-ChildItem -Path (Join-Path $_.FullName "ref") -Directory -ErrorAction SilentlyContinue } | + Select-Object -First 1) + Assert-Condition ( + $referenceDirectory.Count -eq 1 + ) "Could not locate a Microsoft.NETCore.App reference assembly directory." + + $arguments = @( + $csc, "/noconfig", "/nostdlib+", "/target:library", + "/out:$(Join-Path $root 'probe.dll')", "/reportanalyzer" + ) + $arguments += @(Get-ChildItem (Join-Path $referenceDirectory[0].FullName "*.dll") | + ForEach-Object { "/r:$($_.FullName)" }) + $arguments += @(Get-ChildItem (Join-Path $analyzerDirectory "*.dll") | + ForEach-Object { "/analyzer:$($_.FullName)" }) + $arguments += $sourceFile + + $output = & dotnet @arguments 2>&1 + $loadDiagnostics = @($output | Where-Object { $_ -match "CS9057|CS8784|CS8032|CS8034" }) + Assert-Condition ( + $loadDiagnostics.Count -eq 0 + ) ("Package '$($Package.Id)' analyzer folder '$RoslynFolder' does not load in " + + "Microsoft.Net.Compilers.Toolset $CompilerToolsetVersion. A consumer on that " + + "compiler silently gets no generated code:`n" + ($loadDiagnostics -join "`n")) + + # /reportanalyzer lists every generator the compiler actually ran, so this turns + # "nothing complained" into "the generator executed". + $ran = @($output | Where-Object { $_ -match "(^|\s)$([Regex]::Escape($GeneratorAssembly)), Version=" }) + Assert-Condition ( + $ran.Count -gt 0 + ) ("Package '$($Package.Id)' analyzer folder '$RoslynFolder' reported no execution of " + + "'$GeneratorAssembly' under Microsoft.Net.Compilers.Toolset $CompilerToolsetVersion. " + + "The payload loaded without complaint but the generator never ran:`n" + + ($output -join "`n")) + + Write-Host "Analyzer folder '$RoslynFolder' of '$($Package.Id)' runs in csc $CompilerToolsetVersion." +} + function Invoke-DotNet { param( @@ -257,14 +678,26 @@ $PackageDirectory = $resolvedPackageDirectory $repoRoot = Split-Path $PSScriptRoot -Parent $validationRoot = Join-Path (Join-Path $repoRoot "artifacts") "source-generator-consumer" Test-ConfigurationPackageIds ( - Join-Path $repoRoot "tools\Opc.Ua.SourceGeneration\Opc.Ua.SourceGeneration.csproj") + Join-Path $repoRoot "tools\Opc.Ua.SourceGeneration.Pack\Opc.Ua.SourceGeneration.Pack.csproj") Test-ConfigurationPackageIds ( - Join-Path $repoRoot "tools\Opc.Ua.SourceGeneration.Stack\Opc.Ua.SourceGeneration.Stack.csproj") + Join-Path $repoRoot "tools\Opc.Ua.SourceGeneration.Stack.Pack\Opc.Ua.SourceGeneration.Stack.Pack.csproj") $modelPackage = Get-PackageInfo "OPCFoundation.NetStandard.Opc.Ua.SourceGeneration" $stackPackage = Get-PackageInfo "OPCFoundation.NetStandard.Opc.Ua.SourceGeneration.Stack" +$migrationPackage = Get-PackageInfo "OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer" Test-PackageContents $modelPackage "Opc.Ua.SourceGeneration.dll" Test-PackageContents $stackPackage "Opc.Ua.SourceGeneration.Stack.dll" +Test-AnalyzerBands $migrationPackage @("roslyn4.14", "roslyn5.0") @( + "Opc.Ua.MigrationAnalyzer.dll", + "Opc.Ua.MigrationAnalyzer.CodeFixer.dll", + "Opc.Ua.MigrationAnalyzer.Generator.dll") + +# Only the model generator exposes MSBuild settings to consumers, so only it ships a +# build/.props; the stack generator must not smuggle in stray build/ content. +Test-PackageBuildProps $modelPackage -RequiresProps +Test-PackageBuildProps $stackPackage + +Test-ExpectedPackageSet (Join-Path $PSScriptRoot "expected-packages.txt") Remove-Item -Path $validationRoot -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $validationRoot | Out-Null @@ -289,9 +722,33 @@ try "@ | Set-Content -Path (Join-Path $validationRoot "NuGet.Config") -Encoding utf8 + # The generator packages carry their whole closure privately, so the clean consumers + # restore from the artifact directory alone - that isolation is part of what they + # assert. The source-generating consumer additionally pulls Opc.Ua.Server, whose + # Microsoft.Extensions.* graph has to come from upstream. + @" + + + + + + + + +"@ | Set-Content -Path (Join-Path $validationRoot "NuGet.WithUpstream.Config") -Encoding utf8 Test-CleanConsumer $modelPackage $validationRoot Test-CleanConsumer $stackPackage $validationRoot + Test-SourceGeneratingConsumer $modelPackage $validationRoot $repoRoot + # The repository builds against the newest band, so the down-level payload is only + # ever exercised here. Every failure mode is a compiler *warning*, so without this + # a broken band ships silently. + Test-DownlevelAnalyzerHost $modelPackage $validationRoot "roslyn4.14" "4.14.0" "Opc.Ua.SourceGeneration" + Test-DownlevelAnalyzerHost $stackPackage $validationRoot "roslyn4.14" "4.14.0" "Opc.Ua.SourceGeneration.Stack" + # The migration analyzer ships the same two bands from a hand-written nuspec rather + # than SourceGeneratorPack.targets, so its down-level payload has its own way to rot. + Test-DownlevelAnalyzerHost $migrationPackage $validationRoot "roslyn4.14" "4.14.0" ` + "Opc.Ua.MigrationAnalyzer.Generator" } finally { diff --git a/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml index d6bd941acd..7a16bd8d01 100644 --- a/.github/workflows/buildandtest.yml +++ b/.github/workflows/buildandtest.yml @@ -129,7 +129,14 @@ jobs: run: ./.azurepipelines/set-version.ps1 - name: Build - run: dotnet build ${{ env.CSPROJECT }} --framework ${{ matrix.framework }} --configuration ${{ matrix.configuration }} /p:CustomTestTarget=${{ matrix.customtesttarget }} /p:UseSharedCompilation=false -maxcpucount:1 + # Build in parallel with the shared compiler (both `dotnet build` defaults). + # These were previously pinned to `-maxcpucount:1 /p:UseSharedCompilation=false` + # to work around a race in the SourceGenerator.Foundations assembly resolver that + # surfaced as intermittent CS8784; that dependency was removed, so the generators + # now resolve through Roslyn's own analyzer loader and the workaround only cost + # build time. Do not re-add without evidence - re-loading the ~19-assembly + # analyzer closure for every project is expensive. + run: dotnet build ${{ env.CSPROJECT }} --framework ${{ matrix.framework }} --configuration ${{ matrix.configuration }} /p:CustomTestTarget=${{ matrix.customtesttarget }} - name: Test # note: /p:CollectCoverage=true is only used to disable deterministic builds @@ -270,7 +277,7 @@ jobs: $failed = @() foreach ($tfm in $tfms) { Write-Host "::group::Build UA.slnx ($tfm)" - dotnet build UA.slnx --configuration Release /p:CustomTestTarget=$tfm /p:UseSharedCompilation=false + dotnet build UA.slnx --configuration Release /p:CustomTestTarget=$tfm if ($LASTEXITCODE -ne 0) { $failed += $tfm Write-Host "::error::UA.slnx build failed for $tfm" @@ -313,7 +320,7 @@ jobs: $failed = @() foreach ($tfm in $tfms) { Write-Host "::group::Build UA.slnx ($tfm)" - dotnet build UA.slnx --configuration Release /p:CustomTestTarget=$tfm /p:UseSharedCompilation=false + dotnet build UA.slnx --configuration Release /p:CustomTestTarget=$tfm if ($LASTEXITCODE -ne 0) { $failed += $tfm Write-Host "::error::UA.slnx build failed for $tfm" diff --git a/Directory.Build.targets b/Directory.Build.targets index ff434bfa78..a7faeb541f 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -94,4 +94,35 @@ Condition="$([System.String]::Copy('%(Analyzer.Identity)').Contains('Microsoft.Gen.Logging'))" /> + + + + <_SourceGeneratorAnalyzer Include="@(Analyzer)" + Condition="'%(Analyzer.MSBuildSourceProjectFile)' != '' AND + $([System.String]::Copy('%(Analyzer.Filename)').StartsWith('Opc.Ua.SourceGeneration'))" /> + <_SourceGeneratorAnalyzerDependency + Include="%(_SourceGeneratorAnalyzer.RootDir)%(_SourceGeneratorAnalyzer.Directory)*.dll" + Exclude="@(_SourceGeneratorAnalyzer); + %(_SourceGeneratorAnalyzer.RootDir)%(_SourceGeneratorAnalyzer.Directory)Microsoft.CodeAnalysis*.dll; + %(_SourceGeneratorAnalyzer.RootDir)%(_SourceGeneratorAnalyzer.Directory)System.Collections.Immutable.dll; + %(_SourceGeneratorAnalyzer.RootDir)%(_SourceGeneratorAnalyzer.Directory)System.Reflection.Metadata.dll" /> + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 254bbc830d..10897fcf1e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,4 +1,8 @@ + + true - - + + diff --git a/UA.slnx b/UA.slnx index dfc097db28..5a2706e8e7 100644 --- a/UA.slnx +++ b/UA.slnx @@ -277,11 +277,18 @@ + + + + + + + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index e9077ec048..54cb74621f 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -20,7 +20,7 @@ The C# language version is pinned (`LangVersion` 14) and analyzer/style rules ar | `src/` | The core stack and higher-level libraries: `Opc.Ua.Types`, `Opc.Ua.Core*`, `Opc.Ua.Client`, `Opc.Ua.Server`, `Opc.Ua.Configuration`, `Opc.Ua.PubSub` (+ transports), the GDS / DI / LDS / WoT libraries, and the `Opc.Ua.Redundancy*` family. | | `samples/` | Reference and sample apps: `ConsoleReferenceServer`, `ConsoleReferenceClient`, `Quickstarts.Servers`, the `Minimal*` / `PumpDeviceIntegrationServer` NativeAOT samples, `Redundant*`, etc. | | `tests/` | Unit and integration test projects, mirroring the library structure, plus shared test frameworks. | -| `tools/` | Source generators, migration analyzers, and the installable `Opc.Ua.Mcp` tool. | +| `tools/` | Source generators, migration analyzers, and the installable `Opc.Ua.Mcp` tool. Each analyzer and generator has a build project and — for the source generators — a `*.Pack` project that packages it under a Roslyn-versioned analyzer folder. | | `docs/` | This documentation set (indexed by [docs/README.md](README.md)). | | `fuzzing/` | SharpFuzz / libFuzzer fuzz targets (see [Fuzzing.md](../fuzzing/Fuzzing.md)). | @@ -51,6 +51,8 @@ Notes: ``` - **Offline / restricted networks.** `NuGetAudit` is enabled and fails the build with `NU1900` when it cannot reach the audit service. If you build offline, pass `-p:NuGetAudit=false`. +- **Source generators are consumed as project references.** Projects that use the in-repo generators reference `tools/Opc.Ua.SourceGeneration[.Stack]` with `OutputItemType=Analyzer`. MSBuild only hands the compiler the generator assembly itself, so `Directory.Build.targets` adds the generator's runtime closure (its output directory, minus the Roslyn host assemblies) as `Analyzer` items — the same payload the generator NuGet packages ship under `analyzers/dotnet//cs`. Without it the generators cannot resolve their dependencies and fail to initialise with `CS8784`. +- **Analyzers and generators are shipped under a Roslyn-versioned analyzer folder.** `roslyn.props` pins the Roslyn API version, and each generator has a `*.Pack` project that ships it under `analyzers/dotnet/roslyn./cs`. See [Repository layout](#repository-layout) and the [support matrix](#supported-analyzer-and-source-generator-hosts). Because the repository's own projects consume that same build, **building this repository requires a Roslyn 5.x host** (the .NET 10 SDK or Visual Studio 2026). ## Running tests @@ -216,6 +218,8 @@ The following NuGet packages are released on a monthly cadence (with hot fixes f For improved source-level debugging, symbol packages are published on nuget.org in `snupkg` format, and `Debug`-compiled packages are available with a `.Debug` suffix. In addition, every successful `master` build publishes preview packages to the [Azure DevOps preview feed](https://opcfoundation.visualstudio.com/opcua-netstandard/_artifacts/feed/opcua-preview). +The full set of packages the preview pipeline produces is pinned in [`.azurepipelines/expected-packages.txt`](../.azurepipelines/expected-packages.txt). `.azurepipelines/validate-source-generator-packages.ps1` fails the build when the packed output does not match it, so adding, removing or renaming a shipped package has to be done deliberately in the same pull request. That script also validates the analyzer packages: their `analyzers/dotnet/roslyn./cs` layout, that they carry their runtime closure privately, that the model generator's auto-imported `build/.props` is named after the package id, and — end to end — that a standalone project consuming the packed generator with a NodeSet actually gets code generated. + ### Supported target frameworks The class libraries currently target: @@ -230,6 +234,21 @@ The class libraries currently target: To keep pull-request CI fast, only (4) and (6) are part of the qualifying build; the other platforms are covered by scheduled or manual CI. See [Running tests](#running-tests) for how to build and test a specific framework locally with `CustomTestTarget` / `tests/customtest.bat`. +### Supported analyzer and source generator hosts + +The analyzer and source generator packages ship under `analyzers/dotnet/roslyn./cs`. The .NET SDK loads the highest folder its compiler supports and **ignores** folders above it, so an older host cleanly skips the analyzer instead of loading it and failing at generator-initialization time. + +| Roslyn API | Package folder | Minimum host | +| --- | --- | --- | +| 4.14 | `analyzers/dotnet/roslyn4.14/cs` | Visual Studio 2022 17.14 / .NET 9 SDK | +| 5.0 | `analyzers/dotnet/roslyn5.0/cs` | Visual Studio 2026 18.0 / .NET 10 SDK | + +The version is declared once in `roslyn.props`. + +> **Adding a band below 4.14 is not just another entry in that file.** The analyzer closure — the generator, `Opc.Ua.SourceGeneration.Core` **and** `Opc.Ua.Types` — must bind against the Roslyn host's own `System.Collections.Immutable` and `System.Reflection.Metadata`. .NET satisfies a reference from a *higher* assembly version but never from a lower one, and those assemblies are supplied by the compiler, so the closure must reference the lowest version across every supported band and must never ship a copy of its own. Roslyn 4.14 and 5.0 both depend on 9.0.0, which is why `$(RoslynRuntimeVersion)` in `roslyn.props` drives the central pin and one build of the non-Roslyn closure serves both bands. Going lower — Roslyn 4.8 wants 7.x — would mean building that whole closure, `Opc.Ua.Types` included, a second time. +> +> Get it wrong and the failure is silent: the generator is skipped (`CS9057`), fails to load (`CS8032`) or throws `MissingMethodException` while initializing (`CS8784`) — all *warnings*, so the consumer just gets no generated code. `validate-source-generator-packages.ps1` therefore refuses any package that ships `Microsoft.CodeAnalysis*`, `System.Collections.Immutable` or `System.Reflection.Metadata`, and runs the packed down-level payload through a real compiler of that band. + ### Versioning From **2.0** onward, package versions are produced by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) (nbgv) from the `version.json` file at the repository root. That file holds the base version (currently `2.0-preview`) and requests [SemVer 2.0](https://semver.org/) package versions (`nugetPackageVersion.semVer: 2`); nbgv derives the version height, prerelease tag, and build metadata from the git history, and `version.props` maps the computed values onto the assembly and package version properties. Stable (public-release) versions are produced only on the `main`, `master`, `develop/*`, and `release/` branches — every other branch yields a prerelease build. diff --git a/docs/migrate/2.0.x/packages.md b/docs/migrate/2.0.x/packages.md index e7a52fda50..2807a660d3 100644 --- a/docs/migrate/2.0.x/packages.md +++ b/docs/migrate/2.0.x/packages.md @@ -42,7 +42,6 @@ The minimum SDK is the **.NET 10 SDK**, and projects compile with **`LangVersion | `Microsoft.Extensions.Options.ConfigurationExtensions` 10.0.8 | Added (pinned) | Centralised pin | | `ModelContextProtocol` 1.3.0 | Added | `tools/Opc.Ua.Mcp/Opc.Ua.Mcp.csproj` | | `ModelContextProtocol.AspNetCore` 1.3.0 | Added | `tools/Opc.Ua.Mcp/Opc.Ua.Mcp.csproj` | -| `SourceGenerator.Foundations` 2.0.14 | Added | `tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.csproj` | | `System.CommandLine` 2.0.8 | Added | `tools/Opc.Ua.Mcp/Opc.Ua.Mcp.csproj` | | `System.Threading.Channels` 10.0.8 | Added | `src/Opc.Ua.Lds.Server/Opc.Ua.Lds.Server.csproj` | | `TUnit` 1.45.8 | Added (test-only) | `tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj` | diff --git a/roslyn.props b/roslyn.props new file mode 100644 index 0000000000..05e1dbfa17 --- /dev/null +++ b/roslyn.props @@ -0,0 +1,57 @@ + + + + + 5.0.0 + + 4.14.0 + + 9.0.0 + + + roslyn$(RoslynApiVersionVS2026.Substring(0, $(RoslynApiVersionVS2026.LastIndexOf('.')))) + roslyn$(RoslynApiVersionVS2022.Substring(0, $(RoslynApiVersionVS2022.LastIndexOf('.')))) + + diff --git a/samples/ConsoleReferenceServer/Dockerfile b/samples/ConsoleReferenceServer/Dockerfile index f7912932e7..a6d3d2c7da 100644 --- a/samples/ConsoleReferenceServer/Dockerfile +++ b/samples/ConsoleReferenceServer/Dockerfile @@ -7,9 +7,14 @@ WORKDIR /src # copy csproj and restore as distinct layers COPY *.* . COPY ["samples/ConsoleReferenceServer/ConsoleReferenceServer.csproj", "samples/ConsoleReferenceServer/"] +# The generator projects import shared MSBuild logic; restore fails with +# "Invalid framework identifier ''" if these are not part of the restore layer. +COPY ["tools/SourceGeneratorVariant.targets", "tools/"] COPY ["tools/Opc.Ua.SourceGeneration.Core/Opc.Ua.SourceGeneration.Core.csproj", "tools/Opc.Ua.SourceGeneration.Core/"] COPY ["tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.csproj", "tools/Opc.Ua.SourceGeneration.Stack/"] +COPY ["tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.Sources.targets", "tools/Opc.Ua.SourceGeneration.Stack/"] COPY ["tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.csproj", "tools/Opc.Ua.SourceGeneration/"] +COPY ["tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.Sources.targets", "tools/Opc.Ua.SourceGeneration/"] COPY ["src/Opc.Ua.Core.Types/Opc.Ua.Core.Types.csproj", "src/Opc.Ua.Core.Types/"] COPY ["src/Opc.Ua.Core/Opc.Ua.Core.csproj", "src/Opc.Ua.Core/"] COPY ["src/Opc.Ua.Core.Schema/Opc.Ua.Core.Schema.csproj", "src/Opc.Ua.Core.Schema/"] diff --git a/tests/Opc.Ua.Core.Schema.Tests/Opc.Ua.Core.Schema.Tests.csproj b/tests/Opc.Ua.Core.Schema.Tests/Opc.Ua.Core.Schema.Tests.csproj index 8f6d6521b7..026b1fe8e2 100644 --- a/tests/Opc.Ua.Core.Schema.Tests/Opc.Ua.Core.Schema.Tests.csproj +++ b/tests/Opc.Ua.Core.Schema.Tests/Opc.Ua.Core.Schema.Tests.csproj @@ -24,6 +24,10 @@ + + diff --git a/tests/Opc.Ua.MigrationAnalyzer.Tests/Generators/MigrationGeneratorTests.cs b/tests/Opc.Ua.MigrationAnalyzer.Tests/Generators/MigrationGeneratorTests.cs index 0a0dbb27f5..2fdcdfca31 100644 --- a/tests/Opc.Ua.MigrationAnalyzer.Tests/Generators/MigrationGeneratorTests.cs +++ b/tests/Opc.Ua.MigrationAnalyzer.Tests/Generators/MigrationGeneratorTests.cs @@ -107,7 +107,13 @@ private static GeneratorDriverRunResult Run(string userSource, string? extraSour OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); - GeneratorDriver driver = CSharpGeneratorDriver.Create(new MigrationGenerator()); + // Hand the driver the same parse options as the input trees; the + // default language version tracks the Roslyn API version, so an + // implicit default makes the generated trees inconsistent with the + // compilation as soon as the SDK moves to a newer C#. + GeneratorDriver driver = CSharpGeneratorDriver.Create( + [new MigrationGenerator().AsSourceGenerator()], + parseOptions: parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation( compilation, out _, diff --git a/tests/Opc.Ua.PubSub.Schema.Tests/Opc.Ua.PubSub.Schema.Tests.csproj b/tests/Opc.Ua.PubSub.Schema.Tests/Opc.Ua.PubSub.Schema.Tests.csproj index 20bb9e9b52..3b38ef4d95 100644 --- a/tests/Opc.Ua.PubSub.Schema.Tests/Opc.Ua.PubSub.Schema.Tests.csproj +++ b/tests/Opc.Ua.PubSub.Schema.Tests/Opc.Ua.PubSub.Schema.Tests.csproj @@ -11,6 +11,10 @@ + + diff --git a/tests/Opc.Ua.SourceGeneration.Stack.Tests/SourceGeneratorDiagnosticTests.cs b/tests/Opc.Ua.SourceGeneration.Stack.Tests/SourceGeneratorDiagnosticTests.cs index d1a88fef02..0f6a86eac2 100644 --- a/tests/Opc.Ua.SourceGeneration.Stack.Tests/SourceGeneratorDiagnosticTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Stack.Tests/SourceGeneratorDiagnosticTests.cs @@ -27,6 +27,7 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ +using System; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; using NUnit.Framework; @@ -145,5 +146,25 @@ public void SourceGeneratorNameIsStackSourceGenerator() { Assert.That(SourceGenerator.Name, Is.EqualTo(nameof(StackSourceGenerator))); } + + [Test] + public void GuardRunsTheActionWhenItDoesNotThrow() + { + bool invoked = false; + + SourceGenerator.Guard(default, () => invoked = true); + + Assert.That(invoked, Is.True); + } + + [Test] + public void GuardRethrowsCancellationSoRoslynCanAbortTheRun() + { + Assert.That( + () => SourceGenerator.Guard( + default, + () => throw new OperationCanceledException()), + Throws.InstanceOf()); + } } } diff --git a/tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGenerationAssemblyTests.cs b/tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGenerationAssemblyTests.cs index 388078163a..48e8fd8663 100644 --- a/tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGenerationAssemblyTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGenerationAssemblyTests.cs @@ -46,7 +46,6 @@ public class StackGenerationAssemblyTests public void GeneratorWithUnsupportedAssemblyNameReportsError() { var generator = new StackSourceGenerator(); - var host = new StackSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Debug .CreateCompilation("UnknownAssembly") @@ -54,7 +53,7 @@ public void GeneratorWithUnsupportedAssemblyNameReportsError() new Dictionary().WithOpcUaCoreStubs(), LanguageVersion.Latest); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host); + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); driver = driver.RunGeneratorsAndUpdateCompilation( compilation, out Compilation outputCompilation, @@ -72,7 +71,6 @@ public void GeneratorWithUnsupportedAssemblyNameReportsError() public void GeneratorWithOpcUaCoreTypesAssemblyNameProducesModels() { var generator = new StackSourceGenerator(); - var host = new StackSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Debug .CreateCompilation("Opc.Ua.Core.Types") @@ -80,7 +78,7 @@ public void GeneratorWithOpcUaCoreTypesAssemblyNameProducesModels() new Dictionary().WithOpcUaCoreStubs(), LanguageVersion.Latest); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host); + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); driver = driver.RunGeneratorsAndUpdateCompilation( compilation, out _, @@ -97,7 +95,6 @@ public void GeneratorWithOpcUaCoreTypesAssemblyNameProducesModels() public void GeneratorWithOpcUaCoreAssemblyNameProducesStack() { var generator = new StackSourceGenerator(); - var host = new StackSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Debug .CreateCompilation("Opc.Ua.Core") @@ -105,7 +102,7 @@ public void GeneratorWithOpcUaCoreAssemblyNameProducesStack() new Dictionary().WithOpcUaCoreStubs(), LanguageVersion.Latest); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host); + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); driver = driver.RunGeneratorsAndUpdateCompilation( compilation, out _, @@ -122,7 +119,6 @@ public void GeneratorWithOpcUaCoreAssemblyNameProducesStack() public void GeneratorWithOldLanguageVersionReportsError() { var generator = new StackSourceGenerator(); - var host = new StackSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Debug .CreateCompilation("Opc.Ua.Test") @@ -130,7 +126,7 @@ public void GeneratorWithOldLanguageVersionReportsError() new Dictionary().WithOpcUaCoreStubs(), LanguageVersion.CSharp12); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host); + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); driver = driver.RunGeneratorsAndUpdateCompilation( compilation, out Compilation outputCompilation, @@ -149,7 +145,6 @@ public void GeneratorWithOldLanguageVersionReportsError() public void GeneratorWithReleaseOptimizationProducesOutput() { var generator = new StackSourceGenerator(); - var host = new StackSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Release .CreateCompilation("Opc.Ua.Test") @@ -157,7 +152,7 @@ public void GeneratorWithReleaseOptimizationProducesOutput() new Dictionary().WithOpcUaCoreStubs(), LanguageVersion.Latest); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host); + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); driver = driver.RunGeneratorsAndUpdateCompilation( compilation, out _, diff --git a/tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGeneratorTests.cs b/tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGeneratorTests.cs index a23d644115..a72abd76e8 100644 --- a/tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGeneratorTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGeneratorTests.cs @@ -53,7 +53,6 @@ public class StackGeneratorTests public void GenerateAndCompileTest(OptimizationLevel optimizationLevel) { var generator = new StackSourceGenerator(); - var host = new StackSourceGeneratorHoist(generator); CSharpCompilation compilation = optimizationLevel.CreateCompilation("Opc.Ua.Test") .AddCode( @@ -62,7 +61,7 @@ public void GenerateAndCompileTest(OptimizationLevel optimizationLevel) LanguageVersion.Latest); // Create the driver the executes the generator - GeneratorDriver driver = CSharpGeneratorDriver.Create(host); + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); // Run it driver = driver.RunGeneratorsAndUpdateCompilation( compilation, diff --git a/tests/Opc.Ua.SourceGeneration.Tests/ModelDependencyScannerTests.cs b/tests/Opc.Ua.SourceGeneration.Tests/ModelDependencyScannerTests.cs index 445cf94e5c..cb8ee75fac 100644 --- a/tests/Opc.Ua.SourceGeneration.Tests/ModelDependencyScannerTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Tests/ModelDependencyScannerTests.cs @@ -136,7 +136,6 @@ public void ScanIgnoresAttributesWithEmptyUriOrPrefix() public void EmittedAssemblyContainsModelDependencyAttribute() { var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation() .AddCode(new Dictionary().WithOpcUaGeneratedStack(), @@ -149,7 +148,7 @@ public void EmittedAssemblyContainsModelDependencyAttribute() ["build_property.ModelSourceGeneratorExclude"] = "Draft" }); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(LanguageVersion.CSharp11)) @@ -194,7 +193,6 @@ public void OverrideResolutionSilentlySkipsLocalGeneration() }, LanguageVersion.CSharp11); var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation() .AddReferences(producer.ToMetadataReference()) @@ -208,7 +206,7 @@ public void OverrideResolutionSilentlySkipsLocalGeneration() ["build_property.ModelSourceGeneratorExclude"] = "Draft" }); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(LanguageVersion.CSharp11)) diff --git a/tests/Opc.Ua.SourceGeneration.Tests/ModelGeneratorTests.cs b/tests/Opc.Ua.SourceGeneration.Tests/ModelGeneratorTests.cs index f105ded249..f3b802b35e 100644 --- a/tests/Opc.Ua.SourceGeneration.Tests/ModelGeneratorTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Tests/ModelGeneratorTests.cs @@ -64,7 +64,6 @@ public void GenerateAndCompileDemoModelXmlTest( OptimizationLevel optimizationLevel) { var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = optimizationLevel.CreateCompilation() .AddCode(new Dictionary().WithOpcUaGeneratedStack(), languageVersion); @@ -85,7 +84,7 @@ public void GenerateAndCompileDemoModelXmlTest( }); // Create the driver the executes the generator - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(languageVersion)) @@ -102,7 +101,6 @@ public void GenerateAndCompileDemoModelNodeSetsTest( OptimizationLevel optimizationLevel) { var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = optimizationLevel.CreateCompilation() .AddCode(new Dictionary().WithOpcUaGeneratedStack(), languageVersion); @@ -119,7 +117,7 @@ public void GenerateAndCompileDemoModelNodeSetsTest( }); // Create the driver the executes the generator - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(languageVersion)) @@ -144,7 +142,6 @@ public void GenerateAndCompileIsa95JobControlNodeSet2Test( LanguageVersion languageVersion) { var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation() .AddCode(new Dictionary().WithOpcUaGeneratedStack(), languageVersion); @@ -156,7 +153,7 @@ public void GenerateAndCompileIsa95JobControlNodeSet2Test( ["build_property.ModelSourceGeneratorOmitEventRecords"] = "true" }); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(languageVersion)) @@ -235,7 +232,6 @@ public void GenerateAndCompileTestDataDesignTest( LanguageVersion languageVersion) { var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation() .AddCode(new Dictionary().WithOpcUaGeneratedStack(), languageVersion); @@ -254,7 +250,7 @@ public void GenerateAndCompileTestDataDesignTest( }); // Create the driver that executes the generator - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(languageVersion)) @@ -293,7 +289,6 @@ public void GenerateAndCompileModelDesignReferencingNodeSet2TypesTest( // generation fails with MODELGEN003 ("The TypeDefinition reference // for node Widget1 is not the expected type: ObjectTypeDesign."). var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation() .AddCode(new Dictionary().WithOpcUaGeneratedStack(), languageVersion); @@ -325,7 +320,7 @@ public void GenerateAndCompileModelDesignReferencingNodeSet2TypesTest( }; // Create the driver that executes the generator - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(languageVersion)) @@ -358,7 +353,6 @@ public void GenerateAndCompileModelDesignReferencingNodeSet2TypesReversedInputOr LanguageVersion languageVersion) { var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation() .AddCode(new Dictionary().WithOpcUaGeneratedStack(), languageVersion); @@ -384,7 +378,7 @@ public void GenerateAndCompileModelDesignReferencingNodeSet2TypesReversedInputOr "http://test.org/UA/CrossModel/Instances" }; - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(languageVersion)) @@ -555,7 +549,6 @@ private static (ImmutableArray Diagnostics, GeneratorDriverRunResult RunMixedModelGenerator(LanguageVersion languageVersion, string bindingSource) { var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Release.CreateCompilation() .AddCode(new Dictionary @@ -583,7 +576,7 @@ private static (ImmutableArray Diagnostics, GeneratorDriverRunResult "http://test.org/UA/CrossModel/Instances" }; - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(languageVersion)) diff --git a/tests/Opc.Ua.SourceGeneration.Tests/NodesetEventRecordTests.cs b/tests/Opc.Ua.SourceGeneration.Tests/NodesetEventRecordTests.cs index 85c198f692..0593017746 100644 --- a/tests/Opc.Ua.SourceGeneration.Tests/NodesetEventRecordTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Tests/NodesetEventRecordTests.cs @@ -67,8 +67,7 @@ public void NodeSetEventTypeGeneratesRecordAndEventFilter() "Opc.Ua.TestEvent" }; var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions( new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) diff --git a/tests/Opc.Ua.SourceGeneration.Tests/NodesetIdentifierSidecarTests.cs b/tests/Opc.Ua.SourceGeneration.Tests/NodesetIdentifierSidecarTests.cs index 4275fdcbbe..f7d08a42b6 100644 --- a/tests/Opc.Ua.SourceGeneration.Tests/NodesetIdentifierSidecarTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Tests/NodesetIdentifierSidecarTests.cs @@ -174,8 +174,7 @@ private static ImmutableArray Run( } var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions( new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) diff --git a/tests/Opc.Ua.SourceGeneration.Tests/NodesetMethodArgumentTests.cs b/tests/Opc.Ua.SourceGeneration.Tests/NodesetMethodArgumentTests.cs index a6773410a0..ecbdd189c5 100644 --- a/tests/Opc.Ua.SourceGeneration.Tests/NodesetMethodArgumentTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Tests/NodesetMethodArgumentTests.cs @@ -59,8 +59,7 @@ public void NodeSetMethodArgumentPropertiesGenerateTypedAsyncHandlerAndResult() ["build_property.ModelSourceGeneratorOmitFluentApi"] = "true" }); var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions( new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) diff --git a/tests/Opc.Ua.SourceGeneration.Tests/TypeGeneratorTests.cs b/tests/Opc.Ua.SourceGeneration.Tests/TypeGeneratorTests.cs index 0f658bc136..b8dc7874e0 100644 --- a/tests/Opc.Ua.SourceGeneration.Tests/TypeGeneratorTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Tests/TypeGeneratorTests.cs @@ -478,7 +478,6 @@ public partial class SecondType } }"; var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpParseOptions parseOptions = new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(LanguageVersion.CSharp13); @@ -492,7 +491,7 @@ public partial class SecondType .WithOpcUaGeneratedStack(), LanguageVersion.CSharp13); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(parseOptions); driver = driver.RunGeneratorsAndUpdateCompilation( @@ -566,7 +565,6 @@ private static GeneratorRunResult RunGenerator( bool expectWarnings = false) { var generator = new ModelSourceGenerator(); - var host = new ModelSourceGeneratorHoist(generator); CSharpCompilation compilation = OptimizationLevel.Release .CreateCompilation() @@ -576,7 +574,7 @@ private static GeneratorRunResult RunGenerator( .WithOpcUaGeneratedStack(), LanguageVersion.Preview); - GeneratorDriver driver = CSharpGeneratorDriver.Create(host) + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator) .WithUpdatedParseOptions(new CSharpParseOptions() .WithKind(SourceCodeKind.Regular) .WithLanguageVersion(LanguageVersion.Preview)); diff --git a/tools/MigrationAnalyzer.slnx b/tools/MigrationAnalyzer.slnx index 993bd3adfa..a57b812439 100644 --- a/tools/MigrationAnalyzer.slnx +++ b/tools/MigrationAnalyzer.slnx @@ -3,14 +3,21 @@ + + + + + + + diff --git a/tools/MigrationAnalyzerVariant.targets b/tools/MigrationAnalyzerVariant.targets new file mode 100644 index 0000000000..29415bcf12 --- /dev/null +++ b/tools/MigrationAnalyzerVariant.targets @@ -0,0 +1,26 @@ + + + + netstandard2.0 + false + true + false + + + + + + + + diff --git a/tools/Opc.Ua.MigrationAnalyzer.CodeFixer.Roslyn4_14/Opc.Ua.MigrationAnalyzer.CodeFixer.Roslyn4_14.csproj b/tools/Opc.Ua.MigrationAnalyzer.CodeFixer.Roslyn4_14/Opc.Ua.MigrationAnalyzer.CodeFixer.Roslyn4_14.csproj new file mode 100644 index 0000000000..a6a0d5624f --- /dev/null +++ b/tools/Opc.Ua.MigrationAnalyzer.CodeFixer.Roslyn4_14/Opc.Ua.MigrationAnalyzer.CodeFixer.Roslyn4_14.csproj @@ -0,0 +1,12 @@ + + + + + $(RoslynApiVersionVS2022) + false + + + \ No newline at end of file diff --git a/tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.Sources.targets b/tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.Sources.targets new file mode 100644 index 0000000000..4868ba3de1 --- /dev/null +++ b/tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.Sources.targets @@ -0,0 +1,51 @@ + + + + $(AssemblyPrefix).MigrationAnalyzer.CodeFixer + Opc.Ua.MigrationAnalyzer.CodeFixer + OPC UA .NET Standard migration code-fix providers (1.5.378 to 2.0). Companion to Opc.Ua.MigrationAnalyzer analyzers. + $(MSBuildThisFileDirectory) + $(NoWarn);RS1007;RS1038;RS2008 + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.csproj b/tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.csproj index 44bd41af1c..8e48372885 100644 --- a/tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.csproj +++ b/tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.csproj @@ -1,48 +1,17 @@ + + + - netstandard2.0 - false - true - $(AssemblyPrefix).MigrationAnalyzer.CodeFixer - Opc.Ua.MigrationAnalyzer.CodeFixer - OPC UA .NET Standard migration code-fix providers (1.5.378 to 2.0). Companion to Opc.Ua.MigrationAnalyzer analyzers. - - $(NoWarn);RS1007;RS1038;RS2008 + $(RoslynApiVersionVS2026) false - - - - - - - - - - - - - - - - - - - + - diff --git a/tools/Opc.Ua.MigrationAnalyzer.Generator.Roslyn4_14/Opc.Ua.MigrationAnalyzer.Generator.Roslyn4_14.csproj b/tools/Opc.Ua.MigrationAnalyzer.Generator.Roslyn4_14/Opc.Ua.MigrationAnalyzer.Generator.Roslyn4_14.csproj new file mode 100644 index 0000000000..1e80ebe9ce --- /dev/null +++ b/tools/Opc.Ua.MigrationAnalyzer.Generator.Roslyn4_14/Opc.Ua.MigrationAnalyzer.Generator.Roslyn4_14.csproj @@ -0,0 +1,12 @@ + + + + + $(RoslynApiVersionVS2022) + false + + + \ No newline at end of file diff --git a/tools/Opc.Ua.MigrationAnalyzer.Generator/Opc.Ua.MigrationAnalyzer.Generator.Sources.targets b/tools/Opc.Ua.MigrationAnalyzer.Generator/Opc.Ua.MigrationAnalyzer.Generator.Sources.targets new file mode 100644 index 0000000000..0456de770c --- /dev/null +++ b/tools/Opc.Ua.MigrationAnalyzer.Generator/Opc.Ua.MigrationAnalyzer.Generator.Sources.targets @@ -0,0 +1,25 @@ + + + + $(AssemblyPrefix).MigrationAnalyzer.Generator + Opc.Ua.MigrationAnalyzer.Generator + OPC UA .NET Standard migration source generator (1.5.378 to 2.0). Emits internal [Obsolete] shim subclasses of List<T> for any <Type>Collection name (including model-compiled user types) the consumer references but that 2.0 has removed, so the consumer code keeps compiling while UA0002 guides the eventual rewrite to List<T> / ArrayOf<T>. + $(MSBuildThisFileDirectory) + true + + $(NoWarn);RS1007;RS1038;RS2008 + + + + + + + + + diff --git a/tools/Opc.Ua.MigrationAnalyzer.Generator/Opc.Ua.MigrationAnalyzer.Generator.csproj b/tools/Opc.Ua.MigrationAnalyzer.Generator/Opc.Ua.MigrationAnalyzer.Generator.csproj index 7c13ae7632..42c700f70d 100644 --- a/tools/Opc.Ua.MigrationAnalyzer.Generator/Opc.Ua.MigrationAnalyzer.Generator.csproj +++ b/tools/Opc.Ua.MigrationAnalyzer.Generator/Opc.Ua.MigrationAnalyzer.Generator.csproj @@ -1,27 +1,16 @@ + + + - netstandard2.0 - false - true - true - $(AssemblyPrefix).MigrationAnalyzer.Generator - Opc.Ua.MigrationAnalyzer.Generator - OPC UA .NET Standard migration source generator (1.5.378 to 2.0). Emits internal [Obsolete] shim subclasses of List<T> for any <Type>Collection name (including model-compiled user types) the consumer references but that 2.0 has removed, so the consumer code keeps compiling while UA0002 guides the eventual rewrite to List<T> / ArrayOf<T>. - - $(NoWarn);RS1007;RS1038;RS2008 + $(RoslynApiVersionVS2026) false - - - - - - - - - - + diff --git a/tools/Opc.Ua.MigrationAnalyzer.Roslyn4_14/Opc.Ua.MigrationAnalyzer.Roslyn4_14.csproj b/tools/Opc.Ua.MigrationAnalyzer.Roslyn4_14/Opc.Ua.MigrationAnalyzer.Roslyn4_14.csproj new file mode 100644 index 0000000000..8166158df3 --- /dev/null +++ b/tools/Opc.Ua.MigrationAnalyzer.Roslyn4_14/Opc.Ua.MigrationAnalyzer.Roslyn4_14.csproj @@ -0,0 +1,12 @@ + + + + + $(RoslynApiVersionVS2022) + false + + + \ No newline at end of file diff --git a/tools/Opc.Ua.MigrationAnalyzer/NugetREADME.md b/tools/Opc.Ua.MigrationAnalyzer/NugetREADME.md index c3886bef10..577ec62525 100644 --- a/tools/Opc.Ua.MigrationAnalyzer/NugetREADME.md +++ b/tools/Opc.Ua.MigrationAnalyzer/NugetREADME.md @@ -137,21 +137,32 @@ whole block once the MigrationAnalyzer package is removed. ## Packaging note -The package ships **two analyzer DLLs** under `analyzers/dotnet/cs/`: - -- `Opc.Ua.MigrationAnalyzer.dll` — the analyzer assembly. Targets `Microsoft.CodeAnalysis 4.x` - (the stable analyzer API) and references **only** `Microsoft.CodeAnalysis.CSharp` so it - loads cleanly in csc.exe's analyzer host (which ships only `Microsoft.CodeAnalysis.dll` - + `CSharp.dll`, not `Workspaces`). All `DiagnosticAnalyzer` types live here. +The package ships **three Roslyn component DLLs** under +`analyzers/dotnet/roslyn./cs/`. The .NET SDK loads that folder when its +compiler supports the Roslyn API and ignores it otherwise, so an older host cleanly +skips the analyzer rather than failing to load it: + +| Roslyn API | Minimum host | +| --- | --- | +| 4.14 | Visual Studio 2022 17.14 / .NET 9 SDK | +| 5.0 | Visual Studio 2026 18.0 / .NET 10 SDK | + +- `Opc.Ua.MigrationAnalyzer.dll` — the analyzer assembly. References **only** + `Microsoft.CodeAnalysis.CSharp` so it loads cleanly in csc.exe's analyzer host + (which ships only `Microsoft.CodeAnalysis.dll` + `CSharp.dll`, not `Workspaces`). + All `DiagnosticAnalyzer` types live here. - `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` — the code-fix assembly. References `Microsoft.CodeAnalysis.CSharp.Workspaces` and hosts all `CodeFixProvider` types. Loaded only by Workspaces-aware hosts (Visual Studio / `dotnet format`). - -This split is necessary because shipping a single DLL that references `Workspaces` -silently fails to load in csc.exe at command-line build time — csc loads the assembly -but JIT-resolution of `Workspaces` types fails (DLL not in bincore), and the analyzer -host swallows the load failure, producing zero diagnostics. Splitting keeps the -analyzer host happy while preserving full IDE/`dotnet format` code-fix functionality. +- `Opc.Ua.MigrationAnalyzer.Generator.dll` — the source generator that emits the + `Collection` shims. + +The analyzer / code-fix split is necessary because shipping a single DLL that +references `Workspaces` silently fails to load in csc.exe at command-line build time — +csc loads the assembly but JIT-resolution of `Workspaces` types fails (DLL not in +bincore), and the analyzer host swallows the load failure, producing zero diagnostics. +Splitting keeps the analyzer host happy while preserving full IDE/`dotnet format` +code-fix functionality. `RS1038` (suggesting separation) is the Roslyn rule that recommends this layout; it is satisfied implicitly by the two-DLL design. diff --git a/tools/Opc.Ua.MigrationAnalyzer/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props b/tools/Opc.Ua.MigrationAnalyzer/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props index 6f51f036f5..d13a983288 100644 --- a/tools/Opc.Ua.MigrationAnalyzer/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props +++ b/tools/Opc.Ua.MigrationAnalyzer/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props @@ -1,19 +1,15 @@ - - - - - diff --git a/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.Sources.targets b/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.Sources.targets new file mode 100644 index 0000000000..ba12644fb1 --- /dev/null +++ b/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.Sources.targets @@ -0,0 +1,35 @@ + + + + $(AssemblyPrefix).MigrationAnalyzer + Opc.Ua.MigrationAnalyzer + OPC UA .NET Standard migration analyzers and code fixers (1.5.378 to 2.0). + $(MSBuildThisFileDirectory) + true + + $(NoWarn);RS1007;RS1038 + + + + + + + + + + + + + + + diff --git a/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.csproj b/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.csproj index 63e778e2d8..f2452bfbed 100644 --- a/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.csproj +++ b/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.csproj @@ -1,16 +1,14 @@ + + + - netstandard2.0 - false - true - $(AssemblyPrefix).MigrationAnalyzer - Opc.Ua.MigrationAnalyzer - OPC UA .NET Standard migration analyzers and code fixers (1.5.378 to 2.0). - - $(NoWarn);RS1007;RS1038 + $(RoslynApiVersionVS2026) - - - - - - - - - - - - - + + + + + + + + @@ -109,11 +99,7 @@ Targets="Build" Properties="Configuration=$(Configuration)" RemoveProperties="TargetFramework" /> - - @@ -122,7 +108,17 @@ inside GetBuildVersion). Static evaluation in a PropertyGroup is too early. --> - version=$(PackageVersion);configuration=$(Configuration);repoRoot=$(MSBuildThisFileDirectory)..\..;analyzerDll=$(MSBuildThisFileDirectory)bin\$(Configuration)\netstandard2.0\Opc.Ua.MigrationAnalyzer.dll;codeFixesDll=$(MSBuildThisFileDirectory)..\Opc.Ua.MigrationAnalyzer.CodeFixer\bin\$(Configuration)\netstandard2.0\Opc.Ua.MigrationAnalyzer.CodeFixer.dll;generatorDll=$(MSBuildThisFileDirectory)..\Opc.Ua.MigrationAnalyzer.Generator\bin\$(Configuration)\netstandard2.0\Opc.Ua.MigrationAnalyzer.Generator.dll;shimBin=$(MSBuildThisFileDirectory)..\Opc.Ua.MigrationAnalyzer.Core\bin\$(Configuration);readme=$(MSBuildThisFileDirectory)NugetREADME.md;propsFile=$(MSBuildThisFileDirectory)OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props + <_MigrationAnalyzerToolsDir>$(MSBuildThisFileDirectory).. + <_MigrationAnalyzerBin50>$(MSBuildThisFileDirectory)bin\$(Configuration)\netstandard2.0 + <_MigrationAnalyzerBin414>$(_MigrationAnalyzerToolsDir)\Opc.Ua.MigrationAnalyzer.Roslyn4_14\bin\$(Configuration)\netstandard2.0 + <_MigrationCodeFixerBin50>$(_MigrationAnalyzerToolsDir)\Opc.Ua.MigrationAnalyzer.CodeFixer\bin\$(Configuration)\netstandard2.0 + <_MigrationCodeFixerBin414>$(_MigrationAnalyzerToolsDir)\Opc.Ua.MigrationAnalyzer.CodeFixer.Roslyn4_14\bin\$(Configuration)\netstandard2.0 + <_MigrationGeneratorBin50>$(_MigrationAnalyzerToolsDir)\Opc.Ua.MigrationAnalyzer.Generator\bin\$(Configuration)\netstandard2.0 + <_MigrationGeneratorBin414>$(_MigrationAnalyzerToolsDir)\Opc.Ua.MigrationAnalyzer.Generator.Roslyn4_14\bin\$(Configuration)\netstandard2.0 + + + version=$(PackageVersion);configuration=$(Configuration);repoRoot=$(MSBuildThisFileDirectory)..\..;roslynFolderVS2026=$(RoslynAnalyzerFolderVS2026);roslynFolderVS2022=$(RoslynAnalyzerFolderVS2022);analyzerDllVS2026=$(_MigrationAnalyzerBin50)\Opc.Ua.MigrationAnalyzer.dll;analyzerDllVS2022=$(_MigrationAnalyzerBin414)\Opc.Ua.MigrationAnalyzer.dll;codeFixesDllVS2026=$(_MigrationCodeFixerBin50)\Opc.Ua.MigrationAnalyzer.CodeFixer.dll;codeFixesDllVS2022=$(_MigrationCodeFixerBin414)\Opc.Ua.MigrationAnalyzer.CodeFixer.dll;generatorDllVS2026=$(_MigrationGeneratorBin50)\Opc.Ua.MigrationAnalyzer.Generator.dll;generatorDllVS2022=$(_MigrationGeneratorBin414)\Opc.Ua.MigrationAnalyzer.Generator.dll;shimBin=$(MSBuildThisFileDirectory)..\Opc.Ua.MigrationAnalyzer.Core\bin\$(Configuration);readme=$(MSBuildThisFileDirectory)NugetREADME.md;propsFile=$(MSBuildThisFileDirectory)OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props + diff --git a/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.nuspec b/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.nuspec index c41dc7ffa1..f291f5994c 100644 --- a/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.nuspec +++ b/tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.nuspec @@ -68,9 +68,12 @@ - - - + + + + + + diff --git a/tools/Opc.Ua.SourceGeneration/NugetREADME.md b/tools/Opc.Ua.SourceGeneration.Pack/NugetREADME.md similarity index 79% rename from tools/Opc.Ua.SourceGeneration/NugetREADME.md rename to tools/Opc.Ua.SourceGeneration.Pack/NugetREADME.md index c2ab0103c0..0dce563bf6 100644 --- a/tools/Opc.Ua.SourceGeneration/NugetREADME.md +++ b/tools/Opc.Ua.SourceGeneration.Pack/NugetREADME.md @@ -14,6 +14,17 @@ the generator to emit C# proxies at build time. The generator participates in the standard `dotnet build` pipeline; no separate tool invocation is required. +## Supported hosts + +The package ships the generator under a Roslyn-versioned analyzer folder. +The .NET SDK loads it when its compiler supports that Roslyn API and +ignores it otherwise, so an older host cleanly skips the generator. + +| Roslyn API | Minimum host | +| --- | --- | +| 4.14 | Visual Studio 2022 17.14 / .NET 9 SDK | +| 5.0 | Visual Studio 2026 18.0 / .NET 10 SDK | + ## Getting started Reference the generator as an **analyzer** (no runtime dependency): diff --git a/tools/Opc.Ua.SourceGeneration.Pack/Opc.Ua.SourceGeneration.Pack.csproj b/tools/Opc.Ua.SourceGeneration.Pack/Opc.Ua.SourceGeneration.Pack.csproj new file mode 100644 index 0000000000..d9322e51a1 --- /dev/null +++ b/tools/Opc.Ua.SourceGeneration.Pack/Opc.Ua.SourceGeneration.Pack.csproj @@ -0,0 +1,30 @@ + + + + + + netstandard2.0 + $(PackagePrefix).Opc.Ua.SourceGeneration + $(AssemblyPrefix).SourceGeneration.Pack + Opc.Ua.SourceGeneration + OPC UA model source generator + + + + + + + + + + + diff --git a/tools/Opc.Ua.SourceGeneration.Roslyn4_14/Opc.Ua.SourceGeneration.Roslyn4_14.csproj b/tools/Opc.Ua.SourceGeneration.Roslyn4_14/Opc.Ua.SourceGeneration.Roslyn4_14.csproj new file mode 100644 index 0000000000..86796a3123 --- /dev/null +++ b/tools/Opc.Ua.SourceGeneration.Roslyn4_14/Opc.Ua.SourceGeneration.Roslyn4_14.csproj @@ -0,0 +1,14 @@ + + + + + $(RoslynApiVersionVS2022) + + + diff --git a/tools/Opc.Ua.SourceGeneration.Stack/NugetREADME.md b/tools/Opc.Ua.SourceGeneration.Stack.Pack/NugetREADME.md similarity index 74% rename from tools/Opc.Ua.SourceGeneration.Stack/NugetREADME.md rename to tools/Opc.Ua.SourceGeneration.Stack.Pack/NugetREADME.md index 0a64455d34..6a2528b114 100644 --- a/tools/Opc.Ua.SourceGeneration.Stack/NugetREADME.md +++ b/tools/Opc.Ua.SourceGeneration.Stack.Pack/NugetREADME.md @@ -19,6 +19,17 @@ just reference `Opc.Ua.Core` and `Opc.Ua.Types`). `netstandard2.0` (Roslyn analyzer host TFM). +## Supported hosts + +The package ships the generator under a Roslyn-versioned analyzer folder. +The .NET SDK loads it when its compiler supports that Roslyn API and +ignores it otherwise, so an older host cleanly skips the generator. + +| Roslyn API | Minimum host | +| --- | --- | +| 4.14 | Visual Studio 2022 17.14 / .NET 9 SDK | +| 5.0 | Visual Studio 2026 18.0 / .NET 10 SDK | + ## Additional documentation See the [main repository README](https://github.com/OPCFoundation/UA-.NETStandard) diff --git a/tools/Opc.Ua.SourceGeneration.Stack.Pack/Opc.Ua.SourceGeneration.Stack.Pack.csproj b/tools/Opc.Ua.SourceGeneration.Stack.Pack/Opc.Ua.SourceGeneration.Stack.Pack.csproj new file mode 100644 index 0000000000..6104f988ff --- /dev/null +++ b/tools/Opc.Ua.SourceGeneration.Stack.Pack/Opc.Ua.SourceGeneration.Stack.Pack.csproj @@ -0,0 +1,23 @@ + + + + + + netstandard2.0 + $(PackagePrefix).Opc.Ua.SourceGeneration.Stack + $(AssemblyPrefix).SourceGeneration.Stack.Pack + Opc.Ua.SourceGeneration + OPC UA stack source generator + + + + + + + diff --git a/tools/Opc.Ua.SourceGeneration.Stack.Roslyn4_14/Opc.Ua.SourceGeneration.Stack.Roslyn4_14.csproj b/tools/Opc.Ua.SourceGeneration.Stack.Roslyn4_14/Opc.Ua.SourceGeneration.Stack.Roslyn4_14.csproj new file mode 100644 index 0000000000..92b0845500 --- /dev/null +++ b/tools/Opc.Ua.SourceGeneration.Stack.Roslyn4_14/Opc.Ua.SourceGeneration.Stack.Roslyn4_14.csproj @@ -0,0 +1,12 @@ + + + + + $(RoslynApiVersionVS2022) + + + diff --git a/tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.Sources.targets b/tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.Sources.targets new file mode 100644 index 0000000000..bee4f5f867 --- /dev/null +++ b/tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.Sources.targets @@ -0,0 +1,28 @@ + + + + $(AssemblyPrefix).SourceGeneration.Stack + $(MSBuildThisFileDirectory) + + + + + + + + + + + + + + + + diff --git a/tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.csproj b/tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.csproj index bd7ef460a7..a9c3b51b40 100644 --- a/tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.csproj +++ b/tools/Opc.Ua.SourceGeneration.Stack/Opc.Ua.SourceGeneration.Stack.csproj @@ -1,63 +1,16 @@ + + + - netstandard2.0 - $(AssemblyPrefix).SourceGeneration.Stack - Opc.Ua.SourceGeneration - OPC UA source generation Library - - $(NoWarn);CS8002 + $(RoslynApiVersionVS2026) - - - $(PackagePrefix).Opc.Ua.SourceGeneration.Stack - true - true - NugetREADME.md - MIT - - true - true - - - $(PackageId).Debug - - - true - true - false - - - - - - - - - - - - - all - Analyzer - - - - - - - - all - - - - - - - - - - - + diff --git a/tools/Opc.Ua.SourceGeneration.Stack/SourceGenerator.cs b/tools/Opc.Ua.SourceGeneration.Stack/SourceGenerator.cs index 1384242879..2898538377 100644 --- a/tools/Opc.Ua.SourceGeneration.Stack/SourceGenerator.cs +++ b/tools/Opc.Ua.SourceGeneration.Stack/SourceGenerator.cs @@ -32,7 +32,7 @@ namespace Opc.Ua.SourceGeneration { - internal static class SourceGenerator + internal static partial class SourceGenerator { /// /// The namespace prefix for generated code diff --git a/tools/Opc.Ua.SourceGeneration.Stack/StackGeneration.cs b/tools/Opc.Ua.SourceGeneration.Stack/StackGeneration.cs index bff8f178db..254df4dc8f 100644 --- a/tools/Opc.Ua.SourceGeneration.Stack/StackGeneration.cs +++ b/tools/Opc.Ua.SourceGeneration.Stack/StackGeneration.cs @@ -33,8 +33,6 @@ using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using ILogger = SGF.Diagnostics.ILogger; -using SourceProductionContext = SGF.SgfSourceProductionContext; namespace Opc.Ua.SourceGeneration { @@ -49,13 +47,12 @@ internal sealed class StackGeneration public StackGeneration( SourceProductionContext context, CompilationOptions compilationOptions, - StackGenerationOptions options, - ILogger logger) + StackGenerationOptions options) { m_context = context; m_options = options; m_compilationOptions = compilationOptions; - m_telemetry = SourceGeneratorTelemetry.Create(logger, m_context); + m_telemetry = SourceGeneratorTelemetry.Create(m_context); } /// @@ -116,7 +113,7 @@ public void Emit(CancellationToken cancellationToken) SourceGenerator.Exception, Location.None, ex.Message, - ex.StackTrace)); + ex.ToString())); } } @@ -126,7 +123,7 @@ public void Emit(CancellationToken cancellationToken) /// private bool CheckCompilationOptions(out StackGenerationType type) { - if (m_compilationOptions.LanguageVersion < LanguageVersion.CSharp13) + if (!m_compilationOptions.IsCSharp13OrLater) { type = StackGenerationType.None; m_context.ReportDiagnostic( diff --git a/tools/Opc.Ua.SourceGeneration.Stack/StackSourceGenerator.cs b/tools/Opc.Ua.SourceGeneration.Stack/StackSourceGenerator.cs index a52ab4f3aa..7f7063e69f 100644 --- a/tools/Opc.Ua.SourceGeneration.Stack/StackSourceGenerator.cs +++ b/tools/Opc.Ua.SourceGeneration.Stack/StackSourceGenerator.cs @@ -28,30 +28,20 @@ * ======================================================================*/ using Microsoft.CodeAnalysis; -using IIncrementalGenerator = SGF.IncrementalGenerator; -using IncrementalGeneratorAttribute = SGF.IncrementalGeneratorAttribute; -using IncrementalGeneratorInitializationContext = SGF.SgfInitializationContext; namespace Opc.Ua.SourceGeneration { /// /// Generates server and client models using the model generator library /// - [IncrementalGenerator] - public class StackSourceGenerator : IIncrementalGenerator + [Generator(LanguageNames.CSharp)] + public sealed class StackSourceGenerator : IIncrementalGenerator { /// - public StackSourceGenerator() - : base(SourceGenerator.Name) + public void Initialize(IncrementalGeneratorInitializationContext context) { - } + SourceGenerator.AttachDebuggerIfRequested(); - /// - public override void OnInitialize(IncrementalGeneratorInitializationContext context) - { -#if DEBUGX - AttachDebugger(); -#endif // Collect options IncrementalValueProvider options = context.AnalyzerConfigOptionsProvider @@ -62,11 +52,12 @@ public override void OnInitialize(IncrementalGeneratorInitializationContext cont context.RegisterSourceOutput( settings.Combine(options), - (context, combination) => new StackGeneration( + static (context, combination) => SourceGenerator.Guard( context, - combination.Left, - combination.Right, - Logger).Emit(context.CancellationToken)); + () => new StackGeneration( + context, + combination.Left, + combination.Right).Emit(context.CancellationToken))); } } } diff --git a/tools/Opc.Ua.SourceGeneration/CompilationOptions.cs b/tools/Opc.Ua.SourceGeneration/CompilationOptions.cs index 2e6266978e..b841453922 100644 --- a/tools/Opc.Ua.SourceGeneration/CompilationOptions.cs +++ b/tools/Opc.Ua.SourceGeneration/CompilationOptions.cs @@ -42,6 +42,14 @@ internal record struct CompilationOptions( Platform Platform, OutputKind OutputKind) { + /// + /// True when the compilation targets C# 13 or newer. The underlying + /// value is compared because the LanguageVersion.CSharp13 member + /// only exists in Roslyn 4.12 and later, while the generator is also + /// built against the Visual Studio 2022 baseline. + /// + public readonly bool IsCSharp13OrLater => (int)LanguageVersion >= 1300; + /// /// Create options from a compilation. /// diff --git a/tools/Opc.Ua.SourceGeneration/DataTypeCompilation.cs b/tools/Opc.Ua.SourceGeneration/DataTypeCompilation.cs index e6981a5df5..52107df508 100644 --- a/tools/Opc.Ua.SourceGeneration/DataTypeCompilation.cs +++ b/tools/Opc.Ua.SourceGeneration/DataTypeCompilation.cs @@ -36,7 +36,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using SourceProductionContext = SGF.SgfSourceProductionContext; namespace Opc.Ua.SourceGeneration { diff --git a/tools/Opc.Ua.SourceGeneration/ModelCompilation.cs b/tools/Opc.Ua.SourceGeneration/ModelCompilation.cs index aaf6d6c891..f975f200ee 100644 --- a/tools/Opc.Ua.SourceGeneration/ModelCompilation.cs +++ b/tools/Opc.Ua.SourceGeneration/ModelCompilation.cs @@ -36,8 +36,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Opc.Ua.SourceGeneration.Dependency; -using ILogger = SGF.Diagnostics.ILogger; -using SourceProductionContext = SGF.SgfSourceProductionContext; namespace Opc.Ua.SourceGeneration { @@ -63,8 +61,7 @@ public ModelCompilation( CompilationOptions compilationOptions, ImmutableArray referencedModels, ImmutableArray nodeManagerBindings, - ImmutableHashSet availableStateTypeNames, - ILogger logger) + ImmutableHashSet availableStateTypeNames) { m_context = context; m_input = inputFiles; @@ -75,7 +72,7 @@ public ModelCompilation( m_nodeManagerBindings = nodeManagerBindings; m_referencedModels = referencedModels; m_availableStateTypeNames = availableStateTypeNames; - m_telemetry = SourceGeneratorTelemetry.Create(logger, m_context); + m_telemetry = SourceGeneratorTelemetry.Create(m_context); } /// @@ -277,7 +274,7 @@ [.. m_identifierFiles.Select(i => i.Path)], SourceGenerator.Exception, Location.None, ex.Message, - ex.StackTrace)); + ex.ToString())); } } diff --git a/tools/Opc.Ua.SourceGeneration/ModelSourceGenerator.cs b/tools/Opc.Ua.SourceGeneration/ModelSourceGenerator.cs index fed1637afd..8dc806b7fa 100644 --- a/tools/Opc.Ua.SourceGeneration/ModelSourceGenerator.cs +++ b/tools/Opc.Ua.SourceGeneration/ModelSourceGenerator.cs @@ -29,30 +29,19 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using IIncrementalGenerator = SGF.IncrementalGenerator; -using IncrementalGeneratorAttribute = SGF.IncrementalGeneratorAttribute; -using IncrementalGeneratorInitializationContext = SGF.SgfInitializationContext; namespace Opc.Ua.SourceGeneration { /// /// Generates server and client models using the model generator library /// - [IncrementalGenerator] - public class ModelSourceGenerator : IIncrementalGenerator + [Generator(LanguageNames.CSharp)] + public sealed class ModelSourceGenerator : IIncrementalGenerator { /// - public ModelSourceGenerator() - : base(SourceGenerator.Name) + public void Initialize(IncrementalGeneratorInitializationContext context) { - } - - /// - public override void OnInitialize(IncrementalGeneratorInitializationContext context) - { -#if DEBUGX - AttachDebugger(); -#endif + SourceGenerator.AttachDebuggerIfRequested(); IncrementalValueProvider> inputFiles = context.AdditionalTextsProvider .Where(f => f.IsDesignOrNodeset2File()) @@ -155,17 +144,18 @@ public override void OnInitialize(IncrementalGeneratorInitializationContext cont context.RegisterSourceOutput( modelCompilationInput, - (context, input) => new ModelCompilation( + static (context, input) => SourceGenerator.Guard( context, - input.InputFiles, - input.CsvFiles, - input.IdentifierFiles, - input.Options, - input.CompilationOptions, - input.ReferencedModels, - input.NodeManagerBindings, - input.AvailableStateTypeNames, - Logger).Emit(context.CancellationToken)); + () => new ModelCompilation( + context, + input.InputFiles, + input.CsvFiles, + input.IdentifierFiles, + input.Options, + input.CompilationOptions, + input.ReferencedModels, + input.NodeManagerBindings, + input.AvailableStateTypeNames).Emit(context.CancellationToken))); IncrementalValueProvider publicDataTypeExtensions = context.AnalyzerConfigOptionsProvider @@ -179,8 +169,10 @@ public override void OnInitialize(IncrementalGeneratorInitializationContext cont .Where(static m => m is not null) .Collect() .Combine(publicDataTypeExtensions), - static (spc, pair) => DataTypeCompilation.EmitBatch( - spc, pair.Left, pair.Right)); + static (spc, pair) => SourceGenerator.Guard( + spc, + () => DataTypeCompilation.EmitBatch( + spc, pair.Left, pair.Right))); } private readonly record struct ModelCompilationInput( diff --git a/tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.Sources.targets b/tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.Sources.targets new file mode 100644 index 0000000000..fb15bc8732 --- /dev/null +++ b/tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.Sources.targets @@ -0,0 +1,22 @@ + + + + $(AssemblyPrefix).SourceGeneration + $(MSBuildThisFileDirectory) + + + + + + + + + + diff --git a/tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.csproj b/tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.csproj index 4a1eda9a27..8178b24299 100644 --- a/tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.csproj +++ b/tools/Opc.Ua.SourceGeneration/Opc.Ua.SourceGeneration.csproj @@ -1,61 +1,15 @@ + + + - netstandard2.0 - $(AssemblyPrefix).SourceGeneration - Opc.Ua.SourceGeneration - OPC UA source generation Library - - $(NoWarn);CS8002 + $(RoslynApiVersionVS2026) - - - $(PackagePrefix).Opc.Ua.SourceGeneration - true - true - NugetREADME.md - MIT - - true - true - - - $(PackageId).Debug - - - true - true - false - - - - - - - - - - - - - all - Analyzer - - - - - - - - all - - - - - - - - - + diff --git a/tools/Opc.Ua.SourceGeneration/SourceGenerator.cs b/tools/Opc.Ua.SourceGeneration/SourceGenerator.cs index 9502f48c1c..3e919aaeaa 100644 --- a/tools/Opc.Ua.SourceGeneration/SourceGenerator.cs +++ b/tools/Opc.Ua.SourceGeneration/SourceGenerator.cs @@ -32,7 +32,7 @@ namespace Opc.Ua.SourceGeneration { - internal static class SourceGenerator + internal static partial class SourceGenerator { /// /// The namespace prefix for generated code diff --git a/tools/Opc.Ua.SourceGeneration/SourceGeneratorHelpers.cs b/tools/Opc.Ua.SourceGeneration/SourceGeneratorHelpers.cs new file mode 100644 index 0000000000..c5ec888f37 --- /dev/null +++ b/tools/Opc.Ua.SourceGeneration/SourceGeneratorHelpers.cs @@ -0,0 +1,87 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Diagnostics; +using Microsoft.CodeAnalysis; + +namespace Opc.Ua.SourceGeneration +{ + /// + /// Shared helpers for the source generator entry points. The file is + /// linked into every generator assembly, each of which supplies its own + /// diagnostic descriptors in the other half of the partial class. + /// + internal static partial class SourceGenerator + { + /// + /// Runs a source output callback and converts an unhandled exception + /// into a diagnostic. Roslyn fails the whole compilation with a bare + /// stack trace when a generator throws, so every callback funnels + /// through here to report a well-formed diagnostic instead. + /// + public static void Guard(SourceProductionContext context, Action action) + { + try + { + action(); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + // ex.ToString() rather than ex.StackTrace: it carries the exception + // type and the inner-exception chain, and is never null - a bare + // StackTrace is null for an exception that never left its throw site. + context.ReportDiagnostic( + Diagnostic.Create( + Exception, + Location.None, + ex.Message, + ex.ToString())); + } + } + + /// + /// Attaches a debugger to the compiler process hosting the generator. + /// Compiled away unless the generator is built with the DEBUGX + /// constant defined. + /// + [Conditional("DEBUGX")] + public static void AttachDebuggerIfRequested() + { + if (!Debugger.IsAttached) + { + Debugger.Launch(); + } + } + } +} diff --git a/tools/Opc.Ua.SourceGeneration/SourceGeneratorPackaging.targets b/tools/Opc.Ua.SourceGeneration/SourceGeneratorPackaging.targets deleted file mode 100644 index 9002af118b..0000000000 --- a/tools/Opc.Ua.SourceGeneration/SourceGeneratorPackaging.targets +++ /dev/null @@ -1,38 +0,0 @@ - - - false - - false - true - true - - $(TargetsForTfmSpecificContentInPackage);IncludeSourceGeneratorRuntimeAssets - - - Compile;CopyFilesToOutputDirectory;ResolveReferences - - - - - <_SourceGeneratorRuntimeAsset Include="@(ReferenceCopyLocalPaths)" - Condition="'$(NoBuild)' != 'true' AND - '%(ReferenceCopyLocalPaths.Extension)' == '.dll' AND - !$([System.String]::Copy('%(Filename)').StartsWith('Microsoft.CodeAnalysis')) AND - '%(ReferenceCopyLocalPaths.NuGetPackageId)' != 'Microsoft.CodeAnalysis.Common' AND - '%(ReferenceCopyLocalPaths.NuGetPackageId)' != 'Microsoft.CodeAnalysis.CSharp' AND - '%(ReferenceCopyLocalPaths.NuGetPackageId)' != 'Microsoft.CodeAnalysis.Workspaces.Common' AND - '%(ReferenceCopyLocalPaths.NuGetPackageId)' != 'Microsoft.CodeAnalysis.CSharp.Workspaces'" /> - - <_SourceGeneratorRuntimeAsset Include="$(TargetDir)*.dll" - Exclude="$(TargetPath);$(TargetDir)Microsoft.CodeAnalysis*.dll" - Condition="'$(NoBuild)' == 'true'" /> - - analyzers/dotnet/cs/$(TargetFileName) - - - analyzers/dotnet/cs/%(_SourceGeneratorRuntimeAsset.Filename)%(_SourceGeneratorRuntimeAsset.Extension) - - - - diff --git a/tools/Opc.Ua.SourceGeneration/SourceGeneratorTelemetry.cs b/tools/Opc.Ua.SourceGeneration/SourceGeneratorTelemetry.cs index 0f02a53684..1f0952a17f 100644 --- a/tools/Opc.Ua.SourceGeneration/SourceGeneratorTelemetry.cs +++ b/tools/Opc.Ua.SourceGeneration/SourceGeneratorTelemetry.cs @@ -31,9 +31,6 @@ using System.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; -using ExternalLogLevel = SGF.Diagnostics.LogLevel; -using IExternalLogger = SGF.Diagnostics.ILogger; -using SourceProductionContext = SGF.SgfSourceProductionContext; namespace Opc.Ua.SourceGeneration { @@ -60,17 +57,16 @@ public void Dispose() } /// - /// Create telemetry context + /// Create telemetry context. Warnings and errors raised by the + /// generator libraries surface as compiler diagnostics; anything + /// below is dropped because a generator has no build output sink. /// - /// - public static SourceGeneratorTelemetry Create( - IExternalLogger logger, - SourceProductionContext context) + public static SourceGeneratorTelemetry Create(SourceProductionContext context) { ILoggerFactory factory = null; try { - factory = new LoggerFactoryAdapter(logger, context); + factory = new LoggerFactoryAdapter(context); var result = new SourceGeneratorTelemetry(factory); factory = null; return result; @@ -89,13 +85,9 @@ private sealed class LoggerFactoryAdapter : ILoggerFactory, ILoggerProvider /// /// Create adapter /// - /// /// - public LoggerFactoryAdapter( - IExternalLogger logger, - SourceProductionContext context) + public LoggerFactoryAdapter(SourceProductionContext context) { - m_logger = logger; m_context = context; } @@ -107,7 +99,7 @@ public void AddProvider(ILoggerProvider provider) /// public ILogger CreateLogger(string categoryName) { - return new LoggerAdapter(categoryName, m_context, m_logger); + return new LoggerAdapter(categoryName, m_context); } /// @@ -115,7 +107,6 @@ public void Dispose() { } - private readonly IExternalLogger m_logger; private readonly SourceProductionContext m_context; } @@ -129,12 +120,10 @@ private sealed class LoggerAdapter : ILogger, IDisposable /// public LoggerAdapter( string categoryName, - SourceProductionContext context, - IExternalLogger logger) + SourceProductionContext context) { m_categoryName = categoryName; m_context = context; - m_logger = logger; } /// @@ -146,9 +135,6 @@ public void Dispose() public bool IsEnabled(LogLevel logLevel) { return logLevel > LogLevel.Information; -#if UNUSED - return m_logger.IsEnabled(GetLogLevel(logLevel)); -#endif } /// @@ -175,13 +161,11 @@ public void Log( return; } - message = $"[{m_categoryName}] {message}"; - m_logger.Log( - GetLogLevel(logLevel), - exception, - message); - - Debug.WriteLine(message); + // Anything without a descriptor only reaches a debugger-attached + // host, so keep the full exception rather than just its message. + Debug.WriteLine(exception is null + ? $"[{m_categoryName}] {message}" + : $"[{m_categoryName}] {message}\n{exception}"); } /// @@ -190,20 +174,7 @@ public IDisposable BeginScope(TState state) where TState : notnull return this; } - private static ExternalLogLevel GetLogLevel(LogLevel logLevel) - { - return logLevel switch - { - LogLevel.Trace or LogLevel.Debug => ExternalLogLevel.Debug, - LogLevel.Information => ExternalLogLevel.Information, - LogLevel.Warning => ExternalLogLevel.Warning, - LogLevel.Error or LogLevel.Critical => ExternalLogLevel.Error, - _ => ExternalLogLevel.Information - }; - } - private readonly string m_categoryName; - private readonly IExternalLogger m_logger; private readonly SourceProductionContext m_context; } diff --git a/tools/SourceGeneration.slnx b/tools/SourceGeneration.slnx index cf7a1ca639..3560284329 100644 --- a/tools/SourceGeneration.slnx +++ b/tools/SourceGeneration.slnx @@ -3,7 +3,11 @@ + + + + diff --git a/tools/SourceGeneratorPack.targets b/tools/SourceGeneratorPack.targets new file mode 100644 index 0000000000..bf33b187f9 --- /dev/null +++ b/tools/SourceGeneratorPack.targets @@ -0,0 +1,101 @@ + + + + netstandard2.0 + true + false + + false + true + true + true + false + MIT + + + NugetREADME.md + false + false + false + + false + false + + true + + $(TargetsForTfmSpecificContentInPackage);IncludeSourceGeneratorAnalyzerAssets + + + + $(PackageId).Debug + + + + false + all + + + + + + + + <_SourceGeneratorVariantTargetFramework>netstandard2.0 + <_SourceGeneratorVariantBin>bin\$(Configuration)\$(_SourceGeneratorVariantTargetFramework) + + + <_SourceGeneratorVariantAssembly + Include="%(SourceGeneratorVariant.RootDir)%(SourceGeneratorVariant.Directory)$(_SourceGeneratorVariantBin)\*.dll" + Exclude="%(SourceGeneratorVariant.RootDir)%(SourceGeneratorVariant.Directory)$(_SourceGeneratorVariantBin)\Microsoft.CodeAnalysis*.dll; + %(SourceGeneratorVariant.RootDir)%(SourceGeneratorVariant.Directory)$(_SourceGeneratorVariantBin)\System.Collections.Immutable.dll; + %(SourceGeneratorVariant.RootDir)%(SourceGeneratorVariant.Directory)$(_SourceGeneratorVariantBin)\System.Reflection.Metadata.dll"> + %(SourceGeneratorVariant.RoslynAnalyzerFolder) + + + analyzers/dotnet/%(_SourceGeneratorVariantAssembly.RoslynAnalyzerFolder)/cs/%(_SourceGeneratorVariantAssembly.Filename)%(_SourceGeneratorVariantAssembly.Extension) + + + + + <_SourceGeneratorProducedFolders>@(_SourceGeneratorVariantAssembly->'%(RoslynAnalyzerFolder)'->Distinct(), ';') + + + <_SourceGeneratorVariantWithoutAssets Include="@(SourceGeneratorVariant)" + Condition="!$(_SourceGeneratorProducedFolders.Contains('%(SourceGeneratorVariant.RoslynAnalyzerFolder)'))" /> + + + + diff --git a/tools/SourceGeneratorVariant.targets b/tools/SourceGeneratorVariant.targets new file mode 100644 index 0000000000..5b2019e383 --- /dev/null +++ b/tools/SourceGeneratorVariant.targets @@ -0,0 +1,40 @@ + + + + netstandard2.0 + Opc.Ua.SourceGeneration + OPC UA source generation Library + true + true + + false + false + true + true + + + + + + + + + + all + + +