From a961892fc232ea76c1f1c4787481f04bdb6b4778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Tue, 30 Mar 2021 14:03:37 +0200 Subject: [PATCH 01/14] Added support for interpolated string to parser --- .../Parser/Binding/BindingTokenizerTests.cs | 10 ++++++++++ .../Parser/Binding/Tokenizer/BindingTokenType.cs | 1 + .../Parser/Binding/Tokenizer/BindingTokenizer.cs | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingTokenizerTests.cs b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingTokenizerTests.cs index c146a7dabf..75cdfdfa1e 100644 --- a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingTokenizerTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingTokenizerTests.cs @@ -239,6 +239,16 @@ public void BindingTokenizer_MultiblockExpression_BunchingOperators_Valid() Assert.AreEqual(index, tokens.Count); } + [TestMethod] + [DataRow("$\"String {Arg}\"")] + [DataRow("$'String {Arg}'")] + public void BindingTokenizer_InterpolatedString_Valid(string expression) + { + var tokens = Tokenize(expression); + Assert.AreEqual(1, tokens.Count); + Assert.AreEqual(BindingTokenType.InterpolatedStringToken, tokens[0].Type); + } + [TestMethod] public void BindingTokenizer_UnaryOperator_BunchingOperators_Valid() { diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenType.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenType.cs index 99e966f28e..ed47387cf9 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenType.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenType.cs @@ -30,6 +30,7 @@ public enum BindingTokenType NotOperator, StringLiteralToken, + InterpolatedStringToken, NullCoalescingOperator, QuestionMarkOperator, diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenizer.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenizer.cs index 5cfeb3ecf5..b61af0f6ac 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenizer.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenizer.cs @@ -211,11 +211,23 @@ private void TokenizeBindingValue() } break; + case '$': case '\'': case '"': - FinishIncompleteIdentifier(); + var bindingTokenType = default(BindingTokenType); + if (ch == '$') + { + Read(); + bindingTokenType = BindingTokenType.InterpolatedStringToken; + } + else + { + FinishIncompleteIdentifier(); + bindingTokenType = BindingTokenType.StringLiteralToken; + } + ReadStringLiteral(out var errorMessage); - CreateToken(BindingTokenType.StringLiteralToken, errorProvider: t => CreateTokenError(t, errorMessage ?? "unknown error")); + CreateToken(bindingTokenType, errorProvider: t => CreateTokenError(t, errorMessage ?? "unknown error")); break; case '?': From a42d14e28c1c18657b8e8e2a1484a445fedfa3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Tue, 30 Mar 2021 16:43:05 +0200 Subject: [PATCH 02/14] Added initial version of interpolated string parser --- .../Parser/Binding/BindingParserTests.cs | 11 +++ .../Parser/Binding/Parser/BindingParser.cs | 68 +++++++++++++++++++ .../InterpolatedStringBindingParserNode.cs | 29 ++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/DotVVM.Framework/Compilation/Parser/Binding/Parser/InterpolatedStringBindingParserNode.cs diff --git a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs index 3cd1d2e206..3b68d7b22b 100644 --- a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs @@ -177,6 +177,17 @@ public void BindingParser_StringLiteral_Valid() Assert.AreEqual("help\"help", ((LiteralExpressionBindingParserNode)result).Value); } + [TestMethod] + public void BindingParser_InterpolatedString_Valid() + { + var result = bindingParserNodeFactory.Parse("$\"Hello {Argument1} with {Argument2}!\"") as InterpolatedStringBindingParserNode; + Assert.AreEqual("Hello {0} with {1}!", result.Format); + Assert.IsFalse(result.HasNodeErrors); + Assert.AreEqual(2, result.Arguments.Count); + Assert.AreEqual("Argument1", ((SimpleNameBindingParserNode)result.Arguments[0]).Name); + Assert.AreEqual("Argument2", ((SimpleNameBindingParserNode)result.Arguments[1]).Name); + } + [TestMethod] public void BindingParser_StringLiteral_SingleQuotes_Valid() { diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index 580c160d5f..36e8340cfc 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -597,6 +597,21 @@ private BindingParserNode ReadAtomicExpression() } return node; } + else if (token != null && token.Type == BindingTokenType.InterpolatedStringToken) + { + // interpolated string + + Read(); + + var (format, arguments) = ParseInterpolatedString(token.Text, out var error); + var node = CreateNode(new InterpolatedStringBindingParserNode(format, arguments), startIndex); + if (error != null) + { + node.NodeErrors.Add(error); + } + + return node; + } else { // identifier @@ -883,6 +898,59 @@ private static string ParseStringLiteral(string text, out string? error) return sb.ToString(); } + private static (string, List) ParseInterpolatedString(string text, out string? error) + { + var preprocessedString = ParseStringLiteral(text, out error).Substring(1); + + bool TryParseArgument(int from, out int end, out BindingParserNode? argument) + { + var index = from; + while (preprocessedString[index++] != '}') + { + if (index == preprocessedString.Length) + { + end = -1; + argument = default; + return false; + } + } + + var tokenizer = new BindingTokenizer(); + tokenizer.Tokenize(preprocessedString.Substring(from + 1, index - (from + 2))); + var parser = new BindingParser() { Tokens = tokenizer.Tokens }; + argument = parser.ReadConditionalExpression(); + end = index; + return argument != null; + } + + var index = 0; + var sb = new StringBuilder(); + var arguments = new List(); + while (index < preprocessedString.Length) + { + if (preprocessedString[index] == '{') + { + if (!TryParseArgument(index, out var end, out var argument)) + { + arguments.Clear(); + error = "Interpolated string is malformed."; + return (string.Empty, arguments); + } + arguments.Add(argument!); + sb.Append("{" + (arguments.Count - 1).ToString() + "}"); + index = end; + continue; + } + else + { + sb.Append(preprocessedString[index]); + index++; + } + } + + return (sb.ToString(), arguments); + } + private T CreateNode(T node, int startIndex, string? error = null) where T : BindingParserNode { node.Tokens.Clear(); diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/InterpolatedStringBindingParserNode.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/InterpolatedStringBindingParserNode.cs new file mode 100644 index 0000000000..b5e7d0b363 --- /dev/null +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/InterpolatedStringBindingParserNode.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.Utils; + +namespace DotVVM.Framework.Compilation.Parser.Binding.Parser +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class InterpolatedStringBindingParserNode : BindingParserNode + { + public string Format { get; set; } + public List Arguments { get; set; } + + public InterpolatedStringBindingParserNode(string format, List arguments) + { + this.Format = format; + this.Arguments = arguments; + } + + public override IEnumerable EnumerateChildNodes() + => base.EnumerateNodes().Concat(Arguments); + + public override string ToDisplayString() + => $"String.Format(\"{Format}\", {Arguments.Select(arg => arg.ToDisplayString()).StringJoin(", ")})"; + } +} From 4a65117d176ca5d6b6a85562b4b0a85962b47713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Wed, 31 Mar 2021 10:02:59 +0200 Subject: [PATCH 03/14] ExpressionBuilderVisitor now recognizes interpolated strings --- .../Binding/BindingCompilationTests.cs | 10 ++++++++++ .../Binding/ExpressionBuildingVisitor.cs | 16 ++++++++++++++++ .../Binding/Parser/BindingParserNodeVisitor.cs | 9 +++++++++ 3 files changed, 35 insertions(+) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index 3cb6d3536b..680a073e0d 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -130,6 +130,16 @@ public void BindingCompiler_Valid_StringLiteralInSingleQuotes() Assert.AreEqual(ExecuteBinding("StringProp + 'def'", viewModel), "abcdef"); } + [TestMethod] + [DataRow(@"$""Interpolated {StringProp} {StringProp}""", "Interpolated abc abc")] + [DataRow(@"$'Interpolated {StringProp} {StringProp}'", "Interpolated abc abc")] + public void BindingCompiler_Valid_InterpolatedString(string expression, string evaluated) + { + var viewModel = new TestViewModel() { StringProp = "abc" }; + var binding = ExecuteBinding(expression, viewModel); + Assert.AreEqual(evaluated, binding); + } + [TestMethod] public void BindingCompiler_Valid_PropertyProperty() { diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index a16ad9fb4c..e4fa8da690 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -105,6 +105,22 @@ protected override Expression VisitLiteralExpression(LiteralExpressionBindingPar return Expression.Constant(node.Value); } + protected override Expression VisitInterpolatedStringExpression(InterpolatedStringBindingParserNode node) + { + var target = new MethodGroupExpression() { + MethodName = nameof(String.Format), + Target = Registry.Resolve(nameof(String)) + }; + + var arguments = new Expression[node.Arguments.Count]; + for (var index = 0; index < node.Arguments.Count; index++) + { + arguments[index] = HandleErrors(node.Arguments[index], Visit)!; + } + + return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format) }.Concat(arguments).ToArray()); + } + protected override Expression VisitParenthesizedExpression(ParenthesizedExpressionBindingParserNode node) { // just visit content diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParserNodeVisitor.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParserNodeVisitor.cs index 864b9fe6aa..5974a795d6 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParserNodeVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParserNodeVisitor.cs @@ -36,6 +36,10 @@ public virtual T Visit(BindingParserNode node) { return VisitLiteralExpression((LiteralExpressionBindingParserNode)node); } + else if (node is InterpolatedStringBindingParserNode) + { + return VisitInterpolatedStringExpression((InterpolatedStringBindingParserNode)node); + } else if (node is MemberAccessBindingParserNode) { return VisitMemberAccess((MemberAccessBindingParserNode)node); @@ -127,6 +131,11 @@ protected virtual T VisitLiteralExpression(LiteralExpressionBindingParserNode no return DefaultVisit(node); } + protected virtual T VisitInterpolatedStringExpression(InterpolatedStringBindingParserNode node) + { + return DefaultVisit(node); + } + protected virtual T VisitMemberAccess(MemberAccessBindingParserNode node) { return DefaultVisit(node); From c03e5ecdd810e3ef0344c93a72f36e8229e3dec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Thu, 1 Apr 2021 14:09:49 +0200 Subject: [PATCH 04/14] Interpolated string with no expressions is translated to a constant expression --- .../Binding/ExpressionBuildingVisitor.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index e4fa8da690..16ced1012f 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -112,13 +112,22 @@ protected override Expression VisitInterpolatedStringExpression(InterpolatedStri Target = Registry.Resolve(nameof(String)) }; - var arguments = new Expression[node.Arguments.Count]; - for (var index = 0; index < node.Arguments.Count; index++) + if (node.Arguments.Any()) { - arguments[index] = HandleErrors(node.Arguments[index], Visit)!; - } + // Translate to a String.Format(...) call + var arguments = new Expression[node.Arguments.Count]; + for (var index = 0; index < node.Arguments.Count; index++) + { + arguments[index] = HandleErrors(node.Arguments[index], Visit)!; + } - return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format) }.Concat(arguments).ToArray()); + return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format) }.Concat(arguments).ToArray()); + } + else + { + // There are no interpolation expressions - we can just return string + return Expression.Constant(node.Format); + } } protected override Expression VisitParenthesizedExpression(ParenthesizedExpressionBindingParserNode node) From 72b921b4cb2c343e28f71355f81bcba099374559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Thu, 1 Apr 2021 14:12:48 +0200 Subject: [PATCH 05/14] Refactored changes in BindingParser --- .../Parser/Binding/Parser/BindingParser.cs | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index 36e8340cfc..804cf36730 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -900,50 +900,38 @@ private static string ParseStringLiteral(string text, out string? error) private static (string, List) ParseInterpolatedString(string text, out string? error) { - var preprocessedString = ParseStringLiteral(text, out error).Substring(1); - - bool TryParseArgument(int from, out int end, out BindingParserNode? argument) - { - var index = from; - while (preprocessedString[index++] != '}') - { - if (index == preprocessedString.Length) - { - end = -1; - argument = default; - return false; - } - } - - var tokenizer = new BindingTokenizer(); - tokenizer.Tokenize(preprocessedString.Substring(from + 1, index - (from + 2))); - var parser = new BindingParser() { Tokens = tokenizer.Tokens }; - argument = parser.ReadConditionalExpression(); - end = index; - return argument != null; - } - + text = ParseStringLiteral(text, out error).Substring(1); var index = 0; var sb = new StringBuilder(); var arguments = new List(); - while (index < preprocessedString.Length) + while (index < text.Length) { - if (preprocessedString[index] == '{') + var current = text[index]; + var next = (index + 1 < text.Length) ? text[index + 1] : null as char?; + + if (next.HasValue && current == next.Value && (current == '{' || current == '}')) { - if (!TryParseArgument(index, out var end, out var argument)) + // If encountered double '{' or '}' do not treat is as an control character + sb.Append(current); + index += 2; + } + else if (current == '{') + { + // Now an interpolation expression must follow + if (!TryParseInterpolationExpression(text, index, out var end, out var argument)) { arguments.Clear(); - error = "Interpolated string is malformed."; + error = "Interpolation expression is malformed."; return (string.Empty, arguments); } arguments.Add(argument!); sb.Append("{" + (arguments.Count - 1).ToString() + "}"); - index = end; + index = end + 1; continue; } else { - sb.Append(preprocessedString[index]); + sb.Append(current); index++; } } @@ -951,6 +939,29 @@ bool TryParseArgument(int from, out int end, out BindingParserNode? argument) return (sb.ToString(), arguments); } + private static bool TryParseInterpolationExpression(string text, int start, out int end, out BindingParserNode? expression) + { + var index = ++start; + while (text[index] != '}' && (index + 1 == text.Length || text[index + 1] != '}')) + { + if (++index == text.Length) + { + end = -1; + expression = null; + return false; + } + } + + end = index + 1; + var tokenizer = new BindingTokenizer(); + var rawExpression = text.Substring(start, end - start); + tokenizer.Tokenize(rawExpression); + var parser = new BindingParser() { Tokens = tokenizer.Tokens }; + expression = parser.ReadConditionalExpression(); + + return expression != null; + } + private T CreateNode(T node, int startIndex, string? error = null) where T : BindingParserNode { node.Tokens.Clear(); From 50f74e4f029906b86411a869a7f1059ac126aa7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Thu, 1 Apr 2021 15:17:35 +0200 Subject: [PATCH 06/14] Fixed some corner-cases when parsing interpolated strings --- .../Parser/Binding/Parser/BindingParser.cs | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index 804cf36730..c8001b77ad 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -918,10 +918,10 @@ private static (string, List) ParseInterpolatedString(string else if (current == '{') { // Now an interpolation expression must follow - if (!TryParseInterpolationExpression(text, index, out var end, out var argument)) + if (!TryParseInterpolationExpression(text, index, out var end, out var argument, out var innerError)) { arguments.Clear(); - error = "Interpolation expression is malformed."; + error = string.Concat(error, " Interpolation expression is malformed. ", innerError).TrimStart(); return (string.Empty, arguments); } arguments.Add(argument!); @@ -929,6 +929,12 @@ private static (string, List) ParseInterpolatedString(string index = end + 1; continue; } + else if (current == '}') + { + var innerError = "Could not find matching opening character '{' for an interpolated expression."; + error = string.Concat(error, " Interpolation expression is malformed. ", innerError).TrimStart(); + return (string.Empty, arguments); + } else { sb.Append(current); @@ -939,25 +945,43 @@ private static (string, List) ParseInterpolatedString(string return (sb.ToString(), arguments); } - private static bool TryParseInterpolationExpression(string text, int start, out int end, out BindingParserNode? expression) + private static bool TryParseInterpolationExpression(string text, int start, out int end, out BindingParserNode? expression, out string? error) { var index = ++start; - while (text[index] != '}' && (index + 1 == text.Length || text[index + 1] != '}')) + var foundEnd = false; + + while (index < text.Length) { - if (++index == text.Length) + if (text[index++] == '}') { - end = -1; - expression = null; - return false; + foundEnd = true; + break; } } - end = index + 1; - var tokenizer = new BindingTokenizer(); + if (!foundEnd) + { + end = -1; + expression = null; + error = "Could not find matching closing character '}' for an interpolated expression."; + return false; + } + + end = index - 1; + if (start == end) + { + // Provided expression is empty + expression = null; + error = "Expected expression, but instead found empty \"{}\"."; + return false; + } + var rawExpression = text.Substring(start, end - start); + var tokenizer = new BindingTokenizer(); tokenizer.Tokenize(rawExpression); var parser = new BindingParser() { Tokens = tokenizer.Tokens }; expression = parser.ReadConditionalExpression(); + error = null; return expression != null; } From e43a377c2e0face3845f279693ce5bdf3b4900c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Thu, 1 Apr 2021 15:18:56 +0200 Subject: [PATCH 07/14] Added more string interpolation tests --- .../Binding/BindingCompilationTests.cs | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index 680a073e0d..547b75d990 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -130,16 +130,64 @@ public void BindingCompiler_Valid_StringLiteralInSingleQuotes() Assert.AreEqual(ExecuteBinding("StringProp + 'def'", viewModel), "abcdef"); } + [TestMethod] + [DataRow(@"$'Non-Interpolated'", "Non-Interpolated")] + [DataRow(@"$'Non-Interpolated {{'", "Non-Interpolated {")] + [DataRow(@"$'Non-Interpolated {{ no-expression }}'", "Non-Interpolated { no-expression }")] + public void BindingCompiler_Valid_InterpolatedString_NoExpressions(string expression, string evaluated) + { + var binding = ExecuteBinding(expression); + Assert.AreEqual(evaluated, binding); + } + + [TestMethod] + [DataRow(@"$'} Malformed'", "Could not find matching opening character '{' for an interpolated expression")] + [DataRow(@"$'{ Malformed'", "Could not find matching closing character '}' for an interpolated expression")] + [DataRow(@"$'Malformed {expr'", "Could not find matching closing character '}' for an interpolated expression")] + [DataRow(@"$'Malformed expr}'", "Could not find matching opening character '{' for an interpolated expression")] + [DataRow(@"$'Malformed {'", "Could not find matching closing character '}' for an interpolated expression")] + [DataRow(@"$'Malformed }'", "Could not find matching opening character '{' for an interpolated expression")] + [DataRow(@"$'Malformed {}'", "Expected expression, but instead found empty")] + public void BindingCompiler_Valid_InterpolatedString_Malformed(string expression, string errorMessage) + { + try + { + ExecuteBinding(expression); + } + catch (Exception e) + { + // Get inner-most exception + var current = e; + while (current.InnerException != null) + current = current.InnerException; + + Assert.AreEqual(typeof(BindingCompilationException), current.GetType()); + StringAssert.Contains(current.Message, errorMessage); + } + } + [TestMethod] [DataRow(@"$""Interpolated {StringProp} {StringProp}""", "Interpolated abc abc")] [DataRow(@"$'Interpolated {StringProp} {StringProp}'", "Interpolated abc abc")] - public void BindingCompiler_Valid_InterpolatedString(string expression, string evaluated) + [DataRow(@"$'Interpolated {StringProp.Length}'", "Interpolated 3")] + public void BindingCompiler_Valid_InterpolatedString_WithSimpleExpressions(string expression, string evaluated) { var viewModel = new TestViewModel() { StringProp = "abc" }; var binding = ExecuteBinding(expression, viewModel); Assert.AreEqual(evaluated, binding); } + [TestMethod] + [DataRow(@"$'Interpolated {IntProp < LongProperty}'", "Interpolated True")] + [DataRow(@"$'Interpolated {StringProp ?? \'StringPropWasNull\'}'", "Interpolated StringPropWasNull")] + [DataRow(@"$'Interpolated {(StringProp == null) ? \'StringPropWasNull\' : \'StringPropWasNotNull\'}'", "Interpolated StringPropWasNull")] + public void BindingCompiler_Valid_InterpolatedString_WithComplexExpressions(string expression, string evaluated) + { + var viewModel = new TestViewModel() { IntProp = 1, LongProperty = 2 }; + var binding = ExecuteBinding(expression, viewModel); + Assert.AreEqual(evaluated, binding); + } + [TestMethod] public void BindingCompiler_Valid_PropertyProperty() { From 12ef741f6928f1b2b31816a9c202c7af27407e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Thu, 1 Apr 2021 16:33:36 +0200 Subject: [PATCH 08/14] Added javascript translation tests --- .../Binding/JavascriptCompilationTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs index 8956861017..002fa39f09 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs @@ -125,6 +125,22 @@ public void JavascriptCompilation_ToString_Invalid() }); } + [TestMethod] + [DataRow(@"$""Interpolated {StringProp} {StringProp}""")] + [DataRow(@"$'Interpolated {StringProp} {StringProp}'")] + public void JavascriptCompilation_InterpolatedString(string expression) + { + var js = CompileBinding(expression, new[] { typeof(TestViewModel) }, typeof(string)); + Assert.AreEqual("dotvvm.globalize.format(\"Interpolated {0} {1}\",[StringProp(),StringProp()])", js); + } + + [TestMethod] + public void JavascriptCompilation_InterpolatedString_NoExpressions() + { + var js = CompileBinding("$'Non-Interpolated {{ no-expr }}'", new[] { typeof(TestViewModel) }); + Assert.AreEqual("\"Non-Interpolated { no-expr }\"", js); + } + [TestMethod] public void JavascriptCompilation_UnwrappedObservables() { From e325c5329faa820e3eda9890ae39ba0aabc24614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Wed, 7 Apr 2021 11:28:56 +0200 Subject: [PATCH 09/14] Fixed nested expressions parsing --- .../Binding/BindingCompilationTests.cs | 13 +- .../Parser/Binding/BindingTokenizerTests.cs | 10 ++ .../Parser/Binding/Parser/BindingParser.cs | 151 +++++++++++------- .../Binding/Tokenizer/BindingTokenizer.cs | 51 +++++- 4 files changed, 159 insertions(+), 66 deletions(-) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index 547b75d990..571ce84c2d 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -141,12 +141,12 @@ public void BindingCompiler_Valid_InterpolatedString_NoExpressions(string expres } [TestMethod] - [DataRow(@"$'} Malformed'", "Could not find matching opening character '{' for an interpolated expression")] + [DataRow(@"$'} Malformed'", "Unexpected token '$' ---->}<----")] [DataRow(@"$'{ Malformed'", "Could not find matching closing character '}' for an interpolated expression")] [DataRow(@"$'Malformed {expr'", "Could not find matching closing character '}' for an interpolated expression")] - [DataRow(@"$'Malformed expr}'", "Could not find matching opening character '{' for an interpolated expression")] + [DataRow(@"$'Malformed expr}'", "Unexpected token '$'Malformed expr ---->}<---- ")] [DataRow(@"$'Malformed {'", "Could not find matching closing character '}' for an interpolated expression")] - [DataRow(@"$'Malformed }'", "Could not find matching opening character '{' for an interpolated expression")] + [DataRow(@"$'Malformed }'", "Unexpected token '$'Malformed ---->}<----")] [DataRow(@"$'Malformed {}'", "Expected expression, but instead found empty")] public void BindingCompiler_Valid_InterpolatedString_Malformed(string expression, string errorMessage) { @@ -170,17 +170,18 @@ public void BindingCompiler_Valid_InterpolatedString_Malformed(string expression [DataRow(@"$""Interpolated {StringProp} {StringProp}""", "Interpolated abc abc")] [DataRow(@"$'Interpolated {StringProp} {StringProp}'", "Interpolated abc abc")] [DataRow(@"$'Interpolated {StringProp.Length}'", "Interpolated 3")] + [DataRow(@"$'{string.Join(', ', IntArray)}'", "1, 2, 3")] public void BindingCompiler_Valid_InterpolatedString_WithSimpleExpressions(string expression, string evaluated) { - var viewModel = new TestViewModel() { StringProp = "abc" }; + var viewModel = new TestViewModel() { StringProp = "abc", IntArray = new[] { 1, 2, 3 } }; var binding = ExecuteBinding(expression, viewModel); Assert.AreEqual(evaluated, binding); } [TestMethod] [DataRow(@"$'Interpolated {IntProp < LongProperty}'", "Interpolated True")] - [DataRow(@"$'Interpolated {StringProp ?? \'StringPropWasNull\'}'", "Interpolated StringPropWasNull")] - [DataRow(@"$'Interpolated {(StringProp == null) ? \'StringPropWasNull\' : \'StringPropWasNotNull\'}'", "Interpolated StringPropWasNull")] + [DataRow(@"$'Interpolated {StringProp ?? 'StringPropWasNull'}'", "Interpolated StringPropWasNull")] + [DataRow(@"$'Interpolated {(StringProp == null) ? 'StringPropWasNull' : 'StringPropWasNotNull'}'", "Interpolated StringPropWasNull")] public void BindingCompiler_Valid_InterpolatedString_WithComplexExpressions(string expression, string evaluated) { var viewModel = new TestViewModel() { IntProp = 1, LongProperty = 2 }; diff --git a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingTokenizerTests.cs b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingTokenizerTests.cs index 75cdfdfa1e..4f2f564a69 100644 --- a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingTokenizerTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingTokenizerTests.cs @@ -249,6 +249,16 @@ public void BindingTokenizer_InterpolatedString_Valid(string expression) Assert.AreEqual(BindingTokenType.InterpolatedStringToken, tokens[0].Type); } + [TestMethod] + [DataRow("$'String {'InnerString'}'")] + [DataRow("$'String {$'Inner {'InnerInnerString'}'}'")] + public void BindingTokenizer_InterpolatedString_InnerString(string expression) + { + var tokens = Tokenize(expression); + Assert.AreEqual(1, tokens.Count); + Assert.AreEqual(BindingTokenType.InterpolatedStringToken, tokens[0].Type); + } + [TestMethod] public void BindingTokenizer_UnaryOperator_BunchingOperators_Valid() { diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index c8001b77ad..c4e985f1fe 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -859,85 +859,122 @@ private static string ParseStringLiteral(string text, out string? error) { error = null; var sb = new StringBuilder(); - for (var i = 1; i < text.Length - 1; i++) + + var index = 1; + while (index < text.Length - 1) { - if (text[i] == '\\') + if (TryParseCharacter(text, ref index, out var character, out var innerError)) { - // handle escaped characters - i++; - if (i == text.Length - 1) - { - error = "The escape character cannot be at the end of the string literal!"; - } - else if (text[i] == '\'' || text[i] == '"' || text[i] == '\\') - { - sb.Append(text[i]); - } - else if (text[i] == 'n') - { - sb.Append('\n'); - } - else if (text[i] == 'r') - { - sb.Append('\r'); - } - else if (text[i] == 't') - { - sb.Append('\t'); - } - else - { - error = "The escape sequence is either not valid or not supported in dotVVM bindings!"; - } + sb.Append(character); } else { - sb.Append(text[i]); + error = innerError; } } + return sb.ToString(); } + private static bool TryParseCharacter(string text, ref int index, out char character, out string? error) + { + var result = TryPeekCharacter(text, index, out var count, out character, out error); + index += count; + return result; + } + + private static bool TryPeekCharacter(string text, int index, out int length, out char character, out string? error) + { + if (text[index] == '\\') + { + // handle escaped characters + length = 2; + index++; + if (index == text.Length - 1) + { + error = "The escape character cannot be at the end of the string literal!"; + character = default; + return false; + } + else if (text[index] == '\'' || text[index] == '"' || text[index] == '\\') + { + character = text[index]; + } + else if (text[index] == 'n') + { + character = '\n'; + } + else if (text[index] == 'r') + { + character = '\r'; + } + else if (text[index] == 't') + { + character = '\t'; + } + else + { + error = "The escape sequence is either not valid or not supported in dotVVM bindings!"; + character = default; + return false; + } + + error = default; + return true; + } + else + { + character = text[index]; + error = default; + length = 1; + return true; + } + } + private static (string, List) ParseInterpolatedString(string text, out string? error) { - text = ParseStringLiteral(text, out error).Substring(1); - var index = 0; + error = null; var sb = new StringBuilder(); var arguments = new List(); - while (index < text.Length) - { - var current = text[index]; - var next = (index + 1 < text.Length) ? text[index + 1] : null as char?; - if (next.HasValue && current == next.Value && (current == '{' || current == '}')) - { - // If encountered double '{' or '}' do not treat is as an control character - sb.Append(current); - index += 2; - } - else if (current == '{') + var index = 2; + while (index < text.Length - 1) + { + if (TryParseCharacter(text, ref index, out var current, out var innerError)) { - // Now an interpolation expression must follow - if (!TryParseInterpolationExpression(text, index, out var end, out var argument, out var innerError)) + var hasNext = TryPeekCharacter(text, index, out var length, out var next, out _); + if (hasNext && current == next && (current == '{' || current == '}')) { - arguments.Clear(); + // If encountered double '{' or '}' do not treat is as an control character + sb.Append(current); + index += length; + } + else if (current == '{') + { + if (!TryParseInterpolationExpression(text, index, out var end, out var argument, out innerError)) + { + arguments.Clear(); + error = string.Concat(error, " Interpolation expression is malformed. ", innerError).TrimStart(); + return (string.Empty, arguments); + } + arguments.Add(argument!); + sb.Append("{" + (arguments.Count - 1).ToString() + "}"); + index = end + 1; + } + else if (current == '}') + { + innerError = "Could not find matching opening character '{' for an interpolated expression."; error = string.Concat(error, " Interpolation expression is malformed. ", innerError).TrimStart(); return (string.Empty, arguments); } - arguments.Add(argument!); - sb.Append("{" + (arguments.Count - 1).ToString() + "}"); - index = end + 1; - continue; - } - else if (current == '}') - { - var innerError = "Could not find matching opening character '{' for an interpolated expression."; - error = string.Concat(error, " Interpolation expression is malformed. ", innerError).TrimStart(); - return (string.Empty, arguments); + else + { + sb.Append(current); + } } else { - sb.Append(current); + error = innerError; index++; } } @@ -947,7 +984,7 @@ private static (string, List) ParseInterpolatedString(string private static bool TryParseInterpolationExpression(string text, int start, out int end, out BindingParserNode? expression, out string? error) { - var index = ++start; + var index = start; var foundEnd = false; while (index < text.Length) diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenizer.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenizer.cs index b61af0f6ac..d6a7aba6b0 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenizer.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Tokenizer/BindingTokenizer.cs @@ -215,18 +215,20 @@ private void TokenizeBindingValue() case '\'': case '"': var bindingTokenType = default(BindingTokenType); + var errorMessage = default(string); + FinishIncompleteIdentifier(); + if (ch == '$') { - Read(); bindingTokenType = BindingTokenType.InterpolatedStringToken; + ReadInterpolatedString(out errorMessage); } else { - FinishIncompleteIdentifier(); bindingTokenType = BindingTokenType.StringLiteralToken; + ReadStringLiteral(out errorMessage); } - ReadStringLiteral(out var errorMessage); CreateToken(bindingTokenType, errorProvider: t => CreateTokenError(t, errorMessage ?? "unknown error")); break; @@ -303,6 +305,11 @@ internal void ReadStringLiteral(out string? errorMessage) ReadStringLiteral(Peek, Read, out errorMessage); } + internal void ReadInterpolatedString(out string? errorMessage) + { + ReadInterpolatedString(Peek, Read, out errorMessage); + } + /// /// Reads the string literal. /// @@ -336,5 +343,43 @@ internal static void ReadStringLiteral(Func peekFunction, Func readF errorMessage = null; } + + internal static void ReadInterpolatedString(Func peekFunction, Func readFunction, out string? errorMessage) + { + readFunction(); + var quoteChar = readFunction(); + var exprDepth = 0; + + while (peekFunction() != quoteChar || exprDepth != 0) + { + if (peekFunction() == NullChar) + { + errorMessage = "Interpolated string was not closed!"; + return; + } + + if (peekFunction() == '\\' && exprDepth == 0) + { + readFunction(); + } + else if (peekFunction() == '{') + { + exprDepth++; + } + else if (peekFunction() == '}') + { + if (--exprDepth <= -1) + { + errorMessage = "Could not find matching '{' character!"; + return; + } + } + + readFunction(); + } + + readFunction(); + errorMessage = null; + } } } From ed66ac77ec25f7a5e32fb3fe93b85a7a8702dff5 Mon Sep 17 00:00:00 2001 From: acizmarik Date: Wed, 7 Apr 2021 11:30:28 +0200 Subject: [PATCH 10/14] Use System.String also if String symbol gets redefined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stanislav Lukeš --- .../Compilation/Binding/ExpressionBuildingVisitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index 16ced1012f..158c1187d1 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -109,7 +109,7 @@ protected override Expression VisitInterpolatedStringExpression(InterpolatedStri { var target = new MethodGroupExpression() { MethodName = nameof(String.Format), - Target = Registry.Resolve(nameof(String)) + Target = typeof(string) }; if (node.Arguments.Any()) From dd77eb92f57854a0a5754dfaf23c0136fd78b969 Mon Sep 17 00:00:00 2001 From: acizmarik Date: Wed, 7 Apr 2021 11:34:47 +0200 Subject: [PATCH 11/14] Refactored changes in ExpressionBuildingVisitor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stanislav Lukeš --- .../Compilation/Binding/ExpressionBuildingVisitor.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index 158c1187d1..e930f6ff7b 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -115,11 +115,7 @@ protected override Expression VisitInterpolatedStringExpression(InterpolatedStri if (node.Arguments.Any()) { // Translate to a String.Format(...) call - var arguments = new Expression[node.Arguments.Count]; - for (var index = 0; index < node.Arguments.Count; index++) - { - arguments[index] = HandleErrors(node.Arguments[index], Visit)!; - } + var arguments = node.Arguments.Select(arg => HandleErrors(node.Arguments[index], Visit))[.ToArray()]; return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format) }.Concat(arguments).ToArray()); } From 0c01a4349e6e581972bdb41c6f96e0819905e650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Wed, 7 Apr 2021 16:46:08 +0200 Subject: [PATCH 12/14] Added support for formatting component Fixed building issues --- .../Binding/BindingCompilationTests.cs | 10 +++ .../Binding/ExpressionBuildingVisitor.cs | 16 ++++- .../Parser/Binding/Parser/BindingParser.cs | 65 +++++++++++++++++-- .../Parser/BindingParserNodeVisitor.cs | 9 +++ .../Parser/FormattedBindingParserNode.cs | 28 ++++++++ 5 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/DotVVM.Framework/Compilation/Parser/Binding/Parser/FormattedBindingParserNode.cs diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index 571ce84c2d..8f112420b9 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -189,6 +189,16 @@ public void BindingCompiler_Valid_InterpolatedString_WithComplexExpressions(stri Assert.AreEqual(evaluated, binding); } + [TestMethod] + [DataRow(@"$'Interpolated {DateFrom:R}'", "Interpolated Fri, 11 Nov 2011 12:11:11 GMT")] + [DataRow(@"$'Interpolated {$'{DateFrom:R}'}'", "Interpolated Fri, 11 Nov 2011 12:11:11 GMT")] + public void BindingCompiler_Valid_InterpolatedString_WithFormattingComponent(string expression, string evaluated) + { + var viewModel = new TestViewModel() { DateFrom = DateTime.Parse("2011-11-11T11:11:11+00:00") }; + var binding = ExecuteBinding(expression, viewModel); + Assert.AreEqual(evaluated, binding); + } + [TestMethod] public void BindingCompiler_Valid_PropertyProperty() { diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index e930f6ff7b..fa992beea3 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -109,14 +109,13 @@ protected override Expression VisitInterpolatedStringExpression(InterpolatedStri { var target = new MethodGroupExpression() { MethodName = nameof(String.Format), - Target = typeof(string) + Target = new StaticClassIdentifierExpression(typeof(string)) }; if (node.Arguments.Any()) { // Translate to a String.Format(...) call - var arguments = node.Arguments.Select(arg => HandleErrors(node.Arguments[index], Visit))[.ToArray()]; - + var arguments = node.Arguments.Select((arg, index) => HandleErrors(node.Arguments[index], Visit)).ToArray(); return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format) }.Concat(arguments).ToArray()); } else @@ -373,6 +372,17 @@ protected override Expression VisitBlock(BlockBindingParserNode node) else return Expression.Block(variables, left, right); } + protected override Expression VisitFormattedExpression(FormattedBindingParserNode node) + { + var target = new MethodGroupExpression() { + MethodName = nameof(String.Format), + Target = new StaticClassIdentifierExpression(typeof(string)) + }; + + var nodeObj = HandleErrors(node.Node, Visit); + return memberExpressionFactory.Call(target, new[] { Expression.Constant(node.Format), nodeObj }); + } + protected override Expression VisitVoid(VoidBindingParserNode node) => Expression.Default(typeof(void)); private Expression? GetMemberOrTypeExpression(IdentifierNameBindingParserNode node, Type[]? typeParameters) diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index c4e985f1fe..daf668be58 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -729,6 +729,53 @@ private IdentifierNameBindingParserNode ReadGenericArguments(int startIndex, Bin return CreateNode(new SimpleNameBindingParserNode(identifier), startIndex); } + private BindingParserNode ReadFormattedExpression() + { + var startIndex = CurrentIndex; + BindingParserNode? node; + + // 1) Parse expression + if (Peek() is BindingToken operatorToken && operatorToken.Type == BindingTokenType.OpenParenthesis) + { + // Conditional expressions must be enclosed in parentheses + node = ReadConditionalExpression(); + if (IsCurrentTokenIncorrect(BindingTokenType.CloseParenthesis)) + { + node.NodeErrors.Add("Expected ')' after this expression."); + } + else + { + Read(); + } + } + else + { + // If expression is not enclosed in parentheses, read null coalescing expression + node = ReadNullCoalescingExpression(); + } + + // 2) Parse formatting component (optional) + if (Peek() is BindingToken delimitingToken && delimitingToken.Type == BindingTokenType.ColonOperator) + { + Read(); + if (IsCurrentTokenIncorrect(BindingTokenType.Identifier)) + { + node.NodeErrors.Add("Expected an identifier after ':'. The identifier should specify formatting for the previous expression!"); + } + + // Scan all remaining tokens + BindingToken? currentToken; + var formatTokens = new List(); + while ((currentToken = Read()) != null) + formatTokens.Add(currentToken); + + var format = $"{{0:{string.Concat(formatTokens.Select(token => token.Text))}}}"; + return CreateNode(new FormattedBindingParserNode(node, format), startIndex); + } + + return node; + } + private static object? ParseNumberLiteral(string text, out string? error) { text = text.ToLower(); @@ -987,12 +1034,22 @@ private static bool TryParseInterpolationExpression(string text, int start, out var index = start; var foundEnd = false; + var exprDepth = 0; while (index < text.Length) { - if (text[index++] == '}') + var current = text[index++]; + if (current == '{') { - foundEnd = true; - break; + exprDepth++; + } + if (current == '}') + { + if (exprDepth == 0) + { + foundEnd = true; + break; + } + exprDepth--; } } @@ -1017,7 +1074,7 @@ private static bool TryParseInterpolationExpression(string text, int start, out var tokenizer = new BindingTokenizer(); tokenizer.Tokenize(rawExpression); var parser = new BindingParser() { Tokens = tokenizer.Tokens }; - expression = parser.ReadConditionalExpression(); + expression = parser.ReadFormattedExpression(); error = null; return expression != null; diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParserNodeVisitor.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParserNodeVisitor.cs index 5974a795d6..f017ce0b3b 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParserNodeVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParserNodeVisitor.cs @@ -68,6 +68,10 @@ public virtual T Visit(BindingParserNode node) { return VisitAssemblyQualifiedName((AssemblyQualifiedNameBindingParserNode)node); } + else if (node is FormattedBindingParserNode) + { + return VisitFormattedExpression((FormattedBindingParserNode)node); + } else if (node is BlockBindingParserNode blockNode) { return VisitBlock(blockNode); @@ -161,6 +165,11 @@ protected virtual T VisitAssemblyQualifiedName(AssemblyQualifiedNameBindingParse return DefaultVisit(node); } + protected virtual T VisitFormattedExpression(FormattedBindingParserNode node) + { + return DefaultVisit(node); + } + protected virtual T VisitBlock(BlockBindingParserNode node) { return DefaultVisit(node); diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/FormattedBindingParserNode.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/FormattedBindingParserNode.cs new file mode 100644 index 0000000000..a75d9b0e86 --- /dev/null +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/FormattedBindingParserNode.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DotVVM.Framework.Compilation.Parser.Binding.Parser +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class FormattedBindingParserNode : BindingParserNode + { + public BindingParserNode Node { get; private set; } + public string Format { get; private set; } + + public FormattedBindingParserNode(BindingParserNode node, string format) + { + this.Node = node; + this.Format = format; + } + + public override IEnumerable EnumerateChildNodes() + => base.EnumerateNodes().Concat(new[] { Node }); + + public override string ToDisplayString() + => $"{Node.ToDisplayString()}:{Format}"; + } +} From 061fe023bb6c2dbceb01d5f126a559ef9c4a2412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Thu, 8 Apr 2021 11:19:06 +0200 Subject: [PATCH 13/14] Fixed issue with conditional expression, improved error reporting --- .../Binding/BindingCompilationTests.cs | 7 +++-- .../Parser/Binding/Parser/BindingParser.cs | 28 ++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index 8f112420b9..be1b2181c8 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -148,7 +148,10 @@ public void BindingCompiler_Valid_InterpolatedString_NoExpressions(string expres [DataRow(@"$'Malformed {'", "Could not find matching closing character '}' for an interpolated expression")] [DataRow(@"$'Malformed }'", "Unexpected token '$'Malformed ---->}<----")] [DataRow(@"$'Malformed {}'", "Expected expression, but instead found empty")] - public void BindingCompiler_Valid_InterpolatedString_Malformed(string expression, string errorMessage) + [DataRow(@"$'Malformed {StringProp; IntProp}'", "Expected end of interpolated expression, but instead found Semicolon")] + [DataRow(@"$'Malformed {(string arg) => arg.Length}'", "Expected end of interpolated expression, but instead found Identifier")] + [DataRow(@"$'Malformed {(StringProp == null) ? 'StringPropWasNull' : 'StringPropWasNotNull'}'", "Conditional expression needs to be enclosed in parentheses")] + public void BindingCompiler_Invalid_InterpolatedString_MalformedExpression(string expression, string errorMessage) { try { @@ -181,7 +184,7 @@ public void BindingCompiler_Valid_InterpolatedString_WithSimpleExpressions(strin [TestMethod] [DataRow(@"$'Interpolated {IntProp < LongProperty}'", "Interpolated True")] [DataRow(@"$'Interpolated {StringProp ?? 'StringPropWasNull'}'", "Interpolated StringPropWasNull")] - [DataRow(@"$'Interpolated {(StringProp == null) ? 'StringPropWasNull' : 'StringPropWasNotNull'}'", "Interpolated StringPropWasNull")] + [DataRow(@"$'Interpolated {((StringProp == null) ? 'StringPropWasNull' : 'StringPropWasNotNull')}'", "Interpolated StringPropWasNull")] public void BindingCompiler_Valid_InterpolatedString_WithComplexExpressions(string expression, string evaluated) { var viewModel = new TestViewModel() { IntProp = 1, LongProperty = 2 }; diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index daf668be58..7d6844c13d 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -734,11 +734,16 @@ private BindingParserNode ReadFormattedExpression() var startIndex = CurrentIndex; BindingParserNode? node; + SkipWhiteSpace(); + // 1) Parse expression if (Peek() is BindingToken operatorToken && operatorToken.Type == BindingTokenType.OpenParenthesis) { // Conditional expressions must be enclosed in parentheses + Read(); + SkipWhiteSpace(); node = ReadConditionalExpression(); + SkipWhiteSpace(); if (IsCurrentTokenIncorrect(BindingTokenType.CloseParenthesis)) { node.NodeErrors.Add("Expected ')' after this expression."); @@ -754,6 +759,8 @@ private BindingParserNode ReadFormattedExpression() node = ReadNullCoalescingExpression(); } + SkipWhiteSpace(); + // 2) Parse formatting component (optional) if (Peek() is BindingToken delimitingToken && delimitingToken.Type == BindingTokenType.ColonOperator) { @@ -773,6 +780,20 @@ private BindingParserNode ReadFormattedExpression() return CreateNode(new FormattedBindingParserNode(node, format), startIndex); } + SkipWhiteSpace(); + if (Peek() != null) + { + if (Peek()!.Type == BindingTokenType.QuestionMarkOperator) + { + // If it seems that user tried to use conditional expression, provide more concrete error message + node.NodeErrors.Add("Conditional expression needs to be enclosed in parentheses."); + } + else + { + node.NodeErrors.Add($"Expected end of interpolated expression, but instead found {Peek()!.Type}"); + } + } + return node; } @@ -1070,12 +1091,17 @@ private static bool TryParseInterpolationExpression(string text, int start, out return false; } + error = null; var rawExpression = text.Substring(start, end - start); var tokenizer = new BindingTokenizer(); tokenizer.Tokenize(rawExpression); var parser = new BindingParser() { Tokens = tokenizer.Tokens }; expression = parser.ReadFormattedExpression(); - error = null; + if (expression.HasNodeErrors) + { + error = string.Join(" ", new[] { $"Error while parsing expression \"{rawExpression}\"." }.Concat(expression.NodeErrors)); + return false; + } return expression != null; } From c0c252af9b9cabc52f227de1197dbb25a9652f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Thu, 8 Apr 2021 12:15:19 +0200 Subject: [PATCH 14/14] Added more tests --- .../Binding/BindingCompilationTests.cs | 16 +++++++++++++--- .../Parser/Binding/BindingParserTests.cs | 12 ++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index be1b2181c8..d69e0a36e8 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -173,10 +173,19 @@ public void BindingCompiler_Invalid_InterpolatedString_MalformedExpression(strin [DataRow(@"$""Interpolated {StringProp} {StringProp}""", "Interpolated abc abc")] [DataRow(@"$'Interpolated {StringProp} {StringProp}'", "Interpolated abc abc")] [DataRow(@"$'Interpolated {StringProp.Length}'", "Interpolated 3")] - [DataRow(@"$'{string.Join(', ', IntArray)}'", "1, 2, 3")] public void BindingCompiler_Valid_InterpolatedString_WithSimpleExpressions(string expression, string evaluated) { - var viewModel = new TestViewModel() { StringProp = "abc", IntArray = new[] { 1, 2, 3 } }; + var viewModel = new TestViewModel() { StringProp = "abc" }; + var binding = ExecuteBinding(expression, viewModel); + Assert.AreEqual(evaluated, binding); + } + + [TestMethod] + [DataRow(@"$'{string.Join(', ', IntArray)}'", "1, 2, 3")] + [DataRow(@"$'{string.Join(', ', 'abc', 'def', $'{string.Join(', ', IntArray)}')}'", "abc, def, 1, 2, 3")] + public void BindingCompiler_Valid_InterpolatedString_NestedExpressions(string expression, string evaluated) + { + var viewModel = new TestViewModel { IntArray = new[] { 1, 2, 3 } }; var binding = ExecuteBinding(expression, viewModel); Assert.AreEqual(evaluated, binding); } @@ -195,9 +204,10 @@ public void BindingCompiler_Valid_InterpolatedString_WithComplexExpressions(stri [TestMethod] [DataRow(@"$'Interpolated {DateFrom:R}'", "Interpolated Fri, 11 Nov 2011 12:11:11 GMT")] [DataRow(@"$'Interpolated {$'{DateFrom:R}'}'", "Interpolated Fri, 11 Nov 2011 12:11:11 GMT")] + [DataRow(@"$'Interpolated {$'{IntProp:0000}'}'", "Interpolated 0006")] public void BindingCompiler_Valid_InterpolatedString_WithFormattingComponent(string expression, string evaluated) { - var viewModel = new TestViewModel() { DateFrom = DateTime.Parse("2011-11-11T11:11:11+00:00") }; + var viewModel = new TestViewModel() { DateFrom = DateTime.Parse("2011-11-11T11:11:11+00:00"), IntProp = 6 }; var binding = ExecuteBinding(expression, viewModel); Assert.AreEqual(evaluated, binding); } diff --git a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs index 3b68d7b22b..10cd51f3ec 100644 --- a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs @@ -188,6 +188,18 @@ public void BindingParser_InterpolatedString_Valid() Assert.AreEqual("Argument2", ((SimpleNameBindingParserNode)result.Arguments[1]).Name); } + [TestMethod] + [DataRow("$'{DateProperty:dd/MM/yyyy}'", "{0:dd/MM/yyyy}")] + [DataRow("$'{IntProperty:####}'", "{0:####}")] + public void BindingParser_InterpolatedString_WithFormattingComponenet_Valid(string expression, string formatOptions) + { + var result = bindingParserNodeFactory.Parse(expression) as InterpolatedStringBindingParserNode; + Assert.IsFalse(result.HasNodeErrors); + Assert.AreEqual(1, result.Arguments.Count); + Assert.AreEqual(typeof(FormattedBindingParserNode), result.Arguments.First().GetType()); + Assert.AreEqual(formatOptions, ((FormattedBindingParserNode)result.Arguments.First()).Format); + } + [TestMethod] public void BindingParser_StringLiteral_SingleQuotes_Valid() {