From d5e9a153c736816c954b74d90147819a80b3de60 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Mon, 14 Jan 2019 11:59:37 -0800 Subject: [PATCH] Correctly handle whitespace that precedes a Razor directive (dotnet/aspnetcore-tooling#117) \n\nCommit migrated from https://github.com/dotnet/aspnetcore-tooling/commit/15556c8ff3a15c41794c45a2d8fe31adf8bbd94d --- .../src/Components/ComponentWhitespacePass.cs | 18 +------ .../src/Legacy/CSharpCodeParser.cs | 51 +++++++++++++++---- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentWhitespacePass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentWhitespacePass.cs index 0cc0e117e9..2713f96de8 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentWhitespacePass.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentWhitespacePass.cs @@ -77,11 +77,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components break; case CSharpCodeIntermediateNode codeIntermediateNode: - // A C# code node could be empty. We can't remove them, but we can skip them. shouldRemoveNode = false; - shouldContinueIteration = - IsEmpty(codeIntermediateNode) || - ComponentDocumentClassifierPass.IsBuildRenderTreeBaseCall(codeIntermediateNode); + shouldContinueIteration = ComponentDocumentClassifierPass.IsBuildRenderTreeBaseCall(codeIntermediateNode); break; default: @@ -113,18 +110,5 @@ namespace Microsoft.AspNetCore.Razor.Language.Components Forwards, Backwards } - - private static bool IsEmpty(CSharpCodeIntermediateNode node) - { - for (var i = 0; i < node.Children.Count; i++) - { - if (!(node.Children[i] is IntermediateToken token && string.IsNullOrWhiteSpace(token.Content))) - { - return false; - } - } - - return true; - } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs index e63c1dbe90..bfdcbad2aa 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs @@ -124,9 +124,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { NextToken(); - // Unless changed, the block is a statement block - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - builder.Add(OutputTokensAsStatementLiteral()); + var precedingWhitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); // We are usually called when the other parser sees a transition '@'. Look for it. SyntaxToken transitionToken = null; @@ -157,20 +155,33 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy if (At(SyntaxKind.LeftBrace)) { + // This is a statement. We want to preserve preceding whitespace in the output. + Accept(precedingWhitespace); + builder.Add(OutputTokensAsStatementLiteral()); + var statementBody = ParseStatementBody(); var statement = SyntaxFactory.CSharpStatement(transition, statementBody); builder.Add(statement); } else if (At(SyntaxKind.LeftParenthesis)) { + // This is an explicit expression. We want to preserve preceding whitespace in the output. + Accept(precedingWhitespace); + builder.Add(OutputTokensAsStatementLiteral()); + var expressionBody = ParseExplicitExpressionBody(); var expression = SyntaxFactory.CSharpExplicitExpression(transition, expressionBody); builder.Add(expression); } else if (At(SyntaxKind.Identifier)) { - if (!TryParseDirective(builder, transition, CurrentToken.Content)) + if (!TryParseDirective(builder, precedingWhitespace, transition, CurrentToken.Content)) { + // Not a directive. + // This is an implicit expression. We want to preserve preceding whitespace in the output. + Accept(precedingWhitespace); + builder.Add(OutputTokensAsStatementLiteral()); + if (string.Equals( CurrentToken.Content, SyntaxConstants.CSharp.HelperKeyword, @@ -189,9 +200,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } else if (At(SyntaxKind.Keyword)) { - if (!TryParseDirective(builder, transition, CurrentToken.Content) && - !TryParseKeyword(builder, transition)) + if (!TryParseDirective(builder, precedingWhitespace, transition, CurrentToken.Content) && + !TryParseKeyword(builder, precedingWhitespace, transition)) { + // Not a directive or keyword. + // This is an implicit expression. We want to preserve preceding whitespace in the output. + Accept(precedingWhitespace); + builder.Add(OutputTokensAsStatementLiteral()); + // Not a directive or a special keyword. Just parse as an implicit expression. var implicitExpressionBody = ParseImplicitExpressionBody(); var implicitExpression = SyntaxFactory.CSharpImplicitExpression(transition, implicitExpressionBody); @@ -202,7 +218,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } else { - // Invalid character + // Invalid character after transition. + // Preserve the preceding whitespace in the output + Accept(precedingWhitespace); + builder.Add(OutputTokensAsStatementLiteral()); + SpanContext.ChunkGenerator = new ExpressionChunkGenerator(); SpanContext.EditHandler = new ImplicitExpressionEditHandler( Language.TokenizeString, @@ -616,7 +636,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy ParseCodeBlock(builder, block); break; case SyntaxKind.Keyword: - if (!TryParseKeyword(builder, transition: null)) + if (!TryParseKeyword(builder, whitespace: null, transition: null)) { ParseStandardStatement(builder); } @@ -797,10 +817,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } } - protected bool TryParseDirective(in SyntaxListBuilder builder, CSharpTransitionSyntax transition, string directive) + protected bool TryParseDirective(in SyntaxListBuilder builder, IEnumerable 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); + builder.Add(OutputTokensAsEphemeralLiteral()); + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; handler(builder, transition); return true; @@ -1544,12 +1568,19 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } } - private bool TryParseKeyword(in SyntaxListBuilder builder, CSharpTransitionSyntax transition) + private bool TryParseKeyword(in SyntaxListBuilder builder, IEnumerable 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()); + } + handler(builder, transition); return true; }