|
' 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.
Imports System.Collections.ObjectModel
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
Imports System.Text
Imports Microsoft.CodeAnalysis.Collections
Imports Microsoft.CodeAnalysis.PooledObjects
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Namespace Microsoft.CodeAnalysis.VisualBasic.ExpressionEvaluator
Friend Module SyntaxHelpers
Friend ReadOnly ParseOptions As VisualBasicParseOptions = VisualBasicParseOptions.Default.WithLanguageVersion(LanguageVersionFacts.CurrentVersion)
''' <summary>
''' Parse expression. Returns null if there are any errors.
''' </summary>
<Extension>
Friend Function ParseExpression(expr As String, diagnostics As DiagnosticBag, allowFormatSpecifiers As Boolean, <Out> ByRef formatSpecifiers As ReadOnlyCollection(Of String)) As ExecutableStatementSyntax
Dim syntax = ParseDebuggerExpression(expr, consumeFullText:=Not allowFormatSpecifiers)
diagnostics.AddRange(syntax.GetDiagnostics())
formatSpecifiers = Nothing
If allowFormatSpecifiers Then
Dim builder = ArrayBuilder(Of String).GetInstance()
If ParseFormatSpecifiers(builder, expr, syntax.Expression.FullWidth, diagnostics) AndAlso builder.Count > 0 Then
formatSpecifiers = New ReadOnlyCollection(Of String)(builder.ToArray())
End If
builder.Free()
End If
Return If(diagnostics.HasAnyErrors(), Nothing, syntax)
End Function
<Extension>
Friend Function ParseAssignment(target As String, expr As String, diagnostics As DiagnosticBag) As AssignmentStatementSyntax
Dim text = SourceText.From(expr, encoding:=Nothing, SourceHashAlgorithms.Default)
Dim expression = SyntaxHelpers.ParseDebuggerExpressionInternal(text, consumeFullText:=True)
' We're creating a SyntaxTree for just the RHS so that the Diagnostic spans for parse errors
' will be correct (with respect to the original input text). If we ever expose a SemanticModel
' for debugger expressions, we should use this SyntaxTree.
Dim syntaxTree = CreateSyntaxTree(expression, text)
diagnostics.AddRange(syntaxTree.GetDiagnostics())
If diagnostics.HasAnyErrors Then
Return Nothing
End If
' Any Diagnostic spans produced in binding will be offset by the length of the "target" expression text.
' If we want to support live squiggles in debugger windows, SemanticModel, etc, we'll want to address this.
Dim targetText = SourceText.From(target, encoding:=Nothing, SourceHashAlgorithms.Default)
Dim targetSyntax = SyntaxHelpers.ParseDebuggerExpressionInternal(targetText, consumeFullText:=True)
Debug.Assert(Not targetSyntax.GetDiagnostics().Any(), "The target of an assignment should never contain Diagnostics if we're being allowed to assign to it in the debugger.")
Dim assignment = InternalSyntax.SyntaxFactory.SimpleAssignmentStatement(
targetSyntax,
New InternalSyntax.PunctuationSyntax(SyntaxKind.EqualsToken, "=", Nothing, Nothing),
expression)
Dim assignmentText = SourceText.From(assignment.ToString(), encoding:=Nothing, SourceHashAlgorithms.Default)
syntaxTree = CreateSyntaxTree(assignment.MakeDebuggerStatementContext(), assignmentText)
Return DirectCast(syntaxTree.GetDebuggerStatement(), AssignmentStatementSyntax)
End Function
''' <summary>
''' Parse statement. Returns null if there are any errors.
''' </summary>
<Extension>
Friend Function ParseStatement(statement As String, diagnostics As DiagnosticBag) As StatementSyntax
Dim syntax = ParseDebuggerStatement(statement)
diagnostics.AddRange(syntax.GetDiagnostics())
Return If(diagnostics.HasAnyErrors(), Nothing, syntax)
End Function
''' <summary>
''' Return set of identifier tokens, with leading And
''' trailing spaces And comma separators removed.
''' </summary>
''' <remarks>
''' The native VB EE didn't support format specifiers.
''' </remarks>
Private Function ParseFormatSpecifiers(
builder As ArrayBuilder(Of String),
expr As String,
offset As Integer,
diagnostics As DiagnosticBag) As Boolean
Dim expectingComma = True
Dim start = -1
Dim n = expr.Length
While offset < n
Dim c = expr(offset)
If SyntaxFacts.IsWhitespace(c) OrElse c = ","c Then
If start >= 0 Then
Dim token = expr.Substring(start, offset - start)
If expectingComma Then
ReportInvalidFormatSpecifier(token, diagnostics)
Return False
End If
builder.Add(token)
start = -1
expectingComma = c <> ","c
ElseIf c = ","c Then
If Not expectingComma Then
ReportInvalidFormatSpecifier(",", diagnostics)
Return False
End If
expectingComma = False
End If
ElseIf start < 0 Then
start = offset
End If
offset = offset + 1
End While
If start >= 0 Then
Dim token = expr.Substring(start)
If expectingComma Then
ReportInvalidFormatSpecifier(token, diagnostics)
Return False
End If
builder.Add(token)
ElseIf Not expectingComma Then
ReportInvalidFormatSpecifier(",", diagnostics)
Return False
End If
' Verify format specifiers are valid identifiers.
For Each token In builder
If Not token.All(AddressOf SyntaxFacts.IsIdentifierPartCharacter) Then
ReportInvalidFormatSpecifier(token, diagnostics)
Return False
End If
Next
Return True
End Function
Private Sub ReportInvalidFormatSpecifier(token As String, diagnostics As DiagnosticBag)
diagnostics.Add(ERRID.ERR_InvalidFormatSpecifier, Location.None, token)
End Sub
''' <summary>
''' Parse a debugger expression (e.g. possibly including pseudo-variables).
''' </summary>
''' <param name="source">The input string</param>
''' <remarks>
''' It would be better if this method returned ExpressionStatementSyntax, but this is the best we can do for
''' the time being due to issues in the binder resolving ambiguities between invocations and array access.
''' </remarks>
Friend Function ParseDebuggerExpression(source As String, consumeFullText As Boolean) As PrintStatementSyntax
Dim text = SourceText.From(source, encoding:=Nothing, SourceHashAlgorithms.Default)
Dim expression = ParseDebuggerExpressionInternal(text, consumeFullText)
Dim statement = InternalSyntax.SyntaxFactory.PrintStatement(
New InternalSyntax.PunctuationSyntax(SyntaxKind.QuestionToken, "?", Nothing, Nothing), expression)
Dim syntaxTree = CreateSyntaxTree(statement.MakeDebuggerStatementContext(), text)
Return DirectCast(syntaxTree.GetDebuggerStatement(), PrintStatementSyntax)
End Function
Private Function ParseDebuggerExpressionInternal(source As SourceText, consumeFullText As Boolean) As InternalSyntax.ExpressionSyntax
Using scanner As New InternalSyntax.Scanner(source, ParseOptions, isScanningForExpressionCompiler:=True) ' NOTE: Default options should be enough
Using p = New InternalSyntax.Parser(scanner)
p.GetNextToken()
Dim node = p.ParseExpression()
If consumeFullText Then node = p.ConsumeUnexpectedTokens(node)
Return node
End Using
End Using
End Function
Private Function ParseDebuggerStatement(source As String) As StatementSyntax
Dim text = SourceText.From(source, encoding:=Nothing, SourceHashAlgorithms.Default)
Using scanner As New InternalSyntax.Scanner(text, ParseOptions, isScanningForExpressionCompiler:=True) ' NOTE: Default options should be enough
Using p = New InternalSyntax.Parser(scanner)
p.GetNextToken()
Dim node = p.ParseStatementInMethodBody()
node = p.ConsumeUnexpectedTokens(node)
Dim syntaxTree = CreateSyntaxTree(node.MakeDebuggerStatementContext(), text)
Return syntaxTree.GetDebuggerStatement()
End Using
End Using
End Function
Private Function CreateSyntaxTree(root As InternalSyntax.VisualBasicSyntaxNode, text As SourceText) As SyntaxTree
Return VisualBasicSyntaxTree.CreateForDebugger(DirectCast(root.CreateRed(Nothing, 0), VisualBasicSyntaxNode), text, ParseOptions)
End Function
<Extension>
Private Function MakeDebuggerStatementContext(statement As InternalSyntax.StatementSyntax) As InternalSyntax.CompilationUnitSyntax
Return InternalSyntax.SyntaxFactory.CompilationUnit(
options:=Nothing,
[imports]:=Nothing,
attributes:=Nothing,
members:=Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList.List(statement),
endOfFileToken:=InternalSyntax.SyntaxFactory.EndOfFileToken)
End Function
<Extension>
Private Function GetDebuggerStatement(syntaxTree As SyntaxTree) As StatementSyntax
Return DirectCast(DirectCast(syntaxTree.GetRoot(), CompilationUnitSyntax).Members.Single(), StatementSyntax)
End Function
''' <summary>
''' This list is based on the statements found in StatementAnalyzer::IsSupportedStatement
''' (vb\language\debugger\statementanalyzer.cpp).
''' We'll add to that list some additional statements that can easily be supported by the new implementation
''' (include all compound assignments, not just "+=", etc).
''' For now, we'll leave out single line If statements, as the parsing for those would require extra
''' complexity on the EE side (ParseStatementInMethodBody should handle them, but it doesn't...).
''' </summary>
Friend Function IsSupportedDebuggerStatement(syntax As StatementSyntax) As Boolean
Select Case syntax.Kind
Case SyntaxKind.AddAssignmentStatement,
SyntaxKind.CallStatement,
SyntaxKind.ConcatenateAssignmentStatement,
SyntaxKind.DivideAssignmentStatement,
SyntaxKind.ExponentiateAssignmentStatement,
SyntaxKind.ExpressionStatement,
SyntaxKind.IntegerDivideAssignmentStatement,
SyntaxKind.LeftShiftAssignmentStatement,
SyntaxKind.MultiplyAssignmentStatement,
SyntaxKind.PrintStatement,
SyntaxKind.ReDimStatement,
SyntaxKind.ReDimPreserveStatement,
SyntaxKind.RightShiftAssignmentStatement,
SyntaxKind.SimpleAssignmentStatement,
SyntaxKind.SubtractAssignmentStatement
Return True
Case Else
Return False
End Select
End Function
Friend Function EscapeKeywordIdentifiers(identifier As String) As String
If SyntaxFacts.IsKeywordKind(SyntaxFacts.GetKeywordKind(identifier)) Then
Dim pooled = PooledStringBuilder.GetInstance()
Dim builder = pooled.Builder
builder.Append("["c)
builder.Append(identifier)
builder.Append("]"c)
Return pooled.ToStringAndFree()
Else
Return identifier
End If
End Function
End Module
End Namespace
|