diff --git a/README.md b/README.md index c97a7f1..4cd2218 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Here instead, we're saying "If condition is met, return some view. Otherwise, ad #####Grammar definition -Valid logical expressions handled by EA parser comply with syntax defined by the following grammar: +Valid expressions handled by EA parser comply with syntax defined by the following grammar: ``` exp => cond-exp diff --git a/build/test.ps1 b/build/test.ps1 index 7080267..c7c1e73 100644 --- a/build/test.ps1 +++ b/build/test.ps1 @@ -24,8 +24,9 @@ $testdlls = "`"`"$eatestdll`"`" `"`"$vatestdll`"`" `"`"$uitestdll`"`"" $webmvcbin = "$rootdir\src\ExpressiveAnnotations.MvcWebSample\bin" # collect JS tests -$maintest = "$rootdir\src\expressive.annotations.validate.test.js" -$formtest = "$rootdir\src\tests.html" +$maintest = "$rootdir\src\expressive.annotations.validate.test.js" +$formtest = "$rootdir\src\form.tests.harness.html" +$formtestnew = "$rootdir\src\form.tests.harness.latestdeps.html" # run tests and analyze code coverage & $opencover -register:user "-target:$xunit" "-targetargs:$testdlls -nologo -noshadow -appveyor" "-targetdir:$webmvcbin" "-filter:+[ExpressiveAnnotations(.MvcUnobtrusive)?]*" -output:csharp-coverage.xml -hideskipped:All -mergebyhash -returntargetcode @@ -37,7 +38,7 @@ if($LastExitCode -ne 0) { throw "C# tests failed" } -& $chutzpah /nologo /path $maintest /path $formtest /junit chutzpah-tests.xml /coverage /coverageIgnores "*test*, *jquery*" /coveragehtml javascript-coverage.htm /lcov javascript-coverage.lcov +& $chutzpah /nologo /path $maintest /path $formtest /path $formtestnew /junit chutzpah-tests.xml /coverage /coverageIgnores "*test*, *jquery*" /coveragehtml javascript-coverage.htm /lcov javascript-coverage.lcov if($LastExitCode -ne 0) { if($env:APPVEYOR -eq $true) { diff --git a/doc/api/api.chm b/doc/api/api.chm index f1a9100..ca92125 100644 Binary files a/doc/api/api.chm and b/doc/api/api.chm differ diff --git a/src/.nuget/packages.config b/src/.nuget/packages.config index 642de3c..421810a 100644 --- a/src/.nuget/packages.config +++ b/src/.nuget/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ExpressiveAnnotations.MvcUnobtrusive.Tests.csproj b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ExpressiveAnnotations.MvcUnobtrusive.Tests.csproj index 6c1866c..a9e5bc9 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ExpressiveAnnotations.MvcUnobtrusive.Tests.csproj +++ b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ExpressiveAnnotations.MvcUnobtrusive.Tests.csproj @@ -39,13 +39,17 @@ 4 + + ..\packages\Castle.Core.3.3.3\lib\net45\Castle.Core.dll + True + ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll True - - ..\packages\Moq.4.2.1409.1722\lib\net40\Moq.dll + + ..\packages\Moq.4.5.10\lib\net45\Moq.dll True @@ -127,7 +131,9 @@ - + + Designer + diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/packages.config b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/packages.config index 85d12c9..4f1a5c6 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/packages.config +++ b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/packages.config @@ -1,10 +1,11 @@  + - + diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive/Properties/AssemblyInfo.cs b/src/ExpressiveAnnotations.MvcUnobtrusive/Properties/AssemblyInfo.cs index 385add6..69a1e90 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive/Properties/AssemblyInfo.cs +++ b/src/ExpressiveAnnotations.MvcUnobtrusive/Properties/AssemblyInfo.cs @@ -37,5 +37,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.4.7.0")] -[assembly: AssemblyFileVersion("2.4.7.0")] +[assembly: AssemblyVersion("2.4.8.0")] +[assembly: AssemblyFileVersion("2.4.8.0")] diff --git a/src/ExpressiveAnnotations.Tests/ExpressiveAnnotations.Tests.csproj b/src/ExpressiveAnnotations.Tests/ExpressiveAnnotations.Tests.csproj index b9a48af..2511e52 100644 --- a/src/ExpressiveAnnotations.Tests/ExpressiveAnnotations.Tests.csproj +++ b/src/ExpressiveAnnotations.Tests/ExpressiveAnnotations.Tests.csproj @@ -40,8 +40,12 @@ 4 - - ..\packages\Moq.4.2.1409.1722\lib\net40\Moq.dll + + ..\packages\Castle.Core.3.3.3\lib\net45\Castle.Core.dll + True + + + ..\packages\Moq.4.5.10\lib\net45\Moq.dll True @@ -93,7 +97,9 @@ - + + Designer + diff --git a/src/ExpressiveAnnotations.Tests/ParserTest.cs b/src/ExpressiveAnnotations.Tests/ParserTest.cs index 272c2ec..25586a5 100644 --- a/src/ExpressiveAnnotations.Tests/ParserTest.cs +++ b/src/ExpressiveAnnotations.Tests/ParserTest.cs @@ -1522,6 +1522,26 @@ public void verify_various_parsing_errors() e = Assert.Throws(() => parser.Parse("GetObject()[0]").Invoke(null)); Assert.Equal("Indexing operation not supported. Subscript operator can be applied to either an array or a type declaring indexer.", e.Error); Assert.Equal(new Location(1, 12), e.Location, new LocationComparer()); + + e = Assert.Throws(() => parser.Parse("SubModel.Unknown1.Unknown2 == 1").Invoke(new Model())); + Assert.Equal("Only public properties, constants and enums are accepted. Identifier 'Unknown1' not known.", e.Error); + Assert.Equal(new Location(1, 10), e.Location, new LocationComparer()); + + e = Assert.Throws(() => parser.Parse("GetObject().Unknown1.Unknown2 == 1").Invoke(new Model())); + Assert.Equal("Only public properties, constants and enums are accepted. Identifier 'Unknown1' not known.", e.Error); + Assert.Equal(new Location(1, 13), e.Location, new LocationComparer()); + + e = Assert.Throws(() => parser.Parse("[SubModel][0].Unknown1.Unknown2 == 1").Invoke(new Model())); + Assert.Equal("Only public properties, constants and enums are accepted. Identifier 'Unknown1' not known.", e.Error); + Assert.Equal(new Location(1, 15), e.Location, new LocationComparer()); + + e = Assert.Throws(() => parser.Parse("[1").Invoke(new Model())); + Assert.Equal("Expected comma or closing bracket. Unexpected end of expression.", e.Error); + Assert.Equal(new Location(1, 3), e.Location, new LocationComparer()); + + e = Assert.Throws(() => parser.Parse("[1)").Invoke(new Model())); + Assert.Equal("Expected comma or closing bracket. Unexpected token: ')'.", e.Error); + Assert.Equal(new Location(1, 3), e.Location, new LocationComparer()); } [Fact] diff --git a/src/ExpressiveAnnotations.Tests/UtilsTest.cs b/src/ExpressiveAnnotations.Tests/UtilsTest.cs index 807db91..0d9c8f9 100644 --- a/src/ExpressiveAnnotations.Tests/UtilsTest.cs +++ b/src/ExpressiveAnnotations.Tests/UtilsTest.cs @@ -210,6 +210,13 @@ public void throw_when_non_positive_parameters_are_provided_for_error_message_co Assert.Equal("Column number should be positive.\r\nParameter name: column", e.Message); } + [Fact] + public void print_token_for_debug_purposes() + { + var token = new Token(TokenType.FLOAT, 1.0, "1.0", new Location(1, 2)); + Assert.Equal(@"""1.0"" FLOAT (1, 2)", token.ToString()); + } + private class Model { [Display(Name = "Value_1")] diff --git a/src/ExpressiveAnnotations.Tests/packages.config b/src/ExpressiveAnnotations.Tests/packages.config index 9ceb64e..d16ec29 100644 --- a/src/ExpressiveAnnotations.Tests/packages.config +++ b/src/ExpressiveAnnotations.Tests/packages.config @@ -1,6 +1,7 @@  - + + diff --git a/src/ExpressiveAnnotations.sln b/src/ExpressiveAnnotations.sln index e02a2e1..3066cf5 100644 --- a/src/ExpressiveAnnotations.sln +++ b/src/ExpressiveAnnotations.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExpressiveAnnotations", "ExpressiveAnnotations\ExpressiveAnnotations.csproj", "{70DD327B-F37C-4291-9388-212AA541E970}" EndProject @@ -18,7 +18,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{5908 expressive.annotations.validate.js = expressive.annotations.validate.js expressive.annotations.validate.min.js = expressive.annotations.validate.min.js expressive.annotations.validate.test.js = expressive.annotations.validate.test.js - tests.html = tests.html + form.tests.harness.html = form.tests.harness.html + form.tests.harness.latestdeps.html = form.tests.harness.latestdeps.html + form.tests.js = form.tests.js EndProjectSection ProjectSection(FolderStartupServices) = postProject {82A7F48D-3B50-4B1E-B82E-3ADA8210C358} = {82A7F48D-3B50-4B1E-B82E-3ADA8210C358} diff --git a/src/ExpressiveAnnotations/Analysis/Parser.cs b/src/ExpressiveAnnotations/Analysis/Parser.cs index 36a20fa..0d2fbdb 100644 --- a/src/ExpressiveAnnotations/Analysis/Parser.cs +++ b/src/ExpressiveAnnotations/Analysis/Parser.cs @@ -371,7 +371,7 @@ private Expression ParseEqualityExp() var type1 = arg1.Type; var type2 = arg2.Type; - Helper.MakeTypesCompatible(arg1, arg2, out arg1, out arg2, oper.Type); + TypeAdapter.MakeTypesCompatible(arg1, arg2, out arg1, out arg2, oper.Type); Wall.Eq(arg1, arg2, type1, type2, oper); switch (oper.Type) @@ -399,7 +399,7 @@ private Expression ParseRelationalExp() var type1 = arg1.Type; var type2 = arg2.Type; - Helper.MakeTypesCompatible(arg1, arg2, out arg1, out arg2, oper.Type); + TypeAdapter.MakeTypesCompatible(arg1, arg2, out arg1, out arg2, oper.Type); Wall.Rel(arg1, arg2, type1, type2, oper); switch (oper.Type) @@ -459,7 +459,7 @@ private Expression ParseAdditiveExp() var type1 = arg1.Type; var type2 = arg2.Type; - Helper.MakeTypesCompatible(arg1, arg2, out arg1, out arg2, oper.Type); + TypeAdapter.MakeTypesCompatible(arg1, arg2, out arg1, out arg2, oper.Type); Wall.Add(arg1, arg2, type1, type2, oper); switch (oper.Type) @@ -491,7 +491,7 @@ private Expression ParseMultiplicativeExp() var arg2 = ParseUnaryExp(); Wall.Mul(arg1, arg2, oper); - Helper.MakeTypesCompatible(arg1, arg2, out arg1, out arg2, oper.Type); + TypeAdapter.MakeTypesCompatible(arg1, arg2, out arg1, out arg2, oper.Type); switch (oper.Type) { diff --git a/src/ExpressiveAnnotations/Analysis/TypeAdapter.cs b/src/ExpressiveAnnotations/Analysis/TypeAdapter.cs new file mode 100644 index 0000000..a957d04 --- /dev/null +++ b/src/ExpressiveAnnotations/Analysis/TypeAdapter.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using System.Linq.Expressions; + +namespace ExpressiveAnnotations.Analysis +{ + internal static class TypeAdapter + { + public static void MakeTypesCompatible(Expression e1, Expression e2, out Expression oute1, out Expression oute2, TokenType operation) + { + Debug.Assert(e1 != null); + Debug.Assert(e2 != null); + + oute1 = e1; + oute2 = e2; + + if (oute1.Type.IsEnum && oute2.Type.IsEnum + && oute1.Type.UnderlyingType() != oute2.Type.UnderlyingType()) // various enum types + return; + + if (operation != TokenType.DIV // do not promote integral numeric values to double - exception for division operation, e.g. 1/2 should evaluate to 0.5 double like in JS + && !oute1.Type.IsEnum && !oute2.Type.IsEnum + && oute1.Type.IsNumeric() && oute2.Type.IsNumeric() + && !oute1.Type.IsFloatingPointNumeric() && !oute2.Type.IsFloatingPointNumeric()) + return; + + // promote numeric values to double - do computations with higher precision (to be compatible with JavaScript, e.g. 1/2 should evaluate to 0.5 double not 0 int) + if (oute1.Type != typeof (double) && oute1.Type != typeof (double?) && oute1.Type.IsNumeric()) + oute1 = oute1.Type.IsNullable() + ? Expression.Convert(oute1, typeof (double?)) + : Expression.Convert(oute1, typeof (double)); + if (oute2.Type != typeof (double) && oute2.Type != typeof (double?) && oute2.Type.IsNumeric()) + oute2 = oute2.Type.IsNullable() + ? Expression.Convert(oute2, typeof (double?)) + : 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.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); + } + + // make DateTime and TimeSpan compatible (also do not care when first argument is TimeSpan and second DateTime because it is not allowed) + if (oute1.Type.IsDateTime() && oute2.Type.IsTimeSpan()) + { + if (oute1.Type.IsNullable() && !oute2.Type.IsNullable()) + oute2 = Expression.Convert(oute2, typeof (TimeSpan?)); + else if (!oute1.Type.IsNullable() && oute2.Type.IsNullable()) + oute1 = Expression.Convert(oute1, typeof (DateTime?)); + } + } + } +} diff --git a/src/ExpressiveAnnotations/ExpressiveAnnotations.csproj b/src/ExpressiveAnnotations/ExpressiveAnnotations.csproj index c3babd9..2ca0925 100644 --- a/src/ExpressiveAnnotations/ExpressiveAnnotations.csproj +++ b/src/ExpressiveAnnotations/ExpressiveAnnotations.csproj @@ -61,6 +61,7 @@ + diff --git a/src/ExpressiveAnnotations/Helper.cs b/src/ExpressiveAnnotations/Helper.cs index 3e74252..f5c25bb 100644 --- a/src/ExpressiveAnnotations/Helper.cs +++ b/src/ExpressiveAnnotations/Helper.cs @@ -32,53 +32,6 @@ public AssemblyTypeProvider(Assembly assembly) internal static class Helper { - public static void MakeTypesCompatible(Expression e1, Expression e2, out Expression oute1, out Expression oute2, TokenType operation) - { - Debug.Assert(e1 != null); - Debug.Assert(e2 != null); - - oute1 = e1; - oute2 = e2; - - if (oute1.Type.IsEnum && oute2.Type.IsEnum - && oute1.Type.UnderlyingType() != oute2.Type.UnderlyingType()) // various enum types - return; - - if (operation != TokenType.DIV // do not promote integral numeric values to double - exception for division operation, e.g. 1/2 should evaluate to 0.5 double like in JS - && !oute1.Type.IsEnum && !oute2.Type.IsEnum - && oute1.Type.IsNumeric() && oute2.Type.IsNumeric() - && !oute1.Type.IsFloatingPointNumeric() && !oute2.Type.IsFloatingPointNumeric()) - return; - - // promote numeric values to double - do computations with higher precision (to be compatible with JavaScript, e.g. 1/2 should evaluate to 0.5 double not 0 int) - if (oute1.Type != typeof (double) && oute1.Type != typeof (double?) && oute1.Type.IsNumeric()) - oute1 = oute1.Type.IsNullable() - ? Expression.Convert(oute1, typeof (double?)) - : Expression.Convert(oute1, typeof (double)); - if (oute2.Type != typeof (double) && oute2.Type != typeof (double?) && oute2.Type.IsNumeric()) - oute2 = oute2.Type.IsNullable() - ? Expression.Convert(oute2, typeof (double?)) - : 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.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); - } - - // make DateTime and TimeSpan compatible (also do not care when first argument is TimeSpan and second DateTime because it is not allowed) - if (oute1.Type.IsDateTime() && oute2.Type.IsTimeSpan()) - { - if (oute1.Type.IsNullable() && !oute2.Type.IsNullable()) - oute2 = Expression.Convert(oute2, typeof (TimeSpan?)); - else if (!oute1.Type.IsNullable() && oute2.Type.IsNullable()) - oute1 = Expression.Convert(oute1, typeof (DateTime?)); - } - } - public static object ExtractValue(object source, string property) { Debug.Assert(source != null); diff --git a/src/ExpressiveAnnotations/Properties/AssemblyInfo.cs b/src/ExpressiveAnnotations/Properties/AssemblyInfo.cs index 6f3cd44..166e6a8 100644 --- a/src/ExpressiveAnnotations/Properties/AssemblyInfo.cs +++ b/src/ExpressiveAnnotations/Properties/AssemblyInfo.cs @@ -42,5 +42,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.5.1.0")] -[assembly: AssemblyFileVersion("2.5.1.0")] +[assembly: AssemblyVersion("2.6.0.0")] +[assembly: AssemblyFileVersion("2.6.0.0")] diff --git a/src/tests.html b/src/form.tests.harness.html similarity index 55% rename from src/tests.html rename to src/form.tests.harness.html index 63106c5..f837fbc 100644 --- a/src/tests.html +++ b/src/form.tests.harness.html @@ -35,48 +35,12 @@ - + - - - - - + - \ No newline at end of file + diff --git a/src/form.tests.harness.latestdeps.html b/src/form.tests.harness.latestdeps.html new file mode 100644 index 0000000..f552e8c --- /dev/null +++ b/src/form.tests.harness.latestdeps.html @@ -0,0 +1,46 @@ + + + + + expressive.annotations.validate.js unit tests + + + + +
+ +
+
+ + + +
+
+ + + + + + + + + + + + diff --git a/src/form.tests.js b/src/form.tests.js new file mode 100644 index 0000000..a0acd4d --- /dev/null +++ b/src/form.tests.js @@ -0,0 +1,32 @@ +(function($, qunit, ea) { + qunit.module("html based full computation flow"); + + qunit.test("verify_sample_form_validation", function(assert) { // relies on form defined in test harness file + + ea.addMethod('Whoami', function() { + return 'root'; + }); + ea.addMethod('ArrayContains', function(value, array) { + return $.inArray(value, array) >= 0; + }); + ea.addValueParser('ArrayParser', function(value, field) { + assert.equal(field, 'ContactDetails.Letters'); + + var array = value.split(','); + return array.length === 0 ? null : array; + }); + + var validator = $('#basic_test_form').validate(); + + var element = $('#basic_test_form').find('[name="ContactDetails.Email"]'); + var result = element.valid(); // trigger wait for result (all is synchronous) + assert.ok(!result); + assert.equal(validator.errorList[0].message, 'Provided email: ea@home.com (yes ea@home.com) cannot be accepted.'); + + element = $('#basic_test_form').find('[name="ContactDetails.PoliticalStability"]'); + result = element.valid(); + assert.ok(!result); + assert.equal(validator.errorList[0].message, '{0}{1}'); + }); + +}($, QUnit, window.ea));