Preserve nullable struct null-check semantics in UA0003 (#4039)

## Failure

UA0003 unwrapped `Nullable<T>` before classifying OPC UA built-in
structs. That
reported valid nullable `== null` and `!= null` checks and offered fixes
whose
`.IsNull` semantics differ from nullable-value absence.

## Fix

- Leave `System.Nullable<T>` comparisons unchanged.
- Continue reporting direct null comparisons on OPC UA structs.
- Add coverage for nullable `NodeId` and `LocalizedText` operand
orderings.

## Reference

- OPC UA .NET 2.0 migration semantics.
- Split from #4021.

## Tests

- `dotnet test
tests\Opc.Ua.MigrationAnalyzer.Tests\Opc.Ua.MigrationAnalyzer.Tests.csproj
-c Release -f net10.0 --nologo` (121 passed)
- `dotnet test
tests\Opc.Ua.MigrationAnalyzer.Tests\Opc.Ua.MigrationAnalyzer.Tests.csproj
-c Release -f net48 --nologo` (121 passed)

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Marc Schier
2026-07-22 04:07:45 +02:00
committed by GitHub
parent e4f4629f48
commit 7b5fd1f534
2 changed files with 39 additions and 4 deletions
@@ -123,6 +123,42 @@ namespace Opc.Ua.MigrationAnalyzer.Tests.Analyzers
Assert.That(diags.Any(d => d.Id == "UA0003"), Is.False);
}
[Test]
public async Task DoesNotReportOnNullableNodeIdEqualsNullAsync()
{
const string source = """
using Opc.Ua;
class C
{
static bool M(NodeId? n) => n == null;
}
""";
ImmutableArray<Diagnostic> diags = await AnalyzerHarness
.GetAnalyzerDiagnosticsAsync(new UA0003NullCheckOnStructTypeAnalyzer(), source)
.ConfigureAwait(false);
Assert.That(diags.Any(d => d.Id == "UA0003"), Is.False);
}
[Test]
public async Task DoesNotReportOnNullableLocalizedTextNotEqualsNullAsync()
{
const string source = """
using Opc.Ua;
class C
{
static bool M(LocalizedText? value) => null != value;
}
""";
ImmutableArray<Diagnostic> diags = await AnalyzerHarness
.GetAnalyzerDiagnosticsAsync(new UA0003NullCheckOnStructTypeAnalyzer(), source)
.ConfigureAwait(false);
Assert.That(diags.Any(d => d.Id == "UA0003"), Is.False);
}
[Test]
public async Task DoesNotReportOnEqualsMethodCallAsync()
{
@@ -85,11 +85,10 @@ namespace Opc.Ua.MigrationAnalyzer.Analyzers
{
return;
}
if (valueType.OriginalDefinition?.SpecialType == SpecialType.System_Nullable_T &&
valueType is INamedTypeSymbol named &&
named.TypeArguments.Length == 1)
if (valueType is INamedTypeSymbol namedType &&
namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
valueType = named.TypeArguments[0];
return;
}
if (!symbols.IsBuiltInStructType(valueType))