572 lines
23 KiB
C#
572 lines
23 KiB
C#
// Copyright (c) Microsoft Open Technologies, Inc. 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.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Microsoft.AspNet.Razor.Editor;
|
|
using Microsoft.AspNet.Razor.Generator;
|
|
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
|
using Microsoft.AspNet.Razor.Text;
|
|
using Microsoft.AspNet.Razor.Tokenizer.Symbols;
|
|
|
|
namespace Microsoft.AspNet.Razor.Parser
|
|
{
|
|
public partial class CSharpCodeParser
|
|
{
|
|
private void SetupDirectives()
|
|
{
|
|
MapDirectives(AddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword);
|
|
MapDirectives(InheritsDirective, SyntaxConstants.CSharp.InheritsKeyword);
|
|
MapDirectives(FunctionsDirective, SyntaxConstants.CSharp.FunctionsKeyword);
|
|
MapDirectives(SectionDirective, SyntaxConstants.CSharp.SectionKeyword);
|
|
MapDirectives(HelperDirective, SyntaxConstants.CSharp.HelperKeyword);
|
|
MapDirectives(LayoutDirective, SyntaxConstants.CSharp.LayoutKeyword);
|
|
MapDirectives(SessionStateDirective, SyntaxConstants.CSharp.SessionStateKeyword);
|
|
}
|
|
|
|
protected virtual void AddTagHelperDirective()
|
|
{
|
|
TagHelperDirective(SyntaxConstants.CSharp.AddTagHelperKeyword, (lookupText) =>
|
|
{
|
|
return new AddTagHelperCodeGenerator(lookupText);
|
|
});
|
|
}
|
|
|
|
protected virtual void LayoutDirective()
|
|
{
|
|
AssertDirective(SyntaxConstants.CSharp.LayoutKeyword);
|
|
AcceptAndMoveNext();
|
|
Context.CurrentBlock.Type = BlockType.Directive;
|
|
|
|
// Accept spaces, but not newlines
|
|
bool foundSomeWhitespace = At(CSharpSymbolType.WhiteSpace);
|
|
AcceptWhile(CSharpSymbolType.WhiteSpace);
|
|
Output(SpanKind.MetaCode, foundSomeWhitespace ? AcceptedCharacters.None : AcceptedCharacters.Any);
|
|
|
|
// First non-whitespace character starts the Layout Page, then newline ends it
|
|
AcceptUntil(CSharpSymbolType.NewLine);
|
|
Span.CodeGenerator = new SetLayoutCodeGenerator(Span.GetContent());
|
|
Span.EditHandler.EditorHints = EditorHints.LayoutPage | EditorHints.VirtualPath;
|
|
bool foundNewline = Optional(CSharpSymbolType.NewLine);
|
|
AddMarkerSymbolIfNecessary();
|
|
Output(SpanKind.MetaCode, foundNewline ? AcceptedCharacters.None : AcceptedCharacters.Any);
|
|
}
|
|
|
|
protected virtual void SessionStateDirective()
|
|
{
|
|
AssertDirective(SyntaxConstants.CSharp.SessionStateKeyword);
|
|
AcceptAndMoveNext();
|
|
|
|
SessionStateDirectiveCore();
|
|
}
|
|
|
|
protected void SessionStateDirectiveCore()
|
|
{
|
|
SessionStateTypeDirective(RazorResources.ParserEror_SessionDirectiveMissingValue, (key, value) => new RazorDirectiveAttributeCodeGenerator(key, value));
|
|
}
|
|
|
|
protected void SessionStateTypeDirective(string noValueError, Func<string, string, SpanCodeGenerator> createCodeGenerator)
|
|
{
|
|
// Set the block type
|
|
Context.CurrentBlock.Type = BlockType.Directive;
|
|
|
|
// Accept whitespace
|
|
CSharpSymbol remainingWs = AcceptSingleWhiteSpaceCharacter();
|
|
|
|
if (Span.Symbols.Count > 1)
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
|
|
Output(SpanKind.MetaCode);
|
|
|
|
if (remainingWs != null)
|
|
{
|
|
Accept(remainingWs);
|
|
}
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
|
|
// Parse a Type Name
|
|
if (!ValidSessionStateValue())
|
|
{
|
|
Context.OnError(CurrentLocation, noValueError);
|
|
}
|
|
|
|
// Pull out the type name
|
|
string sessionStateValue = String.Concat(
|
|
Span.Symbols
|
|
.Cast<CSharpSymbol>()
|
|
.Select(sym => sym.Content)).Trim();
|
|
|
|
// Set up code generation
|
|
Span.CodeGenerator = createCodeGenerator(SyntaxConstants.CSharp.SessionStateKeyword, sessionStateValue);
|
|
|
|
// Output the span and finish the block
|
|
CompleteBlock();
|
|
Output(SpanKind.Code);
|
|
}
|
|
|
|
protected virtual bool ValidSessionStateValue()
|
|
{
|
|
return Optional(CSharpSymbolType.Identifier);
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Coupling will be reviewed at a later date")]
|
|
[SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "C# Keywords are always lower-case")]
|
|
protected virtual void HelperDirective()
|
|
{
|
|
bool nested = Context.IsWithin(BlockType.Helper);
|
|
|
|
// Set the block and span type
|
|
Context.CurrentBlock.Type = BlockType.Helper;
|
|
|
|
// Verify we're on "helper" and accept
|
|
AssertDirective(SyntaxConstants.CSharp.HelperKeyword);
|
|
Block block = new Block(CurrentSymbol.Content.ToString().ToLowerInvariant(), CurrentLocation);
|
|
AcceptAndMoveNext();
|
|
|
|
if (nested)
|
|
{
|
|
Context.OnError(CurrentLocation, RazorResources.ParseError_Helpers_Cannot_Be_Nested);
|
|
}
|
|
|
|
// Accept a single whitespace character if present, if not, we should stop now
|
|
if (!At(CSharpSymbolType.WhiteSpace))
|
|
{
|
|
string error;
|
|
if (At(CSharpSymbolType.NewLine))
|
|
{
|
|
error = RazorResources.ErrorComponent_Newline;
|
|
}
|
|
else if (EndOfFile)
|
|
{
|
|
error = RazorResources.ErrorComponent_EndOfFile;
|
|
}
|
|
else
|
|
{
|
|
error = RazorResources.FormatErrorComponent_Character(CurrentSymbol.Content);
|
|
}
|
|
|
|
Context.OnError(
|
|
CurrentLocation,
|
|
RazorResources.FormatParseError_Unexpected_Character_At_Helper_Name_Start(error));
|
|
PutCurrentBack();
|
|
Output(SpanKind.MetaCode);
|
|
return;
|
|
}
|
|
|
|
CSharpSymbol remainingWs = AcceptSingleWhiteSpaceCharacter();
|
|
|
|
// Output metacode and continue
|
|
Output(SpanKind.MetaCode);
|
|
if (remainingWs != null)
|
|
{
|
|
Accept(remainingWs);
|
|
}
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); // Don't accept newlines.
|
|
|
|
// Expecting an identifier (helper name)
|
|
bool errorReported = !Required(CSharpSymbolType.Identifier, errorIfNotFound: true, errorBase: RazorResources.FormatParseError_Unexpected_Character_At_Helper_Name_Start);
|
|
if (!errorReported)
|
|
{
|
|
Assert(CSharpSymbolType.Identifier);
|
|
AcceptAndMoveNext();
|
|
}
|
|
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
|
|
// Expecting parameter list start: "("
|
|
SourceLocation bracketErrorPos = CurrentLocation;
|
|
if (!Optional(CSharpSymbolType.LeftParenthesis))
|
|
{
|
|
if (!errorReported)
|
|
{
|
|
errorReported = true;
|
|
Context.OnError(
|
|
CurrentLocation,
|
|
RazorResources.FormatParseError_MissingCharAfterHelperName("("));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SourceLocation bracketStart = CurrentLocation;
|
|
if (!Balance(BalancingModes.NoErrorOnFailure,
|
|
CSharpSymbolType.LeftParenthesis,
|
|
CSharpSymbolType.RightParenthesis,
|
|
bracketStart))
|
|
{
|
|
errorReported = true;
|
|
Context.OnError(
|
|
bracketErrorPos,
|
|
RazorResources.ParseError_UnterminatedHelperParameterList);
|
|
}
|
|
Optional(CSharpSymbolType.RightParenthesis);
|
|
}
|
|
|
|
int bookmark = CurrentLocation.AbsoluteIndex;
|
|
IEnumerable<CSharpSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
|
|
// Expecting a "{"
|
|
SourceLocation errorLocation = CurrentLocation;
|
|
bool headerComplete = At(CSharpSymbolType.LeftBrace);
|
|
if (headerComplete)
|
|
{
|
|
Accept(ws);
|
|
AcceptAndMoveNext();
|
|
}
|
|
else
|
|
{
|
|
Context.Source.Position = bookmark;
|
|
NextToken();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
if (!errorReported)
|
|
{
|
|
Context.OnError(
|
|
errorLocation,
|
|
RazorResources.FormatParseError_MissingCharAfterHelperParameters(
|
|
Language.GetSample(CSharpSymbolType.LeftBrace)));
|
|
}
|
|
}
|
|
|
|
// Grab the signature and build the code generator
|
|
AddMarkerSymbolIfNecessary();
|
|
LocationTagged<string> signature = Span.GetContent();
|
|
HelperCodeGenerator blockGen = new HelperCodeGenerator(signature, headerComplete);
|
|
Context.CurrentBlock.CodeGenerator = blockGen;
|
|
|
|
// The block will generate appropriate code,
|
|
Span.CodeGenerator = SpanCodeGenerator.Null;
|
|
|
|
if (!headerComplete)
|
|
{
|
|
CompleteBlock();
|
|
Output(SpanKind.Code);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
Output(SpanKind.Code);
|
|
}
|
|
|
|
// We're valid, so parse the nested block
|
|
AutoCompleteEditHandler bodyEditHandler = new AutoCompleteEditHandler(Language.TokenizeString);
|
|
using (PushSpanConfig(DefaultSpanConfig))
|
|
{
|
|
using (Context.StartBlock(BlockType.Statement))
|
|
{
|
|
Span.EditHandler = bodyEditHandler;
|
|
CodeBlock(false, block);
|
|
CompleteBlock(insertMarkerIfNecessary: true);
|
|
Output(SpanKind.Code);
|
|
}
|
|
}
|
|
Initialize(Span);
|
|
|
|
EnsureCurrent();
|
|
|
|
Span.CodeGenerator = SpanCodeGenerator.Null; // The block will generate the footer code.
|
|
if (!Optional(CSharpSymbolType.RightBrace))
|
|
{
|
|
// The } is missing, so set the initial signature span to use it as an autocomplete string
|
|
bodyEditHandler.AutoCompleteString = "}";
|
|
|
|
// Need to be able to accept anything to properly handle the autocomplete
|
|
bodyEditHandler.AcceptedCharacters = AcceptedCharacters.Any;
|
|
}
|
|
else
|
|
{
|
|
blockGen.Footer = Span.GetContent();
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
CompleteBlock();
|
|
Output(SpanKind.Code);
|
|
}
|
|
|
|
protected virtual void SectionDirective()
|
|
{
|
|
bool nested = Context.IsWithin(BlockType.Section);
|
|
bool errorReported = false;
|
|
|
|
// Set the block and span type
|
|
Context.CurrentBlock.Type = BlockType.Section;
|
|
|
|
// Verify we're on "section" and accept
|
|
AssertDirective(SyntaxConstants.CSharp.SectionKeyword);
|
|
AcceptAndMoveNext();
|
|
|
|
if (nested)
|
|
{
|
|
Context.OnError(CurrentLocation, RazorResources.FormatParseError_Sections_Cannot_Be_Nested(RazorResources.SectionExample_CS));
|
|
errorReported = true;
|
|
}
|
|
|
|
IEnumerable<CSharpSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false));
|
|
|
|
// Get the section name
|
|
string sectionName = String.Empty;
|
|
if (!Required(CSharpSymbolType.Identifier,
|
|
errorIfNotFound: true,
|
|
errorBase: RazorResources.FormatParseError_Unexpected_Character_At_Section_Name_Start))
|
|
{
|
|
if (!errorReported)
|
|
{
|
|
errorReported = true;
|
|
}
|
|
|
|
PutCurrentBack();
|
|
PutBack(ws);
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false));
|
|
}
|
|
else
|
|
{
|
|
Accept(ws);
|
|
sectionName = CurrentSymbol.Content;
|
|
AcceptAndMoveNext();
|
|
}
|
|
Context.CurrentBlock.CodeGenerator = new SectionCodeGenerator(sectionName);
|
|
|
|
SourceLocation errorLocation = CurrentLocation;
|
|
ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false));
|
|
|
|
// Get the starting brace
|
|
bool sawStartingBrace = At(CSharpSymbolType.LeftBrace);
|
|
if (!sawStartingBrace)
|
|
{
|
|
if (!errorReported)
|
|
{
|
|
errorReported = true;
|
|
Context.OnError(errorLocation, RazorResources.ParseError_MissingOpenBraceAfterSection);
|
|
}
|
|
|
|
PutCurrentBack();
|
|
PutBack(ws);
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false));
|
|
Optional(CSharpSymbolType.NewLine);
|
|
Output(SpanKind.MetaCode);
|
|
CompleteBlock();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Accept(ws);
|
|
}
|
|
|
|
// Set up edit handler
|
|
AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString) { AutoCompleteAtEndOfSpan = true };
|
|
|
|
Span.EditHandler = editHandler;
|
|
Span.Accept(CurrentSymbol);
|
|
|
|
// Output Metacode then switch to section parser
|
|
Output(SpanKind.MetaCode);
|
|
SectionBlock("{", "}", caseSensitive: true);
|
|
|
|
Span.CodeGenerator = SpanCodeGenerator.Null;
|
|
// Check for the terminating "}"
|
|
if (!Optional(CSharpSymbolType.RightBrace))
|
|
{
|
|
editHandler.AutoCompleteString = "}";
|
|
Context.OnError(CurrentLocation,
|
|
RazorResources.FormatParseError_Expected_X(
|
|
Language.GetSample(CSharpSymbolType.RightBrace)));
|
|
}
|
|
else
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
CompleteBlock(insertMarkerIfNecessary: false, captureWhitespaceToEndOfLine: true);
|
|
Output(SpanKind.MetaCode);
|
|
return;
|
|
}
|
|
|
|
protected virtual void FunctionsDirective()
|
|
{
|
|
// Set the block type
|
|
Context.CurrentBlock.Type = BlockType.Functions;
|
|
|
|
// Verify we're on "functions" and accept
|
|
AssertDirective(SyntaxConstants.CSharp.FunctionsKeyword);
|
|
Block block = new Block(CurrentSymbol);
|
|
AcceptAndMoveNext();
|
|
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: false));
|
|
|
|
if (!At(CSharpSymbolType.LeftBrace))
|
|
{
|
|
Context.OnError(CurrentLocation,
|
|
RazorResources.FormatParseError_Expected_X(Language.GetSample(CSharpSymbolType.LeftBrace)));
|
|
CompleteBlock();
|
|
Output(SpanKind.MetaCode);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
|
|
// Capture start point and continue
|
|
SourceLocation blockStart = CurrentLocation;
|
|
AcceptAndMoveNext();
|
|
|
|
// Output what we've seen and continue
|
|
Output(SpanKind.MetaCode);
|
|
|
|
AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
|
|
Span.EditHandler = editHandler;
|
|
|
|
Balance(BalancingModes.NoErrorOnFailure, CSharpSymbolType.LeftBrace, CSharpSymbolType.RightBrace, blockStart);
|
|
Span.CodeGenerator = new TypeMemberCodeGenerator();
|
|
if (!At(CSharpSymbolType.RightBrace))
|
|
{
|
|
editHandler.AutoCompleteString = "}";
|
|
Context.OnError(block.Start, RazorResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, "}", "{"));
|
|
CompleteBlock();
|
|
Output(SpanKind.Code);
|
|
}
|
|
else
|
|
{
|
|
Output(SpanKind.Code);
|
|
Assert(CSharpSymbolType.RightBrace);
|
|
Span.CodeGenerator = SpanCodeGenerator.Null;
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
AcceptAndMoveNext();
|
|
CompleteBlock();
|
|
Output(SpanKind.MetaCode);
|
|
}
|
|
}
|
|
|
|
protected virtual void InheritsDirective()
|
|
{
|
|
// Verify we're on the right keyword and accept
|
|
AssertDirective(SyntaxConstants.CSharp.InheritsKeyword);
|
|
AcceptAndMoveNext();
|
|
|
|
InheritsDirectiveCore();
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "directive", Justification = "This only occurs in Release builds, where this method is empty by design")]
|
|
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")]
|
|
[Conditional("DEBUG")]
|
|
protected void AssertDirective(string directive)
|
|
{
|
|
Assert(CSharpSymbolType.Identifier);
|
|
Debug.Assert(String.Equals(CurrentSymbol.Content, directive, StringComparison.Ordinal));
|
|
}
|
|
|
|
protected void InheritsDirectiveCore()
|
|
{
|
|
BaseTypeDirective(RazorResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName, baseType => new SetBaseTypeCodeGenerator(baseType));
|
|
}
|
|
|
|
protected void BaseTypeDirective(string noTypeNameError, Func<string, SpanCodeGenerator> createCodeGenerator)
|
|
{
|
|
// Set the block type
|
|
Context.CurrentBlock.Type = BlockType.Directive;
|
|
|
|
// Accept whitespace
|
|
CSharpSymbol remainingWs = AcceptSingleWhiteSpaceCharacter();
|
|
|
|
if (Span.Symbols.Count > 1)
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
|
|
Output(SpanKind.MetaCode);
|
|
|
|
if (remainingWs != null)
|
|
{
|
|
Accept(remainingWs);
|
|
}
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
|
|
if (EndOfFile || At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine))
|
|
{
|
|
Context.OnError(CurrentLocation, noTypeNameError);
|
|
}
|
|
|
|
// Parse to the end of the line
|
|
AcceptUntil(CSharpSymbolType.NewLine);
|
|
if (!Context.DesignTimeMode)
|
|
{
|
|
// We want the newline to be treated as code, but it causes issues at design-time.
|
|
Optional(CSharpSymbolType.NewLine);
|
|
}
|
|
|
|
// Pull out the type name
|
|
string baseType = Span.GetContent();
|
|
|
|
// Set up code generation
|
|
Span.CodeGenerator = createCodeGenerator(baseType.Trim());
|
|
|
|
// Output the span and finish the block
|
|
CompleteBlock();
|
|
Output(SpanKind.Code);
|
|
}
|
|
|
|
private void TagHelperDirective(string keyword, Func<string, SpanCodeGenerator> codeGeneratorBuilder)
|
|
{
|
|
AssertDirective(keyword);
|
|
|
|
// Accept the directive name
|
|
AcceptAndMoveNext();
|
|
|
|
// Set the block type
|
|
Context.CurrentBlock.Type = BlockType.Directive;
|
|
|
|
var foundWhitespace = At(CSharpSymbolType.WhiteSpace);
|
|
AcceptWhile(CSharpSymbolType.WhiteSpace);
|
|
|
|
// If we found whitespace then any content placed within the whitespace MAY cause a destructive change
|
|
// to the document. We can't accept it.
|
|
Output(SpanKind.MetaCode, foundWhitespace ? AcceptedCharacters.None : AcceptedCharacters.Any);
|
|
|
|
if (EndOfFile || At(CSharpSymbolType.NewLine))
|
|
{
|
|
Context.OnError(CurrentLocation, RazorResources.FormatParseError_DirectiveMustHaveValue(keyword));
|
|
}
|
|
else
|
|
{
|
|
// Need to grab the current location before we accept until the end of the line.
|
|
var startLocation = CurrentLocation;
|
|
|
|
// Parse to the end of the line. Essentially accepts anything until end of line, comments, invalid code
|
|
// etc.
|
|
AcceptUntil(CSharpSymbolType.NewLine);
|
|
|
|
// Pull out the value minus the spaces at the end
|
|
var rawValue = Span.GetContent().Value.TrimEnd();
|
|
var startsWithQuote = rawValue.StartsWith("\"", StringComparison.OrdinalIgnoreCase);
|
|
|
|
// If the value starts with a quote then we should generate appropriate C# code to colorize the value.
|
|
if (startsWithQuote)
|
|
{
|
|
// Set up code generation
|
|
// The generated chunk of this code generator is picked up by CSharpDesignTimeHelpersVisitor which
|
|
// renders the C# to colorize the user provided value. We trim the quotes around the user's value
|
|
// so when we render the code we can project the users value into double quotes to not invoke C#
|
|
// IntelliSense.
|
|
Span.CodeGenerator = codeGeneratorBuilder(rawValue.Trim('"'));
|
|
}
|
|
|
|
// We expect the directive to be surrounded in quotes.
|
|
// The format for taghelper directives are: @directivename "SomeValue"
|
|
if (!startsWithQuote ||
|
|
!rawValue.EndsWith("\"", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Context.OnError(startLocation,
|
|
RazorResources.FormatParseError_DirectiveMustBeSurroundedByQuotes(keyword));
|
|
}
|
|
}
|
|
|
|
// Output the span and finish the block
|
|
CompleteBlock();
|
|
Output(SpanKind.Code);
|
|
}
|
|
}
|
|
}
|