// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Web.Razor.Editor;
using System.Web.Razor.Generator;
using System.Web.Razor.Parser.SyntaxTree;
using System.Web.Razor.Resources;
using System.Web.Razor.Tokenizer;
using System.Web.Razor.Tokenizer.Symbols;
namespace System.Web.Razor.Parser
{
public partial class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer, CSharpSymbol, CSharpSymbolType>
{
internal static readonly int UsingKeywordLength = 5; // using
internal static ISet<string> DefaultKeywords = new HashSet<string>()
{
"if",
"do",
"try",
"for",
"foreach",
"while",
"switch",
"lock",
"using",
"section",
"inherits",
"helper",
"functions",
"namespace",
"class",
"layout",
"sessionstate"
};
private Dictionary<string, Action> _directiveParsers = new Dictionary<string, Action>();
private Dictionary<CSharpKeyword, Action<bool>> _keywordParsers = new Dictionary<CSharpKeyword, Action<bool>>();
public CSharpCodeParser()
{
Keywords = new HashSet<string>();
SetUpKeywords();
SetupDirectives();
}
protected internal ISet<string> Keywords { get; private set; }
public bool IsNested { get; set; }
protected override ParserBase OtherParser
{
get { return Context.MarkupParser; }
}
protected override LanguageCharacteristics<CSharpTokenizer, CSharpSymbol, CSharpSymbolType> Language
{
get { return CSharpLanguageCharacteristics.Instance; }
}
protected void MapDirectives(Action handler, params string[] directives)
{
foreach (string directive in directives)
{
_directiveParsers.Add(directive, handler);
Keywords.Add(directive);
}
}
protected bool TryGetDirectiveHandler(string directive, out Action handler)
{
return _directiveParsers.TryGetValue(directive, out handler);
}
private void MapKeywords(Action<bool> handler, params CSharpKeyword[] keywords)
{
MapKeywords(handler, topLevel: true, keywords: keywords);
}
private void MapKeywords(Action<bool> handler, bool topLevel, params CSharpKeyword[] keywords)
{
foreach (CSharpKeyword keyword in keywords)
{
_keywordParsers.Add(keyword, handler);
if (topLevel)
{
Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword));
}
}
}
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")]
[Conditional("DEBUG")]
internal void Assert(CSharpKeyword expectedKeyword)
{
Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == expectedKeyword);
}
protected internal bool At(CSharpKeyword keyword)
{
return At(CSharpSymbolType.Keyword) && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == keyword;
}
protected internal bool AcceptIf(CSharpKeyword keyword)
{
if (At(keyword))
{
AcceptAndMoveNext();
return true;
}
return false;
}
protected static Func<CSharpSymbol, bool> IsSpacingToken(bool includeNewLines, bool includeComments)
{
return sym => sym.Type == CSharpSymbolType.WhiteSpace ||
(includeNewLines && sym.Type == CSharpSymbolType.NewLine) ||
(includeComments && sym.Type == CSharpSymbolType.Comment);
}
public override void ParseBlock()
{
using (PushSpanConfig(DefaultSpanConfig))
{
if (Context == null)
{
throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
}
// Unless changed, the block is a statement block
using (Context.StartBlock(BlockType.Statement))
{
NextToken();
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
CSharpSymbol current = CurrentSymbol;
if (At(CSharpSymbolType.StringLiteral) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter)
{
Tuple<CSharpSymbol, CSharpSymbol> split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition);
current = split.Item1;
Context.Source.Position = split.Item2.Start.AbsoluteIndex;
NextToken();
}
else if (At(CSharpSymbolType.Transition))
{
NextToken();
}
// Accept "@" if we see it, but if we don't, that's OK. We assume we were started for a good reason
if (current.Type == CSharpSymbolType.Transition)
{
if (Span.Symbols.Count > 0)
{
Output(SpanKind.Code);
}
AtTransition(current);
}
else
{
// No "@" => Jump straight to AfterTransition
AfterTransition();
}
Output(SpanKind.Code);
}
}
}
private void DefaultSpanConfig(SpanBuilder span)
{
span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
span.CodeGenerator = new StatementCodeGenerator();
}
private void AtTransition(CSharpSymbol current)
{
Debug.Assert(current.Type == CSharpSymbolType.Transition);
Accept(current);
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
Span.CodeGenerator = SpanCodeGenerator.Null;
// Output the "@" span and continue here
Output(SpanKind.Transition);
AfterTransition();
}
private void AfterTransition()
{
using (PushSpanConfig(DefaultSpanConfig))
{
EnsureCurrent();
try
{
// What type of block is this?
if (!EndOfFile)
{
if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis)
{
Context.CurrentBlock.Type = BlockType.Expression;
Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
ExplicitExpression();
return;
}
else if (CurrentSymbol.Type == CSharpSymbolType.Identifier)
{
Action handler;
if (TryGetDirectiveHandler(CurrentSymbol.Content, out handler))
{
Span.CodeGenerator = SpanCodeGenerator.Null;
handler();
return;
}
else
{
Context.CurrentBlock.Type = BlockType.Expression;
Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
ImplicitExpression();
return;
}
}
else if (CurrentSymbol.Type == CSharpSymbolType.Keyword)
{
KeywordBlock(topLevel: true);
return;
}
else if (CurrentSymbol.Type == CSharpSymbolType.LeftBrace)
{
VerbatimBlock();
return;
}
}
// Invalid character
Context.CurrentBlock.Type = BlockType.Expression;
Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
AddMarkerSymbolIfNecessary();
Span.CodeGenerator = new ExpressionCodeGenerator();
Span.EditHandler = new ImplicitExpressionEditHandler(
Language.TokenizeString,
DefaultKeywords,
acceptTrailingDot: IsNested)
{
AcceptedCharacters = AcceptedCharacters.NonWhiteSpace
};
if (At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine))
{
Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS);
}
else if (EndOfFile)
{
Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock);
}
else
{
Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, CurrentSymbol.Content);
}
}
finally
{
// Always put current character back in the buffer for the next parser.
PutCurrentBack();
}
}
}
private void VerbatimBlock()
{
Assert(CSharpSymbolType.LeftBrace);
Block block = new Block(RazorResources.BlockName_Code, CurrentLocation);
AcceptAndMoveNext();
// Set up the "{" span and output
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
Span.CodeGenerator = SpanCodeGenerator.Null;
Output(SpanKind.MetaCode);
// Set up auto-complete and parse the code block
AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
Span.EditHandler = editHandler;
CodeBlock(false, block);
Span.CodeGenerator = new StatementCodeGenerator();
AddMarkerSymbolIfNecessary();
if (!At(CSharpSymbolType.RightBrace))
{
editHandler.AutoCompleteString = "}";
}
Output(SpanKind.Code);
if (Optional(CSharpSymbolType.RightBrace))
{
// Set up the "}" span
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
Span.CodeGenerator = SpanCodeGenerator.Null;
}
if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine))
{
PutCurrentBack();
}
CompleteBlock(insertMarkerIfNecessary: false);
Output(SpanKind.MetaCode);
}
private void ImplicitExpression()
{
Context.CurrentBlock.Type = BlockType.Expression;
Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
using (PushSpanConfig(span =>
{
span.EditHandler = new ImplicitExpressionEditHandler(Language.TokenizeString, Keywords, acceptTrailingDot: IsNested);
span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace;
span.CodeGenerator = new ExpressionCodeGenerator();
}))
{
do
{
if (AtIdentifier(allowKeywords: true))
{
AcceptAndMoveNext();
}
}
while (MethodCallOrArrayIndex());
PutCurrentBack();
Output(SpanKind.Code);
}
}
private bool MethodCallOrArrayIndex()
{
if (!EndOfFile)
{
if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis || CurrentSymbol.Type == CSharpSymbolType.LeftBracket)
{
// If we end within "(", whitespace is fine
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
CSharpSymbolType right;
bool success;
using (PushSpanConfig((span, prev) =>
{
prev(span);
span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
}))
{
right = Language.FlipBracket(CurrentSymbol.Type);
success = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates);
}
if (!success)
{
AcceptUntil(CSharpSymbolType.LessThan);
}
if (At(right))
{
AcceptAndMoveNext();
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace;
}
return MethodCallOrArrayIndex();
}
if (CurrentSymbol.Type == CSharpSymbolType.Dot)
{
CSharpSymbol dot = CurrentSymbol;
if (NextToken())
{
if (At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword))
{
// Accept the dot and return to the start
Accept(dot);
return true; // continue
}
else
{
// Put the symbol back
PutCurrentBack();
}
}
if (!IsNested)
{
// Put the "." back
PutBack(dot);
}
else
{
Accept(dot);
}
}
else if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine))
{
PutCurrentBack();
}
}
// Implicit Expression is complete
return false;
}
private void CompleteBlock()
{
CompleteBlock(insertMarkerIfNecessary: true);
}
private void CompleteBlock(bool insertMarkerIfNecessary)
{
CompleteBlock(insertMarkerIfNecessary, captureWhitespaceToEndOfLine: insertMarkerIfNecessary);
}
private void CompleteBlock(bool insertMarkerIfNecessary, bool captureWhitespaceToEndOfLine)
{
if (insertMarkerIfNecessary && Context.LastAcceptedCharacters != AcceptedCharacters.Any)
{
AddMarkerSymbolIfNecessary();
}
EnsureCurrent();
// Read whitespace, but not newlines
// If we're not inserting a marker span, we don't need to capture whitespace
if (!Context.WhiteSpaceIsSignificantToAncestorBlock &&
Context.CurrentBlock.Type != BlockType.Expression &&
captureWhitespaceToEndOfLine &&
!Context.DesignTimeMode &&
!IsNested)
{
CaptureWhitespaceAtEndOfCodeOnlyLine();
}
else
{
PutCurrentBack();
}
}
private void CaptureWhitespaceAtEndOfCodeOnlyLine()
{
IEnumerable<CSharpSymbol> ws = ReadWhile(sym => sym.Type == CSharpSymbolType.WhiteSpace);
if (At(CSharpSymbolType.NewLine))
{
Accept(ws);
AcceptAndMoveNext();
PutCurrentBack();
}
else
{
PutCurrentBack();
PutBack(ws);
}
}
private void ConfigureExplicitExpressionSpan(SpanBuilder sb)
{
sb.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
sb.CodeGenerator = new ExpressionCodeGenerator();
}
private void ExplicitExpression()
{
Block block = new Block(RazorResources.BlockName_ExplicitExpression, CurrentLocation);
Assert(CSharpSymbolType.LeftParenthesis);
AcceptAndMoveNext();
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
Span.CodeGenerator = SpanCodeGenerator.Null;
Output(SpanKind.MetaCode);
using (PushSpanConfig(ConfigureExplicitExpressionSpan))
{
bool success = Balance(
BalancingModes.BacktrackOnFailure | BalancingModes.NoErrorOnFailure | BalancingModes.AllowCommentsAndTemplates,
CSharpSymbolType.LeftParenthesis,
CSharpSymbolType.RightParenthesis,
block.Start);
if (!success)
{
AcceptUntil(CSharpSymbolType.LessThan);
Context.OnError(block.Start, RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, block.Name, ")", "(");
}
// If necessary, put an empty-content marker symbol here
if (Span.Symbols.Count == 0)
{
Accept(new CSharpSymbol(CurrentLocation, String.Empty, CSharpSymbolType.Unknown));
}
// Output the content span and then capture the ")"
Output(SpanKind.Code);
}
Optional(CSharpSymbolType.RightParenthesis);
if (!EndOfFile)
{
PutCurrentBack();
}
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
Span.CodeGenerator = SpanCodeGenerator.Null;
CompleteBlock(insertMarkerIfNecessary: false);
Output(SpanKind.MetaCode);
}
private void Template()
{
if (Context.IsWithin(BlockType.Template))
{
Context.OnError(CurrentLocation, RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested);
}
Output(SpanKind.Code);
using (Context.StartBlock(BlockType.Template))
{
Context.CurrentBlock.CodeGenerator = new TemplateBlockCodeGenerator();
PutCurrentBack();
OtherParserBlock();
}
}
private void OtherParserBlock()
{
ParseWithOtherParser(p => p.ParseBlock());
}
private void SectionBlock(string left, string right, bool caseSensitive)
{
ParseWithOtherParser(p => p.ParseSection(Tuple.Create(left, right), caseSensitive));
}
private void NestedBlock()
{
Output(SpanKind.Code);
bool wasNested = IsNested;
IsNested = true;
using (PushSpanConfig())
{
ParseBlock();
}
Initialize(Span);
IsNested = wasNested;
NextToken();
}
protected override bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions)
{
// No embedded transitions in C#, so ignore that param
return allowTemplatesAndComments
&& ((Language.IsTransition(CurrentSymbol)
&& NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon))
|| Language.IsCommentStart(CurrentSymbol));
}
protected override void HandleEmbeddedTransition()
{
if (Language.IsTransition(CurrentSymbol))
{
PutCurrentBack();
Template();
}
else if (Language.IsCommentStart(CurrentSymbol))
{
RazorComment();
}
}
private void ParseWithOtherParser(Action<ParserBase> parseAction)
{
using (PushSpanConfig())
{
Context.SwitchActiveParser();
parseAction(Context.MarkupParser);
Context.SwitchActiveParser();
}
Initialize(Span);
NextToken();
}
}
}