File: EditAndContinue\ActiveStatementsDescription.cs
Web Access
Project: ..\..\..\src\EditorFeatures\TestUtilities\Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj (Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.EditAndContinue.Contracts;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditAndContinue.UnitTests
{
    internal class ActiveStatementsDescription
    {
        internal static readonly ActiveStatementsDescription Empty = new();
 
        public readonly ImmutableArray<UnmappedActiveStatement> OldStatements;
        public readonly ActiveStatementsMap OldStatementsMap;
        public readonly ImmutableArray<SourceFileSpan> NewMappedSpans;
        public readonly ImmutableArray<ImmutableArray<SourceFileSpan>> NewMappedRegions;
        public readonly ImmutableArray<LinePositionSpan> OldUnmappedTrackingSpans;
 
        private ActiveStatementsDescription()
        {
            OldStatements = ImmutableArray<UnmappedActiveStatement>.Empty;
            NewMappedSpans = ImmutableArray<SourceFileSpan>.Empty;
            OldStatementsMap = ActiveStatementsMap.Empty;
            NewMappedRegions = ImmutableArray<ImmutableArray<SourceFileSpan>>.Empty;
            OldUnmappedTrackingSpans = ImmutableArray<LinePositionSpan>.Empty;
        }
 
        public ActiveStatementsDescription(string oldMarkedSource, string newMarkedSource, Func<string, SyntaxTree> syntaxTreeFactory, ActiveStatementFlags[]? flags)
        {
            var oldSource = ClearTags(oldMarkedSource);
            var newSource = ClearTags(newMarkedSource);
 
            var oldTree = syntaxTreeFactory(oldSource);
            var newTree = syntaxTreeFactory(newSource);
 
            var oldDocumentMap = new Dictionary<string, List<ActiveStatement>>();
            OldStatements = CreateActiveStatementMapFromMarkers(oldMarkedSource, oldTree, flags, oldDocumentMap);
 
            OldStatementsMap = new ActiveStatementsMap(
                documentPathMap: oldDocumentMap.ToImmutableDictionary(e => e.Key, e => e.Value.OrderBy(ActiveStatementsMap.Comparer).ToImmutableArray()),
                instructionMap: OldStatements.ToDictionary(s => new ManagedInstructionId(new ManagedMethodId(Guid.NewGuid(), 0x060000001, version: 1), ilOffset: 0), s => s.Statement));
 
            var newActiveStatementMarkers = GetActiveSpans(newMarkedSource).ToArray();
 
            var activeStatementCount = Math.Max(OldStatements.Length, (newActiveStatementMarkers.Length == 0) ? -1 : newActiveStatementMarkers.Max(m => m.Id));
 
            var newMappedSpans = new ArrayBuilder<SourceFileSpan>();
            var newMappedRegions = new ArrayBuilder<ImmutableArray<SourceFileSpan>>();
            var newExceptionRegionMarkers = GetExceptionRegions(newMarkedSource);
 
            newMappedSpans.ZeroInit(activeStatementCount);
            newMappedRegions.ZeroInit(activeStatementCount);
 
            // initialize with deleted spans (they will retain their file path):
            foreach (var oldStatement in OldStatements)
            {
                newMappedSpans[oldStatement.Statement.Ordinal] = new SourceFileSpan(oldStatement.Statement.FilePath, default);
                newMappedRegions[oldStatement.Statement.Ordinal] = ImmutableArray<SourceFileSpan>.Empty;
            }
 
            // update with spans marked in the new source:
            foreach (var (unmappedSpan, ordinal) in newActiveStatementMarkers)
            {
                newMappedSpans[ordinal] = newTree.GetMappedLineSpan(unmappedSpan);
                newMappedRegions[ordinal] = (ordinal < newExceptionRegionMarkers.Length)
                    ? newExceptionRegionMarkers[ordinal].SelectAsArray(span => (SourceFileSpan)newTree.GetMappedLineSpan(span))
                    : ImmutableArray<SourceFileSpan>.Empty;
            }
 
            NewMappedSpans = newMappedSpans.ToImmutable();
            NewMappedRegions = newMappedRegions.ToImmutable();
 
            // Tracking spans are marked in the new source since the editor moves them around as the user 
            // edits the source and we get their positions when analyzing the new source.
            // The EnC analyzer uses old tracking spans as hints to find matching nodes.
            var newText = newTree.GetText();
            OldUnmappedTrackingSpans = GetTrackingSpans(newMarkedSource, activeStatementCount).
                SelectAsArray(s => newText.Lines.GetLinePositionSpan(s));
        }
 
        internal static ImmutableArray<UnmappedActiveStatement> CreateActiveStatementMapFromMarkers(
            string markedSource,
            SyntaxTree tree,
            ActiveStatementFlags[]? flags,
            Dictionary<string, List<ActiveStatement>> documentMap)
        {
            var activeStatementMarkers = GetActiveSpans(markedSource).ToArray();
            var exceptionRegionMarkers = GetExceptionRegions(markedSource);
 
            return activeStatementMarkers.Aggregate(
                new List<UnmappedActiveStatement>(),
                (list, marker) =>
                {
                    var (unmappedSpan, ordinal) = marker;
                    var mappedSpan = tree.GetMappedLineSpan(unmappedSpan);
                    var documentActiveStatements = documentMap.GetOrAdd(mappedSpan.Path, path => new List<ActiveStatement>());
 
                    var statementFlags = (flags != null) ? flags[ordinal] :
                        ((ordinal == 0) ? ActiveStatementFlags.LeafFrame : ActiveStatementFlags.NonLeafFrame) | ActiveStatementFlags.MethodUpToDate;
 
                    var exceptionRegions = (ordinal < exceptionRegionMarkers.Length)
                        ? exceptionRegionMarkers[ordinal].SelectAsArray(unmappedRegionSpan => (SourceFileSpan)tree.GetMappedLineSpan(unmappedRegionSpan))
                        : ImmutableArray<SourceFileSpan>.Empty;
 
                    var unmappedActiveStatement = new UnmappedActiveStatement(
                        unmappedSpan,
                        new ActiveStatement(
                            ordinal,
                            statementFlags,
                            mappedSpan,
                            instructionId: default),
                        new ActiveStatementExceptionRegions(exceptionRegions, isActiveStatementCovered: true));
 
                    documentActiveStatements.Add(unmappedActiveStatement.Statement);
                    return SetListItem(list, ordinal, unmappedActiveStatement);
                }).ToImmutableArray();
        }
 
        internal static ImmutableArray<UnmappedActiveStatement> GetUnmappedActiveStatements(
           Func<string, string, SyntaxTree> syntaxTreeFactory,
           string[] markedSources,
           string[]? filePaths = null,
           string? extension = null,
           ActiveStatementFlags[]? flags = null)
        {
            var map = new Dictionary<string, List<ActiveStatement>>();
 
            var activeStatements = new ArrayBuilder<UnmappedActiveStatement>();
 
            var sourceIndex = 0;
            foreach (var markedSource in markedSources)
            {
                var documentName = filePaths?[sourceIndex] ?? Path.Combine(TempRoot.Root, TestWorkspace.GetDefaultTestSourceDocumentName(sourceIndex, extension));
                var tree = syntaxTreeFactory(ClearTags(markedSource), documentName);
                var statements = CreateActiveStatementMapFromMarkers(markedSource, tree, flags, map);
 
                activeStatements.AddRange(statements.Where(s => s.Statement != null));
                sourceIndex++;
            }
 
            activeStatements.Sort((x, y) => x.Statement.Ordinal.CompareTo(y.Statement.Ordinal));
            return activeStatements.ToImmutable();
        }
 
        internal static ImmutableArray<ManagedActiveStatementDebugInfo> GetActiveStatementDebugInfos(
           ImmutableArray<UnmappedActiveStatement> activeStatements,
           int[]? methodRowIds = null,
           Guid[]? modules = null,
           int[]? methodVersions = null,
           int[]? ilOffsets = null)
        {
            var moduleId = new Guid("00000000-0000-0000-0000-000000000001");
 
            return activeStatements.Select(s => s.Statement).SelectAsArray(statement =>
                new ManagedActiveStatementDebugInfo(
                    new ManagedInstructionId(
                        new ManagedMethodId(
                            (modules != null) ? modules[statement.Ordinal] : moduleId,
                            new ManagedModuleMethodId(
                                token: 0x06000000 | (methodRowIds != null ? methodRowIds[statement.Ordinal] : statement.Ordinal + 1),
                                version: (methodVersions != null) ? methodVersions[statement.Ordinal] : 1)),
                        ilOffset: (ilOffsets != null) ? ilOffsets[statement.Ordinal] : 0),
                    documentName: statement.FilePath,
                    sourceSpan: statement.Span.ToSourceSpan(),
                    flags: statement.Flags));
        }
 
        internal static string ClearTags(string source)
            => s_tags.Replace(source, m => new string(' ', m.Length));
 
        internal static string[] ClearTags(string[] sources)
            => sources.Select(ClearTags).ToArray();
 
        private static readonly Regex s_tags = new Regex(
            @"[<][/]?(AS|ER|N|TS)[:][.0-9,]+[>]",
            RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
 
        private static readonly Regex s_activeStatementPattern = new Regex(
            @"[<]AS[:]    (?<Id>[0-9,]+) [>]
              (?<ActiveStatement>.*)
              [<][/]AS[:] (\k<Id>)      [>]",
            RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
 
        public static readonly Regex ExceptionRegionPattern = new Regex(
            @"[<]ER[:]      (?<Id>(?:[0-9]+[.][0-9]+[,]?)+)   [>]
              (?<ExceptionRegion>.*)
              [<][/]ER[:]   (\k<Id>)                 [>]",
            RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
 
        private static readonly Regex s_trackingStatementPattern = new Regex(
            @"[<]TS[:]    (?<Id>[0-9,]+) [>]
              (?<TrackingStatement>.*)
              [<][/]TS[:] (\k<Id>)      [>]",
            RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
 
        internal static IEnumerable<int> GetIds(Match match)
            => match.Groups["Id"].Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse);
 
        internal static int[] GetIds(string ids)
            => ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
 
        internal static IEnumerable<(int, int)> GetDottedIds(Match match)
            => from ids in match.Groups["Id"].Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
               let parts = ids.Split('.')
               select (int.Parse(parts[0]), int.Parse(parts[1]));
 
        private static IEnumerable<(TextSpan Span, int[] Ids)> GetSpansRecursive(Regex regex, string contentGroupName, string markedSource, int offset)
        {
            foreach (var match in regex.Matches(markedSource).ToEnumerable())
            {
                var markedSyntax = match.Groups[contentGroupName];
                var ids = GetIds(match.Groups["Id"].Value);
                var absoluteOffset = offset + markedSyntax.Index;
 
                var span = markedSyntax.Length != 0 ? new TextSpan(absoluteOffset, markedSyntax.Length) : new TextSpan();
                yield return (span, ids);
 
                foreach (var nestedSpan in GetSpansRecursive(regex, contentGroupName, markedSyntax.Value, absoluteOffset))
                {
                    yield return nestedSpan;
                }
            }
        }
 
        internal static IEnumerable<(TextSpan Span, int Id)> GetActiveSpans(string markedSource)
        {
            foreach (var (span, ids) in GetSpansRecursive(s_activeStatementPattern, "ActiveStatement", markedSource, offset: 0))
            {
                foreach (var id in ids)
                {
                    yield return (span, id);
                }
            }
        }
 
        internal static TextSpan[] GetTrackingSpans(string src, int count)
        {
            var matches = s_trackingStatementPattern.Matches(src);
            if (matches.Count == 0)
            {
                return Array.Empty<TextSpan>();
            }
 
            var result = new TextSpan[count];
 
            for (var i = 0; i < matches.Count; i++)
            {
                var span = matches[i].Groups["TrackingStatement"];
                foreach (var id in GetIds(matches[i]))
                {
                    result[id] = new TextSpan(span.Index, span.Length);
                }
            }
 
            Contract.ThrowIfTrue(result.Any(span => span == default));
 
            return result;
        }
 
        internal static ImmutableArray<ImmutableArray<TextSpan>> GetExceptionRegions(string markedSource)
        {
            var matches = ExceptionRegionPattern.Matches(markedSource);
            var plainSource = ClearTags(markedSource);
 
            var result = new List<List<TextSpan>>();
 
            for (var i = 0; i < matches.Count; i++)
            {
                var exceptionRegion = matches[i].Groups["ExceptionRegion"];
 
                foreach (var (activeStatementId, exceptionRegionId) in GetDottedIds(matches[i]))
                {
                    EnsureSlot(result, activeStatementId);
                    result[activeStatementId] ??= new List<TextSpan>();
                    EnsureSlot(result[activeStatementId], exceptionRegionId);
 
                    var regionText = plainSource.AsSpan().Slice(exceptionRegion.Index, exceptionRegion.Length);
                    var start = IndexOfDifferent(regionText, ' ');
                    var length = LastIndexOfDifferent(regionText, ' ') - start + 1;
 
                    result[activeStatementId][exceptionRegionId] = new TextSpan(exceptionRegion.Index + start, length);
                }
            }
 
            return result.Select(r => r.AsImmutableOrEmpty()).ToImmutableArray();
        }
 
        public static int IndexOfDifferent(ReadOnlySpan<char> span, char c)
        {
            for (var i = 0; i < span.Length; i++)
            {
                if (span[i] != c)
                {
                    return i;
                }
            }
 
            return -1;
        }
 
        public static int LastIndexOfDifferent(ReadOnlySpan<char> span, char c)
        {
            for (var i = span.Length - 1; i >= 0; i--)
            {
                if (span[i] != c)
                {
                    return i;
                }
            }
 
            return -1;
        }
 
        public static List<T> SetListItem<T>(List<T> list, int i, T item)
        {
            EnsureSlot(list, i);
            list[i] = item;
            return list;
        }
 
        public static void EnsureSlot<T>(List<T> list, int i)
        {
            while (i >= list.Count)
            {
                list.Add(default!);
            }
        }
    }
}