Skip to content

Commit

Permalink
More types incompatibility detection fixes + relevant tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
jwaliszko committed Jan 2, 2015
1 parent 0ee8723 commit f400378
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 20 deletions.
180 changes: 169 additions & 11 deletions src/ExpressiveAnnotations.Tests/ParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,7 @@ public void verify_logic_without_context()
Assert.IsFalse(parser.Parse<object>("0 != 0").Invoke(null));
Assert.IsTrue(parser.Parse<object>("0 >= 0").Invoke(null));
Assert.IsTrue(parser.Parse<object>("0 <= 0").Invoke(null));

Assert.IsTrue(parser.Parse<object>("0 == 0").Invoke(null));

Assert.IsTrue(parser.Parse<object>("0 == 0").Invoke(null));
Assert.IsFalse(parser.Parse<object>("0 == 1").Invoke(null));
Assert.IsTrue(parser.Parse<object>("-1 == -1").Invoke(null));
Assert.IsTrue(parser.Parse<object>("0 != 1").Invoke(null));
Expand All @@ -144,6 +142,7 @@ public void verify_logic_without_context()
Assert.IsTrue(parser.Parse<object>("2 - 1 > 0").Invoke(null));

Assert.IsTrue(parser.Parse<object>("null == null").Invoke(null));
Assert.IsFalse(parser.Parse<object>("null != null").Invoke(null));
Assert.IsTrue(parser.Parse<object>("'' == ''").Invoke(null));
Assert.IsTrue(parser.Parse<object>("' ' == ' '").Invoke(null));
Assert.IsTrue(parser.Parse<object>("' ' != ' '").Invoke(null));
Expand Down Expand Up @@ -190,9 +189,11 @@ public void verify_logic_without_context()
[TestMethod]
public void verify_logic_with_context()
{
var now = DateTime.Now;
var model = new Model
{
Date = DateTime.Now,
NDate = now,
Date = now,
Number = 0,
Flag = true,
Text = "hello world",
Expand All @@ -209,7 +210,8 @@ public void verify_logic_with_context()
Guid2 = Guid.Empty,
SubModel = new Model
{
Date = DateTime.Now.AddDays(1),
NDate = now.AddDays(1),
Date = now.AddDays(1),
Number = 1,
Flag = false,
Text = " hello world ",
Expand Down Expand Up @@ -249,7 +251,8 @@ public void verify_logic_with_context()
Assert.IsFalse(parser.Parse<Model>("SubModel.Flag && true").Invoke(model));

Assert.IsTrue(parser.Parse<Model>("Number < SubModel.Number").Invoke(model));

Assert.IsTrue(parser.Parse<Model>("Date <= NDate").Invoke(model));
Assert.IsTrue(parser.Parse<Model>("NDate != null").Invoke(model));
Assert.IsTrue(parser.Parse<Model>("SubModel.Date < NextWeek()").Invoke(model));
Assert.IsTrue(parser.Parse<Model>("IncNumber(0) == SubModel.Number").Invoke(model));
Assert.IsTrue(parser.Parse<Model>("IncNumber(Number) == SubModel.Number").Invoke(model));
Expand All @@ -265,6 +268,10 @@ public void verify_logic_with_context()
Assert.IsTrue(parser.Parse<Model>("Guid2 == Guid('00000000-0000-0000-0000-000000000000')").Invoke(model));
Assert.IsTrue(parser.Parse<Model>("Guid1 != Guid2").Invoke(model));

var subModel = new Model();
var newModel = new Model {SubModel = subModel, SubModelObject = subModel};
Assert.IsTrue(parser.Parse<Model>("SubModel == SubModelObject").Invoke(newModel));

const string expression =
@"Flag == !false
&& (
Expand Down Expand Up @@ -951,7 +958,22 @@ ... Max(1.1, 1.2, 'a') ...

try
{
var model = new Model { Date = new DateTime(2014, 1, 1) };
parser.Parse<object>("'asd' > null").Invoke(null);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 7:
... > null ...
^--- Operator '>' cannot be applied to operands of type 'System.String' and 'null'.",
e.Message);
}

try
{
var model = new Model();
parser.Parse<Model>("Date == 0").Invoke(model);
Assert.Fail();
}
Expand All @@ -967,27 +989,157 @@ ... Max(1.1, 1.2, 'a') ...

try
{
var model = new Model {Date = new DateTime(2014, 1, 1)};
parser.Parse<Model>("Date != '0'").Invoke(model);
var model = new Model();
parser.Parse<Model>("Date != 'asd'").Invoke(model);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 6:
... != '0' ...
... != 'asd' ...
^--- Operator '!=' cannot be applied to operands of type 'System.DateTime' and 'System.String'.",
e.Message);
}
}

try
{
var model = new Model();
parser.Parse<Model>("Date > 0").Invoke(model);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 6:
... > 0 ...
^--- Operator '>' cannot be applied to operands of type 'System.DateTime' and 'System.Int32'.",
e.Message);
}

try
{
var model = new Model();
parser.Parse<Model>("Date > null").Invoke(model);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 6:
... > null ...
^--- Operator '>' cannot be applied to operands of type 'System.DateTime' and 'null'.",
e.Message);
}

try
{
var model = new Model();
parser.Parse<Model>("Date > SubModelObject").Invoke(model);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 6:
... > SubModelObject ...
^--- Operator '>' cannot be applied to operands of type 'System.DateTime' and 'System.Object'.",
e.Message);
}

try
{
var model = new Model();
parser.Parse<Model>("NDate > SubModelObject").Invoke(model);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 7:
... > SubModelObject ...
^--- Operator '>' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.Object'.",
e.Message);
}

try
{
var model = new Model();
parser.Parse<Model>("NDate > null").Invoke(model);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 7:
... > null ...
^--- Operator '>' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'null'.",
e.Message);
}

try
{
var model = new Model();
parser.Parse<Model>("NDate != 'asd'").Invoke(model);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 7:
... != 'asd' ...
^--- Operator '!=' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.String'.",
e.Message);
}

try
{
var bag = new Bag {Lexer = new Lexer(), Parser = new Parser()};
parser.Parse<Bag>("Lexer != Parser").Invoke(bag);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 7:
... != Parser ...
^--- Operator '!=' cannot be applied to operands of type 'ExpressiveAnnotations.Analysis.Lexer' and 'ExpressiveAnnotations.Analysis.Parser'.",
e.Message);
}

try
{
var model = new Model();
parser.Parse<Model>("SubModelObject > SubModelObject").Invoke(model);
Assert.Fail();
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidOperationException);
Assert.AreEqual(
@"Parse error on line 1, column 16:
... > SubModelObject ...
^--- Operator '>' cannot be applied to operands of type 'System.Object' and 'System.Object'.",
e.Message);
}
}

private class Model
{
public const string Const = "inside";

public object SubModelObject { get; set; }
public Model SubModel { get; set; }

public DateTime? NDate { get; set; }
public DateTime Date { get; set; }
public int? Number { get; set; }
public bool Flag { get; set; }
Expand Down Expand Up @@ -1080,6 +1232,12 @@ public SampleTwo()
public Carriage Vehicle { get; set; }
}

public class Bag
{
public Parser Parser { get; set; }
public Lexer Lexer { get; set; }
}

private enum YesNo
{
Yes,
Expand Down
20 changes: 17 additions & 3 deletions src/ExpressiveAnnotations/Analysis/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,25 +351,39 @@ private Expression ParseRelExp()
var arg1 = ParseNotExp();
if (!new[] {TokenType.LT, TokenType.LE, TokenType.GT, TokenType.GE, TokenType.EQ, TokenType.NEQ}.Contains(PeekType()))
return arg1;
var oper = PeekToken();
var oper = PeekToken();
ReadToken();
var arg2 = ParseNotExp();

if (oper.Type != TokenType.EQ && oper.Type != TokenType.NEQ)
if ((!arg1.Type.IsNumeric() || !arg2.Type.IsNumeric()) && (!arg1.Type.IsDateTime() || !arg2.Type.IsDateTime()))
{
if (!arg1.IsNull() && arg2.IsNull())
throw new ParseErrorException(
string.Format("Operator '{0}' cannot be applied to operands of type '{1}' and 'null'.", oper.Value, arg1.Type),
oper.Location);
if (arg1.IsNull() && !arg2.IsNull())
throw new ParseErrorException(
string.Format("Operator '{0}' cannot be applied to operands of type 'null' and '{1}'.", oper.Value, arg1.Type),
oper.Location);
if (!(arg1.Type.IsNumeric() && arg2.Type.IsNumeric()) && !(arg1.Type.IsDateTime() && arg2.Type.IsDateTime()))
throw new ParseErrorException(
string.Format("Operator '{0}' cannot be applied to operands of type '{1}' and '{2}'.", oper.Value, arg1.Type, arg2.Type),
oper.Location);
}

var type1 = arg1.Type;
var type2 = arg2.Type;
Helper.MakeTypesCompatible(arg1, arg2, out arg1, out arg2);

if (oper.Type == TokenType.EQ || oper.Type == TokenType.NEQ)
if (arg1.Type != arg2.Type && !arg1.Type.IsObject() && !arg2.Type.IsObject())
{
if (arg1.Type != arg2.Type
&& !arg1.IsNull() && !arg2.IsNull()
&& !arg1.Type.IsObject() && !arg2.Type.IsObject())
throw new ParseErrorException(
string.Format("Operator '{0}' cannot be applied to operands of type '{1}' and '{2}'.", oper.Value, type1, type2),
oper.Location);
}

switch (oper.Type)
{
Expand Down
23 changes: 17 additions & 6 deletions src/ExpressiveAnnotations/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ public static void MakeTypesCompatible(Expression e1, Expression e2, out Express
: Expression.Convert(oute2, typeof (double));

// non-nullable operand is converted to nullable if necessary, and the lifted-to-nullable form of the comparison is used (C# rule, which is currently not followed by expression trees)
if (oute1.Type.IsNullable() && !oute2.Type.IsNullable())
oute2 = Expression.Convert(oute2, oute1.Type);
else if (!oute1.Type.IsNullable() && oute2.Type.IsNullable())
oute1 = Expression.Convert(oute1, oute2.Type);
if (oute1.Type.UnderlyingType() == oute2.Type.UnderlyingType())
{
if (oute1.Type.IsNullable() && !oute2.Type.IsNullable())
oute2 = Expression.Convert(oute2, oute1.Type);
else if (!oute1.Type.IsNullable() && oute2.Type.IsNullable())
oute1 = Expression.Convert(oute1, oute2.Type);
}
}

public static bool IsDateTime(this Type type)
Expand Down Expand Up @@ -95,12 +98,20 @@ public static bool IsObject(this Type type)
return typeof (object) == type;
}

public static Type ToNullable(this Type type)
public static bool IsNull(this Expression expr)
{
if (expr == null)
throw new ArgumentNullException("expr");

return "null".Equals(expr.ToString());
}

public static Type UnderlyingType(this Type type)
{
if (type == null)
throw new ArgumentNullException("type");

return typeof (Nullable<>).MakeGenericType(type);
return type.IsNullable() ? Nullable.GetUnderlyingType(type) : type;
}

public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly)
Expand Down

0 comments on commit f400378

Please sign in to comment.