Use PooledArrayBuilder<SyntaxToken> throughout parsers and tokenizer (#10095)

@ToddGrun shared a couple PerfView stacks with me where the Razor
compiler was allocating `List<SyntaxToken>` instances in the tokenizer.
To remove those allocation, this change replaces `List<SyntaxToken>s`
throughout the parsers and tokenizer, and uses
`PooledArrayBuilder<SyntaxToken>` instead. In addition, I removed a
couple of `Concat` calls and replaced them with pooled `StringBuilders`.
This commit is contained in:
Dustin Campbell 2024-03-22 08:53:34 -07:00 коммит произвёл GitHub
Родитель 66658cab3e 5bed9c6638
Коммит d4a6aaafa0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
7 изменённых файлов: 258 добавлений и 148 удалений

Просмотреть файл

@ -151,7 +151,7 @@
<NuGetSolutionRestoreManagerInteropVersion>4.8.0</NuGetSolutionRestoreManagerInteropVersion>
<StreamJsonRpcPackageVersion>2.17.11</StreamJsonRpcPackageVersion>
<SystemRuntimeInteropServicesRuntimePackageVersion>4.3.0</SystemRuntimeInteropServicesRuntimePackageVersion>
<Tooling_MicrosoftCodeAnalysisAnalyzersPackageVersion>3.11.0-beta1.24128.1</Tooling_MicrosoftCodeAnalysisAnalyzersPackageVersion>
<Tooling_MicrosoftCodeAnalysisAnalyzersPackageVersion>3.11.0-beta1.24170.2</Tooling_MicrosoftCodeAnalysisAnalyzersPackageVersion>
<Tooling_MicrosoftCodeAnalysisBannedApiAnalyzersPackageVersion>$(Tooling_MicrosoftCodeAnalysisAnalyzersPackageVersion)</Tooling_MicrosoftCodeAnalysisBannedApiAnalyzersPackageVersion>
<Tooling_RoslynDiagnosticsAnalyzersPackageVersion>$(Tooling_MicrosoftCodeAnalysisAnalyzersPackageVersion)</Tooling_RoslynDiagnosticsAnalyzersPackageVersion>
<Tooling_MicrosoftVisualStudioLanguageServicesPackageVersion>$(MicrosoftVisualStudioLanguageServicesPackageVersion)</Tooling_MicrosoftVisualStudioLanguageServicesPackageVersion>

Просмотреть файл

@ -67,15 +67,15 @@ public class TokenizerLookaheadTest : HtmlTokenizerTestBase
// Act
var i = 3;
IEnumerable<SyntaxToken> previousTokens = null;
SyntaxToken[] previousTokens = null;
var tokenFound = tokenizer.LookaheadUntil((s, p) =>
{
previousTokens = p;
previousTokens = p.ToArray();
return --i == 0;
});
// Assert
Assert.Equal(4, previousTokens.Count());
Assert.Equal(4, previousTokens.Length);
// For the very first element, there will be no previous items, so null is expected
var orderIndex = 0;

Просмотреть файл

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
namespace Microsoft.AspNetCore.Razor.Language.Legacy;
@ -228,7 +229,8 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
{
NextToken();
var precedingWhitespace = ReadWhile(IsSpacingTokenIncludingNewLinesAndComments);
using var precedingWhitespace = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(IsSpacingTokenIncludingNewLinesAndComments, ref precedingWhitespace.AsRef());
// We are usually called when the other parser sees a transition '@'. Look for it.
SyntaxToken? transitionToken = null;
@ -260,7 +262,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
if (At(SyntaxKind.LeftBrace))
{
// This is a statement. We want to preserve preceding whitespace in the output.
Accept(precedingWhitespace);
Accept(in precedingWhitespace);
builder.Add(OutputTokensAsStatementLiteral());
var statementBody = ParseStatementBody();
@ -270,7 +272,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
else if (At(SyntaxKind.LeftParenthesis))
{
// This is an explicit expression. We want to preserve preceding whitespace in the output.
Accept(precedingWhitespace);
Accept(in precedingWhitespace);
builder.Add(OutputTokensAsStatementLiteral());
var expressionBody = ParseExplicitExpressionBody();
@ -279,11 +281,11 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
}
else if (At(SyntaxKind.Identifier))
{
if (!TryParseDirective(builder, precedingWhitespace, transition, CurrentToken.Content))
if (!TryParseDirective(builder, in precedingWhitespace, transition, CurrentToken.Content))
{
// Not a directive.
// This is an implicit expression. We want to preserve preceding whitespace in the output.
Accept(precedingWhitespace);
Accept(in precedingWhitespace);
builder.Add(OutputTokensAsStatementLiteral());
if (string.Equals(
@ -304,12 +306,12 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
}
else if (At(SyntaxKind.Keyword))
{
if (!TryParseDirective(builder, precedingWhitespace, transition, CurrentToken.Content) &&
!TryParseKeyword(builder, precedingWhitespace, transition))
if (!TryParseDirective(builder, in precedingWhitespace, transition, CurrentToken.Content) &&
!TryParseKeyword(builder, in precedingWhitespace, transition))
{
// Not a directive or keyword.
// This is an implicit expression. We want to preserve preceding whitespace in the output.
Accept(precedingWhitespace);
Accept(in precedingWhitespace);
builder.Add(OutputTokensAsStatementLiteral());
// Not a directive or a special keyword. Just parse as an implicit expression.
@ -324,7 +326,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
{
// Invalid character after transition.
// Preserve the preceding whitespace in the output
Accept(precedingWhitespace);
Accept(in precedingWhitespace);
builder.Add(OutputTokensAsStatementLiteral());
chunkGenerator = new ExpressionChunkGenerator();
@ -820,7 +822,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
}
break;
case SyntaxKind.Keyword:
if (!TryParseKeyword(builder, whitespace: null, transition: null))
if (!TryParseKeyword(builder))
{
ParseStandardStatement(builder);
}
@ -911,20 +913,23 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
while (!EndOfFile)
{
var bookmark = CurrentStart.AbsoluteIndex;
var read = ReadWhile(static token =>
token.Kind != SyntaxKind.Semicolon &&
token.Kind != SyntaxKind.RazorCommentTransition &&
token.Kind != SyntaxKind.Transition &&
token.Kind != SyntaxKind.LeftBrace &&
token.Kind != SyntaxKind.LeftParenthesis &&
token.Kind != SyntaxKind.LeftBracket &&
token.Kind != SyntaxKind.RightBrace);
using var read = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static token =>
token.Kind != SyntaxKind.Semicolon &&
token.Kind != SyntaxKind.RazorCommentTransition &&
token.Kind != SyntaxKind.Transition &&
token.Kind != SyntaxKind.LeftBrace &&
token.Kind != SyntaxKind.LeftParenthesis &&
token.Kind != SyntaxKind.LeftBracket &&
token.Kind != SyntaxKind.RightBrace,
ref read.AsRef());
if ((!Context.FeatureFlags.AllowRazorInAllCodeBlocks && At(SyntaxKind.LeftBrace)) ||
At(SyntaxKind.LeftParenthesis) ||
At(SyntaxKind.LeftBracket))
{
Accept(read);
Accept(in read);
if (Balance(builder, BalancingModes.AllowCommentsAndTemplates | BalancingModes.BacktrackOnFailure))
{
TryAccept(SyntaxKind.RightBrace);
@ -938,31 +943,31 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
}
else if (Context.FeatureFlags.AllowRazorInAllCodeBlocks && At(SyntaxKind.LeftBrace))
{
Accept(read);
Accept(in read);
return;
}
else if (At(SyntaxKind.Transition) && (NextIs(SyntaxKind.LessThan, SyntaxKind.Colon)))
{
Accept(read);
Accept(in read);
builder.Add(OutputTokensAsStatementLiteral());
ParseTemplate(builder);
}
else if (At(SyntaxKind.RazorCommentTransition))
{
Accept(read);
Accept(in read);
AcceptMarkerTokenIfNecessary();
builder.Add(OutputTokensAsStatementLiteral());
builder.Add(ParseRazorComment());
}
else if (At(SyntaxKind.Semicolon))
{
Accept(read);
Accept(in read);
AcceptAndMoveNext();
return;
}
else if (At(SyntaxKind.RightBrace))
{
Accept(read);
Accept(in read);
return;
}
else
@ -1006,12 +1011,16 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
}
}
protected bool TryParseDirective(in SyntaxListBuilder<RazorSyntaxNode> builder, IReadOnlyList<SyntaxToken> whitespace, CSharpTransitionSyntax transition, string directive)
private bool TryParseDirective(
in SyntaxListBuilder<RazorSyntaxNode> builder,
ref readonly PooledArrayBuilder<SyntaxToken> whitespace,
CSharpTransitionSyntax transition,
string directive)
{
if (_directiveParserMap.TryGetValue(directive, out var handler))
{
// This is a directive. We don't want to generate the preceding whitespace in the output.
Accept(whitespace);
Accept(in whitespace);
builder.Add(OutputTokensAsEphemeralLiteral());
chunkGenerator = SpanChunkGenerator.Null;
@ -1785,27 +1794,31 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
// qualified-identifier . identifier
protected bool TryParseQualifiedIdentifier(out int identifierLength)
{
using var tokens = new PooledArrayBuilder<SyntaxToken>();
var currentIdentifierLength = 0;
var expectingDot = false;
var tokens = ReadWhile(token =>
{
var type = token.Kind;
if ((expectingDot && type == SyntaxKind.Dot) ||
(!expectingDot && type == SyntaxKind.Identifier))
ReadWhile(
token =>
{
expectingDot = !expectingDot;
return true;
}
var type = token.Kind;
if ((expectingDot && type == SyntaxKind.Dot) ||
(!expectingDot && type == SyntaxKind.Identifier))
{
expectingDot = !expectingDot;
return true;
}
if (type != SyntaxKind.Whitespace &&
type != SyntaxKind.NewLine)
{
expectingDot = false;
currentIdentifierLength += token.Content.Length;
}
if (type != SyntaxKind.Whitespace &&
type != SyntaxKind.NewLine)
{
expectingDot = false;
currentIdentifierLength += token.Content.Length;
}
return false;
});
return false;
},
ref tokens.AsRef());
identifierLength = currentIdentifierLength;
var validQualifiedIdentifier = expectingDot;
@ -1896,18 +1909,18 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
}
}
private bool TryParseKeyword(in SyntaxListBuilder<RazorSyntaxNode> builder, IReadOnlyList<SyntaxToken>? whitespace, CSharpTransitionSyntax? transition)
private bool TryParseKeyword(
in SyntaxListBuilder<RazorSyntaxNode> builder,
ref readonly PooledArrayBuilder<SyntaxToken> whitespace,
CSharpTransitionSyntax? transition)
{
var result = CSharpTokenizer.GetTokenKeyword(CurrentToken);
Debug.Assert(CurrentToken.Kind == SyntaxKind.Keyword && result.HasValue);
if (_keywordParserMap.TryGetValue(result!.Value, out var handler))
{
if (whitespace != null)
{
// This is a keyword. We want to preserve preceding whitespace in the output.
Accept(whitespace);
builder.Add(OutputTokensAsStatementLiteral());
}
// This is a keyword. We want to preserve preceding whitespace in the output.
Accept(in whitespace);
builder.Add(OutputTokensAsStatementLiteral());
handler(builder, transition);
return true;
@ -1916,6 +1929,19 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
return false;
}
private bool TryParseKeyword(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
var result = CSharpTokenizer.GetTokenKeyword(CurrentToken);
Debug.Assert(CurrentToken.Kind == SyntaxKind.Keyword && result.HasValue);
if (_keywordParserMap.TryGetValue(result!.Value, out var handler))
{
handler(builder, null);
return true;
}
return false;
}
private bool AtBooleanLiteral()
{
var result = CSharpTokenizer.GetTokenKeyword(CurrentToken);
@ -2103,12 +2129,13 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
private void ParseAfterIfClause(SyntaxListBuilder<RazorSyntaxNode> builder)
{
// Grab whitespace and razor comments
var whitespace = SkipToNextImportantToken(builder);
using var whitespace = new PooledArrayBuilder<SyntaxToken>();
SkipToNextImportantToken(builder, ref whitespace.AsRef());
// Check for an else part
if (At(CSharpKeyword.Else))
{
Accept(whitespace);
Accept(in whitespace);
Assert(CSharpKeyword.Else);
ParseElseClause(builder);
}
@ -2116,7 +2143,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
{
// No else, return whitespace
PutCurrentBack();
PutBack(whitespace);
PutBack(in whitespace);
SetAcceptedCharacters(AcceptedCharactersInternal.Any);
}
}
@ -2165,19 +2192,20 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
private void ParseAfterTryClause(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
// Grab whitespace
var whitespace = SkipToNextImportantToken(builder);
using var whitespace = new PooledArrayBuilder<SyntaxToken>();
SkipToNextImportantToken(builder, ref whitespace.AsRef());
// Check for a catch or finally part
if (At(CSharpKeyword.Catch))
{
Accept(whitespace);
Accept(in whitespace);
Assert(CSharpKeyword.Catch);
ParseFilterableCatchBlock(builder);
ParseAfterTryClause(builder);
}
else if (At(CSharpKeyword.Finally))
{
Accept(whitespace);
Accept(in whitespace);
Assert(CSharpKeyword.Finally);
ParseUnconditionalBlock(builder);
}
@ -2185,7 +2213,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
{
// Return whitespace and end the block
PutCurrentBack();
PutBack(whitespace);
PutBack(in whitespace);
SetAcceptedCharacters(AcceptedCharactersInternal.Any);
}
}
@ -2245,11 +2273,12 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
private void ParseWhileClause(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
SetAcceptedCharacters(AcceptedCharactersInternal.Any);
var whitespace = SkipToNextImportantToken(builder);
using var whitespace = new PooledArrayBuilder<SyntaxToken>();
SkipToNextImportantToken(builder, ref whitespace.AsRef());
if (At(CSharpKeyword.While))
{
Accept(whitespace);
Accept(in whitespace);
Assert(CSharpKeyword.While);
AcceptAndMoveNext();
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
@ -2261,7 +2290,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
else
{
PutCurrentBack();
PutBack(whitespace);
PutBack(in whitespace);
}
}
@ -2271,14 +2300,15 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
var topLevel = transition != null;
var block = new Block(CurrentToken, CurrentStart);
var usingToken = EatCurrentToken();
var whitespaceOrComments = ReadWhile(IsSpacingTokenIncludingComments);
using var whitespaceOrComments = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(IsSpacingTokenIncludingComments, ref whitespaceOrComments.AsRef());
var atLeftParen = At(SyntaxKind.LeftParenthesis);
var atIdentifier = At(SyntaxKind.Identifier);
var atStatic = At(CSharpKeyword.Static);
// Put the read tokens back and let them be handled later.
PutCurrentBack();
PutBack(whitespaceOrComments);
PutBack(in whitespaceOrComments);
PutBack(usingToken);
EnsureCurrent();
@ -2374,11 +2404,12 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
// non-static using
nonNamespaceTokenCount = TokenBuilder.Count;
TryParseNamespaceOrTypeName(directiveBuilder);
var whitespace = ReadWhile(IsSpacingTokenIncludingNewLinesAndComments);
using var whitespace = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(IsSpacingTokenIncludingNewLinesAndComments, ref whitespace.AsRef());
if (At(SyntaxKind.Assign))
{
// Alias
Accept(whitespace);
Accept(in whitespace);
Assert(SyntaxKind.Assign);
AcceptAndMoveNext();
@ -2390,7 +2421,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
else
{
PutCurrentBack();
PutBack(whitespace);
PutBack(in whitespace);
}
}
else if (At(CSharpKeyword.Static))
@ -2564,17 +2595,18 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
!Context.DesignTimeMode &&
!IsNested)
{
var whitespace = ReadWhile(static token => token.Kind == SyntaxKind.Whitespace);
using var whitespace = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(static token => token.Kind == SyntaxKind.Whitespace, ref whitespace.AsRef());
if (At(SyntaxKind.NewLine))
{
Accept(whitespace);
Accept(in whitespace);
AcceptAndMoveNext();
PutCurrentBack();
}
else
{
PutCurrentBack();
PutBack(whitespace);
PutBack(in whitespace);
}
}
else
@ -2583,14 +2615,18 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
}
}
private IReadOnlyList<SyntaxToken> SkipToNextImportantToken(in SyntaxListBuilder<RazorSyntaxNode> builder)
private void SkipToNextImportantToken(
in SyntaxListBuilder<RazorSyntaxNode> builder,
ref PooledArrayBuilder<SyntaxToken> whitespace)
{
Debug.Assert(whitespace.Count == 0, "Expected empty builder.");
while (!EndOfFile)
{
var whitespace = ReadWhile(IsSpacingTokenIncludingNewLinesAndComments);
ReadWhile(IsSpacingTokenIncludingNewLinesAndComments, ref whitespace);
if (At(SyntaxKind.RazorCommentTransition))
{
Accept(whitespace);
Accept(in whitespace);
SetAcceptedCharacters(AcceptedCharactersInternal.Any);
AcceptMarkerTokenIfNecessary();
builder.Add(OutputTokensAsStatementLiteral());
@ -2599,10 +2635,11 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
}
else
{
return whitespace;
return;
}
whitespace.Clear();
}
return Array.Empty<SyntaxToken>();
}
private void DefaultSpanContextConfig(SpanEditHandlerBuilder? editHandlerBuilder, ref ISpanChunkGenerator? generator)
@ -2722,14 +2759,14 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
if (!EndOfFile &&
!(stopAtEndOfLine && At(SyntaxKind.NewLine)))
{
var tokens = new List<SyntaxToken>();
using var tokens = new PooledArrayBuilder<SyntaxToken>();
do
{
if (IsAtEmbeddedTransition(
(mode & BalancingModes.AllowCommentsAndTemplates) == BalancingModes.AllowCommentsAndTemplates,
(mode & BalancingModes.AllowEmbeddedTransitions) == BalancingModes.AllowEmbeddedTransitions))
{
Accept(tokens);
Accept(in tokens);
tokens.Clear();
ParseEmbeddedTransition(builder);
@ -2768,13 +2805,13 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
}
else
{
Accept(tokens);
Accept(in tokens);
}
}
else
{
// Accept all the tokens we saw
Accept(tokens);
Accept(in tokens);
}
}
return nesting == 0;

Просмотреть файл

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
namespace Microsoft.AspNetCore.Razor.Language.Legacy;
@ -350,8 +351,10 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
((previousSpan is MarkupStartTagSyntax startTag && startTag.IsMarkupTransition) ||
(previousSpan is MarkupEndTagSyntax endTag && endTag.IsMarkupTransition)))
{
var tokens = ReadWhile(
static f => (f.Kind == SyntaxKind.Whitespace) || (f.Kind == SyntaxKind.NewLine));
using var tokens = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static f => (f.Kind == SyntaxKind.Whitespace) || (f.Kind == SyntaxKind.NewLine),
ref tokens.AsRef());
// Make sure the current token is not markup, which can be html start tag or @:
if (!(At(SyntaxKind.OpenAngle) ||
@ -362,7 +365,7 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
}
PutCurrentBack();
PutBack(tokens);
PutBack(in tokens);
EnsureCurrent();
}
if (shouldAcceptWhitespaceAndNewLine)
@ -698,7 +701,7 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
var bookmark = CurrentStart.AbsoluteIndex;
// Skip whitespace
ReadWhile(IsSpacingTokenIncludingNewLines);
SkipWhile(IsSpacingTokenIncludingNewLines);
// Open Angle
if (At(SyntaxKind.OpenAngle) && NextIs(SyntaxKind.ForwardSlash))
@ -1070,21 +1073,25 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
// http://dev.w3.org/html5/spec/tokenization.html#before-attribute-name-state
// Capture whitespace
var attributePrefixWhitespace = ReadWhile(static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine);
using var attributePrefixWhitespace = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine,
ref attributePrefixWhitespace.AsRef());
// http://dev.w3.org/html5/spec/tokenization.html#attribute-name-state
// Read the 'name' (i.e. read until the '=' or whitespace/newline)
if (!TryParseAttributeName(out var nameTokens))
using var nameTokens = new PooledArrayBuilder<SyntaxToken>();
if (!TryParseAttributeName(ref nameTokens.AsRef()))
{
// Unexpected character in tag, enter recovery
Accept(attributePrefixWhitespace);
Accept(in attributePrefixWhitespace);
ParseMiscAttribute(builder);
return;
}
Accept(attributePrefixWhitespace); // Whitespace before attribute name
Accept(in attributePrefixWhitespace); // Whitespace before attribute name
var namePrefix = OutputAsMarkupLiteral();
Accept(nameTokens); // Attribute name
Accept(in nameTokens); // Attribute name
var name = OutputAsMarkupLiteralRequired();
var atMinimizedAttribute = !TokenExistsAfterWhitespace(SyntaxKind.Equals);
@ -1102,9 +1109,8 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
}
}
private bool TryParseAttributeName(out IReadOnlyList<SyntaxToken> nameTokens)
private bool TryParseAttributeName(ref PooledArrayBuilder<SyntaxToken> nameTokens)
{
nameTokens = Array.Empty<SyntaxToken>();
//
// We are currently here <input |name="..." />
// If we encounter a transition (@) here, it can be parsed as CSharp or Markup depending on the feature flag.
@ -1119,7 +1125,7 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
if (IsValidAttributeNameToken(CurrentToken))
{
nameTokens = ReadWhile(
ReadWhile(
static (token, self) =>
token.Kind != SyntaxKind.Whitespace &&
token.Kind != SyntaxKind.NewLine &&
@ -1127,7 +1133,8 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
token.Kind != SyntaxKind.CloseAngle &&
token.Kind != SyntaxKind.OpenAngle &&
(token.Kind != SyntaxKind.ForwardSlash || !self.NextIs(SyntaxKind.CloseAngle)),
this);
this,
ref nameTokens);
return true;
}
@ -1144,12 +1151,15 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
Assert(SyntaxKind.Equals); // We should be at "="
var equalsToken = EatCurrentToken();
var whitespaceAfterEquals = ReadWhile(static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine);
using var whitespaceAfterEquals = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine,
ref whitespaceAfterEquals.AsRef());
var quote = SyntaxKind.Marker;
if (At(SyntaxKind.SingleQuote) || At(SyntaxKind.DoubleQuote))
{
// Found a quote, the whitespace belongs to this attribute.
Accept(whitespaceAfterEquals);
Accept(in whitespaceAfterEquals);
quote = CurrentToken.Kind;
AcceptAndMoveNext();
}
@ -1157,7 +1167,7 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
{
// No quotes found after the whitespace. Put it back so that it can be parsed later.
PutCurrentBack();
PutBack(whitespaceAfterEquals);
PutBack(in whitespaceAfterEquals);
}
MarkupTextLiteralSyntax? valuePrefix = null;
@ -1165,7 +1175,18 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
MarkupTextLiteralSyntax? valueSuffix = null;
// First, determine if this is a 'data-' attribute (since those can't use conditional attributes)
var nameContent = string.Concat(name.LiteralTokens.Nodes.Select(s => s.Content));
string nameContent;
using (StringBuilderPool.GetPooledObject(out var builder))
{
foreach (var node in name.LiteralTokens.Nodes)
{
builder.Append(node.Content);
}
nameContent = builder.ToString();
}
if (IsConditionalAttributeName(nameContent))
{
// We now have the value prefix which is usually whitespace and/or a quote
@ -1238,7 +1259,10 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
private void ParseConditionalAttributeValue(in SyntaxListBuilder<RazorSyntaxNode> builder, SyntaxKind quote)
{
var prefixStart = CurrentStart;
var prefixTokens = ReadWhile(static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine);
using var prefixTokens = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine,
ref prefixTokens.AsRef());
if (At(SyntaxKind.Transition))
{
@ -1247,11 +1271,24 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var markupBuilder = pooledResult.Builder;
Accept(prefixTokens);
Accept(in prefixTokens);
// Render a single "@" in place of "@@".
string prefixContent;
using (var pooledBuilder = StringBuilderPool.GetPooledObject())
{
var prefixBuilder = pooledBuilder.Object;
foreach (var prefixToken in prefixTokens)
{
prefixBuilder.Append(prefixToken.Content);
}
prefixContent = prefixBuilder.ToString();
}
chunkGenerator = new LiteralAttributeChunkGenerator(
new LocationTagged<string>(string.Concat(prefixTokens.Select(s => s.Content)), prefixStart),
new LocationTagged<string>(prefixContent, prefixStart),
new LocationTagged<string>(CurrentToken.Content, CurrentStart));
AcceptAndMoveNext();
SetAcceptedCharacters(AcceptedCharactersInternal.None);
@ -1268,7 +1305,7 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
}
else
{
Accept(prefixTokens);
Accept(in prefixTokens);
var valueStart = CurrentStart;
PutCurrentBack();
@ -1287,13 +1324,14 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
}
else
{
Accept(prefixTokens);
Accept(in prefixTokens);
var prefix = OutputAsMarkupLiteral();
// Literal value
// 'quote' should be "Unknown" if not quoted and tokens coming from the tokenizer should never have
// "Unknown" type.
var valueTokens = ReadWhile(
using var valueTokens = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static (token, arg) =>
// These three conditions find separators which break the attribute value into portions
token.Kind != SyntaxKind.Whitespace &&
@ -1302,8 +1340,10 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
// This condition checks for the end of the attribute value (it repeats some of the checks above
// but for now that's ok)
!arg.self.IsEndOfAttributeValue(arg.quote, token),
(self: this, quote));
Accept(valueTokens);
(self: this, quote),
ref valueTokens.AsRef());
Accept(in valueTokens);
var value = OutputAsMarkupLiteral();
var literalAttributeValue = SyntaxFactory.MarkupLiteralAttributeValue(prefix, value);
@ -1740,12 +1780,12 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
return false;
}
private IReadOnlyList<SyntaxToken> FastReadWhitespaceAndNewLines()
private void FastReadWhitespaceAndNewLines(ref PooledArrayBuilder<SyntaxToken> whitespaceTokens)
{
Debug.Assert(whitespaceTokens.Count == 0, "Expected empty builder.");
if (EnsureCurrent() && (CurrentToken.Kind == SyntaxKind.Whitespace || CurrentToken.Kind == SyntaxKind.NewLine))
{
var whitespaceTokens = new List<SyntaxToken>();
whitespaceTokens.Add(CurrentToken);
NextToken();
@ -1754,16 +1794,13 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
whitespaceTokens.Add(CurrentToken);
NextToken();
}
return whitespaceTokens;
}
return Array.Empty<SyntaxToken>();
}
private ParserState GetParserState(ParseMode mode)
{
var whitespace = FastReadWhitespaceAndNewLines();
using var whitespace = new PooledArrayBuilder<SyntaxToken>();
FastReadWhitespaceAndNewLines(ref whitespace.AsRef());
try
{
if (whitespace.Count == 0 && EndOfFile)
@ -1838,7 +1875,7 @@ internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
if (whitespace.Count > 0)
{
PutCurrentBack();
PutBack(whitespace);
PutBack(in whitespace);
EnsureCurrent();
}
}

Просмотреть файл

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
namespace Microsoft.AspNetCore.Razor.Language.Legacy;
@ -100,17 +101,19 @@ internal abstract class TokenizerBackedParser<TTokenizer> : ParserBase
return CurrentToken;
}
using var _ = ListPool<SyntaxToken>.GetPooledObject(out var tokens);
// We add 1 in order to store the current token.
var tokens = new SyntaxToken[count + 1];
tokens.SetCapacityIfLarger(count + 1);
var currentToken = CurrentToken;
tokens[0] = currentToken;
tokens.Add(currentToken);
// We need to look forward "count" many times.
for (var i = 1; i <= count; i++)
{
NextToken();
tokens[i] = CurrentToken;
tokens.Add(CurrentToken);
}
// Restore Tokenizer's location to where it was pointing before the look-ahead.
@ -141,7 +144,7 @@ internal abstract class TokenizerBackedParser<TTokenizer> : ParserBase
var matchFound = false;
var tokens = new List<SyntaxToken>();
using var _ = ListPool<SyntaxToken>.GetPooledObject(out var tokens);
tokens.Add(CurrentToken);
while (true)
@ -204,15 +207,7 @@ internal abstract class TokenizerBackedParser<TTokenizer> : ParserBase
/// that is the correct format for providing to this method. The caller of this method would,
/// in that case, want to put c, b and a back into the stream, so "a, b, c" is the CORRECT order
/// </remarks>
protected internal void PutBack(IEnumerable<SyntaxToken> tokens)
{
foreach (var token in tokens.Reverse())
{
PutBack(token);
}
}
protected internal void PutBack(IReadOnlyList<SyntaxToken> tokens)
protected void PutBack(ref readonly PooledArrayBuilder<SyntaxToken> tokens)
{
for (int i = tokens.Count - 1; i >= 0; i--)
{
@ -279,14 +274,16 @@ internal abstract class TokenizerBackedParser<TTokenizer> : ParserBase
protected bool TokenExistsAfterWhitespace(SyntaxKind kind, bool includeNewLines = true)
{
var tokenFound = false;
var whitespace = ReadWhile(
using var whitespace = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static (token, includeNewLines) =>
token.Kind == SyntaxKind.Whitespace || (includeNewLines && token.Kind == SyntaxKind.NewLine),
includeNewLines);
includeNewLines,
ref whitespace.AsRef());
tokenFound = At(kind);
PutCurrentBack();
PutBack(whitespace);
PutBack(in whitespace);
EnsureCurrent();
return tokenFound;
@ -302,25 +299,57 @@ internal abstract class TokenizerBackedParser<TTokenizer> : ParserBase
return true;
}
protected internal IReadOnlyList<SyntaxToken> ReadWhile(Func<SyntaxToken, bool> condition)
=> ReadWhile(static (token, condition) => condition(token), condition);
protected internal IReadOnlyList<SyntaxToken> ReadWhile<TArg>(Func<SyntaxToken, TArg, bool> condition, TArg arg)
protected void ReadWhile<TArg>(
Func<SyntaxToken, TArg, bool> predicate,
TArg arg,
ref PooledArrayBuilder<SyntaxToken> result)
{
if (!EnsureCurrent() || !condition(CurrentToken, arg))
Debug.Assert(result.Count == 0, "Expected empty builder.");
if (!EnsureCurrent() || !predicate(CurrentToken, arg))
{
return Array.Empty<SyntaxToken>();
return;
}
var result = new List<SyntaxToken>();
do
{
result.Add(CurrentToken);
NextToken();
}
while (EnsureCurrent() && condition(CurrentToken, arg));
while (EnsureCurrent() && predicate(CurrentToken, arg));
}
return result;
protected void ReadWhile(
Func<SyntaxToken, bool> predicate,
ref PooledArrayBuilder<SyntaxToken> result)
{
Debug.Assert(result.Count == 0, "Expected empty builder.");
if (!EnsureCurrent() || !predicate(CurrentToken))
{
return;
}
do
{
result.Add(CurrentToken);
NextToken();
}
while (EnsureCurrent() && predicate(CurrentToken));
}
protected void SkipWhile(Func<SyntaxToken, bool> predicate)
{
if (!EnsureCurrent() || !predicate(CurrentToken))
{
return;
}
do
{
NextToken();
}
while (EnsureCurrent() && predicate(CurrentToken));
}
protected bool AtIdentifier(bool allowKeywords)
@ -471,21 +500,24 @@ internal abstract class TokenizerBackedParser<TTokenizer> : ParserBase
AcceptWhile(static (token, types) => types.All(expected => expected != token.Kind), types);
}
protected internal void AcceptWhile(Func<SyntaxToken, bool> condition)
protected void AcceptWhile(Func<SyntaxToken, bool> condition)
{
Accept(ReadWhile(condition));
using var tokens = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(condition, ref tokens.AsRef());
Accept(in tokens);
}
protected internal void AcceptWhile<TArg>(Func<SyntaxToken, TArg, bool> condition, TArg arg)
protected void AcceptWhile<TArg>(Func<SyntaxToken, TArg, bool> condition, TArg arg)
{
Accept(ReadWhile(condition, arg));
using var tokens = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(condition, arg, ref tokens.AsRef());
Accept(in tokens);
}
protected internal void Accept(IReadOnlyList<SyntaxToken> tokens)
protected void Accept(ref readonly PooledArrayBuilder<SyntaxToken> tokens)
{
for (int i = 0; i < tokens.Count; i++)
foreach (var token in tokens)
{
var token = tokens[i];
Accept(token);
}
}

Просмотреть файл

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor;
@ -30,5 +29,8 @@ internal static class ListExtensions
public static T[] ToArrayOrEmpty<T>(this List<T>? list)
=> list?.Count > 0
? list.ToArray()
: Array.Empty<T>();
: [];
public static bool Any<T>(this List<T> list)
=> list.Count > 0;
}

Просмотреть файл

@ -434,6 +434,8 @@ internal partial struct PooledArrayBuilder<T> : IDisposable
};
}
public bool Any() => Count > 0;
public void Push(T item)
{
Add(item);