File: StackTraceExplorer\StackTraceAnalyzer.cs
Web Access
Project: src\roslyn\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.StackTraceExplorer;

internal static class StackTraceAnalyzer

{
    /// <summary>
    /// List of parsers to use. Order is important because
    /// take the result from the first parser that returns 
    /// success.
    /// </summary>
    private static readonly ImmutableArray<IStackFrameParser> s_parsers = [new DotnetStackFrameParser(), new VSDebugCallstackParser(), new DefaultStackParser()];

    public static Task<StackTraceAnalysisResult> AnalyzeAsync(string callstack, CancellationToken cancellationToken)
    {
        var result = new StackTraceAnalysisResult(callstack, Parse(callstack, cancellationToken));
        return Task.FromResult(result);
    }

    private static ImmutableArray<ParsedFrame> Parse(string callstack, CancellationToken cancellationToken)
    {
        using var _ = ArrayBuilder<ParsedFrame>.GetInstance(out var builder);

        // if the callstack comes from ActivityLog.xml it has been
        // encoding to be passed over HTTP. This should only decode 
        // specific characters like "&gt;" and "&lt;" to their "normal"
        // equivalents ">" and "<" so we can parse correctly
        callstack = WebUtility.HtmlDecode(callstack);

        var sequence = VirtualCharSequence.Create(0, callstack);

        foreach (var line in SplitLines(sequence))
        {
            cancellationToken.ThrowIfCancellationRequested();

            // For now do the work to removing leading and trailing whitespace. 
            // This keeps behavior we've had, but may not actually be the desired behavior in the long run.
            // Specifically if we ever want to add a copy feature to copy back contents from a frame
            var trimmedLine = Trim(line);

            if (trimmedLine.IsEmpty())
            {
                continue;
            }

            foreach (var parser in s_parsers)
            {
                if (parser.TryParseLine(trimmedLine, out var parsedFrame))
                {
                    builder.Add(parsedFrame);
                    break;
                }
            }
        }

        return builder.ToImmutableAndClear();
    }

    private static IEnumerable<VirtualCharSequence> SplitLines(VirtualCharSequence callstack)
    {
        var position = 0;

        for (var i = 0; i < callstack.Length; i++)
        {
            if (callstack[i] == '\n')
            {
                yield return callstack[position..i];

                // +1 to skip over the \n character
                position = i + 1;
            }
        }

        if (position < callstack.Length)
        {
            yield return callstack[position..];
        }
    }

    private static VirtualCharSequence Trim(VirtualCharSequence virtualChars)
    {
        if (virtualChars.Length == 0)
        {
            return virtualChars;
        }

        var start = 0;
        var end = virtualChars.Length - 1;

        while (char.IsWhiteSpace(virtualChars[start]) && start < end)
        {
            start++;
        }

        while (char.IsWhiteSpace(virtualChars[end]) && end > start)
        {
            end--;
        }

        return virtualChars[start..(end + 1)];
    }
}