/* ========================================================================
* 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);
}