// 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(); } } }