Files
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
..

Coverage NodeSet test harness (issue #4251)

This directory contains a purpose-authored "kitchen-sink" NodeSet2 (Assets/Opc.Ua.CoverageTest.NodeSet2.xml) plus a small dependent secondary model (Assets/Opc.Ua.CoverageTestSecondary.NodeSet2.xml) that deliberately flips a broad set of the knobs defined by UANodeSet.xsd, and a data-driven assertion battery that runs the same checks against the model loaded through three independent pipelines:

  1. Source-generation (hand-written manager) — the XML is consumed at compile time via <AdditionalFiles ModelSourceGeneratorPrefix="Opc.Ua.CoverageTest"> in Opc.Ua.Server.Tests.csproj. The generated AddOpcUaCoverageTest(context) extension composes the predefined nodes inside the hand-written CoverageTestNodeManager : CustomNodeManager2 (CoverageTestSourceGenServer). Every authored node materialises as its generated NodeState subclass.
  2. Runtime import — the identical XML is embedded as a resource and imported at startup through RuntimeNodeSetNodeManagerFactory / AddRuntimeNodeSet (CoverageTestRuntimeServer).
  3. Source-generation (generated fluent manager) — a [NodeManager]-attributed partial (CoverageTestFluentNodeManager) makes the generator emit the entire FluentNodeManagerBase manager, factory and typed fluent-builder surface (CoverageTestFluentGenServer).

Each server is started for real (ServerFixture<T> + ReferenceServer), and the assertion battery is executed once per server, then a cross-pipeline equivalence fixture asserts the three address spaces agree.

Files

File Purpose
Assets/Opc.Ua.CoverageTest.NodeSet2.xml The primary kitchen-sink model.
Assets/Opc.Ua.CoverageTest.NodeSet2.csv Matching SymbolicName/Id/NodeClass table for byte-stable ids.
Assets/Opc.Ua.CoverageTestSecondary.NodeSet2.xml A dependent second-namespace model (cross-namespace references).
CoverageTestCatalogue.cs The expected node + reference catalogue (drives the data-driven tests).
CoverageTestSourceGenServer.cs Source-generation host (CoverageTestNodeManager), owns both namespaces.
CoverageTestRuntimeServer.cs Runtime-import host (two dependency-sorted sources).
CoverageTestFluentNodeManager.cs / CoverageTestSecondaryFluentNodeManager.cs [NodeManager] partials for the generated fluent managers.
CoverageTestFluentGenServer.cs Generated-fluent-manager host.
CoverageNodeSetAssertionsBase.cs The shared, data-driven assertion battery (generic over the server type).
CoverageTestAssertionsTests.cs Concrete fixtures binding the battery to each pipeline.
CoverageTestEquivalenceTests.cs Cross-pipeline structural equivalence fixture (all three servers).

What is exercised

  • Every node class (UAReferenceType, UADataType, UAObjectType, UAVariableType, UAObject, UAVariable, UAMethod, UAView).
  • Every Definition shape (Enumeration, OptionSet, Structure, Structure with an optional field, Union, an abstract structure, non-default Purpose).
  • Scalar, array and matrix variable value shapes.
  • A deep, branching instance tree (depth 4, two branches) navigated child-by-child.
  • NamespaceMetadata publication — the NamespaceMetadata Object exists for both namespaces (all pipelines) and, for the two source-generated pipelines, surfaces the model's NamespaceVersion (1.2.3) and NamespacePublicationDate (2024-06-01). The runtime-import pipeline carries no ModelDependency assembly attribute, so it only asserts the Object and its NamespaceUri.
  • Deterministic method call — invoking the authored AddNumbers method with no bound handler returns BadNotImplemented and yields the identical status on every call.
  • A second (dependent) namespace with cross-namespace HasTypeDefinition, HasSubtype and HasComponent references, hosted in all three pipelines.
  • Custom reference types: hierarchical/asymmetric (with InverseName), symmetric, an abstract type with a concrete subtype, plus inverse authoring.
  • Non-default common/instance/variable/method/view attributes, all five HasModellingRule targets, and a broad set of reference relations (HasComponent, HasProperty, HasOrderedComponent, HasSubtype, HasTypeDefinition, HasEncoding, HasEventSource, HasCondition, HasInterface, GeneratesEvent/AlwaysGeneratesEvent, FromState/ToState/ HasCause/HasEffect, HasTrueSubState/HasFalseSubState, and the custom reference types).

Known pipeline differences (intentionally not asserted for equivalence)

The reference catalogue deliberately excludes edges where the two pipelines legitimately differ, so the equivalence fixture stays strict on everything else:

  • Type dictionaries / DataTypeDescription nodes are not authored. The source generator regenerates the binary/XML schema from the DataTypeDefinitions rather than emitting DataTypeDictionary nodes into the address space, so hand-authored dictionary nodes would diverge. As a consequence the HasDescription and HasDictionaryEntry relations are not covered here.
  • Duplicate hierarchical references to the same child (for example a HasComponent alongside a custom hierarchical reference, or HasNotifier alongside HasEventSource) and a View's Organizes edges to already-parented nodes are represented differently by the two importers, so those specific edges are not asserted for equivalence.
  • Runtime structure stand-ins — the runtime complex-type loader registers the enumeration and option-set stand-ins for this multi-type model but not the structure stand-ins; the structure definitions are validated directly via the DataTypeDefinition round-trip instead.

Authoring this model surfaced two latent source-generator issues and one product bug, all fixed:

  • A top-level <ServerUris> table crashed NodeSetToModelDesign (StringTable.Append(null) for the reserved local-server slot). The guard is in tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs and is regression-tested by NodesetServerUrisTests in Opc.Ua.SourceGeneration.Tests.
  • A model with more than one structure (hence more than one Default Binary encoding) made the generated typed fluent builder emit colliding accessors, because DataTypeEncoding objects hang off their DataType and surfaced as parent-less "root" instances sharing the reserved DefaultBinary/DefaultXml/ DefaultJson symbolic names. FluentBuilderGenerator.GetTopLevelInstances now skips DataTypeEncoding objects (they are encoding metadata, never fluent-wired).
  • NamespaceMetadataPublisher never stamped a model's publication date because the "unset" guard compared against DateTime.MinValue (0001-01-01) while an unset NamespacePublicationDate defaults to the OPC UA epoch (1601-01-01); it now uses DateTimeUtc.IsNull. It also only discovered the ModelDependency attribute through a node manager's sync facade, which is a framework wrapper for async-native (fluent) managers; it now scans both the async manager's and its sync facade's assemblies so the fluent pipeline publishes version metadata too.