diff --git a/src/Opc.Ua.Di.Server/DiNodeManager.cs b/src/Opc.Ua.Di.Server/DiNodeManager.cs index 3a75d3790f..eef4b436a7 100644 --- a/src/Opc.Ua.Di.Server/DiNodeManager.cs +++ b/src/Opc.Ua.Di.Server/DiNodeManager.cs @@ -34,6 +34,7 @@ using Opc.Ua.Di.Server.Builders; using Opc.Ua.Server; using Opc.Ua.Server.Fluent; +using Opc.Ua.Server.NodeManager; using ConformanceUnitNames = Opc.Ua.Di.Server.ConformanceUnits; using ServerProfileUris = Opc.Ua.Di.Server.ServerProfiles; @@ -825,7 +826,8 @@ internal NodeManagerBuilder GetOrCreateBuilder() } } return results; - }); + }, + dataTypeId => PredefinedNodes.Values.FindByDataType(dataTypeId)); AttachToBuilder(m_builder); return m_builder; diff --git a/src/Opc.Ua.ISA95/Design/Common/Opc.ISA95.NodeIds.csv b/src/Opc.Ua.ISA95/Design/Common/Opc.ISA95.NodeIds.csv index 9c61e59342..7fcfd943c9 100644 --- a/src/Opc.Ua.ISA95/Design/Common/Opc.ISA95.NodeIds.csv +++ b/src/Opc.Ua.ISA95/Design/Common/Opc.ISA95.NodeIds.csv @@ -163,71 +163,71 @@ Opc_ISA95_ISA95AssetAssignmentDataType,4970,Variable ISA95AssetAssignmentDataType_Encoding_DefaultBinary,4973,Object Opc_ISA95_ISA95AssetAssignmentDataType_4974,4974,Variable QualificationTestSpecificationType,4977,ObjectType -PersonnelClassPropertyType_xPropertyName_,4979,Variable -PersonnelClassPropertyType_xTestSpecification_,4981,Object -PersonnelClassPropertyType_xTestSpecification__Version,4982,Variable +PersonnelClassPropertyType_PropertyName_Placeholder,4979,Variable +PersonnelClassPropertyType_TestSpecification_Placeholder,4981,Object +PersonnelClassPropertyType_TestSpecification_Placeholder_Version,4982,Variable PersonnelClassType,4996,ObjectType -PersonnelClassType_xPropertyName_,4997,Variable -PersonnelClassType_xTestSpecification_,4999,Object -PersonnelClassType_xTestSpecification__Version,5000,Variable +PersonnelClassType_PropertyName_Placeholder,4997,Variable +PersonnelClassType_TestSpecification_Placeholder,4999,Object +PersonnelClassType_TestSpecification_Placeholder_Version,5000,Variable EquipmentCapabilityTestResultType,5008,VariableType EquipmentCapabilityTestSpecificationType,5015,ObjectType EquipmentClassPropertyType,5017,VariableType -EquipmentClassPropertyType_xPropertyName_,5019,Variable -EquipmentClassPropertyType_xTestSpecification_,5021,Object -EquipmentClassPropertyType_xTestSpecification__Version,5022,Variable -EquipmentPropertyType_xPropertyName_,5023,Variable -EquipmentPropertyType_xTestSpecification_,5025,Object -EquipmentPropertyType_xTestSpecification__Version,5026,Variable -EquipmentPropertyType_xTestResult_,5027,Variable -EquipmentPropertyType_xTestResult__Id,5028,Variable -EquipmentPropertyType_xTestResult__ResultDescription,5029,Variable -EquipmentPropertyType_xTestResult__TestDate,5030,Variable -EquipmentPropertyType_xTestResult__Result,5031,Variable -EquipmentPropertyType_xTestResult__ResultUnitOfMeasure,5032,Variable -EquipmentPropertyType_xTestResult__Expiration,5033,Variable +EquipmentClassPropertyType_PropertyName_Placeholder,5019,Variable +EquipmentClassPropertyType_TestSpecification_Placeholder,5021,Object +EquipmentClassPropertyType_TestSpecification_Placeholder_Version,5022,Variable +EquipmentPropertyType_PropertyName_Placeholder,5023,Variable +EquipmentPropertyType_TestSpecification_Placeholder,5025,Object +EquipmentPropertyType_TestSpecification_Placeholder_Version,5026,Variable +EquipmentPropertyType_TestResult_Placeholder,5027,Variable +EquipmentPropertyType_TestResult_Placeholder_Id,5028,Variable +EquipmentPropertyType_TestResult_Placeholder_ResultDescription,5029,Variable +EquipmentPropertyType_TestResult_Placeholder_TestDate,5030,Variable +EquipmentPropertyType_TestResult_Placeholder_Result,5031,Variable +EquipmentPropertyType_TestResult_Placeholder_ResultUnitOfMeasure,5032,Variable +EquipmentPropertyType_TestResult_Placeholder_Expiration,5033,Variable EquipmentClassType,5034,ObjectType -EquipmentClassType_xPropertyName_,5035,Variable -EquipmentClassType_xTestSpecification_,5037,Object -EquipmentClassType_xTestSpecification__Version,5038,Variable +EquipmentClassType_PropertyName_Placeholder,5035,Variable +EquipmentClassType_TestSpecification_Placeholder,5037,Object +EquipmentClassType_TestSpecification_Placeholder_Version,5038,Variable EquipmentClassType_EquipmentLevel,5039,Variable EquipmentType,5040,ObjectType -EquipmentType_xPropertyName_,5041,Variable -EquipmentType_xEquipmentClass_,5043,Object -EquipmentType_xTestSpecification_,5045,Object -EquipmentType_xTestSpecification__Version,5046,Variable +EquipmentType_PropertyName_Placeholder,5041,Variable +EquipmentType_EquipmentClass_Placeholder,5043,Object +EquipmentType_TestSpecification_Placeholder,5045,Object +EquipmentType_TestSpecification_Placeholder_Version,5046,Variable EquipmentType_EquipmentLevel,5047,Variable GeoSpatialLocationType,5048,VariableType CompanyType,5049,VariableType PhysicalAssetCapabilityTestResultType,5050,VariableType PhysicalAssetCapabilityTestSpecificationType,5057,ObjectType PhysicalAssetClassPropertyType,5059,VariableType -PhysicalAssetClassPropertyType_xPropertyName_,5061,Variable -PhysicalAssetClassPropertyType_xTestSpecification_,5063,Object -PhysicalAssetClassPropertyType_xTestSpecification__Version,5064,Variable +PhysicalAssetClassPropertyType_PropertyName_Placeholder,5061,Variable +PhysicalAssetClassPropertyType_TestSpecification_Placeholder,5063,Object +PhysicalAssetClassPropertyType_TestSpecification_Placeholder_Version,5064,Variable PhysicalAssetPropertyType,5065,VariableType -PhysicalAssetPropertyType_xPropertyName_,5067,Variable -PhysicalAssetPropertyType_xTestSpecification_,5069,Object -PhysicalAssetPropertyType_xTestSpecification__Version,5070,Variable -PhysicalAssetPropertyType_xTestResult_,5071,Variable -PhysicalAssetPropertyType_xTestResult__Id,5072,Variable -PhysicalAssetPropertyType_xTestResult__ResultDescription,5073,Variable -PhysicalAssetPropertyType_xTestResult__TestDate,5074,Variable -PhysicalAssetPropertyType_xTestResult__Result,5075,Variable -PhysicalAssetPropertyType_xTestResult__ResultUnitOfMeasure,5076,Variable -PhysicalAssetPropertyType_xTestResult__Expiration,5077,Variable +PhysicalAssetPropertyType_PropertyName_Placeholder,5067,Variable +PhysicalAssetPropertyType_TestSpecification_Placeholder,5069,Object +PhysicalAssetPropertyType_TestSpecification_Placeholder_Version,5070,Variable +PhysicalAssetPropertyType_TestResult_Placeholder,5071,Variable +PhysicalAssetPropertyType_TestResult_Placeholder_Id,5072,Variable +PhysicalAssetPropertyType_TestResult_Placeholder_ResultDescription,5073,Variable +PhysicalAssetPropertyType_TestResult_Placeholder_TestDate,5074,Variable +PhysicalAssetPropertyType_TestResult_Placeholder_Result,5075,Variable +PhysicalAssetPropertyType_TestResult_Placeholder_ResultUnitOfMeasure,5076,Variable +PhysicalAssetPropertyType_TestResult_Placeholder_Expiration,5077,Variable PhysicalAssetClassType,5078,ObjectType -PhysicalAssetClassType_xPropertyName_,5079,Variable -PhysicalAssetClassType_xTestSpecification_,5081,Object -PhysicalAssetClassType_xTestSpecification__Version,5082,Variable +PhysicalAssetClassType_PropertyName_Placeholder,5079,Variable +PhysicalAssetClassType_TestSpecification_Placeholder,5081,Object +PhysicalAssetClassType_TestSpecification_Placeholder_Version,5082,Variable PhysicalAssetClassType_Manufacturer,5083,Variable PhysicalAssetClassType_ModelNumber,5084,Variable PhysicalAssetType,5085,ObjectType -PhysicalAssetType_xPropertyName_,5086,Variable +PhysicalAssetType_PropertyName_Placeholder,5086,Variable PhysicalAssetType_PhysicalAssetClass,5088,Object -PhysicalAssetType_xTestSpecification_,5091,Object -PhysicalAssetType_xTestSpecification__Version,5092,Variable -EquipmentType_xPhysicalAsset_,5093,Object +PhysicalAssetType_TestSpecification_Placeholder,5091,Object +PhysicalAssetType_TestSpecification_Placeholder_Version,5092,Variable +EquipmentType_PhysicalAsset_Placeholder,5093,Object ISA95TestResultNonEUType,5094,VariableType ISA95TestResultNonEUType_ResultUnitOfMeasure,5099,Variable ISA95TestResultEUType,5101,VariableType @@ -242,147 +242,147 @@ MadeUpOfEquipment,5115,ReferenceType MadeUpOfPhysicalAsset,5116,ReferenceType MadeUpOfMaterialSublot,5117,ReferenceType PersonPropertyType,5118,VariableType -PersonPropertyType_xPropertyName_,5120,Variable -PersonPropertyType_xTestSpecification_,5122,Object -PersonPropertyType_xTestSpecification__Version,5123,Variable -PersonPropertyType_xTestResult_,5124,Variable -PersonPropertyType_xTestResult__Id,5125,Variable -PersonPropertyType_xTestResult__ResultDescription,5126,Variable -PersonPropertyType_xTestResult__TestDate,5127,Variable -PersonPropertyType_xTestResult__Result,5128,Variable -PersonPropertyType_xTestResult__ResultUnitOfMeasure,5129,Variable -PersonPropertyType_xTestResult__Expiration,5130,Variable +PersonPropertyType_PropertyName_Placeholder,5120,Variable +PersonPropertyType_TestSpecification_Placeholder,5122,Object +PersonPropertyType_TestSpecification_Placeholder_Version,5123,Variable +PersonPropertyType_TestResult_Placeholder,5124,Variable +PersonPropertyType_TestResult_Placeholder_Id,5125,Variable +PersonPropertyType_TestResult_Placeholder_ResultDescription,5126,Variable +PersonPropertyType_TestResult_Placeholder_TestDate,5127,Variable +PersonPropertyType_TestResult_Placeholder_Result,5128,Variable +PersonPropertyType_TestResult_Placeholder_ResultUnitOfMeasure,5129,Variable +PersonPropertyType_TestResult_Placeholder_Expiration,5130,Variable PersonType,5131,ObjectType -PersonType_xPropertyName_,5132,Variable -PersonType_xTestSpecification_,5134,Object -PersonType_xTestSpecification__Version,5135,Variable -PhysicalAssetType_xPhysicalAsset_,5136,Object -PhysicalAssetType_xPhysicalAsset__PhysicalLocation,5137,Variable +PersonType_PropertyName_Placeholder,5132,Variable +PersonType_TestSpecification_Placeholder,5134,Object +PersonType_TestSpecification_Placeholder_Version,5135,Variable +PhysicalAssetType_PhysicalAsset_Placeholder,5136,Object +PhysicalAssetType_PhysicalAsset_Placeholder_PhysicalLocation,5137,Variable PhysicalAssetType_PhysicalLocation,5138,Variable -EquipmentType_xEquipment_,5144,Object -EquipmentType_xEquipment__EquipmentLevel,5145,Variable -EquipmentType_xEquipment__xPhysicalAsset_,5146,Object -PhysicalAssetType_xPhysicalAsset__PhysicalAssetClass,5148,Object -PhysicalAssetType_xPhysicalAsset__FixedAssetId,5151,Variable -PhysicalAssetType_xPhysicalAsset__VendorId,5152,Variable +EquipmentType_Equipment_Placeholder,5144,Object +EquipmentType_Equipment_Placeholder_EquipmentLevel,5145,Variable +EquipmentType_Equipment_Placeholder_PhysicalAsset_Placeholder,5146,Object +PhysicalAssetType_PhysicalAsset_Placeholder_PhysicalAssetClass,5148,Object +PhysicalAssetType_PhysicalAsset_Placeholder_FixedAssetId,5151,Variable +PhysicalAssetType_PhysicalAsset_Placeholder_VendorId,5152,Variable PhysicalAssetType_FixedAssetId,5153,Variable PhysicalAssetType_VendorId,5154,Variable MaterialTestResultType,5165,VariableType MaterialTestSpecificationType,5172,ObjectType MaterialDefinitionPropertyType,5174,VariableType -MaterialDefinitionPropertyType_xPropertyName_,5176,Variable -MaterialDefinitionPropertyType_xTestSpecification_,5178,Object -MaterialDefinitionPropertyType_xTestSpecification__Version,5179,Variable +MaterialDefinitionPropertyType_PropertyName_Placeholder,5176,Variable +MaterialDefinitionPropertyType_TestSpecification_Placeholder,5178,Object +MaterialDefinitionPropertyType_TestSpecification_Placeholder_Version,5179,Variable MaterialClassPropertyType,5180,VariableType -MaterialClassPropertyType_xPropertyName_,5182,Variable -MaterialClassPropertyType_xTestSpecification_,5184,Object -MaterialClassPropertyType_xTestSpecification__Version,5185,Variable +MaterialClassPropertyType_PropertyName_Placeholder,5182,Variable +MaterialClassPropertyType_TestSpecification_Placeholder,5184,Object +MaterialClassPropertyType_TestSpecification_Placeholder_Version,5185,Variable MaterialLotPropertyType,5186,VariableType -MaterialLotPropertyType_xTestResult_,5193,Variable -MaterialLotPropertyType_xTestResult__Id,5194,Variable -MaterialLotPropertyType_xTestResult__ResultDescription,5195,Variable -MaterialLotPropertyType_xTestResult__TestDate,5196,Variable -MaterialLotPropertyType_xTestResult__Result,5197,Variable -MaterialLotPropertyType_xTestResult__ResultUnitOfMeasure,5198,Variable -MaterialLotPropertyType_xTestResult__Expiration,5199,Variable -MaterialLotPropertyType_xPropertyName_,5200,Variable -MaterialLotPropertyType_xPropertyName__Id,5201,Variable -MaterialLotPropertyType_xPropertyName__ResultDescription,5202,Variable -MaterialLotPropertyType_xPropertyName__TestDate,5203,Variable -MaterialLotPropertyType_xPropertyName__Result,5204,Variable -MaterialLotPropertyType_xPropertyName__ResultUnitOfMeasure,5205,Variable -MaterialLotPropertyType_xPropertyName__Expiration,5206,Variable -MaterialLotPropertyType_xTestSpecification_,5207,Object -MaterialLotPropertyType_xTestSpecification__Version,5208,Variable +MaterialLotPropertyType_TestResult_Placeholder,5193,Variable +MaterialLotPropertyType_TestResult_Placeholder_Id,5194,Variable +MaterialLotPropertyType_TestResult_Placeholder_ResultDescription,5195,Variable +MaterialLotPropertyType_TestResult_Placeholder_TestDate,5196,Variable +MaterialLotPropertyType_TestResult_Placeholder_Result,5197,Variable +MaterialLotPropertyType_TestResult_Placeholder_ResultUnitOfMeasure,5198,Variable +MaterialLotPropertyType_TestResult_Placeholder_Expiration,5199,Variable +MaterialLotPropertyType_PropertyName_Placeholder,5200,Variable +MaterialLotPropertyType_PropertyName_Placeholder_Id,5201,Variable +MaterialLotPropertyType_PropertyName_Placeholder_ResultDescription,5202,Variable +MaterialLotPropertyType_PropertyName_Placeholder_TestDate,5203,Variable +MaterialLotPropertyType_PropertyName_Placeholder_Result,5204,Variable +MaterialLotPropertyType_PropertyName_Placeholder_ResultUnitOfMeasure,5205,Variable +MaterialLotPropertyType_PropertyName_Placeholder_Expiration,5206,Variable +MaterialLotPropertyType_TestSpecification_Placeholder,5207,Object +MaterialLotPropertyType_TestSpecification_Placeholder_Version,5208,Variable MaterialClassType,5209,ObjectType -MaterialClassType_xPropertyName_,5210,Variable -MaterialClassType_xTestSpecification_,5212,Object -MaterialClassType_xTestSpecification__Version,5213,Variable -MaterialClassType_xAssemblyClass_,5214,Object -MaterialClassType_xAssemblyClass__AssemblyType,5215,Variable -MaterialClassType_xAssemblyClass__AssemblyRelationship,5216,Variable +MaterialClassType_PropertyName_Placeholder,5210,Variable +MaterialClassType_TestSpecification_Placeholder,5212,Object +MaterialClassType_TestSpecification_Placeholder_Version,5213,Variable +MaterialClassType_AssemblyClass_Placeholder,5214,Object +MaterialClassType_AssemblyClass_Placeholder_AssemblyType,5215,Variable +MaterialClassType_AssemblyClass_Placeholder_AssemblyRelationship,5216,Variable MaterialClassType_AssemblyType,5217,Variable MaterialClassType_AssemblyRelationship,5218,Variable MaterialDefinitionType,5219,ObjectType -MaterialDefinitionType_xPropertyName_,5220,Variable -MaterialDefinitionType_xTestSpecification_,5222,Object -MaterialDefinitionType_xTestSpecification__Version,5223,Variable -MaterialDefinitionType_xMaterialClass_,5224,Object -MaterialDefinitionType_xAssemblyClass_,5227,Object -MaterialDefinitionType_xAssemblyClass__AssemblyType,5228,Variable -MaterialDefinitionType_xAssemblyClass__AssemblyRelationship,5229,Variable +MaterialDefinitionType_PropertyName_Placeholder,5220,Variable +MaterialDefinitionType_TestSpecification_Placeholder,5222,Object +MaterialDefinitionType_TestSpecification_Placeholder_Version,5223,Variable +MaterialDefinitionType_MaterialClass_Placeholder,5224,Object +MaterialDefinitionType_AssemblyClass_Placeholder,5227,Object +MaterialDefinitionType_AssemblyClass_Placeholder_AssemblyType,5228,Variable +MaterialDefinitionType_AssemblyClass_Placeholder_AssemblyRelationship,5229,Variable MaterialDefinitionType_AssemblyType,5230,Variable MaterialDefinitionType_AssemblyRelationship,5231,Variable MaterialLotType,5232,ObjectType -MaterialLotType_xPropertyName_,5233,Variable -MaterialLotType_xPropertyName__Id,5234,Variable -MaterialLotType_xPropertyName__ResultDescription,5235,Variable -MaterialLotType_xPropertyName__TestDate,5236,Variable -MaterialLotType_xPropertyName__Result,5237,Variable -MaterialLotType_xPropertyName__ResultUnitOfMeasure,5238,Variable -MaterialLotType_xPropertyName__Expiration,5239,Variable -MaterialLotType_xTestSpecification_,5240,Object -MaterialLotType_xTestSpecification__Version,5241,Variable -MaterialLotType_xMaterialDefinition_,5242,Object -MaterialLotType_xAssemblyLot_,5245,Object -MaterialLotType_xAssemblyLot__xMaterialDefinition_,5246,Object -MaterialLotType_xAssemblyLot__AssemblyType,5249,Variable -MaterialLotType_xAssemblyLot__AssemblyRelationship,5250,Variable -MaterialLotType_xAssemblyLot__Status,5251,Variable -MaterialLotType_xAssemblyLot__StorageLocation,5252,Variable -MaterialLotType_xAssemblyLot__Quantity,5253,Variable +MaterialLotType_PropertyName_Placeholder,5233,Variable +MaterialLotType_PropertyName_Placeholder_Id,5234,Variable +MaterialLotType_PropertyName_Placeholder_ResultDescription,5235,Variable +MaterialLotType_PropertyName_Placeholder_TestDate,5236,Variable +MaterialLotType_PropertyName_Placeholder_Result,5237,Variable +MaterialLotType_PropertyName_Placeholder_ResultUnitOfMeasure,5238,Variable +MaterialLotType_PropertyName_Placeholder_Expiration,5239,Variable +MaterialLotType_TestSpecification_Placeholder,5240,Object +MaterialLotType_TestSpecification_Placeholder_Version,5241,Variable +MaterialLotType_MaterialDefinition_Placeholder,5242,Object +MaterialLotType_AssemblyLot_Placeholder,5245,Object +MaterialLotType_AssemblyLot_Placeholder_MaterialDefinition_Placeholder,5246,Object +MaterialLotType_AssemblyLot_Placeholder_AssemblyType,5249,Variable +MaterialLotType_AssemblyLot_Placeholder_AssemblyRelationship,5250,Variable +MaterialLotType_AssemblyLot_Placeholder_Status,5251,Variable +MaterialLotType_AssemblyLot_Placeholder_StorageLocation,5252,Variable +MaterialLotType_AssemblyLot_Placeholder_Quantity,5253,Variable MaterialLotType_AssemblyType,5254,Variable MaterialLotType_AssemblyRelationship,5255,Variable MaterialLotType_Status,5256,Variable MaterialLotType_StorageLocation,5257,Variable MaterialLotType_Quantity,5258,Variable MaterialSublotType,5259,ObjectType -MaterialSublotType_xPropertyName_,5260,Variable -MaterialSublotType_xPropertyName__Id,5261,Variable -MaterialSublotType_xPropertyName__ResultDescription,5262,Variable -MaterialSublotType_xPropertyName__TestDate,5263,Variable -MaterialSublotType_xPropertyName__Result,5264,Variable -MaterialSublotType_xPropertyName__ResultUnitOfMeasure,5265,Variable -MaterialSublotType_xPropertyName__Expiration,5266,Variable -MaterialSublotType_xTestSpecification_,5267,Object -MaterialSublotType_xTestSpecification__Version,5268,Variable -MaterialSublotType_xAssemblyLot_,5269,Object -MaterialSublotType_xAssemblySublot_,5278,Object -MaterialSublotType_xAssemblySublot__AssemblyType,5279,Variable -MaterialSublotType_xAssemblySublot__AssemblyRelationship,5280,Variable -MaterialSublotType_xAssemblySublot__Status,5281,Variable -MaterialSublotType_xAssemblySublot__StorageLocation,5282,Variable -MaterialSublotType_xAssemblySublot__Quantity,5283,Variable +MaterialSublotType_PropertyName_Placeholder,5260,Variable +MaterialSublotType_PropertyName_Placeholder_Id,5261,Variable +MaterialSublotType_PropertyName_Placeholder_ResultDescription,5262,Variable +MaterialSublotType_PropertyName_Placeholder_TestDate,5263,Variable +MaterialSublotType_PropertyName_Placeholder_Result,5264,Variable +MaterialSublotType_PropertyName_Placeholder_ResultUnitOfMeasure,5265,Variable +MaterialSublotType_PropertyName_Placeholder_Expiration,5266,Variable +MaterialSublotType_TestSpecification_Placeholder,5267,Object +MaterialSublotType_TestSpecification_Placeholder_Version,5268,Variable +MaterialSublotType_AssemblyLot_Placeholder,5269,Object +MaterialSublotType_AssemblySublot_Placeholder,5278,Object +MaterialSublotType_AssemblySublot_Placeholder_AssemblyType,5279,Variable +MaterialSublotType_AssemblySublot_Placeholder_AssemblyRelationship,5280,Variable +MaterialSublotType_AssemblySublot_Placeholder_Status,5281,Variable +MaterialSublotType_AssemblySublot_Placeholder_StorageLocation,5282,Variable +MaterialSublotType_AssemblySublot_Placeholder_Quantity,5283,Variable MaterialSublotType_AssemblyType,5284,Variable MaterialSublotType_AssemblyRelationship,5285,Variable -MaterialSublotType_xSublot_,5286,Object -MaterialSublotType_xSublot__AssemblyType,5287,Variable -MaterialSublotType_xSublot__AssemblyRelationship,5288,Variable -MaterialSublotType_xSublot__Status,5289,Variable -MaterialSublotType_xSublot__StorageLocation,5290,Variable -MaterialSublotType_xSublot__Quantity,5291,Variable +MaterialSublotType_Sublot_Placeholder,5286,Object +MaterialSublotType_Sublot_Placeholder_AssemblyType,5287,Variable +MaterialSublotType_Sublot_Placeholder_AssemblyRelationship,5288,Variable +MaterialSublotType_Sublot_Placeholder_Status,5289,Variable +MaterialSublotType_Sublot_Placeholder_StorageLocation,5290,Variable +MaterialSublotType_Sublot_Placeholder_Quantity,5291,Variable MaterialSublotType_Status,5292,Variable MaterialSublotType_StorageLocation,5293,Variable MaterialSublotType_Quantity,5294,Variable -MaterialLotType_xAssemblySublot_,5295,Object +MaterialLotType_AssemblySublot_Placeholder,5295,Object DefinedByMaterialClass,5300,ReferenceType DefinedByMaterialDefinition,5301,ReferenceType -PersonType_xPersonnelClass_,5302,Object -PhysicalAssetType_xPhysicalAsset__AssetAssignment,5303,Variable -PhysicalAssetType_xPhysicalAsset__AssetAssignment_Id,5304,Variable -PhysicalAssetType_xPhysicalAsset__AssetAssignment_AssignmentDescription,5305,Variable -PhysicalAssetType_xPhysicalAsset__AssetAssignment_StartTime,5306,Variable -PhysicalAssetType_xPhysicalAsset__AssetAssignment_StopTime,5307,Variable +PersonType_PersonnelClass_Placeholder,5302,Object +PhysicalAssetType_PhysicalAsset_Placeholder_AssetAssignment,5303,Variable +PhysicalAssetType_PhysicalAsset_Placeholder_AssetAssignment_Id,5304,Variable +PhysicalAssetType_PhysicalAsset_Placeholder_AssetAssignment_AssignmentDescription,5305,Variable +PhysicalAssetType_PhysicalAsset_Placeholder_AssetAssignment_StartTime,5306,Variable +PhysicalAssetType_PhysicalAsset_Placeholder_AssetAssignment_StopTime,5307,Variable PhysicalAssetType_AssetAssignment,5308,Variable PhysicalAssetType_AssetAssignment_Id,5309,Variable PhysicalAssetType_AssetAssignment_AssignmentDescription,5310,Variable PhysicalAssetType_AssetAssignment_StartTime,5311,Variable PhysicalAssetType_AssetAssignment_StopTime,5312,Variable -EquipmentType_xEquipment__AssetAssignment,5318,Variable -EquipmentType_xEquipment__AssetAssignment_Id,5319,Variable -EquipmentType_xEquipment__AssetAssignment_AssignmentDescription,5320,Variable -EquipmentType_xEquipment__AssetAssignment_StartTime,5321,Variable -EquipmentType_xEquipment__AssetAssignment_StopTime,5322,Variable +EquipmentType_Equipment_Placeholder_AssetAssignment,5318,Variable +EquipmentType_Equipment_Placeholder_AssetAssignment_Id,5319,Variable +EquipmentType_Equipment_Placeholder_AssetAssignment_AssignmentDescription,5320,Variable +EquipmentType_Equipment_Placeholder_AssetAssignment_StartTime,5321,Variable +EquipmentType_Equipment_Placeholder_AssetAssignment_StopTime,5322,Variable EquipmentType_AssetAssignment,5328,Variable EquipmentType_AssetAssignment_Id,5329,Variable EquipmentType_AssetAssignment_AssignmentDescription,5330,Variable diff --git a/src/Opc.Ua.OpenUsd.Server/OpenUsdRepresentationAuthoring.cs b/src/Opc.Ua.OpenUsd.Server/OpenUsdRepresentationAuthoring.cs index 24da7d2be6..75af1d6c15 100644 --- a/src/Opc.Ua.OpenUsd.Server/OpenUsdRepresentationAuthoring.cs +++ b/src/Opc.Ua.OpenUsd.Server/OpenUsdRepresentationAuthoring.cs @@ -138,11 +138,11 @@ public static OpenUsdLiveBindingState AddLiveBinding( throw new ArgumentNullException(nameof(context)); } - // AddxBinding_ instantiates the placeholder as a HasComponent + // AddBinding_Placeholder instantiates the placeholder as a HasComponent // child (browsable) and creates its mandatory base members. The binding // intent is the concrete subtype (§5.4): retype the instance to the // requested OpenUsd{ValueChange,Alarm,History,Command}BindingType. - OpenUsdLiveBindingState b = representation.AddxBinding_(context, new QualifiedName(name, ns)); + OpenUsdLiveBindingState b = representation.AddBinding_Placeholder(context, new QualifiedName(name, ns)); b.TypeDefinitionId = new NodeId(bindingTypeId, ns); // Mandatory base members already exist on the instance; set their values. @@ -245,7 +245,7 @@ public static OpenUsdComponentBindingState AddComponentBinding( throw new ArgumentNullException(nameof(context)); } - OpenUsdComponentBindingState b = representation.AddxComponent_(context, new QualifiedName(name, ns)); + OpenUsdComponentBindingState b = representation.AddComponent_Placeholder(context, new QualifiedName(name, ns)); b.CreateOrReplaceBindingDefinitionId(context, null!).Value = new Uuid(bindingDefinitionId); b.CreateOrReplaceEnabled(context, null!).Value = true; b.CreateOrReplaceCardinality(context, null!).Value = cardinality; diff --git a/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.Properties.cs b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.Properties.cs index ad32765b8e..79f9f4ed1c 100644 --- a/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.Properties.cs +++ b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.Properties.cs @@ -67,7 +67,7 @@ private static UsdAttributeState MaterializeAttribute( ushort ns, UsdMaterializationOptions options) { - UsdAttributeState node = primNode.AddxUsdAttribute_( + UsdAttributeState node = primNode.AddUsdAttribute_Placeholder( context, new QualifiedName(attribute.Name, ns)); UsdValueTypeMapping mapping = UsdValueTypeMap.Map( @@ -140,7 +140,7 @@ private static UsdRelationshipState MaterializeRelationship( UsdRelationship relationship, ushort ns) { - UsdRelationshipState node = primNode.AddxUsdRelationship_( + UsdRelationshipState node = primNode.AddUsdRelationship_Placeholder( context, new QualifiedName(relationship.Name, ns)); var paths = new string[relationship.Targets.Count]; @@ -295,9 +295,9 @@ private static void MaterializeVariantSets( // (fail closed); its selection is still carried by the Selection property above. foreach (UsdPrim branch in variantSet.Variants) { - UsdPrimState branchNode = node.AddxVariant_( + UsdPrimState branchNode = node.AddVariant_Placeholder( context, new QualifiedName(branch.Name, ns)); - // AddxVariant_ leaves the type's placeholder NodeId (i=6055); force a fresh + // AddVariant_Placeholder leaves the type's placeholder NodeId (i=6055); force a fresh // per-instance NodeId so branches on different sets never collide, matching // how the CreateInstanceOf* factories mint instance ids. context.AssignInstanceNodeId(branchNode); diff --git a/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.cs b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.cs index 81293ceae5..d34bc3b96b 100644 --- a/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.cs +++ b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.cs @@ -462,12 +462,12 @@ private static void MaterializePrim( // A prim of a known typed schema is instantiated as that generated ObjectType // subclass, so the node *is* the type it declares and carries the subtype's // members (§5.3). An unknown schema returns null here and degrades to a concrete - // UsdPrimType via AddxUsdPrim_, still keeping its TypeName token (§8.4). + // UsdPrimType via AddUsdPrim_Placeholder, still keeping its TypeName token (§8.4). UsdPrimState? primNode = CreateTypedPrimInstance(context, parent, prim.TypeName, browseName); primNode ??= parent switch { - UsdStageState stageParent => stageParent.AddxUsdPrim_(context, browseName), - UsdPrimState primParent => primParent.AddxUsdPrim_(context, browseName), + UsdStageState stageParent => stageParent.AddUsdPrim_Placeholder(context, browseName), + UsdPrimState primParent => primParent.AddUsdPrim_Placeholder(context, browseName), _ => throw new ArgumentException( "A prim can only be materialized under a stage or another prim.", nameof(parent)) @@ -577,7 +577,7 @@ private static void PopulatePrimNode( /// /// Instantiates a prim as the generated State subclass for a known typed schema (§5.3), - /// attached under exactly as AddxUsdPrim_ or the stage + /// attached under exactly as AddUsdPrim_Placeholder or the stage /// attach idiom would attach a plain prim (HasComponent + a factory NodeId, so it /// is browsable and uniquely identified identically). Returns null for an unknown /// or untyped schema so the caller applies the §8.4 fallback — a concrete @@ -621,7 +621,7 @@ private static void PopulatePrimNode( // The instance factory leaves ReferenceTypeId = Null and seeds a type NodeId; // Attach makes it a HasComponent child with a fresh factory NodeId, matching how - // AddxUsdPrim_ and MaterializeUsdStage attach every other node (§7.1). + // AddUsdPrim_Placeholder and MaterializeUsdStage attach every other node (§7.1). Attach(context, parent, node, Opc.Ua.ReferenceTypeIds.HasComponent); return node; } diff --git a/src/Opc.Ua.Server/Fluent/FluentNodeManagerBase.cs b/src/Opc.Ua.Server/Fluent/FluentNodeManagerBase.cs index 33d2dbf2cc..1fa33488d9 100644 --- a/src/Opc.Ua.Server/Fluent/FluentNodeManagerBase.cs +++ b/src/Opc.Ua.Server/Fluent/FluentNodeManagerBase.cs @@ -171,7 +171,7 @@ protected FluentNodeManagerBase( /// .Configure(Configure) /// .Seal(); /// - /// The three root/nodeId/typeId lookups default to scanning the + /// The root/nodeId/typeId/dataTypeId lookups default to scanning the /// manager's /// dictionary, mirroring the resolver wiring that the /// source-generated NodeManagerBase.CreateAddressSpaceAsync @@ -196,7 +196,8 @@ public NodeManagerBuilder CreateFluentBuilder(ushort defaultNamespaceIndex) defaultNamespaceIndex, browseName => PredefinedNodes.Values.FindByBrowseName(browseName)!, nodeId => PredefinedNodes.FindById(nodeId)!, - PredefinedNodes.Values.FindByTypeDefinition); + PredefinedNodes.Values.FindByTypeDefinition, + PredefinedNodes.Values.FindByDataType); AttachToBuilder(builder); return builder; } diff --git a/src/Opc.Ua.Server/Fluent/INodeManagerBuilder.cs b/src/Opc.Ua.Server/Fluent/INodeManagerBuilder.cs index 989019f0eb..cb9df45e1c 100644 --- a/src/Opc.Ua.Server/Fluent/INodeManagerBuilder.cs +++ b/src/Opc.Ua.Server/Fluent/INodeManagerBuilder.cs @@ -154,8 +154,12 @@ INodeBuilder Node(NodeId nodeId) /// /// /// — the id is null. - /// — no instance carries that type definition. - /// — more than one instance matches; supply a disambiguator via the overload. + /// — + /// no instance carries that type definition. + /// — + /// more than one instance matches; supply a + /// disambiguator via the + /// overload. /// /// INodeBuilder NodeFromTypeId(NodeId typeDefinitionId); @@ -247,5 +251,56 @@ INodeBuilder NodeFromTypeId(NodeId typeDefinitionId, QualifiedNa /// CLR type carried by the variable's Value attribute. /// IVariableBuilder VariableFromTypeId(NodeId typeDefinitionId, QualifiedName browseName); + + /// + /// Resolves the unique variable instance whose + /// DataType attribute equals + /// and returns a typed + /// view. Useful for singleton variables whose well-known DataType + /// is more stable than the deployment-specific browse path. + /// + /// + /// CLR type carried by the variable's Value attribute. + /// + /// + /// The DataType id of the variable to locate (typically a + /// generated DataTypeIds.* constant). + /// + /// + /// + /// — the id is null. + /// — + /// no variable carries that DataType. + /// — + /// more than one variable matches; supply a + /// disambiguator via the + /// + /// overload. + /// — + /// the resolved variable's Value is not assignable to + /// . + /// + /// + IVariableBuilder VariableFromDataTypeId(NodeId dataTypeId); + + /// + /// Like but + /// disambiguates among multiple instances by matching + /// against + /// . + /// + /// + /// CLR type carried by the variable's Value attribute. + /// + /// See . + /// + /// Browse name of the instance to pick out. + /// + /// + /// Same conditions as + /// plus when the + /// disambiguator matches no candidate. + /// + IVariableBuilder VariableFromDataTypeId(NodeId dataTypeId, QualifiedName browseName); } } diff --git a/src/Opc.Ua.Server/Fluent/NodeManagerBuilder.cs b/src/Opc.Ua.Server/Fluent/NodeManagerBuilder.cs index 7763e9dc7a..3fc9b850ef 100644 --- a/src/Opc.Ua.Server/Fluent/NodeManagerBuilder.cs +++ b/src/Opc.Ua.Server/Fluent/NodeManagerBuilder.cs @@ -84,6 +84,13 @@ public sealed class NodeManagerBuilder : INodeManagerBuilder, IFluentDispatcher /// TypeDefinitionId matches the supplied . /// Typically a generated walk over the manager's predefined nodes. /// + /// + /// Delegate that returns every whose + /// DataType matches the supplied . + /// Typically a generated walk over the manager's predefined nodes. + /// When null, DataType lookups always resolve to no + /// candidates (as if no variable declared that DataType). + /// /// /// , , /// , , @@ -95,7 +102,8 @@ public NodeManagerBuilder( ushort defaultNamespaceIndex, Func rootResolver, Func nodeIdResolver, - Func> typeIdResolver) + Func> typeIdResolver, + Func>? dataTypeIdResolver = null) { Context = context ?? throw new ArgumentNullException(nameof(context)); NodeManager = nodeManager ?? throw new ArgumentNullException(nameof(nodeManager)); @@ -103,6 +111,7 @@ public NodeManagerBuilder( m_rootResolver = rootResolver ?? throw new ArgumentNullException(nameof(rootResolver)); m_nodeIdResolver = nodeIdResolver ?? throw new ArgumentNullException(nameof(nodeIdResolver)); m_typeIdResolver = typeIdResolver ?? throw new ArgumentNullException(nameof(typeIdResolver)); + m_dataTypeIdResolver = dataTypeIdResolver ?? (static _ => []); } /// @@ -284,6 +293,27 @@ public IVariableBuilder VariableFromTypeId(NodeId typeDefinition browseName)); } + /// + public IVariableBuilder VariableFromDataTypeId(NodeId dataTypeId) + { + ThrowIfSealed(); + NodeState node = ResolveByDataType(dataTypeId, (QualifiedName)null!); + return ToVariableBuilder(node, FormatNodeId(dataTypeId)); + } + + /// + public IVariableBuilder VariableFromDataTypeId(NodeId dataTypeId, QualifiedName browseName) + { + ThrowIfSealed(); + NodeState node = ResolveByDataType(dataTypeId, browseName); + return ToVariableBuilder( + node, + CoreUtils.Format( + "{0} (browse name '{1}')", + FormatNodeId(dataTypeId), + browseName)); + } + internal VariableBuilder ToVariableBuilder(NodeState node, string lookupHint) { if (node is not BaseVariableState variable) @@ -590,6 +620,68 @@ private NodeState ResolveByTypeDefinition(NodeId typeDefinitionId, QualifiedName return match; } + private NodeState ResolveByDataType(NodeId dataTypeId, QualifiedName browseName) + { + if (dataTypeId.IsNull) + { + throw ServiceResultException.Create( + StatusCodes.BadNodeIdInvalid, + "DataTypeId is null or empty."); + } + + ArrayOf candidates = m_dataTypeIdResolver(dataTypeId); + + if (candidates.Count == 0) + { + throw ServiceResultException.Create( + StatusCodes.BadNodeIdUnknown, + "No predefined variable has DataType '{0}'.", + dataTypeId); + } + + if (browseName.IsNull) + { + if (candidates.Count > 1) + { + throw ServiceResultException.Create( + StatusCodes.BadBrowseNameDuplicated, + "DataType '{0}' is ambiguous: {1} matching variables found. " + + "Pass a QualifiedName disambiguator to VariableFromDataTypeId.", + dataTypeId, + candidates.Count); + } + return candidates[0]; + } + + NodeState? match = null; + for (int i = 0; i < candidates.Count; i++) + { + if (candidates[i].BrowseName == browseName) + { + if (match != null) + { + throw ServiceResultException.Create( + StatusCodes.BadBrowseNameDuplicated, + "DataType '{0}' has multiple variables with browse name '{1}'.", + dataTypeId, + browseName); + } + match = candidates[i]; + } + } + + if (match == null) + { + throw ServiceResultException.Create( + StatusCodes.BadNodeIdUnknown, + "DataType '{0}' has no variable with browse name '{1}'.", + dataTypeId, + browseName); + } + + return match; + } + private void ThrowIfSealed() { if (m_sealed) @@ -621,6 +713,7 @@ private static void ThrowIfDuplicate( private readonly Func m_rootResolver; private readonly Func m_nodeIdResolver; private readonly Func> m_typeIdResolver; + private readonly Func> m_dataTypeIdResolver; private bool m_sealed; private readonly Dictionary m_historyRead = []; private readonly Dictionary m_historyUpdate = []; diff --git a/src/Opc.Ua.Server/NodeManager/NodeStateLookupExtensions.cs b/src/Opc.Ua.Server/NodeManager/NodeStateLookupExtensions.cs index 34a8485995..aaa800293c 100644 --- a/src/Opc.Ua.Server/NodeManager/NodeStateLookupExtensions.cs +++ b/src/Opc.Ua.Server/NodeManager/NodeStateLookupExtensions.cs @@ -34,11 +34,12 @@ namespace Opc.Ua.Server.NodeManager { /// /// Linear-scan + dictionary lookups over a node-manager's predefined - /// node collection. Surfaces the three patterns that node-manager + /// node collection. Surfaces the patterns that node-manager /// subclasses repeatedly hand-roll against /// CustomNodeManager.PredefinedNodes / /// AsyncCustomNodeManager.PredefinedNodes: - /// browse-name root lookup, NodeId lookup, and TypeDefinitionId scan. + /// browse-name root lookup, NodeId lookup, TypeDefinitionId scan, and + /// DataType scan. /// public static class NodeStateLookupExtensions { @@ -135,5 +136,42 @@ public static List FindByTypeDefinition( } return results; } + + /// + /// Returns every in + /// whose + /// equals + /// . The list is empty when no match + /// exists; non-variable nodes are skipped. + /// + /// + /// The set to scan, typically PredefinedNodes.Values. + /// + /// + /// DataType NodeId to match (e.g. a known DataTypeIds + /// constant). + /// + /// + /// is . + /// + public static ArrayOf FindByDataType( + this IEnumerable nodes, + NodeId dataTypeId) + { + if (nodes == null) + { + throw new ArgumentNullException(nameof(nodes)); + } + var results = new List(); + foreach (NodeState node in nodes) + { + if (node is BaseVariableState variable && + variable.DataType == dataTypeId) + { + results.Add(node); + } + } + return new ArrayOf(results.ToArray()); + } } } diff --git a/tests/Opc.Ua.Di.Tests/DeviceBuilderTests.cs b/tests/Opc.Ua.Di.Tests/DeviceBuilderTests.cs index c0aefb2c7b..dd4535a382 100644 --- a/tests/Opc.Ua.Di.Tests/DeviceBuilderTests.cs +++ b/tests/Opc.Ua.Di.Tests/DeviceBuilderTests.cs @@ -31,6 +31,7 @@ using NUnit.Framework; using Opc.Ua.Di.Server; using Opc.Ua.Di.Server.Builders; +using Opc.Ua.Server.Fluent; namespace Opc.Ua.Di.Tests { @@ -134,6 +135,30 @@ public async Task CreateDeviceAsyncRegistersInPredefinedNodes() Assert.That(resolved, Is.SameAs(builder.Device)); } + [Test] + public void VariableFromDataTypeIdResolvesUniquePredefinedVariable() + { + ushort namespaceIndex = m_fixture.Manager.DiNamespaceIndex; + var predefined = new BaseDataVariableState(null) + { + NodeId = new NodeId("UniqueDataTypeVariable", namespaceIndex), + BrowseName = new QualifiedName("UniqueDataTypeVariable", namespaceIndex), + DisplayName = new LocalizedText("UniqueDataTypeVariable"), + DataType = Types.DataTypeIds.String, + ValueRank = ValueRanks.Scalar, + Value = "value" + }; + m_fixture.Manager.AddPlainPredefinedNodeSynchronously(predefined); + NodeManagerBuilder builder = m_fixture.Manager.GetOrCreateBuilder(); + + IVariableBuilder variable = + builder.VariableFromDataTypeId( + Types.DataTypeIds.String, + predefined.BrowseName); + + Assert.That(variable.Node, Is.SameAs(predefined)); + } + [Test] public async Task CreateDeviceAsyncFailsOnDuplicateBrowseName() { diff --git a/tests/Opc.Ua.OpenUsd.Tests/VariantBranchTests.cs b/tests/Opc.Ua.OpenUsd.Tests/VariantBranchTests.cs index a6477de84f..48c47b7acb 100644 --- a/tests/Opc.Ua.OpenUsd.Tests/VariantBranchTests.cs +++ b/tests/Opc.Ua.OpenUsd.Tests/VariantBranchTests.cs @@ -181,7 +181,7 @@ public void BranchesOnDifferentSets_ProduceDistinctBranchNodeIds() branchIds.Add(branch.NodeId); } - // Regression guard: AddxVariant_ leaves the type's placeholder NodeId, so without the + // Regression guard: AddVariant_Placeholder leaves the type's placeholder NodeId, so without the // explicit per-instance NodeId assignment both branches would collide on one id. Assert.That(branchIds[0], Is.Not.EqualTo(branchIds[1])); } diff --git a/tests/Opc.Ua.Server.Tests/Fluent/NodeManagerBuilderTests.cs b/tests/Opc.Ua.Server.Tests/Fluent/NodeManagerBuilderTests.cs index ec4edf7e61..d1a085c545 100644 --- a/tests/Opc.Ua.Server.Tests/Fluent/NodeManagerBuilderTests.cs +++ b/tests/Opc.Ua.Server.Tests/Fluent/NodeManagerBuilderTests.cs @@ -617,6 +617,32 @@ private static BaseObjectState MakeObject(string name, NodeId typeDefId) }; } + private static NodeManagerBuilder CreateBuilderWithDataTypeIndex( + Dictionary> byDataType) + { + return new NodeManagerBuilder( + CreateContext(), + Mock.Of(), + kNs, + _ => null, + _ => null, + _ => [], + dataTypeId => byDataType.TryGetValue(dataTypeId, out ArrayOf list) + ? list + : []); + } + + private static BaseDataVariableState MakeVariable(string name, NodeId dataTypeId) + { + return new BaseDataVariableState(parent: null) + { + NodeId = new NodeId(name, kNs), + BrowseName = new QualifiedName(name, kNs), + DataType = dataTypeId, + ValueRank = ValueRanks.Scalar + }; + } + [Test] public void NodeFromTypeIdResolvesSingleton() { @@ -865,5 +891,115 @@ public void VariableByBrowseNameThrowsBadTypeMismatchOnNonVariable() () => b.Node(root.NodeId).Variable(m.BrowseName)); Assert.That(ex.StatusCode, Is.EqualTo((uint)StatusCodes.BadTypeMismatch)); } + + [Test] + public void VariableFromDataTypeIdResolvesUniqueVariable() + { + NodeId dataTypeId = DataTypeIds.Int32; + BaseDataVariableState only = MakeVariable("ByDataType", dataTypeId); + NodeManagerBuilder b = CreateBuilderWithDataTypeIndex( + new Dictionary> { [dataTypeId] = [only] }); + + IVariableBuilder byDataType = b.VariableFromDataTypeId(dataTypeId); + + Assert.That(byDataType.Node, Is.SameAs(only)); + } + + [Test] + public void VariableFromDataTypeIdNullThrowsBadNodeIdInvalid() + { + NodeManagerBuilder b = CreateBuilderWithDataTypeIndex([]); + + ServiceResultException ex = Assert.Throws( + () => b.VariableFromDataTypeId(NodeId.Null)); + Assert.That(ex.StatusCode, Is.EqualTo((uint)StatusCodes.BadNodeIdInvalid)); + } + + [Test] + public void VariableFromDataTypeIdMissingThrowsBadNodeIdUnknown() + { + NodeManagerBuilder b = CreateBuilderWithDataTypeIndex([]); + + ServiceResultException ex = Assert.Throws( + () => b.VariableFromDataTypeId(DataTypeIds.Int32)); + Assert.That(ex.StatusCode, Is.EqualTo((uint)StatusCodes.BadNodeIdUnknown)); + } + + [Test] + public void VariableFromDataTypeIdAmbiguousThrowsBadBrowseNameDuplicated() + { + NodeId dataTypeId = DataTypeIds.Int32; + BaseDataVariableState a = MakeVariable("Temp1", dataTypeId); + BaseDataVariableState bn = MakeVariable("Temp2", dataTypeId); + NodeManagerBuilder b = CreateBuilderWithDataTypeIndex( + new Dictionary> { [dataTypeId] = [a, bn] }); + + ServiceResultException ex = Assert.Throws( + () => b.VariableFromDataTypeId(dataTypeId)); + Assert.That(ex.StatusCode, Is.EqualTo((uint)StatusCodes.BadBrowseNameDuplicated)); + } + + [Test] + public void VariableFromDataTypeIdWithBrowseNameDisambiguates() + { + NodeId dataTypeId = DataTypeIds.Int32; + BaseDataVariableState a = MakeVariable("Temp1", dataTypeId); + BaseDataVariableState bn = MakeVariable("Temp2", dataTypeId); + NodeManagerBuilder b = CreateBuilderWithDataTypeIndex( + new Dictionary> { [dataTypeId] = [a, bn] }); + + IVariableBuilder byName = b.VariableFromDataTypeId(dataTypeId, bn.BrowseName); + + Assert.That(byName.Node, Is.SameAs(bn)); + } + + [Test] + public void VariableFromDataTypeIdWithBrowseNameMissThrowsBadNodeIdUnknown() + { + NodeId dataTypeId = DataTypeIds.Int32; + BaseDataVariableState a = MakeVariable("Temp1", dataTypeId); + NodeManagerBuilder b = CreateBuilderWithDataTypeIndex( + new Dictionary> { [dataTypeId] = [a] }); + + ServiceResultException ex = Assert.Throws( + () => b.VariableFromDataTypeId(dataTypeId, new QualifiedName("Nope", kNs))); + Assert.That(ex.StatusCode, Is.EqualTo((uint)StatusCodes.BadNodeIdUnknown)); + } + + [Test] + public void VariableFromDataTypeIdWithDuplicateBrowseNameThrowsBadBrowseNameDuplicated() + { + NodeId dataTypeId = DataTypeIds.Int32; + BaseDataVariableState first = MakeVariable("Duplicate", dataTypeId); + BaseDataVariableState second = MakeVariable("Duplicate", dataTypeId); + NodeManagerBuilder b = CreateBuilderWithDataTypeIndex( + new Dictionary> { [dataTypeId] = [first, second] }); + + ServiceResultException ex = Assert.Throws( + () => b.VariableFromDataTypeId(dataTypeId, first.BrowseName)); + Assert.That(ex.StatusCode, Is.EqualTo((uint)StatusCodes.BadBrowseNameDuplicated)); + } + + /// + /// Mirrors : + /// the DataType resolver contract only guarantees the candidates it is + /// handed are instances at the + /// production resolver level; if a custom resolver (as injected here) + /// returns a non-variable node, + /// must still surface a clear + /// rather than an unhandled cast failure. + /// + [Test] + public void VariableFromDataTypeIdThrowsBadTypeMismatchForNonVariable() + { + NodeId dataTypeId = DataTypeIds.Int32; + BaseObjectState only = MakeObject("NotAVariable", ObjectTypeIds.BaseObjectType); + NodeManagerBuilder b = CreateBuilderWithDataTypeIndex( + new Dictionary> { [dataTypeId] = [only] }); + + ServiceResultException ex = Assert.Throws( + () => b.VariableFromDataTypeId(dataTypeId)); + Assert.That(ex.StatusCode, Is.EqualTo((uint)StatusCodes.BadTypeMismatch)); + } } } diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/CompilerUtils.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/CompilerUtils.cs index ccb25e3e97..b88a9e11e7 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/CompilerUtils.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/CompilerUtils.cs @@ -90,6 +90,14 @@ public static AdditionalText From(string resourceName) throw new FileNotFoundException("Resource not found"); } + /// + /// Creates an in-memory additional file with the supplied path and content. + /// + public static AdditionalText FromContent(string path, string content) + { + return new EmbeddedText(path, content); + } + /// /// Creates in-memory additional text. /// @@ -256,6 +264,27 @@ public static CSharpCompilation CreateCompilation( .AddReferences(DefaultReferences); } + /// + /// Runs the generator driver against the compilation and returns the + /// generator diagnostics together with the run result, without + /// asserting success. Use for tests that need to inspect diagnostics + /// produced by malformed, unsupported or otherwise rejected inputs + /// (for example model input that fails to parse or convert) instead + /// of asserting a clean, fully compiling run. Named distinctly from + /// the built-in GeneratorDriver.RunGenerators API (which + /// returns a , not diagnostics) to avoid + /// ambiguity. + /// + public static (ImmutableArray Diagnostics, GeneratorDriverRunResult RunResult) + RunGeneratorsForDiagnostics(this GeneratorDriver driver, Compilation compilation) + { + driver = driver.RunGeneratorsAndUpdateCompilation( + compilation, + out Compilation _, + out ImmutableArray diagnostics); + return (diagnostics, driver.GetRunResult()); + } + /// /// Add code files to compilation /// diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/FluentBuilderGeneratorTests.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/FluentBuilderGeneratorTests.cs index 363ad14103..5d23f858c5 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/FluentBuilderGeneratorTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/FluentBuilderGeneratorTests.cs @@ -334,12 +334,13 @@ public void DeclarationBackedInputOnlyMethodEmitsTypedOnCallOverloads() public void DeclarationBackedNodeSetFixtureCarriesDeclarationInputArgument() { Dictionary files = GenerateForDeclarationBackedNodeSet(); - string values = files.Single(kv => kv.Key.EndsWith(".NodeStates.i.g.cs", StringComparison.Ordinal)).Value; + string values = files.Single( + kv => kv.Key.EndsWith(".NodeStates.ex.g.cs", StringComparison.Ordinal)).Value; Assert.Multiple(() => { Assert.That(values, Does.Contain("AdjustDeclaration_InputArguments")); - Assert.That(values, Does.Contain("setpoint")); + Assert.That(values, Does.Contain("Name = \"setpoint\"")); }); } @@ -459,10 +460,8 @@ private static string GetFluentBuilders() return files.Single(kv => kv.Key.EndsWith(".FluentBuilders.g.cs", StringComparison.Ordinal)).Value; } - // ============================================================ // FB-3 phase 3: per-ObjectType IComponentAccessor/IPropertyAccessor // extensions emitted into the same FluentBuilders file. - // ============================================================ [Test] public void EmittedFluentBuilders_HasPerObjectTypeComponentAccessorClass() @@ -595,12 +594,14 @@ private static Dictionary GenerateForDeclarationBackedNodeSet() string path = Path.Combine(resources, nodeSetFile); var nodesets = new NodesetFileCollection( - [(path, new NodesetFileOptions + [ + (path, new NodesetFileOptions { ModelUri = namespaceUri, Name = "DeclarationBackedMethod", Prefix = "DeclarationBackedMethod" - })], + }) + ], [], fileSystem, telemetry); diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/NodeStateGeneratorTests.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/NodeStateGeneratorTests.cs index cef387d08b..d35cece29c 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/NodeStateGeneratorTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/NodeStateGeneratorTests.cs @@ -37,6 +37,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -834,6 +835,308 @@ public void ConcreteInstanceAnalogChildren_AreMaterializedUnconditionally() } } + /// + /// Verifies DataTypeEncoding objects remain independent predefined + /// nodes when a NodeSet exporter authors a same-namespace + /// ParentNodeId pointing at the owning DataType. Absorbing the + /// encoding into the DataType child collection drops it from the + /// generated NodeManager and leaves the server TypeTree without the + /// encoding-to-DataType relationship required for structured values. + /// + [Test] + public void SameNamespaceEncodingNodesAreEmittedAsPredefinedNodes() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(logLevel: LogLevel.Error); + + Dictionary files = GenerateFromNodeSet( + "SameNamespaceEncoding.NodeSet2.xml", + telemetry); + + string code = files.Single( + kv => kv.Key.EndsWith(".NodeStates.ex.g.cs", StringComparison.Ordinal)).Value; + + Assert.Multiple(() => + { + Assert.That( + code, + Does.Contain("CreateMyDataType_Encoding_DefaultBinary"), + "The Default Binary encoding factory must be emitted."); + Assert.That( + code, + Does.Contain("CreateMyDataType_Encoding_Default_JSON"), + "The Default JSON encoding factory must be emitted."); + Assert.That( + code, + Does.Contain( + "NodeState state = CreateMyDataType_Encoding_DefaultBinary(context);"), + "The Default Binary encoding must be registered as a predefined node."); + Assert.That( + code, + Does.Contain( + "NodeState state = CreateMyDataType_Encoding_Default_JSON(context);"), + "The Default JSON encoding must be registered as a predefined node."); + }); + } + + [Test] + public void MethodArgumentDataTypesUseRuntimeNamespaceTable() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(logLevel: LogLevel.Error); + + Dictionary files = GenerateFromNodeSet( + "MethodArgumentNamespace.NodeSet2.xml", + telemetry); + + string code = files.Single( + kv => kv.Key.EndsWith(".NodeStates.ex.g.cs", StringComparison.Ordinal)).Value; + string factory = ExtractMethodBody( + code, + "CreateMyObjectType_DoWork_InputArguments"); + + Assert.Multiple(() => + { + Assert.That( + factory, + Does.Contain( + "DataType = global::Opc.Ua.NodeId.Create(100u, " + + "global::MethodArguments.Namespaces.MethodArguments, " + + "context.NamespaceUris)")); + Assert.That(factory, Does.Not.Contain("Variant.FromXml")); + }); + } + + [Test] + public void SameNamedMethodArgumentsUseDistinctSymbolsAndPreserveRuntimeNames() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(logLevel: LogLevel.Error); + + Dictionary files = GenerateFromNodeSet( + "SameNamedMethodArguments.NodeSet2.xml", + telemetry); + Dictionary repeatedFiles = GenerateFromNodeSet( + "SameNamedMethodArguments.NodeSet2.xml", + telemetry); + + string nodeStates = files.Single( + kv => kv.Key.EndsWith(".NodeStates.ex.g.cs", StringComparison.Ordinal)).Value; + string nodeStateClasses = files.Single( + kv => kv.Key.EndsWith(".NodeStates.g.cs", StringComparison.Ordinal)).Value; + string proxies = files.Single( + kv => kv.Key.EndsWith(".TypeProxies.g.cs", StringComparison.Ordinal)).Value; + string generatedCode = string.Join("\n", files.Values); + string inputArgumentsFactory = ExtractMethodBody( + nodeStates, + "CreateRoundTripMethodType_InputArguments"); + string outputArgumentsFactory = ExtractMethodBody( + nodeStates, + "CreateRoundTripMethodType_OutputArguments"); + string localCollisionInputFactory = ExtractMethodBody( + nodeStates, + "CreateLocalCollisionMethodType_InputArguments"); + string localCollisionOutputFactory = ExtractMethodBody( + nodeStates, + "CreateLocalCollisionMethodType_OutputArguments"); + string[] inputRuntimeNames = ExtractStringLiteralValues(inputArgumentsFactory); + string[] outputRuntimeNames = ExtractStringLiteralValues(outputArgumentsFactory); + string[] localCollisionInputNames = + ExtractStringLiteralValues(localCollisionInputFactory); + string[] localCollisionOutputNames = + ExtractStringLiteralValues(localCollisionOutputFactory); + string[] proxyRuntimeStrings = ExtractStringLiteralValues(proxies); + string[] expectedInputNames = + [ + "Foo", + "foo", + "Class", + "VersionId", + "Await", + "Ct", + "cT", + "CancellationToken", + "Context", + "ObjectId", + "Method", + "InputArguments", + "Results", + "_result", + "_foo", + "_", + "Nameof", + "__arglist", + "__makeref", + "__reftype", + "__refvalue" + ]; + string[] expectedOutputNames = + [ + "VersionId", + "class", + "Changed", + "OutputArguments", + "ServiceResult", + "Quote\"Name", + "Back\\Slash", + "Line\nBreak", + "Δelta雪", + "Foo", + "RoundTripMethodStateResult", + "Next\u0085Line", + "Line\u2028Separator", + "Paragraph\u2029Separator" + ]; + + Assert.Multiple(() => + { + Assert.That(generatedCode, Does.Contain("string foo,")); + Assert.That(generatedCode, Does.Contain("string foo2,")); + Assert.That(generatedCode, Does.Contain("string @class,")); + Assert.That(generatedCode, Does.Contain("string versionId,")); + Assert.That(generatedCode, Does.Contain("string await2,")); + Assert.That(generatedCode, Does.Contain("string ct2,")); + Assert.That(generatedCode, Does.Contain("string cT3,")); + Assert.That(generatedCode, Does.Contain("string cancellationToken2,")); + Assert.That(generatedCode, Does.Contain("string context2,")); + Assert.That(generatedCode, Does.Contain("string objectId2,")); + Assert.That(generatedCode, Does.Contain("string method2,")); + Assert.That(generatedCode, Does.Contain("string inputArguments2,")); + Assert.That(generatedCode, Does.Contain("string results2,")); + Assert.That(nodeStateClasses, Does.Contain("string _result2,")); + Assert.That(nodeStateClasses, Does.Contain("string _foo,")); + Assert.That(nodeStateClasses, Does.Contain("string _2,")); + Assert.That(nodeStateClasses, Does.Contain("string nameof,")); + Assert.That(nodeStateClasses, Does.Contain("string @__arglist,")); + Assert.That(nodeStateClasses, Does.Contain("string @__makeref,")); + Assert.That(nodeStateClasses, Does.Contain("string @__reftype,")); + Assert.That(nodeStateClasses, Does.Contain("string @__refvalue,")); + Assert.That(generatedCode, Does.Contain("ref string versionIdOut")); + Assert.That(generatedCode, Does.Contain("ref string classOut")); + Assert.That(generatedCode, Does.Contain("ref string outputArgumentsOut")); + Assert.That(generatedCode, Does.Contain("ref string serviceResultOut")); + Assert.That(nodeStateClasses, Does.Contain("ref string fooOut")); + Assert.That(nodeStateClasses, + Does.Contain("ref string roundTripMethodStateResultOut")); + Assert.That(generatedCode, Does.Contain( + "public string VersionIdOut { get; set; }")); + Assert.That(generatedCode, Does.Contain( + "public string ClassOut { get; set; }")); + Assert.That(generatedCode, Does.Contain( + "public string OutputArgumentsOut { get; set; }")); + Assert.That(generatedCode, Does.Contain( + "public string ServiceResultOut { get; set; }")); + Assert.That(nodeStateClasses, Does.Contain( + "public string RoundTripMethodStateResultOut { get; set; }")); + Assert.That(generatedCode, Does.Contain("\\u0085")); + Assert.That(generatedCode, Does.Contain("\\u2028")); + Assert.That(generatedCode, Does.Contain("\\u2029")); + Assert.That(generatedCode, Does.Not.Contain("string await,")); + Assert.That(generatedCode, Does.Not.Contain("string cancellationToken,")); + Assert.That(inputArgumentsFactory, + Does.Not.Contain("Name = \"foo2\"")); + Assert.That(inputArgumentsFactory, + Does.Not.Contain("Name = \"@class\"")); + Assert.That(outputArgumentsFactory, + Does.Not.Contain("Name = \"VersionIdOut\"")); + Assert.That(outputArgumentsFactory, + Does.Not.Contain("Name = \"classOut\"")); + foreach (string expectedName in expectedInputNames) + { + Assert.That(inputRuntimeNames, Does.Contain(expectedName)); + } + foreach (string expectedName in expectedOutputNames) + { + Assert.That(outputRuntimeNames, Does.Contain(expectedName)); + } + Assert.That(localCollisionInputNames, Does.Contain("_foo")); + Assert.That(localCollisionOutputNames, Does.Contain("Foo")); + + Assert.That(proxies, Does.Contain("ValueTask<(")); + Assert.That(proxies, Does.Contain( + "ValueTask LocalCollisionAsync(")); + Assert.That(proxies, Does.Contain( + "TryGetValue(out string _fooOut)")); + Assert.That(proxies, Does.Contain("return _fooOut;")); + Assert.That(proxies, Does.Contain("string versionIdOut")); + Assert.That(proxies, Does.Contain("string classOut")); + Assert.That(proxies, Does.Contain("bool changed")); + Assert.That(proxies, Does.Contain("string outputArgumentsOut")); + Assert.That(proxies, Does.Contain("string serviceResultOut")); + Assert.That(proxies, Does.Contain("string quote_Name")); + Assert.That(proxies, Does.Contain("string back_Slash")); + Assert.That(proxies, Does.Contain("string lineBreak")); + Assert.That(proxies, Does.Contain("string δelta雪")); + Assert.That(proxies, Does.Contain("string fooOut")); + Assert.That(proxies, + Does.Contain("string roundTripMethodStateResult")); + Assert.That(proxies, Does.Contain("string nextLine")); + Assert.That(proxies, Does.Contain("string lineSeparator")); + Assert.That(proxies, Does.Contain("string paragraphSeparator")); + Assert.That(proxies, Does.Contain("string foo,")); + Assert.That(proxies, Does.Contain("string foo2,")); + Assert.That(proxies, Does.Contain("string @class,")); + Assert.That(proxies, Does.Contain("string versionId,")); + Assert.That(proxies, Does.Contain("string await2,")); + Assert.That(proxies, Does.Contain("string ct2,")); + Assert.That(proxies, Does.Contain("string cT3,")); + Assert.That(proxies, Does.Contain("string cancellationToken2,")); + Assert.That(proxies, Does.Contain("string context2,")); + Assert.That(proxies, Does.Contain("string objectId2,")); + Assert.That(proxies, Does.Contain("string method2,")); + Assert.That(proxies, Does.Contain("string inputArguments2,")); + Assert.That(proxies, Does.Contain("string results2,")); + Assert.That(proxies, Does.Contain("string _result,")); + Assert.That(proxies, Does.Contain("string _foo,")); + Assert.That(proxies, Does.Contain("string _2,")); + Assert.That(proxies, Does.Contain("string nameof2,")); + Assert.That(proxies, Does.Contain("string @__arglist,")); + Assert.That(proxies, Does.Contain("string @__makeref,")); + Assert.That(proxies, Does.Contain("string @__reftype,")); + Assert.That(proxies, Does.Contain("string @__refvalue,")); + Assert.That(proxies, Does.Contain("")); + Assert.That(proxies, Does.Not.Contain("")); + Assert.That(proxies, Does.Contain("output 'VersionId'.")); + Assert.That(proxies, Does.Contain("output 'class'.")); + foreach (string expectedName in expectedOutputNames) + { + Assert.That( + proxyRuntimeStrings, + Does.Contain( + $"Method 'RoundTrip' returned an unexpected value " + + $"for output '{expectedName}'.")); + } + + Assert.That(repeatedFiles.Keys, Is.EquivalentTo(files.Keys)); + foreach (string key in files.Keys) + { + Assert.That(repeatedFiles[key], Is.EqualTo(files[key]), key); + } + }); + + using var peStream = new MemoryStream(); + bool success = OptimizationLevel.Debug + .CreateCompilation() + .AddCode( + files.WithOpcUaGeneratedStack(), + LanguageVersion.Latest) + .Emit(peStream) + .Check(TestContext.Out, out int errorCount, out int warnCount); + + Assert.That(success, Is.True, + $"Generated code should compile. Errors: {errorCount}, Warnings: {warnCount}"); + } + + private static string[] ExtractStringLiteralValues(string source) + { + return + [ + .. CSharpSyntaxTree.ParseText(source) + .GetRoot() + .DescendantNodes() + .OfType() + .Where(literal => literal.IsKind(SyntaxKind.StringLiteralExpression)) + .Select(literal => literal.Token.ValueText) + ]; + } + private static Dictionary GenerateFromNodeSet( string nodeSetResource, ITelemetryContext telemetry) diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/ObjectTypeProxyGeneratorTests.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/ObjectTypeProxyGeneratorTests.cs index 6bd3717291..7841400756 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/ObjectTypeProxyGeneratorTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Generators/ObjectTypeProxyGeneratorTests.cs @@ -349,6 +349,35 @@ public void Emit_StructureInput_UsesFromStructureForBoxing() Does.Contain("global::Opc.Ua.Variant.FromStructure(payload)")); } + [Test] + public void StructureOutputUsesSessionMessageContext() + { + MethodDesign method = CreateMethod( + "Read", + outputs: + [ + CreateParameter( + "payload", + BasicDataType.UserDefined, + symbolicName: "MyStruct") + ]); + ObjectTypeDesign objectType = CreateObjectType("FooType", method); + m_mockModelDesign.Setup(m => m.GetNodeDesigns()).Returns([objectType]); + + using var stream = new MemoryStream(); + m_mockFileSystem + .Setup(fs => fs.OpenWrite(It.IsAny())) + .Returns(stream); + + new ObjectTypeProxyGenerator(CreateContext()).Emit(); + string content = Encoding.UTF8.GetString(stream.ToArray()); + + Assert.That( + content, + Does.Contain( + "TryGetValue(out global::Test.MyStruct _payload, base.Session.MessageContext)")); + } + [Test] public void Emit_ObjectTypeWithBaseType_EmitsInheritance() { diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/MethodArgumentNamespace.NodeSet2.xml b/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/MethodArgumentNamespace.NodeSet2.xml new file mode 100644 index 0000000000..5f618058ee --- /dev/null +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/MethodArgumentNamespace.NodeSet2.xml @@ -0,0 +1,94 @@ + + + + http://test.org/UA/MethodArguments/ + + + + + + + + i=38 + i=37 + i=40 + i=45 + i=46 + i=47 + + + + MyDataType + + i=22 + ns=1;i=101 + + + + + + + + Default Binary + + i=76 + ns=1;i=100 + + + + + MyObjectType + + i=58 + ns=1;i=201 + + + + + DoWork + + i=78 + ns=1;i=200 + ns=1;i=202 + + + + + InputArguments + + i=78 + i=68 + ns=1;i=201 + + + + + + i=297 + + + + Options + + ns=1;i=100 + + -1 + + + Structured method options. + + + + + + + + diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/SameNamedMethodArguments.NodeSet2.xml b/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/SameNamedMethodArguments.NodeSet2.xml new file mode 100644 index 0000000000..79984b2a28 --- /dev/null +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/SameNamedMethodArguments.NodeSet2.xml @@ -0,0 +1,812 @@ + + + + http://test.org/UA/SameNamedMethodArguments/ + + + + + + + + i=1 + i=12 + i=37 + i=40 + i=45 + i=46 + i=47 + + + + VersionedObjectType + + i=58 + ns=1;i=1004 + ns=1;i=1013 + + + + + RoundTripMethodType + + ns=1;i=1002 + ns=1;i=1003 + + + + + InputArguments + + i=78 + i=68 + ns=1;i=1001 + + + + + + i=297 + + + + Foo + + i=12 + + -1 + + + The first canonical-name input. + + + + + + + i=297 + + + + foo + + i=12 + + -1 + + + The colliding canonical-name input. + + + + + + + i=297 + + + + Class + + i=12 + + -1 + + + A C# keyword after canonical casing. + + + + + + + i=297 + + + + VersionId + + i=12 + + -1 + + + The requested version identifier. + + + + + + + i=297 + + + + Await + + i=12 + + -1 + + + A contextual C# keyword in asynchronous code. + + + + + + + i=297 + + + + Ct + + i=12 + + -1 + + + A proxy cancellation-token collision. + + + + + + + i=297 + + + + cT + + i=12 + + -1 + + + A mixed-case reserved-name collision. + + + + + + + i=297 + + + + CancellationToken + + i=12 + + -1 + + + A NodeState cancellation-token collision. + + + + + + + i=297 + + + + Context + + i=12 + + -1 + + + A generated-handler context collision. + + + + + + + i=297 + + + + ObjectId + + i=12 + + -1 + + + A generated-handler object identifier collision. + + + + + + + i=297 + + + + Method + + i=12 + + -1 + + + A generated-handler method collision. + + + + + + + i=297 + + + + InputArguments + + i=12 + + -1 + + + A generated input-collection collision. + + + + + + + i=297 + + + + Results + + i=12 + + -1 + + + A generated result-collection collision. + + + + + + + i=297 + + + + _result + + i=12 + + -1 + + + A NodeState result-local collision. + + + + + + + i=297 + + + + _foo + + i=12 + + -1 + + + A proxy output-local collision. + + + + + + + i=297 + + + + _ + + i=12 + + -1 + + + A single-underscore identifier. + + + + + + + i=297 + + + + Nameof + + i=12 + + -1 + + + A contextual operator identifier. + + + + + + + i=297 + + + + __arglist + + i=12 + + -1 + + + A special C# keyword identifier. + + + + + + + i=297 + + + + __makeref + + i=12 + + -1 + + + A special C# keyword identifier. + + + + + + + i=297 + + + + __reftype + + i=12 + + -1 + + + A special C# keyword identifier. + + + + + + + i=297 + + + + __refvalue + + i=12 + + -1 + + + A special C# keyword identifier. + + + + + + + + + + OutputArguments + + i=78 + i=68 + ns=1;i=1001 + + + + + + i=297 + + + + VersionId + + i=12 + + -1 + + + The effective version identifier. + + + + + + + i=297 + + + + class + + i=12 + + -1 + + + The keyword-colliding output. + + + + + + + i=297 + + + + Changed + + i=1 + + -1 + + + Whether the effective version changed. + + + + + + + i=297 + + + + OutputArguments + + i=12 + + -1 + + + A generated output-collection collision. + + + + + + + i=297 + + + + ServiceResult + + i=12 + + -1 + + + A generated result-member collision. + + + + + + + i=297 + + + + Quote"Name + + i=12 + + -1 + + + An authored name containing a quotation mark. + + + + + + + i=297 + + + + Back\Slash + + i=12 + + -1 + + + An authored name containing a backslash. + + + + + + + i=297 + + + + Line Break + + i=12 + + -1 + + + An authored name containing a newline. + + + + + + + i=297 + + + + Δelta雪 + + i=12 + + -1 + + + An authored name containing Unicode letters. + + + + + + + i=297 + + + + Foo + + i=12 + + -1 + + + An output whose local collides with input _foo. + + + + + + + i=297 + + + + RoundTripMethodStateResult + + i=12 + + -1 + + + An output matching its generated result type. + + + + + + + i=297 + + + + Next…Line + + i=12 + + -1 + + + An authored name containing next-line. + + + + + + + i=297 + + + + Line
Separator + + i=12 + + -1 + + + An authored name containing a line separator. + + + + + + + i=297 + + + + Paragraph
Separator + + i=12 + + -1 + + + An authored name containing a paragraph separator. + + + + + + + + + + RoundTrip + + i=78 + ns=1;i=1000 + + + + + LocalCollisionMethodType + + ns=1;i=1011 + ns=1;i=1012 + + + + + InputArguments + + i=78 + i=68 + ns=1;i=1010 + + + + + + i=297 + + + + _foo + + i=12 + + -1 + + + The underscore-prefixed input. + + + + + + + + + + OutputArguments + + i=78 + i=68 + ns=1;i=1010 + + + + + + i=297 + + + + Foo + + i=12 + + -1 + + + The output whose emitted local would be _foo. + + + + + + + + + + LocalCollision + + i=78 + ns=1;i=1000 + + + diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/SameNamespaceEncoding.NodeSet2.xml b/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/SameNamespaceEncoding.NodeSet2.xml new file mode 100644 index 0000000000..50ed0995d2 --- /dev/null +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Resources/SameNamespaceEncoding.NodeSet2.xml @@ -0,0 +1,59 @@ + + + + + http://test.org/UA/SameNamespaceEncoding/ + + + + + + + + i=38 + i=45 + i=40 + + + + MyDataType + + i=22 + ns=1;i=200 + ns=1;i=201 + + + + + + + + Default Binary + + i=76 + ns=1;i=100 + + + + + Default JSON + + i=76 + ns=1;i=100 + + + diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/ModelDesignExtensionsTests.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/ModelDesignExtensionsTests.cs index a33928a02b..4cdea2c826 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/ModelDesignExtensionsTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/ModelDesignExtensionsTests.cs @@ -29,6 +29,11 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; using System.Xml; using Moq; using NUnit.Framework; @@ -46,6 +51,207 @@ namespace Opc.Ua.Schema.Model.Tests [Parallelizable] public class ModelDesignExtensionsTests { + [Test] + public void AssignMethodArgumentCodeNamesAllocatesInputsBeforeOutputs() + { + var method = new MethodDesign + { + InputArguments = + [ + new Parameter { Name = "Foo" }, + new Parameter { Name = "foo" }, + new Parameter { Name = "Class" }, + new Parameter { Name = "VersionId" } + ], + OutputArguments = + [ + new Parameter { Name = "VersionId" }, + new Parameter { Name = "class" }, + new Parameter { Name = "Changed" } + ] + }; + + method.AssignMethodArgumentCodeNames(); + string[] firstInputs = + [.. method.InputArguments.Select(p => p.GetGeneratedCodeIdentifier())]; + string[] firstOutputs = + [.. method.OutputArguments.Select(p => p.GetGeneratedCodeIdentifier())]; + string[] expectedInputs = ["foo", "foo2", "@class", "versionId"]; + string[] expectedOutputs = ["versionIdOut", "classOut", "changed"]; + + method.AssignMethodArgumentCodeNames(); + + Assert.Multiple(() => + { + Assert.That(firstInputs, Is.EqualTo(expectedInputs)); + Assert.That(firstOutputs, Is.EqualTo(expectedOutputs)); + Assert.That( + method.InputArguments.Select(p => p.GetGeneratedCodeIdentifier()), + Is.EqualTo(firstInputs)); + Assert.That( + method.OutputArguments.Select(p => p.GetGeneratedCodeIdentifier()), + Is.EqualTo(firstOutputs)); + }); + } + + [Test] + public void AssignMethodArgumentCodeNamesUsesIndependentReservedScopes() + { + Parameter[] inputs = + [ + new Parameter { Name = "Await" }, + new Parameter { Name = "Ct" }, + new Parameter { Name = "cT" }, + new Parameter { Name = "CancellationToken" } + ]; + Parameter[] outputs = + [ + new Parameter { Name = "OutputArguments" }, + new Parameter { Name = "ServiceResult" } + ]; + var nodeStateScope = new MethodArgumentCodeNameScope( + "await", + "cancellationToken", + "outputArguments", + "serviceResult"); + var proxyScope = new MethodArgumentCodeNameScope( + "await", + "ct", + "outputArguments"); + + ModelDesignExtensions.AssignMethodArgumentCodeNames( + inputs, + outputs, + nodeStateScope); + string[] nodeStateInputs = + [.. inputs.Select(p => p.GetGeneratedCodeIdentifier(scope: nodeStateScope))]; + string[] nodeStateOutputs = + [.. outputs.Select(p => p.GetGeneratedCodeIdentifier(scope: nodeStateScope))]; + string[] expectedNodeStateInputs = + ["await2", "ct", "cT2", "cancellationToken2"]; + string[] expectedNodeStateOutputs = + ["outputArgumentsOut", "serviceResultOut"]; + string[] expectedProxyInputs = + ["await2", "ct2", "cT3", "cancellationToken"]; + string[] expectedProxyOutputs = + ["outputArgumentsOut", "serviceResult"]; + + ModelDesignExtensions.AssignMethodArgumentCodeNames( + inputs, + outputs, + proxyScope); + + Assert.Multiple(() => + { + Assert.That( + nodeStateInputs, + Is.EqualTo(expectedNodeStateInputs)); + Assert.That( + nodeStateOutputs, + Is.EqualTo(expectedNodeStateOutputs)); + Assert.That( + inputs.Select(p => p.GetGeneratedCodeIdentifier(scope: proxyScope)), + Is.EqualTo(expectedProxyInputs)); + Assert.That( + outputs.Select(p => p.GetGeneratedCodeIdentifier(scope: proxyScope)), + Is.EqualTo(expectedProxyOutputs)); + Assert.That( + inputs.Select(p => p.GetGeneratedCodeIdentifier(scope: nodeStateScope)), + Is.EqualTo(nodeStateInputs)); + Assert.That( + outputs.Select(p => p.GetGeneratedCodeIdentifier(scope: nodeStateScope)), + Is.EqualTo(nodeStateOutputs)); + }); + } + + [Test] + public void AssignMethodArgumentCodeNamesReservesEmittedAliasesAndAdditionalNames() + { + Parameter[] inputs = + [ + new Parameter { Name = "_result" }, + new Parameter { Name = "_foo" }, + new Parameter { Name = "_" }, + new Parameter { Name = "Nameof" } + ]; + Parameter[] outputs = + [ + new Parameter { Name = "Foo" }, + new Parameter { Name = "RoundTripMethodStateResult" } + ]; + var scope = new MethodArgumentCodeNameScope( + (identifier, output) => output ? + new[] { identifier, "_" + identifier.TrimStart('@') } : + new[] { identifier }, + "_", + "_result", + "nameof"); + string[] expectedInputs = ["_result2", "_foo", "_2", "nameof2"]; + string[] expectedOutputs = ["fooOut", "roundTripMethodStateResultOut"]; + + ModelDesignExtensions.AssignMethodArgumentCodeNames( + inputs, + outputs, + scope, + "RoundTripMethodStateResult"); + + Assert.Multiple(() => + { + Assert.That( + inputs.Select(p => p.GetGeneratedCodeIdentifier(scope: scope)), + Is.EqualTo(expectedInputs)); + Assert.That( + outputs.Select(p => p.GetGeneratedCodeIdentifier(scope: scope)), + Is.EqualTo(expectedOutputs)); + }); + } + + [Test] + public void GeneratedMethodArgumentNamesAreExternalToSerialization() + { + var method = new MethodDesign + { + InputArguments = + [ + new Parameter { Name = "Foo" }, + new Parameter { Name = "foo" } + ] + }; + method.AssignMethodArgumentCodeNames(); + Parameter parameter = method.InputArguments[1]; + Assert.That(parameter.GetGeneratedCodeIdentifier(), Is.EqualTo("foo2")); + + MemberInfo[] serializableMembers; +#pragma warning disable SYSLIB0050 // Validate the BinaryFormatter member contract without serializing. + serializableMembers = FormatterServices.GetSerializableMembers(typeof(Parameter)); +#pragma warning restore SYSLIB0050 + + var serializer = new DataContractSerializer(typeof(Parameter)); + using var stream = new MemoryStream(); + serializer.WriteObject(stream, parameter); + string serialized = Encoding.UTF8.GetString(stream.ToArray()); + + Assert.Multiple(() => + { + Assert.That( + typeof(Parameter).GetMember( + "GeneratedCodeName", + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic), + Is.Empty); + Assert.That( + serializableMembers.Any( + member => member.Name.Contains( + "GeneratedCode", + StringComparison.Ordinal)), + Is.False); + Assert.That(serialized, Does.Not.Contain("GeneratedCode")); + Assert.That(serialized, Does.Contain("foo")); + Assert.That(serialized, Does.Not.Contain("foo2")); + }); + } + /// /// Tests that DetermineBasicDataType returns BaseDataType when dataType is null. /// diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs index 291c8b9a4a..6a4af48b32 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs @@ -36,6 +36,7 @@ using Microsoft.Extensions.Logging; using NUnit.Framework; using Opc.Ua.Export; +using Opc.Ua.SourceGeneration; using Opc.Ua.Tests; namespace Opc.Ua.Schema.Model.Tests @@ -55,7 +56,11 @@ public class NodeSetToModelDesignTests { private const string OpcUaNamespaceUri = "http://opcfoundation.org/UA/"; private const string CrossModelNamespaceUri = "http://test.org/UA/CrossModel/Types"; + private const string SameNamedArgumentsNamespaceUri = + "http://test.org/UA/SameNamedMethodArguments/"; private const string NodeSetResource = "CrossModelTypes.NodeSet2.xml"; + private const string SameNamedArgumentsResource = + "SameNamedMethodArguments.NodeSet2.xml"; private const string DesignResource = "TestDataDesign.xml"; private VirtualFileSystem m_fileSystem; @@ -205,6 +210,87 @@ public void ImportWithoutBaseModelThrowsInvalidDataException() Assert.That(ex.Message, Does.Contain("WidgetType")); } + [Test] + public void ImportPreservesSameNamedInputAndOutputArgumentNames() + { + ITelemetryContext telemetry = CreateTelemetry(); + string path = ResourcePath(SameNamedArgumentsResource); + var nodesets = new NodesetFileCollection( + [(path, new NodesetFileOptions())], + [], + m_fileSystem, + telemetry); + List designFiles = nodesets.GetDesignFileListForModel( + SameNamedArgumentsNamespaceUri, + out _); + Assert.That(designFiles, Is.Not.Null); + + IFileSystem fileSystem = typeof(Generators).Assembly + .AsFileSystem("Opc.Ua.SourceGeneration.Design") + .WithFallback(m_fileSystem); + IModelDesign model = fileSystem.OpenModelDesign( + new DesignFileCollection { Targets = designFiles }, + [], + telemetry, + useAllowSubtypes: false); + MethodDesign method = model.GetNodeDesigns() + .OfType() + .Single(x => x.SymbolicName?.Name == "RoundTripMethodType"); + string[] expectedInputNames = + [ + "Foo", + "foo", + "Class", + "VersionId", + "Await", + "Ct", + "cT", + "CancellationToken", + "Context", + "ObjectId", + "Method", + "InputArguments", + "Results", + "_result", + "_foo", + "_", + "Nameof", + "__arglist", + "__makeref", + "__reftype", + "__refvalue" + ]; + string[] expectedOutputNames = + [ + "VersionId", + "class", + "Changed", + "OutputArguments", + "ServiceResult", + "Quote\"Name", + "Back\\Slash", + "Line\nBreak", + "Δelta雪", + "Foo", + "RoundTripMethodStateResult", + "Next\u0085Line", + "Line\u2028Separator", + "Paragraph\u2029Separator" + ]; + + Assert.Multiple(() => + { + Assert.That(method.InputArguments, Has.Length.EqualTo(expectedInputNames.Length)); + Assert.That( + method.InputArguments.Select(argument => argument.Name), + Is.EqualTo(expectedInputNames)); + Assert.That(method.OutputArguments, Has.Length.EqualTo(expectedOutputNames.Length)); + Assert.That( + method.OutputArguments.Select(argument => argument.Name), + Is.EqualTo(expectedOutputNames)); + }); + } + [Test] public void TypeSymbolicNameUsesNodeIdNamespace() { @@ -238,7 +324,7 @@ public void InstanceSymbolicNameKeepsBrowseNameNamespace() new UAVariable(), symbolicId, symbolicName); - + Assert.That(normalized, Is.SameAs(symbolicName)); Assert.That(normalized, Is.SameAs(symbolicName)); } diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Shared/SourceGenerationUtilsTests.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/Shared/SourceGenerationUtilsTests.cs index 986f07e608..01f7ebac11 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/Shared/SourceGenerationUtilsTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Shared/SourceGenerationUtilsTests.cs @@ -43,6 +43,37 @@ namespace Opc.Ua.SourceGeneration.Shared.Tests [Parallelizable] public class SourceGenerationUtilsTests { + [TestCase("Foo", false, "foo")] + [TestCase("foo", false, "foo")] + [TestCase("Class", false, "@class")] + [TestCase("@class", false, "@class")] + [TestCase("some-value", false, "some_value")] + [TestCase("1 Value", false, "_1value")] + [TestCase("Class", true, "Class")] + [TestCase("__arglist", false, "@__arglist")] + [TestCase("__makeref", false, "@__makeref")] + [TestCase("__reftype", false, "@__reftype")] + [TestCase("__refvalue", false, "@__refvalue")] + public void ToCSharpIdentifierCanonicalizesAndEscapes( + string input, + bool upperCamelCase, + string expected) + { + Assert.That( + input.ToCSharpIdentifier(upperCamelCase), + Is.EqualTo(expected)); + } + + [Test] + public void AsStringLiteralEscapesUnicodeLineSeparators() + { + const string value = "Next\u0085Line\u2028Paragraph\u2029End"; + + Assert.That( + value.AsStringLiteral(), + Is.EqualTo("\"Next\\u0085Line\\u2028Paragraph\\u2029End\"")); + } + /// /// Tests that IsNull returns the expected result for various XmlQualifiedName inputs. /// diff --git a/tools/Opc.Ua.SourceGeneration.Core/Generators/FluentBuilderGenerator.cs b/tools/Opc.Ua.SourceGeneration.Core/Generators/FluentBuilderGenerator.cs index 09ba25146e..ff61763709 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Generators/FluentBuilderGenerator.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Generators/FluentBuilderGenerator.cs @@ -475,14 +475,16 @@ private void RegisterMethodWrapper( MethodDesign effectiveMethod = method.IsOverridden() ? (MethodDesign)method.GetMergedInstance() : method; + (Parameter[] inputs, Parameter[] outputs) = + MethodDesignArgumentResolver.ResolveMethodArguments(effectiveMethod); m_methodWrappers[key] = new MethodWrapper { Key = key, ClassName = className, LeafName = leafName, ParentKey = parentKey, - Inputs = MethodDesignArgumentResolver.ResolveMethodInputs(effectiveMethod), - Outputs = MethodDesignArgumentResolver.ResolveMethodOutputs(effectiveMethod) + Inputs = inputs, + Outputs = outputs }; } @@ -679,6 +681,15 @@ private void EmitTypedManagerImpl( "global::Opc.Ua.NodeId typeDefinitionId, global::Opc.Ua.QualifiedName browseName", "typeDefinitionId, browseName", typeArg: "TValue", noConstraint: true); + EmitPassThroughGenericMethod(writer, + "global::Opc.Ua.Server.Fluent.IVariableBuilder", "VariableFromDataTypeId", + "global::Opc.Ua.NodeId dataTypeId", "dataTypeId", + typeArg: "TValue", noConstraint: true); + EmitPassThroughGenericMethod(writer, + "global::Opc.Ua.Server.Fluent.IVariableBuilder", "VariableFromDataTypeId", + "global::Opc.Ua.NodeId dataTypeId, global::Opc.Ua.QualifiedName browseName", + "dataTypeId, browseName", + typeArg: "TValue", noConstraint: true); // Typed top-level accessors. foreach (InstanceDesign root in roots) @@ -1956,7 +1967,9 @@ private sealed class ChildAccessor /// public string WrapperClassName; - /// Key into m_wrappers for object children. + /// + /// Key into m_wrappers for object children. + /// public string ChildKey; /// diff --git a/tools/Opc.Ua.SourceGeneration.Core/Generators/MethodDesignArgumentResolver.cs b/tools/Opc.Ua.SourceGeneration.Core/Generators/MethodDesignArgumentResolver.cs index 4b04f84c67..5f1e2c9d29 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Generators/MethodDesignArgumentResolver.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Generators/MethodDesignArgumentResolver.cs @@ -44,25 +44,38 @@ internal static class MethodDesignArgumentResolver /// public static Parameter[] ResolveMethodInputs(MethodDesign method) { - if (method == null) - { - throw new ArgumentNullException(nameof(method)); - } - - return ResolveMethodDefinition(method).InputArguments ?? []; + return ResolveMethodArguments(method).Inputs; } /// /// Resolves the effective output arguments for a method design. /// public static Parameter[] ResolveMethodOutputs(MethodDesign method) + { + return ResolveMethodArguments(method).Outputs; + } + + /// + /// Resolves the effective input and output arguments for a method design. + /// + public static (Parameter[] Inputs, Parameter[] Outputs) ResolveMethodArguments( + MethodDesign method) { if (method == null) { throw new ArgumentNullException(nameof(method)); } - return ResolveMethodDefinition(method).OutputArguments ?? []; + MethodDesign definition = ResolveMethodDefinition(method); + Parameter[] inputs = ResolveArguments( + definition.InputArguments, + definition.MethodDeclarationNode?.InputArguments, + definition.MethodType?.InputArguments); + Parameter[] outputs = ResolveArguments( + definition.OutputArguments, + definition.MethodDeclarationNode?.OutputArguments, + definition.MethodType?.OutputArguments); + return (inputs, outputs); } /// @@ -239,6 +252,29 @@ private static bool DelegatesTo( targetDefinition.OutputArguments ?? []); } + private static Parameter[] ResolveArguments( + Parameter[] methodArguments, + Parameter[] declarationArguments, + Parameter[] methodTypeArguments) + { + if (methodArguments is { Length: > 0 }) + { + return methodArguments; + } + + if (declarationArguments is { Length: > 0 }) + { + return declarationArguments; + } + + if (methodTypeArguments is { Length: > 0 }) + { + return methodTypeArguments; + } + + return []; + } + private static bool HaveSameSignature( Parameter[] firstInputs, Parameter[] firstOutputs, diff --git a/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeManagerTemplates.cs b/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeManagerTemplates.cs index e692ac160c..82e63f55a4 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeManagerTemplates.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeManagerTemplates.cs @@ -112,7 +112,8 @@ public partial class {{Tokens.NodeManagerClassName}} : global::Opc.Ua.Server.Flu __nsIndex, __FindRootByBrowseName, __FindRootByNodeId, - __FindByTypeDefinitionId); + __FindByTypeDefinitionId, + __FindByDataTypeId); // Attach the FluentNodeManagerBase event-source registry // to the builder so Publish(...) extensions can resolve @@ -234,6 +235,48 @@ protected override void OnMonitoredItemCreated( } return __matches; } + + private global::Opc.Ua.ArrayOf __FindByDataTypeId( + global::Opc.Ua.NodeId dataTypeId) + { + if (dataTypeId == null || dataTypeId.IsNull) + { + return []; + } + + var __matches = new global::System.Collections.Generic.List(); + var __queue = new global::System.Collections.Generic.Queue(); + var __seen = new global::System.Collections.Generic.HashSet(); + var __scratch = new global::System.Collections.Generic.List(); + foreach (global::Opc.Ua.NodeState __root in PredefinedNodes.Values) + { + if (__root != null && __seen.Add(__root)) + { + __queue.Enqueue(__root); + } + } + while (__queue.Count > 0) + { + global::Opc.Ua.NodeState __current = __queue.Dequeue(); + if (__current is global::Opc.Ua.BaseVariableState __variable && + __variable.DataType == dataTypeId) + { + __matches.Add(__current); + } + __scratch.Clear(); + __current.GetChildren(SystemContext, __scratch); + for (int __i = 0; __i < __scratch.Count; __i++) + { + global::Opc.Ua.BaseInstanceState __child = __scratch[__i]; + if (__child != null && __seen.Add(__child)) + { + __queue.Enqueue(__child); + } + } + } + return new global::Opc.Ua.ArrayOf( + __matches.ToArray()); + } } } """); diff --git a/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs b/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs index 68adfc6578..ec97c4278a 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs @@ -660,7 +660,7 @@ private TemplateString LoadTemplate_ListOfInputArguments(ILoadContext context) { return null; } - string fieldName = field.GetChildFieldName()[2..]; + string fieldName = GetMethodArgumentIdentifier(field); string typeName = field.DataTypeNode.GetMethodArgumentTypeAsCode( field.ValueRank, m_context.ModelDesign.TargetNamespace.Value, @@ -671,7 +671,10 @@ private TemplateString LoadTemplate_ListOfInputArguments(ILoadContext context) { case BasicDataType.UserDefined: context.Out.WriteLine( - "_inputArguments[{2}].TryGetStructure(out {1} {0});", + "_inputArguments[{2}].TryGetValue(" + + "out {1} {0}, " + + "global::Opc.Ua.SystemContextExtensions.AsMessageContext(_context)" + + ");", fieldName, typeName, context.Index); @@ -700,7 +703,7 @@ private TemplateString LoadTemplate_ListOfOutputDeclarations(ILoadContext contex { return null; } - string fieldName = field.GetChildFieldName()[2..]; + string fieldName = GetMethodArgumentIdentifier(field); string typeName = field.DataTypeNode.GetMethodArgumentTypeAsCode( field.ValueRank, m_context.ModelDesign.TargetNamespace.Value, @@ -711,7 +714,10 @@ private TemplateString LoadTemplate_ListOfOutputDeclarations(ILoadContext contex { case BasicDataType.UserDefined: context.Out.WriteLine( - "_outputArguments[{2}].TryGetStructure(out {1} {0});", + "_outputArguments[{2}].TryGetValue(" + + "out {1} {0}, " + + "global::Opc.Ua.SystemContextExtensions.AsMessageContext(_context)" + + ");", fieldName, typeName, context.Index); @@ -740,7 +746,7 @@ private TemplateString LoadTemplate_ListOfOutputArguments(ILoadContext context) { return null; } - string fieldName = field.GetChildFieldName()[2..]; + string fieldName = GetMethodArgumentIdentifier(field); switch (field.DataTypeNode.BasicDataType) { case BasicDataType.UserDefined: @@ -771,8 +777,8 @@ private TemplateString LoadTemplate_OnCallDeclaration(ILoadContext context) { return null; } - Parameter[] inputArguments = MethodDesignArgumentResolver.ResolveMethodInputs(method); - Parameter[] outputArguments = MethodDesignArgumentResolver.ResolveMethodOutputs(method); + (Parameter[] inputArguments, Parameter[] outputArguments) = + MethodDesignArgumentResolver.ResolveMethodArguments(method); context.Out.WriteLine("global::Opc.Ua.ISystemContext _context,"); context.Out.WriteLine("global::Opc.Ua.MethodState _method,"); context.Out.Write("global::Opc.Ua.NodeId _objectId"); @@ -782,7 +788,7 @@ private TemplateString LoadTemplate_OnCallDeclaration(ILoadContext context) Parameter argument = inputArguments[ii]; context.Out.WriteLine(","); - context.Out.Write("{1} {0}", argument.GetChildFieldName()[2..], + context.Out.Write("{1} {0}", GetMethodArgumentIdentifier(argument), argument.DataTypeNode.GetMethodArgumentTypeAsCode( argument.ValueRank, m_context.ModelDesign.TargetNamespace.Value, @@ -795,7 +801,7 @@ private TemplateString LoadTemplate_OnCallDeclaration(ILoadContext context) Parameter argument = outputArguments[ii]; context.Out.WriteLine(","); - context.Out.Write("ref {1} {0}", argument.GetChildFieldName()[2..], + context.Out.Write("ref {1} {0}", GetMethodArgumentIdentifier(argument), argument.DataTypeNode.GetMethodArgumentTypeAsCode( argument.ValueRank, m_context.ModelDesign.TargetNamespace.Value, @@ -824,7 +830,7 @@ private TemplateString LoadTemplate_OnCallAsyncDeclaration(ILoadContext context) Parameter argument = inputArguments[ii]; context.Out.WriteLine(","); - context.Out.Write("{1} {0}", argument.GetChildFieldName()[2..], + context.Out.Write("{1} {0}", GetMethodArgumentIdentifier(argument), argument.DataTypeNode.GetMethodArgumentTypeAsCode( argument.ValueRank, m_context.ModelDesign.TargetNamespace.Value, @@ -846,7 +852,7 @@ private TemplateString LoadTemplate_ListOfOutputArgumentsFromResult(ILoadContext { return null; } - string fieldName = field.GetChildFieldName()[2..].ToUpperCamelCase(); + string fieldName = GetMethodArgumentIdentifier(field, upperCamelCase: true); switch (field.DataTypeNode.BasicDataType) { case BasicDataType.UserDefined: @@ -877,7 +883,7 @@ private TemplateString LoadTemplate_ListOfResultProperties(ILoadContext context) { return null; } - string fieldName = field.GetChildFieldName()[2..].ToUpperCamelCase(); + string fieldName = GetMethodArgumentIdentifier(field, upperCamelCase: true); context.Out.WriteLine( "public {1} {0} {{ get; set; }}", fieldName, @@ -896,8 +902,8 @@ private TemplateString LoadTemplate_OnCallImplementation(ILoadContext context) { return null; } - Parameter[] inputArguments = MethodDesignArgumentResolver.ResolveMethodInputs(method); - Parameter[] outputArguments = MethodDesignArgumentResolver.ResolveMethodOutputs(method); + (Parameter[] inputArguments, Parameter[] outputArguments) = + MethodDesignArgumentResolver.ResolveMethodArguments(method); context.Out.WriteLine("_result = OnCall("); context.Out.WriteLine(" _context,"); context.Out.WriteLine(" this,"); @@ -906,13 +912,13 @@ private TemplateString LoadTemplate_OnCallImplementation(ILoadContext context) for (int ii = 0; ii < inputArguments.Length; ii++) { context.Out.WriteLine(","); - context.Out.Write(" {0}", inputArguments[ii].GetChildFieldName()[2..]); + context.Out.Write(" {0}", GetMethodArgumentIdentifier(inputArguments[ii])); } for (int ii = 0; ii < outputArguments.Length; ii++) { context.Out.WriteLine(","); - context.Out.Write(" ref {0}", outputArguments[ii].GetChildFieldName()[2..]); + context.Out.Write(" ref {0}", GetMethodArgumentIdentifier(outputArguments[ii])); } context.Out.WriteLine(");"); @@ -935,7 +941,7 @@ private TemplateString LoadTemplate_OnCallAsyncImplementation(ILoadContext conte for (int ii = 0; ii < inputArguments.Length; ii++) { context.Out.WriteLine(","); - context.Out.Write(" {0}", inputArguments[ii].GetChildFieldName()[2..]); + context.Out.Write(" {0}", GetMethodArgumentIdentifier(inputArguments[ii])); } context.Out.WriteLine(","); @@ -2089,13 +2095,18 @@ private void AddNodeStateClassMethodTypeReplacements( IWriteContext context, MethodDesign method) { - Parameter[] inputArguments = MethodDesignArgumentResolver.ResolveMethodInputs(method); - Parameter[] outputArguments = MethodDesignArgumentResolver.ResolveMethodOutputs(method); + (Parameter[] inputArguments, Parameter[] outputArguments) = + MethodDesignArgumentResolver.ResolveMethodArguments(method); string targetNamespace = m_context.ModelDesign.TargetNamespace.Value; string declaredClassName = method.GetNodeStateClassName( targetNamespace, [], applyStandardFallback: false); + ModelDesignExtensions.AssignMethodArgumentCodeNames( + inputArguments, + outputArguments, + s_methodArgumentCodeNameScope, + declaredClassName + "Result"); // Declarations require an unqualified identifier, while fallback // tracking requires the fully-qualified name. Neither may apply // the reference-only fallback policy. @@ -2330,7 +2341,16 @@ private void AddVariableStateFactoryReplacements( return; } - if (node.DecodedValue is IList args) + Parameter[] methodArguments = GetMethodArgumentParameters(nodeToGenerate); + if (methodArguments != null) + { + context.Template.AddReplacement( + Tokens.ValueCode, + NodeStateTemplates.VariantArrayOfValue, + [methodArguments], + WriteTemplate_ArgumentCollection); + } + else if (node.DecodedValue is IList args) { context.Template.AddReplacement( Tokens.ValueCode, @@ -2365,6 +2385,26 @@ private void AddVariableStateFactoryReplacements( } } + private static Parameter[] GetMethodArgumentParameters(NodeToGenerate node) + { + if (node.Parent?.Design is not MethodDesign method) + { + return null; + } + + if (node.Design.SymbolicName == s_inputArgumentsBrowseName) + { + return MethodDesignArgumentResolver.ResolveMethodInputs(method); + } + + if (node.Design.SymbolicName == s_outputArgumentsBrowseName) + { + return MethodDesignArgumentResolver.ResolveMethodOutputs(method); + } + + return null; + } + private void AddReferenceTypeStateFactoryReplacements( IWriteContext context, ReferenceTypeDesign node) @@ -3661,6 +3701,9 @@ private static string GetDescriptionValue(NodeDesign node) { if (node.Description != null && !node.Description.IsAutogenerated) { + // The generated templates assign through a nodeState local that + // is statically typed as NodeState, so models with a child + // Property named "Description" cannot shadow this assignment. return CoreUtils.Format( "nodeState.Description = {0};", node.Description.GetLocalizedTextAsCode()); @@ -3940,6 +3983,15 @@ private record class ReferenceToGenerate( XmlQualifiedName ReferenceTypeId, bool IsInverse); + private static string GetMethodArgumentIdentifier( + Parameter parameter, + bool upperCamelCase = false) + { + return parameter.GetGeneratedCodeIdentifier( + upperCamelCase, + s_methodArgumentCodeNameScope); + } + private sealed record MethodArgumentsToGenerate( string PropertyName, string BrowseName, @@ -3947,6 +3999,36 @@ private sealed record MethodArgumentsToGenerate( private const string kNamespaceTableContextVariable = "context.NamespaceUris"; + private static readonly XmlQualifiedName s_inputArgumentsBrowseName = new( + "InputArguments", + Namespaces.OpcUa); + + private static readonly XmlQualifiedName s_outputArgumentsBrowseName = new( + "OutputArguments", + Namespaces.OpcUa); + + private static readonly MethodArgumentCodeNameScope s_methodArgumentCodeNameScope = new( + "_", + "await", + "ct", + "cancellationToken", + "context", + "_context", + "inputArguments", + "_inputArguments", + "method", + "_method", + "objectId", + "_objectId", + "onCall", + "onCallAsync", + "outputArguments", + "_outputArguments", + "_result", + "results", + "serviceResult", + "ServiceResult"); + private static readonly string[] s_builtInPropertyNames = [ "Description", @@ -3954,7 +4036,11 @@ private sealed record MethodArgumentsToGenerate( "Handle", "Specification", "Update", - "Delete" + "Delete", + // Method children whose generated accessor property shadows the + // identically-named global::Opc.Ua.NodeState.Validate(ISystemContext) + // instance method and therefore must be declared "public new". + "Validate" ]; private static readonly string[] s_builtInMethodNames = diff --git a/tools/Opc.Ua.SourceGeneration.Core/Generators/ObjectTypeProxyGenerator.cs b/tools/Opc.Ua.SourceGeneration.Core/Generators/ObjectTypeProxyGenerator.cs index 15b8251cfd..781f30890c 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Generators/ObjectTypeProxyGenerator.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Generators/ObjectTypeProxyGenerator.cs @@ -527,10 +527,12 @@ private TemplateString LoadTemplate_Method(ILoadContext context) MethodDesign effectiveMethod = method.IsOverridden() ? (MethodDesign)method.GetMergedInstance() : method; - Parameter[] inputs = - MethodDesignArgumentResolver.ResolveMethodInputs(effectiveMethod); - Parameter[] outputs = - MethodDesignArgumentResolver.ResolveMethodOutputs(effectiveMethod); + (Parameter[] inputs, Parameter[] outputs) = + MethodDesignArgumentResolver.ResolveMethodArguments(effectiveMethod); + ModelDesignExtensions.AssignMethodArgumentCodeNames( + inputs, + outputs, + s_methodArgumentCodeNameScope); string methodIdConstant = CoreUtils.Format( "global::{0}.MethodIds.{1}", @@ -570,7 +572,7 @@ private TemplateString LoadTemplate_Method(ILoadContext context) { context.Out.WriteLine( "/// Input argument {1}.", - GetParameterName(inputs[ii]), + GetMethodArgumentIdentifier(inputs[ii]).TrimStart('@'), ii); } context.Out.WriteLine( @@ -592,7 +594,7 @@ private TemplateString LoadTemplate_Method(ILoadContext context) targetNamespace, namespaces, inputs[ii].IsOptional), - GetParameterName(inputs[ii])); + GetMethodArgumentIdentifier(inputs[ii])); } context.Out.WriteLine(); context.Out.WriteLine( @@ -610,7 +612,7 @@ private TemplateString LoadTemplate_Method(ILoadContext context) { context.Out.WriteLine( " if ({0} is null) throw new global::System.ArgumentNullException(nameof({0}));", - GetParameterName(input)); + GetMethodArgumentIdentifier(input)); } } @@ -693,7 +695,7 @@ private static string GetReturnTypeAnnotation( namespaces, outputs[ii].IsOptional)) .Append(' ') - .Append(GetParameterName(outputs[ii])); + .Append(GetMethodArgumentIdentifier(outputs[ii])); } builder.Append(")>"); return builder.ToString(); @@ -778,17 +780,17 @@ private static void EmitOutputDeclaration( namespaces, parameter.IsOptional); string localName = GetLocalVariableName(parameter); - string parameterName = GetParameterName(parameter); switch (parameter.DataTypeNode.BasicDataType) { case BasicDataType.UserDefined: context.Out.WriteLine( - " if (!_outputArguments[{0}].TryGetStructure(out {1} {2}))", + " if (!_outputArguments[{0}].TryGetValue(" + + "out {1} {2}, base.Session.MessageContext))", index, typeName, localName); - EmitConversionFailure(context, methodName, parameterName); + EmitConversionFailure(context, methodName, parameter.Name); break; case BasicDataType.BaseDataType when parameter.ValueRank == ValueRank.Scalar: // The argument is itself a Variant; assign directly. @@ -804,7 +806,7 @@ private static void EmitOutputDeclaration( index, typeName, localName); - EmitConversionFailure(context, methodName, parameterName); + EmitConversionFailure(context, methodName, parameter.Name); break; } } @@ -823,10 +825,11 @@ private static void EmitConversionFailure( " throw new global::Opc.Ua.ServiceResultException("); context.Out.WriteLine( " global::Opc.Ua.StatusCodes.BadUnexpectedError,"); - context.Out.WriteLine( - " \"Method '{0}' returned an unexpected value for output '{1}'.\");", + string message = CoreUtils.Format( + "Method '{0}' returned an unexpected value for output '{1}'.", methodName, parameterName); + context.Out.WriteLine(" {0});", message.AsStringLiteral()); context.Out.WriteLine(" }"); } @@ -836,7 +839,7 @@ private static void EmitConversionFailure( /// private static string BoxInputArgument(Parameter parameter) { - string name = GetParameterName(parameter); + string name = GetMethodArgumentIdentifier(parameter); switch (parameter.DataTypeNode.BasicDataType) { case BasicDataType.UserDefined: @@ -878,21 +881,6 @@ private static bool RequiresNullCheck(Parameter parameter) return parameter.DataTypeNode.BasicDataType == BasicDataType.UserDefined; } - /// - /// Returns a lowerCamelCase parameter name for the given UA argument. - /// Reserved C# identifiers are escaped with the standard '@' prefix. - /// - private static string GetParameterName(Parameter parameter) - { - string name = parameter.Name; - if (string.IsNullOrEmpty(name)) - { - return "value"; - } - string camel = char.ToLowerInvariant(name[0]) + name[1..]; - return s_csharpKeywords.Contains(camel) ? "@" + camel : camel; - } - /// /// Returns the local variable name used inside the method body for /// an output argument. Always prefixed to avoid colliding with @@ -900,34 +888,52 @@ private static string GetParameterName(Parameter parameter) /// private static string GetLocalVariableName(Parameter parameter) { - return "_" + GetParameterName(parameter).TrimStart('@'); + return GetOutputLocalVariableName(GetMethodArgumentIdentifier(parameter)); } - /// - /// C# 12 reserved keywords that, when reused as parameter names, - /// must be escaped with an '@' prefix. Kept narrow on purpose; - /// contextual keywords (e.g. "value", "var") are intentionally - /// omitted because they are valid identifiers. - /// - private static readonly HashSet s_csharpKeywords = - [ - "abstract", "as", "base", "bool", "break", "byte", "case", "catch", - "char", "checked", "class", "const", "continue", "decimal", "default", - "delegate", "do", "double", "else", "enum", "event", "explicit", - "extern", "false", "finally", "fixed", "float", "for", "foreach", - "goto", "if", "implicit", "in", "int", "interface", "internal", "is", - "lock", "long", "namespace", "new", "null", "object", "operator", - "out", "override", "params", "private", "protected", "public", - "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", - "stackalloc", "static", "string", "struct", "switch", "this", - "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", - "unsafe", "ushort", "using", "virtual", "void", "volatile", "while" - ]; + private static string GetOutputLocalVariableName(string identifier) + { + return "_" + identifier.TrimStart('@'); + } + + private static IEnumerable GetMethodArgumentCollisionIdentifiers( + string identifier, + bool output) + { + yield return identifier; + if (output) + { + yield return GetOutputLocalVariableName(identifier); + } + } + + private static string GetMethodArgumentIdentifier(Parameter parameter) + { + return parameter.GetGeneratedCodeIdentifier( + scope: s_methodArgumentCodeNameScope); + } private const string kStandardUaNamespaceUri = "http://opcfoundation.org/UA/"; private const string kStandardUaProxyNamespace = "Opc.Ua"; private const string kRootBaseClass = "global::Opc.Ua.ObjectTypeClient"; + private static readonly MethodArgumentCodeNameScope s_methodArgumentCodeNameScope = new( + GetMethodArgumentCollisionIdentifiers, + "_", + "await", + "ct", + "cancellationToken", + "context", + "inputArguments", + "method", + "nameof", + "objectId", + "outputArguments", + "_outputArguments", + "results", + "serviceResult", + "ServiceResult"); + private readonly IGeneratorContext m_context; private readonly Microsoft.Extensions.Logging.ILogger m_logger; private HashSet m_inheritedMethodNames; diff --git a/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs b/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs index b555d2497d..28ec908ac1 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs @@ -32,6 +32,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using System.Xml; using Opc.Ua.SourceGeneration; using Opc.Ua.Types; @@ -59,6 +60,37 @@ internal enum NullableAnnotation Nullable } + /// + /// Identifies one generated method-argument symbol scope and the names + /// reserved by that scope's emitted scaffolding. + /// + internal sealed class MethodArgumentCodeNameScope + { + public MethodArgumentCodeNameScope(params string[] reservedNames) + : this(null, reservedNames) + { + } + + public MethodArgumentCodeNameScope( + Func> getCollisionIdentifiers, + params string[] reservedNames) + { + ReservedNames = reservedNames == null ? [] : [.. reservedNames]; + m_getCollisionIdentifiers = getCollisionIdentifiers; + } + + public IReadOnlyCollection ReservedNames { get; } + + public IEnumerable GetCollisionIdentifiers( + string identifier, + bool output) + { + return m_getCollisionIdentifiers?.Invoke(identifier, output) ?? [identifier]; + } + + private readonly Func> m_getCollisionIdentifiers; + } + /// /// Dotnet code generation support. /// @@ -505,6 +537,222 @@ public static string EnsureUniqueEnumName(this Parameter target) return target?.Name; } + /// + /// Returns the canonical C# identifier for a method argument. + /// + internal static string GetGeneratedCodeIdentifier( + this Parameter field, + bool upperCamelCase = false, + MethodArgumentCodeNameScope scope = null) + { + string name = field?.Name; + scope ??= s_defaultMethodArgumentCodeNameScope; + lock (s_generatedCodeNamesLock) + { + if (field != null && + s_generatedCodeNames.TryGetValue(field, out GeneratedCodeNameState state) && + state.Names.TryGetValue(scope, out string generatedName) && + !string.IsNullOrEmpty(generatedName)) + { + name = generatedName; + } + } + return name.ToCSharpIdentifier(upperCamelCase); + } + + /// + /// Assigns unique generated-code names against the requested scope while + /// preserving the authored OPC UA names. + /// + internal static void AssignMethodArgumentCodeNames( + this MethodDesign method, + MethodArgumentCodeNameScope scope = null, + params string[] additionalReservedNames) + { + AssignMethodArgumentCodeNames( + method?.InputArguments, + method?.OutputArguments, + scope, + additionalReservedNames); + } + + /// + /// Assigns unique generated-code names for an effective method signature. + /// + internal static void AssignMethodArgumentCodeNames( + Parameter[] inputArguments, + Parameter[] outputArguments, + MethodArgumentCodeNameScope scope = null, + params string[] additionalReservedNames) + { + scope ??= s_defaultMethodArgumentCodeNameScope; + lock (s_generatedCodeNamesLock) + { + ClearGeneratedCodeNames(inputArguments, scope); + ClearGeneratedCodeNames(outputArguments, scope); + + var usedNames = new HashSet(StringComparer.OrdinalIgnoreCase); + AddReservedMethodArgumentNames(usedNames, scope.ReservedNames); + AddReservedMethodArgumentNames(usedNames, additionalReservedNames); + AssignUniqueGeneratedCodeNames( + inputArguments, + usedNames, + output: false, + scope); + AssignUniqueGeneratedCodeNames( + outputArguments, + usedNames, + output: true, + scope); + } + } + + private static void AddReservedMethodArgumentNames( + HashSet usedNames, + IEnumerable reservedNames) + { + if (reservedNames == null) + { + return; + } + foreach (string reservedName in reservedNames) + { + if (!string.IsNullOrWhiteSpace(reservedName)) + { + usedNames.Add(GetIdentifierCollisionKey( + reservedName.ToCSharpIdentifier())); + } + } + } + + private static void AssignUniqueGeneratedCodeNames( + Parameter[] arguments, + HashSet usedNames, + bool output, + MethodArgumentCodeNameScope scope) + { + if (arguments == null) + { + return; + } + + foreach (Parameter argument in arguments) + { + if (argument == null) + { + continue; + } + + string identifier = argument.Name.ToCSharpIdentifier(); + if (TryReserveGeneratedIdentifier( + identifier, + output, + scope, + usedNames)) + { + SetGeneratedCodeName(argument, identifier, scope); + continue; + } + + string stem = identifier.TrimStart('@'); + int suffix = output ? 1 : 2; + while (true) + { + string suffixText; + if (output) + { + suffixText = suffix == 1 ? + "Out" : + "Out" + suffix.ToString(CultureInfo.InvariantCulture); + } + else + { + suffixText = suffix.ToString(CultureInfo.InvariantCulture); + } + + string candidate = (stem + suffixText).ToCSharpIdentifier(); + suffix++; + if (TryReserveGeneratedIdentifier( + candidate, + output, + scope, + usedNames)) + { + SetGeneratedCodeName(argument, candidate, scope); + break; + } + } + } + } + + private static bool TryReserveGeneratedIdentifier( + string identifier, + bool output, + MethodArgumentCodeNameScope scope, + HashSet usedNames) + { + string[] collisionKeys = + [ + .. scope.GetCollisionIdentifiers(identifier, output) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Select(GetIdentifierCollisionKey) + .Distinct(StringComparer.OrdinalIgnoreCase) + ]; + if (collisionKeys.Any(usedNames.Contains)) + { + return false; + } + foreach (string collisionKey in collisionKeys) + { + usedNames.Add(collisionKey); + } + return true; + } + + private static void ClearGeneratedCodeNames( + Parameter[] arguments, + MethodArgumentCodeNameScope scope) + { + if (arguments == null) + { + return; + } + foreach (Parameter argument in arguments) + { + if (argument != null && + s_generatedCodeNames.TryGetValue( + argument, + out GeneratedCodeNameState state)) + { + state.Names.Remove(scope); + } + } + } + + private static string GetIdentifierCollisionKey(string identifier) + { + return identifier.TrimStart('@'); + } + + private static void SetGeneratedCodeName( + Parameter argument, + string name, + MethodArgumentCodeNameScope scope) + { + s_generatedCodeNames.GetOrCreateValue(argument).Names[scope] = name; + } + + private sealed class GeneratedCodeNameState + { + public Dictionary Names { get; } = []; + } + + private static readonly MethodArgumentCodeNameScope + s_defaultMethodArgumentCodeNameScope = new(); + private static readonly System.Threading.Lock s_generatedCodeNamesLock = new(); + private static readonly ConditionalWeakTable + s_generatedCodeNames = new(); + /// /// Returns the field name of a child node. /// diff --git a/tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs b/tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs index edc054c642..7572aeddd3 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs @@ -423,6 +423,23 @@ private XmlQualifiedName ImportSymbolicName(UANode input) return new XmlQualifiedName(input.SymbolicName, browseName.Namespace); } + // A placeholder browse name "" (no explicit SymbolicName in the + // NodeSet) follows the ModelCompiler convention of mapping to + // "Name_Placeholder" so generated identifiers match those produced + // from the equivalent ModelDesign source (e.g. a combined NodeSet + // that incorporates a model authored as ModelDesign). The raw browse + // name is used because ImportQualifiedName rewrites '<' and '>' to '_'. + string rawName = QualifiedName.Parse(input.BrowseName).Name; + if (rawName != null && + rawName.Length > 2 && + rawName[0] == '<' && + rawName[^1] == '>') + { + return new XmlQualifiedName( + ToSymbolicName(rawName[1..^1]) + "_Placeholder", + browseName.Namespace); + } + return new XmlQualifiedName(ToSymbolicName(browseName.Name), browseName.Namespace); } @@ -1201,6 +1218,8 @@ private void UpdateMethodArguments(UAMethod input, MethodDesign output) output.HasArguments = true; } } + + output.AssignMethodArgumentCodeNames(); } private void LinkChildToParent(UAInstance input) @@ -1492,7 +1511,8 @@ private XmlQualifiedName ImportAndFixSymbolicName(UANode input) if (typeDefinitionId == ObjectTypeIds.DataTypeEncodingType) { - if (input.SymbolicName.Contains("Default", StringComparison.Ordinal) && + if (!string.IsNullOrEmpty(input.SymbolicName) && + input.SymbolicName.Contains("Default", StringComparison.Ordinal) && input.SymbolicName.Contains("XML", StringComparison.OrdinalIgnoreCase) && input.SymbolicName != "DefaultXml") { @@ -1940,10 +1960,22 @@ public ModelDesign Import(string prefix, string name) NodeId childId = ImportNodeId(instance.NodeId); if (parentId.NamespaceIndex != childId.NamespaceIndex) - { instance.ParentNodeId = null; } + else if (FindTarget( + node, + ReferenceTypeIds.HasTypeDefinition, + false) == ObjectTypeIds.DataTypeEncodingType) + { + // DataTypeEncoding objects are independent address-space + // nodes even when an exporter sets ParentNodeId to the + // owning DataType. Keeping that parent absorbs the + // encoding into DataType.Children, excludes it from the + // top-level model items and prevents the NodeManager + // generator from registering the encoding node. + instance.ParentNodeId = null; + } } // handle missing ParentNodeId when an inverse reference exists. @@ -2172,6 +2204,24 @@ private void CollectMethodDefinitions( string targetNamespace, Dictionary methods) { + // Index the explicit method nodes already present in the model by + // their symbolic name. A concrete method with arguments normally + // gets a synthesized "MethodType" declaration, but a combined + // NodeSet may already ship that method-type node explicitly (e.g. + // an incorporated companion specification method type). + // Reuse the existing declaration in that case so code generation + // does not emit two identifiers with the same name. + Dictionary existingByName = []; + foreach (NodeDesign node in m_settings.NodesById.Values) + { + if (node is MethodDesign existing && + existing.SymbolicName != null && + !existingByName.ContainsKey(existing.SymbolicName)) + { + existingByName.Add(existing.SymbolicName, existing); + } + } + MethodDesign[] candidates = [ .. m_settings.NodesById.Values @@ -2200,6 +2250,30 @@ .. m_settings.NodesById.Values } } + // Skip methods whose BrowseName belongs to a base namespace + // (e.g. the Core FileType Open/Close/Read/Write methods that a + // FileType instance re-declares): they are instances of a + // base-type method and must reuse that base method type rather + // than get a synthesized method type in this model. + if (method.SymbolicName != null && + method.SymbolicName.Namespace != targetNamespace) + { + continue; + } + + // Skip a standalone node that already is a method type (no owning + // parent and the conventional "MethodType" name, e.g. an + // incorporated companion specification method type. + // Synthesizing a declaration for it would emit a spurious + // "MethodTypeMethodType" node. A parentless method that is + // merely a declaration target of another method still needs one. + if (method.Parent == null && + method.SymbolicName != null && + method.SymbolicName.Name.EndsWith("MethodType", StringComparison.Ordinal)) + { + continue; + } + var baseName = new XmlQualifiedName( method.SymbolicName.Name + "MethodType", method.SymbolicId.Namespace); @@ -2258,6 +2332,30 @@ in groupedCandidates suffix++; } + // Prefer an explicit method-type declaration already in + // the model over synthesizing a colliding duplicate. The + // concrete method carries the authoritative argument + // definitions, so copy them onto the reused declaration + // to guarantee code generation emits the correct method + // signature and result even when the incorporated + // NodeSet declares the method-type argument nodes apart + // from the concrete method. + if (existingByName.TryGetValue(name, out MethodDesign declared) && + !signatureGroup.Any(method => ReferenceEquals(declared, method))) + { + declared.InputArguments = representative.InputArguments; + declared.OutputArguments = representative.OutputArguments; + declared.HasArguments = true; + foreach (MethodDesign method in signatureGroup) + { + method.MethodDeclarationNode = declared; + method.TypeDefinition = null; + method.MethodType = null; + } + reservedNames.Add(name); + continue; + } + var declaration = new MethodDesign { SymbolicId = name, @@ -2302,6 +2400,10 @@ private static string GetOwnerQualifiedMethodTypeName(MethodDesign method) /// private XmlDecoder CreateDecoder(System.Xml.XmlElement source, string sourceNodeSetUri = null) { + // The factory knows the standard OPC UA encodeable types. Without them, structured + // NodeSet2 values such as method Argument lists (InputArguments/OutputArguments) + // cannot be decoded and the generated typed method state would lose its arguments + // and result fields. var messageContext = new ServiceMessageContext(m_telemetry, s_valueDecodingFactory); messageContext.NamespaceUris = m_settings.NamespaceUris; messageContext.ServerUris = m_serverUris; diff --git a/tools/Opc.Ua.SourceGeneration.Core/Shared/SourceGenerationUtils.cs b/tools/Opc.Ua.SourceGeneration.Core/Shared/SourceGenerationUtils.cs index 6ee6fea215..5aab9cc119 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Shared/SourceGenerationUtils.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Shared/SourceGenerationUtils.cs @@ -28,6 +28,7 @@ * ======================================================================*/ using System; +using System.Collections.Generic; using System.Text; using System.Xml; @@ -74,6 +75,55 @@ public static string ToUpperCamelCase(this string name) return CoreUtils.Format("{0}{1}", char.ToUpperInvariant(name[0]), name[1..]); } + /// + /// Converts an authored name to a valid C# identifier. + /// + public static string ToCSharpIdentifier( + this string name, + bool upperCamelCase = false) + { + string source = name?.TrimStart('@'); + if (string.IsNullOrEmpty(source)) + { + return upperCamelCase ? "Value" : "value"; + } + + var buffer = new StringBuilder(source.Length); + bool applyCasing = true; + foreach (char character in source) + { + if (char.IsWhiteSpace(character)) + { + continue; + } + + char identifierCharacter = + char.IsLetterOrDigit(character) || character == '_' ? + character : + '_'; + if (buffer.Length == 0 && char.IsDigit(identifierCharacter)) + { + buffer.Append('_'); + } + if (applyCasing && char.IsLetter(identifierCharacter)) + { + identifierCharacter = upperCamelCase ? + char.ToUpperInvariant(identifierCharacter) : + char.ToLowerInvariant(identifierCharacter); + applyCasing = false; + } + buffer.Append(identifierCharacter); + } + + if (buffer.Length == 0) + { + return upperCamelCase ? "Value" : "value"; + } + + string identifier = buffer.ToString(); + return s_csharpKeywords.Contains(identifier) ? "@" + identifier : identifier; + } + /// /// Convert string to a safe symbol for dotnet use /// @@ -195,7 +245,10 @@ public static string AsStringLiteral(this string value) .Replace("\"", "\\\"", StringComparison.Ordinal) .Replace("\n", "\\n", StringComparison.Ordinal) .Replace("\r", "\\r", StringComparison.Ordinal) - .Replace("\t", "\\t", StringComparison.Ordinal); + .Replace("\t", "\\t", StringComparison.Ordinal) + .Replace("\u0085", "\\u0085", StringComparison.Ordinal) + .Replace("\u2028", "\\u2028", StringComparison.Ordinal) + .Replace("\u2029", "\\u2029", StringComparison.Ordinal); return $"\"{value}\""; } @@ -232,5 +285,21 @@ public static bool IsNull(this XmlQualifiedName qname) return false; } + + private static readonly HashSet s_csharpKeywords = + [ + "abstract", "as", "base", "bool", "break", "byte", "case", "catch", + "char", "checked", "class", "const", "continue", "decimal", "default", + "delegate", "do", "double", "else", "enum", "event", "explicit", + "extern", "false", "finally", "fixed", "float", "for", "foreach", + "goto", "if", "implicit", "in", "int", "interface", "internal", "is", + "lock", "long", "namespace", "new", "null", "object", "operator", + "out", "override", "params", "private", "protected", "public", + "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", + "stackalloc", "static", "string", "struct", "switch", "this", + "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", + "unsafe", "ushort", "using", "virtual", "void", "volatile", "while", + "__arglist", "__makeref", "__reftype", "__refvalue" + ]; } }