diff --git a/.github/agents/opcua-v20-migration.agent.md b/.github/agents/opcua-v20-migration.agent.md
index ab1dc07f31..7d6cdd68f2 100644
--- a/.github/agents/opcua-v20-migration.agent.md
+++ b/.github/agents/opcua-v20-migration.agent.md
@@ -9,13 +9,111 @@ You are an expert migration agent for upgrading OPC UA .NET Standard application
## Strategy
-1. **Build first**: Run `dotnet build` to identify all errors.
-2. **Categorize errors**: Group errors by type (struct nullability, collection types, Variant/object, encoder/decoder, etc.).
-3. **Fix in order**: Apply fixes in the priority order defined below — some fixes resolve cascading errors.
-4. **Rebuild and iterate**: After each batch of fixes, rebuild to verify progress and catch new errors.
-5. **Do not suppress warnings**: Fix [Obsolete] warnings properly using the replacement API, do not add `#pragma warning disable`.
+**Start with the MigrationAnalyzer NuGet package** — it ships an analyzer + code-fix set (UA0001–UA0022) and a compatibility shim DLL that together handle most mechanical migrations automatically. Only fall back to the manual rules below for patterns the analyzers do not cover or that require structural redesign.
-## Priority Order for Fixes
+1. **Install the migration package**: Add the `OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` NuGet to every project that references an `OPCFoundation.NetStandard.Opc.Ua.*` package, as a build-only dependency:
+
+ ```xml
+
+ ```
+
+ The package bundles two payloads:
+ - **Analyzer + code-fix DLLs** (`Opc.Ua.MigrationAnalyzer.dll`, `Opc.Ua.MigrationAnalyzer.CodeFixer.dll`) loaded into csc.exe and the IDE.
+ - **Compatibility shim** (`Opc.Ua.MigrationAnalyzer.Core.dll`) that re-exposes the 1.5.378 obsolete extension surface so 1.5.378-style call sites compile against 2.0 with warnings instead of errors.
+
+2. **Bump the OPC UA package versions to `2.0.*-*`** in every consumer project. Do NOT remove existing `OPCFoundation.NetStandard.Opc.Ua.*` references — just update their `Version` attribute.
+
+3. **Run `dotnet restore` then `dotnet build`**. The shim usually gets the project to compile; what remains are `[Obsolete]` warnings (CS0618) and `UA0001`–`UA0022` analyzer diagnostics that point at the patterns still using the old surface.
+
+4. **Apply analyzer auto-fixes**: in Visual Studio, hover each `UA00xx` diagnostic and apply the offered Quick Fix. From the command line, run:
+
+ ```bash
+ dotnet format analyzers .sln \
+ --diagnostics UA0002 UA0003 UA0004 UA0005 UA0006 UA0007 UA0008 \
+ UA0009 UA0010 UA0012 UA0014 UA0019 UA0020 UA0022 \
+ --severity warn
+ ```
+
+ The 14 listed rules ship batch code-fixes. The remaining `UA0001`/`UA0011`/`UA0015`/`UA0018`/`UA0021` are diagnostic-only — they require manual judgement.
+
+5. **Walk the remaining manual patterns** — anything the analyzers did not flag is covered by the categorical rules below.
+
+6. **Remove the MigrationAnalyzer package** once the project is warning-free. You are then on clean 2.0 with no shim dependency.
+
+7. **Do not suppress `[Obsolete]` or `UA00xx` warnings.** Fix them using the documented replacement; obsolete API will be removed in the next minor release.
+
+## What the MigrationAnalyzer package covers
+
+| ID | Default | Auto-fix | Replaces |
+| ------ | -------- | -------- | --------------------------------------------------------------------------------------- |
+| UA0001 | Info | No | `Utils.Trace` / `Utils.LogX` — manually inject `ILogger` via `ITelemetryContext` |
+| UA0002 | Warning | Yes | Removed `Collection` wrappers → `ArrayOf` / `List` |
+| UA0003 | Warning | Yes | `x == null` on now-struct built-in types → `x.IsNull` |
+| UA0004 | Warning | Yes | `?.` on now-struct built-in types → direct access |
+| UA0005 | Warning | Yes | `byte[]` where `ByteString` is expected → `.ToByteString()` |
+| UA0006 | Warning | Yes | `new Variant(object\|DateTime\|Guid\|byte[])` → `Variant.From(...)` |
+| UA0007 | Warning | Yes | `new NodeId(string)` / `new ExpandedNodeId(string)` → `Parse(...)` |
+| UA0008 | Warning | Yes | `Session.Call(..., params object[])` → wrap args with `Variant.From` |
+| UA0009 | Warning | Yes | `[DataContract]`/`[DataMember]` on config extensions → `[DataType]`/`[DataField]` |
+| UA0010 | Warning | Yes | `using`/`Dispose` on `CertificateIdentifier`/`UserIdentity`/`IUserIdentityTokenHandler` |
+| UA0011 | Info | No | Sync `IUserIdentityTokenHandler.Encrypt/Decrypt/Sign/Verify` → await `*Async` |
+| UA0012 | Warning | Yes | `CertificateFactory.*` static helpers → instance methods |
+| UA0014 | Warning | Yes | `DataValue.IsGood(dv)` static helper → `dv.IsGood` |
+| UA0015 | Info | No | Sync / APM members on GDS / LDS clients → await `*Async` |
+| UA0018 | Info | No | `CertificateIdentifier.Certificate` getter → `LoadCertificate2Async` |
+| UA0019 | Warning | Yes | `new DataValue(StatusCode[, ts])` → `new DataValue { StatusCode = ..., ... }` |
+| UA0020 | Warning | Yes | `EncodeableFactory.GlobalFactory` / `Create()` → `ServiceMessageContext.Factory` / `Fork()` |
+| UA0021 | Info | No | `CertificateValidator` / `CertificateValidationEventArgs` (structural rename — see §X) |
+| UA0022 | Warning | Yes | `ApplicationConfiguration.CertificateValidator` / `ServerBase.CertificateValidator` → `.CertificateManager` |
+
+## What the shim covers
+
+`Opc.Ua.MigrationAnalyzer.Core.dll` re-exposes the 1.5.378 surface as C# 14 `extension` members so 1.5.378 call sites continue to compile:
+
+- **Moved obsolete extensions**: `NodeId` / `Variant` / `DataValue` null-check helpers, `Session` sync helpers, `Subscription` sync helpers, `ApplicationInstance` helpers, `ServerBase.Start` / `Stop`, `TransportChannel` APM, `ChannelBase` static factory methods, and similar surface.
+- **New shims for genuinely-removed members**: `EncodeableFactory.GlobalFactory`, `CertificateIdentifier.Certificate` (throws), sync wrappers for `IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}`, sync + APM wrappers for GDS / LDS client APIs.
+
+## What the shim does NOT cover
+
+These changes are source-level only; no extension method can paper over them. Use the listed analyzer fix.
+
+- `== null` / `!= null` on now-struct types — use the **UA0003** fixer (auto).
+- `?.` member access on now-struct types — use the **UA0004** fixer (auto).
+- `using var x = new CertificateIdentifier(...)` — use **UA0010** to drop the `using`.
+- `[DataContract]`/`[DataMember]` on configuration extensions — use **UA0009**.
+- Removed `Collection` wrappers — use **UA0002**.
+- `CertificateValidator` type rename — see §Certificate validation pipeline below (manual).
+- Removed pre-generated source-generator output — see §Source Generation (manual).
+
+## Sync-over-async caveat
+
+The sync shims (e.g. `handler.Encrypt(bytes)`, `gdsClient.RegisterApplication(...)`, the `Session` / `Subscription` sync helpers) wrap their `*Async` counterparts via `Task.Run(() => …Async(...)).GetAwaiter().GetResult()`. This is intended as a **migration aid only**: it keeps legacy call sites compiling while you port them to `async`/`await`. Do not leave these calls on production hot paths — follow the `UA0011` / `UA0015` guidance and switch to the async APIs.
+
+## TreatWarningsAsErrors recipe
+
+If your project sets `true` and you cannot relax it during the migration window, exclude the migration diagnostics from the failure set:
+
+```xml
+
+ true
+ $(NoWarn);CS0618;UA0001;UA0002;UA0003;UA0004;UA0005;UA0006;UA0007;UA0008;UA0009;UA0010;UA0011;UA0012;UA0014;UA0015;UA0018;UA0019;UA0020;UA0021;UA0022
+
+```
+
+Remove each entry as you finish fixing the corresponding rule, and drop the whole block once the MigrationAnalyzer package is removed.
+
+## Known compatibility gaps
+
+- **Legacy `.NET Framework` projects using the pre-SDK `xmlns="http://schemas.microsoft.com/developer/msbuild/2003"` format** do not honour `PackageReference` injection via `Directory.Build.targets`. To get the analyzer / shim into a legacy WinForms project, add the `` directly to the project file's existing ``.
+- **Projects with `true` and 100+ migration errors** may abort csc.exe before the analyzer reaches some patterns. Apply the `` recipe above, run the analyzer, then re-enable warnings-as-errors.
+
+## Manual migration rules (anything the analyzers / shim do not cover)
+
+The sections below remain the canonical reference for patterns that require human judgement. Apply them in the priority order shown — some fixes resolve cascading errors.
+
+### Priority order for fixes
Apply changes in this order to minimize cascading errors:
diff --git a/Applications/ConsoleReferenceClient/Program.cs b/Applications/ConsoleReferenceClient/Program.cs
index 1b2351b795..44991c3b31 100644
--- a/Applications/ConsoleReferenceClient/Program.cs
+++ b/Applications/ConsoleReferenceClient/Program.cs
@@ -295,7 +295,17 @@ public static Task Main(string[] args)
);
config.TraceConfiguration.DeleteOnLoad = true;
#pragma warning disable CS0618 // Type or member is obsolete
- config.TraceConfiguration.ApplySettings();
+ {
+ TraceConfiguration traceConfiguration = config.TraceConfiguration;
+ if (traceConfiguration.OutputFilePath != null)
+ {
+ Utils.SetTraceLog(traceConfiguration.OutputFilePath, traceConfiguration.DeleteOnLoad);
+ }
+ Utils.SetTraceMask(traceConfiguration.TraceMasks);
+ Utils.SetTraceOutput(traceConfiguration.TraceMasks == 0
+ ? Utils.TraceOutput.Off
+ : Utils.TraceOutput.DebugAndFile);
+ }
#pragma warning restore CS0618 // Type or member is obsolete
}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 2f4a477322..04c2c90c35 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,7 +20,9 @@
-
+
+
+
diff --git a/Docs/MigrationGuide.md b/Docs/MigrationGuide.md
index b15eb6e4d9..06775291e1 100644
--- a/Docs/MigrationGuide.md
+++ b/Docs/MigrationGuide.md
@@ -89,7 +89,7 @@ This document outlines the breaking changes introduced from version to version.
## Migrating from 1.5.378 to 2.0.x
-> **Automate the migration.** Add the `OPCFoundation.NetStandard.Opc.Ua.CodeFixers` analyzer package to your projects to receive analyzer warnings and one-click fixes for the patterns in this guide. Rule IDs `UA0001`-`UA0020` map directly to the sections below.
+> **Automate the migration.** Add the `OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` analyzer package to your projects to receive analyzer warnings and one-click fixes for the patterns in this guide. Rule IDs `UA0001`-`UA0020` map directly to the sections below.
Version 2.0 introduces a major architectural change from pre-generated code files to runtime source generation and more efficient memory use with a several major Breaking Changes requiring changes to your applications.
diff --git a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs
index 0d352e8044..d2a9a4a013 100644
--- a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs
+++ b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs
@@ -365,7 +365,18 @@ ApplicationType.Client or ApplicationType.ClientAndServer &&
}
#pragma warning disable CS0618 // Type or member is obsolete
- ApplicationConfiguration.TraceConfiguration?.ApplySettings();
+ TraceConfiguration? traceConfiguration = ApplicationConfiguration.TraceConfiguration;
+ if (traceConfiguration != null)
+ {
+ if (traceConfiguration.OutputFilePath != null)
+ {
+ Utils.SetTraceLog(traceConfiguration.OutputFilePath, traceConfiguration.DeleteOnLoad);
+ }
+ Utils.SetTraceMask(traceConfiguration.TraceMasks);
+ Utils.SetTraceOutput(traceConfiguration.TraceMasks == 0
+ ? Utils.TraceOutput.Off
+ : Utils.TraceOutput.DebugAndFile);
+ }
#pragma warning restore CS0618 // Type or member is obsolete
await ApplicationConfiguration.ValidateAsync(ApplicationInstance.ApplicationType, ct)
diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs
index 17e65170a5..b1c3e960b2 100644
--- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs
+++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs
@@ -171,7 +171,7 @@ public ValueTask StopAsync(CancellationToken ct = default)
[Obsolete("Use StopAsync")]
public void Stop()
{
- Server?.Stop();
+ Server?.StopAsync().AsTask().GetAwaiter().GetResult();
}
///
@@ -293,7 +293,15 @@ public IApplicationConfigurationBuilderTypes Build(string applicationUri, string
// Trace off
#pragma warning disable CS0618 // Type or member is obsolete
- ApplicationConfiguration.TraceConfiguration.ApplySettings();
+ TraceConfiguration traceConfiguration = ApplicationConfiguration.TraceConfiguration;
+ if (traceConfiguration.OutputFilePath != null)
+ {
+ Utils.SetTraceLog(traceConfiguration.OutputFilePath, traceConfiguration.DeleteOnLoad);
+ }
+ Utils.SetTraceMask(traceConfiguration.TraceMasks);
+ Utils.SetTraceOutput(traceConfiguration.TraceMasks == 0
+ ? Utils.TraceOutput.Off
+ : Utils.TraceOutput.DebugAndFile);
#pragma warning restore CS0618 // Type or member is obsolete
return new ApplicationConfigurationBuilder(this);
diff --git a/Libraries/Opc.Ua.Configuration/Opc.Ua.Configuration.csproj b/Libraries/Opc.Ua.Configuration/Opc.Ua.Configuration.csproj
index 8cc14bf327..445d863361 100644
--- a/Libraries/Opc.Ua.Configuration/Opc.Ua.Configuration.csproj
+++ b/Libraries/Opc.Ua.Configuration/Opc.Ua.Configuration.csproj
@@ -12,6 +12,7 @@
+ $(PackageId).Debug
diff --git a/Libraries/Opc.Ua.Gds.Server.Common/GlobalDiscoverySampleServer.cs b/Libraries/Opc.Ua.Gds.Server.Common/GlobalDiscoverySampleServer.cs
index 2b416a6ca2..874b8c4159 100644
--- a/Libraries/Opc.Ua.Gds.Server.Common/GlobalDiscoverySampleServer.cs
+++ b/Libraries/Opc.Ua.Gds.Server.Common/GlobalDiscoverySampleServer.cs
@@ -74,6 +74,27 @@ public GlobalDiscoverySampleServer(
m_autoApprove = autoApprove;
}
+ ///
+ /// Back-compat ctor matching the 1.5.378 signature (no ).
+ /// Forwards to the modern ctor with a null telemetry context.
+ ///
+ ///
+ /// Preserved so 1.5.378-style sample code (`new GlobalDiscoverySampleServer(database,
+ /// request, certificateGroup, userDatabase, autoApprove)`) continues to compile against
+ /// 2.0 without re-ordering the call site. Consumers should pass an explicit
+ /// via the non-obsolete ctor.
+ ///
+ [Obsolete("Use the constructor that takes an ITelemetryContext parameter instead.")]
+ public GlobalDiscoverySampleServer(
+ IApplicationsDatabase database,
+ ICertificateRequest request,
+ ICertificateGroup certificateGroup,
+ IUserDatabase userDatabase,
+ bool autoApprove = true)
+ : this(database, request, certificateGroup, userDatabase, telemetry: null!, autoApprove)
+ {
+ }
+
///
/// Called before the server starts. Registers GDS-specific
/// encodeable types in the server's message context factory,
diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs
index b668cef57e..0a68634c03 100644
--- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs
+++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs
@@ -2759,7 +2759,17 @@ await CertificateManager.UpdateAsync(
new TraceConfiguration();
#pragma warning disable CS0618 // Type or member is obsolete
- currentConfiguration.TraceConfiguration.ApplySettings();
+ {
+ TraceConfiguration traceConfiguration = currentConfiguration.TraceConfiguration;
+ if (traceConfiguration.OutputFilePath != null)
+ {
+ Utils.SetTraceLog(traceConfiguration.OutputFilePath, traceConfiguration.DeleteOnLoad);
+ }
+ Utils.SetTraceMask(traceConfiguration.TraceMasks);
+ Utils.SetTraceOutput(traceConfiguration.TraceMasks == 0
+ ? Utils.TraceOutput.Off
+ : Utils.TraceOutput.DebugAndFile);
+ }
#pragma warning restore CS0618 // Type or member is obsolete
}
catch (Exception e)
diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj
index b2068996d5..eff7f5e7d0 100644
--- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj
+++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj
@@ -17,6 +17,7 @@
+ $(PackageId).Debug
diff --git a/Stack/Opc.Ua.Core/Stack/Client/ChannelBaseObsolete.cs b/Stack/Opc.Ua.Core/Stack/Client/ChannelBaseObsolete.cs
index 85004a7a7f..a073fa8395 100644
--- a/Stack/Opc.Ua.Core/Stack/Client/ChannelBaseObsolete.cs
+++ b/Stack/Opc.Ua.Core/Stack/Client/ChannelBaseObsolete.cs
@@ -29,7 +29,6 @@
using System;
using Microsoft.Extensions.Logging;
-using Opc.Ua.Security.Certificates;
namespace Opc.Ua
{
@@ -45,183 +44,6 @@ public interface IChannelBase : IDisposable;
[Obsolete("Use ITransportChannel instead.")]
public interface ISessionChannel : IChannelBase;
- ///
- /// Obsolete, use transport channel instead
- ///
- [Obsolete("Use ITransportChannel instead.")]
- public interface IDiscoveryChannel : IChannelBase;
-
- ///
- /// Obsolete, use transport channel instead
- ///
- [Obsolete("Use ITransportChannel instead.")]
- public interface IRegistrationChannel : IChannelBase;
-
- ///
- /// Obsolete Session channel methods
- ///
- [Obsolete("Use UaChannelBase methods instead.")]
- public static class SessionChannel
- {
- ///
- /// Creates a new transport channel.
- ///
- [Obsolete("Use ClientChannelFactory.CreateChannelAsync method instead.")]
- public static ITransportChannel Create(
- ApplicationConfiguration configuration,
- EndpointDescription description,
- EndpointConfiguration endpointConfiguration,
- Certificate clientCertificate,
- IServiceMessageContext messageContext)
- {
- return ClientChannelManager.CreateUaBinaryChannelAsync(
- configuration,
- description,
- endpointConfiguration,
- clientCertificate,
- null,
- messageContext,
- null).AsTask().GetAwaiter().GetResult();
- }
-
- ///
- /// Creates a new transport channel.
- ///
- [Obsolete("Use ClientChannelFactory.CreateChannelAsync method instead.")]
- public static ITransportChannel Create(
- ApplicationConfiguration configuration,
- EndpointDescription description,
- EndpointConfiguration endpointConfiguration,
- Certificate clientCertificate,
- CertificateCollection clientCertificateChain,
- IServiceMessageContext messageContext)
- {
- return ClientChannelManager.CreateUaBinaryChannelAsync(
- configuration,
- description,
- endpointConfiguration,
- clientCertificate,
- clientCertificateChain,
- messageContext,
- null).AsTask().GetAwaiter().GetResult();
- }
-
- ///
- /// Creates a new transport channel.
- ///
- [Obsolete("Use ClientChannelFactory.CreateChannelAsync method instead.")]
- public static ITransportChannel Create(
- ApplicationConfiguration configuration,
- ITransportWaitingConnection connection,
- EndpointDescription description,
- EndpointConfiguration endpointConfiguration,
- Certificate clientCertificate,
- CertificateCollection clientCertificateChain,
- IServiceMessageContext messageContext)
- {
- // create a UA binary channel.
- return ClientChannelManager.CreateUaBinaryChannelAsync(
- configuration,
- connection,
- description,
- endpointConfiguration,
- clientCertificate,
- clientCertificateChain,
- messageContext,
- null).AsTask().GetAwaiter().GetResult();
- }
- }
-
- ///
- /// Obsolete discovery channel methods
- ///
- [Obsolete("Use DiscoveryClient.CreateAsync instead to create a discovery client.")]
- public static class DiscoveryChannel
- {
- ///
- /// Creates a new transport channel for discovery
- ///
- [Obsolete("Use DiscoveryClient.CreateAsync instead to create a discovery client.")]
- public static ITransportChannel Create(
- Uri discoveryUrl,
- EndpointConfiguration endpointConfiguration,
- IServiceMessageContext messageContext,
- Certificate? clientCertificate = null)
- {
- return DiscoveryClient.CreateChannelAsync(
- discoveryUrl,
- endpointConfiguration,
- messageContext,
- clientCertificate).AsTask().GetAwaiter().GetResult();
- }
-
- ///
- /// Creates a new transport channel for discovery
- ///
- [Obsolete("Use CreateAsync instead.")]
- public static ITransportChannel Create(
- ApplicationConfiguration configuration,
- ITransportWaitingConnection connection,
- EndpointConfiguration endpointConfiguration,
- IServiceMessageContext messageContext,
- Certificate? clientCertificate = null)
- {
- return DiscoveryClient.CreateChannelAsync(
- configuration,
- connection,
- endpointConfiguration,
- messageContext,
- clientCertificate).AsTask().GetAwaiter().GetResult();
- }
-
- ///
- /// Creates a new transport channel for discovery
- ///
- [Obsolete("Use CreateAsync instead.")]
- public static ITransportChannel Create(
- ApplicationConfiguration configuration,
- Uri discoveryUrl,
- EndpointConfiguration endpointConfiguration,
- IServiceMessageContext messageContext,
- Certificate? clientCertificate = null)
- {
- return DiscoveryClient.CreateChannelAsync(
- configuration,
- discoveryUrl,
- endpointConfiguration,
- messageContext,
- clientCertificate).AsTask().GetAwaiter().GetResult();
- }
- }
-
- ///
- /// Obsolete Registration channel methods
- ///
- [Obsolete("Use RegistrationClient.CreateAsync instead to create a registrations client.")]
- public static class RegistrationChannel
- {
- ///
- /// Creates a new transport channel that supports registration
- ///
- [Obsolete("Use ClientChannelFactory.CreateChannelAsync instead.")]
- public static ITransportChannel Create(
- ApplicationConfiguration configuration,
- EndpointDescription description,
- EndpointConfiguration endpointConfiguration,
- Certificate clientCertificate,
- IServiceMessageContext messageContext)
- {
- return ClientChannelManager.CreateUaBinaryChannelAsync(
- configuration,
- description,
- endpointConfiguration,
- clientCertificate,
- null,
- messageContext,
- null).AsTask().GetAwaiter().GetResult();
- }
- }
-
///
/// A base class for UA channel objects used access UA interfaces
///
@@ -290,62 +112,6 @@ protected virtual void Dispose(bool disposing)
private readonly ITelemetryContext m_telemetry = null!;
}
- ///
- /// Legacy api to be removed
- ///
- public static class ChannelBaseObsolete
- {
- ///
- /// Schedules an outgoing request.
- ///
- ///
- [Obsolete("WCF channels are not supported anymore.")]
- public static void ScheduleOutgoingRequest(
- this IChannelBase channel,
- IChannelOutgoingRequest request)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// The client side implementation of the InvokeService service contract.
- ///
- ///
- [Obsolete("WCF channels are not supported anymore.")]
- public static InvokeServiceResponseMessage InvokeService(
- this IChannelBase channel,
- InvokeServiceMessage request)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// The operation contract for the InvokeService service.
- ///
- ///
- [Obsolete("WCF channels are not supported anymore.")]
- public static IAsyncResult BeginInvokeService(
- this IChannelBase channel,
- InvokeServiceMessage request,
- AsyncCallback callback,
- object asyncState)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// The method used to retrieve the results of a InvokeService service request.
- ///
- ///
- [Obsolete("WCF channels are not supported anymore.")]
- public static InvokeServiceResponseMessage EndInvokeService(
- this IChannelBase channel,
- IAsyncResult result)
- {
- throw new NotImplementedException();
- }
- }
-
public partial class UaChannelBase : IChannelBase
where TChannel : class, IChannelBase
{
@@ -430,46 +196,4 @@ public void OnOperationCompleted(IAsyncResult ar)
}
}
}
-
- ///
- /// An interface to an object that manages a request received from a client.
- ///
- [Obsolete("WCF channels are no more supported.")]
- public interface IChannelOutgoingRequest
- {
- ///
- /// Gets the request.
- ///
- /// The request.
- IServiceRequest Request { get; }
-
- ///
- /// Gets the handler that must be used to send the request.
- ///
- /// The send request handler.
- ChannelSendRequestEventHandler Handler { get; }
-
- ///
- /// Used to call the default synchronous handler.
- ///
- ///
- /// This method may block the current thread so the caller must not call in the
- /// thread that calls IServerBase.ScheduleIncomingRequest().
- /// This method always traps any exceptions and reports them to the client as a fault.
- ///
- void CallSynchronously();
-
- ///
- /// Used to indicate that the asynchronous operation has completed.
- ///
- /// The response. May be null if an error is provided.
- /// An error to result as a fault.
- void OperationCompleted(IServiceResponse? response, ServiceResult error);
- }
-
- ///
- /// A delegate used to dispatch outgoing service requests.
- ///
- [Obsolete("WCF channels are not supported anymore.")]
- public delegate IServiceResponse ChannelSendRequestEventHandler(IServiceRequest request);
}
diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs
index 2ea5e7a354..f3b06e4ce1 100644
--- a/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs
+++ b/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs
@@ -553,7 +553,15 @@ public static async Task LoadAsync(
if (applyTraceSettings && configuration.TraceConfiguration != null)
{
#pragma warning disable CS0618 // Type or member is obsolete
- configuration.TraceConfiguration.ApplySettings();
+ TraceConfiguration traceConfiguration = configuration.TraceConfiguration;
+ if (traceConfiguration.OutputFilePath != null)
+ {
+ Utils.SetTraceLog(traceConfiguration.OutputFilePath, traceConfiguration.DeleteOnLoad);
+ }
+ Utils.SetTraceMask(traceConfiguration.TraceMasks);
+ Utils.SetTraceOutput(traceConfiguration.TraceMasks == 0
+ ? Utils.TraceOutput.Off
+ : Utils.TraceOutput.DebugAndFile);
#pragma warning restore CS0618 // Type or member is obsolete
}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/CertificateIdentifierShimTests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/CertificateIdentifierShimTests.cs
new file mode 100644
index 0000000000..d52c50cb7c
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/CertificateIdentifierShimTests.cs
@@ -0,0 +1,62 @@
+/* ========================================================================
+ * 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.Threading.Tasks;
+using NUnit.Framework;
+
+namespace Opc.Ua.MigrationAnalyzer.Core.Tests
+{
+ ///
+ /// Runtime tests for .
+ ///
+ [TestFixture]
+ [Category("Shim")]
+ public class CertificateIdentifierShimTests
+ {
+ ///
+ /// Accessing the obsolete Certificate property must throw
+ /// with the migration-pointer
+ /// message that names the async resolver replacement.
+ ///
+ [Test]
+ public Task CertificateGetterThrowsNotSupportedAsync()
+ {
+ var id = new CertificateIdentifier();
+
+#pragma warning disable CS0618 // CertificateIdentifier.Certificate is an intentional shim call.
+ NotSupportedException ex = Assert.Throws(
+ () => { _ = id.Certificate; })!;
+#pragma warning restore CS0618
+
+ Assert.That(ex.Message, Does.Contain("CertificateIdentifierResolver.ResolveAsync"));
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/DataValueObsoleteShimTests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/DataValueObsoleteShimTests.cs
new file mode 100644
index 0000000000..337b48a127
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/DataValueObsoleteShimTests.cs
@@ -0,0 +1,74 @@
+/* ========================================================================
+ * 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.Threading.Tasks;
+using NUnit.Framework;
+
+namespace Opc.Ua.MigrationAnalyzer.Core.Tests
+{
+ ///
+ /// Runtime tests for the obsolete static DataValue.IsGood /
+ /// IsBad shim helpers defined in DataValueObsolete. Regression
+ /// guard for the Phase 11.A fix that corrected the extension receiver type
+ /// from ExtensionObject to DataValue; the wrong receiver type
+ /// would have made these statics unreachable via the DataValue.
+ /// qualifier (which is exactly how callers invoke them).
+ ///
+ [TestFixture]
+ [Category("Shim")]
+ public class DataValueObsoleteShimTests
+ {
+ [Test]
+ public Task IsGoodOnDefaultDataValueReturnsTrueAsync()
+ {
+#pragma warning disable CS0618 // Intentional shim call.
+ bool isGood = DataValue.IsGood(default(DataValue));
+#pragma warning restore CS0618
+
+ Assert.That(isGood, Is.True);
+ return Task.CompletedTask;
+ }
+
+ [Test]
+ public Task IsBadOnBadStatusCodeReturnsTrueAsync()
+ {
+ // Construct via FromStatusCode to avoid the obsolete numeric overload
+ // ambiguity flagged on the DataValue(StatusCode) constructor.
+ StatusCode bad = Opc.Ua.Types.StatusCodes.Bad;
+ DataValue dv = DataValue.FromStatusCode(bad);
+
+#pragma warning disable CS0618 // Intentional shim call.
+ bool isBad = DataValue.IsBad(dv);
+#pragma warning restore CS0618
+
+ Assert.That(isBad, Is.True);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/EncodeableFactoryShimTests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/EncodeableFactoryShimTests.cs
new file mode 100644
index 0000000000..5fc367b909
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/EncodeableFactoryShimTests.cs
@@ -0,0 +1,57 @@
+/* ========================================================================
+ * 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.Threading.Tasks;
+using NUnit.Framework;
+
+namespace Opc.Ua.MigrationAnalyzer.Core.Tests
+{
+ ///
+ /// Runtime tests for .
+ ///
+ [TestFixture]
+ [Category("Shim")]
+ public class EncodeableFactoryShimTests
+ {
+ ///
+ /// The static GlobalFactory shim must return the same
+ /// instance as ServiceMessageContext.GlobalContext.Factory.
+ ///
+ [Test]
+ public Task GlobalFactoryReturnsServiceMessageContextGlobalFactoryAsync()
+ {
+#pragma warning disable CS0618 // EncodeableFactory.GlobalFactory is an intentional shim call.
+ IEncodeableFactory shimFactory = EncodeableFactory.GlobalFactory;
+ Assert.That(shimFactory, Is.Not.Null);
+ Assert.That(shimFactory, Is.SameAs(ServiceMessageContext.GlobalContext.Factory));
+#pragma warning restore CS0618
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/GlobalDiscoveryServerClientShimTests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/GlobalDiscoveryServerClientShimTests.cs
new file mode 100644
index 0000000000..42c65bf196
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/GlobalDiscoveryServerClientShimTests.cs
@@ -0,0 +1,80 @@
+/* ========================================================================
+ * 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.Threading.Tasks;
+using NUnit.Framework;
+using Opc.Ua.Gds.Client;
+
+namespace Opc.Ua.MigrationAnalyzer.Core.Tests
+{
+ ///
+ /// Runtime tests for .
+ ///
+ ///
+ /// is sealed and has
+ /// non-virtual RegisterApplicationAsync / UnregisterApplicationAsync
+ /// methods, so neither Moq nor a hand-rolled subclass can intercept the
+ /// shim's forwarded call. Exercising the shim end-to-end requires a
+ /// live GDS endpoint (full server + secure channel bootstrap), which
+ /// belongs to the integration test suite (Opc.Ua.Gds.Tests), not
+ /// a unit-level runtime check of the shim wiring.
+ ///
+ [TestFixture]
+ [Category("Shim")]
+ public class GlobalDiscoveryServerClientShimTests
+ {
+ ///
+ /// Placeholder for the shim invocation test. Requires a full GDS
+ /// server bootstrap to exercise.
+ ///
+ [Test]
+ [Ignore("Requires GDS server bootstrap: GlobalDiscoveryServerClient " +
+ "is sealed and RegisterApplicationAsync is non-virtual, so the " +
+ "shim cannot be exercised via Moq. Integration coverage lives " +
+ "in Opc.Ua.Gds.Tests.")]
+ public Task RegisterApplicationCallsRegisterApplicationAsyncAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Placeholder for the unregister shim invocation test. Requires a
+ /// full GDS server bootstrap to exercise.
+ ///
+ [Test]
+ [Ignore("Requires GDS server bootstrap: GlobalDiscoveryServerClient " +
+ "is sealed and UnregisterApplicationAsync is non-virtual, so the " +
+ "shim cannot be exercised via Moq. Integration coverage lives " +
+ "in Opc.Ua.Gds.Tests.")]
+ public Task UnregisterApplicationCallsUnregisterApplicationAsyncAsync()
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/LocalDiscoveryServerClientShimTests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/LocalDiscoveryServerClientShimTests.cs
new file mode 100644
index 0000000000..36026ced9c
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/LocalDiscoveryServerClientShimTests.cs
@@ -0,0 +1,64 @@
+/* ========================================================================
+ * 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.Threading.Tasks;
+using NUnit.Framework;
+using Opc.Ua.Gds.Client;
+
+namespace Opc.Ua.MigrationAnalyzer.Core.Tests
+{
+ ///
+ /// Runtime tests for .
+ ///
+ ///
+ /// is not
+ /// virtual, so the APM BeginFindServers/EndFindServers
+ /// adapter cannot be exercised against a Moq stand-in. End-to-end
+ /// validation requires a live LDS endpoint and lives in the discovery
+ /// integration suite.
+ ///
+ [TestFixture]
+ [Category("Shim")]
+ public class LocalDiscoveryServerClientShimTests
+ {
+ ///
+ /// Placeholder for the APM adapter test. Requires a live LDS
+ /// endpoint to drive FindServersAsync.
+ ///
+ [Test]
+ [Ignore("Requires Local Discovery Server endpoint: " +
+ "LocalDiscoveryServerClient.FindServersAsync is non-virtual, so " +
+ "the APM Begin/End shim cannot be intercepted via Moq. " +
+ "Integration coverage lives in Opc.Ua.Lds.Tests.")]
+ public Task BeginEndFindServersDeliverAsyncResultAsync()
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/Opc.Ua.MigrationAnalyzer.Core.Tests.csproj b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/Opc.Ua.MigrationAnalyzer.Core.Tests.csproj
new file mode 100644
index 0000000000..bccfc5b165
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/Opc.Ua.MigrationAnalyzer.Core.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+ $(TestsTargetFrameworks)
+ Opc.Ua.MigrationAnalyzer.Core.Tests
+ true
+ true
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/OpcUaShimAttributeInventoryTests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/OpcUaShimAttributeInventoryTests.cs
new file mode 100644
index 0000000000..ab013b7681
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/OpcUaShimAttributeInventoryTests.cs
@@ -0,0 +1,130 @@
+/* ========================================================================
+ * 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.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using NUnit.Framework;
+
+namespace Opc.Ua.MigrationAnalyzer.Core.Tests
+{
+ ///
+ /// Meta-tests that scan every -marked
+ /// member in the shim assembly. These guard the contract that the
+ /// analyzer relies on: each shim member must carry both an
+ /// and a valid UA00xx rule id.
+ ///
+ [TestFixture]
+ [Category("Shim")]
+ public partial class OpcUaShimAttributeInventoryTests
+ {
+#if NET7_0_OR_GREATER
+ [GeneratedRegex(@"^UA\d{4}$", RegexOptions.CultureInvariant)]
+ private static partial Regex RuleIdRegex();
+#else
+ private static Regex RuleIdRegex() => s_ruleIdRegex;
+ private static readonly Regex s_ruleIdRegex =
+ new(@"^UA\d{4}$", RegexOptions.CultureInvariant | RegexOptions.Compiled);
+#endif
+
+ private static IEnumerable ShimMembers()
+ {
+ Assembly shimAssembly = typeof(OpcUaShimAttribute).Assembly;
+ foreach (Type type in shimAssembly.GetTypes())
+ {
+ foreach (MemberInfo member in type.GetMembers(
+ BindingFlags.Public |
+ BindingFlags.NonPublic |
+ BindingFlags.Static |
+ BindingFlags.Instance |
+ BindingFlags.DeclaredOnly))
+ {
+ if (member.GetCustomAttribute() != null)
+ {
+ yield return member;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Sanity check: the shim assembly exposes at least one
+ /// [OpcUaShim]-attributed member.
+ ///
+ [Test]
+ public Task ShimAssemblyContainsAttributedMembersAsync()
+ {
+ MemberInfo[] members = ShimMembers().ToArray();
+ Assert.That(members, Is.Not.Empty,
+ "Expected at least one [OpcUaShim] member in the shim assembly.");
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Every [OpcUaShim] member must also carry
+ /// so callers see the migration
+ /// guidance at the call site.
+ ///
+ [Test]
+ public Task EveryShimMemberIsAlsoObsoleteAsync()
+ {
+ var missing = ShimMembers()
+ .Where(m => m.GetCustomAttribute() == null)
+ .Select(m => $"{m.DeclaringType?.FullName}.{m.Name}")
+ .ToArray();
+
+ Assert.That(missing, Is.Empty,
+ "These shim members are missing [Obsolete]: " +
+ string.Join(", ", missing));
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Rule ids must match the UA00xx convention so the analyzer
+ /// can correlate the shim with its diagnostic descriptor.
+ ///
+ [Test]
+ public Task EveryShimRuleIdMatchesUa00xxConventionAsync()
+ {
+ var malformed = ShimMembers()
+ .Select(m => (Member: m, Id: m.GetCustomAttribute()!.RuleId))
+ .Where(t => !RuleIdRegex().IsMatch(t.Id))
+ .Select(t => $"{t.Member.DeclaringType?.FullName}.{t.Member.Name}={t.Id}")
+ .ToArray();
+
+ Assert.That(malformed, Is.Empty,
+ "These shim members have non-UA00xx rule ids: " +
+ string.Join(", ", malformed));
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/Properties/AssemblyInfo.cs b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..2b9848014c
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+/* ========================================================================
+ * 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;
+
+[assembly: CLSCompliant(false)]
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/UserIdentityTokenHandlerShimTests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/UserIdentityTokenHandlerShimTests.cs
new file mode 100644
index 0000000000..71f47fa7bb
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/UserIdentityTokenHandlerShimTests.cs
@@ -0,0 +1,149 @@
+/* ========================================================================
+ * 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.Threading;
+using System.Threading.Tasks;
+using Moq;
+using NUnit.Framework;
+using Opc.Ua.Security.Certificates;
+
+namespace Opc.Ua.MigrationAnalyzer.Core.Tests
+{
+ ///
+ /// Runtime tests for .
+ /// Verifies the synchronous shim methods forward to the async
+ /// counterparts on .
+ ///
+ [TestFixture]
+ [Category("Shim")]
+ public class UserIdentityTokenHandlerShimTests
+ {
+ ///
+ /// The sync Encrypt shim must invoke
+ /// on the
+ /// underlying handler.
+ ///
+ [Test]
+ public Task EncryptCallsEncryptAsyncAsync()
+ {
+ var mock = new Mock(MockBehavior.Strict);
+ mock.Setup(h => h.EncryptAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(new ValueTask());
+
+ byte[] nonce = [1, 2, 3, 4];
+ const string policy = "http://opcfoundation.org/UA/SecurityPolicy#None";
+#pragma warning disable CS0618 // Sync Encrypt is an intentional shim call; GlobalContext is a stable test stand-in.
+ IServiceMessageContext ctx = ServiceMessageContext.GlobalContext;
+
+ mock.Object.Encrypt(null!, nonce, policy, ctx);
+#pragma warning restore CS0618
+
+ mock.Verify(h => h.EncryptAsync(
+ null!,
+ nonce,
+ policy,
+ ctx,
+ null,
+ null,
+ null,
+ false,
+ default),
+ Times.Once);
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// The sync Sign shim must return the same
+ /// the async path produced.
+ ///
+ [Test]
+ public Task SignReturnsAsyncResultAsync()
+ {
+ var expected = new SignatureData
+ {
+ Algorithm = "http://opcfoundation.org/UA/test-sign",
+ Signature = [9, 8, 7, 6]
+ };
+ var mock = new Mock(MockBehavior.Strict);
+ mock.Setup(h => h.SignAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(new ValueTask(expected));
+
+ byte[] payload = [1, 2, 3];
+ const string policy = "http://opcfoundation.org/UA/SecurityPolicy#None";
+
+#pragma warning disable CS0618 // Sync Sign is an intentional shim call.
+ SignatureData actual = mock.Object.Sign(payload, policy);
+#pragma warning restore CS0618
+
+ Assert.That(actual, Is.SameAs(expected));
+ mock.Verify(h => h.SignAsync(payload, policy, default), Times.Once);
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// The sync Verify shim must propagate the boolean result
+ /// from the async path.
+ ///
+ [Test]
+ public Task VerifyReturnsAsyncResultAsync()
+ {
+ var mock = new Mock(MockBehavior.Strict);
+ mock.Setup(h => h.VerifyAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(new ValueTask(true));
+
+ byte[] data = [1, 2, 3];
+ var sig = new SignatureData { Algorithm = "alg", Signature = [9] };
+ const string policy = "http://opcfoundation.org/UA/SecurityPolicy#None";
+
+#pragma warning disable CS0618 // Sync Verify is an intentional shim call.
+ bool ok = mock.Object.Verify(data, sig, policy);
+#pragma warning restore CS0618
+
+ Assert.That(ok, Is.True);
+ mock.Verify(h => h.VerifyAsync(data, sig, policy, default), Times.Once);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs
new file mode 100644
index 0000000000..2edade78ae
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs
@@ -0,0 +1,230 @@
+/* ========================================================================
+ * 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.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests
+{
+ ///
+ /// Lightweight, no-extra-package analyzer + code-fix test harness. Lifts the
+ /// useful bits of Microsoft.CodeAnalysis.Testing without taking the dependency.
+ ///
+ /// Each test feeds a small C# source snippet (concatenated with
+ /// ) into the harness; the harness compiles
+ /// the snippet, runs the given , asserts
+ /// the diagnostics, optionally applies the matching ,
+ /// and verifies the fixed source matches an expected string.
+ ///
+ public static class AnalyzerHarness
+ {
+ private static readonly ImmutableArray s_baseReferences = BuildBaseReferences();
+
+ private static ImmutableArray BuildBaseReferences()
+ {
+ string trustedAssemblies = (string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? string.Empty;
+ return [.. trustedAssemblies
+ .Split(Path.PathSeparator)
+ .Where(p => !string.IsNullOrEmpty(p))
+ .Select(p => (MetadataReference)MetadataReference.CreateFromFile(p))];
+ }
+
+ ///
+ /// Compile together with the OPC UA stub surface
+ /// and return the resulting .
+ ///
+ public static CSharpCompilation Compile(string userSource, string assemblyName = "TestAssembly")
+ {
+ CSharpParseOptions parseOptions = new CSharpParseOptions(LanguageVersion.CSharp13);
+ SyntaxTree[] trees =
+ [
+ CSharpSyntaxTree.ParseText(OpcUaStubs.Source, parseOptions, "OpcUaStubs.cs"),
+ CSharpSyntaxTree.ParseText(userSource, parseOptions, "Test.cs"),
+ ];
+ CSharpCompilationOptions options = new CSharpCompilationOptions(
+ OutputKind.DynamicallyLinkedLibrary,
+ allowUnsafe: false,
+ nullableContextOptions: NullableContextOptions.Enable);
+ return CSharpCompilation.Create(assemblyName, trees, s_baseReferences, options);
+ }
+
+ ///
+ /// Compile WITHOUT the OPC UA stub surface, so
+ /// analyzers that branch on "is the legacy symbol present in the compilation?"
+ /// take their symbol-absent fallback. Used by tests that exercise UA0021's
+ /// syntactic-fallback path (when the consumer is on bare 1.6 and the legacy
+ /// CertificateValidator/CertificateValidationEventArgs types are
+ /// no longer defined anywhere).
+ ///
+ public static CSharpCompilation CompileWithoutStubs(string userSource, string assemblyName = "TestAssembly")
+ {
+ CSharpParseOptions parseOptions = new CSharpParseOptions(LanguageVersion.CSharp13);
+ SyntaxTree[] trees =
+ [
+ CSharpSyntaxTree.ParseText(userSource, parseOptions, "Test.cs"),
+ ];
+ CSharpCompilationOptions options = new CSharpCompilationOptions(
+ OutputKind.DynamicallyLinkedLibrary,
+ allowUnsafe: false,
+ nullableContextOptions: NullableContextOptions.Enable);
+ return CSharpCompilation.Create(assemblyName, trees, s_baseReferences, options);
+ }
+
+ ///
+ /// Run against and
+ /// return only the analyzer's diagnostics (compiler diagnostics are filtered out).
+ ///
+ public static Task> GetAnalyzerDiagnosticsAsync(
+ DiagnosticAnalyzer analyzer,
+ string userSource)
+ {
+ CSharpCompilation compilation = Compile(userSource);
+ return RunAsync(analyzer, compilation);
+ }
+
+ ///
+ /// Run against compiled
+ /// WITHOUT the OPC UA stub surface. See .
+ ///
+ public static Task> GetAnalyzerDiagnosticsWithoutStubsAsync(
+ DiagnosticAnalyzer analyzer,
+ string userSource)
+ {
+ CSharpCompilation compilation = CompileWithoutStubs(userSource);
+ return RunAsync(analyzer, compilation);
+ }
+
+ private static async Task> RunAsync(
+ DiagnosticAnalyzer analyzer,
+ CSharpCompilation compilation)
+ {
+ CompilationWithAnalyzers withAnalyzers = compilation.WithAnalyzers(
+ ImmutableArray.Create(analyzer),
+ new CompilationWithAnalyzersOptions(
+ options: null!,
+ onAnalyzerException: null,
+ concurrentAnalysis: true,
+ logAnalyzerExecutionTime: true,
+ reportSuppressedDiagnostics: true));
+ ImmutableArray all = await withAnalyzers.GetAnalyzerDiagnosticsAsync()
+ .ConfigureAwait(false);
+ return all;
+ }
+
+ ///
+ /// Apply to every diagnostic raised by
+ /// against and
+ /// return the fixed source string for "Test.cs".
+ ///
+ public static async Task ApplyFixAsync(
+ DiagnosticAnalyzer analyzer,
+ CodeFixProvider codeFix,
+ string userSource)
+ {
+ CSharpCompilation compilation = Compile(userSource);
+ CompilationWithAnalyzers withAnalyzers = compilation.WithAnalyzers(
+ ImmutableArray.Create(analyzer),
+ new CompilationWithAnalyzersOptions(
+ options: null!,
+ onAnalyzerException: null,
+ concurrentAnalysis: true,
+ logAnalyzerExecutionTime: true,
+ reportSuppressedDiagnostics: true));
+ ImmutableArray diags = await withAnalyzers.GetAnalyzerDiagnosticsAsync()
+ .ConfigureAwait(false);
+
+ SyntaxTree testTree = compilation.SyntaxTrees.First(t => t.FilePath == "Test.cs");
+ Document document = CreateDocument(testTree, codeFix);
+
+ foreach (Diagnostic diag in diags)
+ {
+ if (diag.Location.SourceTree?.FilePath != "Test.cs")
+ {
+ continue;
+ }
+ List actions = [];
+ CodeFixContext ctx = new CodeFixContext(
+ document,
+ diag,
+ (action, _) => actions.Add(action),
+ CancellationToken.None);
+ await codeFix.RegisterCodeFixesAsync(ctx).ConfigureAwait(false);
+ if (actions.Count == 0)
+ {
+ continue;
+ }
+ CodeAction first = actions[0];
+ ImmutableArray ops = await first
+ .GetOperationsAsync(CancellationToken.None)
+ .ConfigureAwait(false);
+ Solution? newSolution = null;
+ foreach (CodeActionOperation op in ops)
+ {
+ if (op is ApplyChangesOperation applyOp)
+ {
+ newSolution = applyOp.ChangedSolution;
+ break;
+ }
+ }
+ if (newSolution != null)
+ {
+ document = newSolution.GetDocument(document.Id)!;
+ }
+ }
+
+ SourceText resultText = await document.GetTextAsync().ConfigureAwait(false);
+ return resultText.ToString();
+ }
+
+ private static Document CreateDocument(SyntaxTree tree, CodeFixProvider _)
+ {
+ AdhocWorkspace workspace = new AdhocWorkspace();
+ ProjectId projectId = ProjectId.CreateNewId();
+ DocumentId documentId = DocumentId.CreateNewId(projectId);
+ Solution solution = workspace.CurrentSolution
+ .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp)
+ .AddMetadataReferences(projectId, s_baseReferences)
+ .AddDocument(documentId, "Test.cs", tree.GetText());
+ return solution.GetDocument(documentId)!;
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0001Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0001Tests.cs
new file mode 100644
index 0000000000..c94cfaab97
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0001Tests.cs
@@ -0,0 +1,146 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0001 (Utils.Trace / Utils.LogX -> ILogger). Diagnostic-only;
+ /// no code fix is shipped — the replacement requires an ILogger instance.
+ ///
+ [TestFixture]
+ public class UA0001Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnUtilsTraceAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M() => Utils.Trace("hello");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0001UtilsTraceToILoggerAnalyzer(), source);
+
+ Diagnostic? ua0001 = diags.SingleOrDefault(d => d.Id == "UA0001");
+ Assert.That(ua0001, Is.Not.Null, "Expected UA0001 to fire on Utils.Trace(...).");
+ Assert.That(
+ ua0001!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("Utils.Trace"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnUtilsLogErrorAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M() => Utils.LogError("error: {0}", 42);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0001UtilsTraceToILoggerAnalyzer(), source);
+
+ Diagnostic? ua0001 = diags.SingleOrDefault(d => d.Id == "UA0001");
+ Assert.That(ua0001, Is.Not.Null, "Expected UA0001 to fire on Utils.LogError(...).");
+ Assert.That(
+ ua0001!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("Utils.LogError"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnUtilsLogInformationAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M() => Utils.LogInformation("info");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0001UtilsTraceToILoggerAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0001"), Is.True,
+ "Expected UA0001 to fire on Utils.LogInformation(...).");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnInstanceILoggerCallAsync()
+ {
+ const string source = """
+ using Microsoft.Extensions.Logging;
+ class C
+ {
+ static void M(ILogger logger) => logger.LogInformation("info");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0001UtilsTraceToILoggerAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0001"), Is.False,
+ "Instance ILogger.LogInformation must not trigger UA0001.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnUnrelatedStaticUtilsTraceAsync()
+ {
+ const string source = """
+ static class MyUtils
+ {
+ public static void Trace(string s) { }
+ }
+ class C
+ {
+ static void M() => MyUtils.Trace("x");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0001UtilsTraceToILoggerAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0001"), Is.False,
+ "A user-defined static Trace on an unrelated class must not trigger UA0001.");
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0002Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0002Tests.cs
new file mode 100644
index 0000000000..667d83caf6
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0002Tests.cs
@@ -0,0 +1,137 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0002 (removed <Type>Collection wrappers).
+ ///
+ [TestFixture]
+ public class UA0002Tests
+ {
+ private static bool IsUserDiagnostic(Diagnostic d)
+ {
+ return d.Id == "UA0002" && d.Location.SourceTree?.FilePath == "Test.cs";
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnInt32CollectionVariableDeclarationAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M() { Int32Collection x = new Int32Collection(); }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0002RemovedCollectionTypeAnalyzer(), source);
+
+ Diagnostic? ua0002 = diags.FirstOrDefault(IsUserDiagnostic);
+ Assert.That(ua0002, Is.Not.Null,
+ "Expected UA0002 to fire on the Int32Collection variable declaration.");
+ Assert.That(
+ ua0002!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("Int32Collection"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnNodeIdCollectionParameterAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(NodeIdCollection ids) { _ = ids; }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0002RemovedCollectionTypeAnalyzer(), source);
+
+ Assert.That(diags.Any(IsUserDiagnostic), Is.True,
+ "Expected UA0002 to fire on the NodeIdCollection parameter type.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnListOfIntAsync()
+ {
+ const string source = """
+ using System.Collections.Generic;
+ class C
+ {
+ static void M() { List x = new List(); _ = x; }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0002RemovedCollectionTypeAnalyzer(), source);
+
+ Assert.That(diags.Any(IsUserDiagnostic), Is.False,
+ "List must not trigger UA0002.");
+ }
+
+ [Test]
+ public async Task FixRewritesInt32CollectionDeclarationAndCreationAsync()
+ {
+ const string source = """
+ using System.Collections.Generic;
+ using Opc.Ua;
+ class C
+ {
+ static void M() { Int32Collection x = new Int32Collection(); _ = x; }
+ }
+ """;
+ const string expected = """
+ using System.Collections.Generic;
+ using Opc.Ua;
+ class C
+ {
+ static void M() { List x = new List(); _ = x; }
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0002RemovedCollectionTypeAnalyzer(),
+ new UA0002RemovedCollectionTypeCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0003Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0003Tests.cs
new file mode 100644
index 0000000000..7c2c26257b
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0003Tests.cs
@@ -0,0 +1,191 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0003 (null comparison on now-struct built-in type).
+ ///
+ [TestFixture]
+ public class UA0003Tests
+ {
+ [Test]
+ public async Task ReportsOnNodeIdEqualsNullAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(NodeId n) => n == null;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0003NullCheckOnStructTypeAnalyzer(), source);
+
+ Diagnostic? ua0003 = diags.SingleOrDefault(d => d.Id == "UA0003");
+ Assert.That(ua0003, Is.Not.Null);
+ Assert.That(
+ ua0003!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("NodeId"));
+ }
+
+ [Test]
+ public async Task ReportsOnNullEqualsNodeIdAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(NodeId n) => null == n;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0003NullCheckOnStructTypeAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0003"), Is.True);
+ }
+
+ [Test]
+ public async Task ReportsOnLocalizedTextNotEqualsNullAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(LocalizedText lt) => lt != null;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0003NullCheckOnStructTypeAnalyzer(), source);
+
+ Diagnostic? ua0003 = diags.SingleOrDefault(d => d.Id == "UA0003");
+ Assert.That(ua0003, Is.Not.Null);
+ Assert.That(
+ ua0003!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("LocalizedText"));
+ }
+
+ [Test]
+ public async Task DoesNotReportOnStringEqualsNullAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(string s) => s == null;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0003NullCheckOnStructTypeAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0003"), Is.False);
+ }
+
+ [Test]
+ public async Task DoesNotReportOnEqualsMethodCallAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(NodeId n) => n.Equals(null);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0003NullCheckOnStructTypeAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0003"), Is.False);
+ }
+
+ [Test]
+ public async Task FixRewritesNodeIdEqualsNullToIsNullAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(NodeId n) => n == null;
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(NodeId n) => n.IsNull;
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0003NullCheckOnStructTypeAnalyzer(),
+ new UA0003NullCheckOnStructTypeCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public async Task FixRewritesLocalizedTextNotEqualsNullToNotIsNullOrEmptyAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(LocalizedText lt) => lt != null;
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(LocalizedText lt) => !lt.IsNullOrEmpty;
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0003NullCheckOnStructTypeAnalyzer(),
+ new UA0003NullCheckOnStructTypeCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0004Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0004Tests.cs
new file mode 100644
index 0000000000..ec04cd1a37
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0004Tests.cs
@@ -0,0 +1,147 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0004 (null-conditional access on now-struct built-in type).
+ ///
+ [TestFixture]
+ public class UA0004Tests
+ {
+ [Test]
+ public async Task ReportsOnNodeIdConditionalAccessAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object M(NodeId nodeId) => nodeId?.NamespaceIndex;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0004ConditionalAccessOnStructAnalyzer(), source);
+
+ Diagnostic? ua0004 = diags.SingleOrDefault(d => d.Id == "UA0004");
+ Assert.That(ua0004, Is.Not.Null, "Expected UA0004 to fire on nodeId?.NamespaceIndex.");
+ Assert.That(
+ ua0004!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("NodeId"));
+ }
+
+ [Test]
+ public async Task ReportsOnDataValueConditionalAccessAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object M(DataValue dv) => dv?.IsGood;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0004ConditionalAccessOnStructAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0004"), Is.True,
+ "Expected UA0004 to fire on dv?.IsGood.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnStringConditionalAccessAsync()
+ {
+ const string source = """
+ class C
+ {
+ static object M(string s) => s?.Length;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0004ConditionalAccessOnStructAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0004"), Is.False,
+ "Conditional access on string must not trigger UA0004.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnPlainMemberAccessAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object M(NodeId nodeId) => nodeId.NamespaceIndex;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0004ConditionalAccessOnStructAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0004"), Is.False,
+ "Plain member access without '?.' must not trigger UA0004.");
+ }
+
+ [Test]
+ public async Task FixRewritesConditionalAccessToDirectAccessAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object M(NodeId nodeId) => nodeId?.NamespaceIndex;
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static object M(NodeId nodeId) => nodeId.NamespaceIndex;
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0004ConditionalAccessOnStructAnalyzer(),
+ new UA0004ConditionalAccessOnStructCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0005Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0005Tests.cs
new file mode 100644
index 0000000000..a7fbabfd4e
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0005Tests.cs
@@ -0,0 +1,153 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0005 (byte[] passed where ByteString is now expected).
+ ///
+ [TestFixture]
+ public class UA0005Tests
+ {
+ [Test]
+ public async Task ReportsOnByteArrayArgumentWhereByteStringIsExpectedAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(ByteString b) { }
+ static void Caller(byte[] arr) => M(arr);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0005ByteArrayToByteStringAnalyzer(), source);
+
+ Diagnostic? ua0005 = diags.SingleOrDefault(d => d.Id == "UA0005");
+ Assert.That(ua0005, Is.Not.Null, "Expected UA0005 to fire when byte[] is passed to a ByteString parameter.");
+ Assert.That(
+ ua0005!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("M"));
+ }
+
+ [Test]
+ public async Task DoesNotReportWhenArgumentAlreadyToByteStringAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(ByteString b) { }
+ static void Caller(byte[] arr) => M(arr.ToByteString());
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0005ByteArrayToByteStringAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0005"), Is.False,
+ "Calling .ToByteString() at the call site must not trigger UA0005.");
+ }
+
+ [Test]
+ public async Task DoesNotReportWhenByteArrayOverloadBindsAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void Caller(byte[] arr) => ByteStringApi.Process(arr);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0005ByteArrayToByteStringAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0005"), Is.False,
+ "When the byte[] overload binds the rule must not fire.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnDefaultLiteralAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(ByteString b) { }
+ static void Caller() => M(default);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0005ByteArrayToByteStringAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0005"), Is.False,
+ "A 'default' literal has type ByteString — UA0005 must not fire.");
+ }
+
+ [Test]
+ public async Task FixAppendsToByteStringAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(ByteString b) { }
+ static void Caller(byte[] arr) => M(arr);
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(ByteString b) { }
+ static void Caller(byte[] arr) => M(arr.ToByteString());
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0005ByteArrayToByteStringAnalyzer(),
+ new UA0005ByteArrayToByteStringCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0006Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0006Tests.cs
new file mode 100644
index 0000000000..3859553903
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0006Tests.cs
@@ -0,0 +1,220 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0006 (obsolete Variant(object|DateTime|Guid|byte[]) constructors).
+ ///
+ [TestFixture]
+ public class UA0006Tests
+ {
+ [Test]
+ public async Task ReportsOnNewVariantObjectAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static Variant M() => new Variant((object)42);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0006ObsoleteVariantCtorAnalyzer(), source);
+
+ Diagnostic? ua0006 = diags.SingleOrDefault(d => d.Id == "UA0006");
+ Assert.That(ua0006, Is.Not.Null);
+ Assert.That(
+ ua0006!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("object"));
+ }
+
+ [Test]
+ public async Task ReportsOnNewVariantDateTimeAsync()
+ {
+ const string source = """
+ using System;
+ using Opc.Ua;
+ class C
+ {
+ static Variant M() => new Variant(DateTime.UtcNow);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0006ObsoleteVariantCtorAnalyzer(), source);
+
+ Diagnostic? ua0006 = diags.SingleOrDefault(d => d.Id == "UA0006");
+ Assert.That(ua0006, Is.Not.Null);
+ Assert.That(
+ ua0006!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("DateTime"));
+ }
+
+ [Test]
+ public async Task ReportsOnNewVariantGuidAsync()
+ {
+ const string source = """
+ using System;
+ using Opc.Ua;
+ class C
+ {
+ static Variant M() => new Variant(Guid.NewGuid());
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0006ObsoleteVariantCtorAnalyzer(), source);
+
+ Diagnostic? ua0006 = diags.SingleOrDefault(d => d.Id == "UA0006");
+ Assert.That(ua0006, Is.Not.Null);
+ Assert.That(
+ ua0006!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("Guid"));
+ }
+
+ [Test]
+ public async Task ReportsOnNewVariantByteArrayAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static Variant M() => new Variant(new byte[] { 1, 2, 3 });
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0006ObsoleteVariantCtorAnalyzer(), source);
+
+ Diagnostic? ua0006 = diags.SingleOrDefault(d => d.Id == "UA0006");
+ Assert.That(ua0006, Is.Not.Null);
+ Assert.That(
+ ua0006!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("byte[]"));
+ }
+
+ [Test]
+ public async Task DoesNotReportOnNewVariantIntAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static Variant M() => new Variant(42);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0006ObsoleteVariantCtorAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0006"), Is.False);
+ }
+
+ [Test]
+ public async Task DoesNotReportOnNewVariantStringAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static Variant M() => new Variant("hello");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0006ObsoleteVariantCtorAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0006"), Is.False);
+ }
+
+ [Test]
+ public async Task FixRewritesDateTimeCtorToVariantFromDateTimeUtcAsync()
+ {
+ const string source = """
+ using System;
+ using Opc.Ua;
+ class C
+ {
+ static Variant M() => new Variant(DateTime.UtcNow);
+ }
+ """;
+ const string expected = """
+ using System;
+ using Opc.Ua;
+ class C
+ {
+ static Variant M() => Variant.From(new DateTimeUtc(DateTime.UtcNow));
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0006ObsoleteVariantCtorAnalyzer(),
+ new UA0006ObsoleteVariantCtorCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public async Task FixRewritesByteArrayCtorToVariantFromToByteStringAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static Variant M(byte[] arr) => new Variant(arr);
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static Variant M(byte[] arr) => Variant.From(arr.ToByteString());
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0006ObsoleteVariantCtorAnalyzer(),
+ new UA0006ObsoleteVariantCtorCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0007Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0007Tests.cs
new file mode 100644
index 0000000000..e80ff78d2f
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0007Tests.cs
@@ -0,0 +1,148 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0007 (obsolete NodeId/ExpandedNodeId string constructor).
+ ///
+ [TestFixture]
+ public class UA0007Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnNewNodeIdStringAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static NodeId M() => new NodeId("ns=1;i=42");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0007ObsoleteNodeIdStringCtorAnalyzer(), source);
+
+ Diagnostic? ua0007 = diags.SingleOrDefault(d => d.Id == "UA0007");
+ Assert.That(ua0007, Is.Not.Null);
+ Assert.That(
+ ua0007!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("NodeId"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnNewExpandedNodeIdStringAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static ExpandedNodeId M() => new ExpandedNodeId("ns=1;i=42");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0007ObsoleteNodeIdStringCtorAnalyzer(), source);
+
+ Diagnostic? ua0007 = diags.SingleOrDefault(d => d.Id == "UA0007");
+ Assert.That(ua0007, Is.Not.Null);
+ Assert.That(
+ ua0007!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("ExpandedNodeId"));
+ }
+
+ [Test]
+ public async Task DoesNotReportOnNewNodeIdUintAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static NodeId M() => new NodeId(42u);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0007ObsoleteNodeIdStringCtorAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0007"), Is.False);
+ }
+
+ [Test]
+ public async Task DoesNotReportOnNewNodeIdUintNamespaceAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static NodeId M() => new NodeId(42u, (ushort)0);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0007ObsoleteNodeIdStringCtorAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0007"), Is.False);
+ }
+
+ [Test]
+ public async Task FixRewritesStringCtorToParseAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static NodeId M(string s) => new NodeId(s);
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static NodeId M(string s) => NodeId.Parse(s);
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0007ObsoleteNodeIdStringCtorAnalyzer(),
+ new UA0007ObsoleteNodeIdStringCtorCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0008Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0008Tests.cs
new file mode 100644
index 0000000000..0b38170d56
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0008Tests.cs
@@ -0,0 +1,190 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0008 (Session.Call params object[] → params Variant[]).
+ ///
+ [TestFixture]
+ public class UA0008Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnSessionCallWithRawArgsAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(Session session, NodeId objId, NodeId methodId)
+ {
+ session.Call(objId, methodId, 1, "two");
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0008SessionCallParamsObjectAnalyzer(), source);
+
+ Diagnostic? ua0008 = diags.SingleOrDefault(d => d.Id == "UA0008");
+ Assert.That(ua0008, Is.Not.Null,
+ "Expected UA0008 to fire on Session.Call with raw int / string args.");
+ Assert.That(
+ ua0008!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("Call"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnSessionCallAsyncWithRawArgsAsync()
+ {
+ const string source = """
+ using System.Threading;
+ using Opc.Ua;
+ class C
+ {
+ static void M(Session session, NodeId objId, NodeId methodId, CancellationToken ct)
+ {
+ _ = session.CallAsync(objId, methodId, ct, 42);
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0008SessionCallParamsObjectAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0008"), Is.True,
+ "Expected UA0008 to fire on Session.CallAsync with a raw int arg.");
+ }
+
+ [Test]
+ public async Task DoesNotReportWhenAllArgsAreVariantAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(Session session, NodeId objId, NodeId methodId)
+ {
+ session.Call(objId, methodId, Variant.From(1), Variant.From("two"));
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0008SessionCallParamsObjectAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0008"), Is.False,
+ "All-Variant arguments must not trigger UA0008.");
+ }
+
+ [Test]
+ public async Task DoesNotReportWhenNoVariadicArgsAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(Session session, NodeId objId, NodeId methodId)
+ {
+ session.Call(objId, methodId);
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0008SessionCallParamsObjectAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0008"), Is.False,
+ "Session.Call with no variadic args must not trigger UA0008.");
+ }
+
+ [Test]
+ public async Task FixWrapsRawArgsWithVariantFromAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(Session session, NodeId o, NodeId m)
+ {
+ session.Call(o, m, 1, "two");
+ }
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(Session session, NodeId o, NodeId m)
+ {
+ session.Call(o, m, Variant.From(1), Variant.From("two"));
+ }
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0008SessionCallParamsObjectAnalyzer(),
+ new UA0008SessionCallParamsObjectCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnShimExtensionCallAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(Session session, NodeId objId, NodeId methodId)
+ {
+ #pragma warning disable CS0618
+ SessionShim.Call(session, objId, methodId, 1, "two");
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0008SessionCallParamsObjectAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0008"), Is.True,
+ "Expected UA0008 to fire on a call resolving to a [OpcUaShim(\"UA0008\")] member.");
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0009Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0009Tests.cs
new file mode 100644
index 0000000000..9dbc8fbcd1
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0009Tests.cs
@@ -0,0 +1,213 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0009 ([DataContract]/[DataMember] -> [DataType]/[DataTypeField]).
+ ///
+ [TestFixture]
+ public class UA0009Tests
+ {
+ [Test]
+ public async Task ReportsOnDataContractClassWithDataMemberPropertyAsync()
+ {
+ const string source = """
+ using System.Runtime.Serialization;
+ namespace Test
+ {
+ [DataContract]
+ public class Foo
+ {
+ [DataMember]
+ public int X { get; set; }
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0009DataContractToDataTypeAnalyzer(), source);
+
+ Diagnostic? ua0009 = diags.SingleOrDefault(d => d.Id == "UA0009");
+ Assert.That(ua0009, Is.Not.Null, "Expected UA0009 on [DataContract] + [DataMember] class.");
+ Assert.That(
+ ua0009!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("Foo"));
+ }
+
+ [Test]
+ public async Task ReportsOnDataContractClassWithoutParseExtensionUseAsync()
+ {
+ // The simplified detection flags any candidate class regardless of whether
+ // ApplicationConfiguration.ParseExtension/UpdateExtension is invoked in the
+ // same compilation. This test pins that behaviour.
+ const string source = """
+ using System.Runtime.Serialization;
+ namespace Test
+ {
+ [DataContract(Name = "Foo")]
+ public class Foo
+ {
+ [DataMember(Order = 1)]
+ public int X { get; set; }
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0009DataContractToDataTypeAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0009"), Is.True,
+ "Expected UA0009 even when no ParseExtension call is present.");
+ }
+
+ [Test]
+ public async Task DoesNotReportWhenDataMemberIsOnFieldOnlyAsync()
+ {
+ const string source = """
+ using System.Runtime.Serialization;
+ namespace Test
+ {
+ [DataContract]
+ public class Foo
+ {
+ [DataMember]
+ public int X;
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0009DataContractToDataTypeAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0009"), Is.False,
+ "Field-only [DataMember] must not trigger UA0009 under simplified detection.");
+ }
+
+ [Test]
+ public async Task DoesNotReportWhenNoDataMemberPresentAsync()
+ {
+ const string source = """
+ using System.Runtime.Serialization;
+ namespace Test
+ {
+ [DataContract]
+ public class Foo
+ {
+ public int X { get; set; }
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0009DataContractToDataTypeAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0009"), Is.False,
+ "[DataContract] alone (no [DataMember]) must not trigger UA0009.");
+ }
+
+ [Test]
+ public async Task FixReplacesDataContractAndDataMemberAttributesAsync()
+ {
+ const string source = """
+ using System.Runtime.Serialization;
+ using Opc.Ua;
+ namespace Test
+ {
+ [DataContract]
+ public partial class Foo
+ {
+ [DataMember]
+ public int X { get; set; }
+ }
+ }
+ """;
+ const string expected = """
+ using System.Runtime.Serialization;
+ using Opc.Ua;
+ namespace Test
+ {
+ [DataType]
+ public partial class Foo
+ {
+ [DataTypeField]
+ public int X { get; set; }
+ }
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0009DataContractToDataTypeAnalyzer(),
+ new UA0009DataContractToDataTypeCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public async Task FixAddsPartialModifierAndUsingOpcUaWhenMissingAsync()
+ {
+ const string source = """
+ using System.Runtime.Serialization;
+ namespace Test
+ {
+ [DataContract]
+ public class Foo
+ {
+ [DataMember]
+ public int X { get; set; }
+ }
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0009DataContractToDataTypeAnalyzer(),
+ new UA0009DataContractToDataTypeCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Does.Contain("using Opc.Ua;"),
+ "Fix must add 'using Opc.Ua;' when missing.");
+ Assert.That(fixedSource, Does.Contain("partial class Foo"),
+ "Fix must add the 'partial' modifier to the class.");
+ Assert.That(fixedSource, Does.Contain("[DataType]"),
+ "Fix must rewrite [DataContract] to [DataType].");
+ Assert.That(fixedSource, Does.Contain("[DataTypeField]"),
+ "Fix must rewrite [DataMember] to [DataTypeField].");
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0010Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0010Tests.cs
new file mode 100644
index 0000000000..a7a0745586
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0010Tests.cs
@@ -0,0 +1,133 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0010 (using/Dispose on identity types that are no longer
+ /// IDisposable in 2.0). The analyzer is diagnostic-only: no code fix is
+ /// registered.
+ ///
+ [TestFixture]
+ public class UA0010Tests
+ {
+ [Test]
+ public async Task ReportsOnUsingDeclarationOfCertificateIdentifierAsync()
+ {
+ const string source = """
+ #pragma warning disable CS1674
+ using Opc.Ua;
+ class C
+ {
+ static void M()
+ {
+ using var ci = new CertificateIdentifier();
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0010RemoveDisposableAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0010"), Is.True,
+ "Expected UA0010 to fire on 'using var ci = new CertificateIdentifier();'.");
+ }
+
+ [Test]
+ public async Task ReportsOnUsingStatementOfUserIdentityAsync()
+ {
+ const string source = """
+ #pragma warning disable CS1674
+ using Opc.Ua;
+ class C
+ {
+ static void M()
+ {
+ using (var ui = new UserIdentity()) { }
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0010RemoveDisposableAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0010"), Is.True,
+ "Expected UA0010 to fire on 'using (var ui = new UserIdentity()) { }'.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnUnrelatedDisposableAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M()
+ {
+ using var ms = new System.IO.MemoryStream();
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0010RemoveDisposableAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0010"), Is.False,
+ "Unrelated IDisposable types must not trigger UA0010.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnPlainDeclarationAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M()
+ {
+ var ci = new CertificateIdentifier();
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0010RemoveDisposableAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0010"), Is.False,
+ "A plain variable declaration without 'using' must not trigger UA0010.");
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0011Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0011Tests.cs
new file mode 100644
index 0000000000..2702646659
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0011Tests.cs
@@ -0,0 +1,190 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0011 (IUserIdentityTokenHandler synchronous Encrypt/Decrypt/Sign/Verify
+ /// replaced by the *Async counterparts).
+ ///
+ [TestFixture]
+ public class UA0011Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnEncryptCallOnInterfaceAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static byte[] M(IUserIdentityTokenHandler handler, byte[] bytes)
+ {
+ #pragma warning disable CS0618
+ return handler.Encrypt(bytes);
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0011TokenHandlerSyncToAsyncAnalyzer(), source);
+
+ Diagnostic? ua0011 = diags.SingleOrDefault(d => d.Id == "UA0011");
+ Assert.That(ua0011, Is.Not.Null, "Expected UA0011 to fire on handler.Encrypt(bytes).");
+ Assert.That(
+ ua0011!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("Encrypt"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnSignCallOnInterfaceAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static byte[] M(IUserIdentityTokenHandler handler, byte[] bytes)
+ {
+ #pragma warning disable CS0618
+ return handler.Sign(bytes);
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0011TokenHandlerSyncToAsyncAnalyzer(), source);
+
+ Diagnostic? ua0011 = diags.SingleOrDefault(d => d.Id == "UA0011");
+ Assert.That(ua0011, Is.Not.Null, "Expected UA0011 to fire on handler.Sign(bytes).");
+ Assert.That(
+ ua0011!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("Sign"));
+ }
+
+ [Test]
+ public async Task DoesNotReportOnEncryptAsyncCallAsync()
+ {
+ const string source = """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Opc.Ua;
+ class C
+ {
+ static async Task M(IUserIdentityTokenHandler handler, byte[] bytes, CancellationToken ct)
+ {
+ return await handler.EncryptAsync(bytes, ct);
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0011TokenHandlerSyncToAsyncAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0011"), Is.False,
+ "EncryptAsync must not trigger UA0011.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnUnrelatedTypeEncryptCallAsync()
+ {
+ const string source = """
+ class Other
+ {
+ public byte[] Encrypt(byte[] data) => data;
+ }
+ class C
+ {
+ static byte[] M(Other o, byte[] bytes) => o.Encrypt(bytes);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0011TokenHandlerSyncToAsyncAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0011"), Is.False,
+ "Encrypt on an unrelated type must not trigger UA0011.");
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnShimExtensionCallAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static byte[] M(IUserIdentityTokenHandler handler, byte[] bytes)
+ {
+ #pragma warning disable CS0618
+ return UserIdentityTokenHandlerShim.Encrypt(handler, bytes);
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0011TokenHandlerSyncToAsyncAnalyzer(), source);
+
+ Diagnostic? ua0011 = diags.SingleOrDefault(d => d.Id == "UA0011");
+ Assert.That(ua0011, Is.Not.Null,
+ "Expected UA0011 to fire on shim extension call carrying [OpcUaShim(\"UA0011\")].");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnShimWithDifferentRuleIdAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static byte[] M(IUserIdentityTokenHandler handler, byte[] bytes)
+ {
+ #pragma warning disable CS0618
+ return UserIdentityTokenHandlerShim.EncryptUnrelated(handler, bytes);
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0011TokenHandlerSyncToAsyncAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0011"), Is.False,
+ "A shim attribute with a different RuleId must not trigger UA0011.");
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0012Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0012Tests.cs
new file mode 100644
index 0000000000..dfd082daa2
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0012Tests.cs
@@ -0,0 +1,127 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0012 (obsolete static CertificateFactory members).
+ ///
+ [TestFixture]
+ public class UA0012Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnCertificateFactoryCreateAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object M() => CertificateFactory.Create("CN=Test");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0012CertificateFactoryStaticToInstanceAnalyzer(), source);
+
+ Diagnostic? ua0012 = diags.SingleOrDefault(d => d.Id == "UA0012");
+ Assert.That(ua0012, Is.Not.Null);
+ Assert.That(
+ ua0012!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("Create"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnCertificateFactoryCreateSigningRequestAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object M() => CertificateFactory.CreateSigningRequest("CN=Test");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0012CertificateFactoryStaticToInstanceAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0012"), Is.True);
+ }
+
+ [Test]
+ public async Task DoesNotReportOnDefaultCertificateFactoryInstanceCallAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object M() => DefaultCertificateFactory.Instance.Create("CN=Test");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0012CertificateFactoryStaticToInstanceAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0012"), Is.False);
+ }
+
+ [Test]
+ public async Task FixRewritesStaticCallToInstanceAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object M(string s) => CertificateFactory.Create(s);
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static object M(string s) => DefaultCertificateFactory.Instance.Create(s);
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0012CertificateFactoryStaticToInstanceAnalyzer(),
+ new UA0012CertificateFactoryStaticToInstanceCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0014Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0014Tests.cs
new file mode 100644
index 0000000000..ac575a7f84
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0014Tests.cs
@@ -0,0 +1,146 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0014 (DataValue.IsGood static helper -> instance property).
+ ///
+ [TestFixture]
+ public class UA0014Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnStaticDataValueIsGoodCallAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(DataValue dv) => DataValue.IsGood(dv);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0014DataValueIsGoodAnalyzer(), source);
+
+ Diagnostic? ua0014 = diags.SingleOrDefault(d => d.Id == "UA0014");
+ Assert.That(ua0014, Is.Not.Null, "Expected UA0014 to fire on DataValue.IsGood(dv).");
+ Assert.That(ua0014!.GetMessage(System.Globalization.CultureInfo.InvariantCulture), Does.Contain("IsGood"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnDataValueExtensionsIsBadCallAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(DataValue dv) => DataValueExtensions.IsBad(dv);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0014DataValueIsGoodAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0014"),
+ "Expected UA0014 to fire on DataValueExtensions.IsBad(dv).");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnInstancePropertyAccessAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(DataValue dv) => dv.IsGood;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0014DataValueIsGoodAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0014"), Is.False,
+ "Instance property access dv.IsGood must not trigger UA0014.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnUnrelatedSingleArgStaticCallAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool IsGood(DataValue dv) => false;
+ static bool M(DataValue dv) => IsGood(dv);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0014DataValueIsGoodAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0014"), Is.False,
+ "A user-defined static IsGood on an unrelated class must not trigger UA0014.");
+ }
+
+ [Test]
+ public async Task FixRewritesStaticCallToInstancePropertyAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(DataValue dv) => DataValue.IsGood(dv);
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(DataValue dv) => dv.IsGood;
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0014DataValueIsGoodAnalyzer(),
+ new UA0014DataValueIsGoodCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0015Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0015Tests.cs
new file mode 100644
index 0000000000..af966ec1d4
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0015Tests.cs
@@ -0,0 +1,191 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0015 (obsolete sync/APM members on GDS/LDS discovery clients).
+ ///
+ [TestFixture]
+ public class UA0015Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnGdsRegisterApplicationAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(GlobalDiscoveryServerClient gdsClient)
+ {
+ #pragma warning disable CS0618
+ gdsClient.RegisterApplication("urn:foo");
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0015GdsSyncToAsyncAnalyzer(), source);
+
+ Diagnostic? ua0015 = diags.SingleOrDefault(d => d.Id == "UA0015");
+ Assert.That(ua0015, Is.Not.Null, "Expected UA0015 to fire on RegisterApplication.");
+ Assert.That(
+ ua0015!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("RegisterApplication"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnServerPushApplyChangesAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(ServerPushConfigurationClient pushClient)
+ {
+ #pragma warning disable CS0618
+ pushClient.ApplyChanges();
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0015GdsSyncToAsyncAnalyzer(), source);
+
+ Diagnostic? ua0015 = diags.SingleOrDefault(d => d.Id == "UA0015");
+ Assert.That(ua0015, Is.Not.Null, "Expected UA0015 to fire on ApplyChanges.");
+ Assert.That(
+ ua0015!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("ApplyChanges"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnLdsBeginFindServersAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(LocalDiscoveryServerClient ldsClient, string endpoint)
+ {
+ #pragma warning disable CS0618
+ ldsClient.BeginFindServers(endpoint, null, null);
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0015GdsSyncToAsyncAnalyzer(), source);
+
+ Diagnostic? ua0015 = diags.SingleOrDefault(d => d.Id == "UA0015");
+ Assert.That(ua0015, Is.Not.Null, "Expected UA0015 to fire on BeginFindServers.");
+ Assert.That(
+ ua0015!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("BeginFindServers"));
+ }
+
+ [Test]
+ public async Task DoesNotReportOnRegisterApplicationAsyncAsync()
+ {
+ const string source = """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Opc.Ua;
+ class C
+ {
+ static async Task M(GlobalDiscoveryServerClient gdsClient, CancellationToken ct)
+ {
+ await gdsClient.RegisterApplicationAsync("urn:foo", ct);
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0015GdsSyncToAsyncAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0015"), Is.False,
+ "RegisterApplicationAsync must not trigger UA0015.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnUnrelatedRegisterApplicationCallAsync()
+ {
+ const string source = """
+ class Other
+ {
+ public void RegisterApplication(string uri) { }
+ }
+ class C
+ {
+ static void M(Other o) => o.RegisterApplication("urn:foo");
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0015GdsSyncToAsyncAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0015"), Is.False,
+ "RegisterApplication on an unrelated type must not trigger UA0015.");
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnShimExtensionCallAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(GlobalDiscoveryServerClient client)
+ {
+ #pragma warning disable CS0618
+ client.RegisterApplicationLegacy("urn:foo");
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0015GdsSyncToAsyncAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0015"), Is.True,
+ "Expected UA0015 to fire on a call resolving to a [OpcUaShim(\"UA0015\")] extension.");
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0018Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0018Tests.cs
new file mode 100644
index 0000000000..d872c2f2a7
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0018Tests.cs
@@ -0,0 +1,164 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0018 (obsolete CertificateIdentifier.Certificate getter).
+ ///
+ [TestFixture]
+ public class UA0018Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnCertificateGetterReadAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object? M(CertificateIdentifierWithObsoleteCertificate id)
+ {
+ #pragma warning disable CS0618
+ var c = id.Certificate;
+ #pragma warning restore CS0618
+ return c;
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(
+ new UA0018CertificateIdentifierCertificateAnalyzer(), source);
+
+ Diagnostic? ua0018 = diags.SingleOrDefault(d => d.Id == "UA0018");
+ Assert.That(ua0018, Is.Not.Null, "Expected UA0018 to fire on id.Certificate read.");
+ Assert.That(
+ ua0018!.GetMessage(CultureInfo.InvariantCulture),
+ Does.Contain("CertificateIdentifier"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnCertificateGetterInConditionAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static bool M(CertificateIdentifierWithObsoleteCertificate id)
+ {
+ #pragma warning disable CS0618
+ if (id.Certificate != null) { return true; }
+ #pragma warning restore CS0618
+ return false;
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(
+ new UA0018CertificateIdentifierCertificateAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0018"), Is.True,
+ "Expected UA0018 to fire on id.Certificate in a null comparison.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnSubjectNamePropertyAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static string? M(CertificateIdentifier id) => id.SubjectName;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(
+ new UA0018CertificateIdentifierCertificateAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0018"), Is.False,
+ "id.SubjectName must not trigger UA0018.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnUnrelatedTypeCertificatePropertyAsync()
+ {
+ const string source = """
+ class Other
+ {
+ public object? Certificate => null;
+ }
+ class C
+ {
+ static object? M(Other o) => o.Certificate;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(
+ new UA0018CertificateIdentifierCertificateAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0018"), Is.False,
+ "Certificate property on an unrelated type must not trigger UA0018.");
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnShimPropertyAccessAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object? M(CertificateIdentifierShimHost host)
+ {
+ #pragma warning disable CS0618
+ return host.Certificate;
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(
+ new UA0018CertificateIdentifierCertificateAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0018"), Is.True,
+ "Expected UA0018 to fire on a property carrying [OpcUaShim(\"UA0018\")].");
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0019Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0019Tests.cs
new file mode 100644
index 0000000000..628778bbcb
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0019Tests.cs
@@ -0,0 +1,153 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0019 (obsolete DataValue(StatusCode[,DateTimeUtc]) constructor).
+ ///
+ [TestFixture]
+ public class UA0019Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnNewDataValueStatusCodeAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static DataValue M(StatusCode sc) => new DataValue(sc);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0019DataValueStatusCodeCtorAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0019"), Is.True);
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnNewDataValueStatusCodeDateTimeUtcAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static DataValue M(StatusCode sc, DateTimeUtc ts) => new DataValue(sc, ts);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0019DataValueStatusCodeCtorAnalyzer(), source);
+
+ Diagnostic? ua0019 = diags.SingleOrDefault(d => d.Id == "UA0019");
+ Assert.That(ua0019, Is.Not.Null);
+ Assert.That(
+ ua0019!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("DateTimeUtc"));
+ }
+
+ [Test]
+ public async Task DoesNotReportOnNewDataValueVariantAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static DataValue M(Variant v) => new DataValue(v);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0019DataValueStatusCodeCtorAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0019"), Is.False);
+ }
+
+ [Test]
+ public async Task FixRewritesStatusCodeCtorToFromStatusCodeAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static DataValue M(StatusCode sc) => new DataValue(sc);
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static DataValue M(StatusCode sc) => DataValue.FromStatusCode(sc);
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0019DataValueStatusCodeCtorAnalyzer(),
+ new UA0019DataValueStatusCodeCtorCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public async Task FixRewritesStatusCodeDateTimeUtcCtorToFromStatusCodeAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static DataValue M(StatusCode sc, DateTimeUtc ts) => new DataValue(sc, ts);
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static DataValue M(StatusCode sc, DateTimeUtc ts) => DataValue.FromStatusCode(sc, ts);
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0019DataValueStatusCodeCtorAnalyzer(),
+ new UA0019DataValueStatusCodeCtorCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0020Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0020Tests.cs
new file mode 100644
index 0000000000..db650c0331
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0020Tests.cs
@@ -0,0 +1,261 @@
+/* ========================================================================
+ * 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.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0020 (EncodeableFactory renames: GlobalFactory and Create).
+ ///
+ [TestFixture]
+ public class UA0020Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnGlobalFactoryAccessAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static EncodeableFactory M() { var f = EncodeableFactory.GlobalFactory; return f; }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0020EncodeableFactoryRenameAnalyzer(), source);
+
+ Diagnostic? ua0020 = diags.SingleOrDefault(d => d.Id == "UA0020");
+ Assert.That(ua0020, Is.Not.Null,
+ "Expected UA0020 to fire on EncodeableFactory.GlobalFactory access.");
+ Assert.That(
+ ua0020!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("GlobalFactory"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnEncodeableFactoryCreateInvocationAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static EncodeableFactory M(EncodeableFactory factory) => factory.Create();
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0020EncodeableFactoryRenameAnalyzer(), source);
+
+ Diagnostic? ua0020 = diags.SingleOrDefault(d => d.Id == "UA0020");
+ Assert.That(ua0020, Is.Not.Null,
+ "Expected UA0020 to fire on EncodeableFactory.Create() invocation.");
+ Assert.That(
+ ua0020!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("Create"));
+ }
+
+ [Test]
+ public async Task DoesNotReportOnForkInvocationAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static EncodeableFactory M(EncodeableFactory factory) => factory.Fork();
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0020EncodeableFactoryRenameAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0020"), Is.False,
+ "factory.Fork() must not trigger UA0020.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnServiceMessageContextFactoryAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static EncodeableFactory M(ServiceMessageContext ctx) => ctx.Factory;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0020EncodeableFactoryRenameAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0020"), Is.False,
+ "ServiceMessageContext.Factory access must not trigger UA0020.");
+ }
+
+ [Test]
+ public async Task FixRewritesCreateInvocationToForkAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static EncodeableFactory M(EncodeableFactory factory) => factory.Create();
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static EncodeableFactory M(EncodeableFactory factory) => factory.Fork();
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0020EncodeableFactoryRenameAnalyzer(),
+ new UA0020EncodeableFactoryRenameCodeFix(),
+ source);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public async Task FixDoesNotRegisterActionForGlobalFactoryFormAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static EncodeableFactory M() => EncodeableFactory.GlobalFactory;
+ }
+ """;
+
+ CodeAction[] actions = await CollectFixActionsAsync(
+ new UA0020EncodeableFactoryRenameAnalyzer(),
+ new UA0020EncodeableFactoryRenameCodeFix(),
+ source);
+
+ Assert.That(actions, Is.Empty,
+ "Form A (GlobalFactory) must not register any code-fix actions.");
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnShimGlobalFactoryAccessAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static EncodeableFactory M()
+ {
+ #pragma warning disable CS0618
+ return EncodeableFactoryShim.GlobalFactory;
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0020EncodeableFactoryRenameAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0020"), Is.True,
+ "Expected UA0020 to fire on a property carrying [OpcUaShim(\"UA0020\")].");
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnShimCreateInvocationAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static EncodeableFactory M(EncodeableFactory factory)
+ {
+ #pragma warning disable CS0618
+ return EncodeableFactoryShim.Create(factory);
+ #pragma warning restore CS0618
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0020EncodeableFactoryRenameAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0020"), Is.True,
+ "Expected UA0020 to fire on a Create invocation carrying [OpcUaShim(\"UA0020\")].");
+ }
+
+ private static async Task CollectFixActionsAsync(
+ Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer analyzer,
+ CodeFixProvider codeFix,
+ string source)
+ {
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(analyzer, source);
+ Diagnostic[] userDiags = diags
+ .Where(d => d.Location.SourceTree?.FilePath == "Test.cs")
+ .ToArray();
+ Assert.That(userDiags, Is.Not.Empty,
+ "Expected at least one diagnostic on Test.cs for the fix-actions probe.");
+
+ Microsoft.CodeAnalysis.CSharp.CSharpCompilation compilation =
+ AnalyzerHarness.Compile(source);
+ Microsoft.CodeAnalysis.SyntaxTree testTree = compilation.SyntaxTrees
+ .First(t => t.FilePath == "Test.cs");
+
+ AdhocWorkspace workspace = new AdhocWorkspace();
+ ProjectId projectId = ProjectId.CreateNewId();
+ DocumentId documentId = DocumentId.CreateNewId(projectId);
+ Solution solution = workspace.CurrentSolution
+ .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp)
+ .AddDocument(documentId, "Test.cs", testTree.GetText());
+ Document document = solution.GetDocument(documentId)!;
+
+ List actions = new List();
+ foreach (Diagnostic diag in userDiags)
+ {
+ CodeFixContext ctx = new CodeFixContext(
+ document,
+ diag,
+ (action, _) => actions.Add(action),
+ CancellationToken.None);
+ await codeFix.RegisterCodeFixesAsync(ctx).ConfigureAwait(false);
+ }
+ return actions.ToArray();
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0021Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0021Tests.cs
new file mode 100644
index 0000000000..b61af0517d
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0021Tests.cs
@@ -0,0 +1,228 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0021 (CertificateValidator / CertificateValidationEventArgs structural rename).
+ /// The rule is diagnostic-only; there is no accompanying code fix because the 1.6
+ /// replacement is structural (event-based per-error accept handler -> async
+ /// ValidateAsync returning CertificateValidationResult plus AcceptError callback).
+ ///
+ [TestFixture]
+ public class UA0021Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnCertificateValidatorReferenceAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(CertificateValidator v) { }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0021CertificateValidatorRenameAnalyzer(), source);
+
+ Diagnostic? ua0021 = diags.SingleOrDefault(d => d.Id == "UA0021");
+ Assert.That(ua0021, Is.Not.Null);
+ Assert.That(
+ ua0021!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("CertificateValidator"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnCertificateValidationEventArgsReferenceAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(CertificateValidationEventArgs e) { }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0021CertificateValidatorRenameAnalyzer(), source);
+
+ Diagnostic? ua0021 = diags.SingleOrDefault(d => d.Id == "UA0021");
+ Assert.That(ua0021, Is.Not.Null);
+ Assert.That(
+ ua0021!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("CertificateValidationEventArgs"));
+ }
+
+ [Test]
+ public async Task DoesNotReportOnReplacementTypesAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(ICertificateManager m, ICertificateValidatorEx v, CertificateValidationResult r) { }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0021CertificateValidatorRenameAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0021"), Is.False);
+ }
+
+ [Test]
+ public async Task DoesNotReportOnUnrelatedSimilarlyNamedTypeAsync()
+ {
+ // A user-defined CertificateValidator in a non-Opc.Ua namespace, with no
+ // 'using Opc.Ua;' anywhere — the symbol resolves to the user's own type
+ // (not [Obsolete]) and the syntactic fallback is also gated by the using.
+ const string source = """
+ namespace Other.Pki
+ {
+ public class CertificateValidator { }
+ public class CertificateValidationEventArgs { }
+ class C
+ {
+ static void M(CertificateValidator v, CertificateValidationEventArgs e) { }
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0021CertificateValidatorRenameAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0021"), Is.False);
+ }
+
+ [Test]
+ public async Task ReportsExactlyTwoDiagnosticsForBothLegacyTypesAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static void M(CertificateValidator v, CertificateValidationEventArgs e) { }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0021CertificateValidatorRenameAnalyzer(), source);
+
+ Assert.That(diags.Count(d => d.Id == "UA0021"), Is.EqualTo(2));
+ }
+
+ ///
+ /// Regression test for the Phase 11.A relaxation of the syntactic-fallback
+ /// using-directive gate. Real-world OPC UA consumer code typically imports
+ /// sub-namespaces like using Opc.Ua.Server; rather than the bare
+ /// using Opc.Ua;. Before the fix, the syntactic fallback only fired
+ /// when the bare directive was present, so UA0021 silently missed every
+ /// reference in files that imported only sub-namespaces. The fix accepts
+ /// any using directive whose name starts with Opc.Ua.
+ ///
+ [Test]
+ public async Task ReportsDiagnosticInFileWithOnlyOpcUaServerUsingAsync()
+ {
+ const string source = """
+ using Opc.Ua.Server; // sub-namespace, no bare Opc.Ua
+ class C
+ {
+ static void M() => System.Console.WriteLine(typeof(CertificateValidator).FullName);
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsWithoutStubsAsync(
+ new UA0021CertificateValidatorRenameAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0021"), Is.True,
+ "UA0021 must fire when the file imports any Opc.Ua sub-namespace (not just bare Opc.Ua).");
+ }
+
+ ///
+ /// Defensive check: code declared inside the Opc.Ua.* namespace tree
+ /// (e.g. a consumer extending the stack) must also trigger the syntactic
+ /// fallback even when there is no using directive at all.
+ ///
+ [Test]
+ public async Task ReportsDiagnosticInNamespaceUnderOpcUaTreeAsync()
+ {
+ const string source = """
+ namespace Opc.Ua.Extensions
+ {
+ class C
+ {
+ static void M() => System.Console.WriteLine(typeof(CertificateValidator).FullName);
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsWithoutStubsAsync(
+ new UA0021CertificateValidatorRenameAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0021"), Is.True);
+ }
+
+ ///
+ /// Negative companion to the relaxation: a file that imports neither
+ /// Opc.Ua nor any sub-namespace, and is not declared under the
+ /// Opc.Ua tree, must NOT trigger the syntactic fallback even if
+ /// it happens to mention an identifier named CertificateValidator.
+ ///
+ [Test]
+ public async Task DoesNotReportInFileWithNoOpcUaUsingOrNamespaceAsync()
+ {
+ const string source = """
+ namespace Other.Pki
+ {
+ class CertificateValidator { }
+ class C
+ {
+ static void M() => System.Console.WriteLine(typeof(CertificateValidator).FullName);
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsWithoutStubsAsync(
+ new UA0021CertificateValidatorRenameAnalyzer(), source);
+
+ Assert.That(diags.Any(d => d.Id == "UA0021"), Is.False);
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0022Tests.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0022Tests.cs
new file mode 100644
index 0000000000..6eefdb5baa
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Analyzers/UA0022Tests.cs
@@ -0,0 +1,161 @@
+/* ========================================================================
+ * 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.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using NUnit.Framework;
+using Opc.Ua.MigrationAnalyzer.Analyzers;
+using Opc.Ua.MigrationAnalyzer.CodeFixer;
+
+namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
+{
+ ///
+ /// Tests for UA0022 (ApplicationConfiguration.CertificateValidator /
+ /// ServerBase.CertificateValidator property rename to CertificateManager).
+ ///
+ [TestFixture]
+ public class UA0022Tests
+ {
+ [Test]
+ public async Task ReportsDiagnosticOnApplicationConfigurationCertificateValidatorAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object? M(ApplicationConfiguration cfg) => cfg.CertificateValidator;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0022CertificateValidatorPropertyRenameAnalyzer(), source).ConfigureAwait(false);
+
+ Diagnostic? ua0022 = diags.SingleOrDefault(d => d.Id == "UA0022");
+ Assert.That(ua0022, Is.Not.Null,
+ "Expected UA0022 to fire on ApplicationConfiguration.CertificateValidator access.");
+ Assert.That(
+ ua0022!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("ApplicationConfiguration"));
+ }
+
+ [Test]
+ public async Task ReportsDiagnosticOnServerBaseCertificateValidatorAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object? M(ServerBase sb) => sb.CertificateValidator;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0022CertificateValidatorPropertyRenameAnalyzer(), source).ConfigureAwait(false);
+
+ Diagnostic? ua0022 = diags.SingleOrDefault(d => d.Id == "UA0022");
+ Assert.That(ua0022, Is.Not.Null,
+ "Expected UA0022 to fire on ServerBase.CertificateValidator access.");
+ Assert.That(
+ ua0022!.GetMessage(System.Globalization.CultureInfo.InvariantCulture),
+ Does.Contain("ServerBase"));
+ }
+
+ [Test]
+ public async Task DoesNotReportOnApplicationConfigurationCertificateManagerAccessAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object? M(ApplicationConfiguration cfg) => cfg.CertificateManager;
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0022CertificateValidatorPropertyRenameAnalyzer(), source).ConfigureAwait(false);
+
+ Assert.That(diags.Any(d => d.Id == "UA0022"), Is.False,
+ "Modern CertificateManager access must not trigger UA0022.");
+ }
+
+ [Test]
+ public async Task DoesNotReportOnUnrelatedTypeCertificateValidatorPropertyAsync()
+ {
+ // No 'using Opc.Ua;' and the type name is unrelated; both detection paths
+ // must remain silent.
+ const string source = """
+ namespace Other.Pki
+ {
+ public class Foo
+ {
+ public int CertificateValidator { get; }
+ }
+ class C
+ {
+ static int M(Foo f) => f.CertificateValidator;
+ }
+ }
+ """;
+
+ ImmutableArray diags = await AnalyzerHarness
+ .GetAnalyzerDiagnosticsAsync(new UA0022CertificateValidatorPropertyRenameAnalyzer(), source).ConfigureAwait(false);
+
+ Assert.That(diags.Any(d => d.Id == "UA0022"), Is.False,
+ "Unrelated CertificateValidator property on a non-Opc.Ua type must not trigger UA0022.");
+ }
+
+ [Test]
+ public async Task FixRewritesCertificateValidatorToCertificateManagerAsync()
+ {
+ const string source = """
+ using Opc.Ua;
+ class C
+ {
+ static object? M(ApplicationConfiguration cfg) => cfg.CertificateValidator;
+ }
+ """;
+ const string expected = """
+ using Opc.Ua;
+ class C
+ {
+ static object? M(ApplicationConfiguration cfg) => cfg.CertificateManager;
+ }
+ """;
+
+ string fixedSource = await AnalyzerHarness.ApplyFixAsync(
+ new UA0022CertificateValidatorPropertyRenameAnalyzer(),
+ new UA0022CertificateValidatorPropertyRenameCodeFix(),
+ source).ConfigureAwait(false);
+
+ Assert.That(fixedSource, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Opc.Ua.MigrationAnalyzer.Tests.csproj b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Opc.Ua.MigrationAnalyzer.Tests.csproj
new file mode 100644
index 0000000000..9c4e2a9f88
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Opc.Ua.MigrationAnalyzer.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+ $(TestsTargetFrameworks)
+ Opc.Ua.MigrationAnalyzer.Tests
+ true
+ true
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Properties/AssemblyInfo.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..2b9848014c
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+/* ========================================================================
+ * 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;
+
+[assembly: CLSCompliant(false)]
diff --git a/Tests/Opc.Ua.MigrationAnalyzer.Tests/Stubs/OpcUaStubs.cs b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Stubs/OpcUaStubs.cs
new file mode 100644
index 0000000000..5ee667f970
--- /dev/null
+++ b/Tests/Opc.Ua.MigrationAnalyzer.Tests/Stubs/OpcUaStubs.cs
@@ -0,0 +1,538 @@
+/* ========================================================================
+ * 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/
+ * ======================================================================*/
+
+namespace Opc.Ua.MigrationAnalyzer.Tests
+{
+ ///
+ /// Minimal hand-written OPC UA 2.0 stubs used by the analyzer tests.
+ /// Stubs are kept narrow on purpose - they reproduce just the public
+ /// surface that the analyzers key off (struct vs class, [Obsolete],
+ /// member signatures). Anything beyond that risks teaching tests to
+ /// pass against the wrong shape.
+ ///
+ public static class OpcUaStubs
+ {
+ public const string Source =
+"""
+#nullable enable
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+namespace Opc.Ua
+{
+ public readonly struct NodeId
+ {
+ public NodeId(uint id) { Identifier = id; NamespaceIndex = 0; }
+ public NodeId(uint id, ushort ns) { Identifier = id; NamespaceIndex = ns; }
+ [Obsolete("Use NodeId.Parse(string) instead.")]
+ public NodeId(string s) { Identifier = s; NamespaceIndex = 0; }
+ public object Identifier { get; }
+ public ushort NamespaceIndex { get; }
+ public bool IsNull => Identifier is null;
+ public static NodeId Parse(string s) => default;
+ public static bool TryParse(string s, out NodeId id) { id = default; return true; }
+ public override string ToString() => Identifier?.ToString() ?? "";
+ public override bool Equals(object? obj) => obj is NodeId n && Equals(Identifier, n.Identifier);
+ public override int GetHashCode() => Identifier?.GetHashCode() ?? 0;
+ public static bool operator ==(NodeId left, object? right) => right is null ? left.IsNull : left.Equals(right);
+ public static bool operator !=(NodeId left, object? right) => !(left == right);
+ public static bool operator ==(object? left, NodeId right) => right == left;
+ public static bool operator !=(object? left, NodeId right) => right != left;
+ }
+
+ public readonly struct ExpandedNodeId
+ {
+ public ExpandedNodeId(uint id) { Identifier = id; }
+ [Obsolete("Use ExpandedNodeId.Parse(string) instead.")]
+ public ExpandedNodeId(string s) { Identifier = s; }
+ public object Identifier { get; }
+ public bool IsNull => Identifier is null;
+ public static ExpandedNodeId Parse(string s) => default;
+ }
+
+ public readonly struct QualifiedName
+ {
+ public QualifiedName(string name) { Name = name; }
+ public string Name { get; }
+ public bool IsNull => string.IsNullOrEmpty(Name);
+ }
+
+ public readonly struct LocalizedText
+ {
+ public LocalizedText(string text) { Text = text; }
+ public string Text { get; }
+ public bool IsNullOrEmpty => string.IsNullOrEmpty(Text);
+ public override bool Equals(object? obj) => obj is LocalizedText lt && Text == lt.Text;
+ public override int GetHashCode() => Text?.GetHashCode() ?? 0;
+ public static bool operator ==(LocalizedText left, object? right) => right is null ? left.IsNullOrEmpty : left.Equals(right);
+ public static bool operator !=(LocalizedText left, object? right) => !(left == right);
+ public static bool operator ==(object? left, LocalizedText right) => right == left;
+ public static bool operator !=(object? left, LocalizedText right) => right != left;
+ }
+
+ public readonly struct ByteString
+ {
+ public ByteString(byte[] data) { Data = data; }
+ public byte[]? Data { get; }
+ public bool IsNull => Data is null;
+ public Span Span => Data;
+ }
+
+ public static class ByteStringExtensions
+ {
+ public static ByteString ToByteString(this byte[] data) => new ByteString(data);
+ }
+
+ public readonly struct StatusCode
+ {
+ public StatusCode(uint code) { Code = code; }
+ public uint Code { get; }
+ }
+
+ public readonly struct DateTimeUtc
+ {
+ public DateTimeUtc(DateTime dt) { UtcDateTime = dt; }
+ public DateTime UtcDateTime { get; }
+ }
+
+ public readonly struct Uuid
+ {
+ public Uuid(Guid g) { Value = g; }
+ public Guid Value { get; }
+ }
+
+ public readonly struct Variant
+ {
+ public Variant(int i) { _value = i; }
+ public Variant(uint i) { _value = i; }
+ public Variant(string s) { _value = s; }
+ public Variant(NodeId n) { _value = n; }
+ [Obsolete("Use Variant.From(object) instead.")]
+ public Variant(object o) { _value = o; }
+ [Obsolete("Use Variant.From(new DateTimeUtc(value)) instead.")]
+ public Variant(DateTime dt) { _value = dt; }
+ [Obsolete("Use Variant.From(new Uuid(value)) instead.")]
+ public Variant(Guid g) { _value = g; }
+ [Obsolete("Use Variant.From(value.ToByteString()) instead.")]
+ public Variant(byte[] b) { _value = b; }
+ private readonly object? _value;
+ public bool IsNull => _value is null;
+ public static Variant Null => default;
+ public static Variant From(T value) => default;
+ }
+
+ public readonly struct ExtensionObject
+ {
+ public ExtensionObject(object body) { Body = body; }
+ public object Body { get; }
+ public bool IsNull => Body is null;
+ }
+
+ public readonly struct DiagnosticInfo
+ {
+ }
+
+ public readonly struct DataValue
+ {
+ public DataValue(Variant v) { Value = v; StatusCode = default; SourceTimestamp = default; }
+ [Obsolete("Use DataValue.FromStatusCode(StatusCode) instead.")]
+ public DataValue(StatusCode sc) { Value = default; StatusCode = sc; SourceTimestamp = default; }
+ [Obsolete("Use DataValue.FromStatusCode(StatusCode, DateTimeUtc) instead.")]
+ public DataValue(StatusCode sc, DateTimeUtc ts) { Value = default; StatusCode = sc; SourceTimestamp = ts; }
+ public Variant Value { get; }
+ public StatusCode StatusCode { get; }
+ public DateTimeUtc SourceTimestamp { get; }
+ public bool IsGood => StatusCode.Code == 0;
+ public bool IsBad => (StatusCode.Code & 0x80000000) != 0;
+ public bool IsUncertain => (StatusCode.Code & 0x40000000) != 0;
+ public bool IsNull => false;
+ public static DataValue Null => default;
+ public static DataValue FromStatusCode(StatusCode sc) => default;
+ public static DataValue FromStatusCode(StatusCode sc, DateTimeUtc ts) => default;
+ [Obsolete("Use the dv.IsGood instance property.")]
+ public static bool IsGood(DataValue dv) => dv.IsGood;
+ [Obsolete("Use the dv.IsBad instance property.")]
+ public static bool IsBad(DataValue dv) => dv.IsBad;
+ [Obsolete("Use the dv.IsUncertain instance property.")]
+ public static bool IsUncertain(DataValue dv) => dv.IsUncertain;
+ [Obsolete("Use the !dv.IsGood instance property.")]
+ public static bool IsNotGood(DataValue dv) => !dv.IsGood;
+ [Obsolete("Use the !dv.IsBad instance property.")]
+ public static bool IsNotBad(DataValue dv) => !dv.IsBad;
+ [Obsolete("Use the !dv.IsUncertain instance property.")]
+ public static bool IsNotUncertain(DataValue dv) => !dv.IsUncertain;
+ }
+
+ public static class DataValueExtensions
+ {
+ [Obsolete("Use the dv.IsGood instance property.")]
+ public static bool IsGood(DataValue dv) => dv.IsGood;
+ [Obsolete("Use the dv.IsBad instance property.")]
+ public static bool IsBad(DataValue dv) => dv.IsBad;
+ [Obsolete("Use the dv.IsUncertain instance property.")]
+ public static bool IsUncertain(DataValue dv) => dv.IsUncertain;
+ [Obsolete("Use the !dv.IsGood instance property.")]
+ public static bool IsNotGood(DataValue dv) => !dv.IsGood;
+ [Obsolete("Use the !dv.IsBad instance property.")]
+ public static bool IsNotBad(DataValue dv) => !dv.IsBad;
+ [Obsolete("Use the !dv.IsUncertain instance property.")]
+ public static bool IsNotUncertain(DataValue dv) => !dv.IsUncertain;
+ }
+
+ public static class CertificateFactory
+ {
+ [Obsolete("Use DefaultCertificateFactory.Instance.Create.")]
+ public static object Create(string subject) => null!;
+ [Obsolete("Use DefaultCertificateFactory.Instance.CreateCertificate.")]
+ public static object CreateCertificate(string subject) => null!;
+ [Obsolete("Use DefaultCertificateFactory.Instance.CreateSigningRequest.")]
+ public static object CreateSigningRequest(string subject) => null!;
+ [Obsolete("Use DefaultCertificateFactory.Instance.RevokeCertificate.")]
+ public static object RevokeCertificate(string subject) => null!;
+ [Obsolete("Use DefaultCertificateFactory.Instance.CreateCertificateWithPEMPrivateKey.")]
+ public static object CreateCertificateWithPEMPrivateKey(string subject) => null!;
+ [Obsolete("Use DefaultCertificateFactory.Instance.CreateCertificateWithPrivateKey.")]
+ public static object CreateCertificateWithPrivateKey(string subject) => null!;
+ }
+
+ public sealed class DefaultCertificateFactory
+ {
+ public static DefaultCertificateFactory Instance { get; } = new();
+ public object Create(string subject) => null!;
+ public object CreateCertificate(string subject) => null!;
+ public object CreateSigningRequest(string subject) => null!;
+ public object RevokeCertificate(string subject) => null!;
+ public object CreateCertificateWithPEMPrivateKey(string subject) => null!;
+ public object CreateCertificateWithPrivateKey(string subject) => null!;
+ }
+
+ // ─── Stubs for UA0001 (Utils.Trace/LogX → ILogger) ───
+ public interface ITelemetryContext
+ {
+ Microsoft.Extensions.Logging.ILogger CreateLogger();
+ }
+ public static partial class Utils
+ {
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void Trace(string message) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void Trace(string format, params object[] args) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void Trace(Exception e, string message) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void Trace(int traceMask, string format, params object[] args) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogError(string message) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogError(string format, params object[] args) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogError(Exception e, string format, params object[] args) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogWarning(string message) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogWarning(string format, params object[] args) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogInformation(string message) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogInformation(string format, params object[] args) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogDebug(string message) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogDebug(string format, params object[] args) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogTrace(string message) { }
+ [Obsolete("Use a customized ITelemetryContext.LoggerFactory instead.")]
+ public static void LogCritical(string message) { }
+ }
+ public static class TraceMasks
+ {
+ public const int Error = 1;
+ public const int Information = 2;
+ }
+
+ // ─── Stubs for UA0002 (Collection types removed) ───
+ public class Int32Collection : List { }
+ public class UInt32Collection : List { }
+ public class StringCollection : List { }
+ public class NodeIdCollection : List { }
+ public class VariantCollection : List { }
+ public class DataValueCollection : List { }
+ public class ByteStringCollection : List { }
+ public class ArgumentCollection : List { }
+ public class ServerSecurityPolicyCollection : List { }
+ public class TransportConfigurationCollection : List { }
+ public class ReverseConnectClientCollection : List { }
+ public class Argument { }
+ public class ServerSecurityPolicy { }
+ public class TransportConfiguration { }
+ public class ReverseConnectClient { }
+ public readonly struct ArrayOf
+ {
+ public ArrayOf(T[] data) { Data = data; }
+ public T[]? Data { get; }
+ }
+
+ // ─── Stubs for UA0005 (byte[] → ByteString) ───
+ // ByteString is already defined above; expose an API surface that takes ByteString.
+ public static class ByteStringApi
+ {
+ public static void Process(ByteString data) { }
+ public static void Process(byte[] data) { }
+ }
+
+ // ─── Stubs for UA0008 (Session.Call params object[] → params Variant[]) ───
+ public interface ISession
+ {
+ object Call(NodeId objectId, NodeId methodId, params Variant[] args);
+ Task