2281 lines
86 KiB
C#
2281 lines
86 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;
|
|
|
|
namespace Microsoft.AspNetCore.Razor.Language.Legacy
|
|
{
|
|
internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer, CSharpSymbol, CSharpSymbolType>
|
|
{
|
|
private static HashSet<char> InvalidNonWhitespaceNameCharacters = new HashSet<char>(new[]
|
|
{
|
|
'@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*'
|
|
});
|
|
|
|
private static readonly Func<CSharpSymbol, bool> IsValidStatementSpacingSymbol =
|
|
IsSpacingToken(includeNewLines: true, includeComments: true);
|
|
|
|
internal static readonly DirectiveDescriptor AddTagHelperDirectiveDescriptor = DirectiveDescriptor.CreateDirective(
|
|
SyntaxConstants.CSharp.AddTagHelperKeyword,
|
|
DirectiveKind.SingleLine,
|
|
builder =>
|
|
{
|
|
builder.AddStringToken(Resources.AddTagHelperDirective_StringToken_Name, Resources.AddTagHelperDirective_StringToken_Description);
|
|
builder.Description = Resources.AddTagHelperDirective_Description;
|
|
});
|
|
|
|
internal static readonly DirectiveDescriptor RemoveTagHelperDirectiveDescriptor = DirectiveDescriptor.CreateDirective(
|
|
SyntaxConstants.CSharp.RemoveTagHelperKeyword,
|
|
DirectiveKind.SingleLine,
|
|
builder =>
|
|
{
|
|
builder.AddStringToken(Resources.RemoveTagHelperDirective_StringToken_Name, Resources.RemoveTagHelperDirective_StringToken_Description);
|
|
builder.Description = Resources.RemoveTagHelperDirective_Description;
|
|
});
|
|
|
|
internal static readonly DirectiveDescriptor TagHelperPrefixDirectiveDescriptor = DirectiveDescriptor.CreateDirective(
|
|
SyntaxConstants.CSharp.TagHelperPrefixKeyword,
|
|
DirectiveKind.SingleLine,
|
|
builder =>
|
|
{
|
|
builder.AddStringToken(Resources.TagHelperPrefixDirective_PrefixToken_Name, Resources.TagHelperPrefixDirective_PrefixToken_Description);
|
|
builder.Description = Resources.TagHelperPrefixDirective_Description;
|
|
});
|
|
|
|
internal static readonly IEnumerable<DirectiveDescriptor> DefaultDirectiveDescriptors = new DirectiveDescriptor[]
|
|
{
|
|
};
|
|
|
|
internal static ISet<string> DefaultKeywords = new HashSet<string>()
|
|
{
|
|
SyntaxConstants.CSharp.TagHelperPrefixKeyword,
|
|
SyntaxConstants.CSharp.AddTagHelperKeyword,
|
|
SyntaxConstants.CSharp.RemoveTagHelperKeyword,
|
|
"if",
|
|
"do",
|
|
"try",
|
|
"for",
|
|
"foreach",
|
|
"while",
|
|
"switch",
|
|
"lock",
|
|
"using",
|
|
"namespace",
|
|
"class",
|
|
};
|
|
|
|
private readonly ISet<string> CurrentKeywords = new HashSet<string>(DefaultKeywords);
|
|
|
|
private Dictionary<string, Action> _directiveParsers = new Dictionary<string, Action>(StringComparer.Ordinal);
|
|
private Dictionary<CSharpKeyword, Action<bool>> _keywordParsers = new Dictionary<CSharpKeyword, Action<bool>>();
|
|
|
|
public CSharpCodeParser(ParserContext context)
|
|
: this(directives: Enumerable.Empty<DirectiveDescriptor>(), context: context)
|
|
{
|
|
}
|
|
|
|
public CSharpCodeParser(IEnumerable<DirectiveDescriptor> directives, ParserContext context)
|
|
: base(context.ParseLeadingDirectives ? FirstDirectiveCSharpLanguageCharacteristics.Instance : CSharpLanguageCharacteristics.Instance, context)
|
|
{
|
|
if (directives == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(directives));
|
|
}
|
|
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
Keywords = new HashSet<string>();
|
|
SetUpKeywords();
|
|
SetupDirectives(directives);
|
|
SetUpExpressions();
|
|
}
|
|
|
|
public HtmlMarkupParser HtmlParser { get; set; }
|
|
|
|
protected internal ISet<string> Keywords { get; private set; }
|
|
|
|
public bool IsNested { get; set; }
|
|
|
|
protected override bool SymbolTypeEquals(CSharpSymbolType x, CSharpSymbolType y) => x == y;
|
|
|
|
protected void MapDirectives(Action handler, params string[] directives)
|
|
{
|
|
foreach (var directive in directives)
|
|
{
|
|
_directiveParsers.Add(directive, () =>
|
|
{
|
|
handler();
|
|
Context.SeenDirectives.Add(directive);
|
|
});
|
|
|
|
Keywords.Add(directive);
|
|
|
|
// These C# keywords are reserved for use in directives. It's an error to use them outside of
|
|
// a directive. This code removes the error generation if the directive *is* registered.
|
|
if (string.Equals(directive, "class", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_keywordParsers.Remove(CSharpKeyword.Class);
|
|
}
|
|
else if (string.Equals(directive, "namespace", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_keywordParsers.Remove(CSharpKeyword.Namespace);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected bool TryGetDirectiveHandler(string directive, out Action handler)
|
|
{
|
|
return _directiveParsers.TryGetValue(directive, out handler);
|
|
}
|
|
|
|
private void MapExpressionKeyword(Action<bool> handler, CSharpKeyword keyword)
|
|
{
|
|
_keywordParsers.Add(keyword, handler);
|
|
|
|
// Expression keywords don't belong in the regular keyword list
|
|
}
|
|
|
|
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 (var keyword in keywords)
|
|
{
|
|
_keywordParsers.Add(keyword, handler);
|
|
if (topLevel)
|
|
{
|
|
Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword));
|
|
}
|
|
}
|
|
}
|
|
|
|
[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(LegacyResources.Parser_Context_Not_Set);
|
|
}
|
|
|
|
Span.Start = CurrentLocation;
|
|
|
|
// Unless changed, the block is a statement block
|
|
using (Context.Builder.StartBlock(BlockKindInternal.Statement))
|
|
{
|
|
NextToken();
|
|
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
|
|
var current = CurrentSymbol;
|
|
if (At(CSharpSymbolType.StringLiteral) &&
|
|
CurrentSymbol.Content.Length > 0 &&
|
|
CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter)
|
|
{
|
|
var split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition);
|
|
current = split.Item1;
|
|
|
|
// Back up to the end of the transition
|
|
Context.Source.Position -= split.Item2.Content.Length;
|
|
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(SpanKindInternal.Code);
|
|
}
|
|
AtTransition(current);
|
|
}
|
|
else
|
|
{
|
|
// No "@" => Jump straight to AfterTransition
|
|
AfterTransition();
|
|
}
|
|
|
|
Output(SpanKindInternal.Code);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DefaultSpanConfig(SpanBuilder span)
|
|
{
|
|
span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
|
|
span.ChunkGenerator = new StatementChunkGenerator();
|
|
}
|
|
|
|
private void AtTransition(CSharpSymbol current)
|
|
{
|
|
Debug.Assert(current.Type == CSharpSymbolType.Transition);
|
|
Accept(current);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
|
|
// Output the "@" span and continue here
|
|
Output(SpanKindInternal.Transition);
|
|
AfterTransition();
|
|
}
|
|
|
|
private void AfterTransition()
|
|
{
|
|
using (PushSpanConfig(DefaultSpanConfig))
|
|
{
|
|
EnsureCurrent();
|
|
try
|
|
{
|
|
// What type of block is this?
|
|
if (!EndOfFile)
|
|
{
|
|
if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis)
|
|
{
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression;
|
|
Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator();
|
|
ExplicitExpression();
|
|
return;
|
|
}
|
|
else if (CurrentSymbol.Type == CSharpSymbolType.Identifier)
|
|
{
|
|
if (TryGetDirectiveHandler(CurrentSymbol.Content, out var handler))
|
|
{
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
handler();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (string.Equals(
|
|
CurrentSymbol.Content,
|
|
SyntaxConstants.CSharp.HelperKeyword,
|
|
StringComparison.Ordinal))
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatParseError_HelperDirectiveNotAvailable(
|
|
SyntaxConstants.CSharp.HelperKeyword),
|
|
CurrentSymbol.Content.Length);
|
|
}
|
|
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression;
|
|
Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator();
|
|
ImplicitExpression();
|
|
return;
|
|
}
|
|
}
|
|
else if (CurrentSymbol.Type == CSharpSymbolType.Keyword)
|
|
{
|
|
if (TryGetDirectiveHandler(CurrentSymbol.Content, out var handler))
|
|
{
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
handler();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
KeywordBlock(topLevel: true);
|
|
return;
|
|
}
|
|
}
|
|
else if (CurrentSymbol.Type == CSharpSymbolType.LeftBrace)
|
|
{
|
|
VerbatimBlock();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Invalid character
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression;
|
|
Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator();
|
|
AddMarkerSymbolIfNecessary();
|
|
Span.ChunkGenerator = new ExpressionChunkGenerator();
|
|
Span.EditHandler = new ImplicitExpressionEditHandler(
|
|
Language.TokenizeString,
|
|
CurrentKeywords,
|
|
acceptTrailingDot: IsNested)
|
|
{
|
|
AcceptedCharacters = AcceptedCharactersInternal.NonWhiteSpace
|
|
};
|
|
if (At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine))
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS,
|
|
CurrentSymbol.Content.Length);
|
|
}
|
|
else if (EndOfFile)
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock,
|
|
length: 1 /* end of file */);
|
|
}
|
|
else
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS(
|
|
CurrentSymbol.Content),
|
|
CurrentSymbol.Content.Length);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// Always put current character back in the buffer for the next parser.
|
|
PutCurrentBack();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void VerbatimBlock()
|
|
{
|
|
Assert(CSharpSymbolType.LeftBrace);
|
|
var block = new Block(LegacyResources.BlockName_Code, CurrentStart);
|
|
AcceptAndMoveNext();
|
|
|
|
// Set up the "{" span and output
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.MetaCode);
|
|
|
|
// Set up auto-complete and parse the code block
|
|
var editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
|
|
Span.EditHandler = editHandler;
|
|
CodeBlock(false, block);
|
|
|
|
Span.ChunkGenerator = new StatementChunkGenerator();
|
|
AddMarkerSymbolIfNecessary();
|
|
if (!At(CSharpSymbolType.RightBrace))
|
|
{
|
|
editHandler.AutoCompleteString = "}";
|
|
}
|
|
Output(SpanKindInternal.Code);
|
|
|
|
if (Optional(CSharpSymbolType.RightBrace))
|
|
{
|
|
// Set up the "}" span
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
}
|
|
|
|
if (!IsNested)
|
|
{
|
|
EnsureCurrent();
|
|
if (At(CSharpSymbolType.NewLine) ||
|
|
(At(CSharpSymbolType.WhiteSpace) && NextIs(CSharpSymbolType.NewLine)))
|
|
{
|
|
Context.NullGenerateWhitespaceAndNewLine = true;
|
|
}
|
|
}
|
|
|
|
Output(SpanKindInternal.MetaCode);
|
|
}
|
|
|
|
private void ImplicitExpression()
|
|
{
|
|
ImplicitExpression(AcceptedCharactersInternal.NonWhiteSpace);
|
|
}
|
|
|
|
// Async implicit expressions include the "await" keyword and therefore need to allow spaces to
|
|
// separate the "await" and the following code.
|
|
private void AsyncImplicitExpression()
|
|
{
|
|
ImplicitExpression(AcceptedCharactersInternal.AnyExceptNewline);
|
|
}
|
|
|
|
private void ImplicitExpression(AcceptedCharactersInternal acceptedCharacters)
|
|
{
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression;
|
|
Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator();
|
|
|
|
using (PushSpanConfig(span =>
|
|
{
|
|
span.EditHandler = new ImplicitExpressionEditHandler(
|
|
Language.TokenizeString,
|
|
Keywords,
|
|
acceptTrailingDot: IsNested);
|
|
span.EditHandler.AcceptedCharacters = acceptedCharacters;
|
|
span.ChunkGenerator = new ExpressionChunkGenerator();
|
|
}))
|
|
{
|
|
do
|
|
{
|
|
if (AtIdentifier(allowKeywords: true))
|
|
{
|
|
AcceptAndMoveNext();
|
|
}
|
|
}
|
|
while (MethodCallOrArrayIndex(acceptedCharacters));
|
|
|
|
PutCurrentBack();
|
|
Output(SpanKindInternal.Code);
|
|
}
|
|
}
|
|
|
|
private bool MethodCallOrArrayIndex(AcceptedCharactersInternal acceptedCharacters)
|
|
{
|
|
if (!EndOfFile)
|
|
{
|
|
if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis ||
|
|
CurrentSymbol.Type == CSharpSymbolType.LeftBracket)
|
|
{
|
|
// If we end within "(", whitespace is fine
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any;
|
|
|
|
CSharpSymbolType right;
|
|
bool success;
|
|
|
|
using (PushSpanConfig((span, prev) =>
|
|
{
|
|
prev(span);
|
|
span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any;
|
|
}))
|
|
{
|
|
right = Language.FlipBracket(CurrentSymbol.Type);
|
|
success = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates);
|
|
}
|
|
|
|
if (!success)
|
|
{
|
|
AcceptUntil(CSharpSymbolType.LessThan);
|
|
}
|
|
if (At(right))
|
|
{
|
|
AcceptAndMoveNext();
|
|
|
|
// At the ending brace, restore the initial accepted characters.
|
|
Span.EditHandler.AcceptedCharacters = acceptedCharacters;
|
|
}
|
|
return MethodCallOrArrayIndex(acceptedCharacters);
|
|
}
|
|
if (At(CSharpSymbolType.QuestionMark))
|
|
{
|
|
var next = Lookahead(count: 1);
|
|
|
|
if (next != null)
|
|
{
|
|
if (next.Type == CSharpSymbolType.Dot)
|
|
{
|
|
// Accept null conditional dot operator (?.).
|
|
AcceptAndMoveNext();
|
|
AcceptAndMoveNext();
|
|
|
|
// If the next piece after the ?. is a keyword or identifier then we want to continue.
|
|
return At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword);
|
|
}
|
|
else if (next.Type == CSharpSymbolType.LeftBracket)
|
|
{
|
|
// We're at the ? for a null conditional bracket operator (?[).
|
|
AcceptAndMoveNext();
|
|
|
|
// Accept the [ and any content inside (it will attempt to balance).
|
|
return MethodCallOrArrayIndex(acceptedCharacters);
|
|
}
|
|
}
|
|
}
|
|
else if (At(CSharpSymbolType.Dot))
|
|
{
|
|
var 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;
|
|
}
|
|
|
|
protected void CompleteBlock()
|
|
{
|
|
CompleteBlock(insertMarkerIfNecessary: true);
|
|
}
|
|
|
|
protected void CompleteBlock(bool insertMarkerIfNecessary)
|
|
{
|
|
CompleteBlock(insertMarkerIfNecessary, captureWhitespaceToEndOfLine: insertMarkerIfNecessary);
|
|
}
|
|
|
|
protected void CompleteBlock(bool insertMarkerIfNecessary, bool captureWhitespaceToEndOfLine)
|
|
{
|
|
if (insertMarkerIfNecessary && Context.Builder.LastAcceptedCharacters != AcceptedCharactersInternal.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.Builder.CurrentBlock.Type != BlockKindInternal.Expression &&
|
|
captureWhitespaceToEndOfLine &&
|
|
!Context.DesignTimeMode &&
|
|
!IsNested)
|
|
{
|
|
CaptureWhitespaceAtEndOfCodeOnlyLine();
|
|
}
|
|
else
|
|
{
|
|
PutCurrentBack();
|
|
}
|
|
}
|
|
|
|
private void CaptureWhitespaceAtEndOfCodeOnlyLine()
|
|
{
|
|
var whitespace = ReadWhile(sym => sym.Type == CSharpSymbolType.WhiteSpace);
|
|
if (At(CSharpSymbolType.NewLine))
|
|
{
|
|
Accept(whitespace);
|
|
AcceptAndMoveNext();
|
|
PutCurrentBack();
|
|
}
|
|
else
|
|
{
|
|
PutCurrentBack();
|
|
PutBack(whitespace);
|
|
}
|
|
}
|
|
|
|
private void ConfigureExplicitExpressionSpan(SpanBuilder sb)
|
|
{
|
|
sb.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
|
|
sb.ChunkGenerator = new ExpressionChunkGenerator();
|
|
}
|
|
|
|
private void ExplicitExpression()
|
|
{
|
|
var block = new Block(LegacyResources.BlockName_ExplicitExpression, CurrentStart);
|
|
Assert(CSharpSymbolType.LeftParenthesis);
|
|
AcceptAndMoveNext();
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.MetaCode);
|
|
using (PushSpanConfig(ConfigureExplicitExpressionSpan))
|
|
{
|
|
var success = Balance(
|
|
BalancingModes.BacktrackOnFailure |
|
|
BalancingModes.NoErrorOnFailure |
|
|
BalancingModes.AllowCommentsAndTemplates,
|
|
CSharpSymbolType.LeftParenthesis,
|
|
CSharpSymbolType.RightParenthesis,
|
|
block.Start);
|
|
|
|
if (!success)
|
|
{
|
|
AcceptUntil(CSharpSymbolType.LessThan);
|
|
Context.ErrorSink.OnError(
|
|
block.Start,
|
|
LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, ")", "("),
|
|
length: 1 /* ( */);
|
|
}
|
|
|
|
// If necessary, put an empty-content marker symbol here
|
|
if (Span.Symbols.Count == 0)
|
|
{
|
|
Accept(new CSharpSymbol(string.Empty, CSharpSymbolType.Unknown));
|
|
}
|
|
|
|
// Output the content span and then capture the ")"
|
|
Output(SpanKindInternal.Code);
|
|
}
|
|
Optional(CSharpSymbolType.RightParenthesis);
|
|
if (!EndOfFile)
|
|
{
|
|
PutCurrentBack();
|
|
}
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
CompleteBlock(insertMarkerIfNecessary: false);
|
|
Output(SpanKindInternal.MetaCode);
|
|
}
|
|
|
|
private void Template()
|
|
{
|
|
if (Context.Builder.ActiveBlocks.Any(block => block.Type == BlockKindInternal.Template))
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested,
|
|
length: 1 /* @ */);
|
|
}
|
|
Output(SpanKindInternal.Code);
|
|
using (Context.Builder.StartBlock(BlockKindInternal.Template))
|
|
{
|
|
Context.Builder.CurrentBlock.ChunkGenerator = new TemplateBlockChunkGenerator();
|
|
PutCurrentBack();
|
|
OtherParserBlock();
|
|
}
|
|
}
|
|
|
|
private void OtherParserBlock()
|
|
{
|
|
ParseWithOtherParser(p => p.ParseBlock());
|
|
}
|
|
|
|
private void SectionBlock(string left, string right, bool caseSensitive)
|
|
{
|
|
ParseWithOtherParser(p => p.ParseRazorBlock(Tuple.Create(left, right), caseSensitive));
|
|
}
|
|
|
|
private void NestedBlock()
|
|
{
|
|
Output(SpanKindInternal.Code);
|
|
|
|
var wasNested = IsNested;
|
|
IsNested = true;
|
|
using (PushSpanConfig())
|
|
{
|
|
ParseBlock();
|
|
}
|
|
|
|
Span.Start = CurrentLocation;
|
|
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, CSharpSymbolType.DoubleColon))
|
|
|| Language.IsCommentStart(CurrentSymbol));
|
|
}
|
|
|
|
protected override void HandleEmbeddedTransition()
|
|
{
|
|
if (Language.IsTransition(CurrentSymbol))
|
|
{
|
|
PutCurrentBack();
|
|
Template();
|
|
}
|
|
else if (Language.IsCommentStart(CurrentSymbol))
|
|
{
|
|
RazorComment();
|
|
}
|
|
}
|
|
|
|
private void ParseWithOtherParser(Action<HtmlMarkupParser> parseAction)
|
|
{
|
|
// When transitioning to the HTML parser we no longer want to act as if we're in a nested C# state.
|
|
// For instance, if <div>@hello.</div> is in a nested C# block we don't want the trailing '.' to be handled
|
|
// as C#; it should be handled as a period because it's wrapped in markup.
|
|
var wasNested = IsNested;
|
|
IsNested = false;
|
|
|
|
using (PushSpanConfig())
|
|
{
|
|
parseAction(HtmlParser);
|
|
}
|
|
|
|
Span.Start = CurrentLocation;
|
|
Initialize(Span);
|
|
|
|
IsNested = wasNested;
|
|
|
|
NextToken();
|
|
}
|
|
|
|
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.Class, CSharpKeyword.Namespace);
|
|
}
|
|
|
|
protected virtual void ReservedDirective(bool topLevel)
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatParseError_ReservedWord(CurrentSymbol.Content),
|
|
CurrentSymbol.Content.Length);
|
|
AcceptAndMoveNext();
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive;
|
|
CompleteBlock();
|
|
Output(SpanKindInternal.MetaCode);
|
|
}
|
|
|
|
private void KeywordBlock(bool topLevel)
|
|
{
|
|
HandleKeyword(topLevel, () =>
|
|
{
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression;
|
|
Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator();
|
|
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 = AcceptedCharactersInternal.Any;
|
|
var whitespace = SkipToNextImportantToken();
|
|
|
|
if (At(CSharpKeyword.While))
|
|
{
|
|
Accept(whitespace);
|
|
Assert(CSharpKeyword.While);
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
if (AcceptCondition() && Optional(CSharpSymbolType.Semicolon))
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PutCurrentBack();
|
|
PutBack(whitespace);
|
|
}
|
|
}
|
|
|
|
private void UsingKeyword(bool topLevel)
|
|
{
|
|
Assert(CSharpKeyword.Using);
|
|
var block = new Block(CurrentSymbol, CurrentStart);
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
|
|
if (At(CSharpSymbolType.LeftParenthesis))
|
|
{
|
|
// using ( ==> Using Statement
|
|
UsingStatement(block);
|
|
}
|
|
else if (At(CSharpSymbolType.Identifier) || At(CSharpKeyword.Static))
|
|
{
|
|
// using Identifier ==> Using Declaration
|
|
if (!topLevel)
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
block.Start,
|
|
LegacyResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock,
|
|
block.Name.Length);
|
|
StandardStatement();
|
|
}
|
|
else
|
|
{
|
|
UsingDeclaration();
|
|
}
|
|
}
|
|
|
|
if (topLevel)
|
|
{
|
|
CompleteBlock();
|
|
}
|
|
}
|
|
|
|
private void UsingDeclaration()
|
|
{
|
|
// Set block type to directive
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive;
|
|
|
|
var start = CurrentStart;
|
|
if (At(CSharpSymbolType.Identifier))
|
|
{
|
|
// non-static using
|
|
NamespaceOrTypeName();
|
|
var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
if (At(CSharpSymbolType.Assign))
|
|
{
|
|
// Alias
|
|
Accept(whitespace);
|
|
Assert(CSharpSymbolType.Assign);
|
|
AcceptAndMoveNext();
|
|
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
|
|
// One more namespace or type name
|
|
NamespaceOrTypeName();
|
|
}
|
|
else
|
|
{
|
|
PutCurrentBack();
|
|
PutBack(whitespace);
|
|
}
|
|
}
|
|
else if (At(CSharpKeyword.Static))
|
|
{
|
|
// static using
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
NamespaceOrTypeName();
|
|
}
|
|
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.AnyExceptNewline;
|
|
Span.ChunkGenerator = new AddImportChunkGenerator(new LocationTagged<string>(
|
|
string.Concat(Span.Symbols.Skip(1).Select(s => s.Content)),
|
|
start));
|
|
|
|
// Optional ";"
|
|
if (EnsureCurrent())
|
|
{
|
|
Optional(CSharpSymbolType.Semicolon);
|
|
}
|
|
}
|
|
|
|
// Used for parsing a qualified name like that which follows the `namespace` keyword.
|
|
//
|
|
// qualified-identifier:
|
|
// identifier
|
|
// qualified-identifier . identifier
|
|
protected bool QualifiedIdentifier(out int identifierLength)
|
|
{
|
|
var currentIdentifierLength = 0;
|
|
var expectingDot = false;
|
|
var tokens = ReadWhile(token =>
|
|
{
|
|
var type = token.Type;
|
|
if ((expectingDot && type == CSharpSymbolType.Dot) ||
|
|
(!expectingDot && type == CSharpSymbolType.Identifier))
|
|
{
|
|
expectingDot = !expectingDot;
|
|
return true;
|
|
}
|
|
|
|
if (type != CSharpSymbolType.WhiteSpace &&
|
|
type != CSharpSymbolType.NewLine)
|
|
{
|
|
expectingDot = false;
|
|
currentIdentifierLength += token.Content.Length;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
identifierLength = currentIdentifierLength;
|
|
var validQualifiedIdentifier = expectingDot;
|
|
if (validQualifiedIdentifier)
|
|
{
|
|
foreach (var token in tokens)
|
|
{
|
|
identifierLength += token.Content.Length;
|
|
Accept(token);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
PutCurrentBack();
|
|
|
|
foreach (var token in tokens)
|
|
{
|
|
identifierLength += token.Content.Length;
|
|
PutBack(token);
|
|
}
|
|
|
|
EnsureCurrent();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected bool NamespaceOrTypeName()
|
|
{
|
|
if (Optional(CSharpSymbolType.LeftParenthesis))
|
|
{
|
|
while (!Optional(CSharpSymbolType.RightParenthesis) && !EndOfFile)
|
|
{
|
|
Optional(CSharpSymbolType.WhiteSpace);
|
|
|
|
if (!NamespaceOrTypeName())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Optional(CSharpSymbolType.WhiteSpace);
|
|
Optional(CSharpSymbolType.Identifier);
|
|
Optional(CSharpSymbolType.WhiteSpace);
|
|
Optional(CSharpSymbolType.Comma);
|
|
}
|
|
|
|
if (At(CSharpSymbolType.WhiteSpace) && NextIs(CSharpSymbolType.QuestionMark))
|
|
{
|
|
// Only accept the whitespace if we are going to consume the next token.
|
|
AcceptAndMoveNext();
|
|
}
|
|
|
|
Optional(CSharpSymbolType.QuestionMark); // Nullable
|
|
|
|
return true;
|
|
}
|
|
else if (Optional(CSharpSymbolType.Identifier) || Optional(CSharpSymbolType.Keyword))
|
|
{
|
|
if (Optional(CSharpSymbolType.DoubleColon))
|
|
{
|
|
if (!Optional(CSharpSymbolType.Identifier))
|
|
{
|
|
Optional(CSharpSymbolType.Keyword);
|
|
}
|
|
}
|
|
if (At(CSharpSymbolType.LessThan))
|
|
{
|
|
TypeArgumentList();
|
|
}
|
|
if (Optional(CSharpSymbolType.Dot))
|
|
{
|
|
NamespaceOrTypeName();
|
|
}
|
|
|
|
if (At(CSharpSymbolType.WhiteSpace) && NextIs(CSharpSymbolType.QuestionMark))
|
|
{
|
|
// Only accept the whitespace if we are going to consume the next token.
|
|
AcceptAndMoveNext();
|
|
}
|
|
|
|
Optional(CSharpSymbolType.QuestionMark); // Nullable
|
|
|
|
if (At(CSharpSymbolType.WhiteSpace) && NextIs(CSharpSymbolType.LeftBracket))
|
|
{
|
|
// Only accept the whitespace if we are going to consume the next token.
|
|
AcceptAndMoveNext();
|
|
}
|
|
|
|
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
|
|
var whitespace = SkipToNextImportantToken();
|
|
|
|
// Check for a catch or finally part
|
|
if (At(CSharpKeyword.Catch))
|
|
{
|
|
Accept(whitespace);
|
|
Assert(CSharpKeyword.Catch);
|
|
FilterableCatchBlock();
|
|
AfterTryClause();
|
|
}
|
|
else if (At(CSharpKeyword.Finally))
|
|
{
|
|
Accept(whitespace);
|
|
Assert(CSharpKeyword.Finally);
|
|
UnconditionalBlock();
|
|
}
|
|
else
|
|
{
|
|
// Return whitespace and end the block
|
|
PutCurrentBack();
|
|
PutBack(whitespace);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any;
|
|
}
|
|
}
|
|
|
|
private void AfterIfClause()
|
|
{
|
|
// Grab whitespace and razor comments
|
|
var whitespace = SkipToNextImportantToken();
|
|
|
|
// Check for an else part
|
|
if (At(CSharpKeyword.Else))
|
|
{
|
|
Accept(whitespace);
|
|
Assert(CSharpKeyword.Else);
|
|
ElseClause();
|
|
}
|
|
else
|
|
{
|
|
// No else, return whitespace
|
|
PutCurrentBack();
|
|
PutBack(whitespace);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any;
|
|
}
|
|
}
|
|
|
|
private void ElseClause()
|
|
{
|
|
if (!At(CSharpKeyword.Else))
|
|
{
|
|
return;
|
|
}
|
|
var block = new Block(CurrentSymbol, CurrentStart);
|
|
|
|
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.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatParseError_SingleLine_ControlFlowStatements_Not_Allowed(
|
|
Language.GetSample(CSharpSymbolType.LeftBrace),
|
|
CurrentSymbol.Content),
|
|
CurrentSymbol.Content.Length);
|
|
}
|
|
|
|
// Parse the statement and then we're done
|
|
Statement(block);
|
|
}
|
|
}
|
|
|
|
private void UnconditionalBlock()
|
|
{
|
|
Assert(CSharpSymbolType.Keyword);
|
|
var block = new Block(CurrentSymbol, CurrentStart);
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
ExpectCodeBlock(block);
|
|
}
|
|
|
|
private void FilterableCatchBlock()
|
|
{
|
|
Assert(CSharpKeyword.Catch);
|
|
|
|
var block = new Block(CurrentSymbol, CurrentStart);
|
|
|
|
// Accept "catch"
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsValidStatementSpacingSymbol);
|
|
|
|
// Parse the catch condition if present. If not present, let the C# compiler complain.
|
|
if (AcceptCondition())
|
|
{
|
|
AcceptWhile(IsValidStatementSpacingSymbol);
|
|
|
|
if (At(CSharpKeyword.When))
|
|
{
|
|
// Accept "when".
|
|
AcceptAndMoveNext();
|
|
AcceptWhile(IsValidStatementSpacingSymbol);
|
|
|
|
// Parse the filter condition if present. If not present, let the C# compiler complain.
|
|
if (!AcceptCondition())
|
|
{
|
|
// Incomplete condition.
|
|
return;
|
|
}
|
|
|
|
AcceptWhile(IsValidStatementSpacingSymbol);
|
|
}
|
|
|
|
ExpectCodeBlock(block);
|
|
}
|
|
}
|
|
|
|
private void ConditionalBlock(bool topLevel)
|
|
{
|
|
Assert(CSharpSymbolType.Keyword);
|
|
var block = new Block(CurrentSymbol, CurrentStart);
|
|
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 = AcceptedCharactersInternal.Any;
|
|
|
|
// Accept whitespace but always keep the last whitespace node so we can put it back if necessary
|
|
var lastWhitespace = AcceptWhiteSpaceInLines();
|
|
|
|
if (EndOfFile)
|
|
{
|
|
if (lastWhitespace != null)
|
|
{
|
|
Accept(lastWhitespace);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var type = CurrentSymbol.Type;
|
|
var loc = CurrentStart;
|
|
|
|
// Both cases @: and @:: are triggered as markup, second colon in second case will be triggered as a plain text
|
|
var isSingleLineMarkup = type == CSharpSymbolType.Transition &&
|
|
(NextIs(CSharpSymbolType.Colon, CSharpSymbolType.DoubleColon));
|
|
|
|
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();
|
|
|
|
// Put back the whitespace unless it precedes a '<text>' tag.
|
|
if (nextSymbol != null &&
|
|
!string.Equals(nextSymbol.Content, SyntaxConstants.TextTagName, StringComparison.Ordinal))
|
|
{
|
|
PutBack(lastWhitespace);
|
|
}
|
|
else
|
|
{
|
|
// If it precedes a '<text>' tag, it should be accepted as code.
|
|
Accept(lastWhitespace);
|
|
}
|
|
}
|
|
|
|
if (isMarkup)
|
|
{
|
|
if (type == CSharpSymbolType.Transition && !isSingleLineMarkup)
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
loc,
|
|
LegacyResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start,
|
|
length: 1 /* @ */);
|
|
}
|
|
|
|
// Markup block
|
|
Output(SpanKindInternal.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(SpanKindInternal.Code);
|
|
RazorComment();
|
|
Statement(block);
|
|
break;
|
|
case CSharpSymbolType.LeftBrace:
|
|
// Verbatim Block
|
|
block = block ?? new Block(LegacyResources.BlockName_Code, CurrentStart);
|
|
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(SpanKindInternal.Code);
|
|
|
|
// Output "@" as hidden span
|
|
Accept(transition);
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.Code);
|
|
|
|
Assert(CSharpSymbolType.Transition);
|
|
AcceptAndMoveNext();
|
|
StandardStatement();
|
|
}
|
|
else
|
|
{
|
|
// Throw errors as necessary, but continue parsing
|
|
if (At(CSharpSymbolType.LeftBrace))
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.ParseError_Unexpected_Nested_CodeBlock,
|
|
length: 1 /* { */);
|
|
}
|
|
|
|
// @( 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 = CurrentStart.AbsoluteIndex;
|
|
var 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(SpanKindInternal.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.LeftBrace, 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.ErrorSink.OnError(
|
|
block.Start,
|
|
LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, '}', '{'),
|
|
length: 1 /* { OR } */);
|
|
}
|
|
else if (acceptTerminatingBrace)
|
|
{
|
|
Assert(CSharpSymbolType.RightBrace);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
AcceptAndMoveNext();
|
|
}
|
|
}
|
|
|
|
private void HandleKeyword(bool topLevel, Action fallback)
|
|
{
|
|
Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword != null);
|
|
if (_keywordParsers.TryGetValue(CurrentSymbol.Keyword.Value, out var handler))
|
|
{
|
|
handler(topLevel);
|
|
}
|
|
else
|
|
{
|
|
fallback();
|
|
}
|
|
}
|
|
|
|
private IEnumerable<CSharpSymbol> SkipToNextImportantToken()
|
|
{
|
|
while (!EndOfFile)
|
|
{
|
|
var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
if (At(CSharpSymbolType.RazorCommentTransition))
|
|
{
|
|
Accept(whitespace);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any;
|
|
RazorComment();
|
|
}
|
|
else
|
|
{
|
|
return whitespace;
|
|
}
|
|
}
|
|
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(SpanKindInternal.Code);
|
|
}
|
|
|
|
private void SetUpExpressions()
|
|
{
|
|
MapExpressionKeyword(AwaitExpression, CSharpKeyword.Await);
|
|
}
|
|
|
|
private void AwaitExpression(bool topLevel)
|
|
{
|
|
// Ensure that we're on the await statement (only runs in debug)
|
|
Assert(CSharpKeyword.Await);
|
|
|
|
// Accept the "await" and move on
|
|
AcceptAndMoveNext();
|
|
|
|
// Accept 1 or more spaces between the await and the following code.
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
|
|
// Top level basically indicates if we're within an expression or statement.
|
|
// Ex: topLevel true = @await Foo() | topLevel false = @{ await Foo(); }
|
|
// Note that in this case @{ <b>@await Foo()</b> } top level is true for await.
|
|
// Therefore, if we're top level then we want to act like an implicit expression,
|
|
// otherwise just act as whatever we're contained in.
|
|
if (topLevel)
|
|
{
|
|
// Setup the Span to be an async implicit expression (an implicit expresison that allows spaces).
|
|
// Spaces are allowed because of "@await Foo()".
|
|
AsyncImplicitExpression();
|
|
}
|
|
}
|
|
|
|
private void SetupDirectives(IEnumerable<DirectiveDescriptor> directiveDescriptors)
|
|
{
|
|
var allDirectives = directiveDescriptors.Concat(DefaultDirectiveDescriptors).ToList();
|
|
|
|
for (var i = 0; i < allDirectives.Count; i++)
|
|
{
|
|
var directiveDescriptor = allDirectives[i];
|
|
CurrentKeywords.Add(directiveDescriptor.Directive);
|
|
MapDirectives(() => HandleDirective(directiveDescriptor), directiveDescriptor.Directive);
|
|
}
|
|
|
|
MapDirectives(TagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword);
|
|
MapDirectives(AddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword);
|
|
MapDirectives(RemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword);
|
|
}
|
|
|
|
private void EnsureDirectiveIsAtStartOfLine()
|
|
{
|
|
// 1 is the offset of the @ transition for the directive.
|
|
if (CurrentStart.CharacterIndex > 1)
|
|
{
|
|
var index = CurrentStart.AbsoluteIndex - 1;
|
|
var lineStart = CurrentStart.AbsoluteIndex - CurrentStart.CharacterIndex;
|
|
while (--index >= lineStart)
|
|
{
|
|
var @char = Context.SourceDocument[index];
|
|
|
|
if (!char.IsWhiteSpace(@char))
|
|
{
|
|
var currentDirective = CurrentSymbol.Content;
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
Resources.FormatDirectiveMustAppearAtStartOfLine(currentDirective),
|
|
length: currentDirective.Length);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleDirective(DirectiveDescriptor descriptor)
|
|
{
|
|
AssertDirective(descriptor.Directive);
|
|
|
|
var directiveErrorSink = new ErrorSink();
|
|
var savedErrorSink = Context.ErrorSink;
|
|
Context.ErrorSink = directiveErrorSink;
|
|
|
|
var directiveChunkGenerator = new DirectiveChunkGenerator(descriptor);
|
|
try
|
|
{
|
|
EnsureDirectiveIsAtStartOfLine();
|
|
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive;
|
|
Context.Builder.CurrentBlock.ChunkGenerator = directiveChunkGenerator;
|
|
|
|
AcceptAndMoveNext();
|
|
Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.None);
|
|
|
|
// Even if an error was logged do not bail out early. If a directive was used incorrectly it doesn't mean it can't be parsed.
|
|
ValidateDirectiveUsage(descriptor);
|
|
|
|
for (var i = 0; i < descriptor.Tokens.Count; i++)
|
|
{
|
|
if (!At(CSharpSymbolType.WhiteSpace) &&
|
|
!At(CSharpSymbolType.NewLine) &&
|
|
!EndOfFile)
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
Resources.FormatDirectiveTokensMustBeSeparatedByWhitespace(descriptor.Directive),
|
|
length: CurrentSymbol.Content.Length);
|
|
return;
|
|
}
|
|
|
|
var tokenDescriptor = descriptor.Tokens[i];
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
|
|
if (tokenDescriptor.Kind == DirectiveTokenKind.Member ||
|
|
tokenDescriptor.Kind == DirectiveTokenKind.Namespace ||
|
|
tokenDescriptor.Kind == DirectiveTokenKind.Type)
|
|
{
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.Code, AcceptedCharactersInternal.WhiteSpace);
|
|
|
|
if (EndOfFile || At(CSharpSymbolType.NewLine))
|
|
{
|
|
// Add a marker symbol to provide CSharp intellisense when we start typing the directive token.
|
|
AddMarkerSymbolIfNecessary();
|
|
Span.ChunkGenerator = new DirectiveTokenChunkGenerator(tokenDescriptor);
|
|
Span.EditHandler = new DirectiveTokenEditHandler(Language.TokenizeString);
|
|
Output(SpanKindInternal.Code, AcceptedCharactersInternal.NonWhiteSpace);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.Markup, AcceptedCharactersInternal.WhiteSpace);
|
|
}
|
|
|
|
if (tokenDescriptor.Optional && (EndOfFile || At(CSharpSymbolType.NewLine)))
|
|
{
|
|
break;
|
|
}
|
|
else if (EndOfFile)
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatUnexpectedEOFAfterDirective(descriptor.Directive, tokenDescriptor.Kind.ToString().ToLowerInvariant()),
|
|
length: 1);
|
|
return;
|
|
}
|
|
|
|
switch (tokenDescriptor.Kind)
|
|
{
|
|
case DirectiveTokenKind.Type:
|
|
if (!NamespaceOrTypeName())
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatDirectiveExpectsTypeName(descriptor.Directive),
|
|
CurrentSymbol.Content.Length);
|
|
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case DirectiveTokenKind.Namespace:
|
|
if (!QualifiedIdentifier(out var identifierLength))
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatDirectiveExpectsNamespace(descriptor.Directive),
|
|
identifierLength);
|
|
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case DirectiveTokenKind.Member:
|
|
if (At(CSharpSymbolType.Identifier))
|
|
{
|
|
AcceptAndMoveNext();
|
|
}
|
|
else
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatDirectiveExpectsIdentifier(descriptor.Directive),
|
|
CurrentSymbol.Content.Length);
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case DirectiveTokenKind.String:
|
|
if (At(CSharpSymbolType.StringLiteral) && CurrentSymbol.Errors.Count == 0)
|
|
{
|
|
AcceptAndMoveNext();
|
|
}
|
|
else
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatDirectiveExpectsQuotedStringLiteral(descriptor.Directive),
|
|
CurrentSymbol.Content.Length);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
Span.ChunkGenerator = new DirectiveTokenChunkGenerator(tokenDescriptor);
|
|
Span.EditHandler = new DirectiveTokenEditHandler(Language.TokenizeString);
|
|
Output(SpanKindInternal.Code, AcceptedCharactersInternal.NonWhiteSpace);
|
|
}
|
|
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
|
|
switch (descriptor.Kind)
|
|
{
|
|
case DirectiveKind.SingleLine:
|
|
Output(SpanKindInternal.None, AcceptedCharactersInternal.WhiteSpace);
|
|
|
|
Optional(CSharpSymbolType.Semicolon);
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.WhiteSpace);
|
|
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
|
|
if (At(CSharpSymbolType.NewLine))
|
|
{
|
|
AcceptAndMoveNext();
|
|
}
|
|
else if (!EndOfFile)
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatUnexpectedDirectiveLiteral(descriptor.Directive, LegacyResources.ErrorComponent_Newline),
|
|
CurrentSymbol.Content.Length);
|
|
}
|
|
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.None, AcceptedCharactersInternal.WhiteSpace);
|
|
break;
|
|
case DirectiveKind.RazorBlock:
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
Output(SpanKindInternal.Markup, AcceptedCharactersInternal.AllWhiteSpace);
|
|
|
|
ParseDirectiveBlock(descriptor, parseChildren: (startingBraceLocation) =>
|
|
{
|
|
// When transitioning to the HTML parser we no longer want to act as if we're in a nested C# state.
|
|
// For instance, if <div>@hello.</div> is in a nested C# block we don't want the trailing '.' to be handled
|
|
// as C#; it should be handled as a period because it's wrapped in markup.
|
|
var wasNested = IsNested;
|
|
IsNested = false;
|
|
|
|
using (PushSpanConfig())
|
|
{
|
|
HtmlParser.ParseRazorBlock(Tuple.Create("{", "}"), caseSensitive: true);
|
|
}
|
|
|
|
Span.Start = CurrentLocation;
|
|
Initialize(Span);
|
|
|
|
IsNested = wasNested;
|
|
|
|
NextToken();
|
|
});
|
|
break;
|
|
case DirectiveKind.CodeBlock:
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
|
|
Output(SpanKindInternal.Markup, AcceptedCharactersInternal.AllWhiteSpace);
|
|
|
|
ParseDirectiveBlock(descriptor, parseChildren: (startingBraceLocation) =>
|
|
{
|
|
NextToken();
|
|
Balance(BalancingModes.NoErrorOnFailure, CSharpSymbolType.LeftBrace, CSharpSymbolType.RightBrace, startingBraceLocation);
|
|
Span.ChunkGenerator = new StatementChunkGenerator();
|
|
Output(SpanKindInternal.Code);
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (directiveErrorSink.Errors.Count > 0)
|
|
{
|
|
var directiveDiagnostics = directiveErrorSink.Errors.Select(error => RazorDiagnostic.Create(error));
|
|
directiveChunkGenerator.Diagnostics.AddRange(directiveDiagnostics);
|
|
}
|
|
|
|
Context.ErrorSink = savedErrorSink;
|
|
}
|
|
}
|
|
|
|
|
|
private void ValidateDirectiveUsage(DirectiveDescriptor descriptor)
|
|
{
|
|
if (descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring)
|
|
{
|
|
if (Context.SeenDirectives.Contains(descriptor.Directive))
|
|
{
|
|
UsageError(Resources.FormatDuplicateDirective(descriptor.Directive));
|
|
return;
|
|
}
|
|
}
|
|
|
|
void UsageError(string message)
|
|
{
|
|
// There wil always be at least 1 child because of the `@` transition.
|
|
var directiveStart = Context.Builder.CurrentBlock.Children.First().Start;
|
|
var errorLength = /* @ */ 1 + descriptor.Directive.Length;
|
|
Context.ErrorSink.OnError(directiveStart, message, errorLength);
|
|
}
|
|
}
|
|
|
|
private void ParseDirectiveBlock(DirectiveDescriptor descriptor, Action<SourceLocation> parseChildren)
|
|
{
|
|
if (EndOfFile)
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatUnexpectedEOFAfterDirective(descriptor.Directive, "{"),
|
|
length: 1 /* { */);
|
|
}
|
|
else if (!At(CSharpSymbolType.LeftBrace))
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
CurrentStart,
|
|
LegacyResources.FormatUnexpectedDirectiveLiteral(descriptor.Directive, "{"),
|
|
CurrentSymbol.Content.Length);
|
|
}
|
|
else
|
|
{
|
|
var editHandler = new AutoCompleteEditHandler(Language.TokenizeString, autoCompleteAtEndOfSpan: true);
|
|
Span.EditHandler = editHandler;
|
|
var startingBraceLocation = CurrentStart;
|
|
Accept(CurrentSymbol);
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.None);
|
|
|
|
parseChildren(startingBraceLocation);
|
|
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
if (!Optional(CSharpSymbolType.RightBrace))
|
|
{
|
|
editHandler.AutoCompleteString = "}";
|
|
Context.ErrorSink.OnError(
|
|
startingBraceLocation,
|
|
LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(descriptor.Directive, "}", "{"),
|
|
length: 1 /* } */);
|
|
}
|
|
else
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
}
|
|
CompleteBlock(insertMarkerIfNecessary: false, captureWhitespaceToEndOfLine: true);
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.None);
|
|
}
|
|
}
|
|
|
|
protected virtual void TagHelperPrefixDirective()
|
|
{
|
|
RazorDiagnostic duplicateDiagnostic = null;
|
|
if (Context.SeenDirectives.Contains(SyntaxConstants.CSharp.TagHelperPrefixKeyword))
|
|
{
|
|
// There wil always be at least 1 child because of the `@` transition.
|
|
var directiveStart = Context.Builder.CurrentBlock.Children.First().Start;
|
|
var errorLength = /* @ */ 1 + SyntaxConstants.CSharp.TagHelperPrefixKeyword.Length;
|
|
duplicateDiagnostic = RazorDiagnosticFactory.CreateParsing_DuplicateDirective(
|
|
SyntaxConstants.CSharp.TagHelperPrefixKeyword,
|
|
new SourceSpan(directiveStart, errorLength));
|
|
}
|
|
|
|
TagHelperDirective(
|
|
SyntaxConstants.CSharp.TagHelperPrefixKeyword,
|
|
(prefix, errors) =>
|
|
{
|
|
if (duplicateDiagnostic != null)
|
|
{
|
|
errors.Add(duplicateDiagnostic);
|
|
}
|
|
|
|
var parsedDirective = ParseDirective(prefix, Span.Start, TagHelperDirectiveType.TagHelperPrefix, errors);
|
|
|
|
return new TagHelperPrefixDirectiveChunkGenerator(
|
|
prefix,
|
|
parsedDirective.DirectiveText,
|
|
errors);
|
|
});
|
|
}
|
|
|
|
// Internal for testing.
|
|
internal void ValidateTagHelperPrefix(
|
|
string prefix,
|
|
SourceLocation directiveLocation,
|
|
List<RazorDiagnostic> diagnostics)
|
|
{
|
|
foreach (var character in prefix)
|
|
{
|
|
// Prefixes are correlated with tag names, tag names cannot have whitespace.
|
|
if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character))
|
|
{
|
|
diagnostics.Add(
|
|
RazorDiagnostic.Create(
|
|
new RazorError(
|
|
Resources.FormatInvalidTagHelperPrefixValue(SyntaxConstants.CSharp.TagHelperPrefixKeyword, character, prefix),
|
|
directiveLocation,
|
|
prefix.Length)));
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private ParsedDirective ParseDirective(
|
|
string directiveText,
|
|
SourceLocation directiveLocation,
|
|
TagHelperDirectiveType directiveType,
|
|
List<RazorDiagnostic> errors)
|
|
{
|
|
var offset = 0;
|
|
directiveText = directiveText.Trim();
|
|
if (directiveText.Length >= 2 &&
|
|
directiveText.StartsWith("\"", StringComparison.Ordinal) &&
|
|
directiveText.EndsWith("\"", StringComparison.Ordinal))
|
|
{
|
|
directiveText = directiveText.Substring(1, directiveText.Length - 2);
|
|
if (string.IsNullOrEmpty(directiveText))
|
|
{
|
|
offset = 1;
|
|
}
|
|
}
|
|
|
|
// If this is the "string literal" form of a directive, we'll need to postprocess the location
|
|
// and content.
|
|
//
|
|
// Ex: @addTagHelper "*, Microsoft.AspNetCore.CoolLibrary"
|
|
// ^ ^
|
|
// Start End
|
|
if (Span.Symbols.Count == 1 && (Span.Symbols[0] as CSharpSymbol)?.Type == CSharpSymbolType.StringLiteral)
|
|
{
|
|
offset += Span.Symbols[0].Content.IndexOf(directiveText, StringComparison.Ordinal);
|
|
|
|
// This is safe because inside one of these directives all of the text needs to be on the
|
|
// same line.
|
|
var original = directiveLocation;
|
|
directiveLocation = new SourceLocation(
|
|
original.FilePath,
|
|
original.AbsoluteIndex + offset,
|
|
original.LineIndex,
|
|
original.CharacterIndex + offset);
|
|
}
|
|
|
|
var parsedDirective = new ParsedDirective()
|
|
{
|
|
DirectiveText = directiveText
|
|
};
|
|
|
|
if (directiveType == TagHelperDirectiveType.TagHelperPrefix)
|
|
{
|
|
ValidateTagHelperPrefix(parsedDirective.DirectiveText, directiveLocation, errors);
|
|
|
|
return parsedDirective;
|
|
}
|
|
|
|
return ParseAddOrRemoveDirective(parsedDirective, directiveLocation, errors);
|
|
}
|
|
|
|
// Internal for testing.
|
|
internal ParsedDirective ParseAddOrRemoveDirective(ParsedDirective directive, SourceLocation directiveLocation, List<RazorDiagnostic> errors)
|
|
{
|
|
var text = directive.DirectiveText;
|
|
var lookupStrings = text?.Split(new[] { ',' });
|
|
|
|
// Ensure that we have valid lookupStrings to work with. The valid format is "typeName, assemblyName"
|
|
if (lookupStrings == null ||
|
|
lookupStrings.Any(string.IsNullOrWhiteSpace) ||
|
|
lookupStrings.Length != 2 ||
|
|
text.StartsWith("'") ||
|
|
text.EndsWith("'"))
|
|
{
|
|
errors.Add(
|
|
RazorDiagnostic.Create(
|
|
new RazorError(
|
|
Resources.FormatInvalidTagHelperLookupText(text),
|
|
directiveLocation,
|
|
Math.Max(text.Length, 1))));
|
|
|
|
return directive;
|
|
}
|
|
|
|
var trimmedAssemblyName = lookupStrings[1].Trim();
|
|
|
|
// + 1 is for the comma separator in the lookup text.
|
|
var assemblyNameIndex =
|
|
lookupStrings[0].Length + 1 + lookupStrings[1].IndexOf(trimmedAssemblyName, StringComparison.Ordinal);
|
|
var assemblyNamePrefix = directive.DirectiveText.Substring(0, assemblyNameIndex);
|
|
|
|
directive.TypePattern = lookupStrings[0].Trim();
|
|
directive.AssemblyName = trimmedAssemblyName;
|
|
|
|
return directive;
|
|
}
|
|
|
|
protected virtual void AddTagHelperDirective()
|
|
{
|
|
TagHelperDirective(
|
|
SyntaxConstants.CSharp.AddTagHelperKeyword,
|
|
(lookupText, errors) =>
|
|
{
|
|
var parsedDirective = ParseDirective(lookupText, Span.Start, TagHelperDirectiveType.AddTagHelper, errors);
|
|
|
|
return new AddTagHelperChunkGenerator(
|
|
lookupText,
|
|
parsedDirective.DirectiveText,
|
|
parsedDirective.TypePattern,
|
|
parsedDirective.AssemblyName,
|
|
errors);
|
|
});
|
|
}
|
|
|
|
protected virtual void RemoveTagHelperDirective()
|
|
{
|
|
TagHelperDirective(
|
|
SyntaxConstants.CSharp.RemoveTagHelperKeyword,
|
|
(lookupText, errors) =>
|
|
{
|
|
var parsedDirective = ParseDirective(lookupText, Span.Start, TagHelperDirectiveType.RemoveTagHelper, errors);
|
|
|
|
return new RemoveTagHelperChunkGenerator(
|
|
lookupText,
|
|
parsedDirective.DirectiveText,
|
|
parsedDirective.TypePattern,
|
|
parsedDirective.AssemblyName,
|
|
errors);
|
|
});
|
|
}
|
|
|
|
[Conditional("DEBUG")]
|
|
protected void AssertDirective(string directive)
|
|
{
|
|
Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Identifier || CurrentSymbol.Type == CSharpSymbolType.Keyword);
|
|
Debug.Assert(string.Equals(CurrentSymbol.Content, directive, StringComparison.Ordinal));
|
|
}
|
|
|
|
protected void BaseTypeDirective(string noTypeNameError, Func<string, SpanChunkGenerator> createChunkGenerator)
|
|
{
|
|
var keywordStartLocation = Span.Start;
|
|
|
|
// Set the block type
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive;
|
|
|
|
var keywordLength = Span.End.AbsoluteIndex - Span.Start.AbsoluteIndex;
|
|
|
|
// Accept whitespace
|
|
var remainingWhitespace = AcceptSingleWhiteSpaceCharacter();
|
|
|
|
if (Span.Symbols.Count > 1)
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
|
|
}
|
|
|
|
Output(SpanKindInternal.MetaCode);
|
|
|
|
if (remainingWhitespace != null)
|
|
{
|
|
Accept(remainingWhitespace);
|
|
}
|
|
|
|
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
|
|
|
|
if (EndOfFile || At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine))
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
keywordStartLocation,
|
|
noTypeNameError,
|
|
keywordLength);
|
|
}
|
|
|
|
// 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
|
|
var baseType = string.Concat(Span.Symbols.Select(s => s.Content));
|
|
|
|
// Set up chunk generation
|
|
Span.ChunkGenerator = createChunkGenerator(baseType.Trim());
|
|
|
|
// Output the span and finish the block
|
|
CompleteBlock();
|
|
Output(SpanKindInternal.Code, AcceptedCharactersInternal.AnyExceptNewline);
|
|
}
|
|
|
|
private void TagHelperDirective(string keyword, Func<string, List<RazorDiagnostic>, ISpanChunkGenerator> chunkGeneratorFactory)
|
|
{
|
|
AssertDirective(keyword);
|
|
|
|
var savedErrorSink = Context.ErrorSink;
|
|
var directiveErrorSink = new ErrorSink();
|
|
Context.ErrorSink = directiveErrorSink;
|
|
|
|
string directiveValue = null;
|
|
try
|
|
{
|
|
EnsureDirectiveIsAtStartOfLine();
|
|
|
|
var keywordStartLocation = CurrentStart;
|
|
|
|
// Accept the directive name
|
|
AcceptAndMoveNext();
|
|
|
|
// Set the block type
|
|
Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive;
|
|
|
|
var keywordLength = Span.End.AbsoluteIndex - Span.Start.AbsoluteIndex;
|
|
|
|
var foundWhitespace = At(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.
|
|
var acceptedCharacters = foundWhitespace ? AcceptedCharactersInternal.None : AcceptedCharactersInternal.AnyExceptNewline;
|
|
Output(SpanKindInternal.MetaCode, acceptedCharacters);
|
|
|
|
AcceptWhile(CSharpSymbolType.WhiteSpace);
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKindInternal.Markup, acceptedCharacters);
|
|
|
|
if (EndOfFile || At(CSharpSymbolType.NewLine))
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
keywordStartLocation,
|
|
LegacyResources.FormatParseError_DirectiveMustHaveValue(keyword),
|
|
keywordLength);
|
|
|
|
directiveValue = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
// Need to grab the current location before we accept until the end of the line.
|
|
var startLocation = CurrentStart;
|
|
|
|
// 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 and remove whitespaces and optional quotes
|
|
var rawValue = string.Concat(Span.Symbols.Select(s => s.Content)).Trim();
|
|
|
|
var startsWithQuote = rawValue.StartsWith("\"", StringComparison.Ordinal);
|
|
var endsWithQuote = rawValue.EndsWith("\"", StringComparison.Ordinal);
|
|
if (startsWithQuote != endsWithQuote)
|
|
{
|
|
Context.ErrorSink.OnError(
|
|
startLocation,
|
|
LegacyResources.FormatParseError_IncompleteQuotesAroundDirective(keyword),
|
|
rawValue.Length);
|
|
}
|
|
|
|
directiveValue = rawValue;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
List<RazorDiagnostic> directiveErrors;
|
|
if (directiveErrorSink.Errors.Count > 0)
|
|
{
|
|
directiveErrors = directiveErrorSink.Errors.Select(RazorDiagnostic.Create).ToList();
|
|
}
|
|
else
|
|
{
|
|
directiveErrors = new List<RazorDiagnostic>();
|
|
}
|
|
|
|
Span.ChunkGenerator = chunkGeneratorFactory(directiveValue, directiveErrors);
|
|
Context.ErrorSink = savedErrorSink;
|
|
}
|
|
|
|
// Output the span and finish the block
|
|
CompleteBlock();
|
|
Output(SpanKindInternal.Code, AcceptedCharactersInternal.AnyExceptNewline);
|
|
}
|
|
|
|
protected class Block
|
|
{
|
|
public Block(string name, SourceLocation start)
|
|
{
|
|
Name = name;
|
|
Start = start;
|
|
}
|
|
|
|
public Block(CSharpSymbol symbol, SourceLocation start)
|
|
: this(GetName(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;
|
|
}
|
|
}
|
|
|
|
internal class ParsedDirective
|
|
{
|
|
public string DirectiveText { get; set; }
|
|
|
|
public string AssemblyName { get; set; }
|
|
|
|
public string TypePattern { get; set; }
|
|
}
|
|
}
|
|
}
|