/* ======================================================================== * Copyright (c) 2005-2025 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.Collections.Immutable; using System.Linq; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Opc.Ua.SourceGeneration.Dependency; namespace Opc.Ua.SourceGeneration { /// /// Compiles the model. This honors the options of the compile command /// of the tool. However, it does not generate identifier files and /// assumes that if identifiers are required they are loaded from a /// csv file that is side by side with the design files. Also, this /// does not produce xsd or bsd nor nodeset files, which cannot be /// added to a compilation using roslyn source generator (yet). /// internal sealed class ModelCompilation { /// /// Create compilation /// public ModelCompilation( SourceProductionContext context, ImmutableArray<(AdditionalText, NodesetFileOptions)> inputFiles, ImmutableArray csvFiles, ImmutableArray identifierFiles, ModelCompilationOptions options, CompilationOptions compilationOptions, ImmutableArray referencedModels, ImmutableArray referencedAccessorProviders, ImmutableArray nodeManagerBindings, ImmutableHashSet availableStateTypeNames) { m_context = context; m_input = inputFiles; m_csvFiles = csvFiles; m_identifierFiles = identifierFiles; m_options = options; m_compilationOptions = compilationOptions; m_nodeManagerBindings = nodeManagerBindings; m_referencedModels = referencedModels; m_referencedAccessorProviders = referencedAccessorProviders; m_availableStateTypeNames = availableStateTypeNames; m_telemetry = SourceGeneratorTelemetry.Create(m_context); } /// /// Perform the compilation /// public void Emit(CancellationToken cancellationToken) { if (!CheckCompilationOptions()) { return; } var sourceFiles = new SourceGeneratorFileSystem( m_input.Select(i => i.Item1).Concat(m_csvFiles)); using var vfs = new VirtualFileSystem(); // Use a virtual file sytem try { if (m_input.Length == 0) { // Nothing to do return; } string[] exclusions = [.. m_options.Exclude .Append("Draft") .Distinct()]; var generatorOptions = new GeneratorOptions { Cancellation = cancellationToken, Exclusions = exclusions, // csharp10 or below does not support utf8 string literals UseUtf8StringLiterals = m_compilationOptions.LanguageVersion >= LanguageVersion.CSharp11, OptimizeForCompileSpeed = m_compilationOptions.OptimizationLevel == OptimizationLevel.Debug, OmitObjectTypeProxies = m_options.OmitObjectTypeProxies, ObjectTypeProxyNamespace = string.IsNullOrWhiteSpace(m_options.ObjectTypeProxyNamespace) ? null : m_options.ObjectTypeProxyNamespace, UseTypeDefinitionModellingRules = m_options.UseTypeDefinitionModellingRules, EmitDependencyMetadata = ResolveEmitDependencyMetadata(), OmitFluentApi = m_options.OmitFluentApi, FluentAccessorsOnly = m_options.FluentAccessorsOnly, OmitEventRecords = m_options.OmitEventRecords }; // Load all available nodeset files from the input NodesetFileCollection nodesets = m_input.ToNodeSetFileCollection( m_csvFiles, sourceFiles, // .WithFallback(vfs), m_telemetry); ReportNodesetIdentifierDiagnostics(nodesets.IdentifierValidationErrors); // Resolve [NodeManager] bindings: validate partial-ness and // build the binding list to pass into both GenerateCode calls // (nodeset-derived and design-file-derived). var bindings = new List(); var bindingByPayload = new Dictionary(); foreach (NodeManagerAttributeDiscovery discovery in m_nodeManagerBindings) { if (discovery == null) { continue; } if (!discovery.IsPartial) { m_context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.NodeManagerNotPartial, discovery.Location, discovery.Binding.TargetNamespace + "." + discovery.Binding.TargetClassName)); continue; } bindings.Add(discovery.Binding); bindingByPayload[discovery.Binding] = discovery; } void reportBinding(NodeManagerAttributeBinding binding, string message) { Location loc = bindingByPayload.TryGetValue(binding, out NodeManagerAttributeDiscovery d) && d != null ? d.Location : Location.None; m_context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.NodeManagerBindingError, loc, message)); } void reportFluentAccessorsOnly( string modelUri, string prefix, string path, string reason) { m_context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.FluentAccessorsOnlyModelError, Location.None, modelUri, prefix, path, reason)); } // Reduce referenced model attributes to a single dictionary by // model URI (with tie-break on highest version+publication date) // so the downstream generators can apply override resolution // and decode the carried dependency payloads. IReadOnlyDictionary referencedModels = BuildReferencedModelMap(); // The design files that are not NodeSet2 inputs form the // ModelDesign pass. Compute them up front so the total model // count (NodeSet2 models + ModelDesign targets) can be shared // with both passes: [NodeManager] bindings are resolved across // both passes, so single-model fallback / ambiguity detection // must see the global model count, not the per-pass count. // Use a set for O(1) membership tests instead of an O(n) // ContainsValue scan per input (Ordinal preserves the previous // dictionary-value equality semantics exactly). var nodesetPaths = new HashSet( nodesets.Files.Values, StringComparer.Ordinal); List designTargets = [.. m_input .Where(f => !nodesetPaths.Contains(f.Item1.Path)) .Select(f => f.Item1.Path)]; var designDependencies = new List(nodesets.DesignFileEntries); designDependencies.AddRange(designTargets); // A [NodeManager] may bind to a model produced by either pass // (a NodeSet2 type model or a ModelDesign instance model). The // "used" set is therefore shared across both passes and the // unmatched-binding diagnostics are reported once, after both // passes — reporting per pass would false-positive a binding // that the other pass matched (issue #3937). HashSet usedBindings = bindings.Count > 0 ? [] : null; int totalModelCount = nodesets.ModelUris.Count() + designTargets.Count; // Enter the standard method-state fallback scope for the whole // model-generation pass: a reference to a standard // global::Opc.Ua.*MethodState class degrades to the base // MethodState only when the typed class is neither present in // the compilation nor declared by this pass. The Stack // generator (which builds Opc.Ua.Core) never enters this scope. using IDisposable fallbackScope = StandardMethodStateFallback.Enter(m_availableStateTypeNames); nodesets.GenerateCode( sourceFiles.WithFallback(vfs), string.Empty, m_telemetry, generatorOptions, m_options.UseAllowSubtypes, referencedModels, bindings.Count > 0 ? bindings : null, bindings.Count > 0 ? reportBinding : null, // Derived from referencedModels, which carries the payloads. null, usedBindings, totalModelCount, reportFluentAccessorsOnly, m_referencedModels, m_referencedAccessorProviders); // Process the remaining design files. Every NodeSet2 input // (encoded with the prefix/name computed by the nodeset // pass) and every other ModelDesign input is supplied as a // dependency so cross-model references resolve — both // ModelDesign -> NodeSet2 (e.g. an instance whose // TypeDefinition is a NodeSet2-defined ObjectType) and // ModelDesign -> ModelDesign across directories. The // referenced models carry the upstream dependency payloads, // so a design file can also subtype a structure of a // referenced model that is not present in AdditionalFiles. new DesignFileCollection { Targets = designTargets, Dependencies = designDependencies, Options = m_options.Options }.GenerateCode( sourceFiles.WithFallback(vfs), string.Empty, m_telemetry, generatorOptions, m_options.UseAllowSubtypes, [.. m_identifierFiles.Select(i => i.Path)], referencedModels, bindings.Count > 0 ? bindings : null, bindings.Count > 0 ? reportBinding : null, usedBindings, totalModelCount, reportFluentAccessorsOnly, m_referencedModels, m_referencedAccessorProviders); // Report any [NodeManager] bindings that neither pass matched, // once, against the shared used-set aggregated across passes. Generators.ReportUnmatchedNodeManagerBindings( bindings, usedBindings, totalModelCount, reportBinding); // Collect all generated cs files and produce them into the compilation foreach (string file in vfs.CreatedFiles .Where(f => f.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))) { string content = Encoding.UTF8.GetString(vfs.Get(file)); m_context.AddSource(file, content); } } catch (Exception ex) { m_context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.Exception, Location.None, ex.Message, ex.ToString())); } } /// /// Tests the compilation options are valid /// /// private bool CheckCompilationOptions() { if (m_compilationOptions.LanguageVersion < LanguageVersion.CSharp8) { m_context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.GenericError, Location.None, "Minimum required language version is CSharp 8.")); return false; } if (m_options.FluentAccessorsOnly && m_options.OmitFluentApi) { m_context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.FluentAccessorsOnlyOptionsError, Location.None, "ModelSourceGeneratorFluentAccessorsOnly and " + "ModelSourceGeneratorOmitFluentApi cannot both be enabled.")); return false; } if (m_options.FluentAccessorsOnly && m_options.Options.GenerateNodeManager) { m_context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.FluentAccessorsOnlyOptionsError, Location.None, "ModelSourceGeneratorFluentAccessorsOnly cannot be combined with " + "ModelSourceGeneratorGenerateNodeManager.")); return false; } return true; } private void ReportNodesetIdentifierDiagnostics( IEnumerable errors) { foreach (NodesetIdentifierValidationError error in errors) { m_context.ReportDiagnostic( Diagnostic.Create( error.Kind switch { NodesetIdentifierValidationErrorKind.MissingFile => SourceGenerator.NodesetIdentifierFileMissing, NodesetIdentifierValidationErrorKind.DuplicateSymbolicName => SourceGenerator.NodesetIdentifierDuplicateSymbolicName, NodesetIdentifierValidationErrorKind.DuplicateNumericId => SourceGenerator.NodesetIdentifierDuplicateNumericId, NodesetIdentifierValidationErrorKind.UnknownSymbol => SourceGenerator.NodesetIdentifierUnknownSymbol, NodesetIdentifierValidationErrorKind.NumericIdMismatch => SourceGenerator.NodesetIdentifierNumericIdMismatch, NodesetIdentifierValidationErrorKind.NodeClassMismatch => SourceGenerator.NodesetIdentifierNodeClassMismatch, NodesetIdentifierValidationErrorKind.AssignedToMultipleModels => SourceGenerator.NodesetIdentifierAssignedToMultipleModels, _ => SourceGenerator.NodesetIdentifierInvalidRow }, Location.None, error.NodeSetFilePath, error.IdentifierFilePath, error.SymbolicName, error.Value)); } } /// /// Group the referenced-assembly attributes by model URI; when more /// than one assembly contributes the same URI, prefer a payload-bearing /// self producer over payloadless transitive re-exports, then use the /// highest (Version, PublicationDate) lexicographic tuple per /// the contract on . /// private IReadOnlyDictionary BuildReferencedModelMap() { if (m_referencedModels.IsDefaultOrEmpty) { return ImmutableDictionary.Empty; } var map = new Dictionary( StringComparer.Ordinal); foreach (ModelDependencyReference candidate in m_referencedModels) { if (!candidate.IsValid) { continue; } if (!map.TryGetValue(candidate.ModelUri, out ModelDependencyReference existing)) { map[candidate.ModelUri] = candidate; continue; } bool candidateIsProducer = IsModelProducer(candidate); bool existingIsProducer = IsModelProducer(existing); int cmp = candidateIsProducer.CompareTo(existingIsProducer); if (cmp == 0) { cmp = string.CompareOrdinal(candidate.Version, existing.Version); } if (cmp == 0) { cmp = string.CompareOrdinal( candidate.PublicationDate, existing.PublicationDate); } if (cmp > 0) { m_context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.ModelDependencyTieBreak, Location.None, candidate.ModelUri, candidate.AssemblyName, existing.AssemblyName)); map[candidate.ModelUri] = candidate; } else if (cmp < 0) { m_context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.ModelDependencyTieBreak, Location.None, existing.ModelUri, existing.AssemblyName, candidate.AssemblyName)); } } return map; } private static bool IsModelProducer(ModelDependencyReference reference) { ModelDependencyV1 dependency = reference.GetDependency(); return dependency != null && string.Equals(dependency.ModelUri, reference.ModelUri, StringComparison.Ordinal); } /// /// Resolve whether the model-dependency assembly attribute /// should be emitted, honouring the compilation's OutputKind /// in Auto mode. /// private bool ResolveEmitDependencyMetadata() { return m_options.EmitDependencyMetadata switch { EmitDependencyMetadataMode.Always => true, EmitDependencyMetadataMode.Never => false, _ => m_compilationOptions.OutputKind is OutputKind.DynamicallyLinkedLibrary or OutputKind.NetModule }; } private readonly SourceProductionContext m_context; private readonly ImmutableArray<(AdditionalText, NodesetFileOptions)> m_input; private readonly ImmutableArray m_csvFiles; private readonly ImmutableArray m_identifierFiles; private readonly ModelCompilationOptions m_options; private readonly CompilationOptions m_compilationOptions; private readonly ImmutableArray m_referencedModels; private readonly ImmutableArray m_referencedAccessorProviders; private readonly ImmutableArray m_nodeManagerBindings; private readonly ImmutableHashSet m_availableStateTypeNames; private readonly SourceGeneratorTelemetry m_telemetry; } }