From a1df1702e5376ca3f35164c80f83033ca2b679cc Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Mon, 8 Jun 2015 16:49:53 -0700 Subject: [PATCH] Add support for C# 6 exception filters. - Added new handling of the C# try catch statement to allow exception filters after catch statements. - Added tests to validate valid and invalid scenarios. #402 --- .../Parser/CSharpCodeParser.Statements.cs | 48 +++++- .../Tokenizer/CSharpKeywordDetector.cs | 3 +- .../Tokenizer/Symbols/CSharpKeyword.cs | 3 +- .../Parser/CSharp/CSharpStatementTest.cs | 163 ++++++++++++++++++ .../CSharpTokenizerIdentifierTest.cs | 1 + 5 files changed, 211 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Statements.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Statements.cs index adc738807e..727509249c 100644 --- a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Statements.cs +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Statements.cs @@ -13,6 +13,9 @@ namespace Microsoft.AspNet.Razor.Parser { public partial class CSharpCodeParser { + private static readonly Func IsValidStatementSpacingSymbol = + IsSpacingToken(includeNewLines: true, includeComments: true); + private void SetUpKeywords() { MapKeywords(ConditionalBlock, CSharpKeyword.For, CSharpKeyword.Foreach, CSharpKeyword.While, CSharpKeyword.Switch, CSharpKeyword.Lock); @@ -247,19 +250,19 @@ namespace Microsoft.AspNet.Razor.Parser private void AfterTryClause() { // Grab whitespace - IEnumerable ws = SkipToNextImportantToken(); + var whitespace = SkipToNextImportantToken(); // Check for a catch or finally part if (At(CSharpKeyword.Catch)) { - Accept(ws); + Accept(whitespace); Assert(CSharpKeyword.Catch); - ConditionalBlock(topLevel: false); + FilterableCatchBlock(); AfterTryClause(); } else if (At(CSharpKeyword.Finally)) { - Accept(ws); + Accept(whitespace); Assert(CSharpKeyword.Finally); UnconditionalBlock(); } @@ -267,7 +270,7 @@ namespace Microsoft.AspNet.Razor.Parser { // Return whitespace and end the block PutCurrentBack(); - PutBack(ws); + PutBack(whitespace); Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; } } @@ -344,6 +347,41 @@ namespace Microsoft.AspNet.Razor.Parser ExpectCodeBlock(block); } + private void FilterableCatchBlock() + { + Assert(CSharpKeyword.Catch); + + var block = new Block(CurrentSymbol); + + // Accept "catch" + AcceptAndMoveNext(); + AcceptWhile(IsValidStatementSpacingSymbol); + + // Parse the catch condition if present. If not present, let the C# compiler complain. + if (AcceptCondition()) + { + AcceptWhile(IsValidStatementSpacingSymbol); + + if (At(CSharpKeyword.When)) + { + // Accept "when". + AcceptAndMoveNext(); + AcceptWhile(IsValidStatementSpacingSymbol); + + // Parse the filter condition if present. If not present, let the C# compiler complain. + if (!AcceptCondition()) + { + // Incomplete condition. + return; + } + + AcceptWhile(IsValidStatementSpacingSymbol); + } + + ExpectCodeBlock(block); + } + } + private void ConditionalBlock(bool topLevel) { Assert(CSharpSymbolType.Keyword); diff --git a/src/Microsoft.AspNet.Razor/Tokenizer/CSharpKeywordDetector.cs b/src/Microsoft.AspNet.Razor/Tokenizer/CSharpKeywordDetector.cs index d11b05c8a3..02b7fd8111 100644 --- a/src/Microsoft.AspNet.Razor/Tokenizer/CSharpKeywordDetector.cs +++ b/src/Microsoft.AspNet.Razor/Tokenizer/CSharpKeywordDetector.cs @@ -88,7 +88,8 @@ namespace Microsoft.AspNet.Razor.Tokenizer { "interface", CSharpKeyword.Interface }, { "break", CSharpKeyword.Break }, { "checked", CSharpKeyword.Checked }, - { "namespace", CSharpKeyword.Namespace } + { "namespace", CSharpKeyword.Namespace }, + { "when", CSharpKeyword.When } }; public static CSharpKeyword? SymbolTypeForIdentifier(string id) diff --git a/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpKeyword.cs b/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpKeyword.cs index 62f85210d9..e5a397ceaf 100644 --- a/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpKeyword.cs +++ b/src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpKeyword.cs @@ -82,6 +82,7 @@ namespace Microsoft.AspNet.Razor.Tokenizer.Symbols Interface, Break, Checked, - Namespace + Namespace, + When } } diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpStatementTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpStatementTest.cs index bbf4494d50..8df251ca00 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpStatementTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpStatementTest.cs @@ -137,6 +137,169 @@ namespace Microsoft.AspNet.Razor.Test.Parser.CSharp )); } + public static TheoryData ExceptionFilterData + { + get + { + var factory = SpanFactory.CreateCsHtml(); + + // document, expectedStatement + return new TheoryData + { + { + "@try { someMethod(); } catch(Exception) when (true) { handleIO(); }", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (true) { handleIO(); }") + .AsStatement()) + }, + { + "@try { A(); } catch(Exception) when (true) { B(); } finally { C(); }", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { A(); } catch(Exception) when (true) { B(); } finally { C(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None)) + }, + { + "@try { A(); } catch(Exception) when (true) { B(); } catch(IOException) when (false) { C(); }", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { A(); } catch(Exception) when (true) { B(); } catch(IOException) " + + "when (false) { C(); }") + .AsStatement()) + }, + { + string.Format("@try{0}{{{0} A();{0}}}{0}catch(Exception) when (true)", Environment.NewLine) + + string.Format("{0}{{{0} B();{0}}}{0}catch(IOException) when (false)", Environment.NewLine) + + string.Format("{0}{{{0} C();{0}}}", Environment.NewLine), + new StatementBlock( + factory.CodeTransition(), + factory + .Code( + string.Format("try{0}{{{0} A();{0}}}{0}catch(Exception) ", Environment.NewLine) + + string.Format("when (true){0}{{{0} B();{0}}}{0}", Environment.NewLine) + + string.Format("catch(IOException) when (false){0}{{{0} ", Environment.NewLine) + + string.Format("C();{0}}}", Environment.NewLine)) + .AsStatement()) + }, + + // Wrapped in @{ block. + { + "@{try { someMethod(); } catch(Exception) when (true) { handleIO(); }}", + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + factory + .Code("try { someMethod(); } catch(Exception) when (true) { handleIO(); }") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)) + }, + + // Partial exception filter data + { + "@try { someMethod(); } catch(Exception) when", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when") + .AsStatement()) + }, + { + "@try { someMethod(); } when", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); }") + .AsStatement()) + }, + { + "@try { someMethod(); } catch(Exception) when { anotherMethod(); }", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when { anotherMethod(); }") + .AsStatement()) + }, + { + "@try { someMethod(); } catch(Exception) when (true)", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (true)") + .AsStatement()) + }, + }; + } + } + + [Theory] + [MemberData(nameof(ExceptionFilterData))] + public void ExceptionFilters(string document, StatementBlock expectedStatement) + { + // Act & Assert + ParseBlockTest(document, expectedStatement); + } + + public static TheoryData ExceptionFilterErrorData + { + get + { + var factory = SpanFactory.CreateCsHtml(); + var unbalancedParenErrorString = "An opening \"(\" is missing the corresponding closing \")\"."; + var unbalancedBracketCatchErrorString = "The catch block is missing a closing \"}\" character. " + + "Make sure you have a matching \"}\" character for all the \"{\" characters within this block, " + + "and that none of the \"}\" characters are being interpreted as markup."; + + // document, expectedStatement, expectedErrors + return new TheoryData + { + { + "@try { someMethod(); } catch(Exception) when (", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (") + .AsStatement()), + new[] { new RazorError(unbalancedParenErrorString, 45, 0, 45) } + }, + { + "@try { someMethod(); } catch(Exception) when (someMethod(", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (someMethod(") + .AsStatement()), + new[] { new RazorError(unbalancedParenErrorString, 45, 0, 45) } + }, + { + "@try { someMethod(); } catch(Exception) when (true) {", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (true) {") + .AsStatement()), + new[] { new RazorError(unbalancedBracketCatchErrorString, 23, 0, 23) } + }, + }; + } + } + + [Theory] + [MemberData(nameof(ExceptionFilterErrorData))] + public void ExceptionFilterErrors( + string document, + StatementBlock expectedStatement, + RazorError[] expectedErrors) + { + // Act & Assert + ParseBlockTest(document, expectedStatement, expectedErrors); + } + [Fact] public void FinallyClause() { diff --git a/test/Microsoft.AspNet.Razor.Test/Tokenizer/CSharpTokenizerIdentifierTest.cs b/test/Microsoft.AspNet.Razor.Test/Tokenizer/CSharpTokenizerIdentifierTest.cs index d094622562..1244fe44bc 100644 --- a/test/Microsoft.AspNet.Razor.Test/Tokenizer/CSharpTokenizerIdentifierTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Tokenizer/CSharpTokenizerIdentifierTest.cs @@ -160,6 +160,7 @@ namespace Microsoft.AspNet.Razor.Test.Tokenizer TestKeyword("break", CSharpKeyword.Break); TestKeyword("checked", CSharpKeyword.Checked); TestKeyword("namespace", CSharpKeyword.Namespace); + TestKeyword("when", CSharpKeyword.When); } private void TestKeyword(string keyword, CSharpKeyword keywordType)