5 Commits

Author SHA1 Message Date
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00
Copilot c03c128384 Treat code analysis (CA) warnings as errors and fix all surfaced analyzer warnings (#4680)
# Description

`TreatWarningsAsErrors` was already enabled, but
`CodeAnalysisTreatWarningsAsErrors` was `false`, so Microsoft Code
Analysis (`CAxxxx`) warnings were non-fatal. This PR sets it to `true`
in `common.props` and fixes all 55 analyzer warnings the net10.0 build
reported.

**Production code**
- `SamplingGroup.cs`: CA2213 is a false positive here because the
detached `CancellationTokenSource` is disposed by `CancelSamplingLoop`
once the loop completes. It is suppressed with a pragma, a justification
and a TODO, following the existing pattern in
`ServerBase.RequestQueue.cs`.

**Tests**
- CA1861: constant array arguments are moved to `static readonly` fields
or replaced with `ArrayOf<T>` collection expressions. In the legacy
interop server I used locals instead, because those arrays become node
values that writes can change in place.
- CA2025: tasks that finish before disposal (`Assert.ThrowsAsync`, or
awaited before `using` scope exit) are suppressed with justification
comments, following existing test patterns.
- CA1812: classes created by the DI container are suppressed with a
comment and a TODO.
- CA2208: benchmark switch defaults now throw
`InvalidOperationException`.
- CA1845: `Substring(0, 8)` is replaced with `[..8]`.
- CA1307: `string.Replace` is replaced with `Split`/`Join`, which also
works on net48.
- CA5394: `Random` is replaced with `RandomNumberGenerator`.

**Docs**
- `docs/DeveloperGuide.md` now says CA warnings also fail the build, and
that any suppression needs a comment explaining why plus a TODO.

**Validation**
- I fixed the 55 warnings from a net10.0 build of `UA.slnx`
(`CustomTestTarget=net10.0`). That build ran before the setting was
turned on, and I have not rebuilt net10.0 since the fixes.
- `CustomTestTarget=net48` build of `UA.slnx`: 0 warnings, 0 errors.
- I also started a full build across all target frameworks with the
setting on. Its log showed no errors or warnings while it ran, but I
couldn't read its final result.
- No tests have been run yet; CI should confirm the multi-TFM build and
the tests.

## Related Issues

- N/A

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-10-06 18:54:28 +02:00
romanett b03575b4e2 Run the interop tests against six foreign OPC UA stacks and extend feature coverage (#4644)
## Proposed changes

Runs the interop fixtures added in #4630 against peers built on six
other open source OPC UA stacks, so the 2.0 client and server are tested
against independent implementations, not only the released 1.5.378
stack. It also extends the fixtures beyond read/write/browse to events,
subscriptions, identity and more services.

### Peers (`tests/Opc.Ua.Interop.Peers`)

| Peer | Stack | Toolchain | Roles |
| --- | --- | --- | --- |
| `node-opcua` | node-opcua 2.186.15 (MIT) | Node.js 24 | server, client
|
| `milo` | Eclipse Milo 1.1.7 (EPL-2.0) | Java 17+, Maven | server,
client |
| `asyncua` | opcua-asyncio 2.0.1 (LGPL-3.0) | Python 3.12 | server,
client |
| `open62541` | open62541 1.5.8 (MPL-2.0), OpenSSL backend | C, CMake |
server, client, ECC |
| `async-opcua` | async-opcua 0.19.0 (MPL-2.0) | Rust | server, client |
| `gopcua` | gopcua 0.9.1 (MIT) | Go | client only |

Every peer speaks the command line and stdout contract of
`Opc.Ua.Interop.LegacyPeer` (`server` / `client --checks`, `PEER-INFO
{json}`, `RESULT {json}`). The stacks are fetched at build time and run
as separate processes; nothing is linked into or shipped with any
package.

### Harness

- **Peer selection:** `OPCUA_INTEROP_PEER_HOST` +
`OPCUA_INTEROP_PEER_HOST_ARGUMENTS` choose the host (`node`, `java
-jar`, `python`) for the peer named by `OPCUA_INTEROP_LEGACY_PEER`.
Native peers (open62541, async-opcua, gopcua) run without a host. Unset,
the 1.5.378 peer runs exactly as before, so the main CI is unchanged.
- **`PEER-INFO {json}`:** stack, version, application URI, software
version and, optionally, the security policies the peer implements.
`LegacyServerInteropTests` compares the ApplicationUri and
SoftwareVersion against it. `EccInteropTests` runs policies beyond the
four 1.05 ECC policies only against a peer that declares them.
- **`expected-differences.json` per peer:** confirmed defects of a
stack, keyed by check name, `Fixture.Test` or a `*` prefix, each with
the source file or spec section that shows it. A listed failure is
reported as inconclusive with its reason; a listed test that passes
produces a warning. The 2.0 client tests apply it through the
fixture-level `[PeerDifferences]` action.

### New coverage (all peers, including the 1.5.378 peer)

- **16 client checks**, run by each peer's client against the 2.0 server
(`LegacyClientFeatureCheckAsync`, Basic256Sha256 SignAndEncrypt):
EventSubscription, ConditionRefresh, AlarmAcknowledge, DeadbandFilter,
QueueOverflow, Triggering, Republish, TransferSubscription,
WrongPasswordRejected, X509UserToken, RegisterNodes, HistoryReadRaw,
NodeManagement, IndexRange, FindServers, SessionReconnect.
- **`ServerFeatureInteropTests`**: the 2.0 client against each peer
server. It covers RaiseEvent, ConditionRefresh, absolute deadband, queue
overflow bit, triggering, republish errors, transfer to a new session,
X509 user token, RegisterNodes, IndexRange, AddNodes/DeleteNodes,
FindServers and session reconnect. Every peer server gained a
`RaiseEvent` method and accepts X509 user tokens.
- **ECC:** with open62541's OpenSSL backend, `EccInteropTests` adds
`ECC_nistP256_AesGcm` and `ECC_nistP256_ChaChaPoly` (both directions;
SignAndEncrypt anonymous and username, Sign, and token renewal across
two renewals). It also adds `ECC_curve25519`/`ECC_curve448` cases, which
are ignored while 2.0 marks those policies unsupported.

### CI and docs

- **Workflow `interop-foreign.yml`:** opt-in (nightly,
`workflow_dispatch`, PRs labelled `interop`), not a required check. It
builds all six peers on Windows, Linux and macOS (open62541 from source
on Linux only, against the distro OpenSSL), with dependency caching.
- **`tests/Opc.Ua.Interop.Peers/README.md`:** how to install the
toolchains, build each peer (including Windows without Visual Studio),
run the fixtures against it, maintain expected differences and add a new
stack. `DeveloperGuide.md` links to it.

## Results

Run locally, sequentially, net10.0, Windows. Not passed = expected
differences, inconclusive by design (e.g. AddNodes on a server without
NodeManagement), or ECC cases a peer or the platform does not support.

| Peer | Fixtures | Total | Passed | Failed |
| --- | --- | --- | --- | --- |
| 1.5.378 | whole interop project | 220 | 218 | 0 |
| node-opcua | server + client + feature | 145 | 143 | 0 |
| Eclipse Milo | server + client + feature | 145 | 141 | 0 |
| asyncua | server + client + feature | 145 | 133 | 0 |
| open62541 (OpenSSL) | server + client + feature + ECC | 199 | 175 | 0
|
| async-opcua | server + client + feature | 145 | 137 | 0 |
| gopcua | client | 104 | 81 | 0 |

After the AEAD cases were added, `EccInteropTests` against the 1.5.378
peer ran separately: 54 total, 26 passed, 0 failed. The new cases are
ignored there because the peer does not declare them.

## Findings

No 2.0 defects beyond the two now fixed in #4654:
- Browse with a null ViewId and a Timestamp now returns
`Bad_ViewTimestampInvalid`.
- Subscription transfer no longer fails when the optional
`Server.GetMonitoredItems` is unavailable.

After #4654 merges, `TransferSubscriptionToNewSessionAsync` can drop its
expected differences for asyncua.

Upstream defects found, all listed in the peers'
`expected-differences.json` with evidence; issues are still to be filed:

- **All five non-.NET stacks:** cannot handle the TestData
`ScalarStructureDataType` (fields of the abstract
Enumeration/Structure/BaseDataType). Milo encodes the abstract
`Enumeration` field as a Variant (Part 6 5.2.4 says Int32).
- **open62541:**
- **Sign-only ECC keys:** derives encryption key and IV into the HKDF
length for Sign-only ECC (Part 6 6.8.1), so Sign mode fails for the four
1.05 curves. The AEAD policies are not affected.
  - **HMAC mismatch:** reported as `Bad_InternalError`.
  - **ConditionRefresh:** needs the experimental A&C option.
- **Response too large:** closes the connection instead of answering
`Bad_ResponseTooLarge`.
  - **Array limit:** no MaxArrayLength.
- **gopcua:**
- **RsaPss:** `RSAOAEPMinPaddingSHA256` is 130 instead of 66, which
breaks `Aes256_Sha256_RsaPss`.
  - **Nil LocalizedText:** encoding a nil `*LocalizedText` panics.
- **asyncua server:**
  - **Queue overflow:** no Overflow bit.
  - **SetTriggering:** not implemented.
- **Republish:** answers Good for an unknown sequence number or
subscription.
  - **GetMonitoredItems:** not implemented.
  - **IndexRange:** ignored on Read and Write.
- **Reconnect:** closes a session without subscriptions on transport
loss.
- **Limits:** no BrowseNext; operation limits not enforced; no
`Bad_ResponseTooLarge`.
- **async-opcua:**
- **X509 user token (client):** treats an empty token SecurityPolicyUri
as None.
- **Reconnect (client):** overwrites the session nonce with the channel
nonce on reconnect.
  - **ConditionRefresh (server):** not implemented.
  - **Overflow bit (server):** set without the DataValue InfoType bit.
  - **ECC:** no ECC policies.
- **Milo:**
- **Wrong password:** `BadIdentityTokenInvalid` instead of
`BadUserAccessDenied` / `BadIdentityTokenRejected`.
  - **Array limit:** accepts arrays above its advertised MaxArrayLength.
  - **AddNodes:** answers `BadNotSupported`.

## Related Issues

- Follows #4630. Related fixes: #4654.

## Types of changes

- [x] Test / CI only

## Checklist

- [x] Interop fixtures pass locally against all seven peers (net10.0,
Windows).
- [x] `dotnet format whitespace --verify-no-changes` on the changed C#
files.
- [ ] `interop-foreign.yml` has not run on GitHub yet; it needs a manual
dispatch or the `interop` label.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:03:05 +02:00
romanett 3bb0cd8d55 Fix ComplexTypes interop and TCP channel dispose CI flakes (#4663)
# Description

Fixes two intermittent CI failures on master.

## Interop: `LegacyClientExtendedCheckAsync("ComplexTypes")` /
`LoadAndDecodeLegacyComplexTypesAsync`

Flaky since #4630. The node reported as "not decoded" is always
`Data_*_UserScalar_VariantValue` (ns=5;i=2340 on the 2.0 reference
server, ns=3;i=3104 on the 1.5 one). Its DataType is the
BaseDataType-derived `VariantDataType`, and `TestDataSystem` fills it
with `DataGenerator.GetRandomVariant()`. About 2.4 % of the time that
draw is an `ExtensionObject` with a random TypeId (e.g. `s=香蕉`) that no
client can decode. Both checks counted every ExtensionObject of a
variable with a non-ns0 DataType as a structure.

**Fix:** both checks now count only variables whose DataType derives
from `Structure`. They browse the `HasSubtype` closure of `Structure` to
decide this.
- In the 2.0 test, the browse-with-continuation loop moved into a shared
`BrowseAsync` helper.
- In the 1.5 peer, the existing Structure-subtype walk in
`DiagnoseEncodingNodesAsync` moved into `BrowseStructureSubtypesAsync`,
which both places now use.

Seen in master CI for fbdd0af, 5d26681 and c801ac1. Locally, pristine
master failed 2 of 16 runs; with this change, 20 of 20 repeated runs of
both tests passed.

## Core: `SecureClientAndTcpListenerExchangeSignedEncryptedRequestAsync`
— "Collection was modified"

`TcpTransportListener.Dispose` took a snapshot of `m_channels.Values`
and then called `Clear()`. In between, `ChannelClosed` on the receive
thread could still `TryRemove` the same channel. Both threads then
believed they owned the `TcpServerChannel`, and
`UaSCUaBinaryChannel.Dispose` had no once-guard, so both disposed the
certificate chain.

**Fix:**
- The listener claims each channel with `TryRemove`. Whichever side
removes a channel is the one that disposes it.
- `UaSCUaBinaryChannel.Dispose()` runs once, guarded by
`Interlocked.Exchange`. The guard sits in the public `Dispose()` rather
than `Dispose(bool)` because `TcpServerChannel` and
`UaSCUaBinaryClientChannel` release certificates before they chain to
the base class. CA1063 is suppressed there with a justification,
following the `ReverseConnectManager` precedent.

This race could not be reproduced locally (0 failures in 4,800
iterations), so the fix rests on code inspection. All 789 Core
`Stack.Transport` / `Stack.Tcp` tests pass on net10.0.

## Related Issues

- No issue filed; CI flakes on master.

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:02:24 +02:00
romanett fbdd0af13b Add 1.5.378 <-> 2.0 interop tests; return BadAttributeIdInvalid for unset optional security attributes (#4630)
# Description

Adds an interop test suite between this stack (2.0) and the released
**1.5.378** NuGet stack, in both directions, and fixes a 2.0 bug the
suite found.

## Interop suite

- **`tests/Opc.Ua.Interop.LegacyPeer`** is a console peer built from the
`OPCFoundation.NetStandard.Opc.Ua.*` 1.5.378 NuGet packages.
- It runs as a child process, because the two stacks share assembly
names.
- It deliberately does not import the repository build settings (its own
`Directory.Build.props/targets/Packages.props`).
- Server mode hosts a small interop server with tight limits, or the 1.5
Quickstarts reference server.
- Client mode runs named checks and prints one JSON result line per
check.
- **`tests/Opc.Ua.Interop.Tests`** builds the peer into `legacy-peer/`
and runs in every CI test leg as a normal `*.Tests.csproj`. It covers:
- sessions over None, the RSA policies and the ECC policies (Sign and
SignAndEncrypt), with anonymous and user name logon, plus
read/write/browse/call/subscribe;
- the existing 2.0 `ClientTest` workers run against the 1.5 reference
server, through the new `ClientTestFramework.ExternalServerUrl`;
- custom data types (structures, unions, optional fields, enums) loaded,
decoded and written back by each side's complex type system;
- certificate trust with auto-accept off: untrusted peers, CA-issued
certificates, revoked certificates;
- multi-chunk messages, encoding limits, response size limits, operation
limits, continuation points and security token renewal (RSA and ECC).
- Every peer check is reported as its own test result.
- `-p:LegacyStackVersion=<version>` tests another 1.5 release.
- Docs: `docs/DeveloperGuide.md`.

## 2.0 fix

**Unset optional security attributes now return
`Bad_AttributeIdInvalid`** (`NodeState.ReadNonValueAttribute`).
- Since 3e5f129c6, unset
RolePermissions/UserRolePermissions/AccessRestrictions returned Good
with a null value.
- This made the 1.5 ComplexTypes loader crash against any 2.0 server.
- 1.5 and Part 4 (Read) return `Bad_AttributeIdInvalid` here.

The suite also found two ECC secure-channel bugs: the Sign-only HKDF
salt length, and renewal chaining the previous secret for policies
without SecureChannelEnhancements. #4626 fixed both on master in the
meantime, so this branch now uses master's version. The ECC Sign and ECC
token-renewal interop tests here cover them.
## Reviewer notes

- **Behaviour change:** `NodeStateOptionalStorageTests` previously
asserted Good with a null value for unset optional attributes. It now
asserts `BadAttributeIdInvalid`.
- Monitored items on such unset attributes now fail with
`BadAttributeIdInvalid`, as in 1.5.
- **Known gaps, not changed here:**
- An unset Description still reads Good with a null value (same root
cause).
- A Null write to AccessRestrictions is accepted, as `NodeStateTests`
asserts.
- **Test runtime:** the suite adds roughly 7 minutes per CI test leg. On
net48 the ECC cases skip, because 2.0 has no ECC on .NET Framework.

## Related Issues

- None filed.

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- Ran: interop suite on net10 (188 passed) and on net48 (passed before
the final review fixes), plus Types, Core, InformationModel,
Core.Security and Sessions on net10.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:35:44 +02:00