|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using QilName = System.Xml.Xsl.Qil.QilName;
namespace System.Xml.Xsl.Xslt
{
// Compiler scope manager keeps track of
// Variable declarations
// Namespace declarations
// Extension and excluded namespaces
internal sealed class CompilerScopeManager<V>
{
public enum ScopeFlags
{
BackwardCompatibility = 0x1,
ForwardCompatibility = 0x2,
CanHaveApplyImports = 0x4,
NsDecl = 0x10, // NS declaration
NsExcl = 0x20, // NS Extencion (null for ExcludeAll)
Variable = 0x40,
CompatibilityFlags = BackwardCompatibility | ForwardCompatibility,
InheritedFlags = CompatibilityFlags | CanHaveApplyImports,
ExclusiveFlags = NsDecl | NsExcl | Variable
}
public struct ScopeRecord
{
public int scopeCount;
public ScopeFlags flags;
public string? ncName; // local-name for variable, prefix for namespace, null for extension or excluded namespace
public string? nsUri; // namespace uri
[AllowNull]
public V value; // value for variable, null for namespace
// Exactly one of these three properties is true for every given record
public bool IsVariable { get { return (flags & ScopeFlags.Variable) != 0; } }
public bool IsNamespace { get { return (flags & ScopeFlags.NsDecl) != 0; } }
// public bool IsExNamespace { get { return (flags & ScopeFlags.NsExcl ) != 0; } }
}
// Number of predefined records minus one
private const int LastPredefRecord = 0;
private ScopeRecord[] _records = new ScopeRecord[32];
private int _lastRecord = LastPredefRecord;
// This is cache of records[lastRecord].scopeCount field;
// most often we will have PushScope()/PopScope pare over the same record.
// It has sence to avoid adresing this field through array access.
private int _lastScopes;
public CompilerScopeManager()
{
// The prefix 'xml' is by definition bound to the namespace name http://www.w3.org/XML/1998/namespace
_records[0].flags = ScopeFlags.NsDecl;
_records[0].ncName = "xml";
_records[0].nsUri = XmlReservedNs.NsXml;
}
public CompilerScopeManager(KeywordsTable atoms)
{
_records[0].flags = ScopeFlags.NsDecl;
_records[0].ncName = atoms.Xml;
_records[0].nsUri = atoms.UriXml;
}
public void EnterScope()
{
_lastScopes++;
}
public void ExitScope()
{
if (0 < _lastScopes)
{
_lastScopes--;
}
else
{
while (_records[--_lastRecord].scopeCount == 0)
{
}
_lastScopes = _records[_lastRecord].scopeCount;
_lastScopes--;
}
}
[Conditional("DEBUG")]
public void CheckEmpty()
{
ExitScope();
Debug.Assert(_lastRecord == 0 && _lastScopes == 0, "PushScope() and PopScope() calls are unbalanced");
}
// returns true if ns decls was added to scope
public bool EnterScope([NotNullWhen(true)] NsDecl? nsDecl)
{
_lastScopes++;
bool hasNamespaces = false;
bool excludeAll = false;
for (; nsDecl != null; nsDecl = nsDecl.Prev)
{
if (nsDecl.NsUri == null)
{
Debug.Assert(nsDecl.Prefix == null, "NS may be null only when prefix is null where it is used for extension-element-prefixes='#all'");
excludeAll = true;
}
else if (nsDecl.Prefix == null)
{
AddExNamespace(nsDecl.NsUri);
}
else
{
hasNamespaces = true;
AddNsDeclaration(nsDecl.Prefix, nsDecl.NsUri);
}
}
if (excludeAll)
{
// #all should be on the top of the stack, because all NSs on this element should be excluded as well
AddExNamespace(null);
}
return hasNamespaces;
}
private void AddRecord()
{
// Store cached fields:
_records[_lastRecord].scopeCount = _lastScopes;
// Extend record buffer:
if (++_lastRecord == _records.Length)
{
ScopeRecord[] newRecords = new ScopeRecord[_lastRecord * 2];
Array.Copy(_records, newRecords, _lastRecord);
_records = newRecords;
}
// reset scope count:
_lastScopes = 0;
}
private void AddRecord(ScopeFlags flag, string? ncName, string? uri, [AllowNull] V value)
{
Debug.Assert(flag == (flag & ScopeFlags.ExclusiveFlags) && BitOperations.IsPow2((uint)flag), "One exclusive flag");
Debug.Assert(uri != null || ncName == null, "null, null means exclude '#all'");
ScopeFlags flags = _records[_lastRecord].flags;
bool canReuseLastRecord = (_lastScopes == 0) && (flags & ScopeFlags.ExclusiveFlags) == 0;
if (!canReuseLastRecord)
{
AddRecord();
flags &= ScopeFlags.InheritedFlags;
}
_records[_lastRecord].flags = flags | flag;
_records[_lastRecord].ncName = ncName;
_records[_lastRecord].nsUri = uri;
_records[_lastRecord].value = value;
}
private void SetFlag(ScopeFlags flag, bool value)
{
Debug.Assert(flag == (flag & ScopeFlags.InheritedFlags) && BitOperations.IsPow2((uint)flag), "one inherited flag");
ScopeFlags flags = _records[_lastRecord].flags;
if (((flags & flag) != 0) != value)
{
// lastScopes == records[lastRecord].scopeCount; // we know this because we are cashing it.
bool canReuseLastRecord = _lastScopes == 0; // last record is from last scope
if (!canReuseLastRecord)
{
AddRecord();
flags &= ScopeFlags.InheritedFlags;
}
if (flag == ScopeFlags.CanHaveApplyImports)
{
flags ^= flag;
}
else
{
flags &= ~ScopeFlags.CompatibilityFlags;
if (value)
{
flags |= flag;
}
}
_records[_lastRecord].flags = flags;
}
Debug.Assert((_records[_lastRecord].flags & ScopeFlags.CompatibilityFlags) != ScopeFlags.CompatibilityFlags,
"BackwardCompatibility and ForwardCompatibility flags are mutually exclusive"
);
}
// Add variable to the current scope. Returns false in case of duplicates.
public void AddVariable(QilName varName, V value)
{
Debug.Assert(varName.LocalName != null && varName.NamespaceUri != null);
AddRecord(ScopeFlags.Variable, varName.LocalName, varName.NamespaceUri, value);
}
// Since the prefix might be redefined in an inner scope, we search in descending order in [to, from]
// If interval is empty (from < to), the function returns null.
private string? LookupNamespace(string prefix, int from, int to)
{
Debug.Assert(prefix != null);
for (int record = from; to <= record; --record)
{
string? recPrefix, recNsUri;
ScopeFlags flags = GetName(ref _records[record], out recPrefix, out recNsUri);
if (
(flags & ScopeFlags.NsDecl) != 0 &&
recPrefix == prefix
)
{
return recNsUri;
}
}
return null;
}
public string? LookupNamespace(string prefix)
{
return LookupNamespace(prefix, _lastRecord, 0);
}
private static ScopeFlags GetName(ref ScopeRecord re, out string? prefix, out string? nsUri)
{
prefix = re.ncName;
nsUri = re.nsUri;
return re.flags;
}
public void AddNsDeclaration(string prefix, string nsUri)
{
AddRecord(ScopeFlags.NsDecl, prefix, nsUri, default(V));
}
public void AddExNamespace(string? nsUri)
{
AddRecord(ScopeFlags.NsExcl, null, nsUri, default(V));
}
public bool IsExNamespace(string nsUri)
{
Debug.Assert(nsUri != null);
int exAll = 0;
for (int record = _lastRecord; 0 <= record; record--)
{
string? recPrefix, recNsUri;
ScopeFlags flags = GetName(ref _records[record], out recPrefix, out recNsUri);
if ((flags & ScopeFlags.NsExcl) != 0)
{
Debug.Assert(recPrefix == null);
if (recNsUri == nsUri)
{
return true; // This namespace is excluded
}
if (recNsUri == null)
{
exAll = record; // #all namespaces below are excluded
}
}
else if (
exAll != 0 &&
(flags & ScopeFlags.NsDecl) != 0 &&
recNsUri == nsUri
)
{
// We need to check that this namespace wasn't undefined before last "#all"
bool undefined = false;
for (int prev = record + 1; prev < exAll; prev++)
{
string? prevPrefix;
GetName(ref _records[prev], out prevPrefix, out _);
if (
(flags & ScopeFlags.NsDecl) != 0 &&
prevPrefix == recPrefix
)
{
// We don't care if records[prev].nsUri == records[record].nsUri.
// In this case the namespace was already undefined above.
undefined = true;
break;
}
}
if (!undefined)
{
return true;
}
}
}
return false;
}
private int SearchVariable(string localName, string uri)
{
Debug.Assert(localName != null);
for (int record = _lastRecord; 0 <= record; --record)
{
string? recLocal, recNsUri;
ScopeFlags flags = GetName(ref _records[record], out recLocal, out recNsUri);
if (
(flags & ScopeFlags.Variable) != 0 &&
recLocal == localName &&
recNsUri == uri
)
{
return record;
}
}
return -1;
}
[return: MaybeNull]
public V LookupVariable(string localName, string uri)
{
int record = SearchVariable(localName, uri);
return (record < 0) ? default(V) : _records[record].value;
}
public bool IsLocalVariable(string localName, string uri)
{
int record = SearchVariable(localName, uri);
while (0 <= --record)
{
if (_records[record].scopeCount != 0)
{
return true;
}
}
return false;
}
public bool ForwardCompatibility
{
get { return (_records[_lastRecord].flags & ScopeFlags.ForwardCompatibility) != 0; }
set { SetFlag(ScopeFlags.ForwardCompatibility, value); }
}
public bool BackwardCompatibility
{
get { return (_records[_lastRecord].flags & ScopeFlags.BackwardCompatibility) != 0; }
set { SetFlag(ScopeFlags.BackwardCompatibility, value); }
}
public bool CanHaveApplyImports
{
get { return (_records[_lastRecord].flags & ScopeFlags.CanHaveApplyImports) != 0; }
set { SetFlag(ScopeFlags.CanHaveApplyImports, value); }
}
internal System.Collections.Generic.IEnumerable<ScopeRecord> GetActiveRecords()
{
int currentRecord = _lastRecord + 1;
// This logic comes from NamespaceEnumerator.MoveNext but also returns variables
while (LastPredefRecord < --currentRecord)
{
if (_records[currentRecord].IsNamespace)
{
// This is a namespace declaration
if (LookupNamespace(_records[currentRecord].ncName!, _lastRecord, currentRecord + 1) != null)
{
continue;
}
// Its prefix has not been redefined later in [currentRecord + 1, lastRecord]
}
yield return _records[currentRecord];
}
}
public NamespaceEnumerator GetEnumerator()
{
return new NamespaceEnumerator(this);
}
internal struct NamespaceEnumerator
{
private readonly CompilerScopeManager<V> _scope;
private readonly int _lastRecord;
private int _currentRecord;
public NamespaceEnumerator(CompilerScopeManager<V> scope)
{
_scope = scope;
_lastRecord = scope._lastRecord;
_currentRecord = _lastRecord + 1;
}
public bool MoveNext()
{
while (LastPredefRecord < --_currentRecord)
{
if (_scope._records[_currentRecord].IsNamespace)
{
// This is a namespace declaration
if (_scope.LookupNamespace(_scope._records[_currentRecord].ncName!, _lastRecord, _currentRecord + 1) == null)
{
// Its prefix has not been redefined later in [currentRecord + 1, lastRecord]
return true;
}
}
}
return false;
}
public ScopeRecord Current
{
get
{
Debug.Assert(LastPredefRecord <= _currentRecord && _currentRecord <= _scope._lastRecord, "MoveNext() either was not called or returned false");
Debug.Assert(_scope._records[_currentRecord].IsNamespace);
return _scope._records[_currentRecord];
}
}
}
}
}
|