From cc4791e2fd35df2ceb82a14c7687ff4037e2da4e Mon Sep 17 00:00:00 2001 From: romanett Date: Fri, 14 Aug 2026 16:57:06 +0200 Subject: [PATCH] Import NodeSet2 Views as standalone top-level nodes in the source generator (#4259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The NodeState source generator mishandled `UAView` nodes imported from a NodeSet2 model. When an exporter set a View's `ParentNodeId` to an organizing folder, the importer/generator absorbed the View into that folder's `Children` and tried to emit an invalid `AddChild(...)` call. Because `ViewState` derives from `NodeState` (not `BaseInstanceState`), a View can never be an `AddChild` component, so the generated code was invalid. ## Fix Make Views standalone top-level predefined nodes linked purely via `Organizes` references — mirroring how `DataType`/`ReferenceType` type nodes and `DataTypeEncoding` are handled: - **`tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs`** — clear the `UAView` `ParentNodeId` so the View stays a standalone top-level node, keep the organizing folder's forward `Organizes` references to those Views, and carry the View's `ContainsNoLoops` attribute through to the `ViewDesign`. - **`tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs`** — return `null` for `ViewDesign` in the `AddChild` and `ReplaceChild` template paths so Views are never emitted as components. - **`tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs`** — new `ViewImport` NodeSet constant and `ImportPreservesFolderOrganizesReferencesToViews` test verifying Views are imported as top-level nodes while the organizing folder retains its forward `Organizes` references to them. ## Validation - `dotnet build tools\Opc.Ua.SourceGeneration.Core` (Release): 0 warnings / 0 errors. - `dotnet test tests\Opc.Ua.SourceGeneration.Core.Tests` (net8.0, filtered to `NodeSetToModelDesign`): all 17 tests pass, including the new `ImportPreservesFolderOrganizesReferencesToViews`. Extracted from the larger PR #4220 work — this PR contains only the source-generator View fix (no WriteMask, UserAccessLevel, NodeSet2.xml, or ReferenceNodeManager changes). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Schema/NodeSetToModelDesignTests.cs | 99 +++++++++++++++++++ .../Generators/NodeStateGenerator.cs | 11 ++- .../Schema/NodeSetToModelDesign.cs | 21 +++- 3 files changed, 129 insertions(+), 2 deletions(-) diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs index 6a4af48b32..fd66f9721d 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs @@ -62,6 +62,70 @@ namespace Opc.Ua.Schema.Model.Tests private const string SameNamedArgumentsResource = "SameNamedMethodArguments.NodeSet2.xml"; private const string DesignResource = "TestDataDesign.xml"; + private const string ViewImportNodeSet = """ + + + + http://test.org/UA/ViewImport/ + + + + + + i=45 + i=40 + i=35 + + + HierarchicalReferences + + i=33 + + + + Organizes + + i=33 + + + + BaseObjectType + + i=58 + + + + Views + + ns=1;s=Views_Operations + ns=1;s=Views_Engineering + i=58 + + + + Operations + + i=87 + + + + Engineering + + i=87 + + + + """; private VirtualFileSystem m_fileSystem; @@ -291,6 +355,41 @@ namespace Opc.Ua.Schema.Model.Tests }); } + /// + /// Verifies importing views as top-level nodes keeps their organizing folder references. + /// + [Test] + public void ImportPreservesFolderOrganizesReferencesToViews() + { + const string path = "memory://ViewImport.NodeSet2.xml"; + m_fileSystem.Add(path, Encoding.UTF8.GetBytes(ViewImportNodeSet)); + + var settings = new NodeSetReaderSettings(); + NodeSetToModelDesign importer = new( + m_fileSystem, + path, + settings, + CreateTelemetry()); + + ModelDesign model = importer.Import("ViewImport", "ViewImport"); + ObjectDesign viewsFolder = model.Items + .OfType() + .Single(x => x.SymbolicName?.Name == "Views"); + string[] expectedViews = ["Operations", "Engineering"]; + + Assert.Multiple(() => + { + Assert.That(model.Items.OfType().Select(x => x.SymbolicName?.Name), Is.EquivalentTo( + expectedViews)); + Assert.That(viewsFolder.Children?.Items, Is.Null.Or.Empty); + Assert.That(viewsFolder.References, Has.Length.EqualTo(2)); + Assert.That(viewsFolder.References.Select(x => x.ReferenceType.Name), Is.All.EqualTo("Organizes")); + Assert.That(viewsFolder.References.Select(x => x.IsInverse), Is.All.False); + Assert.That(viewsFolder.References.Select(x => x.TargetId.Name), Is.EquivalentTo( + expectedViews)); + }); + } + [Test] public void TypeSymbolicNameUsesNodeIdNamespace() { diff --git a/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs b/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs index b2f4a4563b..da75bec82a 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs @@ -1319,6 +1319,14 @@ namespace Opc.Ua.SourceGeneration if (node.Parent != null) { + if (node.Design is ViewDesign) + { + // Views are never emitted as AddChild components (ViewState is + // not a BaseInstanceState). They are collected as standalone + // root nodes and linked purely via Organizes references, the + // same way DataType/ReferenceType type designs are handled. + return null; + } if (node.Design is not InstanceDesign instance) { return null; @@ -1529,7 +1537,8 @@ namespace Opc.Ua.SourceGeneration private TemplateString LoadTemplate_ReplaceChild(ILoadContext context) { if (context.Target is not NodeToGenerate node || - node.Design is not InstanceDesign instance) + node.Design is not InstanceDesign instance || + node.Design is ViewDesign) { return null; } diff --git a/tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs b/tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs index 7572aeddd3..b0efeb2068 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs @@ -950,6 +950,7 @@ namespace Opc.Ua.Schema.Model return; } output.SupportsEvents = (input.EventNotifier & EventNotifiers.SubscribeToEvents) != 0; + output.ContainsNoLoops = input.ContainsNoLoops; } private void UpdateVariableDesign(UAVariable input, VariableDesign output) @@ -1600,7 +1601,10 @@ namespace Opc.Ua.Schema.Model continue; } - if (targetId.NamespaceIndex != nodeId.NamespaceIndex || + if ((ii.IsForward && + referenceTypeId == ReferenceTypeIds.Organizes && + target is ViewDesign) || + targetId.NamespaceIndex != nodeId.NamespaceIndex || IsTypeOf(referenceTypeId, ReferenceTypeIds.NonHierarchicalReferences)) { references.Add(new Reference @@ -1953,6 +1957,21 @@ namespace Opc.Ua.Schema.Model if (node is UAInstance instance) { + // View nodes are independent address-space nodes even when an + // exporter sets ParentNodeId to an organizing folder. ViewState + // derives from NodeState (not BaseInstanceState) and therefore + // cannot be added as an AddChild component. Keeping the parent + // would absorb the view into the folder's Children, exclude it + // from the top-level model items and make the generator emit an + // invalid AddChild(...) call. Clearing the parent keeps the view + // a standalone predefined node linked purely via Organizes + // references (mirrors the DataTypeEncoding handling below and the + // way DataType/ReferenceType type nodes are modelled). + if (node is UAView) + { + instance.ParentNodeId = null; + } + // ensure parents are in the same namespace. if (instance.ParentNodeId != null) {