diff --git a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs index 3ecccbd2..dc55bc35 100644 --- a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs @@ -25,25 +25,8 @@ internal class KeywordsHelper : IKeywordsHelper private readonly ParsingConfig _config; - // Keywords are IgnoreCase - private readonly Dictionary> _keywordMapping = new(StringComparer.OrdinalIgnoreCase) - { - { "true", Expression.Constant(true) }, - { "false", Expression.Constant(false) }, - { "null", Expression.Constant(null) }, - - { SYMBOL_IT, SYMBOL_IT }, - { SYMBOL_PARENT, SYMBOL_PARENT }, - { SYMBOL_ROOT, SYMBOL_ROOT }, - - { FUNCTION_IIF, FUNCTION_IIF }, - { FUNCTION_ISNULL, FUNCTION_ISNULL }, - { FUNCTION_NEW, FUNCTION_NEW }, - { FUNCTION_NULLPROPAGATION, FUNCTION_NULLPROPAGATION }, - { FUNCTION_IS, FUNCTION_IS }, - { FUNCTION_AS, FUNCTION_AS }, - { FUNCTION_CAST, FUNCTION_CAST } - }; + // Keywords compare case depends on the value from ParsingConfig.IsCaseSensitive + private readonly Dictionary> _keywordMapping; // PreDefined Types are not IgnoreCase private static readonly Dictionary PreDefinedTypeMapping = new(); @@ -64,6 +47,25 @@ public KeywordsHelper(ParsingConfig config) { _config = Check.NotNull(config); + _keywordMapping = new(config.IsCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase) + { + { "true", Expression.Constant(true) }, + { "false", Expression.Constant(false) }, + { "null", Expression.Constant(null) }, + + { SYMBOL_IT, SYMBOL_IT }, + { SYMBOL_PARENT, SYMBOL_PARENT }, + { SYMBOL_ROOT, SYMBOL_ROOT }, + + { FUNCTION_IIF, FUNCTION_IIF }, + { FUNCTION_ISNULL, FUNCTION_ISNULL }, + { FUNCTION_NEW, FUNCTION_NEW }, + { FUNCTION_NULLPROPAGATION, FUNCTION_NULLPROPAGATION }, + { FUNCTION_IS, FUNCTION_IS }, + { FUNCTION_AS, FUNCTION_AS }, + { FUNCTION_CAST, FUNCTION_CAST } + }; + if (config.AreContextKeywordsEnabled) { _keywordMapping.Add(KEYWORD_IT, KEYWORD_IT); diff --git a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs index 815700ce..158b0768 100644 --- a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs +++ b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs @@ -12,35 +12,14 @@ public class TextParser private const char DefaultNumberDecimalSeparator = '.'; private static readonly char[] EscapeCharacters = { '\\', 'a', 'b', 'f', 'n', 'r', 't', 'v' }; - // These aliases are supposed to simply the where clause and make it more human readable - private static readonly Dictionary PredefinedOperatorAliases = new(StringComparer.OrdinalIgnoreCase) - { - { "eq", TokenId.Equal }, - { "equal", TokenId.Equal }, - { "ne", TokenId.ExclamationEqual }, - { "notequal", TokenId.ExclamationEqual }, - { "neq", TokenId.ExclamationEqual }, - { "lt", TokenId.LessThan }, - { "LessThan", TokenId.LessThan }, - { "le", TokenId.LessThanEqual }, - { "LessThanEqual", TokenId.LessThanEqual }, - { "gt", TokenId.GreaterThan }, - { "GreaterThan", TokenId.GreaterThan }, - { "ge", TokenId.GreaterThanEqual }, - { "GreaterThanEqual", TokenId.GreaterThanEqual }, - { "and", TokenId.DoubleAmpersand }, - { "AndAlso", TokenId.DoubleAmpersand }, - { "or", TokenId.DoubleBar }, - { "OrElse", TokenId.DoubleBar }, - { "not", TokenId.Exclamation }, - { "mod", TokenId.Percent } - }; - private readonly char _numberDecimalSeparator; private readonly string _text; private readonly int _textLen; private readonly ParsingConfig _parsingConfig; + // These aliases simplify the "Where"-clause and make it more human-readable. + private readonly Dictionary _predefinedOperatorAliases; + private int _textPos; private char _ch; @@ -58,6 +37,29 @@ public TextParser(ParsingConfig config, string text) { _parsingConfig = config; + _predefinedOperatorAliases = new(config.IsCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase) + { + { "eq", TokenId.Equal }, + { "equal", TokenId.Equal }, + { "ne", TokenId.ExclamationEqual }, + { "notequal", TokenId.ExclamationEqual }, + { "neq", TokenId.ExclamationEqual }, + { "lt", TokenId.LessThan }, + { "LessThan", TokenId.LessThan }, + { "le", TokenId.LessThanEqual }, + { "LessThanEqual", TokenId.LessThanEqual }, + { "gt", TokenId.GreaterThan }, + { "GreaterThan", TokenId.GreaterThan }, + { "ge", TokenId.GreaterThanEqual }, + { "GreaterThanEqual", TokenId.GreaterThanEqual }, + { "and", TokenId.DoubleAmpersand }, + { "AndAlso", TokenId.DoubleAmpersand }, + { "or", TokenId.DoubleBar }, + { "OrElse", TokenId.DoubleBar }, + { "not", TokenId.Exclamation }, + { "mod", TokenId.Percent } + }; + _numberDecimalSeparator = config.NumberParseCulture?.NumberFormat.NumberDecimalSeparator[0] ?? DefaultNumberDecimalSeparator; _text = text; @@ -529,14 +531,14 @@ private Exception ParseError(string format, params object[] args) return ParseError(CurrentToken.Pos, format, args); } - private static Exception ParseError(int pos, string format, params object[] args) + private TokenId GetAliasedTokenId(TokenId tokenId, string alias) { - return new ParseException(string.Format(CultureInfo.CurrentCulture, format, args), pos); + return tokenId == TokenId.Identifier && _predefinedOperatorAliases.TryGetValue(alias, out TokenId id) ? id : tokenId; } - private static TokenId GetAliasedTokenId(TokenId tokenId, string alias) + private static Exception ParseError(int pos, string format, params object[] args) { - return tokenId == TokenId.Identifier && PredefinedOperatorAliases.TryGetValue(alias, out TokenId id) ? id : tokenId; + return new ParseException(string.Format(CultureInfo.CurrentCulture, format, args), pos); } private static bool IsHexChar(char c) diff --git a/test/System.Linq.Dynamic.Core.Tests/CustomTypeProviders/CustomTypeProviderTests.cs b/test/System.Linq.Dynamic.Core.Tests/CustomTypeProviders/CustomTypeProviderTests.cs new file mode 100644 index 00000000..e4ea0a6c --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/CustomTypeProviders/CustomTypeProviderTests.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Dynamic.Core.CustomTypeProviders; +using FluentAssertions; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests.CustomTypeProviders +{ + [SuppressMessage("ReSharper", "InconsistentNaming")] + [SuppressMessage("ReSharper", "MemberHidesStaticFromOuterClass")] + public class CustomTypeProviderTests + { + private readonly ParsingConfig _config = new() + { + CustomTypeProvider = new MyCustomTypeProvider(ParsingConfig.Default, typeof(IS), typeof(IS.NOT), typeof(IST), typeof(IST.NOT)), + IsCaseSensitive = true + }; + + [Theory] + [InlineData("IS.NULL(null)", true)] + [InlineData("IS.NOT.NULL(null)", false)] + [InlineData("IST.NULL(null)", true)] + [InlineData("IST.NOT.NULL(null)", false)] + public void Test(string expression, bool expected) + { + // Act 1 + var lambdaExpression = DynamicExpressionParser.ParseLambda( + _config, + false, + [], + typeof(bool), + expression + ); + + // Act 2 + var result = (bool?)lambdaExpression.Compile().DynamicInvoke(); + + // Assert + result.Should().Be(expected); + } + + public class MyCustomTypeProvider : DefaultDynamicLinqCustomTypeProvider + { + private readonly HashSet _types; + + public MyCustomTypeProvider(ParsingConfig config, params Type[] types) : base(config) + { + _types = new HashSet(types); + } + + public override HashSet GetCustomTypes() + { + return _types; + } + } + + public static class IS + { + public static bool NULL(object? value) => value == null; + + public static class NOT + { + public static bool NULL(object? value) => value != null; + } + } + + public static class IST + { + public static bool NULL(object? value) => value == null; + + public static class NOT + { + public static bool NULL(object? value) => value != null; + } + } + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/DefaultDynamicLinqCustomTypeProviderTests.cs b/test/System.Linq.Dynamic.Core.Tests/CustomTypeProviders/DefaultDynamicLinqCustomTypeProviderTests.cs similarity index 88% rename from test/System.Linq.Dynamic.Core.Tests/DefaultDynamicLinqCustomTypeProviderTests.cs rename to test/System.Linq.Dynamic.Core.Tests/CustomTypeProviders/DefaultDynamicLinqCustomTypeProviderTests.cs index 1c9fe130..6fe19ba8 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DefaultDynamicLinqCustomTypeProviderTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/CustomTypeProviders/DefaultDynamicLinqCustomTypeProviderTests.cs @@ -4,11 +4,11 @@ using NFluent; using Xunit; -namespace System.Linq.Dynamic.Core.Tests; +namespace System.Linq.Dynamic.Core.Tests.CustomTypeProviders; public class DefaultDynamicLinqCustomTypeProviderTests { - private readonly IDynamicLinkCustomTypeProvider _sut; + private readonly DefaultDynamicLinqCustomTypeProvider _sut; public DefaultDynamicLinqCustomTypeProviderTests() { @@ -19,7 +19,7 @@ public DefaultDynamicLinqCustomTypeProviderTests() public void DefaultDynamicLinqCustomTypeProvider_ResolveSystemType() { // Act - var type = _sut.ResolveType(typeof(DirectoryInfo).FullName); + var type = _sut.ResolveType(typeof(DirectoryInfo).FullName!); // Assert type.Should().Be(typeof(DirectoryInfo)); @@ -49,7 +49,7 @@ public void DefaultDynamicLinqCustomTypeProvider_ResolveType_UnknownReturnsNull( public void DefaultDynamicLinqCustomTypeProvider_ResolveType_DefinedReturnsType() { // Act - var result = _sut.ResolveType(typeof(DefaultDynamicLinqCustomTypeProviderTests).FullName); + var result = _sut.ResolveType(typeof(DefaultDynamicLinqCustomTypeProviderTests).FullName!); // Assert Check.That(result).IsNotNull();