Skip to content

Commit 5b924e8

Browse files
authored
Use next sibling in SyntaxNode.GetChildPosition() if available (#66876)
1 parent 37875fc commit 5b924e8

File tree

10 files changed

+268
-8
lines changed

10 files changed

+268
-8
lines changed

src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxListTests.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

77
using Microsoft.CodeAnalysis.CSharp.Syntax;
88
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
9+
using Microsoft.CodeAnalysis.PooledObjects;
910
using Microsoft.CodeAnalysis.Test.Utilities;
1011
using System;
1112
using System.Collections.Generic;
13+
using System.Collections.Immutable;
1214
using System.Linq;
1315
using System.Text;
1416
using System.Threading.Tasks;
@@ -305,5 +307,92 @@ public void WithLotsOfChildrenTest()
305307
}
306308
}
307309
}
310+
311+
[Theory]
312+
[CombinatorialData]
313+
public void EnumerateWithManyChildren_Forward(bool trailingSeparator)
314+
{
315+
const int n = 200000;
316+
var builder = new StringBuilder();
317+
builder.Append("int[] values = new[] { ");
318+
for (int i = 0; i < n; i++) builder.Append("0, ");
319+
if (!trailingSeparator) builder.Append("0 ");
320+
builder.AppendLine("};");
321+
322+
var tree = CSharpSyntaxTree.ParseText(builder.ToString());
323+
// Do not descend into InitializerExpressionSyntax since that will populate SeparatedWithManyChildren._children.
324+
var node = tree.GetRoot().DescendantNodes().OfType<InitializerExpressionSyntax>().First();
325+
326+
foreach (var child in node.ChildNodesAndTokens())
327+
{
328+
_ = child.ToString();
329+
}
330+
}
331+
332+
// Tests should timeout when using SeparatedWithManyChildren.GetChildPosition()
333+
// instead of GetChildPositionFromEnd().
334+
[WorkItem(66475, "https://github.com/dotnet/roslyn/issues/66475")]
335+
[Theory]
336+
[CombinatorialData]
337+
public void EnumerateWithManyChildren_Reverse(bool trailingSeparator)
338+
{
339+
const int n = 200000;
340+
var builder = new StringBuilder();
341+
builder.Append("int[] values = new[] { ");
342+
for (int i = 0; i < n; i++) builder.Append("0, ");
343+
if (!trailingSeparator) builder.Append("0 ");
344+
builder.AppendLine("};");
345+
346+
var tree = CSharpSyntaxTree.ParseText(builder.ToString());
347+
// Do not descend into InitializerExpressionSyntax since that will populate SeparatedWithManyChildren._children.
348+
var node = tree.GetRoot().DescendantNodes().OfType<InitializerExpressionSyntax>().First();
349+
350+
foreach (var child in node.ChildNodesAndTokens().Reverse())
351+
{
352+
_ = child.ToString();
353+
}
354+
}
355+
356+
[Theory]
357+
[InlineData("int[] values = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };")]
358+
[InlineData("int[] values = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, };")]
359+
public void EnumerateWithManyChildren_Compare(string source)
360+
{
361+
CSharpSyntaxTree.ParseText(source).VerifyChildNodePositions();
362+
363+
var builder = ArrayBuilder<SyntaxNodeOrToken>.GetInstance();
364+
foreach (var node in parseAndGetInitializer(source).ChildNodesAndTokens().Reverse())
365+
{
366+
builder.Add(node);
367+
}
368+
builder.ReverseContents();
369+
var childNodes1 = builder.ToImmutableAndFree();
370+
371+
builder = ArrayBuilder<SyntaxNodeOrToken>.GetInstance();
372+
foreach (var node in parseAndGetInitializer(source).ChildNodesAndTokens())
373+
{
374+
builder.Add(node);
375+
}
376+
var childNodes2 = builder.ToImmutableAndFree();
377+
378+
Assert.Equal(childNodes1.Length, childNodes2.Length);
379+
380+
for (int i = 0; i < childNodes1.Length; i++)
381+
{
382+
var child1 = childNodes1[i];
383+
var child2 = childNodes2[i];
384+
Assert.Equal(child1.Position, child2.Position);
385+
Assert.Equal(child1.EndPosition, child2.EndPosition);
386+
Assert.Equal(child1.Width, child2.Width);
387+
Assert.Equal(child1.FullWidth, child2.FullWidth);
388+
}
389+
390+
static InitializerExpressionSyntax parseAndGetInitializer(string source)
391+
{
392+
var tree = CSharpSyntaxTree.ParseText(source);
393+
// Do not descend into InitializerExpressionSyntax since that will populate SeparatedWithManyChildren._children.
394+
return tree.GetRoot().DescendantNodes().OfType<InitializerExpressionSyntax>().First();
395+
}
396+
}
308397
}
309398
}

src/Compilers/Core/Portable/Syntax/SyntaxList.SeparatedWithManyChildren.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System;
6-
75
namespace Microsoft.CodeAnalysis.Syntax
86
{
97
internal partial class SyntaxList
108
{
11-
internal class SeparatedWithManyChildren : SyntaxList
9+
internal sealed class SeparatedWithManyChildren : SyntaxList
1210
{
1311
private readonly ArrayElement<SyntaxNode?>[] _children;
1412

@@ -39,6 +37,25 @@ internal SeparatedWithManyChildren(InternalSyntax.SyntaxList green, SyntaxNode?
3937

4038
return _children[i >> 1].Value;
4139
}
40+
41+
internal override int GetChildPosition(int index)
42+
{
43+
// If the previous sibling (ignoring separator) is not cached, but the next sibling
44+
// (ignoring separator) is cached, use the next sibling to determine position.
45+
int valueIndex = (index & 1) != 0 ? index - 1 : index;
46+
// The check for valueIndex >= Green.SlotCount - 2 ignores the last item because the last item
47+
// is a separator and separators are not cached. In those cases, when the index represents
48+
// the last or next to last item, we still want to calculate the position from the end of
49+
// the list rather than the start.
50+
if (valueIndex > 1
51+
&& GetCachedSlot(valueIndex - 2) is null
52+
&& (valueIndex >= Green.SlotCount - 2 || GetCachedSlot(valueIndex + 2) is { }))
53+
{
54+
return GetChildPositionFromEnd(index);
55+
}
56+
57+
return base.GetChildPosition(index);
58+
}
4259
}
4360
}
4461
}

src/Compilers/Core/Portable/Syntax/SyntaxList.SeparatedWithManyWeakChildren.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.CodeAnalysis.Syntax
88
{
99
internal partial class SyntaxList
1010
{
11-
internal class SeparatedWithManyWeakChildren : SyntaxList
11+
internal sealed class SeparatedWithManyWeakChildren : SyntaxList
1212
{
1313
private readonly ArrayElement<WeakReference<SyntaxNode>?>[] _children;
1414

src/Compilers/Core/Portable/Syntax/SyntaxList.WithManyChildren.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.CodeAnalysis.Syntax
88
{
99
internal partial class SyntaxList
1010
{
11-
internal class WithManyChildren : SyntaxList
11+
internal sealed class WithManyChildren : SyntaxList
1212
{
1313
private readonly ArrayElement<SyntaxNode?>[] _children;
1414

src/Compilers/Core/Portable/Syntax/SyntaxList.WithManyWeakChildren.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.CodeAnalysis.Syntax
99
{
1010
internal partial class SyntaxList
1111
{
12-
internal class WithManyWeakChildren : SyntaxList
12+
internal sealed class WithManyWeakChildren : SyntaxList
1313
{
1414
private readonly ArrayElement<WeakReference<SyntaxNode>?>[] _children;
1515

src/Compilers/Core/Portable/Syntax/SyntaxList.WithThreeChildren.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.CodeAnalysis.Syntax
88
{
99
internal partial class SyntaxList
1010
{
11-
internal class WithThreeChildren : SyntaxList
11+
internal sealed class WithThreeChildren : SyntaxList
1212
{
1313
private SyntaxNode? _child0;
1414
private SyntaxNode? _child1;

src/Compilers/Core/Portable/Syntax/SyntaxList.WithTwoChildren.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.Syntax
1010
{
1111
internal partial class SyntaxList
1212
{
13-
internal class WithTwoChildren : SyntaxList
13+
internal sealed class WithTwoChildren : SyntaxList
1414
{
1515
private SyntaxNode? _child0;
1616
private SyntaxNode? _child1;

src/Compilers/Core/Portable/Syntax/SyntaxNode.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,11 @@ internal int GetChildIndex(int slot)
612612
/// </summary>
613613
internal virtual int GetChildPosition(int index)
614614
{
615+
if (this.GetCachedSlot(index) is { } node)
616+
{
617+
return node.Position;
618+
}
619+
615620
int offset = 0;
616621
var green = this.Green;
617622
while (index > 0)
@@ -632,6 +637,36 @@ internal virtual int GetChildPosition(int index)
632637
return this.Position + offset;
633638
}
634639

640+
// Similar to GetChildPosition() but calculating based on the positions of
641+
// following siblings rather than previous siblings.
642+
internal int GetChildPositionFromEnd(int index)
643+
{
644+
if (this.GetCachedSlot(index) is { } node)
645+
{
646+
return node.Position;
647+
}
648+
649+
var green = this.Green;
650+
int offset = green.GetSlot(index)?.FullWidth ?? 0;
651+
int slotCount = green.SlotCount;
652+
while (index < slotCount - 1)
653+
{
654+
index++;
655+
var nextSibling = this.GetCachedSlot(index);
656+
if (nextSibling != null)
657+
{
658+
return nextSibling.Position - offset;
659+
}
660+
var greenChild = green.GetSlot(index);
661+
if (greenChild != null)
662+
{
663+
offset += greenChild.FullWidth;
664+
}
665+
}
666+
667+
return this.EndPosition - offset;
668+
}
669+
635670
public Location GetLocation()
636671
{
637672
return this.SyntaxTree.GetLocation(this.Span);

src/Compilers/Test/Core/Compilation/CompilationExtensions.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
using Roslyn.Test.Utilities;
2828
using Roslyn.Utilities;
2929
using Xunit;
30+
using SeparatedWithManyChildren = Microsoft.CodeAnalysis.Syntax.SyntaxList.SeparatedWithManyChildren;
3031

3132
namespace Microsoft.CodeAnalysis.Test.Utilities
3233
{
@@ -315,6 +316,8 @@ void checkTimeout()
315316
}
316317
}
317318
}
319+
320+
tree.VerifyChildNodePositions();
318321
}
319322

320323
var explicitNodeMap = new Dictionary<SyntaxNode, IOperation>();
@@ -381,6 +384,72 @@ void checkControlFlowGraph(IOperation root, ISymbol associatedSymbol)
381384
}
382385
}
383386

387+
internal static void VerifyChildNodePositions(this SyntaxTree tree)
388+
{
389+
var nodes = tree.GetRoot().DescendantNodesAndSelf();
390+
foreach (var node in nodes)
391+
{
392+
var childNodesAndTokens = node.ChildNodesAndTokens();
393+
if (childNodesAndTokens.Node is { } container)
394+
{
395+
for (int i = 0; i < childNodesAndTokens.Count; i++)
396+
{
397+
if (container.GetNodeSlot(i) is SeparatedWithManyChildren separatedList)
398+
{
399+
verifyPositions(separatedList);
400+
}
401+
}
402+
}
403+
}
404+
405+
static void verifyPositions(SeparatedWithManyChildren separatedList)
406+
{
407+
var green = (Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList)separatedList.Green;
408+
409+
// Calculate positions from start, using existing cache.
410+
int[] positions = getPositionsFromStart(separatedList);
411+
412+
// Calculate positions from end, using existing cache.
413+
AssertEx.Equal(positions, getPositionsFromEnd(separatedList));
414+
415+
// Avoid testing without caches if the number of children is large.
416+
if (separatedList.SlotCount > 100)
417+
{
418+
return;
419+
}
420+
421+
// Calculate positions from start, with empty cache.
422+
AssertEx.Equal(positions, getPositionsFromStart(new SeparatedWithManyChildren(green, null, separatedList.Position)));
423+
424+
// Calculate positions from end, with empty cache.
425+
AssertEx.Equal(positions, getPositionsFromEnd(new SeparatedWithManyChildren(green, null, separatedList.Position)));
426+
}
427+
428+
// Calculate positions from start, using any existing cache of red nodes on separated list.
429+
static int[] getPositionsFromStart(SeparatedWithManyChildren separatedList)
430+
{
431+
int n = separatedList.SlotCount;
432+
var positions = new int[n];
433+
for (int i = 0; i < n; i++)
434+
{
435+
positions[i] = separatedList.GetChildPosition(i);
436+
}
437+
return positions;
438+
}
439+
440+
// Calculate positions from end, using any existing cache of red nodes on separated list.
441+
static int[] getPositionsFromEnd(SeparatedWithManyChildren separatedList)
442+
{
443+
int n = separatedList.SlotCount;
444+
var positions = new int[n];
445+
for (int i = n - 1; i >= 0; i--)
446+
{
447+
positions[i] = separatedList.GetChildPositionFromEnd(i);
448+
}
449+
return positions;
450+
}
451+
}
452+
384453
/// <summary>
385454
/// The reference assembly System.Runtime.InteropServices.WindowsRuntime was removed in net5.0. This builds
386455
/// up <see cref="CompilationReference"/> which contains all of the well known types that were used from that

src/Compilers/VisualBasic/Test/Syntax/Syntax/SyntaxListTests.vb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
' See the LICENSE file in the project root for more information.
44

55
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
6+
Imports Roslyn.Test.Utilities
67

78
Namespace Microsoft.CodeAnalysis.VisualBasic.UnitTests
89
Public Class SyntaxListTests
@@ -241,5 +242,54 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UnitTests
241242
Next
242243
End Sub
243244

245+
<Fact>
246+
Public Sub EnumerateWithManyChildren_Forward()
247+
Const n = 200000
248+
Dim builder As New System.Text.StringBuilder()
249+
builder.AppendLine("Module M")
250+
builder.AppendLine(" Sub Main")
251+
builder.Append(" Dim values As Integer() = {")
252+
For i = 0 To n - 1
253+
builder.Append("0, ")
254+
Next
255+
builder.AppendLine("}")
256+
builder.AppendLine(" End Sub")
257+
builder.AppendLine("End Module")
258+
259+
Dim tree = VisualBasicSyntaxTree.ParseText(builder.ToString())
260+
' Do not descend into CollectionInitializerSyntax since that will populate SeparatedWithManyChildren._children.
261+
Dim node = tree.GetRoot().DescendantNodes().OfType(Of CollectionInitializerSyntax)().First()
262+
263+
For Each child In node.ChildNodesAndTokens()
264+
child.ToString()
265+
Next
266+
End Sub
267+
268+
' Tests should timeout when using SeparatedWithManyChildren.GetChildPosition()
269+
' instead of GetChildPositionFromEnd().
270+
<WorkItem(66475, "https://github.com/dotnet/roslyn/issues/66475")>
271+
<Fact>
272+
Public Sub EnumerateWithManyChildren_Reverse()
273+
Const n = 200000
274+
Dim builder As New System.Text.StringBuilder()
275+
builder.AppendLine("Module M")
276+
builder.AppendLine(" Sub Main")
277+
builder.Append(" Dim values As Integer() = {")
278+
For i = 0 To n - 1
279+
builder.Append("0, ")
280+
Next
281+
builder.AppendLine("}")
282+
builder.AppendLine(" End Sub")
283+
builder.AppendLine("End Module")
284+
285+
Dim tree = VisualBasicSyntaxTree.ParseText(builder.ToString())
286+
' Do not descend into CollectionInitializerSyntax since that will populate SeparatedWithManyChildren._children.
287+
Dim node = tree.GetRoot().DescendantNodes().OfType(Of CollectionInitializerSyntax)().First()
288+
289+
For Each child In node.ChildNodesAndTokens().Reverse()
290+
child.ToString()
291+
Next
292+
End Sub
293+
244294
End Class
245295
End Namespace

0 commit comments

Comments
 (0)