/* ========================================================================
* 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.Concurrent;
using System.Collections.Immutable;
using System.Threading;
namespace Opc.Ua.SourceGeneration
{
///
/// Ambient, execution-context-scoped policy that lets the companion-model
/// generator degrade a reference to a standard
/// (global::Opc.Ua.*) typed MethodState class to the base
/// global::Opc.Ua.MethodState when that typed class is neither
/// present in the compilation (a curated Opc.Ua.Core may omit
/// standard classes such as the FileDirectoryType method states)
/// nor declared by the current generation pass.
///
///
/// The scope is only entered around model generation
/// (ModelCompilation); the Stack generator that builds
/// Opc.Ua.Core itself never enters it, so it keeps the
/// assume-present behaviour. A method-state class is recorded as it is
/// declared (the NodeStates.g.cs pass, which runs before the
/// NodeStates.ex.g.cs reference pass in the same logical execution
/// context), so a model that generates its own standard-namespace
/// method-state classes (e.g. GDS) is never wrongly degraded.
///
internal static class StandardMethodStateFallback
{
///
/// Enter a fallback scope for the duration of one model compilation.
/// The previous scope (if any) is restored on dispose so nested or
/// sequential passes do not leak state.
///
///
/// Simple names of the standard Opc.Ua.*State classes present
/// in the compilation (including referenced assemblies).
///
public static IDisposable Enter(ImmutableHashSet availableStateTypeNames)
{
FallbackContext? previousContext = s_context.Value;
s_context.Value = new FallbackContext(
availableStateTypeNames ?? ImmutableHashSet.Empty);
return new Scope(previousContext!);
}
///
/// Record that the current pass declares the given method-state class.
/// Only classes that resolve to the standard global::Opc.Ua.*
/// namespace (with no further sub-namespace, e.g. a model such as GDS
/// that emits its own Opc.Ua.*MethodState) are tracked, so a
/// later reference to the same standard class is not degraded.
///
///
/// The fully-qualified typed class name, e.g.
/// global::Opc.Ua.RequestAccessTokenMethodState.
///
public static void RecordDeclaredMethodState(string typedClassName)
{
FallbackContext? context = s_context.Value;
if (context != null &&
TryGetStandardSimpleName(typedClassName, out string? simpleName))
{
context.DeclaredMethodStateNames.TryAdd(simpleName!, 0);
}
}
///
/// Whether the given fully-qualified typed method-state reference
/// should be degraded to the base global::Opc.Ua.MethodState.
/// True only when a scope is active, the reference names a standard
/// global::Opc.Ua.*MethodState class (no sub-namespace), and
/// that class is neither declared by this pass nor present in the
/// compilation.
///
public static bool ShouldFallBackToBase(string typedClassName)
{
FallbackContext? context = s_context.Value;
if (context == null)
{
return false;
}
if (!TryGetStandardSimpleName(typedClassName, out string? simpleName))
{
return false;
}
if (context.DeclaredMethodStateNames.ContainsKey(simpleName!))
{
return false;
}
if (context.AvailableStateTypeNames.Contains(simpleName!))
{
return false;
}
return true;
}
///
/// Extract the simple class name when
/// is a reference to a type directly in the standard Opc.Ua
/// namespace (i.e. global::Opc.Ua.Name with no further dotted
/// sub-namespace such as global::Opc.Ua.Di.Name). An optional
/// leading new (factory form) is tolerated.
///
private static bool TryGetStandardSimpleName(string typedClassName, out string? simpleName)
{
simpleName = null;
if (string.IsNullOrEmpty(typedClassName))
{
return false;
}
const string factoryPrefix = "new ";
const string standardPrefix = "global::Opc.Ua.";
string candidate = typedClassName.StartsWith(factoryPrefix, StringComparison.Ordinal)
? typedClassName.Substring(factoryPrefix.Length)
: typedClassName;
if (!candidate.StartsWith(standardPrefix, StringComparison.Ordinal))
{
return false;
}
string remainder = candidate.Substring(standardPrefix.Length);
if (remainder.Length == 0 || remainder.Contains('.', StringComparison.Ordinal))
{
return false;
}
simpleName = remainder;
return true;
}
private sealed class FallbackContext
{
public FallbackContext(ImmutableHashSet availableStateTypeNames)
{
AvailableStateTypeNames = availableStateTypeNames;
DeclaredMethodStateNames =
new ConcurrentDictionary(StringComparer.Ordinal);
}
public ImmutableHashSet AvailableStateTypeNames { get; }
public ConcurrentDictionary DeclaredMethodStateNames { get; }
}
private sealed class Scope : IDisposable
{
public Scope(FallbackContext previousContext)
{
m_previousContext = previousContext;
}
public void Dispose()
{
s_context.Value = m_previousContext;
}
private readonly FallbackContext m_previousContext;
}
private static readonly AsyncLocal s_context = new();
}
}