mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
Import NodeSet2 Views as standalone top-level nodes in the source generator (#4259)
## 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>
This commit is contained in:
@@ -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 = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<UANodeSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
|
||||
<NamespaceUris>
|
||||
<Uri>http://test.org/UA/ViewImport/</Uri>
|
||||
</NamespaceUris>
|
||||
<Models>
|
||||
<Model ModelUri="http://test.org/UA/ViewImport/"
|
||||
PublicationDate="2026-08-12T00:00:00Z"
|
||||
Version="1.0.0" />
|
||||
</Models>
|
||||
<Aliases>
|
||||
<Alias Alias="HasSubtype">i=45</Alias>
|
||||
<Alias Alias="HasTypeDefinition">i=40</Alias>
|
||||
<Alias Alias="Organizes">i=35</Alias>
|
||||
</Aliases>
|
||||
<UAReferenceType NodeId="i=33" BrowseName="HierarchicalReferences" IsAbstract="true">
|
||||
<DisplayName>HierarchicalReferences</DisplayName>
|
||||
<References>
|
||||
<Reference ReferenceType="HasSubtype" IsForward="false">i=33</Reference>
|
||||
</References>
|
||||
</UAReferenceType>
|
||||
<UAReferenceType NodeId="i=35" BrowseName="Organizes">
|
||||
<DisplayName>Organizes</DisplayName>
|
||||
<References>
|
||||
<Reference ReferenceType="HasSubtype" IsForward="false">i=33</Reference>
|
||||
</References>
|
||||
</UAReferenceType>
|
||||
<UAObjectType NodeId="i=58" BrowseName="BaseObjectType">
|
||||
<DisplayName>BaseObjectType</DisplayName>
|
||||
<References>
|
||||
<Reference ReferenceType="HasSubtype" IsForward="false">i=58</Reference>
|
||||
</References>
|
||||
</UAObjectType>
|
||||
<UAObject NodeId="ns=1;s=Views" BrowseName="1:Views">
|
||||
<DisplayName>Views</DisplayName>
|
||||
<References>
|
||||
<Reference ReferenceType="Organizes">ns=1;s=Views_Operations</Reference>
|
||||
<Reference ReferenceType="Organizes">ns=1;s=Views_Engineering</Reference>
|
||||
<Reference ReferenceType="HasTypeDefinition">i=58</Reference>
|
||||
</References>
|
||||
</UAObject>
|
||||
<UAView NodeId="ns=1;s=Views_Operations"
|
||||
BrowseName="1:Operations"
|
||||
ParentNodeId="ns=1;s=Views"
|
||||
ContainsNoLoops="true">
|
||||
<DisplayName>Operations</DisplayName>
|
||||
<References>
|
||||
<Reference ReferenceType="Organizes" IsForward="false">i=87</Reference>
|
||||
</References>
|
||||
</UAView>
|
||||
<UAView NodeId="ns=1;s=Views_Engineering"
|
||||
BrowseName="1:Engineering"
|
||||
ParentNodeId="ns=1;s=Views"
|
||||
ContainsNoLoops="true">
|
||||
<DisplayName>Engineering</DisplayName>
|
||||
<References>
|
||||
<Reference ReferenceType="Organizes" IsForward="false">i=87</Reference>
|
||||
</References>
|
||||
</UAView>
|
||||
</UANodeSet>
|
||||
""";
|
||||
|
||||
private VirtualFileSystem m_fileSystem;
|
||||
|
||||
@@ -291,6 +355,41 @@ namespace Opc.Ua.Schema.Model.Tests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies importing views as top-level nodes keeps their organizing folder references.
|
||||
/// </summary>
|
||||
[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<ObjectDesign>()
|
||||
.Single(x => x.SymbolicName?.Name == "Views");
|
||||
string[] expectedViews = ["Operations", "Engineering"];
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(model.Items.OfType<ViewDesign>().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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user