/* ======================================================================== * 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.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Opc.Ua.SourceGeneration { /// /// Discovered [Opc.Ua.Server.Fluent.NodeManager] binding plus /// the source location of the attribute, used to report friendly /// diagnostics back at the user's class. /// /// /// This is the output of a ForAttributeWithMetadataName transform, /// which runs again on every compilation change. Every member therefore /// compares by value (no arrays, no ) /// so an unchanged attribute leaves the model generation cached. /// internal sealed record class NodeManagerAttributeDiscovery { /// /// The pure binding payload that gets forwarded into the /// Core GenerateCode pipeline. /// public NodeManagerAttributeBinding Binding { get; init; } /// /// Location of the annotated class, used for diagnostics. /// public LocationInfo Location { get; init; } /// /// true when the user-authored target class is declared /// partial. /// public bool IsPartial { get; init; } /// /// Why the annotated class cannot host a generated node manager /// (nested, generic, file-local or in the global namespace), or /// null when it can. /// public string UnsupportedReason { get; init; } /// /// Attribute expressions that Roslyn could not bind to constants. /// public EquatableArray InvalidExpressions { get; init; } = EquatableArray.Empty; /// /// True if the discovery cannot be bound: an unresolved expression, /// an unsupported target or a class that is not partial. Such a /// discovery is reported by and /// skipped by the model compilation. /// public bool IsInvalid => InvalidExpressions.Count > 0 || UnsupportedReason != null || !IsPartial; /// /// Reports why discoveries cannot be bound, at locations re-created /// in the syntax trees of so that /// #pragma warning, per-file severity configuration and /// #line mapping apply. /// public static void ReportDiagnostics( SourceProductionContext context, IEnumerable discoveries, Compilation compilation) { foreach (NodeManagerAttributeDiscovery discovery in discoveries) { if (discovery == null) { continue; } if (discovery.InvalidExpressions.Count > 0) { string targetType = GetTargetTypeName(discovery.Binding); foreach (NodeManagerAttributeExpressionError error in discovery.InvalidExpressions) { context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.NodeManagerArgumentUnresolved, error.Location.ToLocation(compilation), error.ArgumentName, error.Expression, targetType)); } continue; } if (discovery.UnsupportedReason != null) { context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.NodeManagerUnsupportedTarget, discovery.Location.ToLocation(compilation), GetTargetTypeName(discovery.Binding), discovery.UnsupportedReason)); continue; } if (!discovery.IsPartial) { context.ReportDiagnostic( Diagnostic.Create( SourceGenerator.NodeManagerNotPartial, discovery.Location.ToLocation(compilation), GetTargetTypeName(discovery.Binding))); } } } private static string GetTargetTypeName(NodeManagerAttributeBinding binding) { return string.IsNullOrEmpty(binding.TargetNamespace) ? binding.TargetClassName : binding.TargetNamespace + "." + binding.TargetClassName; } /// /// Predicate used by . /// public static bool Handles(SyntaxNode node, CancellationToken ct) { ct.ThrowIfCancellationRequested(); return node is ClassDeclarationSyntax c && c.AttributeLists.Count > 0; } /// /// Build a discovery record from the syntax-provider context. /// public static NodeManagerAttributeDiscovery Create( GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { var symbol = (INamedTypeSymbol)context.TargetSymbol; AttributeData attr = context.Attributes.FirstOrDefault(); string namespaceUri = attr.GetValue(nameof(NodeManagerAttributeBinding.NamespaceUri)); string design = attr.GetValue(nameof(NodeManagerAttributeBinding.Design)); string[] additionalNamespaceUris = attr.GetStringArray( nameof(NodeManagerAttributeBinding.AdditionalNamespaceUris)); EquatableArray invalidExpressions = GetInvalidExpressions(attr, context.SemanticModel, cancellationToken); bool generateFactory = IsNotDisabled( attr, nameof(NodeManagerAttributeBinding.GenerateFactory)); bool generateDefaultConstructor = IsNotDisabled( attr, nameof(NodeManagerAttributeBinding.GenerateDefaultConstructor)); string targetNamespace = symbol.GetFullNamespace(); string targetClassName = symbol.Name; bool isPartial = symbol.DeclaringSyntaxReferences .Any(r => r.GetSyntax(cancellationToken) is TypeDeclarationSyntax tds && tds.Modifiers.Any(SyntaxKind.PartialKeyword)); LocationInfo location = LocationInfo.From(symbol.Locations.FirstOrDefault()); // The generated manager and factory are emitted as top-level // types of the class's namespace, so a nested or generic class // would get an unrelated companion type instead of its members. // An empty namespace means "the model's namespace" to the // generators, and a file-local class cannot be completed from // another file, so those would get a companion type as well. string unsupportedReason = symbol.ContainingType != null ? "it is nested in '" + symbol.ContainingType.ToDisplayString() + "'" : symbol.IsGenericType ? "it is generic" : symbol.ContainingNamespace?.IsGlobalNamespace != false ? "it is declared in the global namespace" : symbol.IsFileLocal ? "it is file-local" : null; return new NodeManagerAttributeDiscovery { Binding = new NodeManagerAttributeBinding { TargetNamespace = targetNamespace, TargetClassName = targetClassName, NamespaceUri = namespaceUri, Design = design, GenerateFactory = generateFactory, GenerateDefaultConstructor = generateDefaultConstructor, // Equatable by content: a fresh array would make every // run's binding unequal to the previous one. AdditionalNamespaceUris = additionalNamespaceUris == null ? null : new EquatableArray(additionalNamespaceUris) }, Location = location, IsPartial = isPartial, UnsupportedReason = unsupportedReason, InvalidExpressions = invalidExpressions }; } /// /// Reads a boolean named argument that defaults to true: /// the flag is off only when it is present and explicitly /// false. /// private static bool IsNotDisabled(AttributeData attribute, string name) { return attribute == null || !attribute.NamedArguments .Any(p => p.Key == name && p.Value.Value is bool b && !b); } private static EquatableArray GetInvalidExpressions( AttributeData attribute, SemanticModel semanticModel, CancellationToken cancellationToken) { if (attribute?.ApplicationSyntaxReference?.GetSyntax(cancellationToken) is not AttributeSyntax attributeSyntax) { return EquatableArray.Empty; } var errors = new List(); AddInvalidExpression( errors, attribute, attributeSyntax, semanticModel, nameof(NodeManagerAttributeBinding.NamespaceUri), isArray: false, cancellationToken); AddInvalidExpression( errors, attribute, attributeSyntax, semanticModel, nameof(NodeManagerAttributeBinding.AdditionalNamespaceUris), isArray: true, cancellationToken); return EquatableArray.From(errors); } private static void AddInvalidExpression( List errors, AttributeData attribute, AttributeSyntax attributeSyntax, SemanticModel semanticModel, string argumentName, bool isArray, CancellationToken cancellationToken) { KeyValuePair namedArgument = attribute.NamedArguments .FirstOrDefault(argument => argument.Key == argumentName); if (namedArgument.Key == null) { return; } TypedConstant value = namedArgument.Value; bool hasError = value.Kind == TypedConstantKind.Error || (isArray && value.Kind == TypedConstantKind.Array && !value.IsNull && value.Values.Any(element => element.Kind == TypedConstantKind.Error)); if (!hasError) { return; } AttributeArgumentSyntax argumentSyntax = attributeSyntax.ArgumentList?.Arguments .FirstOrDefault(argument => argument.NameEquals?.Name.Identifier.ValueText == argumentName); if (argumentSyntax == null) { return; } if (isArray) { ExpressionSyntax[] elementExpressions = GetArrayElementExpressions(argumentSyntax.Expression); if (elementExpressions.Length > 0) { int initialCount = errors.Count; for (int ii = 0; ii < elementExpressions.Length; ii++) { ExpressionSyntax elementExpression = elementExpressions[ii]; bool isInvalid = namedArgument.Value.Kind == TypedConstantKind.Array && ii < namedArgument.Value.Values.Length ? namedArgument.Value.Values[ii].Kind == TypedConstantKind.Error : !semanticModel.GetConstantValue( elementExpression, cancellationToken).HasValue; if (isInvalid) { errors.Add(new NodeManagerAttributeExpressionError( argumentName, elementExpression.ToString(), LocationInfo.From(elementExpression.GetLocation()))); } } if (errors.Count > initialCount) { return; } } } errors.Add(new NodeManagerAttributeExpressionError( argumentName, argumentSyntax.Expression.ToString(), LocationInfo.From(argumentSyntax.Expression.GetLocation()))); } private static ExpressionSyntax[] GetArrayElementExpressions(ExpressionSyntax expression) { SeparatedSyntaxList? expressions = expression switch { ArrayCreationExpressionSyntax array => array.Initializer?.Expressions, ImplicitArrayCreationExpressionSyntax array => array.Initializer.Expressions, _ => null }; return expressions.HasValue ? [.. expressions.Value] : []; } } /// /// An unresolved constant expression in a [NodeManager] attribute. /// internal sealed record class NodeManagerAttributeExpressionError( string ArgumentName, string Expression, LocationInfo Location); }