File: EditAndContinue\ActiveStatementsMap.cs
Web Access
Project: src\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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditAndContinue;
 
internal sealed class ActiveStatementsMap
{
    public static readonly ActiveStatementsMap Empty =
        new(ImmutableDictionary<string, ImmutableArray<ActiveStatement>>.Empty,
            ImmutableDictionary<ManagedInstructionId, ActiveStatement>.Empty);
 
    public static readonly Comparer<ActiveStatement> Comparer =
        Comparer<ActiveStatement>.Create((x, y) => x.FileSpan.Start.CompareTo(y.FileSpan.Start));
 
    private static readonly Comparer<(ManagedActiveStatementDebugInfo, SourceFileSpan, ActiveStatementId)> s_infoSpanComparer =
        Comparer<(ManagedActiveStatementDebugInfo, SourceFileSpan span, ActiveStatementId)>.Create((x, y) => x.span.Start.CompareTo(y.span.Start));
 
    /// <summary>
    /// Groups active statements by document path as listed in the PDB.
    /// Within each group the statements are ordered by their start position.
    /// </summary>
    public readonly IReadOnlyDictionary<string, ImmutableArray<ActiveStatement>> DocumentPathMap;
 
    /// <summary>
    /// Active statements by instruction id.
    /// </summary>
    public readonly IReadOnlyDictionary<ManagedInstructionId, ActiveStatement> InstructionMap;
 
    /// <summary>
    /// Maps syntax tree to active statements with calculated unmapped spans.
    /// </summary>
    private ImmutableDictionary<SyntaxTree, ImmutableArray<UnmappedActiveStatement>> _lazyOldDocumentActiveStatements;
 
    public ActiveStatementsMap(
        IReadOnlyDictionary<string, ImmutableArray<ActiveStatement>> documentPathMap,
        IReadOnlyDictionary<ManagedInstructionId, ActiveStatement> instructionMap)
    {
        Debug.Assert(documentPathMap.All(entry => entry.Value.IsSorted(Comparer)));
 
        DocumentPathMap = documentPathMap;
        InstructionMap = instructionMap;
 
        _lazyOldDocumentActiveStatements = ImmutableDictionary<SyntaxTree, ImmutableArray<UnmappedActiveStatement>>.Empty;
    }
 
    public static ActiveStatementsMap Create(
        ImmutableArray<ManagedActiveStatementDebugInfo> debugInfos,
        ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>> remapping)
    {
        using var _1 = PooledDictionary<string, ArrayBuilder<(ManagedActiveStatementDebugInfo info, SourceFileSpan span, ActiveStatementId id)>>.GetInstance(out var updatedSpansByDocumentPath);
 
        var ordinal = 0;
        foreach (var debugInfo in debugInfos)
        {
            var documentName = debugInfo.DocumentName;
            if (documentName == null)
            {
                // Ignore active statements that do not have a source location.
                continue;
            }
 
            if (!TryGetUpToDateSpan(debugInfo, remapping, out var baseSpan))
            {
                continue;
            }
 
            if (!updatedSpansByDocumentPath.TryGetValue(documentName, out var documentInfos))
            {
                updatedSpansByDocumentPath.Add(documentName, documentInfos = ArrayBuilder<(ManagedActiveStatementDebugInfo, SourceFileSpan, ActiveStatementId)>.GetInstance());
            }
 
            documentInfos.Add((debugInfo, new SourceFileSpan(documentName, baseSpan), new ActiveStatementId(ordinal++)));
        }
 
        foreach (var (_, infos) in updatedSpansByDocumentPath)
        {
            infos.Sort(s_infoSpanComparer);
        }
 
        var byDocumentPath = updatedSpansByDocumentPath.ToImmutableDictionary(
            keySelector: entry => entry.Key,
            elementSelector: entry => entry.Value.SelectAsArray(item => new ActiveStatement(
                id: item.id,
                flags: item.info.Flags,
                span: item.span,
                instructionId: item.info.ActiveInstruction)));
 
        using var _2 = PooledDictionary<ManagedInstructionId, ActiveStatement>.GetInstance(out var byInstruction);
 
        foreach (var (_, statements) in byDocumentPath)
        {
            foreach (var statement in statements)
            {
                try
                {
                    byInstruction.Add(statement.InstructionId, statement);
                }
                catch (ArgumentException)
                {
                    throw new InvalidOperationException($"Multiple active statements with the same instruction id returned by Active Statement Provider");
                }
            }
        }
 
        // TODO: Remove. Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1830914.
        if (EditAndContinueMethodDebugInfoReader.IgnoreCaseWhenComparingDocumentNames)
        {
            byDocumentPath = byDocumentPath.WithComparers(keyComparer: StringComparer.OrdinalIgnoreCase);
        }
 
        return new ActiveStatementsMap(byDocumentPath, byInstruction.ToImmutableDictionary());
    }
 
    private static bool TryGetUpToDateSpan(ManagedActiveStatementDebugInfo activeStatementInfo, ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>> remapping, out LinePositionSpan newSpan)
    {
        // Drop stale active statements - their location in the current snapshot is unknown.
        if (activeStatementInfo.Flags.HasFlag(ActiveStatementFlags.Stale))
        {
            newSpan = default;
            return false;
        }
 
        var activeSpan = activeStatementInfo.SourceSpan.ToLinePositionSpan();
        if (activeStatementInfo.Flags.HasFlag(ActiveStatementFlags.MethodUpToDate))
        {
            newSpan = activeSpan;
            return true;
        }
 
        var instructionId = activeStatementInfo.ActiveInstruction;
 
        // Map active statement spans in non-remappable regions to the latest source locations.
        if (remapping.TryGetValue(instructionId.Method, out var regionsInMethod))
        {
            // Note that active statement spans can be nested. For example,
            // [|var x = y switch { 1 => 0, _ => [|1|] };|]
 
            foreach (var region in regionsInMethod)
            {
                if (!region.IsExceptionRegion &&
                    region.OldSpan.Span == activeSpan &&
                    activeStatementInfo.DocumentName == region.OldSpan.Path)
                {
                    newSpan = region.NewSpan.Span;
                    return true;
                }
            }
        }
 
        // The active statement is in a method instance that was updated during Hot Reload session,
        // at which point the location of the span was not known. 
        newSpan = default;
        return false;
    }
 
    public bool IsEmpty
        => InstructionMap.IsEmpty();
 
    internal async ValueTask<ImmutableArray<UnmappedActiveStatement>> GetOldActiveStatementsAsync(IEditAndContinueAnalyzer analyzer, Document oldDocument, CancellationToken cancellationToken)
    {
        var oldTree = await oldDocument.DocumentState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        var oldRoot = await oldTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
        var oldText = await oldTree.GetTextAsync(cancellationToken).ConfigureAwait(false);
        return GetOldActiveStatements(analyzer, oldTree, oldText, oldRoot, cancellationToken);
    }
 
    internal ImmutableArray<UnmappedActiveStatement> GetOldActiveStatements(IEditAndContinueAnalyzer analyzer, SyntaxTree oldSyntaxTree, SourceText oldText, SyntaxNode oldRoot, CancellationToken cancellationToken)
    {
        Debug.Assert(oldRoot.SyntaxTree == oldSyntaxTree);
 
        return ImmutableInterlocked.GetOrAdd(
            ref _lazyOldDocumentActiveStatements,
            oldSyntaxTree,
            oldSyntaxTree => CalculateOldActiveStatementsAndExceptionRegions(analyzer, oldSyntaxTree, oldText, oldRoot, cancellationToken));
    }
 
    private ImmutableArray<UnmappedActiveStatement> CalculateOldActiveStatementsAndExceptionRegions(IEditAndContinueAnalyzer analyzer, SyntaxTree oldTree, SourceText oldText, SyntaxNode oldRoot, CancellationToken cancellationToken)
    {
        using var _1 = ArrayBuilder<UnmappedActiveStatement>.GetInstance(out var builder);
        using var _2 = PooledHashSet<ActiveStatement>.GetInstance(out var mappedStatements);
 
        void AddStatement(LinePositionSpan unmappedLineSpan, ActiveStatement activeStatement)
        {
            // Protect against stale/invalid active statement spans read from the PDB.
            // Also guard against active statements unmapped to multiple locations in the unmapped file
            // (when multiple #line directives map to the same span that overlaps with the active statement).
            if (TryGetTextSpan(oldText.Lines, unmappedLineSpan, out var unmappedSpan) &&
                oldRoot.FullSpan.Contains(unmappedSpan.Start) &&
                mappedStatements.Add(activeStatement))
            {
                var exceptionRegions = analyzer.GetExceptionRegions(oldRoot, unmappedSpan, activeStatement.IsNonLeaf, cancellationToken);
                builder.Add(new UnmappedActiveStatement(unmappedSpan, activeStatement, exceptionRegions));
            }
        }
 
        var hasAnyLineDirectives = false;
        foreach (var lineMapping in oldTree.GetLineMappings(cancellationToken))
        {
            var unmappedSection = lineMapping.Span;
            var mappedSection = lineMapping.MappedSpan;
 
            hasAnyLineDirectives = true;
 
            var targetPath = mappedSection.HasMappedPath ? mappedSection.Path : oldTree.FilePath;
 
            if (DocumentPathMap.TryGetValue(targetPath, out var activeStatementsInMappedFile))
            {
                var range = GetSpansStartingInSpan(
                    mappedSection.Span.Start,
                    mappedSection.Span.End,
                    activeStatementsInMappedFile,
                    startPositionComparer: (x, y) => x.Span.Start.CompareTo(y));
 
                for (var i = range.Start.Value; i < range.End.Value; i++)
                {
                    var activeStatement = activeStatementsInMappedFile[i];
                    var unmappedLineSpan = ReverseMapLinePositionSpan(unmappedSection, mappedSection.Span, activeStatement.Span);
 
                    AddStatement(unmappedLineSpan, activeStatement);
                }
            }
        }
 
        if (!hasAnyLineDirectives)
        {
            Debug.Assert(builder.IsEmpty);
 
            if (DocumentPathMap.TryGetValue(oldTree.FilePath, out var activeStatements))
            {
                foreach (var activeStatement in activeStatements)
                {
                    AddStatement(activeStatement.Span, activeStatement);
                }
            }
        }
 
        Debug.Assert(builder.IsSorted(Comparer<UnmappedActiveStatement>.Create((x, y) => x.UnmappedSpan.Start.CompareTo(y.UnmappedSpan.End))));
 
        return builder.ToImmutableAndClear();
    }
 
    private static LinePositionSpan ReverseMapLinePositionSpan(LinePositionSpan unmappedSection, LinePositionSpan mappedSection, LinePositionSpan mappedSpan)
    {
        var lineDifference = unmappedSection.Start.Line - mappedSection.Start.Line;
        var unmappedStartLine = mappedSpan.Start.Line + lineDifference;
        var unmappedEndLine = mappedSpan.End.Line + lineDifference;
 
        var unmappedStartColumn = (mappedSpan.Start.Line == mappedSection.Start.Line)
            ? unmappedSection.Start.Character + mappedSpan.Start.Character - mappedSection.Start.Character
            : mappedSpan.Start.Character;
 
        var unmappedEndColumn = (mappedSpan.End.Line == mappedSection.Start.Line)
            ? unmappedSection.Start.Character + mappedSpan.End.Character - mappedSection.Start.Character
            : mappedSpan.End.Character;
 
        return new(new(unmappedStartLine, unmappedStartColumn), new(unmappedEndLine, unmappedEndColumn));
    }
 
    private static bool TryGetTextSpan(TextLineCollection lines, LinePositionSpan lineSpan, out TextSpan span)
    {
        if (lineSpan.Start.Line >= lines.Count || lineSpan.End.Line >= lines.Count)
        {
            span = default;
            return false;
        }
 
        var start = lines[lineSpan.Start.Line].Start + lineSpan.Start.Character;
        var end = lines[lineSpan.End.Line].Start + lineSpan.End.Character;
        span = TextSpan.FromBounds(start, end);
        return true;
    }
 
    /// <summary>
    /// Since an active statement represents a range between two sequence points and its span is associated with the first of these sequence points,
    /// we decide whether the active statement is relevant within given span by checking whether its start location is within that span.
    /// An active statement may overlap a span even if its starting location is not in the span, but such active statement is not relevant 
    /// for analysis of code within the given span.
    /// 
    /// Assumes that <paramref name="spans"/> are sorted by their start position.
    /// </summary>
    internal static Range GetSpansStartingInSpan<TElement, TPosition>(
        TPosition spanStart,
        TPosition spanEnd,
        ImmutableArray<TElement> spans,
        Func<TElement, TPosition, int> startPositionComparer)
    {
        var start = spans.BinarySearch(spanStart, startPositionComparer);
        if (start < 0)
        {
            // ~start points to the next span whose start position is greater than span start position:
            start = ~start;
        }
 
        if (start == spans.Length)
        {
            return default;
        }
 
        var length = spans.AsSpan()[start..].BinarySearch(spanEnd, startPositionComparer);
        if (length < 0)
        {
            // ~length points to the next span whose start position is greater than span start position:
            length = ~length;
        }
 
        while (start > 0 && startPositionComparer(spans[start - 1], spanStart) == 0)
        {
            start--;
        }
 
        var end = start + length;
        while (end < spans.Length && startPositionComparer(spans[end], spanStart) == 0)
        {
            end++;
        }
 
        return new Range(start, end);
    }
}