683 lines
23 KiB
C#
683 lines
23 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using Microsoft.AspNet.Razor.Generator;
|
|
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
|
using Microsoft.AspNet.Razor.Tokenizer.Symbols;
|
|
|
|
namespace Microsoft.AspNet.Razor.Parser
|
|
{
|
|
public partial class CSharpCodeParser
|
|
{
|
|
private void SetUpKeywords()
|
|
{
|
|
MapKeywords(ConditionalBlock, CSharpKeyword.For, CSharpKeyword.Foreach, CSharpKeyword.While, CSharpKeyword.Switch, CSharpKeyword.Lock);
|
|
MapKeywords(CaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default);
|
|
MapKeywords(IfStatement, CSharpKeyword.If);
|
|
MapKeywords(TryStatement, CSharpKeyword.Try);
|
|
MapKeywords(UsingKeyword, CSharpKeyword.Using);
|
|
MapKeywords(DoStatement, CSharpKeyword.Do);
|
|
MapKeywords(ReservedDirective, CSharpKeyword.Namespace, CSharpKeyword.Class);
|
|
}
|
|
|
|
protected virtual void ReservedDirective(bool topLevel)
|
|
{
|
|
Context.OnError(CurrentLocation, RazorResources.FormatParseError_ReservedWord(CurrentSymbol.Content));
|
|
AcceptAndMoveNext();
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
Span.CodeGenerator = SpanCodeGenerator.Null;
|
|
Context.CurrentBlock.Type = BlockType.Directive;
|
|
CompleteBlock();
|
|
Output(SpanKind.MetaCode);
|
|
}
|
|
|
|
private void KeywordBlock(bool topLevel)
|
|
{
|
|
HandleKeyword(topLevel, () =>
|
|
{
|
|
Context.CurrentBlock.Type = BlockType.Expression;
|
|
Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
|
|
ImplicitExpression();
|
|
});
|
|
}
|
|
|
|
private void CaseStatement(bool topLevel)
|
|
{
|
|
Assert(CSharpSymbolType.Keyword);
|
|
Debug.Assert(CurrentSymbol.Keyword != null &&
|
|
(CurrentSymbol.Keyword.Value == CSharpKeyword.Case ||
|
|
CurrentSymbol.Keyword.Value == CSharpKeyword.Default));
|
|
AcceptUntil(CSharpSymbolType.Colon);
|
|
Optional(CSharpSymbolType.Colon);
|
|
}
|
|
|
|
private void DoStatement(bool topLevel)
|
|
{
|
|
Assert(CSharpKeyword.Do);
|
|
UnconditionalBlock();
|
|
WhileClause();
|
|
if (topLevel)
|
|
{
|
|
CompleteBlock();
|
|
}
|
|
}
|
|
|
|
private void WhileClause()
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
|
|
IEnumerable<CSharpSymbol> ws = SkipToNextImportantToken();
|
|
|
|
if (At(CSharpKeyword.While))
|
|
{
|
|
Accept(ws);
|
|
Assert(CSharpKeyword.While);
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
if (AcceptCondition() && Optional(CSharpSymbolType.Semicolon))
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PutCurrentBack();
|
|
PutBack(ws);
|
|
}
|
|
}
|
|
|
|
private void UsingKeyword(bool topLevel)
|
|
{
|
|
Assert(CSharpKeyword.Using);
|
|
var block = new Block(CurrentSymbol);
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
|
|
if (At(CSharpSymbolType.LeftParenthesis))
|
|
{
|
|
// using ( ==> Using Statement
|
|
UsingStatement(block);
|
|
}
|
|
else if (At(CSharpSymbolType.Identifier))
|
|
{
|
|
// using Identifier ==> Using Declaration
|
|
if (!topLevel)
|
|
{
|
|
Context.OnError(block.Start, RazorResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock);
|
|
StandardStatement();
|
|
}
|
|
else
|
|
{
|
|
UsingDeclaration();
|
|
}
|
|
}
|
|
|
|
if (topLevel)
|
|
{
|
|
CompleteBlock();
|
|
}
|
|
}
|
|
|
|
private void UsingDeclaration()
|
|
{
|
|
// Set block type to directive
|
|
Context.CurrentBlock.Type = BlockType.Directive;
|
|
|
|
// Parse a type name
|
|
Assert(CSharpSymbolType.Identifier);
|
|
NamespaceOrTypeName();
|
|
IEnumerable<CSharpSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
if (At(CSharpSymbolType.Assign))
|
|
{
|
|
// Alias
|
|
Accept(ws);
|
|
Assert(CSharpSymbolType.Assign);
|
|
AcceptAndMoveNext();
|
|
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
|
|
// One more namespace or type name
|
|
NamespaceOrTypeName();
|
|
}
|
|
else
|
|
{
|
|
PutCurrentBack();
|
|
PutBack(ws);
|
|
}
|
|
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.AnyExceptNewline;
|
|
Span.CodeGenerator = new AddImportCodeGenerator(
|
|
Span.GetContent(syms => syms.Skip(1)), // Skip "using"
|
|
SyntaxConstants.CSharp.UsingKeywordLength);
|
|
|
|
// Optional ";"
|
|
if (EnsureCurrent())
|
|
{
|
|
Optional(CSharpSymbolType.Semicolon);
|
|
}
|
|
}
|
|
|
|
protected bool NamespaceOrTypeName()
|
|
{
|
|
if (Optional(CSharpSymbolType.Identifier) || Optional(CSharpSymbolType.Keyword))
|
|
{
|
|
Optional(CSharpSymbolType.QuestionMark); // Nullable
|
|
if (Optional(CSharpSymbolType.DoubleColon))
|
|
{
|
|
if (!Optional(CSharpSymbolType.Identifier))
|
|
{
|
|
Optional(CSharpSymbolType.Keyword);
|
|
}
|
|
}
|
|
if (At(CSharpSymbolType.LessThan))
|
|
{
|
|
TypeArgumentList();
|
|
}
|
|
if (Optional(CSharpSymbolType.Dot))
|
|
{
|
|
NamespaceOrTypeName();
|
|
}
|
|
while (At(CSharpSymbolType.LeftBracket))
|
|
{
|
|
Balance(BalancingModes.None);
|
|
Optional(CSharpSymbolType.RightBracket);
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void TypeArgumentList()
|
|
{
|
|
Assert(CSharpSymbolType.LessThan);
|
|
Balance(BalancingModes.None);
|
|
Optional(CSharpSymbolType.GreaterThan);
|
|
}
|
|
|
|
private void UsingStatement(Block block)
|
|
{
|
|
Assert(CSharpSymbolType.LeftParenthesis);
|
|
|
|
// Parse condition
|
|
if (AcceptCondition())
|
|
{
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
|
|
// Parse code block
|
|
ExpectCodeBlock(block);
|
|
}
|
|
}
|
|
|
|
private void TryStatement(bool topLevel)
|
|
{
|
|
Assert(CSharpKeyword.Try);
|
|
UnconditionalBlock();
|
|
AfterTryClause();
|
|
if (topLevel)
|
|
{
|
|
CompleteBlock();
|
|
}
|
|
}
|
|
|
|
private void IfStatement(bool topLevel)
|
|
{
|
|
Assert(CSharpKeyword.If);
|
|
ConditionalBlock(topLevel: false);
|
|
AfterIfClause();
|
|
if (topLevel)
|
|
{
|
|
CompleteBlock();
|
|
}
|
|
}
|
|
|
|
private void AfterTryClause()
|
|
{
|
|
// Grab whitespace
|
|
IEnumerable<CSharpSymbol> ws = SkipToNextImportantToken();
|
|
|
|
// Check for a catch or finally part
|
|
if (At(CSharpKeyword.Catch))
|
|
{
|
|
Accept(ws);
|
|
Assert(CSharpKeyword.Catch);
|
|
ConditionalBlock(topLevel: false);
|
|
AfterTryClause();
|
|
}
|
|
else if (At(CSharpKeyword.Finally))
|
|
{
|
|
Accept(ws);
|
|
Assert(CSharpKeyword.Finally);
|
|
UnconditionalBlock();
|
|
}
|
|
else
|
|
{
|
|
// Return whitespace and end the block
|
|
PutCurrentBack();
|
|
PutBack(ws);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
|
|
}
|
|
}
|
|
|
|
private void AfterIfClause()
|
|
{
|
|
// Grab whitespace and razor comments
|
|
IEnumerable<CSharpSymbol> ws = SkipToNextImportantToken();
|
|
|
|
// Check for an else part
|
|
if (At(CSharpKeyword.Else))
|
|
{
|
|
Accept(ws);
|
|
Assert(CSharpKeyword.Else);
|
|
ElseClause();
|
|
}
|
|
else
|
|
{
|
|
// No else, return whitespace
|
|
PutCurrentBack();
|
|
PutBack(ws);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
|
|
}
|
|
}
|
|
|
|
private void ElseClause()
|
|
{
|
|
if (!At(CSharpKeyword.Else))
|
|
{
|
|
return;
|
|
}
|
|
var block = new Block(CurrentSymbol);
|
|
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
if (At(CSharpKeyword.If))
|
|
{
|
|
// ElseIf
|
|
block.Name = SyntaxConstants.CSharp.ElseIfKeyword;
|
|
ConditionalBlock(block);
|
|
AfterIfClause();
|
|
}
|
|
else if (!EndOfFile)
|
|
{
|
|
// Else
|
|
ExpectCodeBlock(block);
|
|
}
|
|
}
|
|
|
|
private void ExpectCodeBlock(Block block)
|
|
{
|
|
if (!EndOfFile)
|
|
{
|
|
// Check for "{" to make sure we're at a block
|
|
if (!At(CSharpSymbolType.LeftBrace))
|
|
{
|
|
Context.OnError(CurrentLocation,
|
|
RazorResources.FormatParseError_SingleLine_ControlFlowStatements_Not_Allowed(
|
|
Language.GetSample(CSharpSymbolType.LeftBrace),
|
|
CurrentSymbol.Content));
|
|
}
|
|
|
|
// Parse the statement and then we're done
|
|
Statement(block);
|
|
}
|
|
}
|
|
|
|
private void UnconditionalBlock()
|
|
{
|
|
Assert(CSharpSymbolType.Keyword);
|
|
var block = new Block(CurrentSymbol);
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
ExpectCodeBlock(block);
|
|
}
|
|
|
|
private void ConditionalBlock(bool topLevel)
|
|
{
|
|
Assert(CSharpSymbolType.Keyword);
|
|
var block = new Block(CurrentSymbol);
|
|
ConditionalBlock(block);
|
|
if (topLevel)
|
|
{
|
|
CompleteBlock();
|
|
}
|
|
}
|
|
|
|
private void ConditionalBlock(Block block)
|
|
{
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
|
|
// Parse the condition, if present (if not present, we'll let the C# compiler complain)
|
|
if (AcceptCondition())
|
|
{
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
ExpectCodeBlock(block);
|
|
}
|
|
}
|
|
|
|
private bool AcceptCondition()
|
|
{
|
|
if (At(CSharpSymbolType.LeftParenthesis))
|
|
{
|
|
var complete = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates);
|
|
if (!complete)
|
|
{
|
|
AcceptUntil(CSharpSymbolType.NewLine);
|
|
}
|
|
else
|
|
{
|
|
Optional(CSharpSymbolType.RightParenthesis);
|
|
}
|
|
return complete;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void Statement()
|
|
{
|
|
Statement(null);
|
|
}
|
|
|
|
private void Statement(Block block)
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
|
|
|
|
// Accept whitespace but always keep the last whitespace node so we can put it back if necessary
|
|
var lastWhitespace = AcceptWhiteSpaceInLines();
|
|
Debug.Assert(lastWhitespace == null || (lastWhitespace.Start.AbsoluteIndex + lastWhitespace.Content.Length == CurrentLocation.AbsoluteIndex));
|
|
|
|
if (EndOfFile)
|
|
{
|
|
if (lastWhitespace != null)
|
|
{
|
|
Accept(lastWhitespace);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var type = CurrentSymbol.Type;
|
|
var loc = CurrentLocation;
|
|
|
|
var isSingleLineMarkup = type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.Colon);
|
|
var isMarkup = isSingleLineMarkup ||
|
|
type == CSharpSymbolType.LessThan ||
|
|
(type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.LessThan));
|
|
|
|
if (Context.DesignTimeMode || !isMarkup)
|
|
{
|
|
// CODE owns whitespace, MARKUP owns it ONLY in DesignTimeMode.
|
|
if (lastWhitespace != null)
|
|
{
|
|
Accept(lastWhitespace);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var nextSymbol = Lookahead(1);
|
|
|
|
// MARKUP owns whitespace EXCEPT in DesignTimeMode.
|
|
PutCurrentBack();
|
|
|
|
// Don't putback the whitespace if it precedes a '<text>' tag.
|
|
if (nextSymbol != null && !nextSymbol.Content.Equals(SyntaxConstants.TextTagName))
|
|
{
|
|
PutBack(lastWhitespace);
|
|
}
|
|
}
|
|
|
|
if (isMarkup)
|
|
{
|
|
if (type == CSharpSymbolType.Transition && !isSingleLineMarkup)
|
|
{
|
|
Context.OnError(loc, RazorResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start);
|
|
}
|
|
|
|
// Markup block
|
|
Output(SpanKind.Code);
|
|
if (Context.DesignTimeMode && CurrentSymbol != null && (CurrentSymbol.Type == CSharpSymbolType.LessThan || CurrentSymbol.Type == CSharpSymbolType.Transition))
|
|
{
|
|
PutCurrentBack();
|
|
}
|
|
OtherParserBlock();
|
|
}
|
|
else
|
|
{
|
|
// What kind of statement is this?
|
|
HandleStatement(block, type);
|
|
}
|
|
}
|
|
|
|
private void HandleStatement(Block block, CSharpSymbolType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case CSharpSymbolType.RazorCommentTransition:
|
|
Output(SpanKind.Code);
|
|
RazorComment();
|
|
Statement(block);
|
|
break;
|
|
case CSharpSymbolType.LeftBrace:
|
|
// Verbatim Block
|
|
block = block ?? new Block(RazorResources.BlockName_Code, CurrentLocation);
|
|
AcceptAndMoveNext();
|
|
CodeBlock(block);
|
|
break;
|
|
case CSharpSymbolType.Keyword:
|
|
// Keyword block
|
|
HandleKeyword(false, StandardStatement);
|
|
break;
|
|
case CSharpSymbolType.Transition:
|
|
// Embedded Expression block
|
|
EmbeddedExpression();
|
|
break;
|
|
case CSharpSymbolType.RightBrace:
|
|
// Possible end of Code Block, just run the continuation
|
|
break;
|
|
case CSharpSymbolType.Comment:
|
|
AcceptAndMoveNext();
|
|
break;
|
|
default:
|
|
// Other statement
|
|
StandardStatement();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void EmbeddedExpression()
|
|
{
|
|
// First, verify the type of the block
|
|
Assert(CSharpSymbolType.Transition);
|
|
var transition = CurrentSymbol;
|
|
NextToken();
|
|
|
|
if (At(CSharpSymbolType.Transition))
|
|
{
|
|
// Escaped "@"
|
|
Output(SpanKind.Code);
|
|
|
|
// Output "@" as hidden span
|
|
Accept(transition);
|
|
Span.CodeGenerator = SpanCodeGenerator.Null;
|
|
Output(SpanKind.Code);
|
|
|
|
Assert(CSharpSymbolType.Transition);
|
|
AcceptAndMoveNext();
|
|
StandardStatement();
|
|
}
|
|
else
|
|
{
|
|
// Throw errors as necessary, but continue parsing
|
|
if (At(CSharpSymbolType.LeftBrace))
|
|
{
|
|
Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_Nested_CodeBlock);
|
|
}
|
|
|
|
// @( or @foo - Nested expression, parse a child block
|
|
PutCurrentBack();
|
|
PutBack(transition);
|
|
|
|
// Before exiting, add a marker span if necessary
|
|
AddMarkerSymbolIfNecessary();
|
|
|
|
NestedBlock();
|
|
}
|
|
}
|
|
|
|
private void StandardStatement()
|
|
{
|
|
while (!EndOfFile)
|
|
{
|
|
var bookmark = CurrentLocation.AbsoluteIndex;
|
|
IEnumerable<CSharpSymbol> read = ReadWhile(sym => sym.Type != CSharpSymbolType.Semicolon &&
|
|
sym.Type != CSharpSymbolType.RazorCommentTransition &&
|
|
sym.Type != CSharpSymbolType.Transition &&
|
|
sym.Type != CSharpSymbolType.LeftBrace &&
|
|
sym.Type != CSharpSymbolType.LeftParenthesis &&
|
|
sym.Type != CSharpSymbolType.LeftBracket &&
|
|
sym.Type != CSharpSymbolType.RightBrace);
|
|
if (At(CSharpSymbolType.LeftBrace) || At(CSharpSymbolType.LeftParenthesis) || At(CSharpSymbolType.LeftBracket))
|
|
{
|
|
Accept(read);
|
|
if (Balance(BalancingModes.AllowCommentsAndTemplates | BalancingModes.BacktrackOnFailure))
|
|
{
|
|
Optional(CSharpSymbolType.RightBrace);
|
|
}
|
|
else
|
|
{
|
|
// Recovery
|
|
AcceptUntil(CSharpSymbolType.LessThan, CSharpSymbolType.RightBrace);
|
|
return;
|
|
}
|
|
}
|
|
else if (At(CSharpSymbolType.Transition) && (NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon)))
|
|
{
|
|
Accept(read);
|
|
Output(SpanKind.Code);
|
|
Template();
|
|
}
|
|
else if (At(CSharpSymbolType.RazorCommentTransition))
|
|
{
|
|
Accept(read);
|
|
RazorComment();
|
|
}
|
|
else if (At(CSharpSymbolType.Semicolon))
|
|
{
|
|
Accept(read);
|
|
AcceptAndMoveNext();
|
|
return;
|
|
}
|
|
else if (At(CSharpSymbolType.RightBrace))
|
|
{
|
|
Accept(read);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Context.Source.Position = bookmark;
|
|
NextToken();
|
|
AcceptUntil(CSharpSymbolType.LessThan, CSharpSymbolType.RightBrace);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CodeBlock(Block block)
|
|
{
|
|
CodeBlock(true, block);
|
|
}
|
|
|
|
private void CodeBlock(bool acceptTerminatingBrace, Block block)
|
|
{
|
|
EnsureCurrent();
|
|
while (!EndOfFile && !At(CSharpSymbolType.RightBrace))
|
|
{
|
|
// Parse a statement, then return here
|
|
Statement();
|
|
EnsureCurrent();
|
|
}
|
|
|
|
if (EndOfFile)
|
|
{
|
|
Context.OnError(block.Start, RazorResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, '}', '{'));
|
|
}
|
|
else if (acceptTerminatingBrace)
|
|
{
|
|
Assert(CSharpSymbolType.RightBrace);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
AcceptAndMoveNext();
|
|
}
|
|
}
|
|
|
|
private void HandleKeyword(bool topLevel, Action fallback)
|
|
{
|
|
Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword != null);
|
|
Action<bool> handler;
|
|
if (_keywordParsers.TryGetValue(CurrentSymbol.Keyword.Value, out handler))
|
|
{
|
|
handler(topLevel);
|
|
}
|
|
else
|
|
{
|
|
fallback();
|
|
}
|
|
}
|
|
|
|
private IEnumerable<CSharpSymbol> SkipToNextImportantToken()
|
|
{
|
|
while (!EndOfFile)
|
|
{
|
|
IEnumerable<CSharpSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
if (At(CSharpSymbolType.RazorCommentTransition))
|
|
{
|
|
Accept(ws);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
|
|
RazorComment();
|
|
}
|
|
else
|
|
{
|
|
return ws;
|
|
}
|
|
}
|
|
return Enumerable.Empty<CSharpSymbol>();
|
|
}
|
|
|
|
// Common code for Parsers, but FxCop REALLY doesn't like it in the base class.. moving it here for now.
|
|
protected override void OutputSpanBeforeRazorComment()
|
|
{
|
|
AddMarkerSymbolIfNecessary();
|
|
Output(SpanKind.Code);
|
|
}
|
|
|
|
protected class Block
|
|
{
|
|
public Block(string name, SourceLocation start)
|
|
{
|
|
Name = name;
|
|
Start = start;
|
|
}
|
|
|
|
public Block(CSharpSymbol symbol)
|
|
: this(GetName(symbol), symbol.Start)
|
|
{
|
|
}
|
|
|
|
public string Name { get; set; }
|
|
public SourceLocation Start { get; set; }
|
|
|
|
private static string GetName(CSharpSymbol sym)
|
|
{
|
|
if (sym.Type == CSharpSymbolType.Keyword)
|
|
{
|
|
return CSharpLanguageCharacteristics.GetKeyword(sym.Keyword.Value);
|
|
}
|
|
return sym.Content;
|
|
}
|
|
}
|
|
}
|
|
}
|