From 58c0a36200727b449996242ec9976f8f9fd1e612 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Mon, 18 May 2015 15:29:13 -0700 Subject: [PATCH] Add support for null conditional operators in implicit expressions. - Added case in ImplicitExpression handling to understand question marks. - text?. is special compared to text. because with text. we currently validate content after text. to determine if it's an expression or if it's a period. Now with ?. we always treat it as an expression because ?. is not a useful sentance of any kind. - Added unit tests to validate new implicit expression handling - Added runtime and design time code generation tests to validate null conditional operators. #44 --- .../Parser/CSharpCodeParser.cs | 27 +++- .../Generator/CSharpRazorCodeGeneratorTest.cs | 26 ++++ .../CSharp/CSharpImplicitExpressionTest.cs | 91 +++++++++++++ .../NullConditionalExpressions.DesignTime.cs | 99 ++++++++++++++ .../CS/Output/NullConditionalExpressions.cs | 122 ++++++++++++++++++ .../Source/NullConditionalExpressions.cshtml | 11 ++ 6 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/NullConditionalExpressions.DesignTime.cs create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/NullConditionalExpressions.cs create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/NullConditionalExpressions.cshtml diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs index a2290e42e6..6ff789c69e 100644 --- a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs @@ -388,7 +388,32 @@ namespace Microsoft.AspNet.Razor.Parser } return MethodCallOrArrayIndex(acceptedCharacters); } - if (CurrentSymbol.Type == CSharpSymbolType.Dot) + if (At(CSharpSymbolType.QuestionMark)) + { + var next = Lookahead(count: 1); + + if (next != null) + { + if (next.Type == CSharpSymbolType.Dot) + { + // Accept null conditional dot operator (?.). + AcceptAndMoveNext(); + AcceptAndMoveNext(); + + // If the next piece after the ?. is a keyword or identifier then we want to continue. + return At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword); + } + else if (next.Type == CSharpSymbolType.LeftBracket) + { + // We're at the ? for a null conditional bracket operator (?[). + AcceptAndMoveNext(); + + // Accept the [ and any content inside (it will attempt to balance). + return MethodCallOrArrayIndex(acceptedCharacters); + } + } + } + else if (At(CSharpSymbolType.Dot)) { var dot = CurrentSymbol; if (NextToken()) diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpRazorCodeGeneratorTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpRazorCodeGeneratorTest.cs index f0be9641cd..4c0330d1f8 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpRazorCodeGeneratorTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpRazorCodeGeneratorTest.cs @@ -49,6 +49,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator } [Theory] + [InlineData("NullConditionalExpressions")] [InlineData("NestedCodeBlocks")] [InlineData("CodeBlock")] [InlineData("ExplicitExpression")] @@ -72,6 +73,31 @@ namespace Microsoft.AspNet.Razor.Test.Generator RunTest(testType); } + [Fact] + public void CSharpCodeGeneratorCorrectlyGeneratesMappingsForNullConditionalOperator() + { + RunTest("NullConditionalExpressions", + "NullConditionalExpressions.DesignTime", + designTimeMode: true, + tabTest: TabTest.NoTabs, + expectedDesignTimePragmas: new List() + { + BuildLineMapping(2, 0, 564, 22, 2, 6), + BuildLineMapping(9, 1, 5, 656, 29, 6, 13), + BuildLineMapping(22, 1, 766, 34, 18, 6), + BuildLineMapping(29, 2, 5, 858, 41, 6, 22), + BuildLineMapping(51, 2, 986, 46, 27, 6), + BuildLineMapping(58, 3, 5, 1078, 53, 6, 26), + BuildLineMapping(84, 3, 1214, 58, 31, 6), + BuildLineMapping(91, 4, 5, 1306, 65, 6, 41), + BuildLineMapping(132, 4, 1472, 70, 46, 2), + BuildLineMapping(140, 7, 1, 1558, 76, 6, 13), + BuildLineMapping(156, 8, 1, 1656, 81, 6, 22), + BuildLineMapping(181, 9, 1, 1764, 86, 6, 26), + BuildLineMapping(210, 10, 1, 1876, 91, 6, 41) + }); + } + [Fact] public void CSharpCodeGeneratorCorrectlyGeneratesMappingsForAwait() { diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpImplicitExpressionTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpImplicitExpressionTest.cs index 9627f88e52..750c4d5e1b 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpImplicitExpressionTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpImplicitExpressionTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Test.Framework; @@ -17,6 +18,96 @@ namespace Microsoft.AspNet.Razor.Test.Parser.CSharp return new CSharpCodeParser(); } + public static TheoryData NullConditionalOperatorData_Bracket + { + get + { + var noErrors = new RazorError[0]; + Func missingEndParenError = (index) => + new RazorError[1] + { + new RazorError("An opening \"(\" is missing the corresponding closing \")\".", index, 0, index) + }; + Func missingEndBracketError = (index) => + new RazorError[1] + { + new RazorError("An opening \"[\" is missing the corresponding closing \"]\".", index, 0, index) + }; + + // implicitExpression, expectedImplicitExpression, acceptedCharacters, expectedErrors + return new TheoryData + { + { "val??[", "val", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val??[0", "val", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[", "val?[", AcceptedCharacters.Any, missingEndBracketError(5) }, + { "val?(", "val", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[more", "val?[more", AcceptedCharacters.Any, missingEndBracketError(5) }, + { "val?[0]", "val?[0]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[

", "val?[", AcceptedCharacters.Any, missingEndBracketError(5) }, + { "val?[more.

", "val?[more.", AcceptedCharacters.Any, missingEndBracketError(5) }, + { "val??[more

", "val", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[-1]?", "val?[-1]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[abc]?[def", "val?[abc]?[def", AcceptedCharacters.Any, missingEndBracketError(11) }, + { "val?[abc]?[2]", "val?[abc]?[2]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[abc]?.more?[def]", "val?[abc]?.more?[def]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[abc]?.more?.abc", "val?[abc]?.more?.abc", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[null ?? true]", "val?[null ?? true]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[abc?.gef?[-1]]", "val?[abc?.gef?[-1]]", AcceptedCharacters.NonWhiteSpace, noErrors }, + }; + } + } + + [Theory] + [MemberData(nameof(NullConditionalOperatorData_Bracket))] + public void ParseBlockMethodParsesNullConditionalOperatorImplicitExpression_Bracket( + string implicitExpresison, + string expectedImplicitExpression, + AcceptedCharacters acceptedCharacters, + RazorError[] expectedErrors) + { + // Act & Assert + ImplicitExpressionTest( + implicitExpresison, + expectedImplicitExpression, + acceptedCharacters, + expectedErrors); + } + + public static TheoryData NullConditionalOperatorData_Dot + { + get + { + // implicitExpression, expectedImplicitExpression + return new TheoryData + { + { "val?", "val" }, + { "val??", "val" }, + { "val??more", "val" }, + { "val?!", "val" }, + { "val?.", "val?." }, + { "val??.", "val" }, + { "val?.(abc)", "val?." }, + { "val?.

", "val?." }, + { "val?.more", "val?.more" }, + { "val?.more

", "val?.more" }, + { "val??.more

", "val" }, + { "val?.more(false)?.

", "val?.more(false)?." }, + { "val?.more(false)?.abc", "val?.more(false)?.abc" }, + { "val?.more(null ?? true)?.abc", "val?.more(null ?? true)?.abc" }, + }; + } + } + + [Theory] + [MemberData(nameof(NullConditionalOperatorData_Dot))] + public void ParseBlockMethodParsesNullConditionalOperatorImplicitExpression_Dot( + string implicitExpresison, + string expectedImplicitExpression) + { + // Act & Assert + ImplicitExpressionTest(implicitExpresison, expectedImplicitExpression); + } + [Fact] public void NestedImplicitExpression() { diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/NullConditionalExpressions.DesignTime.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/NullConditionalExpressions.DesignTime.cs new file mode 100644 index 0000000000..c9c332648d --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/NullConditionalExpressions.DesignTime.cs @@ -0,0 +1,99 @@ +namespace TestOutput +{ + using System; + using System.Threading.Tasks; + + public class NullConditionalExpressions + { + private static object @__o; + private void @__RazorDesignTimeHelpers__() + { + #pragma warning disable 219 + #pragma warning restore 219 + } + #line hidden + public NullConditionalExpressions() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { +#line 1 "NullConditionalExpressions.cshtml" + + + +#line default +#line hidden + +#line 2 "NullConditionalExpressions.cshtml" +__o = ViewBag?.Data; + +#line default +#line hidden +#line 2 "NullConditionalExpressions.cshtml" + + + +#line default +#line hidden + +#line 3 "NullConditionalExpressions.cshtml" +__o = ViewBag.IntIndexer?[0]; + +#line default +#line hidden +#line 3 "NullConditionalExpressions.cshtml" + + + +#line default +#line hidden + +#line 4 "NullConditionalExpressions.cshtml" +__o = ViewBag.StrIndexer?["key"]; + +#line default +#line hidden +#line 4 "NullConditionalExpressions.cshtml" + + + +#line default +#line hidden + +#line 5 "NullConditionalExpressions.cshtml" +__o = ViewBag?.Method(Value?[23]?.More)?["key"]; + +#line default +#line hidden +#line 5 "NullConditionalExpressions.cshtml" + + +#line default +#line hidden + +#line 8 "NullConditionalExpressions.cshtml" +__o = ViewBag?.Data; + +#line default +#line hidden +#line 9 "NullConditionalExpressions.cshtml" +__o = ViewBag.IntIndexer?[0]; + +#line default +#line hidden +#line 10 "NullConditionalExpressions.cshtml" +__o = ViewBag.StrIndexer?["key"]; + +#line default +#line hidden +#line 11 "NullConditionalExpressions.cshtml" +__o = ViewBag?.Method(Value?[23]?.More)?["key"]; + +#line default +#line hidden + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/NullConditionalExpressions.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/NullConditionalExpressions.cs new file mode 100644 index 0000000000..1dff885ddf --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/NullConditionalExpressions.cs @@ -0,0 +1,122 @@ +#pragma checksum "NullConditionalExpressions.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "c8c4f34e0768aea12ef6ce8e3fe0e384ad023faf" +namespace TestOutput +{ + using System; + using System.Threading.Tasks; + + public class NullConditionalExpressions + { + #line hidden + public NullConditionalExpressions() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { +#line 1 "NullConditionalExpressions.cshtml" + + + +#line default +#line hidden + + Instrumentation.BeginContext(9, 13, false); +#line 2 "NullConditionalExpressions.cshtml" +Write(ViewBag?.Data); + +#line default +#line hidden + Instrumentation.EndContext(); +#line 2 "NullConditionalExpressions.cshtml" + + + +#line default +#line hidden + + Instrumentation.BeginContext(29, 22, false); +#line 3 "NullConditionalExpressions.cshtml" +Write(ViewBag.IntIndexer?[0]); + +#line default +#line hidden + Instrumentation.EndContext(); +#line 3 "NullConditionalExpressions.cshtml" + + + +#line default +#line hidden + + Instrumentation.BeginContext(58, 26, false); +#line 4 "NullConditionalExpressions.cshtml" +Write(ViewBag.StrIndexer?["key"]); + +#line default +#line hidden + Instrumentation.EndContext(); +#line 4 "NullConditionalExpressions.cshtml" + + + +#line default +#line hidden + + Instrumentation.BeginContext(91, 41, false); +#line 5 "NullConditionalExpressions.cshtml" +Write(ViewBag?.Method(Value?[23]?.More)?["key"]); + +#line default +#line hidden + Instrumentation.EndContext(); +#line 5 "NullConditionalExpressions.cshtml" + + +#line default +#line hidden + + Instrumentation.BeginContext(135, 4, true); + WriteLiteral("\r\n\r\n"); + Instrumentation.EndContext(); + Instrumentation.BeginContext(140, 13, false); +#line 8 "NullConditionalExpressions.cshtml" +Write(ViewBag?.Data); + +#line default +#line hidden + Instrumentation.EndContext(); + Instrumentation.BeginContext(153, 2, true); + WriteLiteral("\r\n"); + Instrumentation.EndContext(); + Instrumentation.BeginContext(156, 22, false); +#line 9 "NullConditionalExpressions.cshtml" +Write(ViewBag.IntIndexer?[0]); + +#line default +#line hidden + Instrumentation.EndContext(); + Instrumentation.BeginContext(178, 2, true); + WriteLiteral("\r\n"); + Instrumentation.EndContext(); + Instrumentation.BeginContext(181, 26, false); +#line 10 "NullConditionalExpressions.cshtml" +Write(ViewBag.StrIndexer?["key"]); + +#line default +#line hidden + Instrumentation.EndContext(); + Instrumentation.BeginContext(207, 2, true); + WriteLiteral("\r\n"); + Instrumentation.EndContext(); + Instrumentation.BeginContext(210, 41, false); +#line 11 "NullConditionalExpressions.cshtml" +Write(ViewBag?.Method(Value?[23]?.More)?["key"]); + +#line default +#line hidden + Instrumentation.EndContext(); + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/NullConditionalExpressions.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/NullConditionalExpressions.cshtml new file mode 100644 index 0000000000..fa87620317 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/NullConditionalExpressions.cshtml @@ -0,0 +1,11 @@ +@{ + @ViewBag?.Data + @ViewBag.IntIndexer?[0] + @ViewBag.StrIndexer?["key"] + @ViewBag?.Method(Value?[23]?.More)?["key"] +} + +@ViewBag?.Data +@ViewBag.IntIndexer?[0] +@ViewBag.StrIndexer?["key"] +@ViewBag?.Method(Value?[23]?.More)?["key"] \ No newline at end of file