diff --git a/src/ExpressiveAnnotations.MvcWebSample.UITests/ExpressiveAnnotations.MvcWebSample.UITests.csproj b/src/ExpressiveAnnotations.MvcWebSample.UITests/ExpressiveAnnotations.MvcWebSample.UITests.csproj index bd25f46..f42fb97 100644 --- a/src/ExpressiveAnnotations.MvcWebSample.UITests/ExpressiveAnnotations.MvcWebSample.UITests.csproj +++ b/src/ExpressiveAnnotations.MvcWebSample.UITests/ExpressiveAnnotations.MvcWebSample.UITests.csproj @@ -44,12 +44,12 @@ - - ..\packages\Selenium.WebDriver.2.51.0\lib\net40\WebDriver.dll + + ..\packages\Selenium.WebDriver.2.52.0\lib\net40\WebDriver.dll True - - ..\packages\Selenium.Support.2.51.0\lib\net40\WebDriver.Support.dll + + ..\packages\Selenium.Support.2.52.0\lib\net40\WebDriver.Support.dll True diff --git a/src/ExpressiveAnnotations.MvcWebSample.UITests/packages.config b/src/ExpressiveAnnotations.MvcWebSample.UITests/packages.config index 5d9df7d..c872d7c 100644 --- a/src/ExpressiveAnnotations.MvcWebSample.UITests/packages.config +++ b/src/ExpressiveAnnotations.MvcWebSample.UITests/packages.config @@ -1,8 +1,8 @@  - - + + diff --git a/src/ExpressiveAnnotations.Tests/AttribsTest.cs b/src/ExpressiveAnnotations.Tests/AttribsTest.cs index 2e3c1fd..b19082f 100644 --- a/src/ExpressiveAnnotations.Tests/AttribsTest.cs +++ b/src/ExpressiveAnnotations.Tests/AttribsTest.cs @@ -159,7 +159,6 @@ public void verify_custom_error_message_after_validation() "field: #{Value1}#, expr: 1==1 | Value1: 0_{Value1}_, Internal.Internal.Value1: 2, _{Value2}_"); AssertErrorMessage( // all escaped "field: {{0}}, expr: {{1}} | Value1: {{Value1}}{{Value1:n}}, Internal.Internal.Value1: {{Internal.Internal.Value1}}, {{Internal.Internal.Value2:N}}", - "field: {0}, expr: {1} | Value1: {Value1}{Value1:n}, Internal.Internal.Value1: {Internal.Internal.Value1}, {Internal.Internal.Value2:N}", "field: {0}, expr: {1} | Value1: {Value1}{Value1:n}, Internal.Internal.Value1: {Internal.Internal.Value1}, {Internal.Internal.Value2:N}"); AssertErrorMessage( "field: {{{0}}}, expr: {{{1}}} | Value1: {{{Value1}}}{{{Value1:n}}}, Internal.Internal.Value1: {{{Internal.Internal.Value1}}}, {{{Internal.Internal.Value2:N}}}", @@ -167,7 +166,6 @@ public void verify_custom_error_message_after_validation() "field: {#{Value1}#}, expr: {1==1} | Value1: {0}{_{Value1}_}, Internal.Internal.Value1: {2}, {_{Value2}_}"); AssertErrorMessage( // all double-escaped "field: {{{{0}}}}, expr: {{{{1}}}} | Value1: {{{{Value1}}}}{{{{Value1:n}}}}, Internal.Internal.Value1: {{{{Internal.Internal.Value1}}}}, {{{{Internal.Internal.Value2:N}}}}", - "field: {{0}}, expr: {{1}} | Value1: {{Value1}}{{Value1:n}}, Internal.Internal.Value1: {{Internal.Internal.Value1}}, {{Internal.Internal.Value2:N}}", "field: {{0}}, expr: {{1}} | Value1: {{Value1}}{{Value1:n}}, Internal.Internal.Value1: {{Internal.Internal.Value1}}, {{Internal.Internal.Value2:N}}"); //string.Format("{{0", 1); -> {0 @@ -175,10 +173,15 @@ public void verify_custom_error_message_after_validation() AssertErrorMessage( "field: {{0, expr: {{1 | Value1: {{Value1{{Value1:n, Internal.Internal.Value1: Internal.Internal.Value1}}, Internal.Internal.Value2:N}}", - "field: {0, expr: {1 | Value1: {Value1{Value1:n, Internal.Internal.Value1: Internal.Internal.Value1}, Internal.Internal.Value2:N}", "field: {0, expr: {1 | Value1: {Value1{Value1:n, Internal.Internal.Value1: Internal.Internal.Value1}, Internal.Internal.Value2:N}"); } + [Fact] + public void custom_error_message_tolerates_null_value() + { + AssertErrorMessage("lang: '{Lang}'", "lang: ''"); + } + [Fact] public void verify_format_exceptions_from_incorrect_custom_format_specifiers() // custom specifiers handling should throw the same formatting error as framework implementation, when incorrect nesting is detected { @@ -205,13 +208,13 @@ public void verify_custom_error_message_after_validation() [Fact] public void verify_that_culture_change_affects_validation_message() { - AssertErrorMessage("{Lang:n}", "default", "default"); + AssertErrorMessage("{Lang:n}", "default"); // change culture var culture = Thread.CurrentThread.CurrentUICulture; Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("pl"); - AssertErrorMessage("{Lang:n}", "polski", "polski"); + AssertErrorMessage("{Lang:n}", "polski"); // restore culture Thread.CurrentThread.CurrentUICulture = culture; @@ -295,6 +298,11 @@ public void display_attribute_takes_precedence_over_displayname_attribute() Assert.Equal("assertthat only chosen", results.Single().ErrorMessage); } + private static void AssertErrorMessage(string input, string output) + { + AssertErrorMessage(input, output, output); + } + private static void AssertErrorMessage(string input, string assertThatOutput, string requiredIfOutput) { var assertThat = new AssertThatAttribute("1!=1"); @@ -303,7 +311,7 @@ private static void AssertErrorMessage(string input, string assertThatOutput, st var isValid = typeof (ExpressiveAttribute).GetMethod("IsValid", BindingFlags.NonPublic | BindingFlags.Instance); var context = new ValidationContext(new MsgModel { - Value1 = 0, + Value1 = 0, Internal = new MsgModel { Value1 = 1, diff --git a/src/ExpressiveAnnotations.Tests/ParserTest.cs b/src/ExpressiveAnnotations.Tests/ParserTest.cs index 56868a5..cf3a1d0 100644 --- a/src/ExpressiveAnnotations.Tests/ParserTest.cs +++ b/src/ExpressiveAnnotations.Tests/ParserTest.cs @@ -227,6 +227,9 @@ public void verify_logic_without_context() Assert.True(parser.Parse("'abc' == Trim(' abc ')").Invoke(null)); Assert.True(parser.Parse("Length(null) + Length('abc' + 'cde') >= Length(Trim(' abc def ')) - 2 - -1").Invoke(null)); + Assert.True(parser.Parse("0 == YesNo.Yes").Invoke(null)); + Assert.True(parser.Parse("YesNo.Yes == 0").Invoke(null)); + Assert.True(parser.Parse("YesNo.Yes != YesNo.No").Invoke(null)); } [Fact] @@ -350,6 +353,7 @@ public void verify_logic_with_context() var subModel = new Model(); var newModel = new Model {SubModel = subModel, SubModelObject = subModel}; Assert.True(parser.Parse("SubModel == SubModelObject").Invoke(newModel)); + Assert.True(parser.Parse("SubModelObject == SubModel").Invoke(newModel)); const string expression = @"Flag == !false @@ -590,7 +594,7 @@ public void verify_short_circuit_evaluation() public void verify_enumeration_ambiguity() { var parser = new Parser(); - + // ensure that this doesn't consider Dog and HotDog enums to be ambiguous Assert.True(parser.Parse("Dog.Collie == 0").Invoke(null)); var e = Assert.Throws(() => parser.Parse("Stability.High == 0").Invoke(null)); @@ -822,550 +826,562 @@ public void verify_toolchain_methods_logic() e.Message); } - [Fact] - public void verify_various_parsing_errors() + public static IEnumerable LogicalOperators + { + get { return new[] {"&&", "||"}.Select(x => new object[] {x}); } + } + + [Theory] + [MemberData("LogicalOperators")] + public void verify_type_mismatch_errors_for_logical_operators(string oper) { var parser = new Parser(); parser.RegisterMethods(); - parser.AddFunction("Max", (x, y) => Math.Max(x, y)); - var e = Assert.Throws(() => parser.Parse("1++ +1==2").Invoke(null)); + var e = Assert.Throws(() => parser.Parse($"true {oper} null").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 2: -... ++ +1==2 ... - ^--- Unexpected token: '++'.", + $@"Parse error on line 1, column 6: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Boolean' and 'null'.", e.Message); - e = Assert.Throws(() => parser.Parse("true # false").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Now() {oper} Today()").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... # false ... - ^--- Invalid token.", + $@"Parse error on line 1, column 7: +... {oper} Today() ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.DateTime' and 'System.DateTime'.", e.Message); - e = Assert.Throws(() => parser.Parse("'abc' - 'abc'").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"1 {oper} 2").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 7: -... - 'abc' ... - ^--- Operator '-' cannot be applied to operands of type 'System.String' and 'System.String'.", + $@"Parse error on line 1, column 3: +... {oper} 2 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Int32' and 'System.Int32'.", e.Message); - e = Assert.Throws(() => parser.Parse("0 + null").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"YesNo.Yes {oper} YesNo.No").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 3: -... + null ... - ^--- Operator '+' cannot be applied to operands of type 'System.Int32' and 'null'.", + $@"Parse error on line 1, column 11: +... {oper} YesNo.No ... + ^--- Operator '{oper}' cannot be applied to operands of type 'ExpressiveAnnotations.Tests.ParserTest+YesNo' and 'ExpressiveAnnotations.Tests.ParserTest+YesNo'.", e.Message); - e = Assert.Throws(() => parser.Parse("0 / null").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"null {oper} null").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 3: -... / null ... - ^--- Operator '/' cannot be applied to operands of type 'System.Int32' and 'null'.", + $@"Parse error on line 1, column 6: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'null' and 'null'.", e.Message); + } + + public static IEnumerable EqualityOperators + { + get { return new[] {"==", "!="}.Select(x => new object[] {x}); } + } - e = Assert.Throws(() => parser.Parse("true && null").Invoke(null)); + [Theory] + [MemberData("EqualityOperators")] + public void verify_type_mismatch_errors_for_equality_operators(string oper) + { + var parser = new Parser(); + parser.RegisterMethods(); + + var e = Assert.Throws(() => parser.Parse($"0 {oper} '0'").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... && null ... - ^--- Operator '&&' cannot be applied to operands of type 'System.Boolean' and 'null'.", + $@"Parse error on line 1, column 3: +... {oper} '0' ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Int32' and 'System.String'.", e.Message); - e = Assert.Throws(() => parser.Parse("true || null").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"0.1 {oper} '0'").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... || null ... - ^--- Operator '||' cannot be applied to operands of type 'System.Boolean' and 'null'.", + $@"Parse error on line 1, column 5: +... {oper} '0' ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Double' and 'System.String'.", e.Message); - e = Assert.Throws(() => parser.Parse("!null").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Date {oper} 0").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 1: -... !null ... - ^--- Operator '!' cannot be applied to operand of type 'null'.", + $@"Parse error on line 1, column 6: +... {oper} 0 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.DateTime' and 'System.Int32'.", e.Message); - e = Assert.Throws(() => parser.Parse("'abc' * 'abc'").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Date {oper} 'asd'").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 7: -... * 'abc' ... - ^--- Operator '*' cannot be applied to operands of type 'System.String' and 'System.String'.", + $@"Parse error on line 1, column 6: +... {oper} 'asd' ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.DateTime' and 'System.String'.", e.Message); - e = Assert.Throws(() => parser.Parse("1 + 2 + 'abc' - 'abc' > 0").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"NDate {oper} 'asd'").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 15: -... - 'abc' > 0 ... - ^--- Operator '-' cannot be applied to operands of type 'System.String' and 'System.String'.", + $@"Parse error on line 1, column 7: +... {oper} 'asd' ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.String'.", e.Message); - e = Assert.Throws(() => parser.Parse( - @"1 - 2 - - (6 / ((2*'1.5' - 1) + 1)) * -2 - + 1/2/1 == 3.50").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Lexer {oper} Parser").Invoke(new Bag { Lexer = new Lexer(), Parser = new Parser() })); Assert.Equal( - @"Parse error on line 2, column 15: -... *'1.5' - 1) + 1)) * -2 ... - ^--- Operator '*' cannot be applied to operands of type 'System.Int32' and 'System.String'.", + $@"Parse error on line 1, column 7: +... {oper} Parser ... + ^--- Operator '{oper}' cannot be applied to operands of type 'ExpressiveAnnotations.Analysis.Lexer' and 'ExpressiveAnnotations.Analysis.Parser'.", e.Message); - e = Assert.Throws(() => parser.Parse( - @"1 - 2 - - 6 - + 1/x[0]/1 == 3.50").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"null {oper} 0").Invoke(new Model())); Assert.Equal( - @"Parse error on line 3, column 9: -... x[0]/1 == 3.50 ... - ^--- Only public properties, constants and enums are accepted. Identifier 'x[0]' not known.", + $@"Parse error on line 1, column 6: +... {oper} 0 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'null' and 'System.Int32'.", e.Message); - e = Assert.Throws(() => parser.Parse("WriteLine('hello')").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Cash {oper} null").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 1: -... WriteLine('hello') ... - ^--- Function 'WriteLine' not known.", + $@"Parse error on line 1, column 6: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Decimal' and 'null'.", e.Message); - e = Assert.Throws(() => parser.Parse("1 2").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Utility.Stability.High {oper} YesNo.Yes").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 3: -... 2 ... - ^--- Unexpected token: '2'.", + $@"Parse error on line 1, column 24: +... {oper} YesNo.Yes ... + ^--- Operator '{oper}' cannot be applied to operands of type 'ExpressiveAnnotations.Tests.Utility+Stability' and 'ExpressiveAnnotations.Tests.ParserTest+YesNo'.", e.Message); + } - e = Assert.Throws(() => parser.Parse("(").Invoke(null)); + public static IEnumerable InequalityOperators + { + get { return new[] {">", ">=", "<", "<="}.Select(x => new object[] {x}); } + } + + [Theory] + [MemberData("InequalityOperators")] + public void verify_type_mismatch_errors_for_inequality_operators(string oper) + { + var parser = new Parser(); + parser.RegisterMethods(); + + var e = Assert.Throws(() => parser.Parse($"'a' {oper} 'b'").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 2: -... ... - ^--- Expected ""null"", int, float, bool, string or func. Unexpected end of expression.", + $@"Parse error on line 1, column 5: +... {oper} 'b' ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.String' and 'System.String'.", e.Message); - e = Assert.Throws(() => parser.Parse("(1+1").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"'asd' {oper} null").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 5: -... ... - ^--- Expected closing bracket. Unexpected end of expression.", + $@"Parse error on line 1, column 7: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.String' and 'null'.", e.Message); - e = Assert.Throws(() => parser.Parse("()").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Date {oper} 0").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 2: -... ) ... - ^--- Expected ""null"", int, float, bool, string or func. Unexpected token: ')'.", + $@"Parse error on line 1, column 6: +... {oper} 0 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.DateTime' and 'System.Int32'.", e.Message); - e = Assert.Throws(() => parser.Parse("Max(").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Date {oper} null").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 5: -... ... - ^--- Expected ""null"", int, float, bool, string or func. Unexpected end of expression.", + $@"Parse error on line 1, column 6: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.DateTime' and 'null'.", e.Message); - e = Assert.Throws(() => parser.Parse("Max(1 2").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Date {oper} SubModelObject").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 7: -... 2 ... - ^--- Expected comma or closing bracket. Unexpected token: '2'.", + $@"Parse error on line 1, column 6: +... {oper} SubModelObject ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.DateTime' and 'System.Object'.", e.Message); - e = Assert.Throws(() => parser.Parse("Max(1.1)").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"NDate {oper} SubModelObject").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 1: -... Max(1.1) ... - ^--- Function 'Max' accepting 1 argument not found.", + $@"Parse error on line 1, column 7: +... {oper} SubModelObject ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.Object'.", e.Message); - e = Assert.Throws(() => parser.Parse("Max(1.1, 1.2, 'a')").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"NDate {oper} null").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 1: -... Max(1.1, 1.2, 'a') ... - ^--- Function 'Max' accepting 3 arguments not found.", + $@"Parse error on line 1, column 7: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'null'.", e.Message); - e = Assert.Throws(() => parser.Parse( - @"Max(1, - Max(1, 'a')) == 1.1").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"SubModelObject {oper} SubModelObject").Invoke(new Model())); Assert.Equal( - @"Parse error on line 2, column 14: -... 'a')) == 1.1 ... - ^--- Function 'Max' 2nd argument implicit conversion from 'System.String' to expected 'System.Int32' failed.", + $@"Parse error on line 1, column 16: +... {oper} SubModelObject ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Object' and 'System.Object'.", e.Message); - e = Assert.Throws(() => parser.Parse("Now() && Today()").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"null {oper} null").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 7: -... && Today() ... - ^--- Operator '&&' cannot be applied to operands of type 'System.DateTime' and 'System.DateTime'.", + $@"Parse error on line 1, column 6: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'null' and 'null'.", e.Message); - e = Assert.Throws(() => parser.Parse("1 || 2").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"null {oper} 0").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 3: -... || 2 ... - ^--- Operator '||' cannot be applied to operands of type 'System.Int32' and 'System.Int32'.", + $@"Parse error on line 1, column 6: +... {oper} 0 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'null' and 'System.Int32'.", e.Message); - e = Assert.Throws(() => parser.Parse("'a' >= 'b'").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"0 {oper} SubModelObject").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 5: -... >= 'b' ... - ^--- Operator '>=' cannot be applied to operands of type 'System.String' and 'System.String'.", + $@"Parse error on line 1, column 3: +... {oper} SubModelObject ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Int32' and 'System.Object'.", e.Message); - e = Assert.Throws(() => parser.Parse("!'a'").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Date {oper} SubModelObject").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 1: -... !'a' ... - ^--- Operator '!' cannot be applied to operand of type 'System.String'.", + $@"Parse error on line 1, column 6: +... {oper} SubModelObject ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.DateTime' and 'System.Object'.", e.Message); - e = Assert.Throws(() => parser.Parse("!! Today()").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Span {oper} SubModelObject").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 2: -... ! Today() ... - ^--- Operator '!' cannot be applied to operand of type 'System.DateTime'.", + $@"Parse error on line 1, column 6: +... {oper} SubModelObject ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.TimeSpan' and 'System.Object'.", e.Message); - e = Assert.Throws(() => parser.Parse("0 == '0'").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"Utility.Stability.High {oper} YesNo.Yes").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 3: -... == '0' ... - ^--- Operator '==' cannot be applied to operands of type 'System.Int32' and 'System.String'.", + $@"Parse error on line 1, column 24: +... {oper} YesNo.Yes ... + ^--- Operator '{oper}' cannot be applied to operands of type 'ExpressiveAnnotations.Tests.Utility+Stability' and 'ExpressiveAnnotations.Tests.ParserTest+YesNo'.", e.Message); + } + + public static IEnumerable AddSubOperators + { + get { return new[] {"+", "-"}.Select(x => new object[] {x}); } + } + + [Theory] + [MemberData("AddSubOperators")] + public void verify_type_mismatch_errors_for_addition_and_subtraction_operators(string oper) + { + var parser = new Parser(); + parser.RegisterMethods(); - e = Assert.Throws(() => parser.Parse("0.1 != '0'").Invoke(null)); + var e = Assert.Throws(() => parser.Parse($"0 {oper} null").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 5: -... != '0' ... - ^--- Operator '!=' cannot be applied to operands of type 'System.Double' and 'System.String'.", + $@"Parse error on line 1, column 3: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Int32' and 'null'.", e.Message); - e = Assert.Throws(() => parser.Parse("'asd' > null").Invoke(null)); + e = Assert.Throws(() => parser.Parse($"null {oper} null").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 7: -... > null ... - ^--- Operator '>' cannot be applied to operands of type 'System.String' and 'null'.", + $@"Parse error on line 1, column 6: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'null' and 'null'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Date + Date").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse($"Date {oper} 0").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 6: -... + Date ... - ^--- Operator '+' cannot be applied to operands of type 'System.DateTime' and 'System.DateTime'.", + $@"Parse error on line 1, column 6: +... {oper} 0 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.DateTime' and 'System.Int32'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("NDate + NDate").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse($"NDate {oper} 0").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 7: -... + NDate ... - ^--- Operator '+' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.Nullable`1[System.DateTime]'.", + $@"Parse error on line 1, column 7: +... {oper} 0 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.Int32'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Date + NDate").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse($"Span {oper} 0").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 6: -... + NDate ... - ^--- Operator '+' cannot be applied to operands of type 'System.DateTime' and 'System.Nullable`1[System.DateTime]'.", + $@"Parse error on line 1, column 6: +... {oper} 0 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.TimeSpan' and 'System.Int32'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Date == 0").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse($"NSpan {oper} 0").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 6: -... == 0 ... - ^--- Operator '==' cannot be applied to operands of type 'System.DateTime' and 'System.Int32'.", + $@"Parse error on line 1, column 7: +... {oper} 0 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Nullable`1[System.TimeSpan]' and 'System.Int32'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Date != 'asd'").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse($"0 {oper} SubModelObject").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 6: -... != 'asd' ... - ^--- Operator '!=' cannot be applied to operands of type 'System.DateTime' and 'System.String'.", + $@"Parse error on line 1, column 3: +... {oper} SubModelObject ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Int32' and 'System.Object'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Date > 0").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse($"Date {oper} SubModelObject").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 6: -... > 0 ... - ^--- Operator '>' cannot be applied to operands of type 'System.DateTime' and 'System.Int32'.", + $@"Parse error on line 1, column 6: +... {oper} SubModelObject ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.DateTime' and 'System.Object'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Date > null").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse($"Span {oper} SubModelObject").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 6: -... > null ... - ^--- Operator '>' cannot be applied to operands of type 'System.DateTime' and 'null'.", + $@"Parse error on line 1, column 6: +... {oper} SubModelObject ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.TimeSpan' and 'System.Object'.", e.Message); + } - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Date > SubModelObject").Invoke(model); - }); + public static IEnumerable MulDivOperators + { + get { return new[] {"*", "/"}.Select(x => new object[] {x}); } + } + + [Theory] + [MemberData("MulDivOperators")] + public void verify_type_mismatch_errors_for_multiplication_and_division_operators(string oper) + { + var parser = new Parser(); + parser.RegisterMethods(); + + var e = Assert.Throws(() => parser.Parse($"0 {oper} null").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... > SubModelObject ... - ^--- Operator '>' cannot be applied to operands of type 'System.DateTime' and 'System.Object'.", + $@"Parse error on line 1, column 3: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Int32' and 'null'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("NDate > SubModelObject").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse($"'abc' {oper} 'abc'").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 7: -... > SubModelObject ... - ^--- Operator '>' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.Object'.", + $@"Parse error on line 1, column 7: +... {oper} 'abc' ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.String' and 'System.String'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("NDate > null").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse( + $@"1 - 2 + - (6 / ((2{oper}'1.5' - 1) + 1)) * -2 + + 1/2/1 == 3.50").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 7: -... > null ... - ^--- Operator '>' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'null'.", + $@"Parse error on line 2, column 15: +... {oper}'1.5' - 1) + 1)) * -2 ... + ^--- Operator '{oper}' cannot be applied to operands of type 'System.Int32' and 'System.String'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("NDate != 'asd'").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse($"null {oper} null").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 7: -... != 'asd' ... - ^--- Operator '!=' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.String'.", + $@"Parse error on line 1, column 6: +... {oper} null ... + ^--- Operator '{oper}' cannot be applied to operands of type 'null' and 'null'.", e.Message); + } - e = Assert.Throws(() => - { - var bag = new Bag {Lexer = new Lexer(), Parser = new Parser()}; - parser.Parse("Lexer != Parser").Invoke(bag); - }); + [Fact] + public void verify_type_mismatch_errors_for_addition_operator() + { + var parser = new Parser(); + parser.RegisterMethods(); + + var e = Assert.Throws(() => parser.Parse("Date + Date").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 7: -... != Parser ... - ^--- Operator '!=' cannot be applied to operands of type 'ExpressiveAnnotations.Analysis.Lexer' and 'ExpressiveAnnotations.Analysis.Parser'.", + @"Parse error on line 1, column 6: +... + Date ... + ^--- Operator '+' cannot be applied to operands of type 'System.DateTime' and 'System.DateTime'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("SubModelObject > SubModelObject").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("NDate + NDate").Invoke(new Model())); Assert.Equal( - @"Parse error on line 1, column 16: -... > SubModelObject ... - ^--- Operator '>' cannot be applied to operands of type 'System.Object' and 'System.Object'.", + @"Parse error on line 1, column 7: +... + NDate ... + ^--- Operator '+' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.Nullable`1[System.DateTime]'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("null > null").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("Date + NDate").Invoke(new Model())); Assert.Equal( @"Parse error on line 1, column 6: -... > null ... - ^--- Operator '>' cannot be applied to operands of type 'null' and 'null'.", +... + NDate ... + ^--- Operator '+' cannot be applied to operands of type 'System.DateTime' and 'System.Nullable`1[System.DateTime]'.", e.Message); + } - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("null + null").Invoke(model); - }); + [Fact] + public void verify_type_mismatch_errors_for_subtraction_operator() + { + var parser = new Parser(); + parser.RegisterMethods(); + + var e = Assert.Throws(() => parser.Parse("'abc' - 'abc'").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... + null ... - ^--- Operator '+' cannot be applied to operands of type 'null' and 'null'.", + @"Parse error on line 1, column 7: +... - 'abc' ... + ^--- Operator '-' cannot be applied to operands of type 'System.String' and 'System.String'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("null - null").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("1 + 2 + 'abc' - 'abc' > 0").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... - null ... - ^--- Operator '-' cannot be applied to operands of type 'null' and 'null'.", + @"Parse error on line 1, column 15: +... - 'abc' > 0 ... + ^--- Operator '-' cannot be applied to operands of type 'System.String' and 'System.String'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("'asd' - null").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("'asd' - null").Invoke(new Model())); Assert.Equal( @"Parse error on line 1, column 7: ... - null ... ^--- Operator '-' cannot be applied to operands of type 'System.String' and 'null'.", e.Message); + } - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("null / null").Invoke(model); - }); + [Fact] + public void verify_type_mismatch_errors_for_negation_operator() + { + var parser = new Parser(); + parser.RegisterMethods(); + + var e = Assert.Throws(() => parser.Parse("!null").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... / null ... - ^--- Operator '/' cannot be applied to operands of type 'null' and 'null'.", + @"Parse error on line 1, column 1: +... !null ... + ^--- Operator '!' cannot be applied to operand of type 'null'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("null && null").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("!'a'").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... && null ... - ^--- Operator '&&' cannot be applied to operands of type 'null' and 'null'.", + @"Parse error on line 1, column 1: +... !'a' ... + ^--- Operator '!' cannot be applied to operand of type 'System.String'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("null || null").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("!! Today()").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... || null ... - ^--- Operator '||' cannot be applied to operands of type 'null' and 'null'.", + @"Parse error on line 1, column 2: +... ! Today() ... + ^--- Operator '!' cannot be applied to operand of type 'System.DateTime'.", e.Message); + } - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("null == 0").Invoke(model); - }); + [Fact] + public void verify_various_parsing_errors() + { + var parser = new Parser(); + parser.RegisterMethods(); + parser.AddFunction("Max", (x, y) => Math.Max(x, y)); + + var e = Assert.Throws(() => parser.Parse("1++ +1==2").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... == 0 ... - ^--- Operator '==' cannot be applied to operands of type 'null' and 'System.Int32'.", + @"Parse error on line 1, column 2: +... ++ +1==2 ... + ^--- Unexpected token: '++'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("null < 0").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("true # false").Invoke(null)); Assert.Equal( @"Parse error on line 1, column 6: -... < 0 ... - ^--- Operator '<' cannot be applied to operands of type 'null' and 'System.Int32'.", +... # false ... + ^--- Invalid token.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Cash == null").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse( + @"1 - 2 + - 6 + + 1/x[0]/1 == 3.50").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... == null ... - ^--- Operator '==' cannot be applied to operands of type 'System.Decimal' and 'null'.", + @"Parse error on line 3, column 9: +... x[0]/1 == 3.50 ... + ^--- Only public properties, constants and enums are accepted. Identifier 'x[0]' not known.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Date + 0").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("WriteLine('hello')").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... + 0 ... - ^--- Operator '+' cannot be applied to operands of type 'System.DateTime' and 'System.Int32'.", + @"Parse error on line 1, column 1: +... WriteLine('hello') ... + ^--- Function 'WriteLine' not known.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("NDate - 0").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("1 2").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 7: -... - 0 ... - ^--- Operator '-' cannot be applied to operands of type 'System.Nullable`1[System.DateTime]' and 'System.Int32'.", + @"Parse error on line 1, column 3: +... 2 ... + ^--- Unexpected token: '2'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Span + 0").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("(").Invoke(null)); Assert.Equal( - @"Parse error on line 1, column 6: -... + 0 ... - ^--- Operator '+' cannot be applied to operands of type 'System.TimeSpan' and 'System.Int32'.", + @"Parse error on line 1, column 2: +... ... + ^--- Expected ""null"", int, float, bool, string or func. Unexpected end of expression.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("NSpan - 0").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("(1+1").Invoke(null)); + Assert.Equal( + @"Parse error on line 1, column 5: +... ... + ^--- Expected closing bracket. Unexpected end of expression.", + e.Message); + + e = Assert.Throws(() => parser.Parse("()").Invoke(null)); + Assert.Equal( + @"Parse error on line 1, column 2: +... ) ... + ^--- Expected ""null"", int, float, bool, string or func. Unexpected token: ')'.", + e.Message); + + e = Assert.Throws(() => parser.Parse("Max(").Invoke(null)); + Assert.Equal( + @"Parse error on line 1, column 5: +... ... + ^--- Expected ""null"", int, float, bool, string or func. Unexpected end of expression.", + e.Message); + + e = Assert.Throws(() => parser.Parse("Max(1 2").Invoke(null)); Assert.Equal( @"Parse error on line 1, column 7: -... - 0 ... - ^--- Operator '-' cannot be applied to operands of type 'System.Nullable`1[System.TimeSpan]' and 'System.Int32'.", +... 2 ... + ^--- Expected comma or closing bracket. Unexpected token: '2'.", e.Message); - e = Assert.Throws(() => - { - var model = new Model {Items = new List {new Model()}}; - parser.Parse("Items[0] != null").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("Max(1.1)").Invoke(null)); + Assert.Equal( + @"Parse error on line 1, column 1: +... Max(1.1) ... + ^--- Function 'Max' accepting 1 argument not found.", + e.Message); + + e = Assert.Throws(() => parser.Parse("Max(1.1, 1.2, 'a')").Invoke(null)); + Assert.Equal( + @"Parse error on line 1, column 1: +... Max(1.1, 1.2, 'a') ... + ^--- Function 'Max' accepting 3 arguments not found.", + e.Message); + + e = Assert.Throws(() => parser.Parse( + @"Max(1, + Max(1, 'a')) == 1.1").Invoke(null)); + Assert.Equal( + @"Parse error on line 2, column 14: +... 'a')) == 1.1 ... + ^--- Function 'Max' 2nd argument implicit conversion from 'System.String' to expected 'System.Int32' failed.", + e.Message); + + e = Assert.Throws(() => parser.Parse("Items[0] != null").Invoke(new Model {Items = new List {new Model()}})); Assert.Equal( @"Parse error on line 1, column 1: ... Items[0] != null ... ^--- Identifier 'Items' either does not represent an array type or does not declare indexer.", e.Message); - e = Assert.Throws(() => - { - var model = new Model(); - parser.Parse("Long(1)").Invoke(model); - }); + e = Assert.Throws(() => parser.Parse("Long(1)").Invoke(new Model())); Assert.Equal( @"Parse error on line 1, column 1: ... Long(1) ... ^--- Function 'Long' accepting 1 argument not found.", e.Message); - } + } [Fact] public void unicode_characters_are_supported() diff --git a/src/ExpressiveAnnotations/Analysis/Parser.cs b/src/ExpressiveAnnotations/Analysis/Parser.cs index 606336c..aca4cb5 100644 --- a/src/ExpressiveAnnotations/Analysis/Parser.cs +++ b/src/ExpressiveAnnotations/Analysis/Parser.cs @@ -360,6 +360,13 @@ private Expression ParseRelExp() var type2 = arg2.Type; Helper.MakeTypesCompatible(arg1, arg2, out arg1, out arg2); + if (arg1.Type != arg2.Type + && !arg1.IsNullLiteral() && !arg2.IsNullLiteral() + && !arg1.Type.IsObject() && !arg2.Type.IsObject()) + throw new ParseErrorException( + $"Operator '{oper.Value}' cannot be applied to operands of type '{type1}' and '{type2}'.", + oper.Location); + if (oper.Type == TokenType.EQ || oper.Type == TokenType.NEQ) { if (type1.IsNonNullableValueType() && arg2.IsNullLiteral()) @@ -370,13 +377,6 @@ private Expression ParseRelExp() throw new ParseErrorException( $"Operator '{oper.Value}' cannot be applied to operands of type 'null' and '{type2}'.", oper.Location); - - if (arg1.Type != arg2.Type - && !arg1.IsNullLiteral() && !arg2.IsNullLiteral() - && !arg1.Type.IsObject() && !arg2.Type.IsObject()) - throw new ParseErrorException( - $"Operator '{oper.Value}' cannot be applied to operands of type '{type1}' and '{type2}'.", - oper.Location); } else { diff --git a/src/ExpressiveAnnotations/Helper.cs b/src/ExpressiveAnnotations/Helper.cs index a9508bc..2e88663 100644 --- a/src/ExpressiveAnnotations/Helper.cs +++ b/src/ExpressiveAnnotations/Helper.cs @@ -39,6 +39,9 @@ public static void MakeTypesCompatible(Expression e1, Expression e2, out Express oute1 = e1; oute2 = e2; + if (oute1.Type.IsEnum && oute2.Type.IsEnum && oute1.Type.UnderlyingType() != oute2.Type.UnderlyingType()) + return; + // promote numeric values to double - do all computations with higher precision (to be compatible with JavaScript, e.g. notation 1/2, should give 0.5 double not 0 int) if (oute1.Type != typeof (double) && oute1.Type != typeof (double?) && oute1.Type.IsNumeric()) oute1 = oute1.Type.IsNullable()