Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EnC support for line mappings #52922

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,4 @@ static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.UsingDirective(Microsoft.Code
Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.GlobalKeyword.get -> Microsoft.CodeAnalysis.SyntaxToken
Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.Update(Microsoft.CodeAnalysis.SyntaxToken globalKeyword, Microsoft.CodeAnalysis.SyntaxToken usingKeyword, Microsoft.CodeAnalysis.SyntaxToken staticKeyword, Microsoft.CodeAnalysis.CSharp.Syntax.NameEqualsSyntax alias, Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax name, Microsoft.CodeAnalysis.SyntaxToken semicolonToken) -> Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax
Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.WithGlobalKeyword(Microsoft.CodeAnalysis.SyntaxToken globalKeyword) -> Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax


override Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.GetLineMappings(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IEnumerable<Microsoft.CodeAnalysis.LineMapping>
71 changes: 71 additions & 0 deletions src/Compilers/CSharp/Portable/Syntax/CSharpLineDirectiveMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

Expand Down Expand Up @@ -164,5 +166,74 @@ internal override FileLinePositionSpan TranslateSpanAndVisibility(SourceText sou

return TranslateSpan(entry, treeFilePath, unmappedStartPos, unmappedEndPos);
}

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 && 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)
{
var endLine = lines[unmappedEndLine];
int lineLength = endLine.EndIncludingLineBreak - endLine.Start;

// span ends just at the start of the line containing #line directive
// #line Current "file1"
// [|....\n
// ...........\n|]
// #line Next "file2"
var unmapped = new LinePositionSpan(
new LinePosition(current.UnmappedLine, character: 0),
new LinePosition(unmappedEndLine, lineLength));

var mapped = current.IsHidden ? default : new FileLinePositionSpan(
current.MappedPathOpt ?? string.Empty,
new LinePositionSpan(
new LinePosition(current.MappedLine, character: 0),
new LinePosition(current.MappedLine + unmappedEndLine - current.UnmappedLine, lineLength)),
hasMappedPath: current.MappedPathOpt != null);

yield return new LineMapping(unmapped, mapped);
}

current = next;
}

var lastLine = lines[^1];

// last span (unless the last #line is on the last line):
// #line Current "file1"
// [|....\n
// ...........\n|]
if (current.UnmappedLine <= lastLine.LineNumber)
{
int lineLength = lastLine.EndIncludingLineBreak - lastLine.Start;

var unmapped = new LinePositionSpan(
new LinePosition(current.UnmappedLine, character: 0),
new LinePosition(lastLine.LineNumber, lineLength));

var mapped = current.IsHidden ? default : new FileLinePositionSpan(
current.MappedPathOpt ?? string.Empty,
new LinePositionSpan(
new LinePosition(current.MappedLine, character: 0),
new LinePosition(current.MappedLine + lastLine.LineNumber - current.UnmappedLine, lineLength)),
hasMappedPath: current.MappedPathOpt != null);

yield return new LineMapping(unmapped, mapped);
}
}
}
}
67 changes: 26 additions & 41 deletions src/Compilers/CSharp/Portable/Syntax/CSharpSyntaxTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -607,6 +608,17 @@ public override IList<TextChange> GetChanges(SyntaxTree oldTree)

#region LinePositions and Locations

private CSharpLineDirectiveMap GetDirectiveMap()
{
if (_lazyLineDirectiveMap == null)
{
// Create the line directive map on demand.
Interlocked.CompareExchange(ref _lazyLineDirectiveMap, new CSharpLineDirectiveMap(this), null);
}

return _lazyLineDirectiveMap;
}

/// <summary>
/// Gets the location in terms of path, line and column for a given span.
/// </summary>
Expand All @@ -617,9 +629,7 @@ public override IList<TextChange> GetChanges(SyntaxTree oldTree)
/// </returns>
/// <remarks>The values are not affected by line mapping directives (<c>#line</c>).</remarks>
public override FileLinePositionSpan GetLineSpan(TextSpan span, CancellationToken cancellationToken = default)
{
return new FileLinePositionSpan(this.FilePath, GetLinePosition(span.Start), GetLinePosition(span.End));
}
=> new(FilePath, GetLinePosition(span.Start, cancellationToken), GetLinePosition(span.End, cancellationToken));

/// <summary>
/// Gets the location in terms of path, line and column after applying source line mapping directives (<c>#line</c>).
Expand All @@ -638,25 +648,18 @@ public override FileLinePositionSpan GetLineSpan(TextSpan span, CancellationToke
/// </para>
/// </returns>
public override FileLinePositionSpan GetMappedLineSpan(TextSpan span, CancellationToken cancellationToken = default)
{
if (_lazyLineDirectiveMap == null)
{
// Create the line directive map on demand.
Interlocked.CompareExchange(ref _lazyLineDirectiveMap, new CSharpLineDirectiveMap(this), null);
}

return _lazyLineDirectiveMap.TranslateSpan(this.GetText(cancellationToken), this.FilePath, span);
}
=> GetDirectiveMap().TranslateSpan(GetText(cancellationToken), this.FilePath, span);

/// <inheritdoc/>
public override LineVisibility GetLineVisibility(int position, CancellationToken cancellationToken = default)
{
if (_lazyLineDirectiveMap == null)
{
// Create the line directive map on demand.
Interlocked.CompareExchange(ref _lazyLineDirectiveMap, new CSharpLineDirectiveMap(this), null);
}
=> GetDirectiveMap().GetLineVisibility(GetText(cancellationToken), position);

return _lazyLineDirectiveMap.GetLineVisibility(this.GetText(cancellationToken), position);
/// <inheritdoc/>
public override IEnumerable<LineMapping> GetLineMappings(CancellationToken cancellationToken = default)
{
var map = GetDirectiveMap();
Debug.Assert(map.Entries.Length >= 1);
return (map.Entries.Length == 1) ? Array.Empty<LineMapping>() : map.GetLineMappings(GetText(cancellationToken).Lines);
}

/// <summary>
Expand All @@ -667,30 +670,14 @@ public override LineVisibility GetLineVisibility(int position, CancellationToken
/// <param name="isHiddenPosition">When the method returns, contains a boolean value indicating whether this span is considered hidden or not.</param>
/// <returns>A resulting <see cref="FileLinePositionSpan"/>.</returns>
internal override FileLinePositionSpan GetMappedLineSpanAndVisibility(TextSpan span, out bool isHiddenPosition)
{
if (_lazyLineDirectiveMap == null)
{
// Create the line directive map on demand.
Interlocked.CompareExchange(ref _lazyLineDirectiveMap, new CSharpLineDirectiveMap(this), null);
}

return _lazyLineDirectiveMap.TranslateSpanAndVisibility(this.GetText(), this.FilePath, span, out isHiddenPosition);
}
=> GetDirectiveMap().TranslateSpanAndVisibility(GetText(), FilePath, span, out isHiddenPosition);

/// <summary>
/// Gets a boolean value indicating whether there are any hidden regions in the tree.
/// </summary>
/// <returns>True if there is at least one hidden region.</returns>
public override bool HasHiddenRegions()
{
if (_lazyLineDirectiveMap == null)
{
// Create the line directive map on demand.
Interlocked.CompareExchange(ref _lazyLineDirectiveMap, new CSharpLineDirectiveMap(this), null);
}

return _lazyLineDirectiveMap.HasAnyHiddenRegions();
}
=> GetDirectiveMap().HasAnyHiddenRegions();

/// <summary>
/// Given the error code and the source location, get the warning state based on <c>#pragma warning</c> directives.
Expand Down Expand Up @@ -756,10 +743,8 @@ bool isGeneratedHeuristic()

private GeneratedKind _lazyIsGeneratedCode = GeneratedKind.Unknown;

private LinePosition GetLinePosition(int position)
{
return this.GetText().Lines.GetLinePosition(position);
}
private LinePosition GetLinePosition(int position, CancellationToken cancellationToken)
=> GetText(cancellationToken).Lines.GetLinePosition(position);

/// <summary>
/// Gets a <see cref="Location"/> for the specified text <paramref name="span"/>.
Expand Down
75 changes: 70 additions & 5 deletions src/Compilers/CSharp/Test/Syntax/Diagnostics/LocationsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
Expand Down Expand Up @@ -120,7 +119,7 @@ public void TestLineMapping1()
string sampleProgram = @"using System;
class X {
#line 20 ""banana.cs""
int x;
int x;
int y;
#line 44
int z;
Expand All @@ -147,6 +146,17 @@ class X {
AssertMappedSpanEqual(syntaxTree, "w;", "goo.cs", 8, 4, 8, 6, hasMappedPath: false);
AssertMappedSpanEqual(syntaxTree, "q;\r\nin", "goo.cs", 10, 4, 11, 2, hasMappedPath: false);
AssertMappedSpanEqual(syntaxTree, "a;", "goo.cs", 15, 4, 15, 6, hasMappedPath: false);

var text = syntaxTree.GetText();

AssertEx.Equal(new[]
{
"[|using System;\r\nclass X {\r\n|] -> : (0,0)-(1,11)",
"[|int x;\r\nint y;\r\n|] -> banana.cs: (19,0)-(20,8)",
"[|int z;\r\n|] -> banana.cs: (43,0)-(43,8)",
"[|int w;\r\n|] -> : (8,0)-(8,8)",
"[|int q;\r\nint f;\r\n#if false\r\n#line 17 \"d:\\twing.cs\"\r\n#endif\r\nint a;\r\n}|] -> : (0,0)-(0,0)"
}, syntaxTree.GetLineMappings(default).Select(mapping => $"[|{text.GetSubText(text.Lines.GetTextSpan(mapping.Span))}|] -> {mapping.MappedSpan}"));
}

[Fact]
Expand Down Expand Up @@ -183,8 +193,6 @@ public void TestLineMapping_NoSyntaxTreePath()
#line 20
class X {}
";
var resolver = new TestSourceResolver();

AssertMappedSpanEqual(SyntaxFactory.ParseSyntaxTree(sampleProgram, path: ""), "class X {}", "", 19, 0, 19, 10, hasMappedPath: false);
AssertMappedSpanEqual(SyntaxFactory.ParseSyntaxTree(sampleProgram, path: " "), "class X {}", " ", 19, 0, 19, 10, hasMappedPath: false);
}
Expand Down Expand Up @@ -212,14 +220,71 @@ public void TestLineMappingNoDirectives()
{
string sampleProgram = @"using System;
class X {
int x;
int x;
}";
SyntaxTree syntaxTree = SyntaxFactory.ParseSyntaxTree(sampleProgram, path: "c:\\goo.cs");

AssertMappedSpanEqual(syntaxTree, "ing Sy", "c:\\goo.cs", 0, 2, 0, 8, hasMappedPath: false);
AssertMappedSpanEqual(syntaxTree, "class X", "c:\\goo.cs", 1, 0, 1, 7, hasMappedPath: false);
AssertMappedSpanEqual(syntaxTree, $"System;{Environment.NewLine}class X", "c:\\goo.cs", 0, 6, 1, 7, hasMappedPath: false);
AssertMappedSpanEqual(syntaxTree, "x;", "c:\\goo.cs", 2, 4, 2, 6, hasMappedPath: false);

Assert.Empty(syntaxTree.GetLineMappings(default));
}

[Fact]
public void TestLineMappingFirstAndLastLineDirectives()
{
string sampleProgram = @"#line 20
class X {}
#line 30".NormalizeLineEndings();
var syntaxTree = SyntaxFactory.ParseSyntaxTree(sampleProgram, path: "c:\\goo.cs");

var text = syntaxTree.GetText();

AssertEx.Equal(new[]
{
"[|class X {}\r\n|] -> : (19,0)-(19,12)",
}, syntaxTree.GetLineMappings(default).Select(mapping => $"[|{text.GetSubText(text.Lines.GetTextSpan(mapping.Span))}|] -> {mapping.MappedSpan}"));
}

[Fact]
public void TestLineMappingLastLineDirectiveFollowedByEmptyLine()
{
string sampleProgram = @"#line 30
".NormalizeLineEndings();

var syntaxTree = SyntaxFactory.ParseSyntaxTree(sampleProgram, path: "c:\\goo.cs");

var text = syntaxTree.GetText();

AssertEx.Equal(new[]
{
"[||] -> : (29,0)-(29,0)",
}, syntaxTree.GetLineMappings(default).Select(mapping => $"[|{text.GetSubText(text.Lines.GetTextSpan(mapping.Span))}|] -> {mapping.MappedSpan}"));
}

[Fact]
public void TestLineMappingConsecutiveDirectives()
{
string sampleProgram =
@"#line hidden
#line default
class C {}
#line 5
#line 6
class D {}
".NormalizeLineEndings();

var syntaxTree = SyntaxFactory.ParseSyntaxTree(sampleProgram, path: "c:\\goo.cs");

var text = syntaxTree.GetText();

AssertEx.Equal(new[]
{
"[|class C {}\r\n|] -> : (2,0)-(2,12)",
"[|class D {}\r\n|] -> : (5,0)-(6,0)",
}, syntaxTree.GetLineMappings(default).Select(mapping => $"[|{text.GetSubText(text.Lines.GetTextSpan(mapping.Span))}|] -> {mapping.MappedSpan}"));
}

[WorkItem(537005, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/537005")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,46 @@ public static ImmutableArray<TResult> SelectAsArray<TItem, TArg, TResult>(this A
}
}

/// <summary>
/// Maps an array builder to immutable array.
/// </summary>
/// <typeparam name="TItem"></typeparam>
/// <typeparam name="TArg"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="items">The sequence to map</param>
/// <param name="map">The mapping delegate</param>
/// <param name="arg">The extra input used by mapping delegate</param>
/// <returns>If the items's length is 0, this will return an empty immutable array.</returns>
public static ImmutableArray<TResult> SelectAsArrayWithIndex<TItem, TArg, TResult>(this ArrayBuilder<TItem> items, Func<TItem, int, TArg, TResult> map, TArg arg)
{
switch (items.Count)
{
case 0:
return ImmutableArray<TResult>.Empty;

case 1:
return ImmutableArray.Create(map(items[0], 0, arg));

case 2:
return ImmutableArray.Create(map(items[0], 0, arg), map(items[1], 1, arg));

case 3:
return ImmutableArray.Create(map(items[0], 0, arg), map(items[1], 1, arg), map(items[2], 2, arg));

case 4:
return ImmutableArray.Create(map(items[0], 0, arg), map(items[1], 1, arg), map(items[2], 2, arg), map(items[3], 3, arg));

default:
var builder = ArrayBuilder<TResult>.GetInstance(items.Count);
foreach (var item in items)
{
builder.Add(map(item, builder.Count, arg));
}

return builder.ToImmutableAndFree();
}
}

public static void AddOptional<T>(this ArrayBuilder<T> builder, T? item)
where T : class
{
Expand Down
Loading