Skip to content

Commit

Permalink
Handle razor @: transitions as comments (#72745)
Browse files Browse the repository at this point in the history
* Handle razor `@:` transitions as comments

In Razor, `@:` represents an unconditional transition to HTML for the rest of a given line. In order to handle this gracefully when using the C# lexer in Razor, we'll take the same approach we did for `@* *@` comments, except this time treating it as if it was a `//` comment.

* Remove stray whitespace

* Add a few more tests

* Handle non `@*` and `@:` cases correctly.
  • Loading branch information
333fred committed Mar 29, 2024
1 parent 294969d commit 696b3e0
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 9 deletions.
38 changes: 29 additions & 9 deletions src/Compilers/CSharp/Portable/Parser/Lexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1909,9 +1909,7 @@ private void LexSyntaxTrivia(bool afterFirstToken, bool isTrailing, ref SyntaxLi
}

// normal single line comment
this.ScanToEndOfLine();
var text = TextWindow.GetText(false);
this.AddTrivia(SyntaxFactory.Comment(text), ref triviaList);
lexSingleLineComment(ref triviaList);
onlyWhitespaceOnLine = false;
break;
}
Expand All @@ -1938,12 +1936,27 @@ private void LexSyntaxTrivia(bool afterFirstToken, bool isTrailing, ref SyntaxLi

// not trivia
return;
case '@' when TextWindow.PeekChar(1) == '*':
// Razor comment. We pretend that it's a multi-line comment for error recovery, but it's an error case.
this.AddError(TextWindow.Position, width: 1, ErrorCode.ERR_UnexpectedCharacter, '@');
lexMultiLineComment(ref triviaList, delimiter: '@');
onlyWhitespaceOnLine = false;
break;
case '@':
if ((ch = TextWindow.PeekChar(1)) == '*')
{
// Razor comment. We pretend that it's a multi-line comment for error recovery, but it's an error case.
this.AddError(TextWindow.Position, width: 1, ErrorCode.ERR_UnexpectedCharacter, '@');
lexMultiLineComment(ref triviaList, delimiter: '@');
onlyWhitespaceOnLine = false;
break;
}
else if (ch == ':')
{
// Razor HTML transition. We pretend it's a single-line comment for error recovery.
this.AddError(TextWindow.Position, width: 1, ErrorCode.ERR_UnexpectedCharacter, '@');
lexSingleLineComment(ref triviaList);
onlyWhitespaceOnLine = false;
break;
}
else
{
return;
}
case '\r':
case '\n':
var endOfLine = this.ScanEndOfLine();
Expand Down Expand Up @@ -1992,6 +2005,13 @@ private void LexSyntaxTrivia(bool afterFirstToken, bool isTrailing, ref SyntaxLi
}
}

void lexSingleLineComment(ref SyntaxListBuilder triviaList)
{
this.ScanToEndOfLine();
var text = TextWindow.GetText(false);
this.AddTrivia(SyntaxFactory.Comment(text), ref triviaList);
}

void lexMultiLineComment(ref SyntaxListBuilder triviaList, char delimiter)
{
bool isTerminated;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,51 @@ public static int Main ()
Diagnostic(ErrorCode.ERR_RbraceExpected, "").WithLocation(5, 25));
}

[Fact]
public void CS1035AtColonParsedAsComment_01()
{
var test = """
var x = @:;
""";

ParsingTests.ParseAndValidate(test,
// (1,9): error CS1056: Unexpected character '@'
// var x = @:;
Diagnostic(ErrorCode.ERR_UnexpectedCharacter, "@").WithArguments("@").WithLocation(1, 9),
// (1,12): error CS1733: Expected expression
// var x = @:;
Diagnostic(ErrorCode.ERR_ExpressionExpected, "").WithLocation(1, 12),
// (1,12): error CS1002: ; expected
// var x = @:;
Diagnostic(ErrorCode.ERR_SemicolonExpected, "").WithLocation(1, 12));
}

[Fact]
public void CS1035AtColonParsedAsComment_02()
{
var test = """
@:<div>test</div>
""";

ParsingTests.ParseAndValidate(test,
// (1,1): error CS1056: Unexpected character '@'
// @:<div>test</div>
Diagnostic(ErrorCode.ERR_UnexpectedCharacter, "@").WithArguments("@").WithLocation(1, 1));
}

[Fact]
public void CS1035AtColonParsedAsComment_03()
{
var test = """
@: M() {}
""";

ParsingTests.ParseAndValidate(test,
// (1,1): error CS1056: Unexpected character '@'
// @: M() {}
Diagnostic(ErrorCode.ERR_UnexpectedCharacter, "@").WithArguments("@").WithLocation(1, 1));
}

[Fact, WorkItem(526993, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/526993")]
public void CS1039ERR_UnterminatedStringLit()
{
Expand Down
137 changes: 137 additions & 0 deletions src/Compilers/CSharp/Test/Syntax/LexicalAndXml/LexicalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,143 @@ public void TestMixedMultiLineCommentTerminators_01(char outsideDelimiter, char
Assert.Equal(SyntaxKind.MultiLineCommentTrivia, trivia[0].Kind());
}

[Fact]
[Trait("Feature", "Comments")]
public void TestAtColonTreatedAsComment_RazorRecovery()
{
var text = "@: More text";
var token = LexToken(text);

Assert.NotEqual(default, token);
Assert.Equal(SyntaxKind.EndOfFileToken, token.Kind());
Assert.Equal(text, token.ToFullString());
var errors = token.Errors();
errors.Verify(
// error CS1056: Unexpected character '@'
TestBase.Diagnostic(ErrorCode.ERR_UnexpectedCharacter).WithArguments("@").WithLocation(1, 1));
var trivia = token.GetLeadingTrivia().ToArray();
Assert.Equal(1, trivia.Length);
Assert.NotEqual(default, trivia[0]);
Assert.Equal(SyntaxKind.SingleLineCommentTrivia, trivia[0].Kind());
}

[Fact]
[Trait("Feature", "Comments")]
public void TestAtColonTreatedAsCommentAsTrailingTrivia_RazorRecovery()
{
var text = """
Identifier @: More text
// Regular comment
SecondIdentifier
""";
var tokens = Lex(text).ToList();
var token = tokens[0];

Assert.NotEqual(default, token);
Assert.Equal(SyntaxKind.IdentifierToken, token.Kind());
var errors = token.Errors();
errors.Verify(
// error CS1056: Unexpected character '@'
TestBase.Diagnostic(ErrorCode.ERR_UnexpectedCharacter).WithArguments("@").WithLocation(1, 1));
var trivia = token.GetLeadingTrivia().ToArray();
Assert.Equal(0, trivia.Length);
trivia = token.GetTrailingTrivia().ToArray();
Assert.Equal(3, trivia.Length);
Assert.NotEqual(default, trivia[1]);
Assert.Equal(SyntaxKind.SingleLineCommentTrivia, trivia[1].Kind());
Assert.Equal("@: More text", trivia[1].ToFullString());

token = tokens[1];
Assert.NotEqual(default, token);
Assert.Equal(SyntaxKind.IdentifierToken, token.Kind());
Assert.Equal("""
// Regular comment
SecondIdentifier
""", token.ToFullString());
}

[Fact]
[Trait("Feature", "Comments")]
public void TestAtColonTreatedAsComment_TrailingMultiLine_RazorRecovery()
{
var text = """
@: /*
Identifier
*/
""";

var tokens = Lex(text).ToList();
var token = tokens[0];

Assert.NotEqual(default, token);
Assert.Equal(SyntaxKind.IdentifierToken, token.Kind());
Assert.Equal("""
@: /*
Identifier

""", token.ToFullString());
var errors = token.Errors();
errors.Verify(
// error CS1056: Unexpected character '@'
TestBase.Diagnostic(ErrorCode.ERR_UnexpectedCharacter).WithArguments("@").WithLocation(1, 1));
var trivia = token.GetLeadingTrivia().ToArray();
Assert.Equal(2, trivia.Length);
Assert.NotEqual(default, trivia[0]);
Assert.Equal(SyntaxKind.SingleLineCommentTrivia, trivia[0].Kind());
Assert.Equal("@: /*", trivia[0].ToFullString());
}

[Fact]
[Trait("Feature", "Comments")]
public void TestAtColonTreatedAsComment_PreprocessorDisabled_RazorRecovery()
{
var text = """
#if false
@:
#endif
""";

var token = LexToken(text);

Assert.NotEqual(default, token);
Assert.Equal(SyntaxKind.EndOfFileToken, token.Kind());
Assert.Equal(text, token.ToFullString());
var errors = token.Errors();
errors.Verify();
var trivia = token.GetLeadingTrivia().ToArray();
Assert.Equal(3, trivia.Length);
Assert.Equal(SyntaxKind.IfDirectiveTrivia, trivia[0].Kind());
Assert.Equal(SyntaxKind.DisabledTextTrivia, trivia[1].Kind());
Assert.Equal(SyntaxKind.EndIfDirectiveTrivia, trivia[2].Kind());
}

[Fact]
[Trait("Feature", "Comments")]
public void TestAtColonTreatedAsComment_PreprocessorEnabled_RazorRecovery()
{
var text = """
#if true
@:
#endif
""";

var token = LexToken(text);

Assert.NotEqual(default, token);
Assert.Equal(SyntaxKind.EndOfFileToken, token.Kind());
Assert.Equal(text, token.ToFullString());
var errors = token.Errors();
errors.Verify(
// error CS1056: Unexpected character '@'
TestBase.Diagnostic(ErrorCode.ERR_UnexpectedCharacter).WithArguments("@").WithLocation(1, 1));
var trivia = token.GetLeadingTrivia().ToArray();
Assert.Equal(4, trivia.Length);
Assert.Equal(SyntaxKind.IfDirectiveTrivia, trivia[0].Kind());
Assert.Equal(SyntaxKind.SingleLineCommentTrivia, trivia[1].Kind());
Assert.Equal(SyntaxKind.EndOfLineTrivia, trivia[2].Kind());
Assert.Equal(SyntaxKind.EndIfDirectiveTrivia, trivia[3].Kind());
}

[Fact]
[Trait("Feature", "Comments")]
public void TestCommentWithTextWindowSentinel()
Expand Down

0 comments on commit 696b3e0

Please sign in to comment.