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 CallAsync(NodeId objectId, NodeId methodId, CancellationToken ct, params Variant[] args); + } + public class Session : ISession + { + public object Call(NodeId objectId, NodeId methodId, params Variant[] args) => null!; + public Task CallAsync(NodeId objectId, NodeId methodId, CancellationToken ct, params Variant[] args) => Task.FromResult(null!); + } + + // ─── Stubs for UA0009 ([DataContract]/[DataMember] → [DataType]/[DataTypeField]) ─── + [AttributeUsage(AttributeTargets.Class)] + public sealed class DataTypeAttribute : Attribute + { + public string? TypeId { get; set; } + } + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public sealed class DataTypeFieldAttribute : Attribute + { + public int Order { get; set; } + } + public class ApplicationConfiguration + { + public static T ParseExtension() where T : new() => new(); + public static void UpdateExtension(T value) { } + + // ─── Stubs for UA0022 (CertificateValidator → CertificateManager property rename) ─── + [Obsolete("Use CertificateManager instead.")] + public object? CertificateValidator { get; set; } + public object? CertificateManager { get; set; } + } + + public class ServerBase + { + [Obsolete("Use CertificateManager instead.")] + public object? CertificateValidator { get; set; } + public object? CertificateManager { get; set; } + } + + // ─── Stubs for UA0010 (Remove IDisposable on cert/identity types) ─── + // CertificateIdentifier intentionally does NOT implement IDisposable in 2.0. + public class CertificateIdentifier + { + public string? SubjectName { get; set; } + } + public class UserIdentity + { + public string? Name { get; set; } + } + + // ─── Stubs for UA0011 (TokenHandler sync → async) ─── + public interface IUserIdentityTokenHandler + { + [Obsolete("Use EncryptAsync instead.")] + byte[] Encrypt(byte[] data); + [Obsolete("Use DecryptAsync instead.")] + byte[] Decrypt(byte[] data); + [Obsolete("Use SignAsync instead.")] + byte[] Sign(byte[] data); + [Obsolete("Use VerifyAsync instead.")] + bool Verify(byte[] data, byte[] signature); + Task EncryptAsync(byte[] data, CancellationToken ct); + Task DecryptAsync(byte[] data, CancellationToken ct); + Task SignAsync(byte[] data, CancellationToken ct); + Task VerifyAsync(byte[] data, byte[] signature, CancellationToken ct); + } + + // ─── Stubs for UA0015 (GDS sync → async) ─── + public class GlobalDiscoveryServerClient + { + [Obsolete("Use RegisterApplicationAsync instead.")] + public void RegisterApplication(string applicationUri) { } + [Obsolete("Use UnregisterApplicationAsync instead.")] + public void UnregisterApplication(string applicationUri) { } + public Task RegisterApplicationAsync(string applicationUri, CancellationToken ct) => Task.CompletedTask; + public Task UnregisterApplicationAsync(string applicationUri, CancellationToken ct) => Task.CompletedTask; + } + public class ServerPushConfigurationClient + { + [Obsolete("Use ApplyChangesAsync instead.")] + public void ApplyChanges() { } + public Task ApplyChangesAsync(CancellationToken ct) => Task.CompletedTask; + } + public class LocalDiscoveryServerClient + { + [Obsolete("Use FindServersAsync instead.")] + public IAsyncResult BeginFindServers(string endpoint, AsyncCallback? callback, object? state) => null!; + [Obsolete("Use FindServersAsync instead.")] + public string[] EndFindServers(IAsyncResult result) => System.Array.Empty(); + public Task FindServersAsync(string endpoint, CancellationToken ct) => Task.FromResult(System.Array.Empty()); + } + + // ─── Stubs for UA0018 (CertificateIdentifier.Certificate → ResolveAsync) ─── + // Add a getter that's marked obsolete on the CertificateIdentifier defined above + // (we provide a separate type with the obsolete getter for test isolation). + public class CertificateIdentifierWithObsoleteCertificate + { + [Obsolete("Use CertificateIdentifierResolver.ResolveAsync instead.")] + public object? Certificate => null; + } + public static class CertificateIdentifierResolver + { + public static Task ResolveAsync( + CertificateIdentifier id, + object registry, + bool needPrivateKey, + string applicationUri, + ITelemetryContext telemetry, + CancellationToken ct) => Task.FromResult(null!); + } + + // ─── Stubs for UA0020 (EncodeableFactory renames) ─── + public class EncodeableFactory + { + [Obsolete("Use ServiceMessageContext.Factory instead.")] + public static EncodeableFactory GlobalFactory { get; } = new EncodeableFactory(); + [Obsolete("Use Fork() instead.")] + public EncodeableFactory Create() => new EncodeableFactory(); + public EncodeableFactory Fork() => new EncodeableFactory(); + } + public class ServiceMessageContext + { + public EncodeableFactory Factory { get; } = new EncodeableFactory(); + } + + // ─── Stubs for UA0021 (CertificateValidator / CertificateValidationEventArgs rename) ─── + // The legacy types are kept here so the analyzer's "symbol-present + [Obsolete]" branch + // can be exercised. The 1.6 replacements (ICertificateManager, ICertificateValidatorEx, + // CertificateValidationResult) are stubbed to verify the analyzer does NOT fire on them. + [Obsolete("Use ICertificateManager (via CertificateManagerFactory.Create) instead. See MigrationGuide.md#ua0021.")] + public class CertificateValidator { } + [Obsolete("Use CertificateValidationResult returned from ICertificateValidatorEx.ValidateAsync instead. See MigrationGuide.md#ua0021.")] + public class CertificateValidationEventArgs : EventArgs { } + public interface ICertificateManager { } + public interface ICertificateValidatorEx { } + public class CertificateValidationResult { } + + // ─── OpcUaShim marker attribute and shim wrappers used by analyzer tests ─── + [AttributeUsage(AttributeTargets.All, AllowMultiple = false)] + public sealed class OpcUaShimAttribute : Attribute + { + public string RuleId { get; } + public OpcUaShimAttribute(string ruleId) { RuleId = ruleId; } + } + + // Shim for UA0008: Call/CallAsync with raw object args on ISession-like receiver. + public static class SessionShim + { + [Obsolete("Use ISession.Call(params Variant[]) instead.")] + [OpcUaShim("UA0008")] + public static object Call(this ISession session, NodeId objectId, NodeId methodId, params object[] args) + => null!; + + [Obsolete("Use ISession.CallAsync(params Variant[]) instead.")] + [OpcUaShim("UA0008")] + public static Task CallAsync( + this ISession session, NodeId objectId, NodeId methodId, CancellationToken ct, params object[] args) + => Task.FromResult(null!); + } + + // Shim for UA0011: synchronous Encrypt/Decrypt/Sign/Verify on token handler. + public static class UserIdentityTokenHandlerShim + { + [Obsolete("Use EncryptAsync instead.")] + [OpcUaShim("UA0011")] + public static byte[] Encrypt(this IUserIdentityTokenHandler handler, byte[] data) => null!; + + [Obsolete("Use DecryptAsync instead.")] + [OpcUaShim("UA0011")] + public static byte[] Decrypt(this IUserIdentityTokenHandler handler, byte[] data) => null!; + + // Same shape, different RuleId — used to verify rule-id filtering. + [Obsolete("Different-rule shim used for negative test.")] + [OpcUaShim("UA9999")] + public static byte[] EncryptUnrelated(this IUserIdentityTokenHandler handler, byte[] data) => null!; + } + + // Shim for UA0015: synchronous GDS/LDS members. + public static class GdsClientShim + { + [Obsolete("Use RegisterApplicationAsync instead.")] + [OpcUaShim("UA0015")] + public static void RegisterApplicationLegacy(this GlobalDiscoveryServerClient client, string applicationUri) { } + + [Obsolete("Use FindServersAsync instead.")] + [OpcUaShim("UA0015")] + public static string[] FindServersLegacy(this LocalDiscoveryServerClient client, string endpoint) + => System.Array.Empty(); + } + + // Shim for UA0018: CertificateIdentifier.Certificate getter relocated to shim. + public class CertificateIdentifierShimHost + { + [Obsolete("Use CertificateIdentifierResolver.ResolveAsync instead.")] + [OpcUaShim("UA0018")] + public object? Certificate => null; + } + + // Shim for UA0020: EncodeableFactory.GlobalFactory / Create relocated to shim. + public static class EncodeableFactoryShim + { + [Obsolete("Use ServiceMessageContext.Factory instead.")] + [OpcUaShim("UA0020")] + public static EncodeableFactory GlobalFactory => new EncodeableFactory(); + + [Obsolete("Use Fork() instead.")] + [OpcUaShim("UA0020")] + public static EncodeableFactory Create(this EncodeableFactory factory) => new EncodeableFactory(); + } +} + +namespace Microsoft.Extensions.Logging +{ + public interface ILogger + { + void LogError(string message); + void LogWarning(string message); + void LogInformation(string message); + void LogDebug(string message); + void LogTrace(string message); + void LogCritical(string message); + } +} +"""; + } +} diff --git a/Tests/Opc.Ua.Types.Tests/Opc.Ua.Types.Tests.csproj b/Tests/Opc.Ua.Types.Tests/Opc.Ua.Types.Tests.csproj index 42dc9df060..6d535d3db6 100644 --- a/Tests/Opc.Ua.Types.Tests/Opc.Ua.Types.Tests.csproj +++ b/Tests/Opc.Ua.Types.Tests/Opc.Ua.Types.Tests.csproj @@ -41,6 +41,10 @@ + + diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0002RemovedCollectionTypeCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0002RemovedCollectionTypeCodeFix.cs new file mode 100644 index 0000000000..b4c60cef71 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0002RemovedCollectionTypeCodeFix.cs @@ -0,0 +1,116 @@ +/* ======================================================================== + * 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.Composition; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0002 code fix: rewrite every reference to a removed + /// <Type>Collection wrapper as List<TElement>. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0002RemovedCollectionTypeCodeFix)), Shared] + public sealed class UA0002RemovedCollectionTypeCodeFix : CodeFixProvider + { + private static readonly Dictionary s_shortNameToElement = BuildShortNameMap(); + + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0002); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (Diagnostic diagnostic in context.Diagnostics) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Use 'List'", + createChangedDocument: ct => ApplyAsync(context.Document, ct), + equivalenceKey: DiagnosticIds.UA0002), + diagnostic); + } + return Task.CompletedTask; + } + + private static async Task ApplyAsync(Document document, CancellationToken cancellationToken) + { + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + CollectionRewriter rewriter = new CollectionRewriter(); + SyntaxNode newRoot = rewriter.Visit(root); + return document.WithSyntaxRoot(newRoot); + } + + private static Dictionary BuildShortNameMap() + { + Dictionary map = new Dictionary(); + foreach ((string collectionName, string elementName) in SymbolExtensions.RemovedCollectionTypes) + { + string shortCollection = StripNamespace(collectionName); + string shortElement = StripNamespace(elementName); + map[shortCollection] = shortElement; + } + return map; + } + + private static string StripNamespace(string name) + { + int lastDot = name.LastIndexOf('.'); + return lastDot < 0 ? name : name.Substring(lastDot + 1); + } + + private sealed class CollectionRewriter : CSharpSyntaxRewriter + { + public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) + { + if (s_shortNameToElement.TryGetValue(node.Identifier.ValueText, out string elementName)) + { + GenericNameSyntax replacement = SyntaxFactory.GenericName( + SyntaxFactory.Identifier("List"), + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.ParseTypeName(elementName)))); + return replacement.WithTriviaFrom(node); + } + return base.VisitIdentifierName(node); + } + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0003NullCheckOnStructTypeCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0003NullCheckOnStructTypeCodeFix.cs new file mode 100644 index 0000000000..c39d0643b7 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0003NullCheckOnStructTypeCodeFix.cs @@ -0,0 +1,134 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0003 code fix: rewrite x == null as x.IsNull (or + /// x.IsNullOrEmpty for LocalizedText) and x != null as the + /// negated form. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0003NullCheckOnStructTypeCodeFix)), Shared] + public sealed class UA0003NullCheckOnStructTypeCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0003); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + BinaryExpressionSyntax binary = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(b => b.IsKind(SyntaxKind.EqualsExpression) || b.IsKind(SyntaxKind.NotEqualsExpression)); + if (binary is null) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Use '.IsNull' instead of null comparison", + createChangedDocument: ct => ApplyAsync(context.Document, binary, ct), + equivalenceKey: DiagnosticIds.UA0003), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + BinaryExpressionSyntax binary, + CancellationToken cancellationToken) + { + SemanticModel model = (await document.GetSemanticModelAsync(cancellationToken) + .ConfigureAwait(false))!; + + ExpressionSyntax valueExpr = GetValueExpression(binary); + if (valueExpr is null) + { + return document; + } + + ITypeSymbol valueType = model.GetTypeInfo(valueExpr, cancellationToken).Type; + if (valueType is INamedTypeSymbol nullable && + nullable.OriginalDefinition?.SpecialType == SpecialType.System_Nullable_T && + nullable.TypeArguments.Length == 1) + { + valueType = nullable.TypeArguments[0]; + } + string memberName = valueType?.Name == "LocalizedText" ? "IsNullOrEmpty" : "IsNull"; + + MemberAccessExpressionSyntax access = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + valueExpr.WithoutTrivia(), + SyntaxFactory.IdentifierName(memberName)); + + ExpressionSyntax replacement = binary.IsKind(SyntaxKind.EqualsExpression) + ? (ExpressionSyntax)access + : SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, access); + + replacement = replacement.WithTriviaFrom(binary); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(binary, replacement); + return document.WithSyntaxRoot(newRoot); + } + + private static ExpressionSyntax GetValueExpression(BinaryExpressionSyntax binary) + { + if (binary.Left.IsKind(SyntaxKind.NullLiteralExpression)) + { + return binary.Right; + } + if (binary.Right.IsKind(SyntaxKind.NullLiteralExpression)) + { + return binary.Left; + } + return null; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0004ConditionalAccessOnStructCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0004ConditionalAccessOnStructCodeFix.cs new file mode 100644 index 0000000000..1b5895f3b7 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0004ConditionalAccessOnStructCodeFix.cs @@ -0,0 +1,119 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0004 code fix: drop the leading ?. of a null-conditional chain + /// whose receiver is a now-struct type, leaving any deeper ?. intact. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0004ConditionalAccessOnStructCodeFix)), Shared] + public sealed class UA0004ConditionalAccessOnStructCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0004); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + ConditionalAccessExpressionSyntax condAccess = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (condAccess is null) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Drop '?.' on value-type receiver", + createChangedDocument: ct => ApplyAsync(context.Document, condAccess, ct), + equivalenceKey: DiagnosticIds.UA0004), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + ConditionalAccessExpressionSyntax condAccess, + CancellationToken cancellationToken) + { + ExpressionSyntax receiver = condAccess.Expression; + ExpressionSyntax whenNotNull = condAccess.WhenNotNull; + + MemberBindingExpressionSyntax firstBinding = whenNotNull + .DescendantNodesAndSelf() + .OfType() + .FirstOrDefault(); + if (firstBinding is null) + { + return document; + } + + MemberAccessExpressionSyntax memberAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + receiver.WithoutTrivia(), + firstBinding.Name); + + ExpressionSyntax replacement; + if (firstBinding == whenNotNull) + { + replacement = memberAccess; + } + else + { + replacement = whenNotNull.ReplaceNode(firstBinding, memberAccess); + } + replacement = replacement.WithTriviaFrom(condAccess); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(condAccess, replacement); + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0005ByteArrayToByteStringCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0005ByteArrayToByteStringCodeFix.cs new file mode 100644 index 0000000000..5505f7f8b0 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0005ByteArrayToByteStringCodeFix.cs @@ -0,0 +1,101 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0005 code fix: append .ToByteString() to a byte[] + /// argument that needs to become a ByteString. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0005ByteArrayToByteStringCodeFix)), Shared] + public sealed class UA0005ByteArrayToByteStringCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0005); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + ArgumentSyntax argument = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (argument is null) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Append '.ToByteString()'", + createChangedDocument: ct => ApplyAsync(context.Document, argument, ct), + equivalenceKey: DiagnosticIds.UA0005), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + ArgumentSyntax argument, + CancellationToken cancellationToken) + { + ExpressionSyntax expr = argument.Expression; + + InvocationExpressionSyntax newInvocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + expr.WithoutTrivia(), + SyntaxFactory.IdentifierName("ToByteString"))); + + ArgumentSyntax newArgument = argument.WithExpression(newInvocation); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(argument, newArgument); + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0006ObsoleteVariantCtorCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0006ObsoleteVariantCtorCodeFix.cs new file mode 100644 index 0000000000..1e59647385 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0006ObsoleteVariantCtorCodeFix.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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0006 code fix: rewrite obsolete new Variant(...) as + /// Variant.From(...), wrapping DateTime/Guid/byte[] arguments with + /// the appropriate strongly-typed surface (DateTimeUtc, Uuid, ByteString). + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0006ObsoleteVariantCtorCodeFix)), Shared] + public sealed class UA0006ObsoleteVariantCtorCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0006); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + ObjectCreationExpressionSyntax creation = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (creation is null || + creation.ArgumentList is null || + creation.ArgumentList.Arguments.Count != 1) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Use 'Variant.From(...)'", + createChangedDocument: ct => ApplyAsync(context.Document, creation, ct), + equivalenceKey: DiagnosticIds.UA0006), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + ObjectCreationExpressionSyntax creation, + CancellationToken cancellationToken) + { + SemanticModel model = (await document.GetSemanticModelAsync(cancellationToken) + .ConfigureAwait(false))!; + + ArgumentSyntax arg = creation.ArgumentList!.Arguments[0]; + ITypeSymbol argType = model.GetTypeInfo(arg.Expression, cancellationToken).Type; + ExpressionSyntax inner = BuildInner(arg.Expression.WithoutTrivia(), argType); + + InvocationExpressionSyntax replacement = SyntaxFactory + .InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Variant"), + SyntaxFactory.IdentifierName("From")), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(inner)))) + .WithTriviaFrom(creation); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(creation, replacement); + return document.WithSyntaxRoot(newRoot); + } + + private static ExpressionSyntax BuildInner(ExpressionSyntax argExpr, ITypeSymbol argType) + { + if (argType is null) + { + return argExpr; + } + switch (argType.SpecialType) + { + case SpecialType.System_DateTime: + return SyntaxFactory.ObjectCreationExpression( + SyntaxFactory.IdentifierName("DateTimeUtc"), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(argExpr))), + initializer: null); + } + if (argType.ToDisplayString() == "System.Guid") + { + return SyntaxFactory.ObjectCreationExpression( + SyntaxFactory.IdentifierName("Uuid"), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(argExpr))), + initializer: null); + } + if (argType is IArrayTypeSymbol arr && + arr.ElementType?.SpecialType == SpecialType.System_Byte) + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + argExpr, + SyntaxFactory.IdentifierName("ToByteString"))); + } + return argExpr; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0007ObsoleteNodeIdStringCtorCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0007ObsoleteNodeIdStringCtorCodeFix.cs new file mode 100644 index 0000000000..a579cc6dab --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0007ObsoleteNodeIdStringCtorCodeFix.cs @@ -0,0 +1,120 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0007 code fix: rewrite new NodeId(s) / new ExpandedNodeId(s) + /// as the corresponding Parse(s) call. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0007ObsoleteNodeIdStringCtorCodeFix)), Shared] + public sealed class UA0007ObsoleteNodeIdStringCtorCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0007); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + ObjectCreationExpressionSyntax creation = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (creation is null || creation.ArgumentList is null) + { + continue; + } + + string typeName = GetTypeName(creation); + if (typeName is null) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: $"Use '{typeName}.Parse(s)'", + createChangedDocument: ct => ApplyAsync(context.Document, creation, typeName, ct), + equivalenceKey: $"{DiagnosticIds.UA0007}:{typeName}"), + diagnostic); + } + } + + private static string GetTypeName(ObjectCreationExpressionSyntax creation) + { + switch (creation.Type) + { + case IdentifierNameSyntax id: + return id.Identifier.ValueText; + case QualifiedNameSyntax qn: + return qn.Right.Identifier.ValueText; + default: + return null; + } + } + + private static async Task ApplyAsync( + Document document, + ObjectCreationExpressionSyntax creation, + string typeName, + CancellationToken cancellationToken) + { + InvocationExpressionSyntax replacement = SyntaxFactory + .InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(typeName), + SyntaxFactory.IdentifierName("Parse")), + creation.ArgumentList!) + .WithTriviaFrom(creation); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(creation, replacement); + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0008SessionCallParamsObjectCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0008SessionCallParamsObjectCodeFix.cs new file mode 100644 index 0000000000..db6fea1f02 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0008SessionCallParamsObjectCodeFix.cs @@ -0,0 +1,143 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0008 code fix: wrap each variadic argument of ISession.Call / + /// ISession.CallAsync with Variant.From(...) (replacing + /// null literals with Variant.Null). + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0008SessionCallParamsObjectCodeFix)), Shared] + public sealed class UA0008SessionCallParamsObjectCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0008); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + InvocationExpressionSyntax invocation = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (invocation is null || invocation.ArgumentList is null) + { + continue; + } + + if (!diagnostic.Properties.TryGetValue( + WellKnownProperties.MethodName, + out string methodName)) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Wrap arguments with 'Variant.From(...)'", + createChangedDocument: ct => ApplyAsync(context.Document, invocation, methodName, ct), + equivalenceKey: DiagnosticIds.UA0008), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + InvocationExpressionSyntax invocation, + string methodName, + CancellationToken cancellationToken) + { + int firstVariadicIndex = methodName == "Call" ? 2 : 3; + SeparatedSyntaxList originalArgs = invocation.ArgumentList.Arguments; + + List newArgs = new List(originalArgs.Count); + for (int i = 0; i < originalArgs.Count; i++) + { + ArgumentSyntax arg = originalArgs[i]; + if (i < firstVariadicIndex) + { + newArgs.Add(arg); + continue; + } + newArgs.Add(arg.WithExpression(Wrap(arg.Expression))); + } + + InvocationExpressionSyntax newInvocation = invocation + .WithArgumentList(invocation.ArgumentList.WithArguments( + SyntaxFactory.SeparatedList(newArgs))); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(invocation, newInvocation); + return document.WithSyntaxRoot(newRoot); + } + + private static ExpressionSyntax Wrap(ExpressionSyntax expression) + { + if (expression is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.NullLiteralExpression)) + { + return SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Variant"), + SyntaxFactory.IdentifierName("Null")) + .WithTriviaFrom(expression); + } + + ExpressionSyntax stripped = expression.WithoutTrivia(); + InvocationExpressionSyntax call = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Variant"), + SyntaxFactory.IdentifierName("From")), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(stripped)))); + return call.WithTriviaFrom(expression); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0009DataContractToDataTypeCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0009DataContractToDataTypeCodeFix.cs new file mode 100644 index 0000000000..9738dfaed5 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0009DataContractToDataTypeCodeFix.cs @@ -0,0 +1,218 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0009 code fix: rewrite + /// [DataContract]/[DataMember] attributes to the OPC UA 2.0 + /// [DataType]/[DataTypeField] equivalents, mark the class + /// partial, and add using Opc.Ua; when missing. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0009DataContractToDataTypeCodeFix)), Shared] + public sealed class UA0009DataContractToDataTypeCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0009); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + ClassDeclarationSyntax classDecl = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (classDecl is null) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Migrate to [DataType]/[DataTypeField] and add 'partial'", + createChangedDocument: ct => ApplyAsync(context.Document, classDecl, ct), + equivalenceKey: DiagnosticIds.UA0009), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + ClassDeclarationSyntax classDecl, + CancellationToken cancellationToken) + { + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + + ClassDeclarationSyntax rewritten = RewriteClass(classDecl); + SyntaxNode newRoot = root.ReplaceNode(classDecl, rewritten); + + if (newRoot is CompilationUnitSyntax compilationUnit) + { + newRoot = EnsureUsing(compilationUnit, "Opc.Ua"); + } + + return document.WithSyntaxRoot(newRoot); + } + + private static ClassDeclarationSyntax RewriteClass(ClassDeclarationSyntax classDecl) + { + SyntaxList newAttributeLists = RewriteAttributeLists(classDecl.AttributeLists); + ClassDeclarationSyntax result = classDecl.WithAttributeLists(newAttributeLists); + + if (!result.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + SyntaxToken partialToken = SyntaxFactory.Token(SyntaxKind.PartialKeyword) + .WithTrailingTrivia(SyntaxFactory.Space); + result = result.WithModifiers(result.Modifiers.Add(partialToken)); + } + + List newMembers = new List(result.Members.Count); + foreach (MemberDeclarationSyntax member in result.Members) + { + if (member is PropertyDeclarationSyntax property) + { + SyntaxList rewritten = RewriteAttributeLists(property.AttributeLists); + newMembers.Add(property.WithAttributeLists(rewritten)); + } + else + { + newMembers.Add(member); + } + } + return result.WithMembers(SyntaxFactory.List(newMembers)); + } + + private static SyntaxList RewriteAttributeLists( + SyntaxList attributeLists) + { + List newLists = new List(attributeLists.Count); + foreach (AttributeListSyntax list in attributeLists) + { + List newAttrs = new List(list.Attributes.Count); + foreach (AttributeSyntax attr in list.Attributes) + { + newAttrs.Add(RewriteAttribute(attr)); + } + newLists.Add(list.WithAttributes(SyntaxFactory.SeparatedList(newAttrs))); + } + return SyntaxFactory.List(newLists); + } + + private static AttributeSyntax RewriteAttribute(AttributeSyntax attribute) + { + string simpleName = GetSimpleAttributeName(attribute.Name); + if (simpleName == "DataContract" || simpleName == "DataContractAttribute") + { + return SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DataType")) + .WithTriviaFrom(attribute); + } + if (simpleName == "DataMember" || simpleName == "DataMemberAttribute") + { + AttributeArgumentListSyntax argList = FilterToOrderArgument(attribute.ArgumentList); + AttributeSyntax replacement = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DataTypeField")); + if (argList != null) + { + replacement = replacement.WithArgumentList(argList); + } + return replacement.WithTriviaFrom(attribute); + } + return attribute; + } + + private static string GetSimpleAttributeName(NameSyntax name) + { + switch (name) + { + case IdentifierNameSyntax id: + return id.Identifier.ValueText; + case QualifiedNameSyntax qn: + return qn.Right.Identifier.ValueText; + case AliasQualifiedNameSyntax aq: + return aq.Name.Identifier.ValueText; + default: + return name?.ToString(); + } + } + + private static AttributeArgumentListSyntax FilterToOrderArgument(AttributeArgumentListSyntax argumentList) + { + if (argumentList is null) + { + return null; + } + List kept = new List(); + foreach (AttributeArgumentSyntax arg in argumentList.Arguments) + { + if (arg.NameEquals?.Name.Identifier.ValueText == "Order") + { + kept.Add(arg.WithoutTrivia()); + } + } + if (kept.Count == 0) + { + return null; + } + return SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(kept)); + } + + private static CompilationUnitSyntax EnsureUsing(CompilationUnitSyntax compilationUnit, string namespaceName) + { + foreach (UsingDirectiveSyntax existing in compilationUnit.Usings) + { + if (existing.Name?.ToString() == namespaceName) + { + return compilationUnit; + } + } + + UsingDirectiveSyntax newUsing = SyntaxFactory.UsingDirective( + SyntaxFactory.ParseName(namespaceName)); + return compilationUnit.AddUsings(newUsing); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0010RemoveDisposableCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0010RemoveDisposableCodeFix.cs new file mode 100644 index 0000000000..2c2f3bb6ab --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0010RemoveDisposableCodeFix.cs @@ -0,0 +1,59 @@ +/* ======================================================================== + * 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.Composition; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0010 code fix: intentionally diagnostic-only — removing the + /// using may change variable scope, so we surface the warning and + /// let the developer rework the lifecycle by hand. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0010RemoveDisposableCodeFix)), Shared] + public sealed class UA0010RemoveDisposableCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0010); + + public override FixAllProvider GetFixAllProvider() => null; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + // No automatic code action: removing the 'using' may change the + // variable's scope/lifetime, which is not safe to do mechanically. + return Task.CompletedTask; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0012CertificateFactoryStaticToInstanceCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0012CertificateFactoryStaticToInstanceCodeFix.cs new file mode 100644 index 0000000000..347a71d206 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0012CertificateFactoryStaticToInstanceCodeFix.cs @@ -0,0 +1,106 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0012 code fix: rewrite CertificateFactory.X(args) as + /// DefaultCertificateFactory.Instance.X(args). + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0012CertificateFactoryStaticToInstanceCodeFix)), Shared] + public sealed class UA0012CertificateFactoryStaticToInstanceCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0012); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + InvocationExpressionSyntax invocation = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (invocation is null || invocation.Expression is not MemberAccessExpressionSyntax member) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Use 'DefaultCertificateFactory.Instance'", + createChangedDocument: ct => ApplyAsync(context.Document, invocation, member, ct), + equivalenceKey: DiagnosticIds.UA0012), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + InvocationExpressionSyntax invocation, + MemberAccessExpressionSyntax member, + CancellationToken cancellationToken) + { + MemberAccessExpressionSyntax newReceiver = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("DefaultCertificateFactory"), + SyntaxFactory.IdentifierName("Instance")); + + MemberAccessExpressionSyntax newMember = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + newReceiver, + member.Name); + + InvocationExpressionSyntax replacement = invocation + .WithExpression(newMember) + .WithTriviaFrom(invocation); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(invocation, replacement); + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0014DataValueIsGoodCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0014DataValueIsGoodCodeFix.cs new file mode 100644 index 0000000000..7aba8054c6 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0014DataValueIsGoodCodeFix.cs @@ -0,0 +1,122 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0014 code fix: rewrite DataValue.IsGood(dv) (or the + /// DataValueExtensions extension form) as dv.IsGood. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0014DataValueIsGoodCodeFix)), Shared] + public sealed class UA0014DataValueIsGoodCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0014); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + InvocationExpressionSyntax invocation = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (invocation is null || invocation.ArgumentList.Arguments.Count != 1) + { + continue; + } + + string memberName = GetMemberName(invocation); + if (memberName is null) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: $"Use '{memberName}' instance property", + createChangedDocument: ct => ApplyAsync(context.Document, invocation, memberName, ct), + equivalenceKey: $"{DiagnosticIds.UA0014}:{memberName}"), + diagnostic); + } + } + + private static string GetMemberName(InvocationExpressionSyntax invocation) + { + switch (invocation.Expression) + { + case MemberAccessExpressionSyntax member: + return member.Name.Identifier.ValueText; + case IdentifierNameSyntax id: + return id.Identifier.ValueText; + default: + return null; + } + } + + private static async Task ApplyAsync( + Document document, + InvocationExpressionSyntax invocation, + string memberName, + CancellationToken cancellationToken) + { + ArgumentSyntax arg = invocation.ArgumentList.Arguments[0]; + ExpressionSyntax receiver = arg.Expression.WithoutTrivia(); + + MemberAccessExpressionSyntax replacement = SyntaxFactory + .MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + receiver, + SyntaxFactory.IdentifierName(memberName)) + .WithTriviaFrom(invocation); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(invocation, replacement); + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0019DataValueStatusCodeCtorCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0019DataValueStatusCodeCtorCodeFix.cs new file mode 100644 index 0000000000..6397813619 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0019DataValueStatusCodeCtorCodeFix.cs @@ -0,0 +1,100 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0019 code fix: rewrite new DataValue(sc) / new DataValue(sc, ts) + /// as DataValue.FromStatusCode(sc) / DataValue.FromStatusCode(sc, ts). + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0019DataValueStatusCodeCtorCodeFix)), Shared] + public sealed class UA0019DataValueStatusCodeCtorCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0019); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + ObjectCreationExpressionSyntax creation = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (creation is null || creation.ArgumentList is null) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Use 'DataValue.FromStatusCode(...)'", + createChangedDocument: ct => ApplyAsync(context.Document, creation, ct), + equivalenceKey: DiagnosticIds.UA0019), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + ObjectCreationExpressionSyntax creation, + CancellationToken cancellationToken) + { + InvocationExpressionSyntax replacement = SyntaxFactory + .InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("DataValue"), + SyntaxFactory.IdentifierName("FromStatusCode")), + creation.ArgumentList!) + .WithTriviaFrom(creation); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(creation, replacement); + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0020EncodeableFactoryRenameCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0020EncodeableFactoryRenameCodeFix.cs new file mode 100644 index 0000000000..1a7e58e8e6 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0020EncodeableFactoryRenameCodeFix.cs @@ -0,0 +1,106 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0020 code fix: rewrite instance factory.Create() as + /// factory.Fork(). The GlobalFactory form has no + /// automatic fix because the replacement + /// (ServiceMessageContext.Factory) requires a context instance + /// that the analyzer cannot conjure. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0020EncodeableFactoryRenameCodeFix)), Shared] + public sealed class UA0020EncodeableFactoryRenameCodeFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0020); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + if (!diagnostic.Properties.TryGetValue( + WellKnownProperties.Form, + out string form) || + form != WellKnownProperties.FormCreate) + { + continue; + } + + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + InvocationExpressionSyntax invocation = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (invocation is null || invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Use 'Fork()'", + createChangedDocument: ct => ApplyAsync(context.Document, invocation, memberAccess, ct), + equivalenceKey: DiagnosticIds.UA0020), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + InvocationExpressionSyntax invocation, + MemberAccessExpressionSyntax memberAccess, + CancellationToken cancellationToken) + { + MemberAccessExpressionSyntax newMemberAccess = memberAccess + .WithName(Microsoft.CodeAnalysis.CSharp.SyntaxFactory.IdentifierName("Fork")); + InvocationExpressionSyntax newInvocation = invocation.WithExpression(newMemberAccess); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(invocation, newInvocation); + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0022CertificateValidatorPropertyRenameCodeFix.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0022CertificateValidatorPropertyRenameCodeFix.cs new file mode 100644 index 0000000000..82d43fa22a --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/CodeFixes/UA0022CertificateValidatorPropertyRenameCodeFix.cs @@ -0,0 +1,104 @@ +/* ======================================================================== + * 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.Composition; +using System.Linq; +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.CSharp.Syntax; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.CodeFixer +{ + /// + /// UA0022 code fix: rewrite xxx.CertificateValidator as + /// xxx.CertificateManager. Only the property identifier is + /// renamed; downstream member access on the (now differently-typed) + /// ICertificateManager result may still need manual review. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UA0022CertificateValidatorPropertyRenameCodeFix)), Shared] + public sealed class UA0022CertificateValidatorPropertyRenameCodeFix : CodeFixProvider + { + private const string NewPropertyName = "CertificateManager"; + + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(DiagnosticIds.UA0022); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = (await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false))!; + + foreach (Diagnostic diagnostic in context.Diagnostics) + { + SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + MemberAccessExpressionSyntax memberAccess = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (memberAccess is null || + memberAccess.Name.Identifier.ValueText != "CertificateValidator") + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Use 'CertificateManager' property", + createChangedDocument: ct => ApplyAsync(context.Document, memberAccess, ct), + equivalenceKey: DiagnosticIds.UA0022), + diagnostic); + } + } + + private static async Task ApplyAsync( + Document document, + MemberAccessExpressionSyntax memberAccess, + CancellationToken cancellationToken) + { + SyntaxToken oldIdentifier = memberAccess.Name.Identifier; + SyntaxToken newIdentifier = SyntaxFactory.Identifier( + oldIdentifier.LeadingTrivia, + NewPropertyName, + oldIdentifier.TrailingTrivia); + MemberAccessExpressionSyntax newMemberAccess = memberAccess + .WithName(SyntaxFactory.IdentifierName(newIdentifier)); + + SyntaxNode root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false))!; + SyntaxNode newRoot = root.ReplaceNode(memberAccess, newMemberAccess); + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.csproj b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.csproj new file mode 100644 index 0000000000..44bd41af1c --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Opc.Ua.MigrationAnalyzer.CodeFixer.csproj @@ -0,0 +1,48 @@ + + + netstandard2.0 + false + true + $(AssemblyPrefix).MigrationAnalyzer.CodeFixer + Opc.Ua.MigrationAnalyzer.CodeFixer + OPC UA .NET Standard migration code-fix providers (1.5.378 to 2.0). Companion to Opc.Ua.MigrationAnalyzer analyzers. + + $(NoWarn);RS1007;RS1038;RS2008 + + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Properties/AssemblyInfo.cs b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2b9848014c --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/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/Libraries/Opc.Ua.Client/Session/SessionObsolete.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Client/Session/Session.cs similarity index 98% rename from Libraries/Opc.Ua.Client/Session/SessionObsolete.cs rename to Tools/Opc.Ua.MigrationAnalyzer.Core/Client/Session/Session.cs index f926cd6131..bea052bfe4 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionObsolete.cs +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Client/Session/Session.cs @@ -792,10 +792,17 @@ public static bool ResendData( return (false, Array.Empty()); } } - } - public partial class Session - { + // ---------------------------------------------------------------- + // The following static factory helpers were originally declared as + // `public partial class Session` members on the Session type in + // Libraries/Opc.Ua.Client. Cross-assembly partial classes are not + // supported, so when migrated into this shim assembly they are + // hosted as static methods on SessionObsolete. Callers that + // previously invoked `Session.Create(...)` / `Session.Recreate(...)` + // must now call `SessionObsolete.Create(...)` / `SessionObsolete.Recreate(...)`. + // ---------------------------------------------------------------- + /// /// Creates a new communication session with a server by invoking the CreateSession service /// diff --git a/Libraries/Opc.Ua.Client/Subscription/Classic/SubscriptionObsolete.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Client/Subscription/Classic/Subscription.cs similarity index 100% rename from Libraries/Opc.Ua.Client/Subscription/Classic/SubscriptionObsolete.cs rename to Tools/Opc.Ua.MigrationAnalyzer.Core/Client/Subscription/Classic/Subscription.cs diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.Obsolete.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Configuration/ApplicationInstance.cs similarity index 100% rename from Libraries/Opc.Ua.Configuration/ApplicationInstance.Obsolete.cs rename to Tools/Opc.Ua.MigrationAnalyzer.Core/Configuration/ApplicationInstance.cs diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Security/Certificates/CertificateIdentifier.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Security/Certificates/CertificateIdentifier.cs new file mode 100644 index 0000000000..1b29b52c4d --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Security/Certificates/CertificateIdentifier.cs @@ -0,0 +1,63 @@ +/* ======================================================================== + * 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.Security.Cryptography.X509Certificates; + +namespace Opc.Ua +{ + /// + /// Migration shim — restores the CertificateIdentifier.Certificate + /// instance property that was removed in 1.6 because resolution now + /// requires a registry/telemetry context. Accessing this shim member + /// throws at runtime; the + /// [Obsolete] attribute and matching analyzer rule guide + /// consumers to the async resolver replacement. + /// + public static class CertificateIdentifierShim + { + extension(CertificateIdentifier id) + { + /// + /// Returns the resolved X.509 certificate. Removed in 1.6 because + /// resolution is now async and requires a registry. + /// + [Obsolete("CertificateIdentifier.Certificate was removed in 1.6 because " + + "resolution requires a registry/telemetry context. " + + "Use CertificateIdentifierResolver.ResolveAsync(id, registry, needPrivateKey, applicationUri, telemetry, ct). " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0018")] + [OpcUaShim("UA0018")] + public X509Certificate2? Certificate + => throw new NotSupportedException( + "CertificateIdentifier.Certificate was removed in 1.6 because resolution " + + "requires a registry/telemetry context. Use " + + "CertificateIdentifierResolver.ResolveAsync(id, registry, needPrivateKey, applicationUri, telemetry, ct)."); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Client/ChannelBase.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Client/ChannelBase.cs new file mode 100644 index 0000000000..e1bd1263ad --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Client/ChannelBase.cs @@ -0,0 +1,309 @@ +/* ======================================================================== + * 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 Opc.Ua.Security.Certificates; + +namespace Opc.Ua +{ + /// + /// 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(); + } + } + + /// + /// 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(); + } + } + + /// + /// 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.Obsolete.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Configuration/ApplicationConfiguration.cs similarity index 100% rename from Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.Obsolete.cs rename to Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Configuration/ApplicationConfiguration.cs diff --git a/Stack/Opc.Ua.Core/Stack/Server/ServerBaseObsolete.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Server/ServerBase.cs similarity index 100% rename from Stack/Opc.Ua.Core/Stack/Server/ServerBaseObsolete.cs rename to Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Server/ServerBase.cs diff --git a/Stack/Opc.Ua.Core/Stack/Transport/TransportChannelObsolete.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Transport/TransportChannel.cs similarity index 100% rename from Stack/Opc.Ua.Core/Stack/Transport/TransportChannelObsolete.cs rename to Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Transport/TransportChannel.cs diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Types/IUserIdentityTokenHandler.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Types/IUserIdentityTokenHandler.cs new file mode 100644 index 0000000000..acd8b02c43 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Stack/Types/IUserIdentityTokenHandler.cs @@ -0,0 +1,145 @@ +/* ======================================================================== + * 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 Opc.Ua.Security.Certificates; + +namespace Opc.Ua +{ + /// + /// Migration shim — restores synchronous Encrypt, Decrypt, + /// Sign and Verify operations on the user identity token + /// surface. In 1.6 the legacy synchronous methods on + /// UserIdentityToken were removed in favour of asynchronous + /// counterparts on . These shims + /// block the calling thread on the async path so existing call sites + /// keep compiling; consumers should migrate to the *Async + /// variants. + /// + public static class UserIdentityTokenHandlerShim + { + extension(IUserIdentityTokenHandler handler) + { + /// + /// Encrypts the token. Synchronous shim for + /// . + /// + [Obsolete("Synchronous Encrypt was removed in 1.6. Use EncryptAsync. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0011")] + [OpcUaShim("UA0011")] + public void Encrypt( + Certificate receiverCertificate, + byte[] receiverNonce, + string securityPolicyUri, + IServiceMessageContext context, + Nonce? receiverEphemeralKey = null, + Certificate? senderCertificate = null, + CertificateCollection? senderIssuerCertificates = null, + bool doNotEncodeSenderCertificate = false) + { + handler.EncryptAsync( + receiverCertificate, + receiverNonce, + securityPolicyUri, + context, + receiverEphemeralKey, + senderCertificate, + senderIssuerCertificates, + doNotEncodeSenderCertificate) + .AsTask() + .GetAwaiter() + .GetResult(); + } + + /// + /// Decrypts the token. Synchronous shim for + /// . + /// + [Obsolete("Synchronous Decrypt was removed in 1.6. Use DecryptAsync. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0011")] + [OpcUaShim("UA0011")] + public void Decrypt( + Certificate certificate, + Nonce receiverNonce, + string securityPolicyUri, + IServiceMessageContext context, + Nonce? ephemeralKey = null, + Certificate? senderCertificate = null, + CertificateCollection? senderIssuerCertificates = null, + ICertificateValidatorEx? validator = null) + { + handler.DecryptAsync( + certificate, + receiverNonce, + securityPolicyUri, + context, + ephemeralKey, + senderCertificate, + senderIssuerCertificates, + validator) + .AsTask() + .GetAwaiter() + .GetResult(); + } + + /// + /// Creates a signature with the token. Synchronous shim for + /// . + /// + [Obsolete("Synchronous Sign was removed in 1.6. Use SignAsync. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0011")] + [OpcUaShim("UA0011")] + public SignatureData Sign(byte[] dataToSign, string securityPolicyUri) + { + return handler.SignAsync(dataToSign, securityPolicyUri) + .AsTask() + .GetAwaiter() + .GetResult(); + } + + /// + /// Verifies a signature created with the token. Synchronous shim + /// for . + /// + [Obsolete("Synchronous Verify was removed in 1.6. Use VerifyAsync. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0011")] + [OpcUaShim("UA0011")] + public bool Verify( + byte[] dataToVerify, + SignatureData signatureData, + string securityPolicyUri) + { + return handler.VerifyAsync(dataToVerify, signatureData, securityPolicyUri) + .AsTask() + .GetAwaiter() + .GetResult(); + } + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Types/Encoders/EncodeableFactory.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Types/Encoders/EncodeableFactory.cs new file mode 100644 index 0000000000..774ca1cd79 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Core/Types/Encoders/EncodeableFactory.cs @@ -0,0 +1,54 @@ +/* ======================================================================== + * 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; + +namespace Opc.Ua +{ + /// + /// Migration shim — restores EncodeableFactory.GlobalFactory + /// which was removed in 1.6. Callers should migrate to + /// 's + /// . + /// + public static class EncodeableFactoryShim + { + extension(EncodeableFactory) + { + /// + /// The global encodeable factory shared across the process. + /// + [Obsolete("EncodeableFactory.GlobalFactory was removed in 1.6. " + + "Use ServiceMessageContext.GlobalContext.Factory instead. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0020")] + [OpcUaShim("UA0020")] + public static IEncodeableFactory GlobalFactory => ServiceMessageContext.GlobalContext.Factory; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Gds.Client.Common/GlobalDiscoveryServerClient.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Gds.Client.Common/GlobalDiscoveryServerClient.cs new file mode 100644 index 0000000000..fa5061852b --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Gds.Client.Common/GlobalDiscoveryServerClient.cs @@ -0,0 +1,72 @@ +/* ======================================================================== + * 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; + +namespace Opc.Ua.Gds.Client +{ + /// + /// Migration shim — restores the synchronous RegisterApplication + /// and UnregisterApplication methods on + /// that were removed in 1.6. + /// + public static class GlobalDiscoveryServerClientShim + { + extension(GlobalDiscoveryServerClient client) + { + /// + /// Registers the application synchronously. + /// + [Obsolete("Synchronous RegisterApplication was removed in 1.6. Use RegisterApplicationAsync. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0015")] + [OpcUaShim("UA0015")] + public NodeId RegisterApplication(ApplicationRecordDataType application) + { + return client.RegisterApplicationAsync(application) + .AsTask() + .GetAwaiter() + .GetResult(); + } + + /// + /// Unregisters the application synchronously. + /// + [Obsolete("Synchronous UnregisterApplication was removed in 1.6. Use UnregisterApplicationAsync. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0015")] + [OpcUaShim("UA0015")] + public void UnregisterApplication(NodeId applicationId) + { + client.UnregisterApplicationAsync(applicationId) + .AsTask() + .GetAwaiter() + .GetResult(); + } + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Gds.Client.Common/LocalDiscoveryServerClient.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Gds.Client.Common/LocalDiscoveryServerClient.cs new file mode 100644 index 0000000000..12e94330df --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Gds.Client.Common/LocalDiscoveryServerClient.cs @@ -0,0 +1,92 @@ +/* ======================================================================== + * 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; + +namespace Opc.Ua.Gds.Client +{ + /// + /// Migration shim — restores the APM + /// BeginFindServers / EndFindServers pair on + /// that was removed in 1.6 in + /// favour of FindServersAsync. + /// + public static class LocalDiscoveryServerClientShim + { + extension(LocalDiscoveryServerClient client) + { + /// + /// Begins an asynchronous FindServers call using the + /// classic Begin/End APM pattern. + /// + [Obsolete("BeginFindServers/EndFindServers were removed in 1.6. Use FindServersAsync. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0015")] + [OpcUaShim("UA0015")] + public IAsyncResult BeginFindServers(AsyncCallback? callback, object? state) + { + var tcs = new TaskCompletionSource>(state); + client.FindServersAsync() + .AsTask() + .ContinueWith( + t => + { + if (t.IsFaulted) + { + tcs.TrySetException(t.Exception!.InnerExceptions); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(t.Result); + } + callback?.Invoke(tcs.Task); + }, + TaskScheduler.Default); + return tcs.Task; + } + + /// + /// Completes the asynchronous FindServers call. + /// + [Obsolete("BeginFindServers/EndFindServers were removed in 1.6. Use FindServersAsync. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0015")] + [OpcUaShim("UA0015")] + public ArrayOf EndFindServers(IAsyncResult result) + { + return ((Task>)result) + .GetAwaiter() + .GetResult(); + } + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Gds.Client.Common/ServerPushConfigurationClient.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Gds.Client.Common/ServerPushConfigurationClient.cs new file mode 100644 index 0000000000..f6c666c3e4 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Gds.Client.Common/ServerPushConfigurationClient.cs @@ -0,0 +1,58 @@ +/* ======================================================================== + * 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; + +namespace Opc.Ua.Gds.Client +{ + /// + /// Migration shim — restores the synchronous ApplyChanges + /// method on that was + /// removed in 1.6. + /// + public static class ServerPushConfigurationClientShim + { + extension(ServerPushConfigurationClient client) + { + /// + /// Applies the staged configuration changes synchronously. + /// + [Obsolete("Synchronous ApplyChanges was removed in 1.6. Use ApplyChangesAsync. " + + "See https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#ua0015")] + [OpcUaShim("UA0015")] + public void ApplyChanges() + { + client.ApplyChangesAsync() + .AsTask() + .GetAwaiter() + .GetResult(); + } + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Marker/OpcUaShimAttribute.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Marker/OpcUaShimAttribute.cs new file mode 100644 index 0000000000..77af2637f4 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Marker/OpcUaShimAttribute.cs @@ -0,0 +1,69 @@ +/* ======================================================================== + * 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; + +namespace Opc.Ua +{ + /// + /// Marks an API member as a 1.5.378 → 1.6 migration shim. Used by the + /// Opc.Ua.MigrationAnalyzer analyzer to map calls that bind to a shim + /// extension back to the underlying UA00xx diagnostic rule, so + /// consumers get the same migration guidance whether they call the + /// shim directly or the legacy API in source. + /// + /// + /// Apply this in addition to . The shim + /// keeps the call compilable; the analyzer fires an Info + /// diagnostic that points at the matching migration-guide section. + /// + [AttributeUsage( + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Constructor | + AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OpcUaShimAttribute : Attribute + { + /// + /// The diagnostic rule identifier the shim corresponds to, + /// e.g. "UA0008". + /// + public string RuleId { get; } + + /// + /// Creates a new . + /// + public OpcUaShimAttribute(string ruleId) + { + RuleId = ruleId ?? throw new ArgumentNullException(nameof(ruleId)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Opc.Ua.MigrationAnalyzer.Core.csproj b/Tools/Opc.Ua.MigrationAnalyzer.Core/Opc.Ua.MigrationAnalyzer.Core.csproj new file mode 100644 index 0000000000..3c14459204 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Opc.Ua.MigrationAnalyzer.Core.csproj @@ -0,0 +1,25 @@ + + + $(AssemblyPrefix).MigrationAnalyzer.Core + $(LibTargetFrameworks) + Opc.Ua + OPC UA 1.5.378 → 1.6 compatibility shim. Provides the obsolete extension-method surface that 1.6 removed, marked [Obsolete] so the matching Opc.Ua.MigrationAnalyzer analyzer rules guide consumers off it. Ships in the OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer NuGet alongside the analyzer DLL. + false + true + enable + + $(NoWarn);CS0618;CS0419 + + + + + + + + + + + diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/Properties/AssemblyInfo.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2b9848014c --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/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/Stack/Opc.Ua.Types/BuiltIn/BuiltInType.Obsolete.cs b/Tools/Opc.Ua.MigrationAnalyzer.Core/Types/BuiltIn/BuiltInType.cs similarity index 99% rename from Stack/Opc.Ua.Types/BuiltIn/BuiltInType.Obsolete.cs rename to Tools/Opc.Ua.MigrationAnalyzer.Core/Types/BuiltIn/BuiltInType.cs index 5537ef4860..bb1e3e58a6 100644 --- a/Stack/Opc.Ua.Types/BuiltIn/BuiltInType.Obsolete.cs +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/Types/BuiltIn/BuiltInType.cs @@ -133,7 +133,7 @@ public static bool IsNullOrEmpty(LocalizedText value) /// public static class DataValueObsolete { - extension(ExtensionObject) + extension(DataValue) { /// /// Returns true if the status code is good. diff --git a/Tools/Opc.Ua.MigrationAnalyzer.Core/readme.md b/Tools/Opc.Ua.MigrationAnalyzer.Core/readme.md new file mode 100644 index 0000000000..7eafefd398 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer.Core/readme.md @@ -0,0 +1,40 @@ +# OPC UA 1.5.378 → 1.6 compatibility shim + +This project provides extension-method shims for the obsolete API surface that +the 1.6 release line is moving away from. It ships in the +`OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` NuGet package alongside the +analyzer DLL. + +## Directory convention + +Source files live under `Shims//...` mirroring +the project layout in `Stack/` and `Libraries/`. For example: + +| Source project | Shim path | +| ------------------------- | ---------------- | +| `Opc.Ua.Types` | `Shims/Types/` | +| `Opc.Ua.Core.Types` | `Shims/Core.Types/` | +| `Opc.Ua.Core` | `Shims/Core/` | +| `Opc.Ua.Client` | `Shims/Client/` | +| `Opc.Ua.Configuration` | `Shims/Configuration/` | +| `Opc.Ua.Gds.Client.Common`| `Shims/Gds.Client.Common/` | + +Within each `Shims//` directory the file path mirrors the directory +layout of the source project — e.g. `Stack/Opc.Ua.Core/Stack/Server/ServerBaseObsolete.cs` +moves to `Shims/Core/Stack/Server/ServerBase.cs`. + +## Conventions + +- Every shim member carries **both** `[Obsolete]` and + `[OpcUaShim(RuleId = "UANNNN")]` so the analyzer can route calls back to + the corresponding migration-guide section. +- Sync-over-async shims use `Task.Run(() => XxxAsync()).GetAwaiter().GetResult()` + to avoid sync-context deadlocks (see Phase 6 in the plan). +- Removed types (e.g. the legacy `Collection` wrappers) are + **not** shimmed — consumers must run the UA0002 fixer to migrate + declarations to `List` or `ArrayOf`. + +## Status + +Phase 6.A scaffolding only. Move-from-libraries work happens in Phase 6.C; +new shims for genuinely-removed members in Phase 6.D. diff --git a/Tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Shipped.md b/Tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..5ccc9f037f --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/Tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md b/Tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..8f50880eb5 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md @@ -0,0 +1,26 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|-----------|----------|------------------------------------------------------------------------------------------------------ +UA0001 | Migration | Info | Replace Utils.Trace/Utils.LogX calls with ILogger obtained from ITelemetryContext. +UA0002 | Migration | Warning | Replace removed `Collection` types with `List` or `ArrayOf`. +UA0003 | Migration | Warning | Replace `== null` / `!= null` on now-struct built-in types with the `.IsNull` property. +UA0004 | Migration | Warning | Remove null-conditional `?.` on now-struct built-in types (NodeId, Variant, DataValue, ...). +UA0005 | Migration | Warning | Convert `byte[]` to `ByteString` at API boundaries that now require `ByteString`. +UA0006 | Migration | Warning | Replace obsoleted non-generic Variant constructors with Variant.From. +UA0007 | Migration | Warning | Replace `new NodeId(string)` / `new ExpandedNodeId(string)` with `NodeId.Parse(s)` / `ExpandedNodeId.Parse(s)`. +UA0008 | Migration | Warning | Wrap `params object[]` arguments to `Session.Call`/`CallAsync` with `Variant.From(...)`. +UA0009 | Migration | Warning | Replace `[DataContract]`/`[DataMember]` on configuration extensions with `[DataType]`/`[DataTypeField]`. +UA0010 | Migration | Warning | Remove `using`/`Dispose()` on `CertificateIdentifier`, `UserIdentity`, `IUserIdentityTokenHandler` (no longer IDisposable). +UA0011 | Migration | Info | Replace sync `IUserIdentityTokenHandler.Encrypt/Decrypt/Sign/Verify` with `…Async`. +UA0012 | Migration | Warning | Replace obsolete static `CertificateFactory.*` helpers with `DefaultCertificateFactory.Instance.*`. +UA0014 | Migration | Warning | Replace `DataValue.IsGood(dv)`/`IsBad`/`IsUncertain` static helpers with `dv.IsGood`/`IsBad`/`IsUncertain` instance properties. +UA0015 | Migration | Info | Replace sync/APM members on GDS/LDS clients with their `…Async` counterparts. +UA0018 | Migration | Info | Replace `CertificateIdentifier.Certificate` getter with `CertificateIdentifierResolver.ResolveAsync(...)`. +UA0019 | Migration | Warning | Replace `new DataValue(StatusCode[, ts])` with `DataValue.FromStatusCode(...)`. +UA0020 | Migration | Warning | Replace `EncodeableFactory.GlobalFactory` / `EncodeableFactory.Create()` with `ServiceMessageContext.Factory` / `Fork()`. +UA0021 | Migration | Info | Replace `CertificateValidator` / `CertificateValidationEventArgs` with the 1.6 `ICertificateManager` / `ICertificateValidatorEx` / `CertificateValidationResult` pipeline. See MigrationGuide.md. +UA0022 | Migration | Warning | Replace `ApplicationConfiguration.CertificateValidator` / `ServerBase.CertificateValidator` property access with `.CertificateManager`. diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0001UtilsTraceToILoggerAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0001UtilsTraceToILoggerAnalyzer.cs new file mode 100644 index 0000000000..52c9369b07 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0001UtilsTraceToILoggerAnalyzer.cs @@ -0,0 +1,102 @@ +/* ======================================================================== + * 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/ + * ======================================================================*/ + +// UA0001 is diagnostic-only. The replacement requires an ILogger instance +// obtained from ITelemetryContext.CreateLogger() which the analyzer cannot +// synthesize automatically. A code fix would require the host type to expose +// an ILogger field or an ITelemetryContext from which to derive one. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0001: Flag calls to the obsolete static Opc.Ua.Utils.Trace and + /// Opc.Ua.Utils.LogX helpers. Replacement requires an + /// ILogger from an ITelemetryContext which the analyzer + /// cannot synthesize, so this rule ships diagnostic-only (no code fix). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0001UtilsTraceToILoggerAnalyzer : DiagnosticAnalyzer + { + private static readonly HashSet s_targetNames = + [ + "Trace", + "LogError", + "LogWarning", + "LogInformation", + "LogDebug", + "LogTrace", + "LogCritical", + ]; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0001_UtilsTraceToILogger); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + } + + private static void AnalyzeInvocation(OperationAnalysisContext context) + { + IInvocationOperation invocation = (IInvocationOperation)context.Operation; + IMethodSymbol method = invocation.TargetMethod; + + if (!method.IsStatic || !s_targetNames.Contains(method.Name)) + { + return; + } + + INamedTypeSymbol containing = method.ContainingType; + if (containing is null || containing.ToDisplayString() != "Opc.Ua.Utils") + { + return; + } + + if (!method.IsObsolete()) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0001_UtilsTraceToILogger, + invocation.Syntax.GetLocation(), + "Utils." + method.Name)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0002RemovedCollectionTypeAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0002RemovedCollectionTypeAnalyzer.cs new file mode 100644 index 0000000000..9e2992d1a6 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0002RemovedCollectionTypeAnalyzer.cs @@ -0,0 +1,96 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0002: Detect references to removed <Type>Collection + /// wrappers (Int32Collection, NodeIdCollection, ...) and recommend + /// List<T> or ArrayOf<T>. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0002RemovedCollectionTypeAnalyzer : DiagnosticAnalyzer + { + public const string ElementTypeProperty = "ElementType"; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0002_RemovedCollectionType); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeIdentifier, SyntaxKind.IdentifierName); + } + + private static void AnalyzeIdentifier(SyntaxNodeAnalysisContext context) + { + IdentifierNameSyntax id = (IdentifierNameSyntax)context.Node; + + SymbolInfo info = context.SemanticModel.GetSymbolInfo(id, context.CancellationToken); + if (info.Symbol is not INamedTypeSymbol named) + { + return; + } + + if (!named.TryGetRemovedCollectionElement(out string elementName)) + { + return; + } + + ImmutableDictionary properties = ImmutableDictionary.Empty + .Add(ElementTypeProperty, TrimOpcUaPrefix(elementName)); + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0002_RemovedCollectionType, + id.GetLocation(), + properties, + named.ToDisplayString(), + TrimOpcUaPrefix(elementName))); + } + + internal static string TrimOpcUaPrefix(string name) + { + const string prefix = "Opc.Ua."; + if (name != null && name.StartsWith(prefix, System.StringComparison.Ordinal)) + { + return name.Substring(prefix.Length); + } + return name; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0003NullCheckOnStructTypeAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0003NullCheckOnStructTypeAnalyzer.cs new file mode 100644 index 0000000000..a64d43a295 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0003NullCheckOnStructTypeAnalyzer.cs @@ -0,0 +1,129 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0003: Detect x == null / x != null comparisons on + /// built-in OPC UA types that became readonly structs in 2.0. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0003NullCheckOnStructTypeAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0003_NullCheckOnStructType); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + Dictionary cache = []; + UaSymbols symbols = UaSymbols.For(context.Compilation, cache); + if (!symbols.ReferencesOpcUa) + { + return; + } + context.RegisterOperationAction(c => AnalyzeBinary(c, symbols), OperationKind.Binary); + } + + private static void AnalyzeBinary(OperationAnalysisContext context, UaSymbols symbols) + { + IBinaryOperation op = (IBinaryOperation)context.Operation; + if (op.OperatorKind != BinaryOperatorKind.Equals && + op.OperatorKind != BinaryOperatorKind.NotEquals) + { + return; + } + + IOperation valueOperand = GetValueOperandOrNull(op.LeftOperand, op.RightOperand); + if (valueOperand is null) + { + return; + } + + ITypeSymbol valueType = valueOperand.Type; + if (valueType is null) + { + return; + } + if (valueType.OriginalDefinition?.SpecialType == SpecialType.System_Nullable_T && + valueType is INamedTypeSymbol named && named.TypeArguments.Length == 1) + { + valueType = named.TypeArguments[0]; + } + + if (!symbols.IsBuiltInStructType(valueType)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0003_NullCheckOnStructType, + op.Syntax.GetLocation(), + valueType.Name)); + } + + private static IOperation GetValueOperandOrNull(IOperation left, IOperation right) + { + if (IsNullLiteral(left)) + { + return right; + } + if (IsNullLiteral(right)) + { + return left; + } + return null; + } + + private static bool IsNullLiteral(IOperation op) + { + while (op is IConversionOperation conv) + { + op = conv.Operand; + } + return op is ILiteralOperation lit && + lit.ConstantValue.HasValue && + lit.ConstantValue.Value is null; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0004ConditionalAccessOnStructAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0004ConditionalAccessOnStructAnalyzer.cs new file mode 100644 index 0000000000..23fea6ffc0 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0004ConditionalAccessOnStructAnalyzer.cs @@ -0,0 +1,98 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0004: Detect null-conditional access (?.) whose receiver is + /// one of the now-struct OPC UA built-in types. Since the receiver can + /// never be null, the ?. is misleading. + /// + /// + /// Implemented as a syntax-node action so the analyzer still fires on + /// migration code that no longer compiles (e.g. nodeId?.X where + /// NodeId is now a non-nullable struct). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0004ConditionalAccessOnStructAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0004_ConditionalAccessOnStructType); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + Dictionary cache = []; + UaSymbols symbols = UaSymbols.For(context.Compilation, cache); + if (!symbols.ReferencesOpcUa) + { + return; + } + context.RegisterSyntaxNodeAction( + c => AnalyzeConditionalAccess(c, symbols), + SyntaxKind.ConditionalAccessExpression); + } + + private static void AnalyzeConditionalAccess(SyntaxNodeAnalysisContext context, UaSymbols symbols) + { + ConditionalAccessExpressionSyntax node = (ConditionalAccessExpressionSyntax)context.Node; + ITypeSymbol receiverType = context.SemanticModel + .GetTypeInfo(node.Expression, context.CancellationToken).Type; + if (receiverType is null) + { + return; + } + + if (!symbols.IsBuiltInStructType(receiverType)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0004_ConditionalAccessOnStructType, + node.GetLocation(), + receiverType.Name)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0005ByteArrayToByteStringAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0005ByteArrayToByteStringAnalyzer.cs new file mode 100644 index 0000000000..0291ffc6b0 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0005ByteArrayToByteStringAnalyzer.cs @@ -0,0 +1,118 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0005: Detect call sites passing a byte[] argument where the + /// resolved parameter type is now Opc.Ua.ByteString. + /// + /// + /// Implemented as a syntax-node action so the analyzer fires on migration + /// code that no longer compiles (the byte[]-to-ByteString + /// conversion is gone). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0005ByteArrayToByteStringAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0005_ByteArrayWhereByteStringExpected); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + Dictionary cache = []; + UaSymbols symbols = UaSymbols.For(context.Compilation, cache); + if (!symbols.ReferencesOpcUa || symbols.ByteStringType is null) + { + return; + } + context.RegisterSyntaxNodeAction( + c => AnalyzeInvocation(c, symbols), + SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, UaSymbols symbols) + { + InvocationExpressionSyntax invocation = (InvocationExpressionSyntax)context.Node; + SymbolInfo info = context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken); + IMethodSymbol method = info.Symbol as IMethodSymbol + ?? info.CandidateSymbols.OfType().FirstOrDefault(); + if (method is null) + { + return; + } + + INamedTypeSymbol byteStringType = symbols.ByteStringType; + SeparatedSyntaxList arguments = invocation.ArgumentList.Arguments; + for (int i = 0; i < arguments.Count; i++) + { + ArgumentSyntax arg = arguments[i]; + IParameterSymbol parameter = i < method.Parameters.Length ? method.Parameters[i] : null; + if (parameter is null) + { + continue; + } + if (!SymbolEqualityComparer.Default.Equals(parameter.Type, byteStringType)) + { + continue; + } + + ITypeSymbol valueType = context.SemanticModel + .GetTypeInfo(arg.Expression, context.CancellationToken).Type; + if (valueType is not IArrayTypeSymbol array || + array.ElementType.SpecialType != SpecialType.System_Byte) + { + continue; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0005_ByteArrayWhereByteStringExpected, + arg.GetLocation(), + method.Name)); + } + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0006ObsoleteVariantCtorAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0006ObsoleteVariantCtorAnalyzer.cs new file mode 100644 index 0000000000..08ee46646f --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0006ObsoleteVariantCtorAnalyzer.cs @@ -0,0 +1,115 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0006: Detect new Variant(object|DateTime|Guid|byte[]) obsolete + /// constructor calls and recommend the generic Variant.From<T> + /// factory together with the appropriate wrapper type. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0006ObsoleteVariantCtorAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0006_ObsoleteVariantCtor); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); + } + + private static void AnalyzeObjectCreation(OperationAnalysisContext context) + { + IObjectCreationOperation creation = (IObjectCreationOperation)context.Operation; + IMethodSymbol ctor = creation.Constructor; + if (ctor is null || ctor.Parameters.Length != 1) + { + return; + } + + INamedTypeSymbol containing = ctor.ContainingType; + if (containing is null || containing.ToDisplayString() != "Opc.Ua.Variant") + { + return; + } + + if (!ctor.IsObsolete()) + { + return; + } + + ITypeSymbol argType = ctor.Parameters[0].Type; + string label = GetLabel(argType); + if (label is null) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0006_ObsoleteVariantCtor, + creation.Syntax.GetLocation(), + label)); + } + + private static string GetLabel(ITypeSymbol type) + { + if (type is null) + { + return null; + } + switch (type.SpecialType) + { + case SpecialType.System_Object: + return "object"; + case SpecialType.System_DateTime: + return "DateTime"; + } + if (type.ToDisplayString() == "System.Guid") + { + return "Guid"; + } + if (type is IArrayTypeSymbol arr && + arr.ElementType?.SpecialType == SpecialType.System_Byte) + { + return "byte[]"; + } + return null; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0007ObsoleteNodeIdStringCtorAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0007ObsoleteNodeIdStringCtorAnalyzer.cs new file mode 100644 index 0000000000..41e2698a5c --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0007ObsoleteNodeIdStringCtorAnalyzer.cs @@ -0,0 +1,86 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0007: Detect new NodeId(string) / new ExpandedNodeId(string) + /// constructor calls and recommend the explicit Parse factory. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0007ObsoleteNodeIdStringCtorAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0007_ObsoleteNodeIdStringCtor); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); + } + + private static void AnalyzeObjectCreation(OperationAnalysisContext context) + { + IObjectCreationOperation creation = (IObjectCreationOperation)context.Operation; + IMethodSymbol ctor = creation.Constructor; + if (ctor is null || ctor.Parameters.Length != 1) + { + return; + } + + INamedTypeSymbol containing = ctor.ContainingType; + if (containing is null) + { + return; + } + string containingName = containing.ToDisplayString(); + if (containingName != "Opc.Ua.NodeId" && containingName != "Opc.Ua.ExpandedNodeId") + { + return; + } + + if (ctor.Parameters[0].Type?.SpecialType != SpecialType.System_String) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0007_ObsoleteNodeIdStringCtor, + creation.Syntax.GetLocation(), + containing.Name)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0008SessionCallParamsObjectAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0008SessionCallParamsObjectAnalyzer.cs new file mode 100644 index 0000000000..74ea43612b --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0008SessionCallParamsObjectAnalyzer.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.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0008: Detect ISession.Call / ISession.CallAsync + /// invocations whose variadic arguments are raw values instead of + /// Variant instances, and recommend wrapping with + /// Variant.From(...). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0008SessionCallParamsObjectAnalyzer : DiagnosticAnalyzer + { + public const string MethodNameProperty = WellKnownProperties.MethodName; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0008_SessionCallParamsObject); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + INamedTypeSymbol sessionInterface = context.Compilation.GetTypeByMetadataName("Opc.Ua.ISession"); + INamedTypeSymbol variantType = context.Compilation.GetTypeByMetadataName("Opc.Ua.Variant"); + if (variantType is null) + { + return; + } + context.RegisterSyntaxNodeAction( + c => AnalyzeInvocation(c, sessionInterface, variantType), + SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation( + SyntaxNodeAnalysisContext context, + INamedTypeSymbol sessionInterface, + INamedTypeSymbol variantType) + { + InvocationExpressionSyntax invocation = (InvocationExpressionSyntax)context.Node; + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + return; + } + + string methodName = memberAccess.Name.Identifier.ValueText; + if (methodName != "Call" && methodName != "CallAsync") + { + return; + } + + IMethodSymbol resolvedMethod = context.SemanticModel + .GetSymbolInfo(invocation, context.CancellationToken).Symbol as IMethodSymbol; + bool isShim = resolvedMethod.IsOpcUaShim("UA0008"); + + if (!isShim) + { + if (sessionInterface is null) + { + return; + } + ITypeSymbol receiverType = context.SemanticModel + .GetTypeInfo(memberAccess.Expression, context.CancellationToken).Type; + if (receiverType is null || !receiverType.IsAssignableTo(sessionInterface)) + { + return; + } + } + + int firstVariadicIndex = methodName == "Call" ? 2 : 3; + IReadOnlyList args = invocation.ArgumentList.Arguments; + if (args.Count <= firstVariadicIndex) + { + return; + } + + bool anyNonVariant = false; + for (int i = firstVariadicIndex; i < args.Count; i++) + { + ITypeSymbol argType = context.SemanticModel + .GetTypeInfo(args[i].Expression, context.CancellationToken).Type; + if (argType is null || !SymbolEqualityComparer.Default.Equals(argType, variantType)) + { + anyNonVariant = true; + break; + } + } + + if (!anyNonVariant) + { + return; + } + + ImmutableDictionary properties = ImmutableDictionary.Empty + .Add(MethodNameProperty, methodName); + + ITypeSymbol displayReceiver = context.SemanticModel + .GetTypeInfo(memberAccess.Expression, context.CancellationToken).Type; + string receiverName = displayReceiver?.Name ?? "ISession"; + string display = receiverName + "." + methodName; + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0008_SessionCallParamsObject, + invocation.GetLocation(), + properties, + display)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0009DataContractToDataTypeAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0009DataContractToDataTypeAnalyzer.cs new file mode 100644 index 0000000000..a66cdc9413 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0009DataContractToDataTypeAnalyzer.cs @@ -0,0 +1,131 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0009: Flag classes annotated with + /// System.Runtime.Serialization.DataContractAttribute that also have + /// at least one DataMember property — they are candidates for + /// migration to [Opc.Ua.DataType] / [Opc.Ua.DataTypeField]. + /// + /// + /// Simplified detection: the analyzer does NOT verify that the class is + /// actually consumed by ApplicationConfiguration.ParseExtension<T> + /// or UpdateExtension<T>. Cross-compilation usage scanning would + /// require a CompilationEndAction and full symbol walk. Trade-off: + /// more false positives but a much simpler analyzer. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0009DataContractToDataTypeAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0009_DataContractToDataType); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + Dictionary cache = []; + UaSymbols symbols = UaSymbols.For(context.Compilation, cache); + if (symbols.DataContractType is null || symbols.DataMemberType is null) + { + return; + } + context.RegisterSymbolAction(c => AnalyzeNamedType(c, symbols), SymbolKind.NamedType); + } + + private static void AnalyzeNamedType(SymbolAnalysisContext context, UaSymbols symbols) + { + INamedTypeSymbol type = (INamedTypeSymbol)context.Symbol; + if (type.TypeKind != TypeKind.Class) + { + return; + } + + if (!HasAttribute(type, symbols.DataContractType)) + { + return; + } + + if (!HasDataMemberProperty(type, symbols.DataMemberType)) + { + return; + } + + foreach (Location location in type.Locations) + { + if (location.IsInSource) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0009_DataContractToDataType, + location, + type.Name)); + } + } + } + + private static bool HasAttribute(ISymbol symbol, INamedTypeSymbol attributeType) + { + SymbolEqualityComparer eq = SymbolEqualityComparer.Default; + foreach (AttributeData attr in symbol.GetAttributes()) + { + if (attr.AttributeClass != null && eq.Equals(attr.AttributeClass, attributeType)) + { + return true; + } + } + return false; + } + + private static bool HasDataMemberProperty(INamedTypeSymbol type, INamedTypeSymbol dataMemberType) + { + foreach (ISymbol member in type.GetMembers()) + { + if (member is IPropertySymbol property && HasAttribute(property, dataMemberType)) + { + return true; + } + } + return false; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0010RemoveDisposableAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0010RemoveDisposableAnalyzer.cs new file mode 100644 index 0000000000..2c17d28a59 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0010RemoveDisposableAnalyzer.cs @@ -0,0 +1,204 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0010: Detect using declarations / statements whose variable + /// type is one of the OPC UA identity types that are no longer + /// in 2.0 + /// (CertificateIdentifier, UserIdentity, or any + /// implementation of IUserIdentityTokenHandler). + /// + /// + /// Detection is purely syntactic: the analyzer looks at + /// and + /// nodes that carry the using keyword and resolves the declared + /// variable's type through the . + /// + /// Form B (direct Dispose() invocation) is intentionally not + /// implemented here: the stub OPC UA types are not IDisposable, + /// so any explicit Dispose() call would not compile and there is + /// no operation to bind against in the analyzer test surface. + /// + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0010RemoveDisposableAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0010_RemoveDisposable); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + Dictionary cache = []; + UaSymbols symbols = UaSymbols.For(context.Compilation, cache); + if (!symbols.ReferencesOpcUa) + { + return; + } + if (symbols.CertificateIdentifierType is null && + symbols.UserIdentityType is null && + symbols.UserIdentityTokenHandlerType is null) + { + return; + } + + context.RegisterSyntaxNodeAction( + c => AnalyzeUsingStatement(c, symbols), + SyntaxKind.UsingStatement); + context.RegisterSyntaxNodeAction( + c => AnalyzeLocalDeclaration(c, symbols), + SyntaxKind.LocalDeclarationStatement); + } + + private static void AnalyzeUsingStatement(SyntaxNodeAnalysisContext context, UaSymbols symbols) + { + UsingStatementSyntax usingStmt = (UsingStatementSyntax)context.Node; + if (usingStmt.Declaration is { } declaration) + { + ReportIfMatch(context, symbols, declaration, usingStmt.GetLocation()); + } + else if (usingStmt.Expression is { } expression) + { + ITypeSymbol type = context.SemanticModel.GetTypeInfo(expression, context.CancellationToken).Type; + Report(context, symbols, type, usingStmt.GetLocation()); + } + } + + private static void AnalyzeLocalDeclaration(SyntaxNodeAnalysisContext context, UaSymbols symbols) + { + LocalDeclarationStatementSyntax local = (LocalDeclarationStatementSyntax)context.Node; + if (local.UsingKeyword.IsKind(SyntaxKind.None)) + { + return; + } + ReportIfMatch(context, symbols, local.Declaration, local.GetLocation()); + } + + private static void ReportIfMatch( + SyntaxNodeAnalysisContext context, + UaSymbols symbols, + VariableDeclarationSyntax declaration, + Location location) + { + ITypeSymbol declaredType = context.SemanticModel + .GetTypeInfo(declaration.Type, context.CancellationToken).Type; + + // var: resolve from a variable initializer if available. + if (declaredType is null || declaredType.TypeKind == TypeKind.Error) + { + foreach (VariableDeclaratorSyntax variable in declaration.Variables) + { + if (variable.Initializer?.Value is { } init) + { + ITypeSymbol initType = context.SemanticModel + .GetTypeInfo(init, context.CancellationToken).Type; + if (initType != null) + { + declaredType = initType; + break; + } + } + } + } + + Report(context, symbols, declaredType, location); + } + + private static void Report( + SyntaxNodeAnalysisContext context, + UaSymbols symbols, + ITypeSymbol type, + Location location) + { + if (type is null) + { + return; + } + if (IsTargetType(type, symbols, out string typeName)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0010_RemoveDisposable, + location, + typeName)); + } + } + + private static bool IsTargetType(ITypeSymbol type, UaSymbols symbols, out string name) + { + name = null; + if (symbols.CertificateIdentifierType != null && + SymbolEqualityComparer.Default.Equals(type, symbols.CertificateIdentifierType)) + { + name = symbols.CertificateIdentifierType.Name; + return true; + } + if (symbols.UserIdentityType != null && + SymbolEqualityComparer.Default.Equals(type, symbols.UserIdentityType)) + { + name = symbols.UserIdentityType.Name; + return true; + } + INamedTypeSymbol tokenHandler = symbols.UserIdentityTokenHandlerType; + if (tokenHandler != null) + { + if (SymbolEqualityComparer.Default.Equals(type, tokenHandler)) + { + name = tokenHandler.Name; + return true; + } + foreach (INamedTypeSymbol iface in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(iface, tokenHandler)) + { + name = type.Name; + return true; + } + } + } + return false; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0011TokenHandlerSyncToAsyncAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0011TokenHandlerSyncToAsyncAnalyzer.cs new file mode 100644 index 0000000000..aa029555b8 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0011TokenHandlerSyncToAsyncAnalyzer.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.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0011: Detect calls to the obsolete synchronous + /// Encrypt/Decrypt/Sign/Verify members on + /// Opc.Ua.IUserIdentityTokenHandler (or any implementing type) + /// and recommend their async counterparts. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0011TokenHandlerSyncToAsyncAnalyzer : DiagnosticAnalyzer + { + private const string TokenHandlerFullName = "Opc.Ua.IUserIdentityTokenHandler"; + + private static readonly HashSet s_targetNames = + [ + "Encrypt", + "Decrypt", + "Sign", + "Verify", + ]; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0011_TokenHandlerSyncToAsync); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + INamedTypeSymbol tokenHandler = context.Compilation.GetTypeByMetadataName(TokenHandlerFullName); + if (tokenHandler is null) + { + return; + } + + context.RegisterOperationAction( + ctx => AnalyzeInvocation(ctx, tokenHandler), + OperationKind.Invocation); + } + + private static void AnalyzeInvocation( + OperationAnalysisContext context, + INamedTypeSymbol tokenHandler) + { + IInvocationOperation invocation = (IInvocationOperation)context.Operation; + IMethodSymbol method = invocation.TargetMethod; + + if (method is null || !s_targetNames.Contains(method.Name)) + { + return; + } + + bool isShim = method.IsOpcUaShim("UA0011"); + if (!isShim && !method.IsObsolete()) + { + return; + } + + if (!isShim) + { + INamedTypeSymbol containing = method.ContainingType; + if (containing is null) + { + return; + } + + bool declaredOnHandler = SymbolEqualityComparer.Default.Equals(containing, tokenHandler); + if (!declaredOnHandler) + { + bool implementsHandler = false; + foreach (INamedTypeSymbol iface in containing.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(iface, tokenHandler)) + { + implementsHandler = true; + break; + } + } + if (!implementsHandler) + { + return; + } + } + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0011_TokenHandlerSyncToAsync, + invocation.Syntax.GetLocation(), + method.Name)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0012CertificateFactoryStaticToInstanceAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0012CertificateFactoryStaticToInstanceAnalyzer.cs new file mode 100644 index 0000000000..e7f3e2915d --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0012CertificateFactoryStaticToInstanceAnalyzer.cs @@ -0,0 +1,94 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0012: Detect calls to obsolete static CertificateFactory members + /// and recommend the DefaultCertificateFactory.Instance singleton. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0012CertificateFactoryStaticToInstanceAnalyzer : DiagnosticAnalyzer + { + private static readonly HashSet s_targetNames = + [ + "Create", + "CreateCertificate", + "CreateSigningRequest", + "RevokeCertificate", + "CreateCertificateWithPEMPrivateKey", + "CreateCertificateWithPrivateKey", + ]; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0012_CertificateFactoryStaticToInstance); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + } + + private static void AnalyzeInvocation(OperationAnalysisContext context) + { + IInvocationOperation invocation = (IInvocationOperation)context.Operation; + IMethodSymbol method = invocation.TargetMethod; + + if (!method.IsStatic || !s_targetNames.Contains(method.Name)) + { + return; + } + + INamedTypeSymbol containing = method.ContainingType; + if (containing is null || containing.ToDisplayString() != "Opc.Ua.CertificateFactory") + { + return; + } + + if (!method.IsObsolete()) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0012_CertificateFactoryStaticToInstance, + invocation.Syntax.GetLocation(), + method.Name)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0014DataValueIsGoodAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0014DataValueIsGoodAnalyzer.cs new file mode 100644 index 0000000000..064fc3185e --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0014DataValueIsGoodAnalyzer.cs @@ -0,0 +1,108 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0014: Replace the obsolete static DataValue.IsGood(dv) / + /// IsBad / IsUncertain (+ IsNotXxx) helpers with the + /// matching instance property on DataValue. + /// + /// + /// Detection: any that targets a static + /// method named one of the six helpers on either Opc.Ua.DataValue or + /// Opc.Ua.DataValueExtensions, with a single DataValue argument. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0014DataValueIsGoodAnalyzer : DiagnosticAnalyzer + { + private static readonly HashSet s_targetNames = + [ + "IsGood", + "IsBad", + "IsUncertain", + "IsNotGood", + "IsNotBad", + "IsNotUncertain", + ]; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0014_DataValueIsGoodStaticToInstance); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + } + + private static void AnalyzeInvocation(OperationAnalysisContext context) + { + IInvocationOperation invocation = (IInvocationOperation)context.Operation; + IMethodSymbol method = invocation.TargetMethod; + + if (!method.IsStatic || + method.Parameters.Length != 1 || + !s_targetNames.Contains(method.Name)) + { + return; + } + + INamedTypeSymbol containing = method.ContainingType; + if (containing is null) + { + return; + } + string containingName = containing.ToDisplayString(); + if (containingName != "Opc.Ua.DataValue" && + containingName != "Opc.Ua.DataValueExtensions") + { + return; + } + + ITypeSymbol argType = method.Parameters[0].Type; + if (argType is null || argType.ToDisplayString() != "Opc.Ua.DataValue") + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0014_DataValueIsGoodStaticToInstance, + invocation.Syntax.GetLocation(), + method.Name)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0015GdsSyncToAsyncAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0015GdsSyncToAsyncAnalyzer.cs new file mode 100644 index 0000000000..44f494b202 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0015GdsSyncToAsyncAnalyzer.cs @@ -0,0 +1,115 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0015: Detect calls to obsolete synchronous / APM members on the + /// OPC UA discovery clients (GlobalDiscoveryServerClient, + /// ServerPushConfigurationClient, LocalDiscoveryServerClient) + /// and recommend the async counterparts. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0015GdsSyncToAsyncAnalyzer : DiagnosticAnalyzer + { + private static readonly string[] s_targetTypeNames = + [ + "Opc.Ua.GlobalDiscoveryServerClient", + "Opc.Ua.ServerPushConfigurationClient", + "Opc.Ua.LocalDiscoveryServerClient", + ]; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0015_GdsSyncToAsync); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + HashSet targets = new(SymbolEqualityComparer.Default); + foreach (string name in s_targetTypeNames) + { + INamedTypeSymbol sym = context.Compilation.GetTypeByMetadataName(name); + if (sym != null) + { + targets.Add(sym); + } + } + + context.RegisterOperationAction( + ctx => AnalyzeInvocation(ctx, targets), + OperationKind.Invocation); + } + + private static void AnalyzeInvocation( + OperationAnalysisContext context, + HashSet targets) + { + IInvocationOperation invocation = (IInvocationOperation)context.Operation; + IMethodSymbol method = invocation.TargetMethod; + if (method is null) + { + return; + } + + bool isShim = method.IsOpcUaShim("UA0015"); + if (!isShim) + { + INamedTypeSymbol containing = method.ContainingType; + if (containing is null || !targets.Contains(containing)) + { + return; + } + + if (!method.IsObsolete()) + { + return; + } + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0015_GdsSyncToAsync, + invocation.Syntax.GetLocation(), + method.Name)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0018CertificateIdentifierCertificateAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0018CertificateIdentifierCertificateAnalyzer.cs new file mode 100644 index 0000000000..323154a91d --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0018CertificateIdentifierCertificateAnalyzer.cs @@ -0,0 +1,98 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0018: Detect reads of the obsolete Certificate getter on + /// CertificateIdentifier-family types and recommend + /// CertificateIdentifierResolver.ResolveAsync. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0018CertificateIdentifierCertificateAnalyzer : DiagnosticAnalyzer + { + private const string CertificatePropertyName = "Certificate"; + private const string CertificateIdentifierTypeSuffix = "CertificateIdentifier"; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0018_CertificateIdentifierCertificateGetter); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzePropertyReference, OperationKind.PropertyReference); + } + + private static void AnalyzePropertyReference(OperationAnalysisContext context) + { + IPropertyReferenceOperation reference = (IPropertyReferenceOperation)context.Operation; + IPropertySymbol property = reference.Property; + if (property is null || property.Name != CertificatePropertyName) + { + return; + } + + bool isShim = property.IsOpcUaShim("UA0018"); + if (!isShim) + { + if (!property.IsObsolete()) + { + return; + } + + INamedTypeSymbol containing = property.ContainingType; + if (containing is null) + { + return; + } + + string typeName = containing.Name; + if (typeName is null || + !(typeName == CertificateIdentifierTypeSuffix || + typeName.EndsWith(CertificateIdentifierTypeSuffix, System.StringComparison.Ordinal) || + typeName.Contains(CertificateIdentifierTypeSuffix))) + { + return; + } + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0018_CertificateIdentifierCertificateGetter, + reference.Syntax.GetLocation())); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0019DataValueStatusCodeCtorAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0019DataValueStatusCodeCtorAnalyzer.cs new file mode 100644 index 0000000000..4de6a130fd --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0019DataValueStatusCodeCtorAnalyzer.cs @@ -0,0 +1,97 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0019: Detect new DataValue(StatusCode) and + /// new DataValue(StatusCode, DateTimeUtc) constructor calls and + /// recommend the explicit DataValue.FromStatusCode(...) factory. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0019DataValueStatusCodeCtorAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0019_ObsoleteDataValueStatusCodeCtor); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); + } + + private static void AnalyzeObjectCreation(OperationAnalysisContext context) + { + IObjectCreationOperation creation = (IObjectCreationOperation)context.Operation; + IMethodSymbol ctor = creation.Constructor; + if (ctor is null) + { + return; + } + + INamedTypeSymbol containing = ctor.ContainingType; + if (containing is null || containing.ToDisplayString() != "Opc.Ua.DataValue") + { + return; + } + + if (ctor.Parameters.Length < 1 || ctor.Parameters.Length > 2) + { + return; + } + + if (ctor.Parameters[0].Type?.ToDisplayString() != "Opc.Ua.StatusCode") + { + return; + } + + string extra = string.Empty; + if (ctor.Parameters.Length == 2) + { + if (ctor.Parameters[1].Type?.ToDisplayString() != "Opc.Ua.DateTimeUtc") + { + return; + } + extra = ", DateTimeUtc"; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0019_ObsoleteDataValueStatusCodeCtor, + creation.Syntax.GetLocation(), + extra)); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0020EncodeableFactoryRenameAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0020EncodeableFactoryRenameAnalyzer.cs new file mode 100644 index 0000000000..cf895ce415 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0020EncodeableFactoryRenameAnalyzer.cs @@ -0,0 +1,135 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0020: Detect references to the obsolete + /// EncodeableFactory.GlobalFactory static property (Form A) and + /// instance EncodeableFactory.Create() calls (Form B) and + /// recommend the 2.0 replacements. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0020EncodeableFactoryRenameAnalyzer : DiagnosticAnalyzer + { + // Public surface preserved for source compatibility; values delegate to the + // shared WellKnownProperties (also linked into the CodeFixes assembly). + public const string FormProperty = WellKnownProperties.Form; + public const string FormGlobalFactory = WellKnownProperties.FormGlobalFactory; + public const string FormCreate = WellKnownProperties.FormCreate; + + private const string EncodeableFactoryTypeName = "Opc.Ua.EncodeableFactory"; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0020_EncodeableFactoryRename); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzePropertyReference, OperationKind.PropertyReference); + context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + } + + private static void AnalyzePropertyReference(OperationAnalysisContext context) + { + IPropertyReferenceOperation reference = (IPropertyReferenceOperation)context.Operation; + IPropertySymbol property = reference.Property; + if (property is null || property.Name != "GlobalFactory") + { + return; + } + + bool isShim = property.IsOpcUaShim("UA0020"); + if (!isShim) + { + if (!property.IsStatic) + { + return; + } + INamedTypeSymbol containing = property.ContainingType; + if (containing is null || containing.ToDisplayString() != EncodeableFactoryTypeName) + { + return; + } + } + + ImmutableDictionary properties = ImmutableDictionary.Empty + .Add(FormProperty, FormGlobalFactory); + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0020_EncodeableFactoryRename, + reference.Syntax.GetLocation(), + properties, + "EncodeableFactory.GlobalFactory", + "ServiceMessageContext.Factory")); + } + + private static void AnalyzeInvocation(OperationAnalysisContext context) + { + IInvocationOperation invocation = (IInvocationOperation)context.Operation; + IMethodSymbol method = invocation.TargetMethod; + if (method is null || method.Name != "Create") + { + return; + } + + bool isShim = method.IsOpcUaShim("UA0020"); + if (!isShim) + { + if (method.IsStatic || method.Parameters.Length != 0) + { + return; + } + INamedTypeSymbol containing = method.ContainingType; + if (containing is null || containing.ToDisplayString() != EncodeableFactoryTypeName) + { + return; + } + } + + ImmutableDictionary properties = ImmutableDictionary.Empty + .Add(FormProperty, FormCreate); + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0020_EncodeableFactoryRename, + invocation.Syntax.GetLocation(), + properties, + "EncodeableFactory.Create()", + "Fork()")); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0021CertificateValidatorRenameAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0021CertificateValidatorRenameAnalyzer.cs new file mode 100644 index 0000000000..91f21b025a --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0021CertificateValidatorRenameAnalyzer.cs @@ -0,0 +1,232 @@ +/* ======================================================================== + * 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.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0021: Detect references to the legacy Opc.Ua.CertificateValidator class and + /// Opc.Ua.CertificateValidationEventArgs, which were removed in 1.6 in favour of the + /// new ICertificateManager / ICertificateValidatorEx / + /// CertificateValidationResult pipeline. + /// + /// + /// Diagnostic-only: the 1.5.378 -> 1.6 change is structural (event-based per-error + /// accept handler became an async ValidateAsync call returning a + /// CertificateValidationResult, with per-error accept logic moving to + /// CertificateValidationOptions.AcceptError). There is therefore no accompanying + /// code-fix provider; consumers must perform the migration manually using + /// Docs/MigrationGuide.md#certificatemanager-and-segregated-interfaces. + /// + /// Detection strategy: + /// + /// If the legacy type is present in the compilation (via the 1.5.378 stack or the shim + /// package), fire when an [Obsolete]-marked type with the matching full name is + /// referenced. + /// If the legacy type has been genuinely removed (consumer is on the bare 1.6 stack + /// and the call site no longer compiles), fall back to a syntactic match on the bare + /// identifier name, scoped to source files that import any Opc.Ua namespace (bare + /// using Opc.Ua; or any sub-namespace such as using Opc.Ua.Server;) or that + /// declare a namespace under the Opc.Ua tree. + /// + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0021CertificateValidatorRenameAnalyzer : DiagnosticAnalyzer + { + private const string OpcUaNamespace = "Opc.Ua"; + private const string CertificateValidatorTypeName = "CertificateValidator"; + private const string CertificateValidationEventArgsTypeName = "CertificateValidationEventArgs"; + private const string CertificateValidatorFullName = OpcUaNamespace + "." + CertificateValidatorTypeName; + private const string CertificateValidationEventArgsFullName = + OpcUaNamespace + "." + CertificateValidationEventArgsTypeName; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0021_CertificateValidatorRename); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static startContext => + { + INamedTypeSymbol? legacyValidator = + startContext.Compilation.GetTypeByMetadataName(CertificateValidatorFullName); + INamedTypeSymbol? legacyEventArgs = + startContext.Compilation.GetTypeByMetadataName(CertificateValidationEventArgsFullName); + + bool hasLegacySymbol = legacyValidator is not null || legacyEventArgs is not null; + + if (hasLegacySymbol) + { + startContext.RegisterSyntaxNodeAction( + ctx => AnalyzeIdentifierSymbol(ctx, legacyValidator, legacyEventArgs), + SyntaxKind.IdentifierName); + } + else + { + startContext.RegisterSyntaxNodeAction( + AnalyzeIdentifierSyntactic, + SyntaxKind.IdentifierName); + } + }); + } + + private static void AnalyzeIdentifierSymbol( + SyntaxNodeAnalysisContext context, + INamedTypeSymbol? legacyValidator, + INamedTypeSymbol? legacyEventArgs) + { + IdentifierNameSyntax identifier = (IdentifierNameSyntax)context.Node; + string text = identifier.Identifier.ValueText; + if (text != CertificateValidatorTypeName && text != CertificateValidationEventArgsTypeName) + { + return; + } + + SymbolInfo info = context.SemanticModel.GetSymbolInfo(identifier, context.CancellationToken); + ISymbol? symbol = info.Symbol; + if (symbol is null) + { + return; + } + + INamedTypeSymbol? namedType = symbol as INamedTypeSymbol + ?? (symbol as IMethodSymbol)?.ContainingType; + if (namedType is null) + { + return; + } + + bool matchesLegacy = + (legacyValidator is not null + && SymbolEqualityComparer.Default.Equals(namedType, legacyValidator)) || + (legacyEventArgs is not null + && SymbolEqualityComparer.Default.Equals(namedType, legacyEventArgs)); + if (!matchesLegacy) + { + return; + } + + if (!namedType.IsObsolete()) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0021_CertificateValidatorRename, + identifier.GetLocation(), + namedType.Name)); + } + + private static void AnalyzeIdentifierSyntactic(SyntaxNodeAnalysisContext context) + { + IdentifierNameSyntax identifier = (IdentifierNameSyntax)context.Node; + string text = identifier.Identifier.ValueText; + if (text != CertificateValidatorTypeName && text != CertificateValidationEventArgsTypeName) + { + return; + } + + if (!HasOpcUaContext(identifier)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0021_CertificateValidatorRename, + identifier.GetLocation(), + text)); + } + + private static bool HasOpcUaContext(SyntaxNode node) + { + for (SyntaxNode? current = node; current is not null; current = current.Parent) + { + if (current is BaseNamespaceDeclarationSyntax ns) + { + if (IsOpcUaNamespaceName(ns.Name) || ContainsOpcUaUsing(ns.Usings)) + { + return true; + } + } + + if (current is CompilationUnitSyntax compilationUnit) + { + return ContainsOpcUaUsing(compilationUnit.Usings); + } + } + + return false; + } + + private static bool ContainsOpcUaUsing(SyntaxList usings) + { + foreach (UsingDirectiveSyntax @using in usings) + { + if (@using.Alias is not null || @using.StaticKeyword.IsKind(SyntaxKind.StaticKeyword)) + { + continue; + } + if (@using.Name is null) + { + continue; + } + if (IsOpcUaNamespaceName(@using.Name)) + { + return true; + } + } + return false; + } + + private static bool IsOpcUaNamespaceName(NameSyntax? name) + { + if (name is null) + { + return false; + } + string text = name.ToString(); + // Match bare "Opc.Ua" or any sub-namespace such as "Opc.Ua.Server", + // "Opc.Ua.Configuration", "Opc.Ua.Client.ComplexTypes" etc. We require a '.' + // separator after "Opc.Ua" so that a hypothetical sibling namespace like + // "Opc.UaFoo" does not match. + return string.Equals(text, OpcUaNamespace, StringComparison.Ordinal) + || text.StartsWith(OpcUaNamespace + ".", StringComparison.Ordinal); + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0022CertificateValidatorPropertyRenameAnalyzer.cs b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0022CertificateValidatorPropertyRenameAnalyzer.cs new file mode 100644 index 0000000000..f88e345277 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0022CertificateValidatorPropertyRenameAnalyzer.cs @@ -0,0 +1,283 @@ +/* ======================================================================== + * 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.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Opc.Ua.MigrationAnalyzer.Diagnostics; +using Opc.Ua.MigrationAnalyzer.Helpers; + +namespace Opc.Ua.MigrationAnalyzer.Analyzers +{ + /// + /// UA0022: Detect access to the legacy + /// ApplicationConfiguration.CertificateValidator / + /// ServerBase.CertificateValidator properties (removed in 2.0) and + /// recommend the new CertificateManager property (type + /// ICertificateManager). + /// + /// + /// Dual-mode detection mirrors UA0021: + /// + /// Semantic path () when + /// the legacy CertificateValidator property is still present and + /// marked [Obsolete]. + /// Syntactic fallback () + /// when the legacy property has been removed entirely and the call site no + /// longer compiles — gated by using Opc.Ua; and a receiver whose + /// (possibly error) type name still ends with + /// ApplicationConfiguration / ServerBase. + /// + /// The receiver short name is passed as the {0} message arg. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class UA0022CertificateValidatorPropertyRenameAnalyzer : DiagnosticAnalyzer + { + private const string OpcUaNamespace = "Opc.Ua"; + private const string CertificateValidatorPropertyName = "CertificateValidator"; + private const string ApplicationConfigurationTypeName = "ApplicationConfiguration"; + private const string ServerBaseTypeName = "ServerBase"; + private const string ApplicationConfigurationFullName = + OpcUaNamespace + "." + ApplicationConfigurationTypeName; + private const string ServerBaseFullName = OpcUaNamespace + "." + ServerBaseTypeName; + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(DiagnosticDescriptors.UA0022_CertificateValidatorPropertyRename); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static startContext => + { + INamedTypeSymbol? appConfig = + startContext.Compilation.GetTypeByMetadataName(ApplicationConfigurationFullName); + INamedTypeSymbol? serverBase = + startContext.Compilation.GetTypeByMetadataName(ServerBaseFullName); + + startContext.RegisterOperationAction( + ctx => AnalyzePropertyReference(ctx, appConfig, serverBase), + OperationKind.PropertyReference); + + startContext.RegisterSyntaxNodeAction( + ctx => AnalyzeMemberAccessSyntactic(ctx, appConfig, serverBase), + SyntaxKind.SimpleMemberAccessExpression); + }); + } + + private static void AnalyzePropertyReference( + OperationAnalysisContext context, + INamedTypeSymbol? appConfig, + INamedTypeSymbol? serverBase) + { + IPropertyReferenceOperation reference = (IPropertyReferenceOperation)context.Operation; + IPropertySymbol property = reference.Property; + if (property is null || property.Name != CertificateValidatorPropertyName) + { + return; + } + + INamedTypeSymbol containing = property.ContainingType; + if (containing is null) + { + return; + } + + string? receiverName = null; + if (appConfig is not null && IsOrInheritsFrom(containing, appConfig)) + { + receiverName = ApplicationConfigurationTypeName; + } + else if (serverBase is not null && IsOrInheritsFrom(containing, serverBase)) + { + receiverName = ServerBaseTypeName; + } + + if (receiverName is null) + { + return; + } + + if (!property.IsObsolete()) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0022_CertificateValidatorPropertyRename, + reference.Syntax.GetLocation(), + receiverName)); + } + + private static void AnalyzeMemberAccessSyntactic( + SyntaxNodeAnalysisContext context, + INamedTypeSymbol? appConfig, + INamedTypeSymbol? serverBase) + { + MemberAccessExpressionSyntax memberAccess = (MemberAccessExpressionSyntax)context.Node; + if (memberAccess.Name.Identifier.ValueText != CertificateValidatorPropertyName) + { + return; + } + + // Skip if semantic path already matched (avoid double-fire). + SymbolInfo symInfo = context.SemanticModel.GetSymbolInfo(memberAccess, context.CancellationToken); + if (symInfo.Symbol is IPropertySymbol resolved && + resolved.ContainingType is INamedTypeSymbol containingResolved) + { + string fullName = containingResolved.ToDisplayString(); + if (fullName == ApplicationConfigurationFullName || + fullName == ServerBaseFullName) + { + // Semantic action handles it (and gates on [Obsolete]). + return; + } + if ((appConfig is not null && IsOrInheritsFrom(containingResolved, appConfig)) || + (serverBase is not null && IsOrInheritsFrom(containingResolved, serverBase))) + { + return; + } + } + + if (!HasOpcUaUsing(memberAccess)) + { + return; + } + + TypeInfo receiverType = context.SemanticModel.GetTypeInfo( + memberAccess.Expression, + context.CancellationToken); + ITypeSymbol? type = receiverType.Type ?? receiverType.ConvertedType; + string? receiverName = ClassifyReceiverName(type); + if (receiverName is null) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.UA0022_CertificateValidatorPropertyRename, + memberAccess.GetLocation(), + receiverName)); + } + + private static string? ClassifyReceiverName(ITypeSymbol? type) + { + if (type is null) + { + return null; + } + string name = type.Name; + if (string.IsNullOrEmpty(name)) + { + // Error symbol with no name — be lenient: examine the display string. + string display = type.ToDisplayString(); + if (display.Contains(ApplicationConfigurationTypeName, StringComparison.Ordinal)) + { + return ApplicationConfigurationTypeName; + } + if (display.Contains(ServerBaseTypeName, StringComparison.Ordinal)) + { + return ServerBaseTypeName; + } + return null; + } + + if (name.Contains(ApplicationConfigurationTypeName, StringComparison.Ordinal)) + { + return ApplicationConfigurationTypeName; + } + if (name.Contains(ServerBaseTypeName, StringComparison.Ordinal)) + { + return ServerBaseTypeName; + } + return null; + } + + private static bool IsOrInheritsFrom(INamedTypeSymbol candidate, INamedTypeSymbol target) + { + SymbolEqualityComparer eq = SymbolEqualityComparer.Default; + INamedTypeSymbol? current = candidate; + while (current is not null) + { + if (eq.Equals(current, target)) + { + return true; + } + current = current.BaseType; + } + return false; + } + + private static bool HasOpcUaUsing(SyntaxNode node) + { + for (SyntaxNode? current = node; current is not null; current = current.Parent) + { + if (current is BaseNamespaceDeclarationSyntax ns && + ContainsOpcUaUsing(ns.Usings)) + { + return true; + } + + if (current is CompilationUnitSyntax compilationUnit) + { + return ContainsOpcUaUsing(compilationUnit.Usings); + } + } + + return false; + } + + private static bool ContainsOpcUaUsing(SyntaxList usings) + { + foreach (UsingDirectiveSyntax @using in usings) + { + if (@using.Alias is not null || @using.StaticKeyword.IsKind(SyntaxKind.StaticKeyword)) + { + continue; + } + if (@using.Name is null) + { + continue; + } + string text = @using.Name.ToString(); + if (string.Equals(text, OpcUaNamespace, StringComparison.Ordinal) || + text.StartsWith(OpcUaNamespace + ".", StringComparison.Ordinal)) + { + return true; + } + } + return false; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs b/Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 0000000000..53995385d5 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,193 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; + +namespace Opc.Ua.MigrationAnalyzer.Diagnostics +{ + /// + /// Centralised registry of every shipped + /// by the OPC UA migration analyzer package. Descriptors are created once + /// and shared by analyzers and tests, so message text and severity stay + /// consistent across the codebase. + /// + internal static class DiagnosticDescriptors + { + private static DiagnosticDescriptor Create( + string id, + string title, + string messageFormat, + DiagnosticSeverity defaultSeverity, + string description) + { + return new DiagnosticDescriptor( + id: id, + title: title, + messageFormat: messageFormat, + category: DiagnosticIds.Category, + defaultSeverity: defaultSeverity, + isEnabledByDefault: true, + description: description, + helpLinkUri: DiagnosticIds.HelpLinkFor(id)); + } + + public static readonly DiagnosticDescriptor UA0001_UtilsTraceToILogger = Create( + DiagnosticIds.UA0001, + "Replace Utils.Trace / Utils.LogX with ILogger", + "'{0}' is deprecated. Use an ILogger obtained from ITelemetryContext.CreateLogger() instead.", + DiagnosticSeverity.Info, + "Opc.Ua.Utils.Trace and the Utils.LogX helpers are obsolete in 2.0. Logging is now performed through ILogger instances created from an ITelemetryContext that flows through the constructor."); + + public static readonly DiagnosticDescriptor UA0002_RemovedCollectionType = Create( + DiagnosticIds.UA0002, + "Removed collection wrapper type", + "'{0}' was removed in 2.0. Use 'List<{1}>' for mutable storage or 'ArrayOf<{1}>' for read-only consumers.", + DiagnosticSeverity.Warning, + "Generated Collection wrappers (Int32Collection, VariantCollection, NodeIdCollection, ...) were removed in 2.0 in favour of List and ArrayOf."); + + public static readonly DiagnosticDescriptor UA0003_NullCheckOnStructType = Create( + DiagnosticIds.UA0003, + "Null comparison on now-struct built-in type", + "'{0}' is now a value type; use IsNull instead of comparing with null", + DiagnosticSeverity.Warning, + "NodeId, ExpandedNodeId, QualifiedName, LocalizedText, ExtensionObject, DataValue, Variant and ByteString are readonly structs in 2.0. Comparing them with null is misleading. Use the .IsNull property (or LocalizedText.IsNullOrEmpty) instead."); + + public static readonly DiagnosticDescriptor UA0004_ConditionalAccessOnStructType = Create( + DiagnosticIds.UA0004, + "Null-conditional access on now-struct built-in type", + "'?.' on '{0}' is unnecessary because '{0}' is now a value type. Use a direct access or guard with '.IsNull'.", + DiagnosticSeverity.Warning, + "NodeId, Variant, DataValue and the other built-in types became structs in 2.0 — the null-conditional operator is no longer meaningful on them."); + + public static readonly DiagnosticDescriptor UA0005_ByteArrayWhereByteStringExpected = Create( + DiagnosticIds.UA0005, + "Pass ByteString where required", + "A 'byte[]' is being passed where 'ByteString' is now expected by '{0}'. Call '.ToByteString()' on the array.", + DiagnosticSeverity.Warning, + "2.0 APIs that previously took byte[] now require Opc.Ua.ByteString. Convert with the .ToByteString() extension."); + + public static readonly DiagnosticDescriptor UA0006_ObsoleteVariantCtor = Create( + DiagnosticIds.UA0006, + "Obsolete Variant constructor", + "'new Variant({0})' is obsolete. Use 'Variant.From({0})' (and the matching Uuid/DateTimeUtc/ByteString wrapper if needed).", + DiagnosticSeverity.Warning, + "The non-generic Variant constructors accepting object/DateTime/Guid/byte[] were obsoleted in 2.0. Variant.From(T) preserves the value's type information correctly."); + + public static readonly DiagnosticDescriptor UA0007_ObsoleteNodeIdStringCtor = Create( + DiagnosticIds.UA0007, + "Obsolete NodeId(string) constructor", + "'new {0}(string)' is obsolete. Use '{0}.Parse(s)' (or 'TryParse' for untrusted input).", + DiagnosticSeverity.Warning, + "new NodeId(string) and new ExpandedNodeId(string) were obsoleted in 2.0 in favour of explicit Parse / TryParse."); + + public static readonly DiagnosticDescriptor UA0008_SessionCallParamsObject = Create( + DiagnosticIds.UA0008, + "Wrap Session.Call arguments with Variant.From", + "'{0}' now takes 'params Variant[]'. Wrap each argument with 'Variant.From(...)' (or 'Variant.Null' for null).", + DiagnosticSeverity.Warning, + "Session.Call / Session.CallAsync changed from params object[] to params Variant[] in 2.0."); + + public static readonly DiagnosticDescriptor UA0009_DataContractToDataType = Create( + DiagnosticIds.UA0009, + "Replace [DataContract]/[DataMember] on configuration extensions", + "'{0}' is consumed by ParseExtension or UpdateExtension; use [DataType]/[DataTypeField] from Opc.Ua and mark the class partial", + DiagnosticSeverity.Warning, + "Configuration extension classes serialised through ParseExtension/UpdateExtension use the source-generator-driven [DataType]/[DataTypeField] attributes in 2.0."); + + public static readonly DiagnosticDescriptor UA0010_RemoveDisposable = Create( + DiagnosticIds.UA0010, + "Remove using/Dispose on non-IDisposable identity", + "'{0}' is no longer IDisposable in 2.0 — remove the 'using' (or the explicit Dispose call). Lifecycle is owned by CertificateManager.", + DiagnosticSeverity.Warning, + "CertificateIdentifier, UserIdentity and IUserIdentityTokenHandler are no longer IDisposable in 2.0."); + + public static readonly DiagnosticDescriptor UA0011_TokenHandlerSyncToAsync = Create( + DiagnosticIds.UA0011, + "User identity token handler — use async members", + "'{0}' is removed in 2.0; call the async counterpart and propagate the CancellationToken", + DiagnosticSeverity.Info, + "IUserIdentityTokenHandler synchronous Encrypt/Decrypt/Sign/Verify have been replaced by their *Async counterparts."); + + public static readonly DiagnosticDescriptor UA0012_CertificateFactoryStaticToInstance = Create( + DiagnosticIds.UA0012, + "Obsolete static CertificateFactory member", + "'CertificateFactory.{0}' is obsolete. Use 'DefaultCertificateFactory.Instance.{0}'.", + DiagnosticSeverity.Warning, + "Static CertificateFactory helpers (Create, CreateCertificate, CreateSigningRequest, RevokeCertificate, ...) were obsoleted in 2.0 in favour of the singleton DefaultCertificateFactory.Instance."); + + public static readonly DiagnosticDescriptor UA0014_DataValueIsGoodStaticToInstance = Create( + DiagnosticIds.UA0014, + "Use DataValue.IsGood instance property", + "'DataValue.{0}(dv)' is obsolete. Use 'dv.{0}'.", + DiagnosticSeverity.Warning, + "The static DataValue.IsGood/IsBad/IsUncertain helpers became instance properties in 2.0."); + + public static readonly DiagnosticDescriptor UA0015_GdsSyncToAsync = Create( + DiagnosticIds.UA0015, + "GDS/LDS client — use async members", + "'{0}' is removed in 2.0; call the async counterpart and propagate the CancellationToken", + DiagnosticSeverity.Info, + "Synchronous and APM members on GlobalDiscoveryServerClient / LocalDiscoveryServerClient / ServerPushConfigurationClient were removed in 2.0."); + + public static readonly DiagnosticDescriptor UA0018_CertificateIdentifierCertificateGetter = Create( + DiagnosticIds.UA0018, + "Use CertificateIdentifierResolver.ResolveAsync", + "'CertificateIdentifier.Certificate' is removed in 2.0; call CertificateIdentifierResolver.ResolveAsync", + DiagnosticSeverity.Info, + "The synchronous CertificateIdentifier.Certificate getter was removed in 2.0. Use the async resolver."); + + public static readonly DiagnosticDescriptor UA0019_ObsoleteDataValueStatusCodeCtor = Create( + DiagnosticIds.UA0019, + "Obsolete DataValue(StatusCode) constructor", + "'new DataValue(StatusCode{0})' silently lost the value semantics. Use 'DataValue.FromStatusCode(...)'.", + DiagnosticSeverity.Warning, + "new DataValue(StatusCode) / new DataValue(StatusCode, DateTimeUtc) were obsoleted in 2.0 because they silently resolved to the StatusCode overload and lost the value. DataValue.FromStatusCode is explicit."); + + public static readonly DiagnosticDescriptor UA0020_EncodeableFactoryRename = Create( + DiagnosticIds.UA0020, + "EncodeableFactory member renamed", + "'{0}' was replaced in 2.0. Use '{1}' instead.", + DiagnosticSeverity.Warning, + "EncodeableFactory.GlobalFactory was removed (consumers now obtain the factory from ServiceMessageContext.Factory) and EncodeableFactory.Create was renamed to Fork."); + + public static readonly DiagnosticDescriptor UA0021_CertificateValidatorRename = Create( + DiagnosticIds.UA0021, + "CertificateValidator / CertificateValidationEventArgs renamed in 1.6", + "'{0}' was replaced in 1.6 by the new CertificateManager pipeline (ICertificateManager / ICertificateValidatorEx / CertificateValidationResult). The migration is structural (event-based -> async result + AcceptError callback) — see MigrationGuide.md#ua0021.", + DiagnosticSeverity.Info, + "The CertificateValidator class and CertificateValidationEventArgs were removed in 1.6. The new ICertificateManager (composed of ICertificateValidatorEx, ICertificateRegistry, ICertificateTrustListManager, ICertificateLifecycle) replaces them; per-error accept logic moves from the CertificateValidation event to CertificateValidationOptions.AcceptError. This rule is diagnostic-only because the migration changes the API shape (no mechanical rename)."); + + public static readonly DiagnosticDescriptor UA0022_CertificateValidatorPropertyRename = Create( + DiagnosticIds.UA0022, + "ApplicationConfiguration.CertificateValidator / ServerBase.CertificateValidator renamed in 2.0", + "'{0}.CertificateValidator' was removed in 2.0 — use '{0}.CertificateManager' (type ICertificateManager)", + DiagnosticSeverity.Warning, + "Configure via CertificateManagerFactory.Create(securityConfiguration, telemetry, ...). See MigrationGuide.md."); + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticIds.cs b/Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticIds.cs new file mode 100644 index 0000000000..f5e7d95f7c --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticIds.cs @@ -0,0 +1,73 @@ +/* ======================================================================== + * 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.Diagnostics +{ + /// + /// Stable identifiers for every diagnostic shipped by the + /// OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer analyzer package. + /// Keep IDs immutable across releases — consumers may use them + /// in #pragma warning disable or .editorconfig. + /// + internal static class DiagnosticIds + { + public const string UA0001 = "UA0001"; + public const string UA0002 = "UA0002"; + public const string UA0003 = "UA0003"; + public const string UA0004 = "UA0004"; + public const string UA0005 = "UA0005"; + public const string UA0006 = "UA0006"; + public const string UA0007 = "UA0007"; + public const string UA0008 = "UA0008"; + public const string UA0009 = "UA0009"; + public const string UA0010 = "UA0010"; + public const string UA0011 = "UA0011"; + public const string UA0012 = "UA0012"; + public const string UA0014 = "UA0014"; + public const string UA0015 = "UA0015"; + public const string UA0018 = "UA0018"; + public const string UA0019 = "UA0019"; + public const string UA0020 = "UA0020"; + public const string UA0021 = "UA0021"; + public const string UA0022 = "UA0022"; + + /// The diagnostic category every UA00xx rule belongs to. + public const string Category = "Migration"; + + /// + /// Base URL for per-rule help. Each rule appends its own ID. + /// Points at the MigrationGuide.md "Automated migration" section. + /// + public const string HelpLinkUriBase = + "https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md#"; + + /// Compose a per-rule help URL anchored at the rule ID. + public static string HelpLinkFor(string id) => HelpLinkUriBase + id.ToLowerInvariant(); + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/WellKnownProperties.cs b/Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/WellKnownProperties.cs new file mode 100644 index 0000000000..794cd5eec9 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/WellKnownProperties.cs @@ -0,0 +1,56 @@ +/* ======================================================================== + * 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.Diagnostics +{ + /// + /// Shared DiagnosticDescriptor.Properties keys passed from analyzers + /// to their companion code-fix providers. + /// + /// + /// Defined here (in the analyzer DLL) and linked into the companion CodeFixes + /// project so both assemblies can use the exact same string constants without + /// the CodeFixes assembly needing a ProjectReference back to the analyzer + /// (which would create a NuGet restore cycle). + /// + internal static class WellKnownProperties + { + /// UA0008: method name extracted from a Session.Call invocation. + public const string MethodName = "MethodName"; + + /// UA0020: form discriminator key for EncodeableFactory rename. + public const string Form = "Form"; + + /// UA0020 form: legacy EncodeableFactory.GlobalFactory getter. + public const string FormGlobalFactory = "A"; + + /// UA0020 form: legacy factory.Create() instance call. + public const string FormCreate = "B"; + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Helpers/SymbolExtensions.cs b/Tools/Opc.Ua.MigrationAnalyzer/Helpers/SymbolExtensions.cs new file mode 100644 index 0000000000..38656a0346 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Helpers/SymbolExtensions.cs @@ -0,0 +1,215 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; + +namespace Opc.Ua.MigrationAnalyzer.Helpers +{ + /// + /// Shared helpers for symbol-shape queries used by multiple analyzers. + /// Lifted out of individual analyzers to keep detection patterns consistent. + /// + internal static class SymbolExtensions + { + /// + /// True iff carries an [Obsolete] attribute. + /// + public static bool IsObsolete(this ISymbol symbol) + { + if (symbol is null) + { + return false; + } + foreach (AttributeData attr in symbol.GetAttributes()) + { + INamedTypeSymbol cls = attr.AttributeClass; + if (cls != null && cls.ToDisplayString() == "System.ObsoleteAttribute") + { + return true; + } + } + return false; + } + + /// + /// True iff carries + /// [Opc.Ua.OpcUaShimAttribute(ruleId)] for the given + /// . For extension methods reduced from a + /// static declaration, also inspects + /// since the attribute lives on the original declaration. + /// + public static bool IsOpcUaShim(this ISymbol symbol, string ruleId) + { + if (symbol is null) + { + return false; + } + if (HasShimAttribute(symbol, ruleId)) + { + return true; + } + if (symbol is IMethodSymbol method && method.ReducedFrom != null) + { + return HasShimAttribute(method.ReducedFrom, ruleId); + } + return false; + } + + private static bool HasShimAttribute(ISymbol symbol, string ruleId) + { + foreach (AttributeData attr in symbol.GetAttributes()) + { + INamedTypeSymbol cls = attr.AttributeClass; + if (cls is null || cls.ToDisplayString() != "Opc.Ua.OpcUaShimAttribute") + { + continue; + } + if (attr.ConstructorArguments.Length == 1 && + attr.ConstructorArguments[0].Value is string id && + id == ruleId) + { + return true; + } + } + return false; + } + + /// + /// True iff is declared (directly or via override) + /// on the named type referenced by . + /// + public static bool IsDeclaredOn(this ISymbol member, string declaringTypeFullName) + { + INamedTypeSymbol declaring = member?.ContainingType; + while (declaring != null) + { + if (declaring.ToDisplayString() == declaringTypeFullName) + { + return true; + } + declaring = declaring.BaseType; + } + return false; + } + + /// + /// True iff is assignable to the named target type + /// (walks and ). + /// + public static bool IsAssignableTo(this ITypeSymbol type, ITypeSymbol target) + { + if (type is null || target is null) + { + return false; + } + + SymbolEqualityComparer eq = SymbolEqualityComparer.Default; + ITypeSymbol current = type; + while (current != null) + { + if (eq.Equals(current, target)) + { + return true; + } + current = current.BaseType; + } + foreach (INamedTypeSymbol iface in type.AllInterfaces) + { + if (eq.Equals(iface, target)) + { + return true; + } + } + return false; + } + + /// + /// Closed list of removed {Type}Collection wrappers that 2.0 dropped + /// in favour of List<T> / ArrayOf<T>. The element + /// type name is the second tuple item. + /// + public static IReadOnlyList<(string CollectionName, string ElementName)> RemovedCollectionTypes { get; } = + new[] + { + ("Opc.Ua.BooleanCollection", "bool"), + ("Opc.Ua.SByteCollection", "sbyte"), + ("Opc.Ua.ByteCollection", "byte"), + ("Opc.Ua.Int16Collection", "short"), + ("Opc.Ua.UInt16Collection", "ushort"), + ("Opc.Ua.Int32Collection", "int"), + ("Opc.Ua.UInt32Collection", "uint"), + ("Opc.Ua.Int64Collection", "long"), + ("Opc.Ua.UInt64Collection", "ulong"), + ("Opc.Ua.FloatCollection", "float"), + ("Opc.Ua.DoubleCollection", "double"), + ("Opc.Ua.StringCollection", "string"), + ("Opc.Ua.DateTimeCollection", "Opc.Ua.DateTimeUtc"), + ("Opc.Ua.GuidCollection", "Opc.Ua.Uuid"), + ("Opc.Ua.ByteStringCollection", "Opc.Ua.ByteString"), + ("Opc.Ua.XmlElementCollection", "System.Xml.XmlElement"), + ("Opc.Ua.NodeIdCollection", "Opc.Ua.NodeId"), + ("Opc.Ua.ExpandedNodeIdCollection", "Opc.Ua.ExpandedNodeId"), + ("Opc.Ua.QualifiedNameCollection", "Opc.Ua.QualifiedName"), + ("Opc.Ua.LocalizedTextCollection", "Opc.Ua.LocalizedText"), + ("Opc.Ua.StatusCodeCollection", "Opc.Ua.StatusCode"), + ("Opc.Ua.VariantCollection", "Opc.Ua.Variant"), + ("Opc.Ua.DiagnosticInfoCollection", "Opc.Ua.DiagnosticInfo"), + ("Opc.Ua.DataValueCollection", "Opc.Ua.DataValue"), + ("Opc.Ua.ExtensionObjectCollection", "Opc.Ua.ExtensionObject"), + ("Opc.Ua.ArgumentCollection", "Opc.Ua.Argument"), + ("Opc.Ua.ServerSecurityPolicyCollection", "Opc.Ua.ServerSecurityPolicy"), + ("Opc.Ua.TransportConfigurationCollection", "Opc.Ua.TransportConfiguration"), + ("Opc.Ua.ReverseConnectClientCollection", "Opc.Ua.ReverseConnectClient"), + }; + + /// + /// Convenience: returns the element type metadata name if + /// is one of the removed collection wrappers. + /// + public static bool TryGetRemovedCollectionElement(this ITypeSymbol type, out string elementName) + { + elementName = null; + if (type is null) + { + return false; + } + string typeFullName = type.ToDisplayString(); + foreach ((string collection, string element) in RemovedCollectionTypes) + { + if (collection == typeFullName) + { + elementName = element; + return true; + } + } + return false; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Helpers/UaSymbols.cs b/Tools/Opc.Ua.MigrationAnalyzer/Helpers/UaSymbols.cs new file mode 100644 index 0000000000..eadef898f9 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Helpers/UaSymbols.cs @@ -0,0 +1,165 @@ +/* ======================================================================== + * 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 Microsoft.CodeAnalysis; + +namespace Opc.Ua.MigrationAnalyzer.Helpers +{ + /// + /// Cached lookup of well-known OPC UA s for a + /// single . Analyzers register a compilation-start + /// action that creates one instance per compilation so symbol resolution + /// happens at most once per build. + /// + internal sealed class UaSymbols + { + private UaSymbols(Compilation compilation) + { + Compilation = compilation; + + // Struct built-ins (NodeId, Variant, ...) that became readonly structs in 2.0. + BuiltInStructTypes = ImmutableArray.Create( + Get("Opc.Ua.NodeId"), + Get("Opc.Ua.ExpandedNodeId"), + Get("Opc.Ua.QualifiedName"), + Get("Opc.Ua.LocalizedText"), + Get("Opc.Ua.ExtensionObject"), + Get("Opc.Ua.DataValue"), + Get("Opc.Ua.Variant"), + Get("Opc.Ua.ByteString")); + + UtilsType = Get("Opc.Ua.Utils"); + VariantType = Get("Opc.Ua.Variant"); + VariantNullField = VariantType?.GetMembers("Null").Length > 0 ? "Null" : null; + DataValueType = Get("Opc.Ua.DataValue"); + NodeIdType = Get("Opc.Ua.NodeId"); + ExpandedNodeIdType = Get("Opc.Ua.ExpandedNodeId"); + ByteStringType = Get("Opc.Ua.ByteString"); + StatusCodeType = Get("Opc.Ua.StatusCode"); + DateTimeUtcType = Get("Opc.Ua.DateTimeUtc"); + UuidType = Get("Opc.Ua.Uuid"); + CertificateIdentifierType = Get("Opc.Ua.CertificateIdentifier"); + UserIdentityType = Get("Opc.Ua.UserIdentity"); + UserIdentityTokenHandlerType = Get("Opc.Ua.IUserIdentityTokenHandler"); + CertificateFactoryType = Get("Opc.Ua.CertificateFactory"); + SessionType = Get("Opc.Ua.Client.Session"); + SessionInterfaceType = Get("Opc.Ua.Client.ISession"); + EncodeableFactoryType = Get("Opc.Ua.EncodeableFactory"); + ServiceMessageContextType = Get("Opc.Ua.ServiceMessageContext"); + TelemetryContextType = Get("Opc.Ua.ITelemetryContext"); + LoggerType = Get("Microsoft.Extensions.Logging.ILogger"); + DataContractType = Get("System.Runtime.Serialization.DataContractAttribute"); + DataMemberType = Get("System.Runtime.Serialization.DataMemberAttribute"); + } + + public Compilation Compilation { get; } + + /// NodeId, Variant, DataValue, ... — all readonly structs in 2.0. + public ImmutableArray BuiltInStructTypes { get; } + + public INamedTypeSymbol UtilsType { get; } + public INamedTypeSymbol VariantType { get; } + + /// Name of the static "null" field on Variant (e.g. Variant.Null). + public string VariantNullField { get; } + + public INamedTypeSymbol DataValueType { get; } + public INamedTypeSymbol NodeIdType { get; } + public INamedTypeSymbol ExpandedNodeIdType { get; } + public INamedTypeSymbol ByteStringType { get; } + public INamedTypeSymbol StatusCodeType { get; } + public INamedTypeSymbol DateTimeUtcType { get; } + public INamedTypeSymbol UuidType { get; } + public INamedTypeSymbol CertificateIdentifierType { get; } + public INamedTypeSymbol UserIdentityType { get; } + public INamedTypeSymbol UserIdentityTokenHandlerType { get; } + public INamedTypeSymbol CertificateFactoryType { get; } + public INamedTypeSymbol SessionType { get; } + public INamedTypeSymbol SessionInterfaceType { get; } + public INamedTypeSymbol EncodeableFactoryType { get; } + public INamedTypeSymbol ServiceMessageContextType { get; } + public INamedTypeSymbol TelemetryContextType { get; } + public INamedTypeSymbol LoggerType { get; } + public INamedTypeSymbol DataContractType { get; } + public INamedTypeSymbol DataMemberType { get; } + + /// True iff the compilation references at least one OPC UA 2.0 surface. + public bool ReferencesOpcUa => + NodeIdType != null || VariantType != null || DataValueType != null; + + public bool IsBuiltInStructType(ITypeSymbol type) + { + if (type is null) + { + return false; + } + foreach (INamedTypeSymbol candidate in BuiltInStructTypes) + { + if (candidate != null && SymbolEqualityComparer.Default.Equals(candidate, type)) + { + return true; + } + } + return false; + } + + /// Resolve a well-known type by fully-qualified metadata name. + public INamedTypeSymbol Get(string fullyQualifiedName) + { + return Compilation.GetTypeByMetadataName(fullyQualifiedName); + } + + public static UaSymbols Create(Compilation compilation) + { + if (compilation is null) + { + throw new ArgumentNullException(nameof(compilation)); + } + return new UaSymbols(compilation); + } + + /// + /// Convenience: pull a cached instance keyed by + /// the Compilation. Analyzers should call this in their compilation-start + /// callback so symbols are resolved exactly once per build. + /// + public static UaSymbols For(Compilation compilation, Dictionary cache) + { + if (!cache.TryGetValue(compilation, out UaSymbols symbols)) + { + symbols = Create(compilation); + cache[compilation] = symbols; + } + return symbols; + } + } +} diff --git a/Tools/Opc.Ua.MigrationAnalyzer/NugetREADME.md b/Tools/Opc.Ua.MigrationAnalyzer/NugetREADME.md new file mode 100644 index 0000000000..459188da84 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/NugetREADME.md @@ -0,0 +1,153 @@ +# OPC UA migration analyzers, code fixers, and compatibility shim + +## What you get + +A single NuGet install (`OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer`) that +ships **two things** to help migrate from OPC UA .NET Standard 1.5.378 to 1.6: + +- a Roslyn **analyzer + code-fixer** set (`UA0001`–`UA0022`) that flags every + pattern covered by [`Docs/MigrationGuide.md`](../../Docs/MigrationGuide.md) + and, where safe, applies the fix automatically; and +- a **compatibility shim** assembly (`Opc.Ua.MigrationAnalyzer.Core.dll`) that + re-supplies the obsolete extension surface 1.6 moved or removed, so most + consumer projects still compile after the upgrade. + +## How to migrate + +1. Add the 1.6 OPC UA packages **and** the MigrationAnalyzer package to your + consumer project: + + ```xml + + ``` + +2. Run `dotnet build`. Your code should compile: the shim covers the + `[Obsolete]` extension surface that 1.6 moved or removed, so what remains + are warnings rather than errors. +3. Walk through the `UA00xx` analyzer warnings in the IDE and apply the + offered auto-fixes. A handful (`UA0001`, `UA0011`, `UA0015`, `UA0018`) are + `Info`-level and need a manual review. +4. Once the project is warning-free, remove the + `OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` package reference. You are + on clean 1.6 with no shim dependency. + +## Rules + +| ID | Default | Replaces | +| ------ | -------- | ----------------------------------------------------------------------------------------| +| UA0001 | Info | `Utils.Trace` / `Utils.LogX` | +| UA0002 | Warning | Removed `Collection` wrappers | +| UA0003 | Warning | `x == null` on now-struct built-in types | +| UA0004 | Warning | `?.` on now-struct built-in types | +| UA0005 | Warning | `byte[]` where `ByteString` is now expected | +| UA0006 | Warning | `new Variant(object\|DateTime\|Guid\|byte[])` | +| UA0007 | Warning | `new NodeId(string)` / `new ExpandedNodeId(string)` | +| UA0008 | Warning | `Session.Call(..., params object[])` argument wrapping | +| UA0009 | Warning | `[DataContract]`/`[DataMember]` on configuration extensions | +| UA0010 | Warning | `using`/`Dispose` on `CertificateIdentifier`, `UserIdentity`, `IUserIdentityTokenHandler` | +| UA0011 | Info | Sync `IUserIdentityTokenHandler.Encrypt/Decrypt/Sign/Verify` | +| UA0012 | Warning | `CertificateFactory.*` static helpers | +| UA0014 | Warning | `DataValue.IsGood(dv)` static helper | +| UA0015 | Info | Sync / APM members on GDS / LDS clients | +| UA0018 | Info | `CertificateIdentifier.Certificate` getter | +| UA0019 | Warning | `new DataValue(StatusCode[, ts])` | +| UA0020 | Warning | `EncodeableFactory.GlobalFactory` / `Create()` | +| UA0021 | Info | `CertificateValidator` / `CertificateValidationEventArgs` (structural rename in 1.6) | +| UA0022 | Warning | `ApplicationConfiguration.CertificateValidator` / `ServerBase.CertificateValidator` (renamed in 2.0 to `.CertificateManager`) | + +## What the shim provides + +`Opc.Ua.MigrationAnalyzer.Core.dll` is delivered as a regular reference assembly and +re-exposes the 1.5.378 surface in two flavors: + +- **Moved obsolete extensions** the 1.6 libraries no longer carry inline: + `NodeId` / `Variant` / `DataValue` null-check helpers, `Session` sync + helpers, `Subscription` sync helpers, `ApplicationInstance` helpers, + `ServerBase.Start` / `Stop`, `TransportChannel` APM (`BeginX` / `EndX`), + `ChannelBase` static factory methods, and similar surface. +- **New shims for genuinely-removed members**: + - `EncodeableFactory.GlobalFactory` + - `CertificateIdentifier.Certificate` (throws `NotSupportedException`) + - sync wrappers for + `IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}` + - sync + APM wrappers for the 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. +- `?.` member access on now-struct types — use the **UA0004** fixer. +- `using var x = new CertificateIdentifier(...)` — use the **UA0010** fixer + to drop the `using` / `Dispose` call. +- `[DataContract]` / `[DataMember]` on configuration extension classes — use + the **UA0009** fixer. +- Removed `Collection` wrappers such as `Int32Collection`, + `NodeIdCollection`, etc. — use the **UA0002** fixer to rewrite to + `List` or `ArrayOf`. + +## Sync-over-async caveat + +The sync shims (for example `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 + +``` + +Remove each entry as you finish fixing the corresponding rule, and drop the +whole block once the MigrationAnalyzer package is removed. + +## Packaging note + +The package ships **two analyzer DLLs** under `analyzers/dotnet/cs/`: + +- `Opc.Ua.MigrationAnalyzer.dll` — the analyzer assembly. Targets `Microsoft.CodeAnalysis 4.x` + (the stable analyzer API) and references **only** `Microsoft.CodeAnalysis.CSharp` so it + loads cleanly in csc.exe's analyzer host (which ships only `Microsoft.CodeAnalysis.dll` + + `CSharp.dll`, not `Workspaces`). All `DiagnosticAnalyzer` types live here. +- `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` — the code-fix assembly. References + `Microsoft.CodeAnalysis.CSharp.Workspaces` and hosts all `CodeFixProvider` types. + Loaded only by Workspaces-aware hosts (Visual Studio / `dotnet format`). + +This split is necessary because shipping a single DLL that references `Workspaces` +silently fails to load in csc.exe at command-line build time — csc loads the assembly +but JIT-resolution of `Workspaces` types fails (DLL not in bincore), and the analyzer +host swallows the load failure, producing zero diagnostics. Splitting keeps the +analyzer host happy while preserving full IDE/`dotnet format` code-fix functionality. + +`RS1038` (suggesting separation) is the Roslyn rule that recommends this layout; +it is satisfied implicitly by the two-DLL design. + +## Suppression recipes + +To suppress an individual rule for a single line: + +```csharp +#pragma warning disable UA0008 // Wrap Session.Call arguments with Variant.From +session.Call(objectId, methodId, "legacy"); +#pragma warning restore UA0008 +``` + +To set a project-wide severity, add to your `.editorconfig`: + +```ini +[*.cs] +dotnet_diagnostic.UA0001.severity = none # silence UA0001 entirely +dotnet_diagnostic.UA0008.severity = error # treat UA0008 as an error +``` diff --git a/Tools/Opc.Ua.MigrationAnalyzer/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props b/Tools/Opc.Ua.MigrationAnalyzer/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props new file mode 100644 index 0000000000..82e6a110cd --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props @@ -0,0 +1,17 @@ + + + + + + + diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.csproj b/Tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.csproj new file mode 100644 index 0000000000..ae78c085fd --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.csproj @@ -0,0 +1,89 @@ + + + netstandard2.0 + false + true + $(AssemblyPrefix).MigrationAnalyzer + Opc.Ua.MigrationAnalyzer + OPC UA .NET Standard migration analyzers and code fixers (1.5.378 to 2.0). + + $(NoWarn);RS1007;RS1038 + + + + $(PackagePrefix).Opc.Ua.MigrationAnalyzer + true + true + true + true + true + + false + $(MSBuildThisFileDirectory)Opc.Ua.MigrationAnalyzer.nuspec + $(MSBuildThisFileDirectory) + + + + + + + + + + + + + + + + + + + + + + + false + all + + + + + + + + + + + version=$(PackageVersion);configuration=$(Configuration);repoRoot=$(MSBuildThisFileDirectory)..\..;analyzerDll=$(MSBuildThisFileDirectory)bin\$(Configuration)\netstandard2.0\Opc.Ua.MigrationAnalyzer.dll;codeFixesDll=$(MSBuildThisFileDirectory)..\Opc.Ua.MigrationAnalyzer.CodeFixer\bin\$(Configuration)\netstandard2.0\Opc.Ua.MigrationAnalyzer.CodeFixer.dll;shimBin=$(MSBuildThisFileDirectory)..\Opc.Ua.MigrationAnalyzer.Core\bin\$(Configuration);readme=$(MSBuildThisFileDirectory)NugetREADME.md;propsFile=$(MSBuildThisFileDirectory)OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props + + + diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.nuspec b/Tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.nuspec new file mode 100644 index 0000000000..09379d0ff0 --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/Opc.Ua.MigrationAnalyzer.nuspec @@ -0,0 +1,84 @@ + + + + OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer + $version$ + OPC UA .NET Standard 1.5.378 to 1.6 migration analyzers, code fixers, and compatibility shim + OPC Foundation + OPC Foundation + true + LICENSE.txt + https://github.com/OPCFoundation/UA-.NETStandard + images/logo.jpg + NugetREADME.md + OPC UA .NET Standard migration analyzers and code fixers (UA0001-UA0020) bundled with the Opc.Ua.MigrationAnalyzer.Core compatibility assembly that re-exposes the 1.5.378 obsolete surface to ease the move to 1.6. + Copyright (c) 2004-2025 OPC Foundation, Inc + OPCFoundation OPC UA analyzer codefix roslyn migration shim + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tools/Opc.Ua.MigrationAnalyzer/Properties/AssemblyInfo.cs b/Tools/Opc.Ua.MigrationAnalyzer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2b9848014c --- /dev/null +++ b/Tools/Opc.Ua.MigrationAnalyzer/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/Tools/Opc.Ua.SourceGeneration.Core/Generators/ClientApiTemplates.cs b/Tools/Opc.Ua.SourceGeneration.Core/Generators/ClientApiTemplates.cs index d008db8344..cf32bfbbe8 100644 --- a/Tools/Opc.Ua.SourceGeneration.Core/Generators/ClientApiTemplates.cs +++ b/Tools/Opc.Ua.SourceGeneration.Core/Generators/ClientApiTemplates.cs @@ -168,7 +168,11 @@ public interface I{{Tokens.ServiceSet}}ClientMethods{{Tokens.BaseInterfaces}} try { - global::Opc.Ua.IServiceResponse? genericResponse = TransportChannel.SendRequest(request); + global::Opc.Ua.IServiceResponse? genericResponse = TransportChannel + .SendRequestAsync(request, global::System.Threading.CancellationToken.None) + .AsTask() + .GetAwaiter() + .GetResult(); if (genericResponse == null) { @@ -200,7 +204,28 @@ public interface I{{Tokens.ServiceSet}}ClientMethods{{Tokens.BaseInterfaces}} UpdateRequestHeader(request, requestHeader == null, "{{Tokens.Name}}"); - return TransportChannel.BeginSendRequest(request, callback, asyncState); + global::System.Threading.Tasks.Task task = TransportChannel + .SendRequestAsync(request, global::System.Threading.CancellationToken.None) + .AsTask(); + global::System.Threading.Tasks.TaskCompletionSource tcs = + new global::System.Threading.Tasks.TaskCompletionSource(asyncState); + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.TrySetException(t.Exception!.InnerExceptions); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(t.Result); + } + callback?.Invoke(tcs.Task); + }, global::System.Threading.Tasks.TaskScheduler.Default); + return tcs.Task; } /// @@ -213,7 +238,10 @@ public interface I{{Tokens.ServiceSet}}ClientMethods{{Tokens.BaseInterfaces}} try { - global::Opc.Ua.IServiceResponse? genericResponse = TransportChannel.EndSendRequest(result); + global::Opc.Ua.IServiceResponse? genericResponse = + ((global::System.Threading.Tasks.Task)result) + .GetAwaiter() + .GetResult(); if (genericResponse == null) { diff --git a/Tools/SourceGeneration.slnx b/Tools/Roslyn.slnx similarity index 70% rename from Tools/SourceGeneration.slnx rename to Tools/Roslyn.slnx index 659a85f8dd..129c946761 100644 --- a/Tools/SourceGeneration.slnx +++ b/Tools/Roslyn.slnx @@ -5,6 +5,13 @@ + + + + + + + diff --git a/UA.slnx b/UA.slnx index 8f5e96f730..ce8d7d67dc 100644 --- a/UA.slnx +++ b/UA.slnx @@ -121,6 +121,12 @@ + + + + + + @@ -151,6 +157,7 @@ +