| File: Syntax\CSharpLineDirectiveMap.cs | Web Access |
| Project: src\roslyn\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); } } }