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

StackFrame Parser/Lexer as EmbeddedLanguage #56957

Merged
merged 62 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
f560e75
First pass implementation with a few tests as PoC
ryzngard Oct 5, 2021
872a90d
* Add generic method tests.
ryzngard Oct 6, 2021
ba21c9e
Update src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackF…
ryzngard Oct 6, 2021
9c04adb
Restructure tests so that each tree is evaluated more thoroughly
ryzngard Oct 6, 2021
c8b0bd3
Add more tests and clean up test framework making it easier to write …
ryzngard Oct 6, 2021
6f8fcd4
Fix the parsing so there is no look behind. Make the "at " trivia be …
ryzngard Oct 6, 2021
541f3fa
Parse trivia for method parens
ryzngard Oct 6, 2021
a5c49bc
Merge branch 'features/stackframe_parser' of https://www.github.com/r…
ryzngard Oct 6, 2021
1a0252c
Fix so trailing trivia is on the EOL token. Make sure to use TextTriv…
ryzngard Oct 6, 2021
35099cc
Get file parsing working and add a simple test
ryzngard Oct 7, 2021
59e54aa
change to TryParse* naming
ryzngard Oct 7, 2021
7d6ab41
Fix array bracket scanning
ryzngard Oct 7, 2021
1830c57
More file information tests. Clean up array and arity parsing.
ryzngard Oct 8, 2021
b171e55
Fix invalid path parsing and add tests
ryzngard Oct 8, 2021
87964e6
Add more file path tests. Require that a path is fully formed with li…
ryzngard Oct 9, 2021
15479df
Get rid of TextToken. It doesn't make sense and only causes potential…
ryzngard Oct 9, 2021
b0e6e6e
Update to remove scan arity from lexer and instead scan numbers with …
ryzngard Oct 11, 2021
0866c7b
Fix so parameters are better handled.
ryzngard Oct 25, 2021
3b4cbcb
PR feedback
ryzngard Oct 26, 2021
09c7ba4
Merge branch 'main' into features/stackframe_parser
ryzngard Oct 26, 2021
9b95418
Break up the TryParseIdentifierExpression function. Add tests for inv…
ryzngard Oct 26, 2021
9f28a29
Update the lexer to handle line numbers and paths instead of the pars…
ryzngard Oct 26, 2021
b3bdfa1
PR feedback changes
ryzngard Oct 26, 2021
ceac888
Move to EmbeddedSeparatedSyntaxNodeList
ryzngard Oct 27, 2021
f972f11
Update for passing tests and include keywords as identifiers
ryzngard Oct 27, 2021
dd729d2
Make creation of trivia private
ryzngard Oct 27, 2021
ebabf0f
Missed one
ryzngard Oct 27, 2021
80c7b77
Change StackFrameTree to inherit from EmbeddedSyntaxTree
ryzngard Oct 27, 2021
3904852
PR feedback. Make name nodes better handled to avoid common cases whe…
ryzngard Oct 27, 2021
598571c
Clean up type hierarchy by including declaration node as a type. Remo…
ryzngard Oct 27, 2021
ad40dfa
Remove exception usage from StackFrameParser
ryzngard Oct 27, 2021
1beb9dc
Fix tests
ryzngard Oct 27, 2021
cba32de
Fix spelling
ryzngard Oct 27, 2021
ab1a662
Add required overrides
ryzngard Oct 27, 2021
eeefa48
Remove StackFrameParseException reference in comment
ryzngard Oct 28, 2021
3958af9
PR feedback
ryzngard Oct 29, 2021
29754a2
shakes fist at blank lines
ryzngard Oct 29, 2021
1abd3ba
add assert for non null
ryzngard Oct 29, 2021
a017ec2
Remove equality overloads
ryzngard Oct 29, 2021
4a4b2eb
cleanup unused usings
ryzngard Oct 29, 2021
6f73bb6
fix default comparison
ryzngard Nov 1, 2021
875dc31
Merge branch 'main' into features/stackframe_parser
ryzngard Nov 1, 2021
0401dc4
PR feedback. Add more tests for invalid identifiers
ryzngard Nov 1, 2021
06f0a69
Add some asserts
ryzngard Nov 1, 2021
6654637
Typing
CyrusNajmabadi Nov 1, 2021
e74a7f1
cleanup test factory code
ryzngard Nov 1, 2021
99bf2bf
PR cleanup 1
ryzngard Nov 2, 2021
87ba143
Fix type hierarchy for ParameterDeclaration
ryzngard Nov 2, 2021
9eaa65e
Fix test build breaks
ryzngard Nov 2, 2021
7dec0eb
Fix tests and add more
ryzngard Nov 2, 2021
73d38e4
Refactor
ryzngard Nov 2, 2021
07007cc
Add space test for array commas
ryzngard Nov 2, 2021
d283218
Change to TryParseRequired
ryzngard Nov 3, 2021
80e5fcc
Move local functions back out
ryzngard Nov 3, 2021
604ec77
Change to Result instead of ParseResult. Allow lexer to have stronger…
ryzngard Nov 3, 2021
1fcea01
Fix test. Line number is required if looked for by the lexer
ryzngard Nov 3, 2021
c358299
Add more tests with invalid spacing
ryzngard Nov 3, 2021
9ca8a9c
Add extension method
ryzngard Nov 4, 2021
750a12a
Add clarifying comment and more tests
ryzngard Nov 4, 2021
eaeb334
Be strict about \r and \n characters.
ryzngard Nov 4, 2021
62d4bad
Add fuzzy testing for all inputs to ensure no crash
ryzngard Nov 4, 2021
a4013bb
Sort usings
ryzngard Nov 4, 2021
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// 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.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis.EmbeddedLanguages.Common;
using Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.UnitTests.EmbeddedLanguages.StackFrame
{
using StackFrameToken = EmbeddedSyntaxToken<StackFrameKind>;
using StackFrameTrivia = EmbeddedSyntaxTrivia<StackFrameKind>;
using StackFrameNodeOrToken = EmbeddedSyntaxNodeOrToken<StackFrameKind, StackFrameNode>;
ryzngard marked this conversation as resolved.
Show resolved Hide resolved

public partial class StackFrameParserTests
{
private static StackFrameToken CreateToken(StackFrameKind kind, string s, ImmutableArray<StackFrameTrivia> leadingTrivia = default, ImmutableArray<StackFrameTrivia> trailingTrivia = default)
=> new(
kind,
leadingTrivia.IsDefaultOrEmpty ? ImmutableArray<StackFrameTrivia>.Empty : leadingTrivia,
CodeAnalysis.EmbeddedLanguages.VirtualChars.VirtualCharSequence.Create(0, s),
trailingTrivia.IsDefaultOrEmpty ? ImmutableArray<StackFrameTrivia>.Empty : trailingTrivia,
ImmutableArray<EmbeddedDiagnostic>.Empty,
value: null!);

private static StackFrameTrivia CreateTrivia(StackFrameKind kind, string s)
=> new(kind, CodeAnalysis.EmbeddedLanguages.VirtualChars.VirtualCharSequence.Create(0, s), ImmutableArray<EmbeddedDiagnostic>.Empty);

private static ImmutableArray<StackFrameTrivia> CreateTriviaArray(StackFrameTrivia trivia)
=> ImmutableArray.Create(trivia);

private static readonly StackFrameToken DotToken = CreateToken(StackFrameKind.DotToken, ".");
private static readonly StackFrameToken CommaToken = CreateToken(StackFrameKind.CommaToken, ",");
private static readonly StackFrameToken OpenParenToken = CreateToken(StackFrameKind.OpenParenToken, "(");
private static readonly StackFrameToken CloseParenToken = CreateToken(StackFrameKind.CloseParenToken, ")");
private static readonly StackFrameToken OpenBracketToken = CreateToken(StackFrameKind.OpenBracketToken, "[");
private static readonly StackFrameToken CloseBracketToken = CreateToken(StackFrameKind.CloseBracketToken, "]");
private static readonly StackFrameToken SpaceToken = CreateToken(StackFrameKind.TextToken, " ");
ryzngard marked this conversation as resolved.
Show resolved Hide resolved

private static readonly StackFrameTrivia SpaceTrivia = CreateTrivia(StackFrameKind.WhitespaceTrivia, " ");

private static void AssertEqual(StackFrameNodeOrToken expected, StackFrameNodeOrToken actual)
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
{
AssertEqual(expected.Node, actual.Node);
AssertEqual(expected.Token, actual.Token);
}

private static void AssertEqual(StackFrameNode? expected, StackFrameNode? actual)
{
if (expected is null)
{
Assert.Null(actual);
return;
}

AssertEx.NotNull(actual);

Assert.Equal(expected.Kind, actual.Kind);
Assert.True(expected.ChildCount == actual.ChildCount, PrintChildDifference(expected, actual));

for (var i = 0; i < expected.ChildCount; i++)
{
AssertEqual(expected.ChildAt(i), actual.ChildAt(i));
}

static string PrintChildDifference(StackFrameNode expected, StackFrameNode actual)
{
var sb = new StringBuilder();
sb.Append("Expected: ");
Print(expected, sb);
sb.AppendLine();

sb.Append("Actual: ");
Print(actual, sb);

return sb.ToString();
}
}

private static void Print(StackFrameNode node, StringBuilder sb)
{
foreach (var child in node)
{
if (child.IsNode)
{
Print(child.Node, sb);
}
else
{
if (!child.Token.LeadingTrivia.IsDefaultOrEmpty)
{
Print(child.Token.LeadingTrivia, sb);
}

sb.Append(child.Token.VirtualChars.CreateString());

if (!child.Token.TrailingTrivia.IsDefaultOrEmpty)
{
Print(child.Token.TrailingTrivia, sb);
}
}
}
}

private static void Print(ImmutableArray<StackFrameTrivia> triviaArray, StringBuilder sb)
{
if (triviaArray.IsDefault)
{
sb.Append("<default>");
return;
}

if (triviaArray.IsEmpty)
{
sb.Append("<empty>");
return;
}

foreach (var trivia in triviaArray)
{
sb.Append(trivia.VirtualChars.CreateString());
}
}

private static void AssertEqual(StackFrameToken expected, StackFrameToken actual)
{
AssertEqual(expected.LeadingTrivia, actual.LeadingTrivia);
AssertEqual(expected.TrailingTrivia, actual.TrailingTrivia);

Assert.Equal(expected.Kind, actual.Kind);
Assert.Equal(expected.IsMissing, actual.IsMissing);
Assert.Equal(expected.VirtualChars.CreateString(), actual.VirtualChars.CreateString());
}

private static void AssertEqual(ImmutableArray<StackFrameTrivia> expected, ImmutableArray<StackFrameTrivia> actual)
{
var diffMessage = PrintDiff();

if (expected.IsDefault)
{
Assert.True(actual.IsDefault, diffMessage);
return;
}

Assert.False(actual.IsDefault, diffMessage);
Assert.True(expected.Length == actual.Length, diffMessage);

for (var i = 0; i < expected.Length; i++)
{
AssertEqual(expected[i], actual[i]);
}

string PrintDiff()
{
var sb = new StringBuilder();
sb.Append("Expected: ");

if (!expected.IsDefaultOrEmpty)
{
sb.Append('[');
}

Print(expected, sb);

if (expected.IsDefaultOrEmpty)
{
sb.AppendLine();
}
else
{
sb.AppendLine("]");
}

sb.Append("Actual: ");

if (!actual.IsDefaultOrEmpty)
{
sb.Append('[');
}

Print(actual, sb);

if (!actual.IsDefaultOrEmpty)
{
sb.Append(']');
}

return sb.ToString();
}
}

private static void AssertEqual(StackFrameTrivia expected, StackFrameTrivia actual)
{
Assert.Equal(expected.Kind, actual.Kind);
Assert.Equal(expected.VirtualChars.CreateString(), actual.VirtualChars.CreateString());
}

private static StackFrameArgumentList ArgumentList(params StackFrameNodeOrToken[] nodesOrTokens)
{
var nodesWithParens = nodesOrTokens.Prepend(OpenParenToken).Append(CloseParenToken).ToImmutableArray();
return new StackFrameArgumentList(nodesWithParens);
}

private static StackFrameMethodDeclarationNode MethodDeclaration(
StackFrameMemberAccessExpressionNode memberAccessExpression,
StackFrameArgumentList argumentList,
StackFrameTypeArgumentList? typeArgumnets = null)
{
return new StackFrameMethodDeclarationNode(memberAccessExpression, typeArgumnets, argumentList);
}

private static StackFrameMemberAccessExpressionNode MemberAccessExpression(StackFrameExpressionNode expressionNode, StackFrameBaseIdentifierNode identifierNode)
=> new(expressionNode, DotToken, identifierNode);

private static StackFrameIdentifierNode Identifier(string identifierName, ImmutableArray<StackFrameTrivia> leadingTrivia = default, ImmutableArray<StackFrameTrivia> trailingTrivia = default)
=> new(CreateToken(StackFrameKind.IdentifierToken, identifierName, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia));

private static StackFrameArrayExpressionNode ArrayExpression(StackFrameExpressionNode identifier, params StackFrameToken[] arrayTokens)
=> new(identifier, arrayTokens.ToImmutableArray());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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 Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.UnitTests.EmbeddedLanguages.StackFrame
{
public partial class StackFrameParserTests
{
[Fact]
public void TestMethodOneParam()
{
var input = @"at ConsoleApp4.MyClass.M(string s)";
var tree = StackFrameParser.TryParse(input);
AssertEx.NotNull(tree);

Assert.True(tree.Root.AtTrivia.HasValue);
Assert.False(tree.Root.InTrivia.HasValue);
Assert.Null(tree.Root.FileInformationExpression);
Assert.False(tree.Root.TrailingTrivia.HasValue);

var methodDeclaration = tree.Root.MethodDeclaration;

var expectedMethodDeclaration = MethodDeclaration(
MemberAccessExpression(
MemberAccessExpression(
Identifier("ConsoleApp4"),
Identifier("MyClass")),
Identifier("M")),

ArgumentList(
Identifier("string", trailingTrivia: CreateTriviaArray(SpaceTrivia)),
Identifier("s"))
);

AssertEqual(expectedMethodDeclaration, methodDeclaration);
}

[Fact]
public void TestMethodTwoParam()
{
var input = @"at ConsoleApp4.MyClass.M(string s, string t)";
var tree = StackFrameParser.TryParse(input);
AssertEx.NotNull(tree);

Assert.True(tree.Root.AtTrivia.HasValue);
Assert.False(tree.Root.InTrivia.HasValue);
Assert.Null(tree.Root.FileInformationExpression);
Assert.False(tree.Root.TrailingTrivia.HasValue);

var methodDeclaration = tree.Root.MethodDeclaration;

var expectedMethodDeclaration = MethodDeclaration(
MemberAccessExpression(
MemberAccessExpression(
Identifier("ConsoleApp4"),
Identifier("MyClass")),
Identifier("M")),

ArgumentList(
Identifier("string", trailingTrivia: CreateTriviaArray(SpaceTrivia)),
Identifier("s"),
CommaToken,
Identifier("string", leadingTrivia: CreateTriviaArray(SpaceTrivia), trailingTrivia: CreateTriviaArray(SpaceTrivia)),
Identifier("t"))
);

AssertEqual(expectedMethodDeclaration, methodDeclaration);
}

[Fact]
public void TestMethodArrayParam()
{
var input = @"at ConsoleApp4.MyClass.M(string[] s)";
var tree = StackFrameParser.TryParse(input);
AssertEx.NotNull(tree);

Assert.True(tree.Root.AtTrivia.HasValue);
Assert.False(tree.Root.InTrivia.HasValue);
Assert.Null(tree.Root.FileInformationExpression);
Assert.False(tree.Root.TrailingTrivia.HasValue);

var methodDeclaration = tree.Root.MethodDeclaration;

var expectedMethodDeclaration = MethodDeclaration(
MemberAccessExpression(
MemberAccessExpression(
Identifier("ConsoleApp4"),
Identifier("MyClass")),
Identifier("M")),

ArgumentList(
ArrayExpression(Identifier("string"), OpenBracketToken, CloseBracketToken),
Identifier("s", leadingTrivia: CreateTriviaArray(SpaceTrivia)))
);

AssertEqual(expectedMethodDeclaration, methodDeclaration);
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure if you already did it, but:

  1. have your tests validate you don't crash on any prefix/suffix string of your test strings.
  2. for every node type try to test a few edge cases to make sure you don't crash.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the edge cases. think about empty tokens, or an extra intermediary token. so things like A.B.(params)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have your tests validate you don't crash on any prefix/suffix string of your test strings

Added as inputs to TtestIdentifierNames

for every node type try to test a few edge cases to make sure you don't crash

I tried to do this with various input into TestInvalidInputs. Let me know if you can think of explicit things I missed

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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.Text;

namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame
{
internal interface IStackFrameNodeVisitor
{
void Visit(StackFrameTextNode stackFrameTextNode);
void Visit(StackFrameMethodDeclarationNode stackFrameMethodDeclarationNode);
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
void Visit(StackFrameMemberAccessExpressionNode stackFrameMemberAccessExpressionNode);
void Visit(StackFrameTypeArgumentList stackFrameTypeArguments);
void Visit(StackFrameArgumentList stackFrameArgumentList);
void Visit(StackFrameIdentifierNode stackFrameIdentifierNode);
void Visit(StackFrameGenericTypeIdentifier stackFrameGenericTypeIdentifier);
void Visit(StackFrameTypeArgument stackFrameTypeArgument);
void Visit(StackFrameArrayExpressionNode stackFrameArrayExpressionNode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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 Microsoft.CodeAnalysis.EmbeddedLanguages.Common;

namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame
{
internal class StackFrameCompilationUnit
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
{
public readonly EmbeddedSyntaxTrivia<StackFrameKind>? AtTrivia;
public readonly StackFrameMethodDeclarationNode MethodDeclaration;
public readonly EmbeddedSyntaxTrivia<StackFrameKind>? InTrivia;
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
public readonly StackFrameFileInformationNode? FileInformationExpression;
public readonly EmbeddedSyntaxTrivia<StackFrameKind>? TrailingTrivia;
ryzngard marked this conversation as resolved.
Show resolved Hide resolved

public StackFrameCompilationUnit(EmbeddedSyntaxTrivia<StackFrameKind>? atTrivia, StackFrameMethodDeclarationNode methodDeclaration, EmbeddedSyntaxTrivia<StackFrameKind>? inTrivia, StackFrameFileInformationNode? fileInformationExpression, EmbeddedSyntaxTrivia<StackFrameKind>? trailingTrivia)
{
AtTrivia = atTrivia;
MethodDeclaration = methodDeclaration;
InTrivia = inTrivia;
FileInformationExpression = fileInformationExpression;
TrailingTrivia = trailingTrivia;
}
}
}
Loading