File: Syntax\CSharpLineDirectiveMap.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.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Syntax
{
    /// <summary>
    /// Adds C# specific parts to the line directive map.
    /// </summary>
    internal class CSharpLineDirectiveMap : LineDirectiveMap<DirectiveTriviaSyntax>
    {
        public CSharpLineDirectiveMap(SyntaxTree syntaxTree)
            : base(syntaxTree)
        {
        }
 
        // Add all active #line directives under trivia into the list, in source code order.
        protected override bool ShouldAddDirective(DirectiveTriviaSyntax directive)
        {
            return directive.IsActive && (directive.Kind() is SyntaxKind.LineDirectiveTrivia or SyntaxKind.LineSpanDirectiveTrivia);
        }
 
        // Given a directive and the previous entry, create a new entry.
        protected override LineMappingEntry GetEntry(DirectiveTriviaSyntax directiveNode, SourceText sourceText, LineMappingEntry previous)
        {
            Debug.Assert(ShouldAddDirective(directiveNode));
 
            // Get line number of NEXT line, hence the +1.
            var directiveLineNumber = sourceText.Lines.IndexOf(directiveNode.SpanStart) + 1;
 
            if (directiveNode is LineSpanDirectiveTriviaSyntax spanDirective)
            {
                return GetLineSpanDirectiveEntry(spanDirective, directiveLineNumber);
            }
 
            var directive = (LineDirectiveTriviaSyntax)directiveNode;
 
            // The default for the current entry does the same thing as the previous entry, except
            // resetting hidden.
            var unmappedLine = directiveLineNumber;
            var mappedLine = (previous.State == PositionState.RemappedSpan) ? unmappedLine : previous.MappedLine + directiveLineNumber - previous.UnmappedLine;
            var mappedPathOpt = (previous.State == PositionState.RemappedSpan) ? null : previous.MappedPathOpt;
            PositionState state = PositionState.Unmapped;
 
            // Modify the current entry based on the directive.
            SyntaxToken lineToken = directive.Line;
 
            if (!lineToken.IsMissing)
            {
                switch (lineToken.Kind())
                {
                    case SyntaxKind.HiddenKeyword:
                        state = PositionState.Hidden;
                        break;
 
                    case SyntaxKind.DefaultKeyword:
                        mappedLine = unmappedLine;
                        mappedPathOpt = null;
                        state = PositionState.Unmapped;
                        break;
 
                    case SyntaxKind.NumericLiteralToken:
                        // skip both the mapped line and the filename if the line number is not valid
                        if (!lineToken.ContainsDiagnostics)
                        {
                            object? value = lineToken.Value;
                            if (value is int)
                            {
                                // convert one-based line number into zero-based line number
                                mappedLine = ((int)value) - 1;
                            }
 
                            if (directive.File.Kind() == SyntaxKind.StringLiteralToken)
                            {
                                mappedPathOpt = (string?)directive.File.Value;
                            }
 
                            state = PositionState.Remapped;
                        }
 
                        break;
                }
            }
 
            return new LineMappingEntry(
                unmappedLine,
                mappedLine,
                mappedPathOpt,
                state);
        }
 
        private static LineMappingEntry GetLineSpanDirectiveEntry(LineSpanDirectiveTriviaSyntax spanDirective, int unmappedLine)
        {
            if (!spanDirective.HasErrors &&
                tryGetPosition(spanDirective.Start, isEnd: false, out LinePosition mappedStart) &&
                tryGetPosition(spanDirective.End, isEnd: true, out LinePosition mappedEnd) &&
                tryGetOptionalCharacterOffset(spanDirective.CharacterOffset, out int? characterOffset) &&
                tryGetStringLiteralValue(spanDirective.File, out string? mappedPathOpt))
            {
                return new LineMappingEntry(unmappedLine, new LinePositionSpan(mappedStart, mappedEnd), characterOffset, mappedPathOpt);
            }
            return new LineMappingEntry(unmappedLine, unmappedLine, mappedPathOpt: null, PositionState.Unmapped);
 
            static bool tryGetNumericLiteralValue(in SyntaxToken token, out int value, bool oneBased)
            {
                if (!token.IsMissing &&
                    token.Kind() == SyntaxKind.NumericLiteralToken &&
                    token.Value is int tokenValue)
                {
                    value = tokenValue;
 
                    // convert one-based line number into zero-based line number
                    if (oneBased)
                    {
                        value--;
                    }
 
                    return true;
                }
                value = 0;
                return false;
            }
 
            static bool tryGetStringLiteralValue(in SyntaxToken token, out string? value)
            {
                if (token.Kind() == SyntaxKind.StringLiteralToken)
                {
                    value = (string?)token.Value;
                    return true;
                }
                value = null;
                return false;
            }
 
            // returns false on error
            static bool tryGetOptionalCharacterOffset(in SyntaxToken token, out int? value)
            {
                if (!token.IsMissing)
                {
                    if (token.Kind() == SyntaxKind.None)
                    {
                        value = null;
                        return true;
                    }
                    int val = 0;
                    if (tryGetNumericLiteralValue(token, out val, oneBased: false))
                    {
                        value = val;
                        return true;
                    }
                }
                value = null;
                return false;
            }
 
            // returns false on error
            static bool tryGetPosition(LineDirectivePositionSyntax syntax, bool isEnd, out LinePosition position)
            {
                if (tryGetNumericLiteralValue(syntax.Line, out int line, oneBased: true) &&
                    tryGetNumericLiteralValue(syntax.Character, out int character, oneBased: true))
                {
                    position = new LinePosition(line, isEnd ? character + 1 : character);
                    return true;
                }
                position = default;
                return false;
            }
        }
 
        protected override LineMappingEntry InitializeFirstEntry()
        {
            // The first entry of the map is always 0,0,null,Unmapped -- the default mapping.
            return new LineMappingEntry(0, 0, null, PositionState.Unmapped);
        }
 
        public override LineVisibility GetLineVisibility(SourceText sourceText, int position)
        {
            var unmappedPos = sourceText.Lines.GetLinePosition(position);
 
            // if there's only one entry (which is created as default for each file), all lines
            // are treated as being visible
            if (Entries.Length == 1)
            {
                Debug.Assert(Entries[0].State == PositionState.Unmapped);
                return LineVisibility.Visible;
            }
 
            var index = FindEntryIndex(unmappedPos.Line);
            var entry = Entries[index];
 
            // the state should not be set to the ones used for VB only.
            Debug.Assert(entry.State != PositionState.Unknown &&
                         entry.State != PositionState.RemappedAfterHidden &&
                         entry.State != PositionState.RemappedAfterUnknown);
 
            switch (entry.State)
            {
                case PositionState.Unmapped:
                    if (index == 0)
                    {
                        return LineVisibility.BeforeFirstLineDirective;
                    }
                    else
                    {
                        return LineVisibility.Visible;
                    }
 
                case PositionState.Remapped:
                case PositionState.RemappedSpan:
                    return LineVisibility.Visible;
 
                case PositionState.Hidden:
                    return LineVisibility.Hidden;
 
                default:
                    throw ExceptionUtilities.UnexpectedValue(entry.State);
            }
        }
 
        // C# does not have unknown visibility state
        protected override LineVisibility GetUnknownStateVisibility(int index)
            => throw ExceptionUtilities.Unreachable();
 
        internal override FileLinePositionSpan TranslateSpanAndVisibility(SourceText sourceText, string treeFilePath, TextSpan span, out bool isHiddenPosition)
        {
            var lines = sourceText.Lines;
            var unmappedStartPos = lines.GetLinePosition(span.Start);
            var unmappedEndPos = lines.GetLinePosition(span.End);
 
            // most common case is where we have only one mapping entry.
            if (this.Entries.Length == 1)
            {
                Debug.Assert(this.Entries[0].State == PositionState.Unmapped);
                Debug.Assert(this.Entries[0].MappedLine == this.Entries[0].UnmappedLine);
                Debug.Assert(this.Entries[0].MappedLine == 0);
                Debug.Assert(this.Entries[0].MappedPathOpt == null);
 
                isHiddenPosition = false;
                return new FileLinePositionSpan(treeFilePath, unmappedStartPos, unmappedEndPos);
            }
 
            var entry = FindEntry(unmappedStartPos.Line);
 
            // the state should not be set to the ones used for VB only.
            Debug.Assert(entry.State != PositionState.Unknown &&
                            entry.State != PositionState.RemappedAfterHidden &&
                            entry.State != PositionState.RemappedAfterUnknown);
 
            isHiddenPosition = entry.State == PositionState.Hidden;
 
            return TranslateSpan(entry, treeFilePath, unmappedStartPos, unmappedEndPos);
        }
    }
}