Skip to content

Commit cf474c9

Browse files
author
N. Taylor Mullen
committed
Add support for single line control flow statements in code blocks.
- This adds support for C# single line variations of: `if`, `for`, `foreach`, `do`, `while`, `lock` and `using`. - Turns out the existing parser had 99% of the support for these scenarios already. Therefore, in this change set I went ahead and added exhaustive tests to verify things worked end-to-end. - Added a restriction to single line markup to not allow markup in single line control flow statements. Updated resx to provide a better error message for cases when users do use markup in single line control flow statements. #9637
1 parent ef31a96 commit cf474c9

File tree

40 files changed

+2675
-71
lines changed

40 files changed

+2675
-71
lines changed

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

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1756,14 +1756,29 @@ private void ParseExpectedCodeBlock(in SyntaxListBuilder<RazorSyntaxNode> builde
17561756
{
17571757
if (!EndOfFile)
17581758
{
1759-
// Check for "{" to make sure we're at a block
1760-
if (!At(SyntaxKind.LeftBrace))
1759+
// If it's a block control flow statement the current syntax token will be a LeftBrace {,
1760+
// otherwise we're acting on a single line control flow statement which cannot allow markup.
1761+
1762+
if (At(SyntaxKind.LessThan))
1763+
{
1764+
// if (...) <p>Hello World</p>
1765+
Context.ErrorSink.OnError(
1766+
RazorDiagnosticFactory.CreateParsing_SingleLineControlFlowStatementsCannotContainMarkup(
1767+
new SourceSpan(CurrentStart, CurrentToken.Content.Length)));
1768+
}
1769+
else if (At(SyntaxKind.Transition) && NextIs(SyntaxKind.Colon))
1770+
{
1771+
// if (...) @: <p>The time is @DateTime.Now</p>
1772+
Context.ErrorSink.OnError(
1773+
RazorDiagnosticFactory.CreateParsing_SingleLineControlFlowStatementsCannotContainMarkup(
1774+
new SourceSpan(CurrentStart, contentLength: 2 /* @: */)));
1775+
}
1776+
else if (At(SyntaxKind.Transition) && NextIs(SyntaxKind.Transition))
17611777
{
1778+
// if (...) @@JohnDoe <strong>Hi!</strong>
17621779
Context.ErrorSink.OnError(
1763-
RazorDiagnosticFactory.CreateParsing_SingleLineControlFlowStatementsNotAllowed(
1764-
new SourceSpan(CurrentStart, CurrentToken.Content.Length),
1765-
Language.GetSample(SyntaxKind.LeftBrace),
1766-
CurrentToken.Content));
1780+
RazorDiagnosticFactory.CreateParsing_SingleLineControlFlowStatementsCannotContainMarkup(
1781+
new SourceSpan(CurrentStart, contentLength: 2 /* @@ */)));
17671782
}
17681783

17691784
// Parse the statement and then we're done

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,11 @@ public static RazorDiagnostic CreateParsing_ReservedWord(SourceSpan location, st
113113
internal static readonly RazorDiagnosticDescriptor Parsing_SingleLineControlFlowStatementsNotAllowed =
114114
new RazorDiagnosticDescriptor(
115115
$"{DiagnosticPrefix}1008",
116-
() => Resources.ParseError_SingleLine_ControlFlowStatements_Not_Allowed,
116+
() => Resources.ParseError_SingleLine_ControlFlowStatements_CannotContainMarkup,
117117
RazorDiagnosticSeverity.Error);
118-
public static RazorDiagnostic CreateParsing_SingleLineControlFlowStatementsNotAllowed(SourceSpan location, string expected, string actual)
118+
public static RazorDiagnostic CreateParsing_SingleLineControlFlowStatementsCannotContainMarkup(SourceSpan location)
119119
{
120-
return RazorDiagnostic.Create(Parsing_SingleLineControlFlowStatementsNotAllowed, location, expected, actual);
120+
return RazorDiagnostic.Create(Parsing_SingleLineControlFlowStatementsNotAllowed, location);
121121
}
122122

123123
internal static readonly RazorDiagnosticDescriptor Parsing_AtInCodeMustBeFollowedByColonParenOrIdentifierStart =

src/Razor/src/Microsoft.AspNetCore.Razor.Language/Resources.resx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -331,18 +331,8 @@
331331
<data name="ParseError_Sections_Cannot_Be_Nested" xml:space="preserve">
332332
<value>Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed.</value>
333333
</data>
334-
<data name="ParseError_SingleLine_ControlFlowStatements_Not_Allowed" xml:space="preserve">
335-
<value>Expected a "{0}" but found a "{1}". Block statements must be enclosed in "{{" and "}}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed:
336-
337-
@if(isLoggedIn)
338-
&lt;p&gt;Hello, @user&lt;/p&gt;
339-
340-
Instead, wrap the contents of the block in "{{}}":
341-
342-
@if(isLoggedIn) {{
343-
&lt;p&gt;Hello, @user&lt;/p&gt;
344-
}}</value>
345-
<comment>{0} is only ever a single character</comment>
334+
<data name="ParseError_SingleLine_ControlFlowStatements_CannotContainMarkup" xml:space="preserve">
335+
<value>Single-statement control-flow statements in Razor documents statements cannot contain markup. Markup should be enclosed in "{{" and "}}".</value>
346336
</data>
347337
<data name="ParseError_TextTagCannotContainAttributes" xml:space="preserve">
348338
<value>"&lt;text&gt;" and "&lt;/text&gt;" tags cannot contain attributes.</value>

src/Razor/test/RazorLanguage.Test/IntegrationTests/CodeGenerationIntegrationTest.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public CodeGenerationIntegrationTest()
1616
}
1717

1818
#region Runtime
19+
[Fact]
20+
public void SingleLineControlFlowStatements_Runtime()
21+
{
22+
RunTimeTest();
23+
}
24+
1925
[Fact]
2026
public void CSharp8_Runtime()
2127
{
@@ -485,6 +491,12 @@ public void Implements_Runtime()
485491
#endregion
486492

487493
#region DesignTime
494+
[Fact]
495+
public void SingleLineControlFlowStatements_DesignTime()
496+
{
497+
DesignTimeTest();
498+
}
499+
488500
[Fact]
489501
public void CSharp8_DesignTime()
490502
{

src/Razor/test/RazorLanguage.Test/IntegrationTests/ComponentCodeGenerationTestBase.cs

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

2020
#region Basics
2121

22+
[Fact]
23+
public void SingleLineControlFlowStatements_InCodeDirective()
24+
{
25+
// Arrange
26+
27+
// Act
28+
var generated = CompileToCSharp(@"
29+
@using Microsoft.AspNetCore.Components.RenderTree;
30+
31+
@code {
32+
void RenderChildComponent(RenderTreeBuilder builder)
33+
{
34+
var output = string.Empty;
35+
if (builder == null) output = ""Builder is null!"";
36+
else output = ""Builder is not null!"";
37+
<p>Output: @output</p>
38+
}
39+
}");
40+
41+
// Assert
42+
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
43+
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
44+
CompileToAssembly(generated);
45+
}
46+
47+
[Fact]
48+
public void SingleLineControlFlowStatements_InCodeBlock()
49+
{
50+
// Arrange
51+
52+
// Act
53+
var generated = CompileToCSharp(@"
54+
@using Microsoft.AspNetCore.Components.RenderTree;
55+
56+
@{
57+
var output = string.Empty;
58+
if (builder == null) output = ""Builder is null!"";
59+
else output = ""Builder is not null!"";
60+
<p>Output: @output</p>
61+
}");
62+
63+
// Assert
64+
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
65+
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
66+
CompileToAssembly(generated);
67+
}
2268
[Fact]
2369
public void ChildComponent_InFunctionsDirective()
2470
{

src/Razor/test/RazorLanguage.Test/Legacy/CSharpBlockTest.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,35 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
88
{
99
public class CSharpBlockTest : ParserTestBase
1010
{
11+
[Fact]
12+
public void CSharpBlock_SingleLineControlFlowStatement_Error()
13+
{
14+
ParseDocumentTest(
15+
@"@{
16+
var time = DateTime.Now;
17+
if (time.ToBinary() % 2 == 0) <p>The time: @time</p>
18+
19+
if (time.ToBinary() %3 == 0)
20+
// For some reason we want to render the time now?
21+
<p>The confusing time: @time</p>
22+
23+
if (time.ToBinary() % 4 == 0)
24+
@: <p>The time: @time</p>
25+
26+
if (time.ToBinary() % 5 == 0) @@SomeGitHubUserName <strong>Hi!</strong>
27+
}");
28+
}
29+
30+
[Fact]
31+
public void CSharpBlock_SingleLineControlFlowStatement()
32+
{
33+
ParseDocumentTest(
34+
@"@{
35+
var time = DateTime.Now;
36+
if (time.ToBinary() % 2 == 0) @time
37+
}");
38+
}
39+
1140
[Fact]
1241
public void LocalFunctionsWithRazor()
1342
{

src/Razor/test/RazorLanguage.Test/Legacy/CSharpFunctionsTest.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,45 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
88
{
99
public class CSharpFunctionsTest : ParserTestBase
1010
{
11+
[Fact]
12+
public void Functions_SingleLineControlFlowStatement_Error()
13+
{
14+
ParseDocumentTest(
15+
RazorLanguageVersion.Version_3_0,
16+
@"
17+
@functions {
18+
string GetAnnouncmentText(string message)
19+
{
20+
if (message.Length > 0) <p>Message: @message</p>
21+
22+
if (message == null)
23+
// Nothing to render
24+
<p>Message was null</p>
25+
26+
if (DateTime.Now.ToBinary() % 2 == 0)
27+
@: <p>The time: @time</p>
28+
29+
if (message != null) @@SomeGitHubUserName <strong>@message</strong>
30+
}
31+
}
32+
", new[] { FunctionsDirective.Directive, }, designTime: false);
33+
}
34+
35+
[Fact]
36+
public void Functions_SingleLineControlFlowStatement()
37+
{
38+
ParseDocumentTest(
39+
RazorLanguageVersion.Version_3_0,
40+
@"
41+
@functions {
42+
string GetAnnouncmentText(string message)
43+
{
44+
if (message.Length > 0) return ""Anouncement: "" + message;
45+
}
46+
}
47+
", new[] { FunctionsDirective.Directive, }, designTime: false);
48+
}
49+
1150
[Fact]
1251
public void MarkupInFunctionsBlock_DoesNotParseWhenNotSupported()
1352
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<p>Before Text</p>
2+
3+
@{
4+
if (DateTime.Now.ToBinary() % 2 == 0) @("Current time is divisible by 2") else @DateTime.Now
5+
6+
object Bar()
7+
{
8+
if (DateTime.Now.ToBinary() % 2 == 0)
9+
return "Current time is divisible by 2";
10+
else if (DateTime.Now.ToBinary() % 3 == 0)
11+
return "Current time is divisible by 3";
12+
else
13+
return DateTime.Now;
14+
}
15+
16+
for (var i = 0; i < 10; i++)
17+
// Incrementing a number
18+
i--;
19+
20+
@foreach (var item in new[] {"hello"})
21+
@item
22+
23+
do
24+
@currentCount
25+
while (--currentCount >= 0);
26+
27+
while (--currentCount <= 10)
28+
currentCount++;
29+
30+
using (var reader = new System.IO.StreamReader("/something"))
31+
@reader.ReadToEnd()
32+
33+
@lock (this)
34+
currentCount++;
35+
}
36+
37+
@functions {
38+
public string Foo()
39+
{
40+
var x = "";
41+
42+
if (DateTime.Now.ToBinary() % 2 == 0)
43+
return "Current time is divisible by 2";
44+
else
45+
return "It isn't divisible by two";
46+
47+
for (var i = 0; i < 10; i++)
48+
// Incrementing a number
49+
i--;
50+
51+
foreach (var item in new[] {"hello"})
52+
@item
53+
54+
do
55+
@currentCount
56+
while (--currentCount >= 0);
57+
58+
while (--currentCount <= 10)
59+
currentCount++;
60+
61+
using (var reader = new System.IO.StreamReader("/something"))
62+
@reader.ReadToEnd()
63+
64+
lock (this)
65+
currentCount++;
66+
}
67+
68+
int currentCount = 0;
69+
70+
public void IncrementCount()
71+
{
72+
if (true) currentCount++;
73+
}
74+
75+
}
76+
77+
@for (var i = 0; i < 10; i++)
78+
@i
79+
80+
@foreach (var item in new[] {"hello"})
81+
@item
82+
83+
@do
84+
@currentCount
85+
while (--currentCount >= 0);
86+
87+
@while (--currentCount <= 10)
88+
currentCount++;
89+
90+
@using (var reader = new System.IO.StreamReader("/something"))
91+
// Reading the entire file
92+
@reader.ReadToEnd()
93+
94+
@lock (this)
95+
currentCount++;
96+
97+
@if (true) @@GitHubUserName <p>Hello!</p>
98+
99+
@if (true)
100+
@: <p>The time is @DateTime.Now</p>
101+
102+
<p>After Text</p>

0 commit comments

Comments
 (0)