File: Syntax\LineDirectiveMap.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// 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 Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.PooledObjects;
 
namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// The LineDirectiveMap is created to enable translating positions, using the #line directives
    /// in a file. The basic implementation creates an ordered array of line mapping entries, one
    /// for each #line directive in the file (plus one at the beginning). If the file has no
    /// directives, then the array has just one element in it. To map line numbers, a binary search
    /// of the mapping entries is done and nearest line mapping is applied.
    /// </summary>
    internal abstract partial class LineDirectiveMap<TDirective>
        where TDirective : SyntaxNode
    {
        internal readonly ImmutableArray<LineMappingEntry> Entries;
 
        // Get all active #line directives under trivia into the list, in source code order.
        protected abstract bool ShouldAddDirective(TDirective directive);
 
        // Given a directive and the previous entry, create a new entry.
        protected abstract LineMappingEntry GetEntry(TDirective directive, SourceText sourceText, LineMappingEntry previous);
 
        // Creates the first entry with language specific content
        protected abstract LineMappingEntry InitializeFirstEntry();
 
        protected LineDirectiveMap(SyntaxTree syntaxTree)
        {
            // Accumulate all the directives, in source code order
            var syntaxRoot = (SyntaxNodeOrToken)syntaxTree.GetRoot();
            IList<TDirective> directives = syntaxRoot.GetDirectives<TDirective>(filter: ShouldAddDirective);
            Debug.Assert(directives != null);
 
            // Create the entry map.
            Entries = CreateEntryMap(syntaxTree, directives);
        }
 
        // Given a span and a default file name, return a FileLinePositionSpan that is the mapped
        // span, taking into account line directives.
        public FileLinePositionSpan TranslateSpan(SourceText sourceText, string treeFilePath, TextSpan span)
        {
            var unmappedStartPos = sourceText.Lines.GetLinePosition(span.Start);
            var unmappedEndPos = sourceText.Lines.GetLinePosition(span.End);
            var entry = FindEntry(unmappedStartPos.Line);
 
            return TranslateSpan(entry, treeFilePath, unmappedStartPos, unmappedEndPos);
        }
 
        protected FileLinePositionSpan TranslateSpan(in LineMappingEntry entry, string treeFilePath, LinePosition unmappedStartPos, LinePosition unmappedEndPos)
        {
            string path = entry.MappedPathOpt ?? treeFilePath;
            var span = entry.State == PositionState.RemappedSpan ?
                TranslateEnhancedLineDirectiveSpan(entry, unmappedStartPos, unmappedEndPos) :
                TranslateLineDirectiveSpan(entry, unmappedStartPos, unmappedEndPos);
            return new FileLinePositionSpan(path, span, hasMappedPath: entry.MappedPathOpt != null);
        }
 
        private static LinePositionSpan TranslateLineDirectiveSpan(in LineMappingEntry entry, LinePosition unmappedStartPos, LinePosition unmappedEndPos)
        {
            return new LinePositionSpan(translatePosition(entry, unmappedStartPos), translatePosition(entry, unmappedEndPos));
 
            static LinePosition translatePosition(in LineMappingEntry entry, LinePosition unmapped)
            {
                int mappedLine = unmapped.Line - entry.UnmappedLine + entry.MappedLine;
                return (mappedLine == -1) ? new LinePosition(unmapped.Character) : new LinePosition(mappedLine, unmapped.Character);
            }
        }
 
        private static LinePositionSpan TranslateEnhancedLineDirectiveSpan(in LineMappingEntry entry, LinePosition unmappedStartPos, LinePosition unmappedEndPos)
        {
            // A span starting on the first line, at or before 'UnmappedCharacterOffset' is
            // mapped to the entire 'MappedSpan', regardless of the size of the unmapped span,
            // even if the unmapped span ends before 'UnmappedCharacterOffset'.
            if (unmappedStartPos.Line == entry.UnmappedLine &&
                unmappedStartPos.Character < entry.UnmappedCharacterOffset.GetValueOrDefault())
            {
                return entry.MappedSpan;
            }
 
            // A span starting on the first line after 'UnmappedCharacterOffset', or starting on
            // a subsequent line, is mapped to a span of corresponding size.
            return new LinePositionSpan(translatePosition(entry, unmappedStartPos), translatePosition(entry, unmappedEndPos));
 
            static LinePosition translatePosition(in LineMappingEntry entry, LinePosition unmapped)
            {
                return new LinePosition(
                    unmapped.Line - entry.UnmappedLine + entry.MappedSpan.Start.Line,
                    unmapped.Line == entry.UnmappedLine ?
                        entry.MappedSpan.Start.Character + unmapped.Character - entry.UnmappedCharacterOffset.GetValueOrDefault() :
                        unmapped.Character);
            }
        }
 
        /// <summary>
        /// Determines whether the position is considered to be hidden from the debugger or not.
        /// </summary>
        public abstract LineVisibility GetLineVisibility(SourceText sourceText, int position);
 
        /// <summary>
        /// Combines TranslateSpan and IsHiddenPosition to not search the entries twice when emitting sequence points
        /// </summary>
        internal abstract FileLinePositionSpan TranslateSpanAndVisibility(SourceText sourceText, string treeFilePath, TextSpan span, out bool isHiddenPosition);
 
        /// <summary>
        /// Are there any hidden regions in the map?
        /// </summary>
        /// <returns>True if there's at least one hidden region in the map.</returns>
        public bool HasAnyHiddenRegions()
        {
            return this.Entries.Any(static e => e.State == PositionState.Hidden);
        }
 
        // Find the line mapped entry with the largest unmapped line number <= lineNumber.
        protected LineMappingEntry FindEntry(int lineNumber)
        {
            int r = FindEntryIndex(lineNumber);
 
            return this.Entries[r];
        }
 
        // Find the index of the line mapped entry with the largest unmapped line number <= lineNumber.
        protected int FindEntryIndex(int lineNumber)
        {
            int r = Entries.BinarySearch(new LineMappingEntry(lineNumber));
            return r >= 0 ? r : ((~r) - 1);
        }
 
        // Given the ordered list of all directives in the file, return the ordered line mapping
        // entry for the file. This always starts with the null mapped that maps line 0 to line 0.
        private ImmutableArray<LineMappingEntry> CreateEntryMap(SyntaxTree tree, IList<TDirective> directives)
        {
            var entries = ArrayBuilder<LineMappingEntry>.GetInstance(directives.Count + 1);
 
            var current = InitializeFirstEntry();
            entries.Add(current);
 
            if (directives.Count > 0)
            {
                var sourceText = tree.GetText();
                foreach (var directive in directives)
                {
                    current = GetEntry(directive, sourceText, current);
                    entries.Add(current);
                }
            }
 
#if DEBUG
            // Make sure the entries array is correctly sorted. 
            for (int i = 0; i < entries.Count - 1; ++i)
            {
                Debug.Assert(entries[i].CompareTo(entries[i + 1]) < 0);
            }
#endif
 
            return entries.ToImmutableAndFree();
        }
 
        protected abstract LineVisibility GetUnknownStateVisibility(int index);
 
        /// <summary>
        /// The caller is expected to not call this if <see cref="Entries"/> is empty.
        /// </summary>
        public IEnumerable<LineMapping> GetLineMappings(TextLineCollection lines)
        {
            Debug.Assert(Entries.Length > 1);
 
            var current = Entries[0];
 
            // the first entry is always initialized to unmapped:
            Debug.Assert(
                current.State is PositionState.Unmapped or PositionState.Unknown &&
                current.UnmappedLine == 0 &&
                current.MappedLine == 0 &&
                current.MappedPathOpt == null);
 
            for (int i = 1; i < Entries.Length; i++)
            {
                var next = Entries[i];
 
                int unmappedEndLine = next.UnmappedLine - 2;
                Debug.Assert(unmappedEndLine >= current.UnmappedLine - 1);
 
                // Skip empty spans - two consecutive #line directives or #line on the first line.
                if (unmappedEndLine >= current.UnmappedLine)
                {
                    // C#: Span ends just at the start of the line containing #line directive
                    //
                    // #line Current "file1"
                    // [|....\n
                    // ...........\n|]
                    // #line Next "file2"
                    //
                    // VB: Span starts at the beginning of the line following the #ExternalSource directive and ends at the start of the line preceding #End ExternalSource.
                    // #ExternalSource("file", 1)
                    // [|....\n
                    // ...........\n|]
                    // #End ExternalSource
 
                    var endLine = lines[unmappedEndLine];
                    int lineLength = endLine.EndIncludingLineBreak - endLine.Start;
 
                    yield return CreateLineMapping(current, unmappedEndLine, lineLength, currentIndex: i - 1);
                }
 
                current = next;
            }
 
            var lastLine = lines[^1];
 
            // Last span (unless the last #line/#End ExternalSource is on the last line):
            // #line Current "file1"
            // [|....\n
            // ...........\n|]
            //
            // #End ExternalSource
            // [|....\n
            // ...........\n|]
            if (current.UnmappedLine <= lastLine.LineNumber)
            {
                int lineLength = lastLine.EndIncludingLineBreak - lastLine.Start;
                int unmappedEndLine = lastLine.LineNumber;
 
                yield return CreateLineMapping(current, unmappedEndLine, lineLength, currentIndex: Entries.Length - 1);
            }
        }
 
        private LineMapping CreateLineMapping(in LineMappingEntry entry, int unmappedEndLine, int lineLength, int currentIndex)
        {
            var unmapped = new LinePositionSpan(
                new LinePosition(entry.UnmappedLine, character: 0),
                new LinePosition(unmappedEndLine, lineLength));
 
            if (entry.State == PositionState.Hidden ||
                entry.State == PositionState.Unknown && GetUnknownStateVisibility(currentIndex) == LineVisibility.Hidden)
            {
                return new LineMapping(unmapped, characterOffset: null, mappedSpan: default);
            }
 
            string path = entry.MappedPathOpt ?? string.Empty;
            bool hasMappedPath = entry.MappedPathOpt != null;
 
            if (entry.State == PositionState.RemappedSpan)
            {
                return new LineMapping(
                    unmapped,
                    characterOffset: entry.UnmappedCharacterOffset,
                    new FileLinePositionSpan(path, entry.MappedSpan, hasMappedPath));
            }
 
            var mappedSpan = new LinePositionSpan(
                new LinePosition(entry.MappedLine, character: 0),
                new LinePosition(entry.MappedLine + unmappedEndLine - entry.UnmappedLine, lineLength));
            var mapped = new FileLinePositionSpan(path, mappedSpan, hasMappedPath);
            return new LineMapping(unmapped, characterOffset: null, mapped);
        }
    }
}