Skip to content

Commit b1a640b

Browse files
author
N. Taylor Mullen
committed
Allow markup in local functions and @functions.
- Allow markup to exist in all code blocks. Prior to this change whenever we'd see nested curly braces we would do dumb brace matching to skip over any potentially risky code; now we treat every curly brace as an opportunity to intermingle Markup. - One fix I had to introduce was now that functions blocks are parsed like `@{}` blocks I ran into cases where certain reserved keywords would get improperly parsed. This exposed a bug in our parsing where we’d treat **class** and **namespace** as directives without a transition in a `@{}` block. For instance this: ``` @{ class } ``` would barf in the old parser by treating the `class` piece as a directive even though it did not have a transition. To account for this I changed our reserved directives to be parsed as directives instead of keywords (it's how they should have been parsed anyhow). This isn't a breaking change because the directive parsing logic is a subset of how keywords get parsed. - One quirk this change introduces is a difference in behavior in regards to one error case. Before this change if you were to have `@if (foo)) { var bar = foo; }` the entire statement would be classified as C# and you'd get a C# error on the trailing `)`. With my changes we try to keep group statements together more closely and allow for HTML in unexpected or end of statement scenarios. So, with these new changes the above example would only have `@if (foo))` classified as C# and the rest as markup because the original was invalid. - Added lots of tests, - Modified the feature flag to maintain the old behavior when disabled. dotnet/aspnetcore#5110
1 parent cfee40a commit b1a640b

File tree

58 files changed

+3004
-59
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3004
-59
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,14 @@ private CSharpStatementBodySyntax ParseStatementBody(Block block = null)
482482
// Set up auto-complete and parse the code block
483483
var editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
484484
SpanContext.EditHandler = editHandler;
485-
ParseCodeBlock(builder, block, acceptTerminatingBrace: false);
485+
ParseCodeBlock(builder, block);
486+
487+
if (EndOfFile)
488+
{
489+
Context.ErrorSink.OnError(
490+
RazorDiagnosticFactory.CreateParsing_ExpectedEndOfBlockBeforeEOF(
491+
new SourceSpan(block.Start, contentLength: 1 /* { OR } */), block.Name, "}", "{"));
492+
}
486493

487494
EnsureCurrent();
488495
SpanContext.ChunkGenerator = new StatementChunkGenerator();
@@ -521,7 +528,7 @@ private CSharpStatementBodySyntax ParseStatementBody(Block block = null)
521528
return SyntaxFactory.CSharpStatementBody(leftBrace, codeBlock, rightBrace);
522529
}
523530

524-
private void ParseCodeBlock(in SyntaxListBuilder<RazorSyntaxNode> builder, Block block, bool acceptTerminatingBrace = true)
531+
private void ParseCodeBlock(in SyntaxListBuilder<RazorSyntaxNode> builder, Block block)
525532
{
526533
EnsureCurrent();
527534
while (!EndOfFile && !At(SyntaxKind.RightBrace))
@@ -530,19 +537,6 @@ private void ParseCodeBlock(in SyntaxListBuilder<RazorSyntaxNode> builder, Block
530537
ParseStatement(builder, block: block);
531538
EnsureCurrent();
532539
}
533-
534-
if (EndOfFile)
535-
{
536-
Context.ErrorSink.OnError(
537-
RazorDiagnosticFactory.CreateParsing_ExpectedEndOfBlockBeforeEOF(
538-
new SourceSpan(block.Start, contentLength: 1 /* { OR } */), block.Name, "}", "{"));
539-
}
540-
else if (acceptTerminatingBrace)
541-
{
542-
Assert(SyntaxKind.RightBrace);
543-
SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
544-
AcceptAndMoveNext();
545-
}
546540
}
547541

548542
private void ParseStatement(in SyntaxListBuilder<RazorSyntaxNode> builder, Block block)
@@ -634,6 +628,24 @@ private void ParseStatement(in SyntaxListBuilder<RazorSyntaxNode> builder, Block
634628
// Verbatim Block
635629
AcceptAndMoveNext();
636630
ParseCodeBlock(builder, block);
631+
632+
// ParseCodeBlock is responsible for parsing the insides of a code block (non-inclusive of braces).
633+
// Therefore, there's one of two cases after parsing:
634+
// 1. We've hit the End of File (incomplete parse block).
635+
// 2. It's a complete parse block and we're at a right brace.
636+
637+
if (EndOfFile)
638+
{
639+
Context.ErrorSink.OnError(
640+
RazorDiagnosticFactory.CreateParsing_ExpectedEndOfBlockBeforeEOF(
641+
new SourceSpan(block.Start, contentLength: 1 /* { OR } */), block.Name, "}", "{"));
642+
}
643+
else
644+
{
645+
Assert(SyntaxKind.RightBrace);
646+
SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
647+
AcceptAndMoveNext();
648+
}
637649
break;
638650
case SyntaxKind.Keyword:
639651
if (!TryParseKeyword(builder, whitespace: null, transition: null))
@@ -736,7 +748,7 @@ private void ParseStandardStatement(in SyntaxListBuilder<RazorSyntaxNode> builde
736748
token.Kind != SyntaxKind.LeftBracket &&
737749
token.Kind != SyntaxKind.RightBrace);
738750

739-
if (At(SyntaxKind.LeftBrace) ||
751+
if ((!Context.FeatureFlags.AllowRazorInAllCodeBlocks && At(SyntaxKind.LeftBrace)) ||
740752
At(SyntaxKind.LeftParenthesis) ||
741753
At(SyntaxKind.LeftBracket))
742754
{
@@ -752,6 +764,11 @@ private void ParseStandardStatement(in SyntaxListBuilder<RazorSyntaxNode> builde
752764
return;
753765
}
754766
}
767+
else if (Context.FeatureFlags.AllowRazorInAllCodeBlocks && At(SyntaxKind.LeftBrace))
768+
{
769+
Accept(read);
770+
return;
771+
}
755772
else if (At(SyntaxKind.Transition) && (NextIs(SyntaxKind.LessThan, SyntaxKind.Colon)))
756773
{
757774
Accept(read);
@@ -847,6 +864,17 @@ private void SetupDirectiveParsers(IEnumerable<DirectiveDescriptor> directiveDes
847864
MapDirectives(ParseTagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword);
848865
MapDirectives(ParseAddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword);
849866
MapDirectives(ParseRemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword);
867+
868+
// If there wasn't any extensible directives relating to the reserved directives then map them.
869+
if (!_directiveParserMap.ContainsKey("class"))
870+
{
871+
MapDirectives(ParseReservedDirective, "class");
872+
}
873+
874+
if (!_directiveParserMap.ContainsKey("namespace"))
875+
{
876+
MapDirectives(ParseReservedDirective, "namespace");
877+
}
850878
}
851879

852880
private void EnsureDirectiveIsAtStartOfLine()
@@ -883,17 +911,6 @@ protected void MapDirectives(Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTr
883911
});
884912

885913
Keywords.Add(directive);
886-
887-
// These C# keywords are reserved for use in directives. It's an error to use them outside of
888-
// a directive. This code removes the error generation if the directive *is* registered.
889-
if (string.Equals(directive, "class", StringComparison.OrdinalIgnoreCase))
890-
{
891-
_keywordParserMap.Remove(CSharpKeyword.Class);
892-
}
893-
else if (string.Equals(directive, "namespace", StringComparison.OrdinalIgnoreCase))
894-
{
895-
_keywordParserMap.Remove(CSharpKeyword.Namespace);
896-
}
897914
}
898915
}
899916

@@ -1409,11 +1426,22 @@ private void ParseExtensibleDirective(in SyntaxListBuilder<RazorSyntaxNode> buil
14091426
ParseDirectiveBlock(directiveBuilder, descriptor, parseChildren: (childBuilder, startingBraceLocation) =>
14101427
{
14111428
NextToken();
1412-
Balance(childBuilder, BalancingModes.NoErrorOnFailure, SyntaxKind.LeftBrace, SyntaxKind.RightBrace, startingBraceLocation);
1413-
SpanContext.ChunkGenerator = new StatementChunkGenerator();
1429+
14141430
var existingEditHandler = SpanContext.EditHandler;
14151431
SpanContext.EditHandler = new CodeBlockEditHandler(Language.TokenizeString);
14161432

1433+
if (Context.FeatureFlags.AllowRazorInAllCodeBlocks)
1434+
{
1435+
var block = new Block(descriptor.Directive, directiveStart);
1436+
ParseCodeBlock(childBuilder, block);
1437+
}
1438+
else
1439+
{
1440+
Balance(childBuilder, BalancingModes.NoErrorOnFailure, SyntaxKind.LeftBrace, SyntaxKind.RightBrace, startingBraceLocation);
1441+
}
1442+
1443+
SpanContext.ChunkGenerator = new StatementChunkGenerator();
1444+
14171445
AcceptMarkerTokenIfNecessary();
14181446

14191447
childBuilder.Add(OutputTokensAsStatementLiteral());
@@ -1607,7 +1635,6 @@ private void SetupKeywordParsers()
16071635
MapKeywords(ParseTryStatement, CSharpKeyword.Try);
16081636
MapKeywords(ParseDoStatement, CSharpKeyword.Do);
16091637
MapKeywords(ParseUsingKeyword, CSharpKeyword.Using);
1610-
MapKeywords(ParseReservedDirective, CSharpKeyword.Class, CSharpKeyword.Namespace);
16111638
}
16121639

16131640
private void MapExpressionKeyword(Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax> handler, CSharpKeyword keyword)

src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CodeBlockEditHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ internal static bool ModifiesInvalidContent(SyntaxNode target, SourceChange chan
7676
{
7777
var relativePosition = change.Span.AbsoluteIndex - target.Position;
7878

79-
if (target.GetContent().IndexOfAny(new[] { '{', '}' }, relativePosition, change.Span.Length) >= 0)
79+
if (target.GetContent().IndexOfAny(new[] { '{', '}', '@', '<', '*', }, relativePosition, change.Span.Length) >= 0)
8080
{
8181
return true;
8282
}
@@ -103,7 +103,7 @@ internal static bool IsAcceptableInsertion(SourceChange change)
103103
// Internal for testing
104104
internal static bool ContainsInvalidContent(SourceChange change)
105105
{
106-
if (change.NewText.IndexOfAny(new[] { '{', '}' }) >= 0)
106+
if (change.NewText.IndexOfAny(new[] { '{', '}', '@', '<', '*', }) >= 0)
107107
{
108108
return true;
109109
}

src/Razor/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public static RazorParserFeatureFlags Create(RazorLanguageVersion version)
1010
var allowMinimizedBooleanTagHelperAttributes = false;
1111
var allowHtmlCommentsInTagHelpers = false;
1212
var allowComponentFileKind = false;
13+
var allowRazorInAllCodeBlocks = false;
1314
var experimental_AllowConditionalDataDashAttributes = false;
1415

1516
if (version.CompareTo(RazorLanguageVersion.Version_2_1) >= 0)
@@ -23,6 +24,7 @@ public static RazorParserFeatureFlags Create(RazorLanguageVersion version)
2324
{
2425
// Added in 3.0
2526
allowComponentFileKind = true;
27+
allowRazorInAllCodeBlocks = true;
2628
}
2729

2830
if (version.CompareTo(RazorLanguageVersion.Experimental) >= 0)
@@ -34,6 +36,7 @@ public static RazorParserFeatureFlags Create(RazorLanguageVersion version)
3436
allowMinimizedBooleanTagHelperAttributes,
3537
allowHtmlCommentsInTagHelpers,
3638
allowComponentFileKind,
39+
allowRazorInAllCodeBlocks,
3740
experimental_AllowConditionalDataDashAttributes);
3841
}
3942

@@ -43,6 +46,8 @@ public static RazorParserFeatureFlags Create(RazorLanguageVersion version)
4346

4447
public abstract bool AllowComponentFileKind { get; }
4548

49+
public abstract bool AllowRazorInAllCodeBlocks { get; }
50+
4651
public abstract bool EXPERIMENTAL_AllowConditionalDataDashAttributes { get; }
4752

4853
private class DefaultRazorParserFeatureFlags : RazorParserFeatureFlags
@@ -51,11 +56,13 @@ public DefaultRazorParserFeatureFlags(
5156
bool allowMinimizedBooleanTagHelperAttributes,
5257
bool allowHtmlCommentsInTagHelpers,
5358
bool allowComponentFileKind,
59+
bool allowRazorInAllCodeBlocks,
5460
bool experimental_AllowConditionalDataDashAttributes)
5561
{
5662
AllowMinimizedBooleanTagHelperAttributes = allowMinimizedBooleanTagHelperAttributes;
5763
AllowHtmlCommentsInTagHelpers = allowHtmlCommentsInTagHelpers;
5864
AllowComponentFileKind = allowComponentFileKind;
65+
AllowRazorInAllCodeBlocks = allowRazorInAllCodeBlocks;
5966
EXPERIMENTAL_AllowConditionalDataDashAttributes = experimental_AllowConditionalDataDashAttributes;
6067
}
6168

@@ -65,6 +72,8 @@ public DefaultRazorParserFeatureFlags(
6572

6673
public override bool AllowComponentFileKind { get; }
6774

75+
public override bool AllowRazorInAllCodeBlocks { get; }
76+
6877
public override bool EXPERIMENTAL_AllowConditionalDataDashAttributes { get; }
6978
}
7079
}

src/Razor/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/CodeGenerationIntegrationTest.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ public void Templates_Runtime()
4646
RunTimeTest();
4747
}
4848

49+
[Fact]
50+
public void Markup_InCodeBlocks_Runtime()
51+
{
52+
RunTimeTest();
53+
}
54+
55+
[Fact]
56+
public void Markup_InCodeBlocksWithTagHelper_Runtime()
57+
{
58+
RunRuntimeTagHelpersTest(TestTagHelperDescriptors.SimpleTagHelperDescriptors);
59+
}
60+
4961
[Fact]
5062
public void StringLiterals_Runtime()
5163
{
@@ -503,6 +515,18 @@ public void Templates_DesignTime()
503515
DesignTimeTest();
504516
}
505517

518+
[Fact]
519+
public void Markup_InCodeBlocks_DesignTime()
520+
{
521+
DesignTimeTest();
522+
}
523+
524+
[Fact]
525+
public void Markup_InCodeBlocksWithTagHelper_DesignTime()
526+
{
527+
RunDesignTimeTagHelpersTest(TestTagHelperDescriptors.SimpleTagHelperDescriptors);
528+
}
529+
506530
[Fact]
507531
public void StringLiterals_DesignTime()
508532
{

src/Razor/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/ComponentCodeGenerationTestBase.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,76 @@ protected ComponentCodeGenerationTestBase()
1919

2020
#region Basics
2121

22+
[Fact]
23+
public void ChildComponent_InFunctionsDirective()
24+
{
25+
// Arrange
26+
AdditionalSyntaxTrees.Add(Parse(@"
27+
using Microsoft.AspNetCore.Components;
28+
29+
namespace Test
30+
{
31+
public class MyComponent : ComponentBase
32+
{
33+
}
34+
}
35+
"));
36+
37+
// Act
38+
var generated = CompileToCSharp(@"
39+
@addTagHelper *, TestAssembly
40+
@using Microsoft.AspNetCore.Components.RenderTree;
41+
42+
@{ RenderChildComponent(builder); }
43+
44+
@functions {
45+
void RenderChildComponent(RenderTreeBuilder builder)
46+
{
47+
<MyComponent />
48+
}
49+
}");
50+
51+
// Assert
52+
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
53+
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
54+
CompileToAssembly(generated);
55+
}
56+
57+
[Fact]
58+
public void ChildComponent_InLocalFunction()
59+
{
60+
// Arrange
61+
AdditionalSyntaxTrees.Add(Parse(@"
62+
using Microsoft.AspNetCore.Components;
63+
64+
namespace Test
65+
{
66+
public class MyComponent : ComponentBase
67+
{
68+
}
69+
}
70+
"));
71+
72+
// Act
73+
var generated = CompileToCSharp(@"
74+
@addTagHelper *, TestAssembly
75+
@using Microsoft.AspNetCore.Components.RenderTree;
76+
@{
77+
void RenderChildComponent()
78+
{
79+
<MyComponent />
80+
}
81+
}
82+
83+
@{ RenderChildComponent(); }
84+
");
85+
86+
// Assert
87+
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
88+
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
89+
CompileToAssembly(generated);
90+
}
91+
2292
[Fact]
2393
public void ChildComponent_Simple()
2494
{

src/Razor/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpBlockTest.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,36 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
88
{
99
public class CSharpBlockTest : ParserTestBase
1010
{
11+
[Fact]
12+
public void LocalFunctionsWithRazor()
13+
{
14+
ParseDocumentTest(
15+
@"@{
16+
void Foo()
17+
{
18+
var time = DateTime.Now
19+
<strong>Hello the time is @time</strong>
20+
}
21+
}");
22+
}
23+
24+
[Fact]
25+
public void LocalFunctionsWithGenerics()
26+
{
27+
ParseDocumentTest(
28+
@"@{
29+
void Foo()
30+
{
31+
<strong>Hello the time is @{ DisplayCount(new List<string>()); }</strong>
32+
}
33+
34+
void DisplayCount<T>(List<T> something)
35+
{
36+
<text>The count is something.Count</text>
37+
}
38+
}");
39+
}
40+
1141
[Fact]
1242
public void NestedCodeBlockWithCSharpAt()
1343
{

0 commit comments

Comments
 (0)