diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index 591522a6fc..3cb6d3536b 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -612,6 +612,24 @@ public void BindingCompiler_ComparisonOperators() Assert.AreEqual(false, result); } + [TestMethod] + public void BindingCompiler_Variables() + { + Assert.AreEqual(2, ExecuteBinding("var a = 1; a + 1")); + Assert.AreEqual(typeof(int), ExecuteBinding("var a = 1; a.GetType()")); + + var result = ExecuteBinding("var a = 1; var b = a + LongProperty; var c = b + StringProp; c", new [] { new TestViewModel { LongProperty = 1, StringProp = "X" } }); + Assert.AreEqual("2X", result); + } + + [TestMethod] + public void BindingCompiler_VariableShadowing() + { + Assert.AreEqual(121L, ExecuteBinding("var LongProperty = LongProperty + 120; LongProperty", new TestViewModel { LongProperty = 1 })); + Assert.AreEqual(7, ExecuteBinding("var a = 1; var b = (var a = 5; a + 1); a + b")); + Assert.AreEqual(3, ExecuteBinding("var a = 1; var a = a + 1; var a = a + 1; a")); + } + [TestMethod] public void BindingCompiler_Errors_AssigningToType() { diff --git a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs index e105e96a54..8956861017 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs @@ -425,6 +425,34 @@ public void StaticCommandCompilation_IndexParameterInParent() Assert.AreEqual("$parentContext.$parentContext.$index()", result); } + [TestMethod] + public void JavascriptCompilation_Variable() + { + var result = CompileBinding("var a = 1; var b = 2; var c = 3; a + b + c", typeof(TestViewModel)); + Assert.AreEqual("function(a,b,c){a=1;b=2;c=3;return a+b+c;}()", result); + } + + [TestMethod] + public void JavascriptCompilation_Variable_Nested() + { + var result = CompileBinding("var a = 1; var b = (var a = 5; a + 1); a + b", typeof(TestViewModel)); + Assert.AreEqual("function(a0,b){a0=1;b=function(a){a=5;return a+1;}();return a0+b;}()", result); + } + + [TestMethod] + public void JavascriptCompilation_Variable_Property() + { + var result = CompileBinding("var a = _this.StringProp; var b = _this.StringProp2; StringProp2 = a + b", typeof(TestViewModel)); + Assert.AreEqual("function(a,b){a=StringProp();b=StringProp2();return StringProp2(a+b);}()", result); + } + + [TestMethod] + public void JavascriptCompilation_Variable_VM() + { + var result = CompileBinding("var a = _parent; var b = _this.StringProp2; StringProp2 = a + b", new [] { typeof(string), typeof(TestViewModel) }); + Assert.AreEqual("function(a,b){a=$parent;b=StringProp2();return StringProp2(a+b);}()", result); + } + [TestMethod] public void JavascriptCompilation_AssignAndUse() { diff --git a/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs index ce4f7e0cc9..42a000f72f 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs @@ -113,6 +113,13 @@ public void StaticCommandCompilation_ChainedCommands() Assert.AreEqual("(function(a,b){return new Promise(function(resolve,reject){dotvvm.staticCommandPostback(a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[b.$data.StringProp()],options).then(function(r_0){dotvvm.staticCommandPostback(a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[dotvvm.globalize.bindingNumberToString(r_0)()],options).then(function(r_1){resolve(b.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_1)()).StringProp());},reject);},reject);});}(this,ko.contextFor(this)))", result); } + [TestMethod] + public void StaticCommandCompilation_MultipleCommandsWithVariable() + { + var result = CompileBinding("var lenVar = StaticCommands.GetLength(StringProp).ToString(); StringProp = StaticCommands.GetLength(lenVar).ToString();", niceMode: false, typeof(TestViewModel)); + Assert.AreEqual("(function(a,d,b,c){return new Promise(function(resolve,reject){dotvvm.staticCommandPostback(a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[d.$data.StringProp()],options).then(function(r_0){(c=b=dotvvm.globalize.bindingNumberToString(r_0)(),dotvvm.staticCommandPostback(a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[b],options).then(function(r_1){resolve((c,d.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_1)()).StringProp(),null));},reject));},reject);});}(this,ko.contextFor(this)))", result); + } + [TestMethod] public void StaticCommandCompilation_ChainedCommandsWithSemicolon() { diff --git a/src/DotVVM.Framework.Tests.Common/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html b/src/DotVVM.Framework.Tests.Common/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html index fcd1d7fa18..9a9065042f 100644 --- a/src/DotVVM.Framework.Tests.Common/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html +++ b/src/DotVVM.Framework.Tests.Common/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html @@ -39,6 +39,6 @@ ], "typeMetadata": {} }"> - + diff --git a/src/DotVVM.Framework.Tests.Common/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html b/src/DotVVM.Framework.Tests.Common/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html index 5170ee03f5..6c0492b327 100644 --- a/src/DotVVM.Framework.Tests.Common/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html +++ b/src/DotVVM.Framework.Tests.Common/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html @@ -79,6 +79,6 @@ } } }"> - + diff --git a/src/DotVVM.Framework.Tests.Common/DotVVM.Framework.Tests.Common.csproj b/src/DotVVM.Framework.Tests.Common/DotVVM.Framework.Tests.Common.csproj index 664b531fef..6a1088af78 100644 --- a/src/DotVVM.Framework.Tests.Common/DotVVM.Framework.Tests.Common.csproj +++ b/src/DotVVM.Framework.Tests.Common/DotVVM.Framework.Tests.Common.csproj @@ -6,9 +6,9 @@ True - Library - netcoreapp3.0 latest + Exe + netcoreapp3.1 True @@ -24,16 +24,12 @@ - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + - - + + diff --git a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs index 3504b0df46..3cd1d2e206 100644 --- a/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Parser/Binding/BindingParserTests.cs @@ -934,6 +934,29 @@ public void BindingParser_MultiblockExpression_EmptyBlockMiddle_Invalid(string b Assert.AreEqual(node.EndPosition, lastExpression.EndPosition); Assert.AreEqual(voidBlockExpectedLenght, middleExpression.Length); + } + [DataRow("var x=A(); !x", "x", DisplayName = "Variable (var) expression")] + [DataRow("var var=A(); !var", "var", DisplayName = "Variable (var) expression, name=var")] + [DataRow("var x = A(); !x", "x", DisplayName = "Variable (var) expression with whitespaces")] + public void BindingParser_VariableExpression_Simple(string bindingExpression, string variableName) + { + var parser = bindingParserNodeFactory.SetupParser(bindingExpression); + var node = parser.ReadExpression().CastTo(); + + var firstExpression = + node.FirstExpression.As(); + + var secondExpression = node.SecondExpression.As(); + + Assert.IsNotNull(firstExpression, "Expected path was not found in the expression tree."); + Assert.IsNotNull(secondExpression, "Expected path was not found in the expression tree."); + + Assert.AreEqual(0, node.StartPosition); + Assert.AreEqual(node.EndPosition, secondExpression.EndPosition); + Assert.IsNotNull(node.Variable); + Assert.AreEqual(variableName, node.Variable.Name); + Assert.AreEqual(firstExpression.EndPosition + 1, secondExpression.StartPosition); + Assert.AreEqual(SkipWhitespaces(bindingExpression), SkipWhitespaces(node.ToDisplayString())); } @@ -979,6 +1002,28 @@ public void BindingParser_MultiblockExpression_EmptyBlockFourBlocks_Invalid(stri Assert.AreEqual(SkipWhitespaces(bindingExpression), SkipWhitespaces(node.ToDisplayString())); } + + [TestMethod] + public void BindingParser_VariableExpression_3Vars() + { + var parser = bindingParserNodeFactory.SetupParser("var a = 1; var b = 2; var c = 3; a+b+c"); + var node1 = parser.ReadExpression().CastTo(); + var node2 = node1.SecondExpression.CastTo(); + var node3 = node2.SecondExpression.CastTo(); + + Assert.AreEqual(0, node1.StartPosition); + Assert.AreEqual(node1.EndPosition, node2.EndPosition); + Assert.AreEqual(node1.EndPosition, node3.EndPosition); + Assert.IsNotNull(node1.Variable); + Assert.IsNotNull(node2.Variable); + Assert.IsNotNull(node3.Variable); + Assert.AreEqual("a", node1.Variable.Name); + + Assert.AreEqual("var a = 1; var b = 2; var c = 3; a + b + c", node1.ToDisplayString()); + Assert.AreEqual("var b = 2; var c = 3; a + b + c", node2.ToDisplayString()); + Assert.AreEqual("var c = 3; a + b + c", node3.ToDisplayString()); + Assert.AreEqual("a + b + c", node3.SecondExpression.ToDisplayString()); + } private static string SkipWhitespaces(string str) => string.Join("", str.Where(c => !char.IsWhiteSpace(c))); diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index e655219247..a16ad9fb4c 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Linq.Expressions; using DotVVM.Framework.Compilation.Parser.Binding.Parser; @@ -6,16 +7,20 @@ using DotVVM.Framework.Utils; using System.Linq; using System.Threading.Tasks; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace DotVVM.Framework.Compilation.Binding { public class ExpressionBuildingVisitor : BindingParserNodeVisitor { public TypeRegistry Registry { get; set; } - public Expression Scope { get; set; } + public Expression? Scope { get; set; } public bool ResolveOnlyTypeName { get; set; } + public ImmutableDictionary Variables { get; set; } = + ImmutableDictionary.Empty; - private List currentErrors; + private List? currentErrors; private readonly MemberExpressionFactory memberExpressionFactory; public ExpressionBuildingVisitor(TypeRegistry registry, MemberExpressionFactory memberExpressionFactory) @@ -24,6 +29,7 @@ public ExpressionBuildingVisitor(TypeRegistry registry, MemberExpressionFactory this.memberExpressionFactory = memberExpressionFactory; } + [return: MaybeNull] protected T HandleErrors(TNode node, Func action, string defaultErrorMessage = "Binding compilation failed", bool allowResultNull = true) where TNode : BindingParserNode { @@ -44,6 +50,7 @@ protected T HandleErrors(TNode node, Func action, string def } if (!allowResultNull && result == null) { + if (currentErrors == null) currentErrors = new List(); currentErrors.Add(new BindingCompilationException(defaultErrorMessage, node)); } return result; @@ -64,8 +71,8 @@ protected void ThrowOnErrors() if (currentErrors.Count == 1) { if (currentErrors[0].StackTrace == null - || (currentErrors[0] is BindingCompilationException && (currentErrors[0] as BindingCompilationException).Tokens == null) - || (currentErrors[0] is AggregateException && (currentErrors[0] as AggregateException).Message == null)) + || (currentErrors[0] is BindingCompilationException compilationException && compilationException.Tokens == null) + || (currentErrors[0] is AggregateException aggregateException && aggregateException.Message == null)) throw currentErrors[0]; } throw new AggregateException(currentErrors); @@ -208,7 +215,7 @@ protected override Expression VisitFunctionCall(FunctionCallBindingParserNode no var args = new Expression[node.ArgumentExpressions.Count]; for (int i = 0; i < args.Length; i++) { - args[i] = HandleErrors(node.ArgumentExpressions[i], Visit); + args[i] = HandleErrors(node.ArgumentExpressions[i], Visit)!; } ThrowOnErrors(); @@ -217,14 +224,14 @@ protected override Expression VisitFunctionCall(FunctionCallBindingParserNode no protected override Expression VisitSimpleName(SimpleNameBindingParserNode node) { - return GetMemberOrTypeExpression(node, null); + return GetMemberOrTypeExpression(node, null) ?? Expression.Default(typeof(void)); } protected override Expression VisitConditionalExpression(ConditionalExpressionBindingParserNode node) { var condition = HandleErrors(node.ConditionExpression, n => TypeConversion.ImplicitConversion(Visit(n), typeof(bool), true)); - var trueExpr = HandleErrors(node.TrueExpression, Visit); - var falseExpr = HandleErrors(node.FalseExpression, Visit); + var trueExpr = HandleErrors(node.TrueExpression, Visit)!; + var falseExpr = HandleErrors(node.FalseExpression, Visit)!; ThrowOnErrors(); if (trueExpr.Type != falseExpr.Type) @@ -248,9 +255,9 @@ protected override Expression VisitMemberAccess(MemberAccessBindingParserNode no var target = Visit(node.TargetExpression); - if (target is UnknownStaticClassIdentifierExpression) + if (target is UnknownStaticClassIdentifierExpression unknownClass) { - var name = (target as UnknownStaticClassIdentifierExpression).Name + "." + identifierName; + var name = unknownClass.Name + "." + identifierName; var resolvedTypeExpression = Registry.Resolve(name, throwOnNotFound: false) ?? new UnknownStaticClassIdentifierExpression(name); @@ -269,7 +276,7 @@ protected override Expression VisitGenericName(GenericNameBindingParserNode node { var typeParameters = ResolveGenericArgumets(node.CastTo()); - return GetMemberOrTypeExpression(node, typeParameters); + return GetMemberOrTypeExpression(node, typeParameters) ?? Expression.Default(typeof(void)); } protected override Expression VisitLambda(LambdaBindingParserNode node) @@ -313,37 +320,58 @@ protected override Expression VisitLambdaParameter(LambdaParameterBindingParserN protected override Expression VisitBlock(BlockBindingParserNode node) { var left = HandleErrors(node.FirstExpression, Visit) ?? Expression.Default(typeof(void)); + + var originalVariables = this.Variables; + ParameterExpression? variable = null; + if (node.Variable is object) + { + variable = Expression.Parameter(left.Type, node.Variable.Name); + this.Variables = this.Variables.SetItem(node.Variable.Name, variable); + + left = Expression.Assign(variable, left); + } + var right = HandleErrors(node.SecondExpression, Visit) ?? Expression.Default(typeof(void)); + + this.Variables = originalVariables; ThrowOnErrors(); if (typeof(Task).IsAssignableFrom(left.Type)) { + if (variable is object) + throw new NotImplementedException("Variable definition of type Task is not supported."); return ExpressionHelper.RewriteTaskSequence(left, right); } + var variables = new [] { variable }.Where(x => x != null); if (right is BlockExpression rightBlock) { // flat the `(a; b; c; d; e; ...)` expression down - return Expression.Block(rightBlock.Variables, new Expression[] { left }.Concat(rightBlock.Expressions)); + return Expression.Block(variables.Concat(rightBlock.Variables), new Expression[] { left }.Concat(rightBlock.Expressions)); } - else return Expression.Block(left, right); + else return Expression.Block(variables, left, right); } protected override Expression VisitVoid(VoidBindingParserNode node) => Expression.Default(typeof(void)); - private Expression GetMemberOrTypeExpression(IdentifierNameBindingParserNode node, Type[] typeParameters) + private Expression? GetMemberOrTypeExpression(IdentifierNameBindingParserNode node, Type[]? typeParameters) { - if (string.IsNullOrWhiteSpace(node.Name)) return null; - - var expr = - Scope == null - ? Registry.Resolve(node.Name, throwOnNotFound: false) - : (memberExpressionFactory.GetMember(Scope, node.Name, typeParameters, throwExceptions: false, onlyMemberTypes: ResolveOnlyTypeName) - ?? Registry.Resolve(node.Name, throwOnNotFound: false)); + var name = node.Name; + if (string.IsNullOrWhiteSpace(name)) return null; + var expr = getExpression(); - if (expr == null) return new UnknownStaticClassIdentifierExpression(node.Name); + if (expr is null) return new UnknownStaticClassIdentifierExpression(name); if (expr is ParameterExpression && expr.Type == typeof(UnknownTypeSentinel)) throw new Exception($"Type of '{expr}' could not be resolved."); return expr; + + Expression? getExpression() + { + if (Variables.TryGetValue(name, out var variable)) + return variable; + if (Scope is object && memberExpressionFactory.GetMember(Scope, node.Name, typeParameters, throwExceptions: false, onlyMemberTypes: ResolveOnlyTypeName) is Expression scopeMember) + return scopeMember; + return Registry.Resolve(node.Name, throwOnNotFound: false); + } } private Type[] ResolveGenericArgumets(GenericNameBindingParserNode node) diff --git a/src/DotVVM.Framework/Compilation/Binding/StaticCommandBindingCompiler.cs b/src/DotVVM.Framework/Compilation/Binding/StaticCommandBindingCompiler.cs index fd0cee0b3f..f7aef2913f 100644 --- a/src/DotVVM.Framework/Compilation/Binding/StaticCommandBindingCompiler.cs +++ b/src/DotVVM.Framework/Compilation/Binding/StaticCommandBindingCompiler.cs @@ -50,6 +50,51 @@ public JsExpression CompileToJavascript(DataContextStack dataContext, Expression { expression = ReplaceCommandArgs(expression); + var js = TranslateVariableDeclaration(expression, e => CreateCommandExpression(dataContext, e)); + + if (js is JsInvocationExpression invocation && invocation.Target is JsIdentifierExpression identifier && identifier.Identifier == "resolve") + { + // optimize `new Promise(function (resolve) { resolve(x) })` to `Promise.resolve(x)` + identifier.ReplaceWith(new JsIdentifierExpression("Promise").Member("resolve")); + return js; + } + else + { + return new JsNewExpression(new JsIdentifierExpression("Promise"), new JsFunctionExpression( + new [] { new JsIdentifier("resolve"), new JsIdentifier("reject") }, + new JsBlockStatement(new JsExpressionStatement(js)) + )); + } + } + + private JsExpression TranslateVariableDeclaration(Expression expression, Func core) + { + expression = VariableHoistingVisitor.HoistVariables(expression); + if (expression is BlockExpression block && block.Variables.Any()) + { + var realBlock = block.Update(Enumerable.Empty(), block.Expressions); + + var variables = block.Variables; + var replacedVariables = ExpressionUtils.Replace( + Expression.Lambda(realBlock, variables), + variables.Select(v => { + var tmpVar = new JsTemporaryVariableParameter(); + return Expression.Parameter(v.Type, v.Name).AddParameterAnnotation(new BindingParameterAnnotation(extensionParameter: + new JavascriptTranslationVisitor.FakeExtensionParameter(_ => new JsSymbolicParameter(tmpVar)) + )); + }).ToArray() + ); + + return core(replacedVariables); + } + else + { + return core(expression); + } + } + + private JsExpression CreateCommandExpression(DataContextStack dataContext, Expression expression) + { var knockoutContext = new JsSymbolicParameter( JavascriptTranslator.KnockoutContextParameter, @@ -117,22 +162,7 @@ public JsExpression CompileToJavascript(DataContextStack dataContext, Expression else if (sp.Symbol == JavascriptTranslator.KnockoutViewModelParameter) sp.ReplaceWith(new JsSymbolicParameter(currentContextVariable).Member("$data")); else if (sp.Symbol == CommandBindingExpression.SenderElementParameter) sp.Symbol = senderVariable; } - - { - if (js is JsInvocationExpression invocation && invocation.Target is JsIdentifierExpression identifier && identifier.Identifier == "resolve") - { - // optimize `new Promise(function (resolve) { resolve(x) })` to `Promise.resolve(x)` - identifier.ReplaceWith(new JsIdentifierExpression("Promise").Member("resolve")); - return js; - } - else - { - return new JsNewExpression(new JsIdentifierExpression("Promise"), new JsFunctionExpression( - new[] { new JsIdentifier("resolve"), new JsIdentifier("reject") }, - new JsBlockStatement(new JsExpressionStatement(js)) - )); - } - } + return js; } private Func CreatePromiseMethodCallAnnotationFactory(DataContextStack dataContext, Expression ex) diff --git a/src/DotVVM.Framework/Compilation/Binding/VariableHoistingVisitor.cs b/src/DotVVM.Framework/Compilation/Binding/VariableHoistingVisitor.cs new file mode 100644 index 0000000000..c588800b92 --- /dev/null +++ b/src/DotVVM.Framework/Compilation/Binding/VariableHoistingVisitor.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace DotVVM.Framework.Compilation.Binding +{ + /// Takes all variables from block expressions and hoists them into a top-level expression. + /// This prevents glitches with undefined variables that would be introduced by following steps in static command translation. + public class VariableHoistingVisitor: ExpressionVisitor + { + public List Variables { get; } = new List(); + protected override Expression VisitBlock(BlockExpression node) + { + Variables.AddRange(node.Variables); + + if (node.Expressions.Count == 1) + return node.Expressions.Single(); + return node.Update(Enumerable.Empty(), node.Expressions); + } + + /// Takes all variables from block expressions and hoists them into a top-level expression. + /// This prevents glitches with undefined variables that would be introduced by following steps in static command translation. + public static Expression HoistVariables(Expression expression) + { + var v = new VariableHoistingVisitor(); + expression = v.Visit(expression); + + if (v.Variables.Count == 0) + { + return expression; + } + else if (expression is BlockExpression blockExpression) + { + return blockExpression.Update(v.Variables, blockExpression.Expressions); + } + else + { + return Expression.Block(v.Variables, new [] { expression }); + } + } + } +} diff --git a/src/DotVVM.Framework/Compilation/Javascript/JsTemporaryVariableResolver.cs b/src/DotVVM.Framework/Compilation/Javascript/JsTemporaryVariableResolver.cs index c37a1c6111..827d3088fe 100644 --- a/src/DotVVM.Framework/Compilation/Javascript/JsTemporaryVariableResolver.cs +++ b/src/DotVVM.Framework/Compilation/Javascript/JsTemporaryVariableResolver.cs @@ -97,7 +97,7 @@ IEnumerable getChars(bool isFirst) public sealed class JsTemporaryVariableParameter: CodeSymbolicParameter { public JsExpression Initializer { get; } - public string PreferedName { get; } + public string PreferredName { get; } public JsTemporaryVariableParameter(JsExpression initializer = null) : base("tmp_var[" + initializer?.ToString() + "]") @@ -105,4 +105,4 @@ public JsTemporaryVariableParameter(JsExpression initializer = null) this.Initializer = initializer; } } -} \ No newline at end of file +} diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs index 175bf568ec..580c160d5f 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BindingParser.cs @@ -474,6 +474,24 @@ private BindingParserNode ReadIdentifierExpression(bool onlyTypeName) { expression = ReadArrayAccess(startIndex, expression); } + else if (!onlyTypeName && next.Type == BindingTokenType.Identifier && expression is SimpleNameBindingParserNode keywordNameExpression) + { + // we have `identifier identifier` - the first one must be a KEYWORD USAGE + + var keyword = keywordNameExpression.Name; + if (keyword == "var") + { + return ReadVariableExpression(startIndex); + } + else if (keyword == "val" || keyword == "let" || keyword == "const") + { + expression = CreateNode(expression, startIndex, $"Variable declaration using {keyword} is not supported. Did you intend to use the var keyword?"); + } + else + { + expression = CreateNode(expression, startIndex, $"Expression '{expression.ToDisplayString()}' can not be followed by an identifier. Did you intent to declare a variable using the var keyword?"); + } + } else { break; @@ -483,6 +501,35 @@ private BindingParserNode ReadIdentifierExpression(bool onlyTypeName) return expression; } + private BindingParserNode ReadVariableExpression(int startIndex) + { + var variableName = ReadIdentifierNameExpression(); + if (!(variableName is SimpleNameBindingParserNode)) + { + variableName = CreateNode(variableName, variableName.StartPosition, $"Variable name can not be generic, please use the `var {variableName.Name} = X` syntax."); + } + + var incorrectEquals = IsCurrentTokenIncorrect(BindingTokenType.AssignOperator); + if (!incorrectEquals) + { + Read(); + } + + var value = ReadSemicolonSeparatedExpression(); + + if (value is BlockBindingParserNode resultBlock) + { + return CreateNode( + new BlockBindingParserNode(resultBlock.FirstExpression, resultBlock.SecondExpression, variableName), + startIndex, + !incorrectEquals ? null : $"Expected variable declaration `var {variableName.Name} = {resultBlock.FirstExpression}`"); + } + else + { + return CreateNode(value, startIndex, $"Variable declaration must be followed by a semicolon and another expression. Please add the return value after `var {variableName.Name} = {value}; ...` or remove the `var {variableName.Name} = ` in case you only want to invoke the expression."); + } + } + private BindingParserNode ReadArrayAccess(int startIndex, BindingParserNode expression) { // array access diff --git a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BlockBindingParserNode.cs b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BlockBindingParserNode.cs index b7697e11bb..df4e7d045a 100644 --- a/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BlockBindingParserNode.cs +++ b/src/DotVVM.Framework/Compilation/Parser/Binding/Parser/BlockBindingParserNode.cs @@ -11,17 +11,20 @@ public class BlockBindingParserNode : BindingParserNode { public BindingParserNode FirstExpression { get; } public BindingParserNode SecondExpression { get; } + /// When not null, the result of the first expression is assigned to a variable with the specified name. + public IdentifierNameBindingParserNode? Variable { get; } - public BlockBindingParserNode(BindingParserNode firstExpression, BindingParserNode secondExpression) + public BlockBindingParserNode(BindingParserNode firstExpression, BindingParserNode secondExpression, IdentifierNameBindingParserNode? variable = null) { this.FirstExpression = firstExpression; this.SecondExpression = secondExpression; + this.Variable = variable; } public override IEnumerable EnumerateChildNodes() => new [] { FirstExpression, SecondExpression }; public override string ToDisplayString() - => $"{FirstExpression.ToDisplayString()}; {SecondExpression.ToDisplayString()}"; + => (Variable is object ? $"var {Variable.Name} = " : "") + $"{FirstExpression.ToDisplayString()}; {SecondExpression.ToDisplayString()}"; } } diff --git a/src/DotVVM.Framework/Controls/Infrastructure/BodyResourceLinks.cs b/src/DotVVM.Framework/Controls/Infrastructure/BodyResourceLinks.cs index 78a468d162..f2f68dabec 100644 --- a/src/DotVVM.Framework/Controls/Infrastructure/BodyResourceLinks.cs +++ b/src/DotVVM.Framework/Controls/Infrastructure/BodyResourceLinks.cs @@ -33,9 +33,7 @@ protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext writer.RenderSelfClosingTag("input"); // init on load - var initCode = $@" -window.dotvvm.init({JsonConvert.ToString(CultureInfo.CurrentCulture.Name, '"', StringEscapeHandling.EscapeHtml)}); -"; + var initCode = $@"window.dotvvm.init({JsonConvert.ToString(CultureInfo.CurrentCulture.Name, '"', StringEscapeHandling.EscapeHtml)});"; new InlineScriptResource(initCode, defer: true) .Render(writer, context, "dotvvm-init-script");