File: Language\Syntax\SyntaxNode.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.AspNetCore.Razor.Language.Syntax;
 
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
internal abstract partial class SyntaxNode(GreenNode green, SyntaxNode parent, int position)
{
    internal GreenNode Green { get; } = green;
    public SyntaxNode Parent { get; } = parent;
    public int Position { get; } = position;
 
    public int EndPosition => Position + Width;
 
    public SyntaxKind Kind => Green.Kind;
 
    public int Width => Green.Width;
 
    public int SpanStart => Position;
 
    public TextSpan Span => new(Position, Green.Width);
 
    internal int SlotCount => Green.SlotCount;
 
    public bool IsList => Green.IsList;
 
    public bool IsMissing => Green.IsMissing;
 
    public bool IsToken => Green.IsToken;
 
    public bool ContainsDiagnostics => Green.ContainsDiagnostics;
 
    internal abstract SyntaxNode? GetNodeSlot(int index);
 
    internal abstract SyntaxNode? GetCachedSlot(int index);
 
    internal SyntaxNode? GetRed(ref SyntaxNode? field, int slot)
    {
        var result = field;
 
        if (result == null)
        {
            var green = Green.GetSlot(slot);
            if (green != null)
            {
                Interlocked.CompareExchange(ref field, green.CreateRed(this, GetChildPosition(slot)), null);
                result = field;
            }
        }
 
        return result;
    }
 
    // Special case of above function where slot = 0, does not need GetChildPosition
    internal SyntaxNode? GetRedAtZero(ref SyntaxNode? field)
    {
        var result = field;
 
        if (result == null)
        {
            var green = Green.GetSlot(0);
            if (green != null)
            {
                Interlocked.CompareExchange(ref field, green.CreateRed(this, Position), null);
                result = field;
            }
        }
 
        return result;
    }
 
    protected T? GetRed<T>(ref T field, int slot)
        where T : SyntaxNode
    {
        var result = field;
 
        if (result == null)
        {
            var green = Green.GetSlot(slot);
            if (green != null)
            {
                Interlocked.CompareExchange(ref field, (T)green.CreateRed(this, this.GetChildPosition(slot)), null);
                result = field;
            }
        }
 
        return result;
    }
 
    // special case of above function where slot = 0, does not need GetChildPosition
    protected T? GetRedAtZero<T>(ref T field)
        where T : SyntaxNode
    {
        var result = field;
 
        if (result == null)
        {
            var green = Green.GetSlot(0);
            if (green != null)
            {
                Interlocked.CompareExchange(ref field, (T)green.CreateRed(this, Position), null);
                result = field;
            }
        }
 
        return result;
    }
 
    internal SyntaxNode? GetRedElement(ref SyntaxNode? element, int slot)
    {
        Debug.Assert(IsList);
 
        var result = element;
 
        if (result == null)
        {
            var green = Green.GetRequiredSlot(slot);
            // passing list's parent
            Interlocked.CompareExchange(ref element, green.CreateRed(Parent, GetChildPosition(slot)), null);
            result = element;
        }
 
        return result;
    }
 
    internal int GetChildIndex(int slot)
    {
        var index = 0;
 
        for (var i = 0; i < slot; i++)
        {
            var item = Green.GetSlot(i);
            if (item != null)
            {
                if (item.IsList)
                {
                    index += item.SlotCount;
                }
                else
                {
                    index++;
                }
            }
        }
 
        return index;
    }
 
    internal int GetChildPosition(int index)
    {
        var offset = 0;
        var green = Green;
 
        while (index > 0)
        {
            index--;
            var prevSibling = GetCachedSlot(index);
            if (prevSibling != null)
            {
                return prevSibling.EndPosition + offset;
            }
 
            var greenChild = green.GetSlot(index);
            if (greenChild != null)
            {
                offset += greenChild.Width;
            }
        }
 
        return Position + offset;
    }
 
    internal SyntaxNodeOrToken ChildThatContainsPosition(int position)
    {
        //PERF: it is very important to keep this method fast.
 
        if (!Span.Contains(position))
        {
            throw new ArgumentOutOfRangeException(nameof(position));
        }
 
        var childNodeOrToken = ChildSyntaxList.ChildThatContainsPosition(this, position);
        Debug.Assert(childNodeOrToken.Span.Contains(position), "ChildThatContainsPosition's return value does not contain the requested position.");
        return childNodeOrToken;
    }
 
    /// <summary>
    /// Gets the first token of the tree rooted by this node. Skips zero-width tokens.
    /// </summary>
    /// <returns>The first token or <c>default(SyntaxToken)</c> if it doesn't exist.</returns>
    public SyntaxToken GetFirstToken(bool includeZeroWidth = false)
    {
        return SyntaxNavigator.GetFirstToken(this, includeZeroWidth);
    }
 
    /// <summary>
    /// Gets the last token of the tree rooted by this node. Skips zero-width tokens.
    /// </summary>
    /// <returns>The last token or <c>default(SyntaxToken)</c> if it doesn't exist.</returns>
    public SyntaxToken GetLastToken(bool includeZeroWidth = false)
    {
        return SyntaxNavigator.GetLastToken(this, includeZeroWidth);
    }
 
    /// <summary>
    /// The list of child nodes of this node, where each element is a SyntaxNode instance.
    /// </summary>
    public ChildSyntaxList ChildNodesAndTokens()
    {
        return new ChildSyntaxList(this);
    }
 
    /// <summary>
    /// Gets a list of the child nodes in prefix document order.
    /// </summary>
    public IEnumerable<SyntaxNode> ChildNodes()
    {
        foreach (var nodeOrToken in ChildNodesAndTokens())
        {
            if (nodeOrToken.IsNode)
            {
                yield return nodeOrToken.AsNode()!;
            }
        }
    }
 
    /// <summary>
    /// Gets a list of the direct child tokens of this node.
    /// </summary>
    public IEnumerable<SyntaxToken> ChildTokens()
    {
        foreach (var nodeOrToken in ChildNodesAndTokens())
        {
            if (nodeOrToken.IsToken)
            {
                yield return nodeOrToken.AsToken();
            }
        }
    }
 
    /// <summary>
    /// Gets a list of ancestor nodes
    /// </summary>
    public IEnumerable<SyntaxNode> Ancestors()
        => Parent?.AncestorsAndSelf() ?? [];
 
    /// <summary>
    /// Gets a list of ancestor nodes (including this node)
    /// </summary>
    public IEnumerable<SyntaxNode> AncestorsAndSelf()
    {
        for (var node = this; node != null; node = node.Parent)
        {
            yield return node;
        }
    }
 
    /// <summary>
    /// Gets the first node of type TNode that matches the predicate.
    /// </summary>
    public TNode? FirstAncestorOrSelf<TNode>(Func<TNode, bool>? predicate = null)
        where TNode : SyntaxNode
    {
        for (var node = this; node != null; node = node.Parent)
        {
            if (node is TNode typedNode && (predicate == null || predicate(typedNode)))
            {
                return typedNode;
            }
        }
 
        return default;
    }
 
    /// <summary>
    /// Gets a list of descendant nodes in prefix document order.
    /// </summary>
    /// <param name="descendIntoChildren">An optional function that determines if the search descends into the argument node's children.</param>
    public IEnumerable<SyntaxNode> DescendantNodes(Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesImpl(Span, descendIntoChildren, includeSelf: false);
    }
 
    /// <summary>
    /// Gets a list of descendant nodes in prefix document order.
    /// </summary>
    /// <param name="span">The span the node's full span must intersect.</param>
    /// <param name="descendIntoChildren">An optional function that determines if the search descends into the argument node's children.</param>
    public IEnumerable<SyntaxNode> DescendantNodes(TextSpan span, Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesImpl(span, descendIntoChildren, includeSelf: false);
    }
 
    /// <summary>
    /// Gets a list of descendant nodes (including this node) in prefix document order.
    /// </summary>
    /// <param name="descendIntoChildren">An optional function that determines if the search descends into the argument node's children.</param>
    public IEnumerable<SyntaxNode> DescendantNodesAndSelf(Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesImpl(Span, descendIntoChildren, includeSelf: true);
    }
 
    /// <summary>
    /// Gets a list of descendant nodes (including this node) in prefix document order.
    /// </summary>
    /// <param name="span">The span the node's full span must intersect.</param>
    /// <param name="descendIntoChildren">An optional function that determines if the search descends into the argument node's children.</param>
    public IEnumerable<SyntaxNode> DescendantNodesAndSelf(TextSpan span, Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesImpl(span, descendIntoChildren, includeSelf: true);
    }
 
    /// <summary>
    /// Gets a list of descendant nodes and tokens in prefix document order.
    /// </summary>
    /// <param name="descendIntoChildren">An optional function that determines if the search descends into the argument node's children.</param>
    public IEnumerable<SyntaxNodeOrToken> DescendantNodesAndTokens(Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesAndTokensImpl(Span, descendIntoChildren, includeSelf: false);
    }
 
    //// <summary>
    /// Gets a list of the descendant nodes and tokens in prefix document order.
    /// </summary>
    /// <param name="span">The span the node's full span must intersect.</param>
    /// <param name="descendIntoChildren">An optional function that determines if the search descends into the argument node's children.</param>
    public IEnumerable<SyntaxNodeOrToken> DescendantNodesAndTokens(TextSpan span, Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesAndTokensImpl(span, descendIntoChildren, includeSelf: false);
    }
 
    /// <summary>
    /// Gets a list of descendant nodes (including this node) in prefix document order.
    /// </summary>
    /// <param name="descendIntoChildren">An optional function that determines if the search descends into the argument node's children.</param>
    public IEnumerable<SyntaxNodeOrToken> DescendandNodesAndTokensAndSelf(Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesAndTokensImpl(Span, descendIntoChildren, includeSelf: true);
    }
 
    /// <summary>
    /// Gets a list of descendant nodes (including this node) in prefix document order.
    /// </summary>
    /// <param name="descendIntoChildren">An optional function that determines if the search descends into the argument node's children.</param>
    public IEnumerable<SyntaxNodeOrToken> DescendandNodesAndTokensAndSelf(TextSpan span, Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesAndTokensImpl(span, descendIntoChildren, includeSelf: true);
    }
 
    /// <summary>
    /// Gets a list of all the tokens in the span of this node.
    /// </summary>
    public IEnumerable<SyntaxToken> DescendantTokens(Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesAndTokens(Span, descendIntoChildren)
            .Where(static x => x.IsToken)
            .Select(static x => x.AsToken());
    }
 
    /// <summary>
    /// Gets a list of all the tokens in the span of this node.
    /// </summary>
    public IEnumerable<SyntaxToken> DescendantTokens(TextSpan span, Func<SyntaxNode, bool>? descendIntoChildren = null)
    {
        return DescendantNodesAndTokens(span, descendIntoChildren)
            .Where(static x => x.IsToken)
            .Select(static x => x.AsToken());
    }
 
    protected internal abstract SyntaxNode ReplaceCore<TNode>(
        IEnumerable<TNode>? nodes = null,
        Func<TNode, TNode, SyntaxNode>? computeReplacementNode = null,
        IEnumerable<SyntaxToken>? tokens = null,
        Func<SyntaxToken, SyntaxToken, SyntaxToken>? computeReplacementToken = null)
        where TNode : SyntaxNode;
 
    protected internal abstract SyntaxNode ReplaceNodeInListCore(SyntaxNode originalNode, IEnumerable<SyntaxNode> replacementNodes);
    protected internal abstract SyntaxNode InsertNodesInListCore(SyntaxNode nodeInList, IEnumerable<SyntaxNode> nodesToInsert, bool insertBefore);
    protected internal abstract SyntaxNode ReplaceTokenInListCore(SyntaxToken originalToken, IEnumerable<SyntaxToken> newTokens);
    protected internal abstract SyntaxNode InsertTokensInListCore(SyntaxToken originalToken, IEnumerable<SyntaxToken> newTokens, bool insertBefore);
 
    public RazorDiagnostic[] GetDiagnostics()
    {
        return Green.GetDiagnostics();
    }
 
    public bool IsEquivalentTo(SyntaxNode other)
    {
        if (this == other)
        {
            return true;
        }
 
        if (other == null)
        {
            return false;
        }
 
        return Green.IsEquivalentTo(other.Green);
    }
 
    /// <summary>
    /// Finds a descendant token of this node whose span includes the supplied position.
    /// </summary>
    /// <param name="position">The character position of the token relative to the beginning of the file.</param>
    /// <param name="includeWhitespace">
    /// True to return whitespace or newline tokens. If false, finds the closest non-whitespace, non-newline token that matches the following algorithm:
    /// <list type="number">
    /// <item>
    /// <description>
    /// Scan backwards until a non-whitespace token is found. If a newline is found, continue to the next step. Otherwise, return the found token.
    /// </description>
    /// </item>
    /// <item>
    /// <description>
    /// Scan forwards until a non-whitespace, non-newline token is found. Return the found token.
    /// </description>
    /// </item>
    /// </param>
    /// <exception cref="ArgumentOutOfRangeException">
    /// Thrown when requested position is out of range of the root token requested. This includes the whitespace scanning: calling FindToken(0, false)
    /// on a whitespace token will throw.
    /// </exception>
    public SyntaxToken FindToken(int position, bool includeWhitespace = false)
    {
        if (position == EndPosition && this is RazorDocumentSyntax document)
        {
            return document.EndOfFile;
        }
 
        if (!Span.Contains(position))
        {
            throw new ArgumentOutOfRangeException(nameof(position));
        }
 
        SyntaxNodeOrToken curNode = this;
 
        while (true)
        {
            Debug.Assert(curNode.Kind is < SyntaxKind.FirstAvailableTokenKind and >= 0);
            Debug.Assert(curNode.Span.Contains(position));
 
            var node = curNode.AsNode();
 
            if (node != null)
            {
                curNode = node.ChildThatContainsPosition(position);
            }
            else
            {
                // Once we've found the token that covers the exact position, we potentially need to account for whitespace to
                // partially emulate Roslyn's behavior. The rule is pretty simple:
                //
                //  After a non-whitespace token, all whitespace up to and including the next newline is considered part of the previous token.
                //  All whitespace after it is considered part of the next token.
                //
                // Roslyn, of course, includes all trivia in this rule, and also uses trivia to represent comments. Razor does neither of these things,
                // and we only want to skip past whitespace. Therefore, the algorithm we implement is:
                //
                //  Walk backwards until we find a non-whitespace token. If we find something that isn't a newline, that is the node requested.
                //  If we find a newline, we need to walk forwards until we find the first non-whitespace or newline token. That is the node requested.
                var foundToken = curNode.AsToken();
                if (includeWhitespace || foundToken.Kind is not (SyntaxKind.Whitespace or SyntaxKind.NewLine))
                {
                    return foundToken;
                }
 
                var originalFoundToken = foundToken;
 
                // Walk backwards until we find a non-whitespace token. We accomplish this by looking up the stack and walking nodes backwards from where we
                // were located.
                if (tryWalkBackwards(originalFoundToken, out foundToken))
                {
                    return foundToken;
                }
 
                // Encountered a newline while backtracking, so we need to walk forward instead.
                return walkForward(originalFoundToken);
            }
 
            bool tryWalkBackwards(SyntaxToken originalFoundToken, out SyntaxToken foundToken)
            {
                foundToken = originalFoundToken;
                do
                {
                    foundToken = foundToken.GetPreviousToken(includeZeroWidth: true);
                    if (foundToken.Kind is SyntaxKind.None or SyntaxKind.NewLine)
                    {
                        return false;
                    }
 
                    if (foundToken.SpanStart < this.SpanStart)
                    {
                        // User requested a position that is out of range of the root token requested.
                        throw new ArgumentOutOfRangeException(nameof(position));
                    }
                }
                while (foundToken.Kind is SyntaxKind.Whitespace);
 
                return true;
            }
 
            SyntaxToken walkForward(SyntaxToken originalFoundToken)
            {
                var currentToken = originalFoundToken;
                do
                {
                    currentToken = currentToken.GetNextToken(includeZeroWidth: true);
 
                    if (currentToken.Kind == SyntaxKind.None || currentToken.Span.End > this.Span.End)
                    {
                        // Walked all the way forward to the end of the root that was requested and did not find any non-whitespace tokens. The user requested
                        // something out of range.
                        throw new ArgumentOutOfRangeException(nameof(position));
                    }
                }
                while (currentToken is { Kind: SyntaxKind.NewLine or SyntaxKind.Whitespace });
 
                return currentToken;
            }
        }
    }
 
    public override string ToString()
    {
        return Green.ToString();
    }
 
    protected virtual string GetDebuggerDisplay()
    {
        if (IsToken)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0};[{1}]", Kind, ToString());
        }
 
        return string.Format(CultureInfo.InvariantCulture, "{0} [{1}..{2})", Kind, Position, EndPosition);
    }
}