/* ======================================================================== * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Xml; using Opc.Ua.Schema.Model; using Opc.Ua.SourceGeneration.Dependency; namespace Opc.Ua.SourceGeneration { /// /// Emits one [assembly: Opc.Ua.ModelDependencyAttribute(...)] line /// per model the generated assembly emits (self-declaration) and one per /// dependency model declared by the . The /// self-declaration entry additionally carries a base64-encoded /// Deflate-compressed payload that /// downstream source generators can decode to resolve cross-assembly /// type references without re-walking AdditionalFiles. /// internal sealed class ModelDependencyGenerator : IGenerator { public ModelDependencyGenerator( IGeneratorContext context, bool? fluentAccessorsEmitted = null) { m_context = context ?? throw new ArgumentNullException(nameof(context)); m_fluentAccessorsEmitted = fluentAccessorsEmitted; } /// public IEnumerable Emit() { Namespace target = m_context.ModelDesign.TargetNamespace; if (target == null || string.IsNullOrEmpty(target.Value) || string.IsNullOrEmpty(target.Prefix)) { return []; } List entries = CollectEntries(target); if (entries.Count == 0) { return []; } string fileName = Path.Combine( m_context.OutputFolder, CoreUtils.Format("{0}.ModelDependencies.g.cs", target.Prefix)); using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName); using var templateWriter = new TemplateWriter(writer); var template = new Template(templateWriter, ModelDependencyTemplates.File); template.AddReplacement( Tokens.ListOfModelDependencies, ModelDependencyTemplates.Entry, entries, WriteEntry); template.Render(); return [fileName.AsTextFileResource()]; } private List CollectEntries(Namespace target) { var entries = new List(); var seen = new HashSet(StringComparer.Ordinal); // Self-declaration: one entry for the target namespace using the // generator-resolved target version / publication date so that // downstream tie-breaks have authoritative metadata. This is the // only entry that carries a payload — the compact type-table // ModelDependencyV1 blob. string selfVersion = m_context.ModelDesign.TargetVersion ?? target.Version; string selfPubDate = FormatDate(m_context.ModelDesign.TargetPublicationDate) ?? target.PublicationDate; string? selfPayload = BuildSelfPayload(target); entries.Add(new Entry( target.Value, target.Prefix, selfVersion, selfPubDate, target.Name, selfPayload)); seen.Add(target.Value); // Re-emit dependencies declared on the model (transitive closure). // Skip the OpcUa root namespace (always implicit) and skip self. foreach (Namespace ns in m_context.ModelDesign.Namespaces ?? []) { if (ns == null || string.IsNullOrEmpty(ns.Value) || string.IsNullOrEmpty(ns.Prefix)) { continue; } if (ns.Value == Types.Namespaces.OpcUa) { continue; } if (!seen.Add(ns.Value)) { continue; } entries.Add(new Entry( ns.Value, ns.Prefix, ns.Version, ns.PublicationDate, ns.Name, null)); } // Re-emit the closure picked up from referenced assemblies so // downstream consumers see one merged closure on this assembly. foreach (KeyValuePair entry in m_context.ReferencedModels) { ModelDependencyReference r = entry.Value; if (!r.IsValid) { continue; } if (r.ModelUri == Types.Namespaces.OpcUa) { continue; } if (!seen.Add(r.ModelUri)) { continue; } entries.Add(new Entry( r.ModelUri, r.Prefix, r.Version, r.PublicationDate, r.Name, null)); } return entries; } private string? BuildSelfPayload(Namespace target) { // OpcUa root is implicit to every consumer; do not emit a payload. if (target.Value == Types.Namespaces.OpcUa) { return null; } ModelDependencyV1 payload = BuildPayload(target); if (payload.Nodes.Count == 0 && !payload.FluentAccessorsEmitted.HasValue) { return null; } return payload.ToBase64Payload(); } private ModelDependencyV1 BuildPayload(Namespace target) { var payload = new ModelDependencyV1 { ModelUri = target.Value, FluentAccessorsEmitted = m_fluentAccessorsEmitted }; string targetUri = target.Value; foreach (NodeDesign node in m_context.ModelDesign.Nodes ?? []) { if (m_context.ModelDesign.IsExcluded(node)) { continue; } if (node.SymbolicId is null || !string.Equals(node.SymbolicId.Namespace, targetUri, StringComparison.Ordinal)) { continue; } if (node is not TypeDesign type) { continue; } DependencyNodeKind kind = type switch { ObjectTypeDesign => DependencyNodeKind.ObjectType, VariableTypeDesign => DependencyNodeKind.VariableType, ReferenceTypeDesign => DependencyNodeKind.ReferenceType, DataTypeDesign => DependencyNodeKind.DataType, _ => DependencyNodeKind.Unknown }; if (kind == DependencyNodeKind.Unknown) { continue; } var entry = new DependencyNode { SymbolicName = type.SymbolicId.Name ?? string.Empty, SymbolicNamespace = type.SymbolicId.Namespace ?? string.Empty, ClassName = type.ClassName ?? type.SymbolicId.Name ?? string.Empty, Kind = kind, BaseTypeName = type.BaseType?.Name, BaseTypeNamespace = type.BaseType?.Namespace, NumericId = type.NumericIdSpecified ? type.NumericId : 0u, StringId = string.IsNullOrEmpty(type.StringId) ? null : type.StringId, GuidId = type.GuidIdSpecified ? type.GuidId.ToString("D", CultureInfo.InvariantCulture) : null, OpaqueId = type.OpaqueId != null ? Convert.ToBase64String(type.OpaqueId) : null, IsAbstract = type.IsAbstract, Security = DependencySecurity.FromNode(type, m_context.ModelDesign) }; if (type is VariableTypeDesign variableTypeDesign) { // A consumer that types a variable with this VariableType // has to know the restriction to decide whether the // generated state class needs a template parameter. Without // it the consumer's DataTypeNode stays null and the node // state generator dereferences it (see // ModelDesignExtensions.GetNodeStateClassName). entry.DataTypeName = variableTypeDesign.DataType?.Name ?? string.Empty; entry.DataTypeNamespace = variableTypeDesign.DataType?.Namespace ?? string.Empty; entry.ValueRank = variableTypeDesign.ValueRankSpecified ? (int)variableTypeDesign.ValueRank : null; } if (type is DataTypeDesign dataType) { entry.IsEnumeration = dataType.IsEnumeration; if (dataType.Fields != null && dataType.Fields.Length > 0) { var fields = new List(dataType.Fields.Length); foreach (Parameter field in dataType.Fields) { if (field == null) { continue; } fields.Add(new DependencyDataField( name: field.Name ?? string.Empty, dataTypeName: field.DataType?.Name ?? string.Empty, dataTypeNamespace: field.DataType?.Namespace ?? string.Empty, valueRank: (int)field.ValueRank) { // A subtype in a consumer model needs these to // continue the encoding mask and publish the // inherited fields' definition correctly. IsOptional = field.IsOptional, AllowSubTypes = field.AllowSubTypes, ArrayDimensions = string.IsNullOrEmpty(field.ArrayDimensions) ? null : field.ArrayDimensions }); } entry.Fields = fields; } } else { // For ObjectType / VariableType / ReferenceType, carry // their declared InstanceDesign children so downstream // consumers can recognise inherited members and suppress // duplicate emission. if (type.HasChildren && type.Children?.Items != null) { var children = new List(type.Children.Items.Length); foreach (InstanceDesign child in type.Children.Items) { if (child != null) { children.Add(CreateChild(child)); } } DependencyChild CreateChild(InstanceDesign child) { byte instanceKind = child switch { PropertyDesign => 3, VariableDesign => 2, MethodDesign => 4, _ => 1 }; byte modellingRule = child.ModellingRule switch { ModellingRule.Mandatory => 1, ModellingRule.Optional => 2, ModellingRule.OptionalPlaceholder => 3, ModellingRule.MandatoryPlaceholder => 4, ModellingRule.ExposesItsArray => 5, _ => 0 }; var entryChild = new DependencyChild { BrowseName = child.BrowseName ?? child.SymbolicName?.Name ?? string.Empty, SymbolicName = child.SymbolicName?.Name ?? string.Empty, TypeDefinitionName = child.TypeDefinition?.Name ?? string.Empty, TypeDefinitionNamespace = child.TypeDefinition?.Namespace ?? string.Empty, ModellingRule = modellingRule, InstanceKind = instanceKind, Security = DependencySecurity.FromNode( child.GetMergedInstance(), m_context.ModelDesign) }; // A child re-declared from a base type in another // namespace keeps that namespace on its browse // name. Carrying it only when it differs from the // declaring model keeps the payload unchanged for // the common case. string childBrowseNamespace = child.SymbolicName?.Namespace ?? string.Empty; if (childBrowseNamespace.Length > 0 && !string.Equals( childBrowseNamespace, targetUri, StringComparison.Ordinal)) { entryChild.BrowseNameNamespace = childBrowseNamespace; } // Likewise only a reference type that departs from // the kind default needs carrying; the consumer // applies the default for everything else. if (child.ReferenceType != null && !(string.Equals( child.ReferenceType.Name, ModelDependencyV1.GetDefaultReferenceTypeName(instanceKind), StringComparison.Ordinal) && string.Equals( child.ReferenceType.Namespace, ModelDependencyV1.OpcUaNamespaceUri, StringComparison.Ordinal))) { entryChild.ReferenceTypeName = child.ReferenceType.Name ?? string.Empty; entryChild.ReferenceTypeNamespace = child.ReferenceType.Namespace ?? string.Empty; } if (child is VariableDesign variable) { var effectiveVariable = (VariableDesign)variable.GetMergedInstance(); entryChild.TypeDefinitionName = effectiveVariable.TypeDefinition?.Name ?? string.Empty; entryChild.TypeDefinitionNamespace = effectiveVariable.TypeDefinition?.Namespace ?? string.Empty; entryChild.DataTypeName = effectiveVariable.DataType?.Name ?? string.Empty; entryChild.DataTypeNamespace = effectiveVariable.DataType?.Namespace ?? string.Empty; entryChild.ValueRank = (int)effectiveVariable.ValueRank; entryChild.AccessLevel = (byte)effectiveVariable.AccessLevel; entryChild.AccessLevelSpecified = effectiveVariable.AccessLevelSpecified; entryChild.RawAccessLevel = effectiveVariable.RawAccessLevel; entryChild.RawUserAccessLevel = effectiveVariable.RawUserAccessLevel; entryChild.MinimumSamplingInterval = effectiveVariable.MinimumSamplingInterval; entryChild.MinimumSamplingIntervalSpecified = effectiveVariable.MinimumSamplingIntervalSpecified; entryChild.Historizing = effectiveVariable.Historizing; entryChild.HistorizingSpecified = effectiveVariable.HistorizingSpecified; entryChild.DefaultValueXml = effectiveVariable.DefaultValue?.OuterXml; entryChild.NodeSetValueIsNull = effectiveVariable.NodeSetValueIsNull; entryChild.DefaultValueNamespaceUris = GetDefaultValueNamespaceUris(effectiveVariable); } else if (child is MethodDesign method) { var effectiveMethod = (MethodDesign)method.GetMergedInstance(); entryChild.TypeDefinitionName = effectiveMethod.TypeDefinition?.Name ?? string.Empty; entryChild.TypeDefinitionNamespace = effectiveMethod.TypeDefinition?.Namespace ?? string.Empty; entryChild.InputArguments = DependencyMethodArgs( MethodDesignArgumentResolver.ResolveMethodInputs( effectiveMethod)); entryChild.OutputArguments = DependencyMethodArgs( MethodDesignArgumentResolver.ResolveMethodOutputs( effectiveMethod)); if (MethodDesignArgumentResolver.HasMethodArguments( effectiveMethod)) { XmlQualifiedName methodStateIdentity = MethodDesignArgumentResolver.ResolveMethodStateIdentity( effectiveMethod); if (methodStateIdentity != null) { entryChild.MethodStateName = methodStateIdentity.Name ?? string.Empty; entryChild.MethodStateNamespace = methodStateIdentity.Namespace ?? string.Empty; } MethodDesign? declaration = effectiveMethod.MethodDeclarationNode ?? (MethodDesignArgumentResolver.HasDeclaredArguments( effectiveMethod) ? effectiveMethod : null); if (declaration != null) { XmlQualifiedName declarationIdentity = declaration.SymbolicId ?? declaration.SymbolicName; if (declarationIdentity != null) { entryChild.MethodDeclarationName = declarationIdentity.Name ?? string.Empty; entryChild.MethodDeclarationNamespace = declarationIdentity.Namespace ?? string.Empty; } entryChild.MethodDeclarationNumericId = declaration.NumericIdSpecified ? declaration.NumericId : declarationIdentity == effectiveMethod.SymbolicId && effectiveMethod.NumericIdSpecified ? effectiveMethod.NumericId : 0; entryChild.MethodDeclarationStringId = string.IsNullOrEmpty(declaration.StringId) ? declarationIdentity == effectiveMethod.SymbolicId ? effectiveMethod.StringId : null : declaration.StringId; } } } if (child.Children?.Items != null) { var descendants = new List(child.Children.Items.Length); foreach (InstanceDesign descendant in child.Children.Items) { if (descendant != null) { descendants.Add(CreateChild(descendant)); } } entryChild.Children = [.. descendants]; } return entryChild; } if (children.Count > 0) { entry.Children = children; } } } payload.Nodes.Add(entry); } // Deterministic order so the assembly is byte-reproducible. payload.Nodes.Sort((a, b) => { int c = string.CompareOrdinal(a.SymbolicNamespace, b.SymbolicNamespace); return c != 0 ? c : string.CompareOrdinal(a.SymbolicName, b.SymbolicName); }); return payload; } /// /// The namespace table the indexes in the default value XML of the /// variable refer to: the table recorded when the value was decoded /// from a NodeSet or a dependency design, otherwise this design's own /// namespaces - the table the producer resolves the value against. /// private string[]? GetDefaultValueNamespaceUris(VariableDesign variable) { if (variable.DefaultValue == null) { return null; } NamespaceTable table = (variable.DecodedValue != null ? variable.DecodedValueNamespaceUris : null) ?? ModelDesignExtensions.CreateDesignNamespaceTable(m_context.ModelDesign.Namespaces); return table.ToArray(); } private static List DependencyMethodArgs(Parameter[] args) { if (args == null || args.Length == 0) { return []; } var result = new List(args.Length); foreach (Parameter a in args) { if (a == null) { continue; } result.Add(new DependencyMethodArg( name: a.Name ?? string.Empty, dataTypeName: a.DataType?.Name ?? string.Empty, dataTypeNamespace: a.DataType?.Namespace ?? string.Empty, valueRank: (int)a.ValueRank)); } return result; } private static bool WriteEntry(IWriteContext context) { if (context.Target is not Entry entry) { return false; } context.Template.AddReplacement(Tokens.ModelUri, SourceGenerationUtils.Escape(entry.ModelUri)); context.Template.AddReplacement(Tokens.Prefix, SourceGenerationUtils.Escape(entry.Prefix)); context.Template.AddReplacement(Tokens.ModelVersion, FormatNullableLiteral(entry.Version)); context.Template.AddReplacement( Tokens.ModelPublicationDate, FormatNullableLiteral(entry.PublicationDate)); context.Template.AddReplacement(Tokens.ModelName, FormatNullableLiteral(entry.Name)); context.Template.AddReplacement(Tokens.ModelPayload, FormatNullableLiteral(entry.Payload)); return context.Template.Render(); } private static string FormatNullableLiteral(string? value) { return string.IsNullOrEmpty(value) ? "null" : CoreUtils.Format("\"{0}\"", SourceGenerationUtils.Escape(value)); } private static string? FormatDate(DateTime? d) { return d?.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); } private readonly record struct Entry( string ModelUri, string Prefix, string Version, string PublicationDate, string Name, string? Payload); private readonly IGeneratorContext m_context; private readonly bool? m_fluentAccessorsEmitted; } }