File: Language\Legacy\LegacySyntaxNodeExtensions.cs
Web Access
Project: src\src\roslyn\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.Frozen;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Razor.Language.Syntax;

namespace Microsoft.AspNetCore.Razor.Language.Legacy;

internal static partial class LegacySyntaxNodeExtensions
{
    private class SpanData
    {
        public SyntaxNodeOrToken? Previous;
        public bool PreviousComputed;
        public SyntaxNodeOrToken? Next;
        public bool NextComputed;
    }

    /// <summary>
    ///  Caches previous/next span result for a particular node. A conditional weak table
    ///  is used to avoid adding fields to all syntax nodes.
    /// </summary>
    private static readonly ConditionalWeakTable<SyntaxNode, SpanData> s_spanDataTable = new();

    private static readonly FrozenSet<SyntaxKind> s_transitionSpanKinds = FrozenSet.Create(
        SyntaxKind.CSharpTransition,
        SyntaxKind.MarkupTransition);

    private static readonly FrozenSet<SyntaxKind> s_commentSpanKinds = FrozenSet.Create(
        SyntaxKind.RazorCommentTransition,
        SyntaxKind.RazorCommentStar,
        SyntaxKind.RazorCommentLiteral);

    private static readonly FrozenSet<SyntaxKind> s_codeSpanKinds = FrozenSet.Create(
        SyntaxKind.CSharpStatementLiteral,
        SyntaxKind.CSharpExpressionLiteral,
        SyntaxKind.CSharpEphemeralTextLiteral);

    private static readonly FrozenSet<SyntaxKind> s_markupSpanKinds = FrozenSet.Create(
        SyntaxKind.MarkupTextLiteral,
        SyntaxKind.MarkupEphemeralTextLiteral);

    private static readonly FrozenSet<SyntaxKind> s_allSpanKinds = CreateAllSpanKindsSet();

    private static FrozenSet<SyntaxKind> CreateAllSpanKindsSet()
    {
        var set = new HashSet<SyntaxKind>();

        set.UnionWith(s_transitionSpanKinds);
        set.Add(SyntaxKind.RazorMetaCode);
        set.UnionWith(s_commentSpanKinds);
        set.UnionWith(s_codeSpanKinds);
        set.UnionWith(s_markupSpanKinds);
        set.Add(SyntaxKind.UnclassifiedTextLiteral);

        return set.ToFrozenSet();
    }

    internal static ISpanChunkGenerator? GetChunkGenerator(this SyntaxNode node)
     => (node as ILegacySyntax)?.ChunkGenerator;

    public static SpanEditHandler? GetEditHandler(this SyntaxNode node)
        => (node as ILegacySyntax)?.EditHandler;

    [Obsolete("Use FindToken or FindInnermostNode instead", error: false)]
    public static SyntaxNode? LocateOwner(this SyntaxNode node, SourceChange change)
    {
        ArgHelper.ThrowIfNull(node);

        if (change.Span.AbsoluteIndex < node.Position)
        {
            // Early escape for cases where changes overlap multiple spans
            // In those cases, the span will return false, and we don't want to search the whole tree
            // So if the current span starts after the change, we know we've searched as far as we need to
            return null;
        }

        if (node.EndPosition < change.Span.AbsoluteIndex)
        {
            // no need to look into this node as it completely precedes the change
            return null;
        }

        if (node.IsSpanKind())
        {
            var editHandler = node.GetEditHandler() ?? SpanEditHandler.GetDefault(AcceptedCharactersInternal.Any);
            return editHandler.OwnsChange(node, change) ? node : null;
        }

        return node switch
        {
            MarkupStartTagSyntax startTag => LocateOwnerForSyntaxList(startTag.LegacyChildren, change),
            MarkupEndTagSyntax endTag => LocateOwnerForSyntaxList(endTag.LegacyChildren, change),
            MarkupTagHelperStartTagSyntax startTagHelper => LocateOwnerForSyntaxList(startTagHelper.LegacyChildren, change),
            MarkupTagHelperEndTagSyntax endTagHelper => LocateOwnerForSyntaxList(endTagHelper.LegacyChildren, change),
            _ => LocateOwnerForChildSyntaxList(node.ChildNodesAndTokens(), change)
        };

        static SyntaxNode? LocateOwnerForSyntaxList(in SyntaxList<RazorSyntaxNode> list, SourceChange change)
        {
            foreach (var child in list)
            {
                if (child.LocateOwner(change) is { } owner)
                {
                    return owner;
                }
            }

            return null;
        }

        static SyntaxNode? LocateOwnerForChildSyntaxList(in ChildSyntaxList list, SourceChange change)
        {
            foreach (var child in list)
            {
                if (child.AsNode(out var node) && node.LocateOwner(change) is { } owner)
                {
                    return owner;
                }
            }

            return null;
        }
    }

    public static bool IsTransitionSpanKind(this SyntaxNode node)
    {
        ArgHelper.ThrowIfNull(node);

        return s_transitionSpanKinds.Contains(node.Kind);
    }

    public static bool IsMetaCodeSpanKind(this SyntaxNodeOrToken nodeOrToken)
    {
        return nodeOrToken.AsNode(out var node) && node.IsMetaCodeSpanKind();
    }

    public static bool IsMetaCodeSpanKind(this SyntaxNode node)
    {
        ArgHelper.ThrowIfNull(node);

        return node.Kind is SyntaxKind.RazorMetaCode;
    }

    public static bool IsCommentSpanKind(this SyntaxNode node)
    {
        ArgHelper.ThrowIfNull(node);

        return s_commentSpanKinds.Contains(node.Kind);
    }

    public static bool IsCodeSpanKind(this SyntaxNode node)
    {
        ArgHelper.ThrowIfNull(node);

        return s_codeSpanKinds.Contains(node.Kind);
    }

    public static bool IsMarkupSpanKind(this SyntaxNode node)
    {
        ArgHelper.ThrowIfNull(node);

        return s_markupSpanKinds.Contains(node.Kind);
    }

    public static bool IsNoneSpanKind(this SyntaxNode node)
    {
        ArgHelper.ThrowIfNull(node);

        return node.Kind is SyntaxKind.UnclassifiedTextLiteral;
    }

    public static bool IsSpanKind(this SyntaxNode node)
        => s_allSpanKinds.Contains(node.Kind);

    public static bool IsSpanKind(this SyntaxNodeOrToken nodeOrToken)
        => s_allSpanKinds.Contains(nodeOrToken.Kind);

    private static IEnumerable<SyntaxNodeOrToken> FlattenSpansInReverse(this SyntaxNode node)
    {
        using var stack = new ChildSyntaxListReversedEnumeratorStack(node);

        while (stack.TryGetNextNodeOrToken(out var nextNode))
        {
            if (nextNode.AsNode() is MarkupStartTagSyntax startTag)
            {
                var children = startTag.LegacyChildren;

                for (var i = children.Count - 1; i >= 0; i--)
                {
                    var tagChild = children[i];
                    if (tagChild.IsSpanKind())
                    {
                        yield return tagChild;
                    }
                }
            }
            else if (nextNode.AsNode() is MarkupEndTagSyntax endTag)
            {
                var children = endTag.LegacyChildren;

                for (var i = children.Count - 1; i >= 0; i--)
                {
                    var tagChild = children[i];
                    if (tagChild.IsSpanKind())
                    {
                        yield return tagChild;
                    }
                }
            }
            else if (nextNode.IsSpanKind())
            {
                yield return nextNode;
            }
        }
    }

    public static IEnumerable<SyntaxNode> FlattenSpans(this SyntaxNode node)
    {
        ArgHelper.ThrowIfNull(node);

        foreach (var child in node.DescendantNodes())
        {
            if (child is MarkupStartTagSyntax startTag)
            {
                foreach (var tagChild in startTag.LegacyChildren)
                {
                    if (tagChild.IsSpanKind())
                    {
                        yield return tagChild;
                    }
                }
            }
            else if (child is MarkupEndTagSyntax endTag)
            {
                foreach (var tagChild in endTag.LegacyChildren)
                {
                    if (tagChild.IsSpanKind())
                    {
                        yield return tagChild;
                    }
                }
            }
            else if (child.IsSpanKind())
            {
                yield return child;
            }
        }
    }

    public static SyntaxNodeOrToken? PreviousSpan(this SyntaxNode node)
    {
        ArgHelper.ThrowIfNull(node);

        var spanData = s_spanDataTable.GetOrCreateValue(node);

        lock (spanData)
        {
            if (spanData.PreviousComputed)
            {
                return spanData.Previous;
            }

            var parent = node.Parent;
            while (parent is not null)
            {
                foreach (var span in parent.FlattenSpansInReverse())
                {
                    if (span.EndPosition <= node.Position && span != node)
                    {
                        spanData.PreviousComputed = true;
                        spanData.Previous = span;

                        return span;
                    }
                }

                parent = parent.Parent;
            }

            spanData.PreviousComputed = true;
            spanData.Previous = default;

            return default;
        }
    }

    public static SyntaxNodeOrToken? NextSpan(this SyntaxNode node)
    {
        ArgHelper.ThrowIfNull(node);

        var spanData = s_spanDataTable.GetOrCreateValue(node);

        lock (spanData)
        {
            if (spanData.NextComputed)
            {
                return spanData.Next;
            }

            var parent = node.Parent;
            while (parent is not null)
            {
                foreach (var span in parent.FlattenSpans())
                {
                    if (span.Position >= node.Position && span != node)
                    {
                        spanData.NextComputed = true;
                        spanData.Next = span;

                        return span;
                    }
                }

                parent = parent.Parent;
            }

            spanData.NextComputed = true;
            spanData.Next = default;

            return null;
        }
    }
}