Skip to content

Commit

Permalink
Merge pull request #1031 from riganti/fix/array-and-nullable-type-dec…
Browse files Browse the repository at this point in the history
…larations

Fixed issue with array and nullable type declarations in lambda parameters
  • Loading branch information
acizmarik committed Jun 16, 2021
2 parents 6379a61 + 9f779b1 commit 0e553db
Show file tree
Hide file tree
Showing 10 changed files with 385 additions and 52 deletions.
70 changes: 70 additions & 0 deletions src/DotVVM.Framework.Tests/Binding/BindingCompilationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ public void BindingCompiler_Valid_Lambda_Test()
Assert.AreEqual(65, result2);
}

[TestMethod]
public void BindingCompiler_GenericMethodCall_ExplicitTypeParameters()
{
var viewModel = new TestViewModel { StringProp = "abc" };
var result = (Type)ExecuteBinding("GetType<string>(StringProp)", viewModel);
Assert.AreEqual(typeof(string), result);
}

[TestMethod]
[DataRow("() => ;", typeof(Action), null)]
[DataRow("() => \"HelloWorld\"", typeof(Func<string>), typeof(string))]
Expand Down Expand Up @@ -379,6 +387,68 @@ public void BindingCompiler_Valid_LambdaParameter_PreferFunc(string expr)
Assert.AreEqual("Func", result);
}

[TestMethod]
[DataRow("(int? arg) => arg.Value + 1", typeof(Func<int?, int>))]
[DataRow("(double? arg) => arg.Value + 0.1", typeof(Func<double?, double>))]
public void BindingCompiler_Valid_LambdaParameter_Nullable(string expr, Type type)
{
var viewModel = new TestLambdaCompilation();
var result = ExecuteBinding(expr, viewModel);
Assert.AreEqual(type, result.GetType());
}

[TestMethod]
[DataRow("(int[] array) => array[0]", typeof(Func<int[], int>))]
[DataRow("(double[] array) => array[0]", typeof(Func<double[], double>))]
[DataRow("(int[][] jaggedArray) => jaggedArray[0][1]", typeof(Func<int[][], int>))]
[DataRow("(int[][] jaggedArray) => jaggedArray[0]", typeof(Func<int[][], int[]>))]
public void BindingCompiler_Valid_LambdaParameter_Array(string expr, Type type)
{
var viewModel = new TestLambdaCompilation();
var result = ExecuteBinding(expr, viewModel);
Assert.AreEqual(type, result.GetType());
}

[TestMethod]
[DataRow("(int?[] arrayOfNullables) => arrayOfNullables[0]", typeof(Func<int?[], int?>))]
[DataRow("(System.Collections.Generic.List<int?> list) => list[0]", typeof(Func<List<int?>, int?>))]
[DataRow("(System.Collections.Generic.List<int?[]> list) => list[0]", typeof(Func<List<int?[]>, int?[]>))]
[DataRow("(System.Collections.Generic.List<int?[][]> list) => list[0][0]", typeof(Func<List<int?[][]>, int?[]>))]
[DataRow("(System.Collections.Generic.Dictionary<int?,double?> dict) => dict[0]", typeof(Func<Dictionary<int?, double?>, double?>))]
[DataRow("(System.Collections.Generic.Dictionary<int?[],double?> dict) => 0", typeof(Func<Dictionary<int?[], double?>, int>))]
public void BindingCompiler_Valid_LambdaParameter_CombinedTypeModifies(string expr, Type type)
{
var viewModel = new TestLambdaCompilation();
var result = ExecuteBinding(expr, viewModel);
Assert.AreEqual(type, result.GetType());
}

[TestMethod]
[DataRow("(string? arg) => arg")]
[DataRow("(int[]? arg) => arg")]
public void BindingCompiler_Invalid_LambdaParameter_NullableReferenceTypes(string expr)
{
var exceptionThrown = false;
try
{
var viewModel = new TestLambdaCompilation();
ExecuteBinding(expr, viewModel);
}
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, "as nullable is not supported!");
exceptionThrown = true;
}

Assert.IsTrue(exceptionThrown);
}

[TestMethod]
public void BindingCompiler_Valid_ExtensionMethods()
{
Expand Down
39 changes: 16 additions & 23 deletions src/DotVVM.Framework.Tests/Parser/Binding/BindingParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -620,15 +620,14 @@ public void BindingParser_GenericExpresion_SimpleList()

var memberAccess = node as MemberAccessBindingParserNode;
Assert.IsNotNull(memberAccess);
var target = memberAccess.TargetExpression as MemberAccessBindingParserNode;
var enumerator = memberAccess.MemberNameExpression as IdentifierNameBindingParserNode;
var target = memberAccess.TargetExpression as GenericTypeReferenceBindingParserNode;
var enumerator = memberAccess.MemberNameExpression;
Assert.IsNotNull(target);
Assert.IsTrue(enumerator?.Name == "Enumerator");
var genericName = target.MemberNameExpression.CastTo<GenericNameBindingParserNode>();
Assert.AreEqual("Enumerator", enumerator?.Name);

Assert.IsTrue(genericName.Name == "List");
Assert.IsTrue(genericName.TypeArguments.Count == 1);
Assert.IsTrue(genericName.TypeArguments[0].CastTo<IdentifierNameBindingParserNode>().Name == "string");
Assert.AreEqual("System.Collections.Generic.List", target.Type.ToDisplayString());
Assert.AreEqual(1, target.Arguments.Count);
Assert.AreEqual("string", target.Arguments[0].CastTo<ActualTypeReferenceBindingParserNode>().Type.ToDisplayString());
}

[TestMethod]
Expand All @@ -638,18 +637,17 @@ public void BindingParser_GenericExpresion_Dictionary()
var node = parser.ReadExpression();

var memberAccess = node.CastTo<MemberAccessBindingParserNode>();
var target = memberAccess.TargetExpression.CastTo<MemberAccessBindingParserNode>();
var target = memberAccess.TargetExpression.CastTo<GenericTypeReferenceBindingParserNode>();
var valueCollection = memberAccess.MemberNameExpression.CastTo<IdentifierNameBindingParserNode>();
var genericType = target.MemberNameExpression.CastTo<GenericNameBindingParserNode>();

Assert.IsTrue(genericType.Name == "Dictionary");
Assert.IsTrue(valueCollection.Name == "ValueCollection");
Assert.AreEqual("System.Collections.Generic.Dictionary", target.Type.ToDisplayString());
Assert.AreEqual("ValueCollection", valueCollection.Name);

var arg0 = genericType.TypeArguments[0].CastTo<IdentifierNameBindingParserNode>();
var arg1 = genericType.TypeArguments[1].CastTo<IdentifierNameBindingParserNode>();
var arg0 = target.Arguments[0].CastTo<ActualTypeReferenceBindingParserNode>();
var arg1 = target.Arguments[1].CastTo<ActualTypeReferenceBindingParserNode>();

Assert.IsTrue(arg0?.Name == "string");
Assert.IsTrue(arg1?.Name == "int");
Assert.AreEqual("string", arg0?.Type.ToDisplayString());
Assert.AreEqual("int", arg1?.Type.ToDisplayString());
}

[TestMethod]
Expand All @@ -661,16 +659,11 @@ public void BindingParser_GenericExpresion_DictionaryTupleInside()

var memberAccess = node as MemberAccessBindingParserNode;
Assert.IsNotNull(memberAccess);
var target = memberAccess.TargetExpression as MemberAccessBindingParserNode;
var valueCollection = memberAccess.MemberNameExpression as IdentifierNameBindingParserNode;
var target = memberAccess.TargetExpression as GenericTypeReferenceBindingParserNode;
var valueCollection = memberAccess.MemberNameExpression;
Assert.IsNotNull(target);
Assert.IsNotNull(valueCollection);

var arg0 = target.MemberNameExpression.CastTo<GenericNameBindingParserNode>()
.TypeArguments[0].CastTo<GenericNameBindingParserNode>();
var arg1 = target.MemberNameExpression.CastTo<GenericNameBindingParserNode>()
.TypeArguments[1].CastTo<GenericNameBindingParserNode>();

Assert.IsTrue(string.Equals(originalString, node.ToDisplayString()));
}

Expand Down Expand Up @@ -721,7 +714,7 @@ public void BindingParser_GenericExpresion_MultipleInside()
var parser = bindingParserNodeFactory.SetupParser(originalString);
var node = parser.ReadExpression();

Assert.IsTrue(node is MemberAccessBindingParserNode);
Assert.IsTrue(node is TypeOrFunctionReferenceBindingParserNode);
Assert.IsTrue(string.Equals(originalString, node.ToDisplayString()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ protected override Expression VisitMemberAccess(MemberAccessBindingParserNode no
{
var nameNode = node.MemberNameExpression;
var typeParameters = nameNode is GenericNameBindingParserNode
? ResolveGenericArgumets(nameNode.CastTo<GenericNameBindingParserNode>())
? ResolveGenericArguments(nameNode.CastTo<GenericNameBindingParserNode>().TypeArguments)
: null;
var identifierName = (typeParameters?.Count() ?? 0) > 0
? $"{nameNode.Name}`{typeParameters.Count()}"
Expand All @@ -352,9 +352,39 @@ protected override Expression VisitMemberAccess(MemberAccessBindingParserNode no

protected override Expression VisitGenericName(GenericNameBindingParserNode node)
{
var typeParameters = ResolveGenericArgumets(node.CastTo<GenericNameBindingParserNode>());
var parameters = ResolveGenericArguments(node.TypeArguments);
return GetMemberOrTypeExpression(node, parameters) ?? Expression.Default(typeof(void));
}

protected override Expression VisitTypeReference(TypeReferenceBindingParserNode node)
{
if (node is ActualTypeReferenceBindingParserNode actualType)
{
return Visit(actualType.Type);
}
else if (node is NullableTypeReferenceBindingParserNode nullableType)
{
var innerTypeExpr = Visit(nullableType.InnerType);
if (!innerTypeExpr.Type.IsValueType)
throw new BindingCompilationException($"Wrapping {innerTypeExpr.Type} as nullable is not supported!", node);

return new StaticClassIdentifierExpression(innerTypeExpr.Type.MakeNullableType());
}
else if (node is ArrayTypeReferenceBindingParserNode arrayType)
{
var elementTypeExpr = Visit(arrayType.ElementType);
return new StaticClassIdentifierExpression(elementTypeExpr.Type.MakeArrayType());
}
else if (node is GenericTypeReferenceBindingParserNode genericType)
{
var identifierName = $"{genericType.Type.ToDisplayString()}`{genericType.Arguments.Count()}";
var parameters = ResolveGenericArguments(genericType.Arguments);

return GetMemberOrTypeExpression(node, typeParameters) ?? Expression.Default(typeof(void));
var resolvedTypeExpr = Registry.Resolve(identifierName, throwOnNotFound: false) ?? new UnknownStaticClassIdentifierExpression(identifierName);
return new StaticClassIdentifierExpression(resolvedTypeExpr.Type.MakeGenericType(parameters));
}

throw new DotvvmCompilationException($"Unknown type reference binding node {node.GetType()}!");
}

protected override Expression VisitLambda(LambdaBindingParserNode node)
Expand All @@ -375,7 +405,7 @@ protected override Expression VisitLambda(LambdaBindingParserNode node)
}

for (var i = 0; i < lambdaParameters.Length; i++)
lambdaParameters[i] = (ParameterExpression)HandleErrors(node.ParameterExpressions[i], Visit)!;
lambdaParameters[i] = (ParameterExpression)Visit(node.ParameterExpressions[i]);

// Make sure that parameter identifiers are distinct
if (lambdaParameters.GroupBy(param => param.Name).Any(group => group.Count() > 1))
Expand Down Expand Up @@ -509,22 +539,21 @@ protected override Expression VisitFormattedExpression(FormattedBindingParserNod
}
}

private Type[] ResolveGenericArgumets(GenericNameBindingParserNode node)
private Type[] ResolveGenericArguments(List<TypeReferenceBindingParserNode> arguments)
{
var parameters = new Type[node.TypeArguments.Count];
var resolvedArguments = new Type[arguments.Count];

for (int i = 0; i < node.TypeArguments.Count; i++)
for (var i = 0; i < arguments.Count; i++)
{
var typeArgument = node.TypeArguments[i];

parameters[i] = Visit(typeArgument).Type;
var typeArgument = arguments[i];
resolvedArguments[i] = Visit(typeArgument).Type;
}
return parameters;
return resolvedArguments;
}

private void ThrowIfNotTypeNameRelevant(BindingParserNode node)
{
if (ResolveOnlyTypeName && !(node is MemberAccessBindingParserNode) && !(node is IdentifierNameBindingParserNode) && !(node is AssemblyQualifiedNameBindingParserNode))
if (ResolveOnlyTypeName && !(node is MemberAccessBindingParserNode) && !(node is IdentifierNameBindingParserNode) && !(node is AssemblyQualifiedNameBindingParserNode) && !(node is TypeReferenceBindingParserNode) && !(node is TypeOrFunctionReferenceBindingParserNode))
{
throw new Exception("Only type name is supported.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ public IAbstractBaseTypeDirective BuildBaseTypeDirective(DothtmlDirectiveNode di
private Expression? ParseDirectiveExpression(DothtmlDirectiveNode directive, BindingParserNode expressionSyntax)
{
TypeRegistry registry;
if (expressionSyntax is TypeOrFunctionReferenceBindingParserNode typeOrFunction)
expressionSyntax = typeOrFunction.ToTypeReference();

if (expressionSyntax is AssemblyQualifiedNameBindingParserNode assemblyQualifiedName)
{
registry = TypeRegistry.DirectivesDefault(compiledAssemblyCache, assemblyQualifiedName.AssemblyName.ToDisplayString());
Expand Down
Loading

0 comments on commit 0e553db

Please sign in to comment.