File: Language\Syntax\SyntaxNodeExtensions.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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.CodeAnalysis.CSharp;
 
namespace Microsoft.AspNetCore.Razor.Language.Syntax;
 
internal static class SyntaxNodeExtensions
{
    public static TNode WithDiagnostics<TNode>(this TNode node, params RazorDiagnostic[] diagnostics) where TNode : SyntaxNode
    {
        return (TNode)node.Green.SetDiagnostics(diagnostics).CreateRed(node.Parent, node.Position);
    }
 
    public static TNode AppendDiagnostic<TNode>(this TNode node, params ReadOnlySpan<RazorDiagnostic> diagnostics) where TNode : SyntaxNode
    {
        RazorDiagnostic[] allDiagnostics = [
            .. node.GetDiagnostics(),
            .. diagnostics];
 
        return node.WithDiagnostics(allDiagnostics);
    }
 
    /// <summary>
    /// Gets top-level and nested diagnostics from the <paramref name="node"/>.
    /// </summary>
    /// <typeparam name="TNode">The type of syntax node.</typeparam>
    /// <param name="node">The syntax node.</param>
    /// <param name="list"></param>
    /// <returns>The list of <see cref="RazorDiagnostic">RazorDiagnostics</see>.</returns>
    public static void CollectAllDiagnostics<TNode>(this TNode node, List<RazorDiagnostic> list)
        where TNode : SyntaxNode
    {
        var walker = new DiagnosticSyntaxWalker(list);
        walker.Visit(node);
    }
 
    public static SourceLocation GetSourceLocation(this SyntaxNodeOrToken nodeOrToken, RazorSourceDocument source)
        => nodeOrToken.IsToken
            ? nodeOrToken.AsToken().GetSourceLocation(source)
            : nodeOrToken.AsNode()?.GetSourceLocation(source) ?? default;
 
    public static SourceLocation GetSourceLocation(this SyntaxNode node, RazorSourceDocument source)
    {
        try
        {
            if (source.Text.Length == 0)
            {
                // Just a marker symbol
                return new SourceLocation(source.FilePath, 0, 0, 0);
            }
            if (node.Position == source.Text.Length)
            {
                // E.g. Marker symbol at the end of the document
                var lastPosition = source.Text.Length - 1;
                var endsWithLineBreak = SyntaxFacts.IsNewLine(source.Text[lastPosition]);
                var lastLocation = source.Text.Lines.GetLinePosition(lastPosition);
                return new SourceLocation(
                    source.FilePath, // GetLocation prefers RelativePath but we want FilePath.
                    lastPosition + 1,
                    lastLocation.Line + (endsWithLineBreak ? 1 : 0),
                    endsWithLineBreak ? 0 : lastLocation.Character + 1);
            }
 
            var location = source.Text.Lines.GetLinePosition(node.Position);
            return new SourceLocation(
                source.FilePath, // GetLocation prefers RelativePath but we want FilePath.
                node.Position,
                location);
        }
        catch (IndexOutOfRangeException)
        {
            Debug.Assert(false, "Node position should stay within document length.");
            return new SourceLocation(source.FilePath, node.Position, 0, 0);
        }
    }
 
    public static SourceLocation GetSourceLocation(this SyntaxToken token, RazorSourceDocument source)
    {
        try
        {
            if (source.Text.Length == 0)
            {
                // Just a marker symbol
                return new SourceLocation(source.FilePath, 0, 0, 0);
            }
            if (token.Position == source.Text.Length)
            {
                // E.g. Marker symbol at the end of the document
                var lastPosition = source.Text.Length - 1;
                var endsWithLineBreak = SyntaxFacts.IsNewLine(source.Text[lastPosition]);
                var lastLocation = source.Text.Lines.GetLinePosition(lastPosition);
                return new SourceLocation(
                    source.FilePath, // GetLocation prefers RelativePath but we want FilePath.
                    lastPosition + 1,
                    lastLocation.Line + (endsWithLineBreak ? 1 : 0),
                    endsWithLineBreak ? 0 : lastLocation.Character + 1);
            }
 
            var location = source.Text.Lines.GetLinePosition(token.Position);
            return new SourceLocation(
                source.FilePath, // GetLocation prefers RelativePath but we want FilePath.
                token.Position,
                location);
        }
        catch (IndexOutOfRangeException)
        {
            Debug.Assert(false, "Node position should stay within document length.");
            return new SourceLocation(source.FilePath, token.Position, 0, 0);
        }
    }
 
    public static SourceSpan GetSourceSpan(this SyntaxNode node, RazorSourceDocument source)
    {
        var location = node.GetSourceLocation(source);
        var endLocation = source.Text.Lines.GetLinePosition(node.EndPosition);
        var lineCount = endLocation.Line - location.LineIndex;
        return new SourceSpan(location.FilePath, location.AbsoluteIndex, location.LineIndex, location.CharacterIndex, node.Width, lineCount, endLocation.Character);
    }
 
    public static SourceSpan GetSourceSpan(this SyntaxToken token, RazorSourceDocument source)
    {
        var location = token.GetSourceLocation(source);
        var endLocation = source.Text.Lines.GetLinePosition(token.EndPosition);
        var lineCount = endLocation.Line - location.LineIndex;
        return new SourceSpan(location.FilePath, location.AbsoluteIndex, location.LineIndex, location.CharacterIndex, token.Width, lineCount, endLocation.Character);
    }
 
    /// <summary>
    /// Creates a new tree of nodes with the specified nodes, tokens and trivia replaced.
    /// </summary>
    /// <typeparam name="TRoot">The type of the root node.</typeparam>
    /// <param name="root">The root node of the tree of nodes.</param>
    /// <param name="nodes">The nodes to be replaced.</param>
    /// <param name="computeReplacementNode">A function that computes a replacement node for the
    /// argument nodes. The first argument is the original node. The second argument is the same
    /// node potentially rewritten with replaced descendants.</param>
    public static TRoot ReplaceSyntax<TRoot>(
        this TRoot root,
        IEnumerable<SyntaxNode> nodes,
        Func<SyntaxNode, SyntaxNode, SyntaxNode> computeReplacementNode,
        IEnumerable<SyntaxToken> tokens,
        Func<SyntaxToken, SyntaxToken, SyntaxToken> computeReplacementToken)
        where TRoot : SyntaxNode
    {
        return (TRoot)root.ReplaceCore(
            nodes: nodes, computeReplacementNode: computeReplacementNode,
            tokens: tokens, computeReplacementToken: computeReplacementToken);
    }
 
    /// <summary>
    /// Creates a new tree of nodes with the specified old node replaced with a new node.
    /// </summary>
    /// <typeparam name="TRoot">The type of the root node.</typeparam>
    /// <typeparam name="TNode">The type of the nodes being replaced.</typeparam>
    /// <param name="root">The root node of the tree of nodes.</param>
    /// <param name="nodes">The nodes to be replaced; descendants of the root node.</param>
    /// <param name="computeReplacementNode">A function that computes a replacement node for the
    /// argument nodes. The first argument is the original node. The second argument is the same
    /// node potentially rewritten with replaced descendants.</param>
    public static TRoot ReplaceNodes<TRoot, TNode>(this TRoot root, IEnumerable<TNode> nodes, Func<TNode, TNode, SyntaxNode> computeReplacementNode)
        where TRoot : SyntaxNode
        where TNode : SyntaxNode
    {
        return (TRoot)root.ReplaceCore(nodes: nodes, computeReplacementNode: computeReplacementNode);
    }
 
    /// <summary>
    /// Creates a new tree of nodes with the specified old node replaced with a new node.
    /// </summary>
    /// <typeparam name="TRoot">The type of the root node.</typeparam>
    /// <param name="root">The root node of the tree of nodes.</param>
    /// <param name="oldNode">The node to be replaced; a descendant of the root node.</param>
    /// <param name="newNode">The new node to use in the new tree in place of the old node.</param>
    public static TRoot ReplaceNode<TRoot>(this TRoot root, SyntaxNode oldNode, SyntaxNode newNode)
        where TRoot : SyntaxNode
    {
        if (oldNode == newNode)
        {
            return root;
        }
 
        return (TRoot)root.ReplaceCore(nodes: new[] { oldNode }, computeReplacementNode: (o, r) => newNode);
    }
 
    /// <summary>
    /// Creates a new tree of nodes with specified old node replaced with a new nodes.
    /// </summary>
    /// <typeparam name="TRoot">The type of the root node.</typeparam>
    /// <param name="root">The root of the tree of nodes.</param>
    /// <param name="oldNode">The node to be replaced; a descendant of the root node and an element of a list member.</param>
    /// <param name="newNodes">A sequence of nodes to use in the tree in place of the old node.</param>
    public static TRoot ReplaceNode<TRoot>(this TRoot root, SyntaxNode oldNode, IEnumerable<SyntaxNode> newNodes)
        where TRoot : SyntaxNode
    {
        return (TRoot)root.ReplaceNodeInListCore(oldNode, newNodes);
    }
 
    /// <summary>
    /// Creates a new tree of nodes with the specified old node replaced with a new node.
    /// </summary>
    /// <typeparam name="TRoot">The type of the root node.</typeparam>
    /// <param name="root">The root node of the tree of nodes.</param>
    /// <param name="tokens">The token to be replaced; descendants of the root node.</param>
    /// <param name="computeReplacementToken">A function that computes a replacement token for
    /// the argument tokens. The first argument is the original token. The second argument is
    /// the same token potentially rewritten with replaced trivia.</param>
    public static TRoot ReplaceTokens<TRoot>(this TRoot root, IEnumerable<SyntaxToken> tokens, Func<SyntaxToken, SyntaxToken, SyntaxToken> computeReplacementToken)
        where TRoot : SyntaxNode
    {
        return (TRoot)root.ReplaceCore<SyntaxNode>(tokens: tokens, computeReplacementToken: computeReplacementToken);
    }
 
    /// <summary>
    /// Creates a new tree of nodes with the specified old token replaced with a new token.
    /// </summary>
    /// <typeparam name="TRoot">The type of the root node.</typeparam>
    /// <param name="root">The root node of the tree of nodes.</param>
    /// <param name="oldToken">The token to be replaced.</param>
    /// <param name="newToken">The new token to use in the new tree in place of the old
    /// token.</param>
    public static TRoot ReplaceToken<TRoot>(this TRoot root, SyntaxToken oldToken, SyntaxToken newToken)
        where TRoot : SyntaxNode
    {
        return (TRoot)root.ReplaceCore<SyntaxNode>(tokens: [oldToken], computeReplacementToken: (o, r) => newToken);
    }
 
    /// <summary>
    /// Creates a new tree of nodes with new nodes inserted before the specified node.
    /// </summary>
    /// <typeparam name="TRoot">The type of the root node.</typeparam>
    /// <param name="root">The root of the tree of nodes.</param>
    /// <param name="nodeInList">The node to insert before; a descendant of the root node an element of a list member.</param>
    /// <param name="newNodes">A sequence of nodes to insert into the tree immediately before the specified node.</param>
    public static TRoot InsertNodesBefore<TRoot>(this TRoot root, SyntaxNode nodeInList, IEnumerable<SyntaxNode> newNodes)
        where TRoot : SyntaxNode
    {
        return (TRoot)root.InsertNodesInListCore(nodeInList, newNodes, insertBefore: true);
    }
 
    /// <summary>
    /// Creates a new tree of nodes with new nodes inserted after the specified node.
    /// </summary>
    /// <typeparam name="TRoot">The type of the root node.</typeparam>
    /// <param name="root">The root of the tree of nodes.</param>
    /// <param name="nodeInList">The node to insert after; a descendant of the root node an element of a list member.</param>
    /// <param name="newNodes">A sequence of nodes to insert into the tree immediately after the specified node.</param>
    public static TRoot InsertNodesAfter<TRoot>(this TRoot root, SyntaxNode nodeInList, IEnumerable<SyntaxNode> newNodes)
        where TRoot : SyntaxNode
    {
        return (TRoot)root.InsertNodesInListCore(nodeInList, newNodes, insertBefore: false);
    }
 
    public static string GetContent<TNode>(this TNode node) where TNode : SyntaxNode
    {
        return node.Green.ToString();
    }
 
    private sealed class DiagnosticSyntaxWalker(List<RazorDiagnostic> diagnostics) : SyntaxWalker
    {
        private readonly List<RazorDiagnostic> _diagnostics = diagnostics ?? [];
 
        public override void Visit(SyntaxNode node)
        {
            if (node?.ContainsDiagnostics == true)
            {
                var diagnostics = node.GetDiagnostics();
 
                _diagnostics.AddRange(diagnostics);
 
                base.Visit(node);
            }
        }
 
        public override void VisitToken(SyntaxToken token)
        {
            if (token.ContainsDiagnostics == true)
            {
                var diagnostics = token.GetDiagnostics();
 
                _diagnostics.AddRange(diagnostics);
 
                base.VisitToken(token);
            }
        }
    }
}