Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for variables in value bindings #869

Merged
merged 8 commits into from
Mar 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@
],
"typeMetadata": {}
}">
<script defer="" src="data:text/javascript;base64,DQp3aW5kb3cuZG90dnZtLmluaXQoImVuLVVTIik7DQo="></script>
<script defer="" src="data:text/javascript;base64,d2luZG93LmRvdHZ2bS5pbml0KCJlbi1VUyIpOw=="></script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@
}
}
}">
<script defer="" src="data:text/javascript;base64,DQp3aW5kb3cuZG90dnZtLmluaXQoImVuLVVTIik7DQo="></script>
<script defer="" src="data:text/javascript;base64,d2luZG93LmRvdHZ2bS5pbml0KCJlbi1VUyIpOw=="></script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<SignAssembly>True</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>latest</LangVersion>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<PublicSign>True</PublicSign>
Expand All @@ -24,16 +24,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.14.0" />
<PackageReference Include="Ben.Demystifier" Version="0.1.4" />
<PackageReference Include="CheckTestOutput" Version="0.3.0" />
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Ben.Demystifier" Version="0.1.6" />
<PackageReference Include="CheckTestOutput" Version="0.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.8.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
</ItemGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BlockBindingParserNode>();

var firstExpression =
node.FirstExpression.As<FunctionCallBindingParserNode>();

var secondExpression = node.SecondExpression.As<UnaryOperatorBindingParserNode>();

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()));
}

Expand Down Expand Up @@ -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<BlockBindingParserNode>();
var node2 = node1.SecondExpression.CastTo<BlockBindingParserNode>();
var node3 = node2.SecondExpression.CastTo<BlockBindingParserNode>();

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)));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using DotVVM.Framework.Compilation.Parser.Binding.Parser;
using DotVVM.Framework.Compilation.Parser.Binding.Tokenizer;
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<Expression>
{
public TypeRegistry Registry { get; set; }
public Expression Scope { get; set; }
public Expression? Scope { get; set; }
public bool ResolveOnlyTypeName { get; set; }
public ImmutableDictionary<string, ParameterExpression> Variables { get; set; } =
ImmutableDictionary<string, ParameterExpression>.Empty;

private List<Exception> currentErrors;
private List<Exception>? currentErrors;
private readonly MemberExpressionFactory memberExpressionFactory;

public ExpressionBuildingVisitor(TypeRegistry registry, MemberExpressionFactory memberExpressionFactory)
Expand All @@ -24,6 +29,7 @@ public ExpressionBuildingVisitor(TypeRegistry registry, MemberExpressionFactory
this.memberExpressionFactory = memberExpressionFactory;
}

[return: MaybeNull]
protected T HandleErrors<T, TNode>(TNode node, Func<TNode, T> action, string defaultErrorMessage = "Binding compilation failed", bool allowResultNull = true)
where TNode : BindingParserNode
{
Expand All @@ -44,6 +50,7 @@ protected T HandleErrors<T, TNode>(TNode node, Func<TNode, T> action, string def
}
if (!allowResultNull && result == null)
{
if (currentErrors == null) currentErrors = new List<Exception>();
currentErrors.Add(new BindingCompilationException(defaultErrorMessage, node));
}
return result;
Expand All @@ -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);
Expand Down Expand Up @@ -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();

Expand All @@ -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)
Expand All @@ -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);

Expand All @@ -269,7 +276,7 @@ protected override Expression VisitGenericName(GenericNameBindingParserNode node
{
var typeParameters = ResolveGenericArgumets(node.CastTo<GenericNameBindingParserNode>());

return GetMemberOrTypeExpression(node, typeParameters);
return GetMemberOrTypeExpression(node, typeParameters) ?? Expression.Default(typeof(void));
}

protected override Expression VisitLambda(LambdaBindingParserNode node)
Expand Down Expand Up @@ -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)
Expand Down
Loading