File: Syntax\SyntaxTreeDiagnosticEnumerator.cs
Web Access
Project: src\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CSharp
{
    /// <summary>
    /// An enumerator for diagnostic lists.
    /// </summary>
    internal static class SyntaxTreeDiagnosticEnumerator
    {
        private const int DefaultStackCapacity = 8;
 
        public static IEnumerable<Diagnostic> EnumerateDiagnostics(SyntaxTree syntaxTree, GreenNode root, int position)
        {
            // Note:during iteration, '_position' is:
            // <list type="number">
            // <item>The FullStart of a node when we're on a node.</item>
            // <item>The FullStart of a trivia when we're on trivia.</item>
            // <item>The FullStart of a list when we're on a list.</item>
            // <item>The *Start* (not FullStart) of a token when we're on a token.</item>
            // </list>
            //
            // The last is because when we hit a token, we will have processed its leading trivia, and will have moved
            // forward.
            //
            // Note that the offset for a diagnostic is relative to its Start (see <see
            // cref="SyntaxDiagnosticInfo.Offset"/>). So for tokens we don't need to do anything.  For all other
            // constructs, we need to add the leading trivia width to the offset to get the correct location.
 
            Debug.Assert(root.ContainsDiagnostics, "Caller should have checked that the root has diagnostics");
 
            using var stack = new NodeIterationStack(DefaultStackCapacity);
            stack.PushNodeOrToken(root);
 
            var fullTreeLength = syntaxTree.GetRoot().FullSpan.Length;
 
            while (stack.Any())
            {
                var node = stack.Top.Node;
 
                if (!stack.Top.ProcessedDiagnostics)
                {
                    foreach (SyntaxDiagnosticInfo sdi in node.GetDiagnostics())
                    {
                        // For tokens, we've already seen leading trivia on the stack.  So we don't need to adjust the
                        // offset. For everything else, we need to add the leading trivia offset so that we are at the
                        // right 'Start' position that offset is relative to.  See documentation of position above.
                        int leadingWidthToAdd = node.IsToken ? 0 : node.GetLeadingTriviaWidth();
 
                        // don't produce locations outside of tree span
                        var spanStart = Math.Min(position + leadingWidthToAdd + sdi.Offset, fullTreeLength);
                        var spanEnd = Math.Min(spanStart + sdi.Width, fullTreeLength);
                        yield return new CSDiagnostic(sdi, new SourceLocation(syntaxTree, TextSpan.FromBounds(spanStart, spanEnd)));
                    }
                    stack.Top.ProcessedDiagnostics = true;
                }
 
                processNode(node);
            }
 
            yield break;
 
            void processNode(GreenNode node)
            {
                // SlotCount is 0 when we hit tokens or normal trivia. In this case, we just want to move past the
                // normal width of the item.  Importantly, in the case of tokens, we don't want to move the full-width.
                // That would make us double-count the widths of the leading/trailing trivia which we're walking into as
                // normal green nodes.
                //
                // As this has no children and has had its diagnostics processed, pop it and process its parent.
                if (node.SlotCount == 0)
                {
                    position += node.Width;
                }
                else
                {
                    for (var nextSlotIndex = stack.Top.SlotIndex + 1; nextSlotIndex < node.SlotCount; nextSlotIndex++)
                    {
                        var child = node.GetSlot(nextSlotIndex);
                        if (child == null)
                            continue;
 
                        // If the child doesn't have diagnostics anywhere in it, we can skip it entirely.
                        if (!child.ContainsDiagnostics)
                        {
                            position += child.FullWidth;
                            continue;
                        }
 
                        stack.Top.SlotIndex = nextSlotIndex;
                        stack.PushNodeOrToken(child);
 
                        // Pushed a child to process, return out to continue processing it.
                        return;
                    }
                }
 
                // Done with this node. Pop it so we continue processing its parent.
                stack.Pop();
            }
        }
 
        private struct NodeIteration(GreenNode node)
        {
            /// <summary>
            /// The node we're on.
            /// </summary>
            public readonly GreenNode Node = node;
 
            /// <summary>
            /// The index of the child of <see cref="Node"/> that we're on. Initially at -1 as we're pointing at the
            /// node itself, not any children.
            /// </summary>
            public int SlotIndex = -1;
 
            /// <summary>
            /// We'll hit nodes multiple times.  First, when we run into them, and then after processing each chiild and
            /// popping back up to it.  This tracks whether we've processed the diagnostics already so we don't do it
            /// twice.
            /// </summary>
            public bool ProcessedDiagnostics;
        }
 
        private struct NodeIterationStack(int capacity) : IDisposable
        {
            private NodeIteration[] _stack = ArrayPool<NodeIteration>.Shared.Rent(capacity);
            private int _count;
 
            public readonly void Dispose()
                => ArrayPool<NodeIteration>.Shared.Return(_stack, clearArray: true);
 
            public void PushNodeOrToken(GreenNode node)
            {
                if (node is Syntax.InternalSyntax.SyntaxToken token)
                {
                    PushToken(token);
                }
                else
                {
                    Push(node);
                }
            }
 
            private void PushToken(Syntax.InternalSyntax.SyntaxToken token)
            {
                // Push in reverse order of processing.
                this.Push(token.GetTrailingTrivia());
                this.Push(token);
                this.Push(token.GetLeadingTrivia());
            }
 
            private void Push(GreenNode? node)
            {
                if (node is null)
                    return;
 
                if (_count >= _stack.Length)
                {
                    var tmp = ArrayPool<NodeIteration>.Shared.Rent(_stack.Length * 2);
                    Array.Copy(_stack, tmp, _stack.Length);
                    ArrayPool<NodeIteration>.Shared.Return(_stack, clearArray: true);
                    _stack = tmp;
                }
 
                _stack[_count] = new NodeIteration(node);
                _count++;
            }
 
            public void Pop()
                => _count--;
 
            public readonly bool Any()
                => _count > 0;
 
            public readonly ref NodeIteration Top
                => ref _stack[_count - 1];
        }
    }
}