diff --git a/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.Utilities.cs b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.Utilities.cs new file mode 100644 index 0000000000000..c29d0622b5c08 --- /dev/null +++ b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.Utilities.cs @@ -0,0 +1,355 @@ +// 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.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis.EmbeddedLanguages.Common; +using Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame; +using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; +using Roslyn.Test.Utilities; +using Xunit; +using static Microsoft.CodeAnalysis.Editor.UnitTests.EmbeddedLanguages.StackFrame.StackFrameSyntaxFactory; + +namespace Microsoft.CodeAnalysis.Editor.UnitTests.EmbeddedLanguages.StackFrame +{ + using StackFrameNodeOrToken = EmbeddedSyntaxNodeOrToken; + using StackFrameToken = EmbeddedSyntaxToken; + using StackFrameTrivia = EmbeddedSyntaxTrivia; + + public partial class StackFrameParserTests + { + private static void Verify( + string input, + StackFrameMethodDeclarationNode? methodDeclaration = null, + bool expectFailure = false, + StackFrameFileInformationNode? fileInformation = null, + StackFrameToken? eolTokenOpt = null) + { + FuzzyTest(input); + + var tree = StackFrameParser.TryParse(input); + if (expectFailure) + { + Assert.Null(tree); + return; + } + + AssertEx.NotNull(tree); + VerifyCharacterSpans(input, tree); + + if (methodDeclaration is null) + { + Assert.Null(tree.Root.MethodDeclaration); + } + else + { + AssertEqual(methodDeclaration, tree.Root.MethodDeclaration); + } + + if (fileInformation is null) + { + Assert.Null(tree.Root.FileInformationExpression); + } + else + { + AssertEqual(fileInformation, tree.Root.FileInformationExpression); + } + + var eolToken = eolTokenOpt.HasValue + ? eolTokenOpt.Value + : CreateToken(StackFrameKind.EndOfFrame, ""); + + AssertEqual(eolToken, tree.Root.EndOfLineToken); + } + + /// + /// Tests that with a given input, no crashes are found + /// with multiple substrings of the input + /// + private static void FuzzyTest(string input) + { + for (var i = 0; i < input.Length - 1; i++) + { + StackFrameParser.TryParse(input[i..]); + StackFrameParser.TryParse(input[..^i]); + + for (var j = 0; j + i < input.Length; j++) + { + var start = input[..j]; + var end = input[(j + i)..]; + StackFrameParser.TryParse(start + end); + } + } + } + + private static void AssertEqual(StackFrameNodeOrToken expected, StackFrameNodeOrToken actual) + { + Assert.Equal(expected.IsNode, actual.IsNode); + if (expected.IsNode) + { + AssertEqual(expected.Node, actual.Node); + } + else + { + 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 triviaArray, StringBuilder sb) + { + if (triviaArray.IsDefault) + { + sb.Append(""); + return; + } + + if (triviaArray.IsEmpty) + { + sb.Append(""); + return; + } + + foreach (var trivia in triviaArray) + { + sb.Append(trivia.VirtualChars.CreateString()); + } + } + + private static void AssertEqual(StackFrameToken expected, StackFrameToken actual) + { + Assert.Equal(expected.Kind, actual.Kind); + Assert.Equal(expected.IsMissing, actual.IsMissing); + Assert.Equal(expected.VirtualChars.CreateString(), actual.VirtualChars.CreateString()); + + AssertEqual(expected.LeadingTrivia, actual.LeadingTrivia, expected); + AssertEqual(expected.TrailingTrivia, actual.TrailingTrivia, expected); + } + + private static void VerifyCharacterSpans(string originalText, StackFrameTree tree) + { + var textSeq = VirtualCharSequence.Create(0, originalText); + var index = 0; + List enumeratedParsedCharacters = new(); + + foreach (var charSeq in Enumerate(tree.Root)) + { + foreach (var ch in charSeq) + { + enumeratedParsedCharacters.Add(ch); + + if (textSeq[index++] != ch) + { + Assert.True(false, PrintDifference()); + } + } + } + + // Make sure we enumerated the total input + Assert.Equal(textSeq.Length, index); + + string PrintDifference() + { + var sb = new StringBuilder(); + + var start = Math.Max(0, index - 10); + var end = Math.Min(index, originalText.Length - 1); + + sb.Append("Expected: \t"); + PrintString(originalText, start, end, sb); + sb.AppendLine(); + + sb.Append("Actual: \t"); + var enumeratedString = new string(enumeratedParsedCharacters.Select(ch => (char)ch.Value).ToArray()); + PrintString(enumeratedString, start, end, sb); + sb.AppendLine(); + + return sb.ToString(); + + static void PrintString(string s, int start, int end, StringBuilder sb) + { + if (start > 0) + { + sb.Append("..."); + } + + sb.Append(s[start..end]); + + if (end < s.Length - 1) + { + sb.Append("..."); + } + } + } + } + + private static IEnumerable Enumerate(StackFrameNode node) + { + foreach (var nodeOrToken in node) + { + if (nodeOrToken.IsNode) + { + foreach (var charSequence in Enumerate(nodeOrToken.Node)) + { + yield return charSequence; + } + } + else if (nodeOrToken.Token.Kind != StackFrameKind.None) + { + foreach (var charSequence in Enumerate(nodeOrToken.Token)) + { + yield return charSequence; + } + } + else + { + // If we encounter a None token make sure it has default values + Assert.True(nodeOrToken.Token.IsMissing); + Assert.True(nodeOrToken.Token.LeadingTrivia.IsDefault); + Assert.True(nodeOrToken.Token.TrailingTrivia.IsDefault); + Assert.Null(nodeOrToken.Token.Value); + Assert.True(nodeOrToken.Token.VirtualChars.IsDefault); + } + } + } + + private static IEnumerable Enumerate(StackFrameToken token) + { + foreach (var trivia in token.LeadingTrivia) + { + yield return trivia.VirtualChars; + } + + yield return token.VirtualChars; + + foreach (var trivia in token.TrailingTrivia) + { + yield return trivia.VirtualChars; + } + } + + private static void AssertEqual(ImmutableArray expected, ImmutableArray actual, StackFrameToken token) + { + 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.AppendLine($"Trivia is different on {token.Kind}"); + 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()); + } + } +} diff --git a/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.cs b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.cs new file mode 100644 index 0000000000000..666f56a11c4b0 --- /dev/null +++ b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.cs @@ -0,0 +1,443 @@ +// 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 Xunit; +using System.Collections.Immutable; +using static Microsoft.CodeAnalysis.Editor.UnitTests.EmbeddedLanguages.StackFrame.StackFrameSyntaxFactory; +using static Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame.StackFrameExtensions; + +namespace Microsoft.CodeAnalysis.Editor.UnitTests.EmbeddedLanguages.StackFrame +{ + public partial class StackFrameParserTests + { + [Fact] + public void TestNoParams() + => Verify( + @"at ConsoleApp4.MyClass.M()", + methodDeclaration: MethodDeclaration( + QualifiedName("ConsoleApp4.MyClass.M", leadingTrivia: AtTrivia), + argumentList: EmptyParams) + ); + + [Theory] + [InlineData("C", 1)] + [InlineData("C", 100)] + [InlineData("a‿", 5)] // Unicode character with connection + [InlineData("abcdefg", 99999)] + public void TestArity(string typeName, int arity) + => Verify( + $"at ConsoleApp4.{typeName}`{arity}.M()", + methodDeclaration: MethodDeclaration( + QualifiedName( + QualifiedName( + Identifier("ConsoleApp4", leadingTrivia: AtTrivia), + GenericType(typeName, arity)), + Identifier("M")), + + argumentList: EmptyParams) + ); + + [Fact] + public void TestTrailingTrivia() + => Verify( + @"at ConsoleApp4.MyClass.M() some other text", + methodDeclaration: MethodDeclaration( + QualifiedName("ConsoleApp4.MyClass.M", leadingTrivia: AtTrivia), + argumentList: EmptyParams), + + eolTokenOpt: EOLToken.With(leadingTrivia: CreateTriviaArray(" some other text")) + ); + + [Fact] + public void TestTrailingTrivia_InTriviaNoSpace() + => Verify( + @"at ConsoleApp4.MyClass.M() inC:\My\Path\C.cs:line 26", + methodDeclaration: MethodDeclaration( + QualifiedName("ConsoleApp4.MyClass.M", leadingTrivia: AtTrivia), + argumentList: EmptyParams), + + eolTokenOpt: EOLToken.With(leadingTrivia: CreateTriviaArray(@" inC:\My\Path\C.cs:line 26")) + ); + + [Fact] + public void TestTrailingTrivia_InTriviaNoSpace2() + => Verify( + @"at ConsoleApp4.MyClass.M()in C:\My\Path\C.cs:line 26", + methodDeclaration: MethodDeclaration( + QualifiedName("ConsoleApp4.MyClass.M", leadingTrivia: AtTrivia), + argumentList: EmptyParams), + + eolTokenOpt: EOLToken.With(leadingTrivia: CreateTriviaArray(@"in C:\My\Path\C.cs:line 26")) + ); + + [Fact] + public void TestNoParams_NoAtTrivia() + => Verify( + @"ConsoleApp4.MyClass.M()", + methodDeclaration: MethodDeclaration( + QualifiedName("ConsoleApp4.MyClass.M"), + argumentList: EmptyParams) + ); + + [Fact] + public void TestNoParams_SpaceInParams_NoAtTrivia() + => Verify( + @"ConsoleApp4.MyClass.M( )", + methodDeclaration: MethodDeclaration( + QualifiedName("ConsoleApp4.MyClass.M"), + argumentList: ParameterList( + OpenParenToken.With(trailingTrivia: ImmutableArray.Create(SpaceTrivia(2))), + CloseParenToken)) + ); + + [Fact] + public void TestNoParams_SpaceTrivia() + => Verify( + @" ConsoleApp4.MyClass.M()", + methodDeclaration: MethodDeclaration( + QualifiedName("ConsoleApp4.MyClass.M", leadingTrivia: SpaceTrivia()), + argumentList: EmptyParams) + ); + + [Fact] + public void TestNoParams_SpaceTrivia2() + => Verify( + @" ConsoleApp4.MyClass.M()", + methodDeclaration: MethodDeclaration( + QualifiedName("ConsoleApp4.MyClass.M", leadingTrivia: SpaceTrivia(2)), + argumentList: EmptyParams) + ); + + [Fact] + public void TestMethodOneParam() + => Verify( + @"at ConsoleApp4.MyClass.M(string s)", + methodDeclaration: MethodDeclaration( + QualifiedName( + QualifiedName( + Identifier("ConsoleApp4", leadingTrivia: AtTrivia), + Identifier("MyClass")), + Identifier("M")), + + argumentList: ParameterList( + Parameter( + Identifier("string"), + IdentifierToken("s", leadingTrivia: SpaceTrivia()))) + ) + ); + + [Fact] + public void TestMethodOneParamSpacing() + => Verify( + @"at ConsoleApp4.MyClass.M( string s )", + methodDeclaration: MethodDeclaration( + QualifiedName( + QualifiedName( + Identifier("ConsoleApp4", leadingTrivia: AtTrivia), + Identifier("MyClass")), + Identifier("M")), + + argumentList: ParameterList( + OpenParenToken.With(trailingTrivia: SpaceTrivia().ToImmutableArray()), + CloseParenToken, + Parameter( + Identifier("string"), + IdentifierToken("s", leadingTrivia: SpaceTrivia(), trailingTrivia: SpaceTrivia()))) + ) + ); + + [Fact] + public void TestMethodTwoParam() + => Verify( + @"at ConsoleApp4.MyClass.M(string s, string t)", + methodDeclaration: MethodDeclaration( + QualifiedName( + QualifiedName( + Identifier("ConsoleApp4", leadingTrivia: AtTrivia), + Identifier("MyClass")), + Identifier("M")), + + argumentList: ParameterList( + Parameter( + Identifier("string"), + IdentifierToken("s", leadingTrivia: SpaceTrivia())), + Parameter( + Identifier("string", leadingTrivia: SpaceTrivia()), + IdentifierToken("t", leadingTrivia: SpaceTrivia()))) + ) + ); + + [Fact] + public void TestMethodArrayParam() + => Verify( + @"at ConsoleApp4.MyClass.M(string[] s)", + methodDeclaration: MethodDeclaration( + QualifiedName( + QualifiedName( + Identifier("ConsoleApp4", leadingTrivia: AtTrivia), + Identifier("MyClass")), + Identifier("M")), + + argumentList: ParameterList( + Parameter(ArrayType(Identifier("string"), ArrayRankSpecifier(trailingTrivia: SpaceTrivia())), + IdentifierToken("s"))) + ) + ); + + [Fact] + public void TestMethodArrayParamWithSpace() + => Verify( + "M.N(string[ , , ] s)", + methodDeclaration: MethodDeclaration( + QualifiedName("M.N"), + argumentList: ParameterList( + Parameter( + ArrayType(Identifier("string"), + ArrayRankSpecifier( + OpenBracketToken.With(trailingTrivia: SpaceTrivia().ToImmutableArray()), + CloseBracketToken.With(trailingTrivia: SpaceTrivia().ToImmutableArray()), + CommaToken.With(trailingTrivia: SpaceTrivia().ToImmutableArray()), + CommaToken.With(trailingTrivia: SpaceTrivia().ToImmutableArray()))), + IdentifierToken("s") + ) + )) + ); + + [Fact] + public void TestCommaArrayParam() + => Verify( + @"at ConsoleApp4.MyClass.M(string[,] s)", + methodDeclaration: MethodDeclaration( + QualifiedName( + QualifiedName( + Identifier("ConsoleApp4", leadingTrivia: AtTrivia), + Identifier("MyClass")), + Identifier("M")), + + argumentList: ParameterList( + Parameter( + ArrayType(Identifier("string"), ArrayRankSpecifier(1, trailingTrivia: SpaceTrivia())), + IdentifierToken("s"))) + ) + ); + + [Fact] + public void TestInvalidParameterIdentifier_MemberAccess() + => Verify("at ConsoleApp4.MyClass(string my.string.name)", expectFailure: true); + + [Fact] + public void TestInvalidParameterIdentifier_TypeArity() + => Verify("at ConsoleApp4.MyClass(string s`1)", expectFailure: true); + + [Fact] + public void TestGenericMethod_Brackets() + => Verify( + @"at ConsoleApp4.MyClass.M[T](T t)", + methodDeclaration: MethodDeclaration( + QualifiedName( + QualifiedName( + Identifier("ConsoleApp4", leadingTrivia: AtTrivia), + Identifier("MyClass")), + Identifier("M")), + typeArguments: TypeArgumentList(TypeArgument("T")), + argumentList: ParameterList( + Parameter( + Identifier("T"), + IdentifierToken("t", leadingTrivia: SpaceTrivia()))) + ) + ); + + [Fact] + public void TestGenericMethod() + => Verify( + @"at ConsoleApp4.MyClass.M(T t)", + methodDeclaration: MethodDeclaration( + QualifiedName( + QualifiedName( + Identifier("ConsoleApp4", leadingTrivia: AtTrivia), + Identifier("MyClass")), + Identifier("M")), + typeArguments: TypeArgumentList(useBrackets: false, TypeArgument("T")), + argumentList: ParameterList( + Parameter( + Identifier("T"), + IdentifierToken("t", leadingTrivia: SpaceTrivia()))) + ) + ); + + [Theory] + [InlineData("_")] + [InlineData("_s")] + [InlineData("S0m3th1ng")] + [InlineData("ü")] // Unicode character + [InlineData("uʶ")] // character and modifier character + [InlineData("a\u00AD")] // Soft hyphen formatting character + [InlineData("a‿")] // Connecting punctuation (combining character) + [InlineData("at")] + [InlineData("line")] + [InlineData("in")] + public void TestIdentifierNames(string identifierName) + => Verify( + @$"at {identifierName}.{identifierName}[{identifierName}]({identifierName} {identifierName})", + methodDeclaration: MethodDeclaration( + QualifiedName($"{identifierName}.{identifierName}", leadingTrivia: AtTrivia), + typeArguments: TypeArgumentList(TypeArgument(identifierName)), + argumentList: ParameterList( + Parameter( + Identifier(identifierName), + IdentifierToken(identifierName, leadingTrivia: SpaceTrivia()))) + ) + ); + + [Fact] + public void TestInvalidSpacingBeforeQualifiedName() + => Verify( + @"at MyNamespace. MyClass.MyMethod()", expectFailure: true); + + [Fact] + public void TestInvalidSpacingAfterQualifiedName2() + => Verify( + @"at MyNamespace.MyClass .MyMethod()", expectFailure: true); + + [Fact] + public void TestWhitespaceAroundBrackets() + => Verify( + @"at MyNamespace.MyClass.MyMethod[ T ]()", + methodDeclaration: MethodDeclaration( + QualifiedName("MyNamespace.MyClass.MyMethod", leadingTrivia: AtTrivia), + typeArguments: TypeArgumentList( + TypeArgument(IdentifierToken("T", leadingTrivia: SpaceTrivia(), trailingTrivia: SpaceTrivia())))) + ); + + [Fact] + public void TestAnonymousMethod() + => Verify( + @"Microsoft.VisualStudio.DesignTools.SurfaceDesigner.Tools.EventRouter.ScopeElement_MouseUp.AnonymousMethod__0()", + methodDeclaration: MethodDeclaration( + QualifiedName("Microsoft.VisualStudio.DesignTools.SurfaceDesigner.Tools.EventRouter.ScopeElement_MouseUp.AnonymousMethod__0"), + argumentList: EmptyParams) + ); + + [Fact] + public void TestFileInformation() + => Verify( + @"M.M() in C:\folder\m.cs:line 1", + methodDeclaration: MethodDeclaration( + QualifiedName("M.M"), + argumentList: EmptyParams), + + fileInformation: FileInformation( + Path(@"C:\folder\m.cs"), + ColonToken, + Line(1)) + ); + + [Fact] + public void TestFileInformation_PartialPath() + => Verify(@"M.M() in C:\folder\m.cs:line", expectFailure: true); + + [Fact] + public void TestFileInformation_PartialPath2() + => Verify(@"M.M() in C:\folder\m.cs:", expectFailure: true); + + [Fact] + public void TestFileInformation_PartialPath3() + => Verify(@"M.M() in C:\folder\m.cs:[trailingtrivia]", expectFailure: true); + + [Theory] + [InlineData(@"C:\folder\m.cs", 1)] + [InlineData(@"m.cs", 1)] + [InlineData(@"C:\folder\m.cs", 123456789)] + [InlineData(@"..\m.cs", 1)] + [InlineData(@".\m.cs", 1)] + public void TestFilePaths(string path, int line) + => Verify( + $"M.M() in {path}:line {line}", + methodDeclaration: MethodDeclaration( + QualifiedName("M.M"), + argumentList: EmptyParams), + + fileInformation: FileInformation( + Path(path), + ColonToken, + Line(line)) + ); + + [Fact] + public void TestFileInformation_TrailingTrivia() + => Verify( + @"M.M() in C:\folder\m.cs:line 1[trailingtrivia]", + methodDeclaration: MethodDeclaration( + QualifiedName("M.M"), + argumentList: EmptyParams), + + fileInformation: FileInformation( + Path(@"C:\folder\m.cs"), + ColonToken, + Line(1).With(trailingTrivia: CreateTriviaArray("[trailingtrivia]"))), + + eolTokenOpt: EOLToken + ); + + [Fact] + public void TestFileInformation_InvalidDirectory() + => Verify(@"M.M() in <\m.cs", expectFailure: true); + + [Theory] + [InlineData("")] + [InlineData("lkasjdlkfjalskdfj")] + [InlineData("\n")] + [InlineData("at ")] + [InlineData(@"at M()")] // Method with no class is invalid + [InlineData(@"at M.1c()")] // Invalid start character for identifier + [InlineData(@"at 1M.C()")] + [InlineData(@"at M.C(string& s)")] // "string&" represents a reference (ref, out) and is not supported yet + [InlineData(@"at StreamJsonRpc.JsonRpc.d__139`1.MoveNext()")] // Generated/Inline methods are not supported yet + [InlineData(@"at M(")] // Missing closing paren + [InlineData(@"at M)")] // MIssing open paren + [InlineData(@"at M.M[T>(T t)")] // Mismatched generic opening/close + [InlineData(@"at M.M>(T t)")] // Invalid nested generics + [InlineData("at M.M(T t)")] // Invalid generic in parameter + [InlineData(@"at M.M(string[ s)")] // Opening array bracket no close + [InlineData(@"at M.M(string] s)")] // Close only array bracket + [InlineData(@"at M.M(string[][][ s)")] + [InlineData(@"at M.M(string[[]] s)")] + [InlineData("at M.M(string s, string t,")] // Trailing comma in parameters + [InlineData(@"at M.N`.P()")] // Missing numeric for arity + [InlineData(@"at M.N`9N.P()")] // Invalid character after arity + [InlineData("M.N.P.()")] // Trailing . with no identifier before arguments + [InlineData("M.N(X.Y. x)")] // Trailing . in argument type + [InlineData("M.N[T.Y]()")] // Generic type arguments should not be qualified types + [InlineData("M.N(X.Y x.y)")] // argument names should not be qualified + [InlineData("M.N(params)")] // argument with type but no name + [InlineData("M.N [T]()")] // Space between identifier and bracket + [InlineData("M.N(string [] s)")] // Space between type and array brackets + [InlineData("M.N ()")] // Space between method declaration and parameters + [InlineData("M.N .O.P(string s)")] // Space in type qualified name + [InlineData("\r\nM.N()")] + [InlineData("\nM.N()")] + [InlineData("\rM.N()")] + [InlineData("M.N(\r\n)")] + [InlineData("M.N(\r)")] + [InlineData("M.N(\n)")] + public void TestInvalidInputs(string input) + => Verify(input, expectFailure: true); + + [Theory] + [InlineData("at ")] + [InlineData("in ")] + [InlineData("line ")] + public void TestKeywordsAsIdentifiers(string keyword) + => Verify(@$"MyNamespace.MyType.MyMethod[{keyword}]({keyword} {keyword})", + methodDeclaration: MethodDeclaration( + QualifiedName("MyNamespace.MyType.MyMethod"), + typeArguments: TypeArgumentList(TypeArgument(IdentifierToken(keyword.Trim(), trailingTrivia: SpaceTrivia()))), + argumentList: ParameterList( + Parameter( + Identifier(keyword.Trim()), + IdentifierToken(keyword.Trim(), leadingTrivia: SpaceTrivia(2), trailingTrivia: SpaceTrivia())))) + ); + } +} diff --git a/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameSyntaxFactory.cs b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameSyntaxFactory.cs new file mode 100644 index 0000000000000..8830e4ae412b4 --- /dev/null +++ b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameSyntaxFactory.cs @@ -0,0 +1,203 @@ +// 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.Immutable; +using System.Linq; +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; + using StackFrameTrivia = EmbeddedSyntaxTrivia; + using StackFrameNodeOrToken = EmbeddedSyntaxNodeOrToken; + + internal static class StackFrameSyntaxFactory + { + public static StackFrameToken CreateToken(StackFrameKind kind, string s, ImmutableArray leadingTrivia = default, ImmutableArray trailingTrivia = default) + => new( + kind, + leadingTrivia.IsDefaultOrEmpty ? ImmutableArray.Empty : leadingTrivia, + CodeAnalysis.EmbeddedLanguages.VirtualChars.VirtualCharSequence.Create(0, s), + trailingTrivia.IsDefaultOrEmpty ? ImmutableArray.Empty : trailingTrivia, + ImmutableArray.Empty, + value: null!); + + public static StackFrameTrivia CreateTrivia(StackFrameKind kind, string text) + => new(kind, CodeAnalysis.EmbeddedLanguages.VirtualChars.VirtualCharSequence.Create(0, text), ImmutableArray.Empty); + + public static ImmutableArray CreateTriviaArray(params string[] strings) + => strings.Select(s => CreateTrivia(StackFrameKind.SkippedTextTrivia, s)).ToImmutableArray(); + + public static readonly StackFrameToken DotToken = CreateToken(StackFrameKind.DotToken, "."); + public static readonly StackFrameToken CommaToken = CreateToken(StackFrameKind.CommaToken, ","); + public static readonly StackFrameToken OpenParenToken = CreateToken(StackFrameKind.OpenParenToken, "("); + public static readonly StackFrameToken CloseParenToken = CreateToken(StackFrameKind.CloseParenToken, ")"); + public static readonly StackFrameToken OpenBracketToken = CreateToken(StackFrameKind.OpenBracketToken, "["); + public static readonly StackFrameToken CloseBracketToken = CreateToken(StackFrameKind.CloseBracketToken, "]"); + public static readonly StackFrameToken LessThanToken = CreateToken(StackFrameKind.LessThanToken, "<"); + public static readonly StackFrameToken GreaterThanToken = CreateToken(StackFrameKind.GreaterThanToken, ">"); + public static readonly StackFrameToken GraveAccentToken = CreateToken(StackFrameKind.GraveAccentToken, "`"); + public static readonly StackFrameToken EOLToken = CreateToken(StackFrameKind.EndOfFrame, ""); + public static readonly StackFrameToken ColonToken = CreateToken(StackFrameKind.ColonToken, ":"); + + public static readonly StackFrameTrivia AtTrivia = CreateTrivia(StackFrameKind.AtTrivia, "at "); + public static readonly StackFrameTrivia LineTrivia = CreateTrivia(StackFrameKind.LineTrivia, "line "); + public static readonly StackFrameTrivia InTrivia = CreateTrivia(StackFrameKind.InTrivia, " in "); + + public static readonly StackFrameParameterList EmptyParams = ParameterList(OpenParenToken, CloseParenToken); + + public static StackFrameParameterDeclarationNode Parameter(StackFrameTypeNode type, StackFrameToken identifier) + => new(type, identifier); + + public static StackFrameParameterList ParameterList(params StackFrameParameterDeclarationNode[] parameters) + => ParameterList(OpenParenToken, CloseParenToken, parameters); + + public static StackFrameParameterList ParameterList(StackFrameToken openToken, StackFrameToken closeToken, params StackFrameParameterDeclarationNode[] parameters) + { + var separatedList = parameters.Length == 0 + ? EmbeddedSeparatedSyntaxNodeList.Empty + : new(CommaSeparateList(parameters)); + + return new(openToken, separatedList, closeToken); + + static ImmutableArray CommaSeparateList(StackFrameParameterDeclarationNode[] parameters) + { + var builder = ImmutableArray.CreateBuilder(); + builder.Add(parameters[0]); + + for (var i = 1; i < parameters.Length; i++) + { + builder.Add(CommaToken); + builder.Add(parameters[i]); + } + + return builder.ToImmutable(); + } + } + + public static StackFrameMethodDeclarationNode MethodDeclaration( + StackFrameQualifiedNameNode memberAccessExpression, + StackFrameTypeArgumentList? typeArguments = null, + StackFrameParameterList? argumentList = null) + { + return new StackFrameMethodDeclarationNode(memberAccessExpression, typeArguments, argumentList ?? ParameterList(OpenParenToken, CloseParenToken)); + } + + public static StackFrameQualifiedNameNode QualifiedName(string s, StackFrameTrivia? leadingTrivia = null, StackFrameTrivia? trailingTrivia = null) + => QualifiedName(s, leadingTrivia.ToImmutableArray(), trailingTrivia.ToImmutableArray()); + + public static StackFrameQualifiedNameNode QualifiedName(string s, ImmutableArray leadingTrivia, ImmutableArray trailingTrivia) + { + StackFrameNameNode? current = null; + Assert.True(s.Contains('.')); + + var identifiers = s.Split('.'); + for (var i = 0; i < identifiers.Length; i++) + { + var identifier = identifiers[i]; + + if (current is null) + { + current = Identifier(IdentifierToken(identifier, leadingTrivia: leadingTrivia, trailingTrivia: ImmutableArray.Empty)); + } + else if (i == identifiers.Length - 1) + { + var rhs = Identifier(IdentifierToken(identifier, leadingTrivia: ImmutableArray.Empty, trailingTrivia: trailingTrivia)); + current = QualifiedName(current, rhs); + } + else + { + current = QualifiedName(current, Identifier(identifier)); + } + } + + AssertEx.NotNull(current); + return (StackFrameQualifiedNameNode)current; + } + + public static StackFrameTrivia SpaceTrivia(int count = 1) + => CreateTrivia(StackFrameKind.WhitespaceTrivia, new string(' ', count)); + + public static StackFrameQualifiedNameNode QualifiedName(StackFrameNameNode left, StackFrameSimpleNameNode right) + => new(left, DotToken, right); + + public static StackFrameToken IdentifierToken(string identifierName) + => IdentifierToken(identifierName, leadingTrivia: null, trailingTrivia: null); + + public static StackFrameToken IdentifierToken(string identifierName, StackFrameTrivia? leadingTrivia = null, StackFrameTrivia? trailingTrivia = null) + => IdentifierToken(identifierName, leadingTrivia.ToImmutableArray(), trailingTrivia.ToImmutableArray()); + + public static StackFrameToken IdentifierToken(string identifierName, ImmutableArray leadingTrivia, ImmutableArray trailingTrivia) + => CreateToken(StackFrameKind.IdentifierToken, identifierName, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia); + + public static StackFrameIdentifierNameNode Identifier(string name) + => Identifier(IdentifierToken(name)); + + public static StackFrameIdentifierNameNode Identifier(StackFrameToken identifier) + => new(identifier); + + public static StackFrameIdentifierNameNode Identifier(string name, StackFrameTrivia? leadingTrivia = null, StackFrameTrivia? trailingTrivia = null) + => Identifier(IdentifierToken(name, leadingTrivia, trailingTrivia)); + + public static StackFrameArrayRankSpecifier ArrayRankSpecifier(int commaCount = 0, StackFrameTrivia? leadingTrivia = null, StackFrameTrivia? trailingTrivia = null) + => new(OpenBracketToken.With(leadingTrivia: leadingTrivia.ToImmutableArray()), CloseBracketToken.With(trailingTrivia: trailingTrivia.ToImmutableArray()), Enumerable.Repeat(CommaToken, commaCount).ToImmutableArray()); + + public static StackFrameArrayRankSpecifier ArrayRankSpecifier(StackFrameToken openToken, StackFrameToken closeToken, params StackFrameToken[] commaTokens) + => new(openToken, closeToken, commaTokens.ToImmutableArray()); + + public static StackFrameArrayTypeNode ArrayType(StackFrameNameNode identifier, params StackFrameArrayRankSpecifier[] arrayTokens) + => new(identifier, arrayTokens.ToImmutableArray()); + + public static StackFrameGenericNameNode GenericType(string identifierName, int arity) + => new(CreateToken(StackFrameKind.IdentifierToken, identifierName), GraveAccentToken, CreateToken(StackFrameKind.NumberToken, arity.ToString())); + + public static StackFrameTypeArgumentList TypeArgumentList(params StackFrameIdentifierNameNode[] typeArguments) + => TypeArgumentList(useBrackets: true, typeArguments); + + public static StackFrameTypeArgumentList TypeArgumentList(bool useBrackets, params StackFrameIdentifierNameNode[] typeArguments) + { + using var _ = PooledObjects.ArrayBuilder.GetInstance(out var builder); + var openToken = useBrackets ? OpenBracketToken : LessThanToken; + var closeToken = useBrackets ? CloseBracketToken : GreaterThanToken; + + var isFirst = true; + foreach (var typeArgument in typeArguments) + { + if (isFirst) + { + isFirst = false; + } + else + { + builder.Add(CommaToken); + } + + builder.Add(typeArgument); + } + + var typeArgumentsList = new EmbeddedSeparatedSyntaxNodeList(builder.ToImmutable()); + + return new(openToken, typeArgumentsList, closeToken); + } + + public static StackFrameIdentifierNameNode TypeArgument(string identifier) + => new(CreateToken(StackFrameKind.IdentifierToken, identifier)); + + public static StackFrameIdentifierNameNode TypeArgument(StackFrameToken identifier) + => new(identifier); + + public static StackFrameFileInformationNode FileInformation(StackFrameToken path, StackFrameToken colon, StackFrameToken line) + => new(path.With(leadingTrivia: ImmutableArray.Create(InTrivia)), colon, line); + + public static StackFrameToken Path(string path) + => CreateToken(StackFrameKind.PathToken, path); + + public static StackFrameToken Line(int lineNumber) + => CreateToken(StackFrameKind.NumberToken, lineNumber.ToString(), leadingTrivia: ImmutableArray.Create(LineTrivia)); + } +} diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/IStackFrameNodeVisitor.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/IStackFrameNodeVisitor.cs new file mode 100644 index 0000000000000..fa948cc6e9bae --- /dev/null +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/IStackFrameNodeVisitor.cs @@ -0,0 +1,21 @@ +// 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. + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame +{ + internal interface IStackFrameNodeVisitor + { + void Visit(StackFrameCompilationUnit node); + void Visit(StackFrameMethodDeclarationNode node); + void Visit(StackFrameQualifiedNameNode node); + void Visit(StackFrameTypeArgumentList node); + void Visit(StackFrameParameterList node); + void Visit(StackFrameGenericNameNode node); + void Visit(StackFrameIdentifierNameNode node); + void Visit(StackFrameArrayRankSpecifier node); + void Visit(StackFrameFileInformationNode node); + void Visit(StackFrameArrayTypeNode node); + void Visit(StackFrameParameterDeclarationNode node); + } +} diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/Result.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/Result.cs new file mode 100644 index 0000000000000..253c454ce6459 --- /dev/null +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/Result.cs @@ -0,0 +1,33 @@ +// 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. + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame +{ + internal readonly struct Result + { + public readonly bool Success; + public readonly T? Value; + + public static readonly Result Abort = new(false, default); + public static readonly Result Empty = new(true, default); + + public Result(T? value) + : this(true, value) + { } + + private Result(bool success, T? value) + { + Success = success; + Value = value; + } + + public void Deconstruct(out bool success, out T? value) + { + success = Success; + value = Value; + } + + public static implicit operator Result(T value) => new(value); + } +} diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameCompilationUnit.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameCompilationUnit.cs new file mode 100644 index 0000000000000..ee4b529cb31e7 --- /dev/null +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameCompilationUnit.cs @@ -0,0 +1,61 @@ +// 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 Microsoft.CodeAnalysis.EmbeddedLanguages.Common; + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame +{ + using StackFrameNodeOrToken = EmbeddedSyntaxNodeOrToken; + using StackFrameToken = EmbeddedSyntaxToken; + using StackFrameTrivia = EmbeddedSyntaxTrivia; + + /// + /// The root unit for a stackframe. Includes the method declaration for the stack frame and optional file information. + /// Any leading "at " is considered trivia of , and " in " is put as trivia for the . + /// Remaining unparsable text is put as leading trivia on the + /// + internal class StackFrameCompilationUnit : StackFrameNode + { + /// + /// Represents the method declaration for a stack frame. Requires at least a member + /// access and argument list with no parameters to be considered valid + /// + public readonly StackFrameMethodDeclarationNode MethodDeclaration; + + /// + /// File information for a stack frame. May be optionally contained. If available, represents + /// the file path of a stackframe and optionally the line number. This is available as hint information + /// and may be useful for a user, but is not always accurate when mapping back to source. + /// + public readonly StackFrameFileInformationNode? FileInformationExpression; + + /// + /// The end token of a frame. Any trailing text is added as leading trivia of this token. + /// + public readonly StackFrameToken EndOfLineToken; + + public StackFrameCompilationUnit(StackFrameMethodDeclarationNode methodDeclaration, StackFrameFileInformationNode? fileInformationExpression, StackFrameToken endOfLineToken) + : base(StackFrameKind.CompilationUnit) + { + MethodDeclaration = methodDeclaration; + FileInformationExpression = fileInformationExpression; + EndOfLineToken = endOfLineToken; + } + + internal override int ChildCount => 3; + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + => index switch + { + 0 => MethodDeclaration, + 1 => FileInformationExpression, + 2 => EndOfLineToken, + _ => throw new InvalidOperationException() + }; + } +} diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameExtensions.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameExtensions.cs new file mode 100644 index 0000000000000..31bbc0cfc956b --- /dev/null +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameExtensions.cs @@ -0,0 +1,31 @@ +// 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 Microsoft.CodeAnalysis.EmbeddedLanguages.Common; + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame +{ + using StackFrameTrivia = EmbeddedSyntaxTrivia; + + internal static class StackFrameExtensions + { + /// + /// Creates an with a single value or empty + /// if the has no value + /// + public static ImmutableArray ToImmutableArray(this StackFrameTrivia? trivia) + => trivia.HasValue ? ImmutableArray.Create(trivia.Value) : ImmutableArray.Empty; + + /// + /// Creates an with a single trivia item in it + /// + /// + /// This is created for convenience so callers don't have to have different patterns between nullable and + /// non nullable calues + /// + public static ImmutableArray ToImmutableArray(this StackFrameTrivia trivia) + => ImmutableArray.Create(trivia); + } +} diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameKind.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameKind.cs new file mode 100644 index 0000000000000..262a4425e9962 --- /dev/null +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameKind.cs @@ -0,0 +1,54 @@ +// 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. + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame +{ + internal enum StackFrameKind + { + None = 0, + + // Nodes + CompilationUnit, + MethodDeclaration, + MemberAccess, + ArrayTypeExpression, + GenericTypeIdentifier, + TypeArgument, + TypeIdentifier, + Parameter, + ParameterList, + ArrayExpression, + FileInformation, + + // Tokens + EndOfFrame, + AmpersandToken, + OpenBracketToken, + CloseBracketToken, + OpenParenToken, + CloseParenToken, + DotToken, + PlusToken, + CommaToken, + ColonToken, + EqualsToken, + GreaterThanToken, + LessThanToken, + MinusToken, + SingleQuoteToken, + GraveAccentToken, // ` + BackslashToken, + ForwardSlashToken, + IdentifierToken, + PathToken, + NumberToken, + + // Trivia + WhitespaceTrivia, + AtTrivia, // "at " portion of the stack frame + InTrivia, // optional " in " portion of the stack frame + LineTrivia, // optional "line " string indicating the line number of a file + SkippedTextTrivia, // any skipped text that isn't a node, token, or special kind of trivia already presented + } +} diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameLexer.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameLexer.cs new file mode 100644 index 0000000000000..5d329d25cff24 --- /dev/null +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameLexer.cs @@ -0,0 +1,419 @@ +// 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.Immutable; +using System.Diagnostics; +using System.IO; +using Microsoft.CodeAnalysis.EmbeddedLanguages.Common; +using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame +{ + using StackFrameToken = EmbeddedSyntaxToken; + using StackFrameTrivia = EmbeddedSyntaxTrivia; + + internal struct StackFrameLexer + { + public readonly VirtualCharSequence Text; + public int Position { get; private set; } + + private StackFrameLexer(string text) + : this(VirtualCharSequence.Create(0, text)) + { + } + + private StackFrameLexer(VirtualCharSequence text) : this() + => Text = text; + + public static StackFrameLexer? TryCreate(string text) + { + foreach (var c in text) + { + if (c == '\r' || c == '\n') + { + return null; + } + } + + return new(text); + } + + public static StackFrameLexer? TryCreate(VirtualCharSequence text) + { + foreach (var c in text) + { + if (c.Value == '\r' || c.Value == '\n') + { + return null; + } + } + + return new(text); + } + + public VirtualChar CurrentChar => Position < Text.Length ? Text[Position] : default; + + public VirtualCharSequence GetSubSequenceToCurrentPos(int start) + => GetSubSequence(start, Position); + + public VirtualCharSequence GetSubSequence(int start, int end) + => Text.GetSubSequence(TextSpan.FromBounds(start, end)); + + public StackFrameTrivia? TryScanRemainingTrivia() + { + if (Position == Text.Length) + { + return null; + } + + var start = Position; + Position = Text.Length; + + return CreateTrivia(StackFrameKind.SkippedTextTrivia, GetSubSequenceToCurrentPos(start)); + } + + public StackFrameToken? TryScanIdentifier() + => TryScanIdentifier(scanAtTrivia: false, scanLeadingWhitespace: false, scanTrailingWhitespace: false); + + public StackFrameToken? TryScanIdentifier(bool scanAtTrivia, bool scanLeadingWhitespace, bool scanTrailingWhitespace) + { + var originalPosition = Position; + var atTrivia = scanAtTrivia ? TryScanAtTrivia() : null; + var leadingWhitespace = scanLeadingWhitespace ? TryScanWhiteSpace() : null; + + var startPosition = Position; + var ch = CurrentChar; + if (!UnicodeCharacterUtilities.IsIdentifierStartCharacter((char)ch.Value)) + { + // If we scan only trivia but don't get an identifier, we want to make sure + // to reset back to this original position to let the trivia be consumed + // in some other fashion if necessary + Position = originalPosition; + return null; + } + + while (UnicodeCharacterUtilities.IsIdentifierPartCharacter((char)ch.Value)) + { + Position++; + ch = CurrentChar; + } + + var identifierSequence = GetSubSequenceToCurrentPos(startPosition); + var trailingWhitespace = scanTrailingWhitespace ? TryScanWhiteSpace() : null; + + return CreateToken( + StackFrameKind.IdentifierToken, + leadingTrivia: CreateTrivia(atTrivia, leadingWhitespace), + identifierSequence, + trailingTrivia: CreateTrivia(trailingWhitespace)); + } + + public StackFrameToken CurrentCharAsToken() + { + if (Position == Text.Length) + { + return CreateToken(StackFrameKind.EndOfFrame, VirtualCharSequence.Empty); + } + + var ch = Text[Position]; + return CreateToken(GetKind(ch), Text.GetSubSequence(new TextSpan(Position, 1))); + } + + /// + /// Progress the position by one if the current character + /// matches the kind. + /// + /// + /// if the position was incremented + /// + public bool ScanCurrentCharAsTokenIfMatch(StackFrameKind kind, out StackFrameToken token) + => ScanCurrentCharAsTokenIfMatch(kind, scanTrailingWhitespace: false, out token); + + /// + /// Progress the position by one if the current character + /// matches the kind. + /// + /// + /// if the position was incremented + /// + public bool ScanCurrentCharAsTokenIfMatch(StackFrameKind kind, bool scanTrailingWhitespace, out StackFrameToken token) + { + if (GetKind(CurrentChar) == kind) + { + token = CurrentCharAsToken(); + Position++; + + if (scanTrailingWhitespace) + { + token = token.With(trailingTrivia: CreateTrivia(TryScanWhiteSpace())); + } + + return true; + } + + token = default; + return false; + } + + /// + /// Progress the position by one if the current character + /// matches the kind. + /// + /// + /// if the position was incremented + /// + public bool ScanCurrentCharAsTokenIfMatch(Func isMatch, out StackFrameToken token) + { + if (isMatch(GetKind(CurrentChar))) + { + token = CurrentCharAsToken(); + Position++; + return true; + } + + token = default; + return false; + } + + public StackFrameTrivia? TryScanAtTrivia() + // TODO: Handle multiple languages? Right now we're going to only parse english + => TryScanStringTrivia("at ", StackFrameKind.AtTrivia); + + public StackFrameTrivia? TryScanInTrivia() + // TODO: Handle multiple languages? Right now we're going to only parse english + => TryScanStringTrivia(" in ", StackFrameKind.InTrivia); + + public StackFrameTrivia? TryScanLineTrivia() + // TODO: Handle multiple languages? Right now we're going to only parse english + => TryScanStringTrivia("line ", StackFrameKind.LineTrivia); + + /// + /// Attempts to parse and a path following https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#file-and-directory-names + /// Uses as a tool to determine if the path is correct for returning. + /// + public Result TryScanPath() + { + var inTrivia = TryScanInTrivia(); + if (!inTrivia.HasValue) + { + return Result.Empty; + } + + var startPosition = Position; + + while (Position < Text.Length) + { + // Path needs to do a look ahead to determine if adding the next character + // invalidates the path. Break if it does. + // + // This helps to keep the complex rules for what FileInfo does to validate path by calling to it directly. + // We can't simply check all invalid characters for a path because location in the path is important, and we're not + // in the business of validating if something is correctly a file. It's cheap enough to let the FileInfo constructor do that. The downside + // is that we are constructing a new object with every pass. If this becomes problematic we can revisit and fine a more + // optimized pattern to handle all of the edge cases. + // + // Example: C:\my\path \:other + // ^-- the ":" breaks the path, but we can't simply break on all ":" (which is included in the invalid characters for Path) + // + + var str = GetSubSequence(startPosition, Position + 1).CreateString(); + + var isValidPath = IOUtilities.PerformIO(() => + { + var fileInfo = new FileInfo(str); + return true; + }, false); + + if (!isValidPath) + { + break; + } + + Position++; + } + + if (startPosition == Position) + { + return Result.Abort; + } + + return CreateToken(StackFrameKind.PathToken, inTrivia.ToImmutableArray(), GetSubSequenceToCurrentPos(startPosition)); + } + + /// + /// Returns a number token with the and remainging + /// attached to it. + /// + /// + public StackFrameToken? TryScanRequiredLineNumber() + { + var lineTrivia = TryScanLineTrivia(); + if (!lineTrivia.HasValue) + { + return null; + } + + var numberToken = TryScanNumbers(); + if (!numberToken.HasValue) + { + return null; + } + + var remainingTrivia = TryScanRemainingTrivia(); + + return numberToken.Value.With( + leadingTrivia: lineTrivia.ToImmutableArray(), + trailingTrivia: remainingTrivia.ToImmutableArray()); + } + + public StackFrameToken? TryScanNumbers() + { + var start = Position; + while (IsNumber(CurrentChar)) + { + Position++; + } + + if (start == Position) + { + return null; + } + + return CreateToken(StackFrameKind.NumberToken, GetSubSequenceToCurrentPos(start)); + } + + public static bool IsBlank(VirtualChar ch) + { + // List taken from the native regex parser. + switch (ch.Value) + { + case '\u0009': + case '\u000A': + case '\u000C': + case '\u000D': + case ' ': + return true; + default: + return false; + } + } + + public static StackFrameToken CreateToken(StackFrameKind kind, VirtualCharSequence virtualChars) + => CreateToken(kind, ImmutableArray.Empty, virtualChars); + + public static StackFrameToken CreateToken(StackFrameKind kind, ImmutableArray leadingTrivia, VirtualCharSequence virtualChars) + => new(kind, leadingTrivia, virtualChars, ImmutableArray.Empty, ImmutableArray.Empty, value: null!); + + public static StackFrameToken CreateToken(StackFrameKind kind, ImmutableArray leadingTrivia, VirtualCharSequence virtualChars, ImmutableArray trailingTrivia) + => new(kind, leadingTrivia, virtualChars, trailingTrivia, ImmutableArray.Empty, value: null!); + + private static StackFrameTrivia CreateTrivia(StackFrameKind kind, VirtualCharSequence virtualChars) + => CreateTrivia(kind, virtualChars, ImmutableArray.Empty); + + private static StackFrameTrivia CreateTrivia(StackFrameKind kind, VirtualCharSequence virtualChars, ImmutableArray diagnostics) + { + // Empty trivia is not supported in StackFrames + Debug.Assert(virtualChars.Length > 0); + return new(kind, virtualChars, diagnostics); + } + + private static ImmutableArray CreateTrivia(params StackFrameTrivia?[] triviaArray) + { + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var trivia in triviaArray) + { + if (trivia.HasValue) + { + builder.Add(trivia.Value); + } + } + + return builder.ToImmutable(); + } + + private bool IsStringAtPosition(string val) + => IsAtStartOfText(Position, val); + + private bool IsAtStartOfText(int position, string val) + { + for (var i = 0; i < val.Length; i++) + { + if (position + i >= Text.Length || + Text[position + i] != val[i]) + { + return false; + } + } + + return true; + } + + private StackFrameTrivia? TryScanStringTrivia(string valueToLookFor, StackFrameKind triviaKind) + { + if (IsStringAtPosition(valueToLookFor)) + { + var start = Position; + Position += valueToLookFor.Length; + + return CreateTrivia(triviaKind, GetSubSequenceToCurrentPos(start)); + } + + return null; + } + + private StackFrameTrivia? TryScanWhiteSpace() + { + var startPosition = Position; + + while (IsBlank(CurrentChar)) + { + Position++; + } + + if (Position == startPosition) + { + return null; + } + + return CreateTrivia(StackFrameKind.WhitespaceTrivia, GetSubSequenceToCurrentPos(startPosition)); + } + + private static StackFrameKind GetKind(VirtualChar ch) + => ch.Value switch + { + '\n' => throw new InvalidOperationException(), + '\r' => throw new InvalidOperationException(), + '&' => StackFrameKind.AmpersandToken, + '[' => StackFrameKind.OpenBracketToken, + ']' => StackFrameKind.CloseBracketToken, + '(' => StackFrameKind.OpenParenToken, + ')' => StackFrameKind.CloseParenToken, + '.' => StackFrameKind.DotToken, + '+' => StackFrameKind.PlusToken, + ',' => StackFrameKind.CommaToken, + ':' => StackFrameKind.ColonToken, + '=' => StackFrameKind.EqualsToken, + '>' => StackFrameKind.GreaterThanToken, + '<' => StackFrameKind.LessThanToken, + '-' => StackFrameKind.MinusToken, + '\'' => StackFrameKind.SingleQuoteToken, + '`' => StackFrameKind.GraveAccentToken, + '\\' => StackFrameKind.BackslashToken, + '/' => StackFrameKind.ForwardSlashToken, + _ => IsBlank(ch) + ? StackFrameKind.WhitespaceTrivia + : IsNumber(ch) + ? StackFrameKind.NumberToken + : StackFrameKind.SkippedTextTrivia + }; + + private static bool IsNumber(VirtualChar ch) + => ch.Value is >= '0' and <= '9'; + } +} diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs new file mode 100644 index 0000000000000..5c1c921b0cb1d --- /dev/null +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs @@ -0,0 +1,438 @@ +// 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.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis.EmbeddedLanguages.Common; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame +{ + using StackFrameNodeOrToken = EmbeddedSyntaxNodeOrToken; + using StackFrameToken = EmbeddedSyntaxToken; + + internal abstract class StackFrameNode : EmbeddedSyntaxNode + { + protected StackFrameNode(StackFrameKind kind) : base(kind) + { + } + + public abstract void Accept(IStackFrameNodeVisitor visitor); + } + + internal abstract class StackFrameDeclarationNode : StackFrameNode + { + protected StackFrameDeclarationNode(StackFrameKind kind) : base(kind) + { + } + } + + internal sealed class StackFrameMethodDeclarationNode : StackFrameDeclarationNode + { + public readonly StackFrameQualifiedNameNode MemberAccessExpression; + public readonly StackFrameTypeArgumentList? TypeArguments; + public readonly StackFrameParameterList ArgumentList; + + public StackFrameMethodDeclarationNode( + StackFrameQualifiedNameNode memberAccessExpression, + StackFrameTypeArgumentList? typeArguments, + StackFrameParameterList argumentList) + : base(StackFrameKind.MethodDeclaration) + { + MemberAccessExpression = memberAccessExpression; + TypeArguments = typeArguments; + ArgumentList = argumentList; + } + + internal override int ChildCount => 3; + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + => index switch + { + 0 => MemberAccessExpression, + 1 => TypeArguments, + 2 => ArgumentList, + _ => throw new InvalidOperationException(), + }; + } + + /// + /// Base class for all type nodes + /// + internal abstract class StackFrameTypeNode : StackFrameNode + { + protected StackFrameTypeNode(StackFrameKind kind) : base(kind) + { + } + } + + /// + /// Base class for all name nodes + /// + /// + /// All of these are . If a node requires an identifier or name that + /// is not a type then it should use with + /// directly. + /// + internal abstract class StackFrameNameNode : StackFrameTypeNode + { + protected StackFrameNameNode(StackFrameKind kind) : base(kind) + { + } + } + + /// + /// Base class for and + /// + internal abstract class StackFrameSimpleNameNode : StackFrameNameNode + { + public readonly StackFrameToken Identifier; + + protected StackFrameSimpleNameNode(StackFrameToken identifier, StackFrameKind kind) : base(kind) + { + Debug.Assert(identifier.Kind == StackFrameKind.IdentifierToken); + Identifier = identifier; + } + } + + /// + /// Represents a qualified name, such as "MyClass.MyMethod" + /// + internal sealed class StackFrameQualifiedNameNode : StackFrameNameNode + { + public readonly StackFrameNameNode Left; + public readonly StackFrameToken DotToken; + public readonly StackFrameSimpleNameNode Right; + + public StackFrameQualifiedNameNode(StackFrameNameNode left, StackFrameToken dotToken, StackFrameSimpleNameNode right) : base(StackFrameKind.MemberAccess) + { + Debug.Assert(dotToken.Kind == StackFrameKind.DotToken); + + Left = left; + DotToken = dotToken; + Right = right; + } + + internal override int ChildCount => 3; + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + => index switch + { + 0 => Left, + 1 => DotToken, + 2 => Right, + _ => throw new InvalidOperationException() + }; + } + + /// + /// The simplest identifier node, which wraps a + /// + internal sealed class StackFrameIdentifierNameNode : StackFrameSimpleNameNode + { + internal override int ChildCount => 1; + + public StackFrameIdentifierNameNode(StackFrameToken identifier) + : base(identifier, StackFrameKind.TypeIdentifier) + { + } + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + => index switch + { + 0 => Identifier, + _ => throw new InvalidOperationException() + }; + } + + /// + /// An identifier with an arity, such as "MyNamespace.MyClass`1" + /// + internal sealed class StackFrameGenericNameNode : StackFrameSimpleNameNode + { + /// + /// The "`" token in arity identifiers. Must be + /// + public readonly StackFrameToken GraveAccentToken; + + public readonly StackFrameToken NumberToken; + + internal override int ChildCount => 3; + + public StackFrameGenericNameNode(StackFrameToken identifier, StackFrameToken graveAccentToken, StackFrameToken numberToken) + : base(identifier, StackFrameKind.GenericTypeIdentifier) + { + Debug.Assert(graveAccentToken.Kind == StackFrameKind.GraveAccentToken); + Debug.Assert(numberToken.Kind == StackFrameKind.NumberToken); + + GraveAccentToken = graveAccentToken; + NumberToken = numberToken; + } + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + => index switch + { + 0 => Identifier, + 1 => GraveAccentToken, + 2 => NumberToken, + _ => throw new InvalidOperationException() + }; + } + + /// + /// Represents an array type declaration, such as string[,][] + /// + internal sealed class StackFrameArrayTypeNode : StackFrameTypeNode + { + /// + /// The type identifier without the array indicators. + /// string[][] + /// ^----^ + /// + public readonly StackFrameNameNode TypeIdentifier; + + /// + /// Each unique array identifier for the type + /// string[,][] + /// ^--- First array expression = "[,]" + /// ^- Second array expression = "[]" + /// + public ImmutableArray ArrayExpressions; + + public StackFrameArrayTypeNode(StackFrameNameNode typeIdentifier, ImmutableArray arrayExpressions) : base(StackFrameKind.ArrayTypeExpression) + { + Debug.Assert(!arrayExpressions.IsDefaultOrEmpty); + TypeIdentifier = typeIdentifier; + ArrayExpressions = arrayExpressions; + } + + internal override int ChildCount => 1 + ArrayExpressions.Length; + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + => index switch + { + 0 => TypeIdentifier, + _ => ArrayExpressions[index - 1] + }; + } + + internal sealed class StackFrameArrayRankSpecifier : StackFrameNode + { + public readonly StackFrameToken OpenBracket; + public readonly StackFrameToken CloseBracket; + public readonly ImmutableArray CommaTokens; + + public StackFrameArrayRankSpecifier(StackFrameToken openBracket, StackFrameToken closeBracket, ImmutableArray commaTokens) + : base(StackFrameKind.ArrayExpression) + { + Debug.Assert(!commaTokens.IsDefault); + Debug.Assert(openBracket.Kind == StackFrameKind.OpenBracketToken); + Debug.Assert(closeBracket.Kind == StackFrameKind.CloseBracketToken); + Debug.Assert(commaTokens.All(static t => t.Kind == StackFrameKind.CommaToken)); + + OpenBracket = openBracket; + CloseBracket = closeBracket; + CommaTokens = commaTokens; + } + + internal override int ChildCount => 2 + CommaTokens.Length; + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + { + if (index == 0) + { + return OpenBracket; + } + + if (index == ChildCount - 1) + { + return CloseBracket; + } + + return CommaTokens[index - 1]; + } + } + + /// + /// The type argument list for a method declaration. + /// + /// + /// Ex: MyType.MyMethod[T, U, V](T t, U u, V v) + /// ^----------------------- "[" = Open Token + /// ^------^ ------------ "T, U, V" = SeparatedStackFrameNodeList<StackFrameTypeArgumentNode> + /// ^-------------- "]" = Close Token + /// + /// + /// + internal sealed class StackFrameTypeArgumentList : StackFrameNode + { + public readonly StackFrameToken OpenToken; + public readonly EmbeddedSeparatedSyntaxNodeList TypeArguments; + public readonly StackFrameToken CloseToken; + + public StackFrameTypeArgumentList( + StackFrameToken openToken, + EmbeddedSeparatedSyntaxNodeList typeArguments, + StackFrameToken closeToken) + : base(StackFrameKind.TypeArgument) + { + Debug.Assert(openToken.Kind is StackFrameKind.OpenBracketToken or StackFrameKind.LessThanToken); + Debug.Assert(typeArguments.Length > 0); + Debug.Assert(openToken.Kind == StackFrameKind.OpenBracketToken ? closeToken.Kind == StackFrameKind.CloseBracketToken : closeToken.Kind == StackFrameKind.GreaterThanToken); + + OpenToken = openToken; + TypeArguments = typeArguments; + CloseToken = closeToken; + } + + internal override int ChildCount => TypeArguments.NodesAndTokens.Length + 2; + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + { + if (index >= ChildCount) + { + throw new InvalidOperationException(); + } + + if (index == 0) + { + return OpenToken; + } + + if (index == ChildCount - 1) + { + return CloseToken; + } + + // Includes both the nodes and separator tokens as children + return TypeArguments.NodesAndTokens[index - 1]; + } + } + + internal sealed class StackFrameParameterDeclarationNode : StackFrameDeclarationNode + { + public readonly StackFrameTypeNode Type; + public readonly StackFrameToken Identifier; + + internal override int ChildCount => 2; + + public StackFrameParameterDeclarationNode(StackFrameTypeNode type, StackFrameToken identifier) + : base(StackFrameKind.Parameter) + { + Debug.Assert(identifier.Kind == StackFrameKind.IdentifierToken); + Type = type; + Identifier = identifier; + } + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + => index switch + { + 0 => Type, + 1 => Identifier, + _ => throw new InvalidOperationException() + }; + } + + internal sealed class StackFrameParameterList : StackFrameNode + { + public readonly StackFrameToken OpenParen; + public readonly EmbeddedSeparatedSyntaxNodeList Parameters; + public readonly StackFrameToken CloseParen; + + public StackFrameParameterList( + StackFrameToken openToken, + EmbeddedSeparatedSyntaxNodeList parameters, + StackFrameToken closeToken) + : base(StackFrameKind.ParameterList) + { + Debug.Assert(openToken.Kind == StackFrameKind.OpenParenToken); + Debug.Assert(closeToken.Kind == StackFrameKind.CloseParenToken); + + OpenParen = openToken; + Parameters = parameters; + CloseParen = closeToken; + } + + internal override int ChildCount => 2 + Parameters.NodesAndTokens.Length; + + public override void Accept(IStackFrameNodeVisitor visitor) + { + visitor.Visit(this); + } + + internal override StackFrameNodeOrToken ChildAt(int index) + { + if (index == 0) + { + return OpenParen; + } + + if (index == ChildCount - 1) + { + return CloseParen; + } + + // Include both nodes and tokens here as children of the StackFrameParameterList + return Parameters.NodesAndTokens[index - 1]; + } + } + + internal sealed class StackFrameFileInformationNode : StackFrameNode + { + public readonly StackFrameToken Path; + public readonly StackFrameToken? Colon; + public readonly StackFrameToken? Line; + + public StackFrameFileInformationNode(StackFrameToken path, StackFrameToken? colon, StackFrameToken? line) : base(StackFrameKind.FileInformation) + { + Debug.Assert(path.Kind == StackFrameKind.PathToken); + Debug.Assert(colon.HasValue == line.HasValue); + Debug.Assert(!line.HasValue || line.Value.Kind == StackFrameKind.NumberToken); + + Path = path; + Colon = colon; + Line = line; + } + + internal override int ChildCount => 3; + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + => index switch + { + 0 => Path, + 1 => Colon.HasValue ? Colon.Value : null, + 2 => Line.HasValue ? Line.Value : null, + _ => throw new InvalidOperationException() + }; + } +} diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameParser.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameParser.cs new file mode 100644 index 0000000000000..5ce37d2e5e994 --- /dev/null +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameParser.cs @@ -0,0 +1,446 @@ +// 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.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.EmbeddedLanguages.Common; +using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame +{ + using StackFrameNodeOrToken = EmbeddedSyntaxNodeOrToken; + using StackFrameToken = EmbeddedSyntaxToken; + using StackFrameTrivia = EmbeddedSyntaxTrivia; + + /// + /// Attempts to parse a stack frame line from given input. StackFrame is generally + /// defined as a string line in a StackTrace. See https://docs.microsoft.com/en-us/dotnet/api/system.environment.stacktrace for + /// more documentation on dotnet stack traces. + /// + internal struct StackFrameParser + { + private StackFrameLexer _lexer; + + private StackFrameParser(StackFrameLexer lexer) + { + _lexer = lexer; + } + + private StackFrameToken CurrentCharAsToken() => _lexer.CurrentCharAsToken(); + + /// + /// Given an input text, and set of options, parses out a fully representative syntax tree + /// and list of diagnostics. Parsing should always succeed, except in the case of the stack + /// overflowing. + /// + public static StackFrameTree? TryParse(VirtualCharSequence text) + { + if (text.IsDefault) + { + return null; + } + + try + { + var lexer = StackFrameLexer.TryCreate(text); + if (!lexer.HasValue) + { + return null; + } + + return new StackFrameParser(lexer.Value).TryParseTree(); + } + catch (InsufficientExecutionStackException) + { + return null; + } + } + + /// + /// Constructs a and calls + /// + public static StackFrameTree? TryParse(string text) + => TryParse(VirtualCharSequence.Create(0, text)); + + /// + /// Attempts to parse the full tree. Returns null on malformed data + /// + private StackFrameTree? TryParseTree() + { + var methodDeclaration = TryParseRequiredMethodDeclaration(); + if (methodDeclaration is null) + { + return null; + } + + var fileInformationResult = TryParseFileInformation(); + if (!fileInformationResult.Success) + { + return null; + } + + var remainingTrivia = _lexer.TryScanRemainingTrivia(); + + var eolToken = CurrentCharAsToken().With(leadingTrivia: remainingTrivia.ToImmutableArray()); + + Contract.ThrowIfFalse(_lexer.Position == _lexer.Text.Length); + Contract.ThrowIfFalse(eolToken.Kind == StackFrameKind.EndOfFrame); + + var root = new StackFrameCompilationUnit(methodDeclaration, fileInformationResult.Value, eolToken); + + return new(_lexer.Text, root); + } + + /// + /// Attempts to parse the full method declaration, optionally adding leading whitespace as trivia. Includes + /// all of the generic indicators for types, + /// + /// Ex: [|MyClass.MyMethod(string s)|] + /// + private StackFrameMethodDeclarationNode? TryParseRequiredMethodDeclaration() + { + var identifierNode = TryParseRequiredNameNode(scanAtTrivia: true); + + // + // TryParseRequiredNameNode does not necessarily return a qualified name even if + // it parses a name. For method declarations, a fully qualified name is required so + // we know both the class (and namespace) that the method is contained in. + // + if (identifierNode is not StackFrameQualifiedNameNode memberAccessExpression) + { + return null; + } + + var (success, typeArguments) = TryParseTypeArguments(); + if (!success) + { + return null; + } + + var methodParameters = TryParseRequiredMethodParameters(); + if (methodParameters is null) + { + return null; + } + + return new StackFrameMethodDeclarationNode(memberAccessExpression, typeArguments, methodParameters); + } + + /// + /// Parses a which could either be a or . + /// + /// Nodes will be parsed for arity but not generic type arguments. + /// + /// + /// All of the following are valid nodes, where "$$" marks the parsing starting point, and "[|" + "|]" mark the endpoints of the parsed node excluding trivia + /// * [|$$MyNamespace.MyClass.MyMethod|](string s) + /// * MyClass.MyMethod([|$$string|] s) + /// * MyClass.MyMethod([|$$string[]|] s) + /// * [|$$MyClass`1.MyMethod|](string s) + /// * [|$$MyClass.MyMethod|][T](T t) + /// + /// + /// + private StackFrameNameNode? TryParseRequiredNameNode(bool scanAtTrivia) + { + var currentIdentifer = _lexer.TryScanIdentifier(scanAtTrivia: scanAtTrivia, scanLeadingWhitespace: true, scanTrailingWhitespace: false); + if (!currentIdentifer.HasValue) + { + return null; + } + + var (success, genericIdentifier) = TryScanGenericTypeIdentifier(currentIdentifer.Value); + if (!success) + { + return null; + } + + RoslynDebug.AssertNotNull(genericIdentifier); + StackFrameNameNode nameNode = genericIdentifier; + + while (true) + { + (success, var memberAccess) = TryParseQualifiedName(nameNode); + if (!success) + { + return null; + } + + if (memberAccess is null) + { + Debug.Assert(nameNode is StackFrameQualifiedNameNode or StackFrameSimpleNameNode); + return nameNode; + } + + nameNode = memberAccess; + } + } + + /// + /// Given an existing left hand side node or token, parse a qualified name if possible. Returns + /// null with success if a dot token is not available + /// + private Result TryParseQualifiedName(StackFrameNameNode lhs) + { + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.DotToken, out var dotToken)) + { + return Result.Empty; + } + + var identifier = _lexer.TryScanIdentifier(); + if (!identifier.HasValue) + { + return Result.Abort; + } + + var (success, rhs) = TryScanGenericTypeIdentifier(identifier.Value); + if (!success) + { + return Result.Abort; + } + + RoslynDebug.AssertNotNull(rhs); + return new StackFrameQualifiedNameNode(lhs, dotToken, rhs); + } + + /// + /// Given an identifier, attempts to parse the type identifier arity for it. + /// + /// + /// ex: MyNamespace.MyClass`1.MyMethod() + /// ^--------------------- MyClass would be the identifier passed in + /// ^-------------- Grave token + /// ^------------- Arity token of "1" + /// + /// + private Result TryScanGenericTypeIdentifier(StackFrameToken identifierToken) + { + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.GraveAccentToken, out var graveAccentToken)) + { + return new(new StackFrameIdentifierNameNode(identifierToken)); + } + + var arity = _lexer.TryScanNumbers(); + if (!arity.HasValue) + { + return Result.Abort; + } + + return new StackFrameGenericNameNode(identifierToken, graveAccentToken, arity.Value); + } + + /// + /// Type arguments for stacks are only valid on method declarations, and can have either '[' or '<' as the + /// starting character depending on output source. + /// + /// ex: MyNamespace.MyClass.MyMethod[T](T t) + /// ex: MyNamespace.MyClass.MyMethod<T<(T t) + /// + /// Assumes the identifier "MyMethod" has already been parsed, and the type arguments will need to be parsed. + /// + private Result TryParseTypeArguments() + { + if (!_lexer.ScanCurrentCharAsTokenIfMatch( + kind => kind is StackFrameKind.OpenBracketToken or StackFrameKind.LessThanToken, + out var openToken)) + { + return Result.Empty; + } + + var closeBracketKind = openToken.Kind is StackFrameKind.OpenBracketToken + ? StackFrameKind.CloseBracketToken + : StackFrameKind.GreaterThanToken; + + using var _ = ArrayBuilder.GetInstance(out var builder); + var currentIdentifier = _lexer.TryScanIdentifier(scanAtTrivia: false, scanLeadingWhitespace: true, scanTrailingWhitespace: true); + StackFrameToken closeToken = default; + + while (currentIdentifier.HasValue && currentIdentifier.Value.Kind == StackFrameKind.IdentifierToken) + { + builder.Add(new StackFrameIdentifierNameNode(currentIdentifier.Value)); + + if (_lexer.ScanCurrentCharAsTokenIfMatch(closeBracketKind, out closeToken)) + { + break; + } + + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.CommaToken, out var commaToken)) + { + return Result.Abort; + } + + builder.Add(commaToken); + currentIdentifier = _lexer.TryScanIdentifier(); + } + + if (builder.Count == 0) + { + return Result.Abort; + } + + if (closeToken.IsMissing) + { + return Result.Abort; + } + + var separatedList = new EmbeddedSeparatedSyntaxNodeList(builder.ToImmutable()); + return new StackFrameTypeArgumentList(openToken, separatedList, closeToken); + } + + /// + /// MyNamespace.MyClass.MyMethod[|(string s1, string s2, int i1)|] + /// Takes parameter declarations from method text and parses them into a . + /// + /// + /// This method assumes that the caller requires method parameters, and returns null for all failure cases. The caller + /// should escalate to abort parsing on null values. + /// + private StackFrameParameterList? TryParseRequiredMethodParameters() + { + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.OpenParenToken, scanTrailingWhitespace: true, out var openParen)) + { + return null; + } + + if (_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.CloseParenToken, out var closeParen)) + { + return new(openParen, EmbeddedSeparatedSyntaxNodeList.Empty, closeParen); + } + + using var _ = ArrayBuilder.GetInstance(out var builder); + + while (true) + { + var (success, parameterNode) = ParseParameterNode(); + if (!success) + { + return null; + } + + RoslynDebug.AssertNotNull(parameterNode); + builder.Add(parameterNode); + + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.CommaToken, out var commaToken)) + { + break; + } + + builder.Add(commaToken); + } + + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.CloseParenToken, out closeParen)) + { + return null; + } + + var parameters = new EmbeddedSeparatedSyntaxNodeList(builder.ToImmutable()); + return new(openParen, parameters, closeParen); + } + + /// + /// Parses a by parsing identifiers first representing the type and then the parameter identifier. + /// Ex: System.String[] s + /// ^--------------^ -- Type = "System.String[]" + /// ^-- Identifier = "s" + /// + private Result ParseParameterNode() + { + var nameNode = TryParseRequiredNameNode(scanAtTrivia: false); + if (nameNode is null) + { + return Result.Abort; + } + + StackFrameTypeNode? typeIdentifier = nameNode; + if (CurrentCharAsToken().Kind == StackFrameKind.OpenBracketToken) + { + var (success, arrayIdentifiers) = ParseArrayRankSpecifiers(); + if (!success || arrayIdentifiers.IsDefault) + { + return Result.Abort; + } + + typeIdentifier = new StackFrameArrayTypeNode(nameNode, arrayIdentifiers); + } + + var identifier = _lexer.TryScanIdentifier(scanAtTrivia: false, scanLeadingWhitespace: true, scanTrailingWhitespace: true); + if (!identifier.HasValue) + { + return Result.Abort; + } + + return new StackFrameParameterDeclarationNode(typeIdentifier, identifier.Value); + } + + /// + /// Parses the array rank specifiers for an identifier. + /// Ex: string[,][] + /// ^----^ both are array rank specifiers + /// 0: "[,] + /// 1: "[]" + /// + private Result> ParseArrayRankSpecifiers() + { + using var _ = ArrayBuilder.GetInstance(out var builder); + using var _1 = ArrayBuilder.GetInstance(out var commaBuilder); + + while (true) + { + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.OpenBracketToken, scanTrailingWhitespace: true, out var openBracket)) + { + return new(builder.ToImmutable()); + } + + while (_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.CommaToken, scanTrailingWhitespace: true, out var commaToken)) + { + commaBuilder.Add(commaToken); + } + + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.CloseBracketToken, scanTrailingWhitespace: true, out var closeBracket)) + { + return Result>.Abort; + } + + builder.Add(new StackFrameArrayRankSpecifier(openBracket, closeBracket, commaBuilder.ToImmutableAndClear())); + } + } + + /// + /// Parses text for a valid file path using valid file characters. It's very possible this includes a path that doesn't exist but + /// forms a valid path identifier. + /// + /// Can return if only a path is available but not line numbers, but throws if the value after the path is a colon as the expectation + /// is that line number should follow. + /// + private Result TryParseFileInformation() + { + var (success, path) = _lexer.TryScanPath(); + if (!success) + { + return Result.Abort; + } + + if (path.Kind == StackFrameKind.None) + { + return Result.Empty; + } + + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.ColonToken, out var colonToken)) + { + return new StackFrameFileInformationNode(path, colon: null, line: null); + } + + var lineNumber = _lexer.TryScanRequiredLineNumber(); + if (!lineNumber.HasValue) + { + return Result.Abort; + } + + return new StackFrameFileInformationNode(path, colonToken, lineNumber); + } + } +} diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameTree.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameTree.cs new file mode 100644 index 0000000000000..49886856eb58c --- /dev/null +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameTree.cs @@ -0,0 +1,18 @@ +// 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 Microsoft.CodeAnalysis.EmbeddedLanguages.Common; +using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame +{ + internal class StackFrameTree : EmbeddedSyntaxTree + { + public StackFrameTree(VirtualCharSequence text, StackFrameCompilationUnit root) + : base(text, root, ImmutableArray.Empty) + { + } + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index f616833480ad6..d2868a4b2cbe8 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -192,6 +192,7 @@ + diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSeparatedSyntaxNodeList.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSeparatedSyntaxNodeList.cs new file mode 100644 index 0000000000000..4cd837cabae59 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSeparatedSyntaxNodeList.cs @@ -0,0 +1,107 @@ +// 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.Immutable; +using System.Diagnostics; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.EmbeddedLanguages.Common +{ + internal readonly struct EmbeddedSeparatedSyntaxNodeList + where TSyntaxKind : struct + where TSyntaxNode : EmbeddedSyntaxNode + where TDerivedNode : TSyntaxNode + { + public ImmutableArray> NodesAndTokens { get; } + public int Length { get; } + public int SeparatorLength { get; } + + public static readonly EmbeddedSeparatedSyntaxNodeList Empty + = new(ImmutableArray>.Empty); + + public EmbeddedSeparatedSyntaxNodeList( + ImmutableArray> nodesAndTokens) + { + Contract.ThrowIfTrue(nodesAndTokens.IsDefault); + NodesAndTokens = nodesAndTokens; + + var allLength = NodesAndTokens.Length; + Length = (allLength + 1) / 2; + SeparatorLength = allLength / 2; + + Verify(); + } + + [Conditional("DEBUG")] + private void Verify() + { + for (var i = 0; i < NodesAndTokens.Length; i++) + { + if ((i & 1) == 0) + { + // All even values should be TNode + Debug.Assert(NodesAndTokens[i].IsNode); + Debug.Assert(NodesAndTokens[i].Node is EmbeddedSyntaxNode); + } + else + { + // All odd values should be separator tokens + Debug.Assert(!NodesAndTokens[i].IsNode); + } + } + } + + /// + /// Retrieves only nodes, skipping the separator tokens + /// + public TDerivedNode this[int index] + { + get + { + if (index < Length && index >= 0) + { + // x2 here to get only even indexed numbers. Follows same logic + // as SeparatedSyntaxList in that the separator tokens are not returned + var nodeOrToken = NodesAndTokens[index * 2]; + Debug.Assert(nodeOrToken.IsNode); + RoslynDebug.AssertNotNull(nodeOrToken.Node); + return (TDerivedNode)nodeOrToken.Node; + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + public Enumerator GetEnumerator() => new(this); + + public struct Enumerator + { + private readonly EmbeddedSeparatedSyntaxNodeList _list; + private int _currentIndex; + + public Enumerator(EmbeddedSeparatedSyntaxNodeList list) + { + _list = list; + _currentIndex = -1; + Current = null!; + } + + public TDerivedNode Current { get; private set; } + + public bool MoveNext() + { + _currentIndex++; + if (_currentIndex >= _list.Length) + { + Current = null!; + return false; + } + + Current = _list[_currentIndex]; + return true; + } + } + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSyntaxNode.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSyntaxNode.cs index 1d7c493a5381f..2c5689b88beb7 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSyntaxNode.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSyntaxNode.cs @@ -47,6 +47,9 @@ protected EmbeddedSyntaxNode(TSyntaxKind kind) internal abstract int ChildCount { get; } internal abstract EmbeddedSyntaxNodeOrToken ChildAt(int index); + public EmbeddedSyntaxNodeOrToken this[int index] => ChildAt(index); + public EmbeddedSyntaxNodeOrToken this[Index index] => this[index.GetOffset(this.ChildCount)]; + public TextSpan GetSpan() { var start = int.MaxValue; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSyntaxNodeOrToken.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSyntaxNodeOrToken.cs index 7861db94a9b91..8024776f059b4 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSyntaxNodeOrToken.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common/EmbeddedSyntaxNodeOrToken.cs @@ -15,9 +15,8 @@ internal struct EmbeddedSyntaxNodeOrToken public readonly TSyntaxNode? Node; public readonly EmbeddedSyntaxToken Token; - private EmbeddedSyntaxNodeOrToken(TSyntaxNode node) : this() + private EmbeddedSyntaxNodeOrToken(TSyntaxNode? node) : this() { - RoslynDebug.AssertNotNull(node); Node = node; } @@ -30,7 +29,7 @@ private EmbeddedSyntaxNodeOrToken(EmbeddedSyntaxToken token) : this [MemberNotNullWhen(true, nameof(Node))] public bool IsNode => Node != null; - public static implicit operator EmbeddedSyntaxNodeOrToken(TSyntaxNode node) + public static implicit operator EmbeddedSyntaxNodeOrToken(TSyntaxNode? node) => new(node); public static implicit operator EmbeddedSyntaxNodeOrToken(EmbeddedSyntaxToken token)