diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a90e948
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,48 @@
+# top-most EditorConfig file
+root = true
+
+# charset
+[*]
+charset = utf-8
+
+# lines
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = off
+
+# tabs
+[*]
+indent_style = space
+indent_size = 4
+tab_width = 4
+
+[*.{csproj,props,resx,targets}]
+indent_style = space
+indent_size = 2
+tab_width = 2
+
+# rules
+# CA2225: Operator overloads have named alternates
+dotnet_diagnostic.CA2225.severity = none
+# CA1716: Identifiers should not match keywords
+dotnet_diagnostic.CA1716.severity = none
+# CA1054: URI-like parameters should not be strings
+dotnet_diagnostic.CA1054.severity = none
+# CA1056: URI-like properties should not be strings
+dotnet_diagnostic.CA1056.severity = none
+# CA1062: Validate arguments of public methods
+dotnet_diagnostic.CA1062.severity = none
+# CA1033: Interface methods should be callable by child types
+dotnet_diagnostic.CA1033.severity = none
+# CA1034: Nested types should not be visible
+dotnet_diagnostic.CA1034.severity = none
+# CA1308: Normalize strings to uppercase
+dotnet_diagnostic.CA1308.severity = none
+# CA1805: Do not initialize unnecessarily
+dotnet_diagnostic.CA1805.severity = none
+# CA1031: Do not catch general exception types
+dotnet_diagnostic.CA1031.severity = none
+# CA1714: Flags enums should have plural names
+dotnet_diagnostic.CA1714.severity = none
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
new file mode 100644
index 0000000..ada77ec
--- /dev/null
+++ b/.github/workflows/build-and-test.yml
@@ -0,0 +1,19 @@
+name: build & test
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: TagBites/actions/dotnet-build@master
+ with:
+ solution: TagBites.Expressions.sln
+ - uses: TagBites/actions/dotnet-test@master
diff --git a/.github/workflows/publish-preview.yml b/.github/workflows/publish-preview.yml
new file mode 100644
index 0000000..2417644
--- /dev/null
+++ b/.github/workflows/publish-preview.yml
@@ -0,0 +1,21 @@
+name: publish preview
+
+on:
+ push:
+ tags:
+ - "v?[0-9]+.[0-9]+.[0-9]+-preview.[0-9]+"
+
+jobs:
+ publish-preview:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: TagBites/actions/dotnet-build@master
+ with:
+ solution: TagBites.Expressions.sln
+ - uses: TagBites/actions/nuget-publish@master
+ with:
+ nuget-source: "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
+ nuget-key: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..63365ea
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,21 @@
+name: publish
+
+on:
+ push:
+ tags:
+ - "v?[0-9]+.[0-9]+.[0-9]+"
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: TagBites/actions/dotnet-build@master
+ with:
+ solution: TagBites.Expressions.sln
+ - uses: TagBites/actions/nuget-publish@master
+ with:
+ nuget-source: "https://api.nuget.org/v3/index.json"
+ nuget-key: "${{ secrets.NUGET_KEY }}"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9cd5166
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+# User-specific files
+*.DotSettings
+*.user
+
+# Folders
+/Build/keys
+/Build/*.exe
+/Build/*.dll
+/Build/*.nupkg
+/TestResults
+.vs/
+packages/
+[Bb]in/
+[Oo]bj/
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..01c6a0d
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,63 @@
+
+
+
+
+ Tag Bites sp. z o.o.
+ Tag Bites sp. z o.o.
+ 2012
+
+ © $(CopyrightSinceYear)-$([System.DateTime]::Today.ToString(`yyyy`)) $(Company)
+
+
+
+
+ $(SolutionDir)bin\$(MSBuildProjectName)\
+ $(SolutionDir)bin\
+ $(SolutionDir)bin\obj\$(MSBuildProjectName)\
+
+
+
+
+ latest
+ true
+ true
+ enable
+
+
+
+
+ true
+
+
+
+
+ 1701;1702;1591;NU5048;NU5125;IDE0290
+
+
+
+
+ en-US
+
+
+
+
+ $(DefaultItemExcludes);*.csproj.DotSettings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000..cc768b7
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,21 @@
+
+
+
+
+ All
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..80e4179
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Tag Bites
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Package.props b/Package.props
new file mode 100644
index 0000000..d8d6d9e
--- /dev/null
+++ b/Package.props
@@ -0,0 +1,17 @@
+
+
+ Converts C# text expressions into LINQ expressions using **Roslyn**, supporting complete language syntax.
+ LINQ;expressions;Roslyn;csharp
+
+ https://github.com/TagBites/TagBites.Expressions
+
+
+ false
+ MIT
+
+ git
+ https://github.com/TagBites/TagBites.Expressions.git
+
+ README.md
+
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a5d79fd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+# TagBites.Expressions
+
+[![Nuget](https://img.shields.io/nuget/v/TagBites.Expressions.svg)](https://www.nuget.org/packages/TagBites.Expressions/)
+[![License](https://img.shields.io/github/license/TagBites/TagBites.Expressions)](https://github.com/TagBites/TagBites.Expressions/blob/master/LICENSE)
+
+Converts C# text expressions into LINQ expressions using **Roslyn**, supporting complete language syntax.
+
+## Example
+
+```csharp
+public void BasicUseTest()
+{
+ var expression = "new [] { 1, 2, 3 }.Select(x => (x, x + 1).Item2).Sum()";
+ var func = ExpressionParser.Parse(expression, null).Compile();
+
+ Assert.Equal(9, func.DynamicInvoke());
+}
+
+public void SimpleTest()
+{
+ var func = Parse("(a + b) / (double)b");
+ Assert.Equal(2.5d, func(3, 2));
+
+ func = Parse("a switch { 1 => b, 2 => b * 2, _ => b + a }");
+ Assert.Equal(2, func(1, 2));
+ Assert.Equal(4, func(2, 2));
+ Assert.Equal(5, func(3, 2));
+
+ static Func Parse(string expression)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(int), "a"),
+ (typeof(int), "b")
+ },
+ ResultCastType = typeof(double)
+ };
+ var lambda = ExpressionParser.Parse(expression, options);
+ return (Func)lambda.Compile();
+ }
+}
+
+public void TypeTest()
+{
+ var m = new TestModel { X = 1, Y = 2 };
+
+ var func = Parse("X + Y");
+ Assert.Equal(3, func(m));
+
+ func = Parse("X + Nested.X");
+ Assert.Equal(3, func(m));
+
+ func = Parse("X + new TestModel { X = 1, Y = 2 }.Y");
+ Assert.Equal(3, func(m));
+
+ func = Parse("X + (X == 1 ? Nested.X : Nested.Y)");
+ Assert.Equal(3, func(m));
+ Assert.Equal(7, func(new TestModel { X = 2, Y = 3 }));
+
+ static Func Parse(string expression)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(TestModel), "this")
+ },
+ UseFirstParameterAsThis = true,
+ ResultType = typeof(int)
+ };
+ var lambda = ExpressionParser.Parse(expression, options);
+ return (Func)lambda.Compile();
+ }
+}
+
+private class TestModel
+{
+ private TestModel? _nested;
+
+ public int X { get; set; }
+ public int Y { get; set; }
+ public TestModel Nested => _nested ??= new TestModel { X = Y, Y = X + Y };
+}
+```
diff --git a/TagBites.Expressions.sln b/TagBites.Expressions.sln
new file mode 100644
index 0000000..24f5661
--- /dev/null
+++ b/TagBites.Expressions.sln
@@ -0,0 +1,49 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33205.214
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{62CB84CB-ABBE-4834-BBD3-9084940552FE}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ .gitignore = .gitignore
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ Package.props = Package.props
+ README.md = README.md
+ Version.props = Version.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagBites.Expressions", "src\TagBites.Expressions\TagBites.Expressions.csproj", "{259881F0-BEA4-4FED-AA24-409352657EF7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagBites.Expressions.Tests", "tests\TagBites.Expressions.Tests\TagBites.Expressions.Tests.csproj", "{A0222F96-ED70-4D16-A4B6-D4407024E491}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1870CE1A-8E9A-40F1-AA6F-5A4502F1679E}"
+ ProjectSection(SolutionItems) = preProject
+ .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml
+ .github\workflows\publish-preview.yml = .github\workflows\publish-preview.yml
+ .github\workflows\publish.yml = .github\workflows\publish.yml
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {259881F0-BEA4-4FED-AA24-409352657EF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {259881F0-BEA4-4FED-AA24-409352657EF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {259881F0-BEA4-4FED-AA24-409352657EF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {259881F0-BEA4-4FED-AA24-409352657EF7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A0222F96-ED70-4D16-A4B6-D4407024E491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0222F96-ED70-4D16-A4B6-D4407024E491}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A0222F96-ED70-4D16-A4B6-D4407024E491}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A0222F96-ED70-4D16-A4B6-D4407024E491}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {4C4ED72A-E0F0-44B5-8F52-EBB27C7E235B}
+ EndGlobalSection
+EndGlobal
diff --git a/Version.props b/Version.props
new file mode 100644
index 0000000..d25303f
--- /dev/null
+++ b/Version.props
@@ -0,0 +1,23 @@
+
+
+ preview
+
+
+
+
+ $(MinVerMajor).$(MinVerMinor).$(MinVerPatch)
+ $(MinVerPreRelease)
+ $(VersionPrefix)
+ $(VersionPrefix)-$(VersionSuffix)
+
+ $(VersionFull)
+ $(VersionFull)
+ $(VersionPrefix).0
+ $(VersionPrefix).0
+ $(VersionFull)
+
+
+
+
+
+
diff --git a/src/TagBites.Expressions/Expressions/ExpressionParser.cs b/src/TagBites.Expressions/Expressions/ExpressionParser.cs
new file mode 100644
index 0000000..eee5679
--- /dev/null
+++ b/src/TagBites.Expressions/Expressions/ExpressionParser.cs
@@ -0,0 +1,82 @@
+using System.Linq.Expressions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace TagBites.Expressions;
+
+[PublicAPI]
+public static class ExpressionParser
+{
+ public static LambdaExpression Parse(string script, ExpressionParserOptions? options)
+ {
+ return TryParse(script, options, out var expression, out var errorMessage)
+ ? expression!
+ : throw new ExpressionParserException(errorMessage!);
+ }
+ public static bool TryParse(string script, ExpressionParserOptions? options, out LambdaExpression? expression, out string? errorMessage)
+ {
+ options ??= new ExpressionParserOptions();
+
+ var root = PrepareScript(script);
+ var diagnostics = root.GetDiagnostics();
+
+ var error = diagnostics.FirstOrDefault(x => x.Severity == DiagnosticSeverity.Error && x.Id != "CS1002");
+ if (error != null)
+ {
+ expression = null;
+ errorMessage = error.GetMessage();
+ }
+ else
+ {
+ var sv = new ExpressionBuilder(options);
+
+ try
+ {
+ expression = sv.CreateLambdaExpression(root);
+ errorMessage = sv.FirstError;
+
+ if (!options.AllowReflection && expression != null)
+ {
+ var reflectionVisitor = new ReflectionDetector();
+ reflectionVisitor.Visit(expression);
+
+ if (reflectionVisitor.HasReflectionCall)
+ {
+ expression = null;
+ errorMessage = "Reflection is not allowed.";
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ expression = null;
+ errorMessage = e.Message;
+ }
+ }
+
+ return expression != null;
+ }
+
+ public static (IList Identifiers, IList UnknownIdentifiers) DetectIdentifiers(string script, ExpressionParserOptions? options)
+ {
+ options ??= new ExpressionParserOptions();
+
+ var root = PrepareScript(script);
+ var visitor = new IdentifierDetector(options);
+
+ visitor.Visit(root);
+
+ return (visitor.Identifiers, visitor.UnknownIdentifiers);
+ }
+
+ private static SyntaxNode PrepareScript(string script)
+ {
+ if (string.IsNullOrWhiteSpace(script))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(script));
+
+ var tree = CSharpSyntaxTree.ParseText(script, CSharpParseOptions.Default.WithKind(SourceCodeKind.Script));
+ var root = tree.GetRoot();
+
+ return root;
+ }
+}
diff --git a/src/TagBites.Expressions/Expressions/ExpressionParserException.cs b/src/TagBites.Expressions/Expressions/ExpressionParserException.cs
new file mode 100644
index 0000000..baa30dd
--- /dev/null
+++ b/src/TagBites.Expressions/Expressions/ExpressionParserException.cs
@@ -0,0 +1,8 @@
+namespace TagBites.Expressions;
+
+public sealed class ExpressionParserException : Exception
+{
+ public ExpressionParserException(string message)
+ : base(message)
+ { }
+}
diff --git a/src/TagBites.Expressions/Expressions/ExpressionParserOptions.cs b/src/TagBites.Expressions/Expressions/ExpressionParserOptions.cs
new file mode 100644
index 0000000..1bbb5b9
--- /dev/null
+++ b/src/TagBites.Expressions/Expressions/ExpressionParserOptions.cs
@@ -0,0 +1,57 @@
+using System.Linq.Expressions;
+
+namespace TagBites.Expressions;
+
+[PublicAPI]
+public class ExpressionParserOptions
+{
+ ///
+ /// Expected and required result type of the expression.
+ ///
+ public Type? ResultType { get; set; }
+ ///
+ /// A type to convert expression to, for example to create general lambda like Func<object>.
+ ///
+ public Type? ResultCastType { get; set; }
+
+ public IList<(Type Type, string Name)> Parameters { get; } = new List<(Type, string)>();
+ public bool UseFirstParameterAsThis { get; set; }
+ public bool UseReducedExpressions { get; set; }
+ public bool AllowReflection { get; set; }
+ public bool AllowRuntimeCast { get; set; }
+
+ public ICollection IncludedTypes { get; } = new TypeCollection();
+ internal IDictionary IncludedTypesMap => (TypeCollection)IncludedTypes;
+ public Func? CustomPropertyResolver { get; set; }
+
+ public ExpressionParserOptions()
+ {
+#if !DEBUG
+ UseReducedExpressions = true;
+#endif
+ }
+
+ private class TypeCollection : Dictionary, ICollection
+ {
+ public bool IsReadOnly => false;
+
+
+ public bool Contains(Type item) => TryGetValue(item.Name, out var t) && item == t;
+ public void CopyTo(Type[] array, int arrayIndex) => Values.CopyTo(array, arrayIndex);
+
+ public void Add(Type item)
+ {
+ if (TryGetValue(item.Name, out var t))
+ if (item != t)
+ throw new ArgumentException($"Different type with the same name '{t.Name}' has already been included.");
+ else
+ return;
+
+ Add(item.Name, item);
+ }
+ public bool Remove(Type item) => Remove(item.Name);
+
+ IEnumerator IEnumerable.GetEnumerator() => Values.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => Values.GetEnumerator();
+ }
+}
diff --git a/src/TagBites.Expressions/Expressions/IExpressionMemberResolverContext.cs b/src/TagBites.Expressions/Expressions/IExpressionMemberResolverContext.cs
new file mode 100644
index 0000000..6364d02
--- /dev/null
+++ b/src/TagBites.Expressions/Expressions/IExpressionMemberResolverContext.cs
@@ -0,0 +1,17 @@
+using System.Linq.Expressions;
+
+namespace TagBites.Expressions;
+
+[PublicAPI]
+public interface IExpressionMemberResolverContext
+{
+ Expression Instance { get; }
+ object? InstanceTypeInfo { get; }
+
+ string MemberName { get; }
+ string? MemberFullPath { get; }
+
+
+ ParameterExpression GetParameter(string name);
+ Expression IncludeTypeInfo(Expression expression, object typeInfo);
+}
diff --git a/src/TagBites.Expressions/Expressions/Visitors/ExpressionBuilder.cs b/src/TagBites.Expressions/Expressions/Visitors/ExpressionBuilder.cs
new file mode 100644
index 0000000..972a3a4
--- /dev/null
+++ b/src/TagBites.Expressions/Expressions/Visitors/ExpressionBuilder.cs
@@ -0,0 +1,2099 @@
+using System.Globalization;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using TagBites.Utils;
+
+namespace TagBites.Expressions;
+
+// TODO namespaces support VisitQualifiedName
+
+internal class ExpressionBuilder : CSharpSyntaxVisitor
+{
+ private static MethodInfo? s_typeInfoWrapper;
+
+ private readonly ExpressionParserOptions _options;
+ private readonly ParameterExpression? _thisParameter;
+ private readonly List _parameters;
+ private Expression? _tmp;
+ private Expression? _extensionInstance;
+ private List? _nestedParameters;
+ private MemberResolverContext? _resolverContext;
+ private ParameterExpression? _variableContextParameter;
+ private List<(Type Type, string Name, int Index)>? _variables;
+ private int _nextVariableIndex;
+
+ public string? FirstError { get; private set; }
+
+ public ExpressionBuilder(ExpressionParserOptions options)
+ {
+ _options = options;
+ _parameters = options.Parameters.Select(x => Expression.Parameter(x.Type, x.Name)).ToList();
+
+ if (options.UseFirstParameterAsThis && _parameters.Count > 0)
+ _thisParameter = _parameters[0];
+ }
+
+
+ public LambdaExpression? CreateLambdaExpression(SyntaxNode node)
+ {
+ var expression = Visit(node);
+ if (expression == null)
+ return null;
+
+ var isNullableExpression = IsNullableType(expression.Type);
+ if (_options.ResultType != null)
+ {
+ var isNullableResultType = IsNullableType(_options.ResultType);
+ var isNullableResult = !_options.ResultType.IsValueType || isNullableResultType;
+ var isAssignable = isNullableResultType && !isNullableExpression && Nullable.GetUnderlyingType(_options.ResultType) == expression.Type
+ || _options.ResultType.IsAssignableFrom(expression.Type);
+
+ if (isNullableExpression && !isNullableResult || !isAssignable)
+ {
+ ToError(node, $"Result type is expected to be '{_options.ResultType.GetFriendlyTypeName()}', but type '{expression.Type.GetFriendlyTypeName()}' is returned.");
+ return null;
+ }
+
+ //if (_options.ResultType != expression.Type && !isNullableResult)
+ // expression = Expression.Convert(expression, _options.ResultType);
+ }
+
+ if (_options.ResultCastType != null && _options.ResultCastType != expression.Type)
+ expression = Expression.Convert(expression, _options.ResultCastType);
+
+ if (_variableContextParameter != null)
+ {
+ var innerLambda = Expression.Lambda(expression, _parameters.Concat(new[] { _variableContextParameter }));
+ expression = Expression.Invoke(innerLambda, _parameters.Cast().Concat(new[] { Expression.New(_variableContextParameter.Type.GetConstructor(new[] { typeof(int) })!, Expression.Constant(_nextVariableIndex)) }));
+ }
+
+ return Expression.Lambda(expression, _parameters);
+ }
+
+ public override Expression? Visit(SyntaxNode? node)
+ {
+ if (node == null)
+ return null;
+
+ try
+ {
+ var expression = base.Visit(node);
+ return expression != null && _options.UseReducedExpressions && expression is not DelayLambdaExpression && expression.NodeType == ExpressionType.Extension
+ ? expression.Reduce()
+ : expression;
+ }
+ catch (Exception e)
+ {
+ return ToError(node, e.Message);
+ }
+ }
+ public override Expression? DefaultVisit(SyntaxNode node) => ToError(node);
+
+ public override Expression? VisitCompilationUnit(CompilationUnitSyntax node)
+ {
+ if (node.Members.Count == 1 && node.Members[0] is GlobalStatementSyntax gs)
+ return Visit(gs);
+
+ if (node.Members.FirstOrDefault() is IncompleteMemberSyntax)
+ return ToError(node, "Incomplete syntax.");
+
+ return ToError(node);
+ }
+ public override Expression? VisitGlobalStatement(GlobalStatementSyntax node) => Visit(node.Statement);
+ public override Expression? VisitExpressionStatement(ExpressionStatementSyntax node) => Visit(node.Expression);
+ public override Expression? VisitParenthesizedExpression(ParenthesizedExpressionSyntax node) => Visit(node.Expression);
+ public override Expression? VisitArgument(ArgumentSyntax node) => Visit(node.Expression);
+ public override Expression? VisitConditionalExpression(ConditionalExpressionSyntax node)
+ {
+ var condition = Visit(node.Condition);
+ if (condition == null)
+ return null;
+
+ var whenTrue = Visit(node.WhenTrue);
+ if (whenTrue == null)
+ return null;
+
+ var whenFalse = Visit(node.WhenFalse);
+ if (whenFalse == null)
+ return null;
+
+ if (!EnsureTheSameTypes(node, ref whenTrue, ref whenFalse))
+ return null;
+
+ return Expression.Condition(condition, whenTrue, whenFalse);
+ }
+ public override Expression? VisitSwitchExpression(SwitchExpressionSyntax node)
+ {
+ var governing = Visit(node.GoverningExpression);
+ if (governing == null)
+ return null;
+
+ // Create paths
+ var paths = new List<(Expression When, Expression Then)>();
+ Expression? switchExpression = null!;
+
+ for (var i = 0; i < node.Arms.Count; i++)
+ {
+ var arm = node.Arms[i];
+
+ var expression = Visit(arm.Expression);
+ if (expression == null)
+ return null;
+
+ // Condition
+ Expression? condition = null;
+
+ switch (arm.Pattern)
+ {
+ case DiscardPatternSyntax when i + 1 != node.Arms.Count || switchExpression != null:
+ return ToError(node, "Invalid switch syntax.");
+ case DiscardPatternSyntax:
+ break;
+
+ case ConstantPatternSyntax cps:
+ {
+ var value = Visit(cps.Expression);
+ if (value == null)
+ return null;
+
+ if (!EnsureArgumentType(arm.Pattern, governing.Type, ref value))
+ return ToError(arm.Pattern, "Switch governing and arm type mismatch.");
+
+ condition = Expression.MakeBinary(ExpressionType.Equal, governing, value);
+ break;
+ }
+
+ default:
+ return ToError(arm.Pattern);
+ }
+
+ if (condition == null)
+ switchExpression = expression;
+ else
+ paths.Add((condition, expression));
+ }
+
+ if (switchExpression == null)
+ return ToError(node);
+
+ // Convert to if-else
+ for (var i = paths.Count - 1; i >= 0; i--)
+ {
+ if (switchExpression.Type != paths[i].Then.Type)
+ return ToError(node.Arms[i].Expression, "Switch expressions types mismatch.");
+
+ switchExpression = Expression.Condition(paths[i].When, paths[i].Then, switchExpression);
+ }
+
+ return switchExpression;
+ }
+ public override Expression? VisitBinaryExpression(BinaryExpressionSyntax node)
+ {
+ var left = Visit(node.Left);
+ if (left == null)
+ return null;
+
+ var right = Visit(node.Right);
+ if (right == null)
+ return null;
+
+ var expressionType = (SyntaxKind)node.OperatorToken.RawKind switch
+ {
+ // Math
+ SyntaxKind.PlusToken => ExpressionType.Add,
+ SyntaxKind.MinusToken => ExpressionType.Subtract,
+ SyntaxKind.AsteriskToken => ExpressionType.Multiply,
+ SyntaxKind.SlashToken => ExpressionType.Divide,
+ SyntaxKind.PercentToken => ExpressionType.Modulo,
+
+ // Bitwise
+ SyntaxKind.BarToken => ExpressionType.Or,
+ SyntaxKind.AmpersandToken => ExpressionType.And,
+ SyntaxKind.CaretToken => ExpressionType.ExclusiveOr,
+ SyntaxKind.LessThanLessThanToken => ExpressionType.LeftShift,
+ SyntaxKind.GreaterThanGreaterThanToken => ExpressionType.RightShift,
+
+ // Logic
+ SyntaxKind.BarBarToken => ExpressionType.OrElse,
+ SyntaxKind.AmpersandAmpersandToken => ExpressionType.AndAlso,
+
+ SyntaxKind.EqualsEqualsToken => ExpressionType.Equal,
+ SyntaxKind.ExclamationEqualsToken => ExpressionType.NotEqual,
+ SyntaxKind.GreaterThanToken => ExpressionType.GreaterThan,
+ SyntaxKind.GreaterThanEqualsToken => ExpressionType.GreaterThanOrEqual,
+ SyntaxKind.LessThanToken => ExpressionType.LessThan,
+ SyntaxKind.LessThanEqualsToken => ExpressionType.LessThanOrEqual,
+
+ // Cast
+ SyntaxKind.IsKeyword => ExpressionType.TypeIs,
+ SyntaxKind.AsKeyword => ExpressionType.TypeAs,
+
+ _ => ExpressionType.Throw
+ };
+
+ // Special operators
+ if (expressionType == ExpressionType.Throw)
+ {
+ // operator ??
+ if ((SyntaxKind)node.OperatorToken.RawKind == SyntaxKind.QuestionQuestionToken)
+ {
+ var condition = left;
+
+ if (IsNullableType(left.Type))
+ left = Expression.MakeMemberAccess(left, left.Type.GetProperty(nameof(Nullable.Value))!);
+
+ if (!EnsureTheSameTypes(node, ref left, ref right))
+ return null;
+
+ return Expression.Condition(ToIsNotNull(condition), left, right);
+ }
+
+ // Unknown
+ return ToError(node, $"Unsupported binary operator '{node.OperatorToken.ValueText}'.");
+ }
+
+ // is operator
+ if (expressionType == ExpressionType.TypeIs)
+ return ToIsOperator(left, right);
+
+ // as operator
+ if (expressionType == ExpressionType.TypeAs)
+ return ToAsOperator(node, left, right);
+
+ // Operator + is not defined for string - use Contact instead
+ if (expressionType == ExpressionType.Add && (left.Type == typeof(string) || right.Type == typeof(string)))
+ {
+ var contactMethod = typeof(string).GetMethod(nameof(string.Concat), BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(object), typeof(object) }, null);
+ var toStringMethod = typeof(object).GetMethod("ToString", BindingFlags.Public | BindingFlags.Instance);
+
+ if (left.Type != typeof(string))
+ left = CallWhenNotNull(left, toStringMethod!);
+
+ if (right.Type != typeof(string))
+ right = CallWhenNotNull(right, toStringMethod!);
+
+ return Expression.Call(null, contactMethod!, left, right);
+ }
+
+ // Operator < <= > >= is not defined for string - use Compare instead
+ if (left.Type == typeof(string) && right.Type == typeof(string) && expressionType is ExpressionType.LessThan or ExpressionType.LessThanOrEqual or ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual)
+ {
+ var compareMethod = typeof(string).GetMethod(nameof(string.Compare), BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(string), typeof(string) }, null);
+ var compareExpression = Expression.Call(null, compareMethod!, left, right);
+ return Expression.MakeBinary(expressionType, compareExpression, Expression.Constant(0));
+ }
+
+ // Operator || && for bool only
+ if (expressionType is ExpressionType.AndAlso or ExpressionType.OrElse)
+ {
+ var leftType = Nullable.GetUnderlyingType(left.Type) ?? left.Type;
+ if (leftType != typeof(bool))
+ return ToError(node.Left, "Expected boolean expression.");
+
+ var rightType = Nullable.GetUnderlyingType(right.Type) ?? right.Type;
+ if (rightType != typeof(bool))
+ return ToError(node.Right, "Expected boolean expression.");
+ }
+
+ // Ensure types
+ if (!EnsureTheSameTypes(node, ref left, ref right))
+ return null;
+
+ return Expression.MakeBinary(expressionType, left, right);
+ }
+ public override Expression? VisitPrefixUnaryExpression(PrefixUnaryExpressionSyntax node)
+ {
+ var operand = Visit(node.Operand);
+ if (operand == null)
+ return null;
+
+ var expressionType = (SyntaxKind)node.OperatorToken.RawKind switch
+ {
+ SyntaxKind.ExclamationToken => ExpressionType.Not,
+ SyntaxKind.PlusToken => ExpressionType.UnaryPlus,
+ SyntaxKind.MinusToken => ExpressionType.Negate,
+
+ _ => ExpressionType.Throw
+ };
+ if (expressionType == ExpressionType.Throw)
+ return ToError(node, $"Unsupported unary operator '{node.OperatorToken.ValueText}'.");
+
+ return Expression.MakeUnary(expressionType, operand, null);
+ }
+ public override Expression? VisitCastExpression(CastExpressionSyntax node)
+ {
+ var expression = Visit(node.Expression);
+ if (expression == null)
+ return null;
+
+ var type = ResolveType(node.Type);
+ if (type == null)
+ return null;
+
+ return Expression.Convert(expression, type);
+ }
+ public override Expression? VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
+ {
+ var instance = Visit(node.Expression);
+ if (instance == null)
+ return null;
+
+ var name = node.Name.Identifier.Text;
+ return ResolveCustomMember(instance, name)
+ ?? ResolveMember(node, instance, name);
+ }
+ public override Expression? VisitMemberBindingExpression(MemberBindingExpressionSyntax node)
+ {
+ var instance = Pop(node);
+ if (instance == null)
+ return null;
+
+ var name = node.Name.Identifier.Text;
+ return ResolveCustomMember(instance, name)
+ ?? ResolveMember(node, instance, name);
+ }
+ public override Expression? VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node)
+ {
+ var instance = Visit(node.Expression);
+ if (instance == null)
+ return null;
+
+ // Resolve Member
+ var valueInstance = instance;
+
+ if (instance.Type.IsValueType && Nullable.GetUnderlyingType(instance.Type) != null)
+ valueInstance = Expression.MakeMemberAccess(instance, instance.Type.GetProperty("Value")!);
+
+ Push(valueInstance);
+ var whenNotNull = Visit(node.WhenNotNull);
+ if (whenNotNull == null)
+ return null;
+
+ return new ConditionalAccessExpression(instance, whenNotNull);
+ }
+ public override Expression? VisitInvocationExpression(InvocationExpressionSyntax node)
+ {
+ // Parameters
+ var parameters = ResolveParameters(node.ArgumentList.Arguments);
+ if (parameters == null)
+ return null;
+
+ // Find instance, type, name syntax
+ Expression? instanceExpression;
+ Type? instanceType;
+ SimpleNameSyntax methodNameSyntax;
+
+ switch (node.Expression)
+ {
+ case IdentifierNameSyntax ins:
+ {
+ instanceExpression = null;
+ methodNameSyntax = ins;
+ }
+ break;
+
+ case MemberAccessExpressionSyntax ma:
+ {
+ instanceExpression = Visit(ma.Expression);
+ if (instanceExpression == null)
+ return null;
+
+ methodNameSyntax = ma.Name;
+ break;
+ }
+
+ case MemberBindingExpressionSyntax mbs:
+ {
+ instanceExpression = TryPop();
+ if (instanceExpression == null)
+ return null;
+
+ methodNameSyntax = mbs.Name;
+ break;
+ }
+
+ default:
+ return ToError(node);
+ }
+
+ switch (instanceExpression)
+ {
+ case ConstantExpression { Value: Type type }:
+ instanceExpression = null;
+ instanceType = type;
+ break;
+
+ default:
+ instanceType = instanceExpression?.Type;
+ break;
+ }
+
+ // Method name with generics
+ var methodName = methodNameSyntax.Identifier.Text;
+ Type[]? genericTypes = null;
+
+ if (methodNameSyntax is GenericNameSyntax gns)
+ {
+ genericTypes = new Type[gns.TypeArgumentList.Arguments.Count];
+ for (var i = 0; i < genericTypes.Length; i++)
+ {
+ var type = ResolveType(gns.TypeArgumentList.Arguments[0]);
+ if (type == null)
+ return null;
+
+ genericTypes[i] = type;
+ }
+ }
+
+ // Custom keywords
+ if (instanceType == null)
+ {
+ if (_options.AllowRuntimeCast
+ && genericTypes == null
+ && ResolveCustomKeywords(node, methodName, parameters) is { } expression)
+ {
+ return expression;
+ }
+
+ if (FirstError != null)
+ return null;
+
+ return ToError(node, $"Method '{methodName}' not found for this arguments.");
+ }
+
+ // Instance or static method
+ {
+ var methods = GetMethods(instanceType, methodName, instanceExpression == null ? BindingFlags.Static : BindingFlags.Instance);
+ if (methods.Count > 0)
+ {
+ if (genericTypes != null)
+ {
+ methods = methods
+ .Where(x => x.IsGenericMethodDefinition && x.GetGenericArguments().Length == genericTypes.Length)
+ .Select(x => x.MakeGenericMethod(genericTypes))
+ .ToList();
+ }
+
+ if (TryResolveMethodCall(node, instanceExpression, parameters, methods, out var expression))
+ return expression;
+ }
+ }
+
+ // Extension methods
+ {
+ // Select method override
+ var methods = GetExtensionMethods(instanceType, methodName);
+ if (methods.Count > 0)
+ {
+ if (genericTypes != null)
+ {
+ methods = methods
+ .Where(x => x.IsGenericMethodDefinition && x.GetGenericArguments().Length == genericTypes.Length)
+ .Select(x => x.MakeGenericMethod(genericTypes))
+ .ToList();
+ }
+
+ parameters = new[] { instanceExpression! }.Concat(parameters).ToList();
+
+ var oldExtensionInstance = _extensionInstance;
+ _extensionInstance = instanceExpression;
+ {
+ if (TryResolveMethodCall(node, null, parameters, methods, out var expression))
+ return expression;
+ }
+ _extensionInstance = oldExtensionInstance;
+ }
+ }
+
+ return ToError(node, $"Method '{methodName}' not found for this arguments.");
+ }
+ public override Expression? VisitElementAccessExpression(ElementAccessExpressionSyntax node)
+ {
+ // Parameters
+ var parameters = ResolveParameters(node.ArgumentList.Arguments);
+ if (parameters == null)
+ return null;
+
+ // Find instance, type, metod name
+ var instanceExpression = Visit(node.Expression);
+ if (instanceExpression == null)
+ return null;
+
+ return ResolveItemCall(node, instanceExpression, parameters);
+ }
+ public override Expression? VisitElementBindingExpression(ElementBindingExpressionSyntax node)
+ {
+ // Parameters
+ var parameters = ResolveParameters(node.ArgumentList.Arguments);
+ if (parameters == null)
+ return null;
+
+ // Find instance, type, metod name
+ var instanceExpression = Pop(node);
+ if (instanceExpression == null)
+ return null;
+
+ return ResolveItemCall(node, instanceExpression, parameters);
+ }
+ public override Expression? VisitObjectCreationExpression(ObjectCreationExpressionSyntax node)
+ {
+ // Parameters
+ var args = ResolveParameters(node.ArgumentList?.Arguments);
+ if (args == null)
+ return null;
+
+ // Type
+ var type = ResolveType(node.Type);
+ if (type == null)
+ return null;
+
+ // Constructor
+ var constructors = type.GetConstructors()
+ .Select(x => (Method: x, Parameters: x.GetParameters()))
+ .Where(x => HasMatchingParameters(x.Parameters, args))
+ .OrderBy(x => x.Parameters.Length)
+ .ToList();
+
+ switch (constructors.Count)
+ {
+ case 0:
+ return ToError(node, $"Constructor for '{type.GetFriendlyTypeName()}' not found.");
+ case > 1 when constructors[0].Parameters.Length == constructors[1].Parameters.Length:
+ return ToError(node, $"Ambiguous call for '{type.GetFriendlyTypeName()}' constructor.");
+ }
+
+ var constructorInfo = constructors[0].Method;
+ var constructorParams = constructorInfo.GetParameters();
+
+ // Default values
+ for (var i = args.Count; i < constructorParams.Length; i++)
+ {
+ if (!constructorParams[i].HasDefaultValue)
+ return ToError(node, "Mismatch argument count.");
+
+ if (args is not List)
+ args = args.ToList();
+
+ args.Add(Expression.Constant(constructorParams[i].DefaultValue));
+ }
+
+ // Create
+ var instance = Expression.New(constructorInfo, args);
+
+ // Initializer
+ if (node.Initializer?.Expressions.Count > 0)
+ {
+ var bindings = new List();
+
+ foreach (var item in node.Initializer.Expressions)
+ {
+ if (item is not AssignmentExpressionSyntax { Left: IdentifierNameSyntax identifier } ae)
+ return ToError(item);
+
+ var memberName = identifier.Identifier.Text;
+ var member = GetAssignMember(type, memberName);
+ if (member == null)
+ return ToError(item, "Member not found.");
+
+ var expression = Visit(ae.Right);
+ if (expression == null)
+ return null;
+
+ bindings.Add(Expression.Bind(member, expression));
+ }
+
+ return Expression.MemberInit(instance, bindings);
+ }
+
+ return instance;
+ }
+ public override Expression? VisitArrayCreationExpression(ArrayCreationExpressionSyntax node)
+ {
+ var type = ResolveType(node.Type.ElementType);
+ if (type == null)
+ return null;
+
+ if (node.Type.RankSpecifiers.Count > 1 || node.Type.RankSpecifiers[0].Sizes[0] is not OmittedArraySizeExpressionSyntax)
+ return ToError(node.Type, "Array with explicit range specifiers is not supported.");
+
+ var expressions = new List();
+
+ if (node.Initializer != null)
+ foreach (var expression in node.Initializer.Expressions)
+ {
+ var exp = Visit(expression);
+ if (exp == null)
+ return null;
+ expressions.Add(exp);
+ }
+
+ return Expression.NewArrayInit(type, expressions);
+ }
+ public override Expression? VisitImplicitArrayCreationExpression(ImplicitArrayCreationExpressionSyntax node)
+ {
+ var expressions = new List();
+ Type? type = null;
+
+ foreach (var expression in node.Initializer.Expressions)
+ {
+ var exp = Visit(expression);
+ if (exp == null)
+ return null;
+
+ expressions.Add(exp);
+
+ if (type == null)
+ type = exp.Type;
+ else if (type != exp.Type)
+ return ToError(node, "Type not found for implicit array creation.");
+ }
+
+ return Expression.NewArrayInit(type!, expressions);
+ }
+
+ public override Expression? VisitIsPatternExpression(IsPatternExpressionSyntax node)
+ {
+ var left = Visit(node.Expression);
+ if (left == null)
+ return null;
+
+ return ResolvePattern(left, node.Pattern);
+ }
+ private Expression? ResolvePattern(Expression expression, PatternSyntax pattern)
+ {
+ var expressionType = expression.Type;
+
+ switch (pattern)
+ {
+ // is null, is not null, is {}
+ case ConstantPatternSyntax { Expression: LiteralExpressionSyntax { Token.Text: "null" } }:
+ case UnaryPatternSyntax { OperatorToken.Text: "not", Pattern: ConstantPatternSyntax { Expression: LiteralExpressionSyntax { Token.Text: "null" } } }:
+ case RecursivePatternSyntax { Designation: null, PositionalPatternClause: null, Type: null, PropertyPatternClause.Subpatterns.Count: 0 }:
+ {
+ var isNullCheck = pattern is ConstantPatternSyntax;
+
+ if (IsNullableType(expressionType))
+ {
+ var hasValue = Expression.MakeMemberAccess(expression, expressionType.GetProperty(nameof(Nullable.HasValue))!);
+ return isNullCheck ? Expression.Not(hasValue) : hasValue;
+ }
+
+ if (expressionType.IsValueType)
+ return ToError(pattern, $"Cannot convert null to '{expressionType.Name}'.");
+
+ var isNull = Expression.ReferenceEqual(expression, Expression.Constant(null, expressionType));
+ return isNullCheck ? isNull : Expression.Not(isNull);
+ }
+
+ // is const
+ case ConstantPatternSyntax p:
+ {
+ var right = Visit(p.Expression);
+ if (right == null)
+ return null;
+
+ if (!EnsureTheSameTypes(pattern, ref expression, ref right))
+ return null;
+
+ return Expression.MakeBinary(ExpressionType.Equal, expression, right);
+ }
+
+ // is type
+ case TypePatternSyntax p:
+ {
+ var type = ResolveType(p.Type);
+ if (type == null)
+ return null;
+
+ return ToIsOperator(expression, Expression.Constant(type));
+ }
+
+ // or, and
+ case BinaryPatternSyntax p:
+ {
+ var left = ResolvePattern(expression, p.Left);
+ if (left == null)
+ return null;
+ var right = ResolvePattern(expression, p.Right);
+ if (right == null)
+ return null;
+
+ return (SyntaxKind)p.OperatorToken.RawKind switch
+ {
+ SyntaxKind.OrKeyword => Expression.OrElse(left, right),
+ SyntaxKind.AndKeyword => Expression.AndAlso(left, right),
+ _ => ToError(pattern)
+ };
+ }
+
+ // >, <, >=, <=
+ case RelationalPatternSyntax p:
+ {
+ var right = Visit(p.Expression);
+ if (right == null)
+ return null;
+
+ if (!EnsureTheSameTypes(pattern, ref expression, ref right))
+ return null;
+
+ var opr = (SyntaxKind)p.OperatorToken.RawKind switch
+ {
+ SyntaxKind.GreaterThanToken => ExpressionType.GreaterThan,
+ SyntaxKind.GreaterThanEqualsToken => ExpressionType.GreaterThanOrEqual,
+ SyntaxKind.LessThanToken => ExpressionType.LessThan,
+ SyntaxKind.LessThanEqualsToken => ExpressionType.LessThanOrEqual,
+ _ => (ExpressionType?)null
+ };
+ if (!opr.HasValue)
+ return ToError(pattern);
+
+ return Expression.MakeBinary(opr.Value, expression, right);
+ }
+
+ // not
+ case UnaryPatternSyntax p:
+ {
+ var right = ResolvePattern(expression, p.Pattern);
+ if (right == null)
+ return null;
+
+ return (SyntaxKind)p.OperatorToken.RawKind switch
+ {
+ SyntaxKind.NotKeyword => Expression.Not(right),
+ _ => ToError(p)
+ };
+ }
+
+ // is ()
+ case ParenthesizedPatternSyntax p:
+ {
+ // ReSharper disable once TailRecursiveCall
+ return ResolvePattern(expression, p.Pattern);
+ }
+
+ // is var x
+ case VarPatternSyntax { Designation: SingleVariableDesignationSyntax v }:
+ {
+ var name = v.Identifier.Text;
+ var declareExpression = DeclareVariable(v, expression, name);
+ if (declareExpression == null)
+ return null;
+
+ return Expression.Block(declareExpression, Expression.Constant(true));
+ }
+
+ // is ... x
+ case DeclarationPatternSyntax { Designation: SingleVariableDesignationSyntax v } p:
+ {
+ var type = ResolveType(p.Type);
+ if (type == null)
+ return null;
+
+ var name = v.Identifier.Text;
+ var declareExpression = DeclareVariable(v, Expression.Convert(expression, type), name);
+ if (declareExpression == null)
+ return null;
+
+ return Expression.AndAlso(
+ ToIsOperator(expression, Expression.Constant(type)),
+ Expression.Block(declareExpression, Expression.Constant(true)));
+ }
+
+ // is { } x
+ case RecursivePatternSyntax { PositionalPatternClause: null } p:
+ {
+ Expression checkExpression;
+
+ if (!IsNullableType(expressionType))
+ checkExpression = ToIsNotNull(expression);
+ else
+ {
+ checkExpression = Expression.MakeMemberAccess(expression, expressionType.GetProperty(nameof(Nullable.HasValue))!);
+ expression = Expression.MakeMemberAccess(expression, expressionType.GetProperty(nameof(Nullable.Value))!);
+ }
+
+ // Type
+ if (p.Type != null)
+ {
+ var customType = ResolveType(p.Type);
+ if (customType == null)
+ return null;
+
+ checkExpression = ToIsOperator(expression, Expression.Constant(customType));
+ expression = ToCast(expression, customType);
+ }
+
+ // Properties
+ if (p.PropertyPatternClause?.Subpatterns.Count > 0)
+ {
+ foreach (var property in p.PropertyPatternClause.Subpatterns)
+ {
+ var propertyName = property.NameColon!.Name.Identifier.Text;
+ var propertyValueExpression = ResolveCustomMember(expression, propertyName)
+ ?? ResolveMember(property, expression, propertyName);
+ if (propertyValueExpression == null)
+ return null;
+
+ var condition = ResolvePattern(propertyValueExpression, property.Pattern);
+ if (condition == null)
+ return null;
+
+ checkExpression = Expression.AndAlso(checkExpression, condition);
+ }
+ }
+
+ // Variable
+ if (p.Designation != null && p.Designation is not DiscardDesignationSyntax)
+ {
+ if (p.Designation is not SingleVariableDesignationSyntax v)
+ return ToError(p.Designation);
+
+ var name = v.Identifier.Text;
+ var declareExpression = DeclareVariable(v, Expression.Convert(expression, expression.Type), name);
+ if (declareExpression == null)
+ return null;
+
+ checkExpression = Expression.AndAlso(checkExpression, Expression.Block(declareExpression, Expression.Constant(true)));
+ }
+
+ return checkExpression;
+ }
+
+ // is ... _
+ case DiscardPatternSyntax:
+ return expression;
+
+ // Not supported yet
+ case ListPatternSyntax:
+ case SlicePatternSyntax:
+ return ToError(pattern);
+ }
+
+ return ToError(pattern);
+ }
+
+ public override Expression VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node) => new DelayLambdaExpression(node);
+ public override Expression VisitParenthesizedLambdaExpression(ParenthesizedLambdaExpressionSyntax node) => new DelayLambdaExpression(node);
+
+ public override Expression? VisitNullableType(NullableTypeSyntax node)
+ {
+ var type = ResolveType(node.ElementType);
+ if (type == null)
+ return null;
+
+ if (!type.IsValueType || Nullable.GetUnderlyingType(type) != null)
+ return ToError(node, "Invalid nullable type.");
+
+ return Expression.Constant(typeof(Nullable<>).MakeGenericType(type));
+ }
+ public override Expression? VisitPredefinedType(PredefinedTypeSyntax node)
+ {
+ var type = ResolveType(node);
+ return type != null
+ ? Expression.Constant(type)
+ : null;
+ }
+ public override Expression? VisitThisExpression(ThisExpressionSyntax node)
+ {
+ return _thisParameter ?? ToError(node, "Keyword 'this' is not valid in a static property or method.");
+ }
+ public override Expression? VisitLiteralExpression(LiteralExpressionSyntax node)
+ {
+ if (Equals(node.Token.Value, "default"))
+ {
+ if (_options.ResultType != null)
+ return Expression.Default(_options.ResultType);
+
+ return ToError(node, "Default keyword is not supported.");
+ }
+
+ return Expression.Constant(node.Token.Value);
+ }
+
+ public override Expression? VisitInterpolatedStringExpression(InterpolatedStringExpressionSyntax node)
+ {
+ var format = new StringBuilder();
+ var args = new List();
+
+ foreach (var content in node.Contents)
+ {
+ switch (content)
+ {
+ case InterpolatedStringTextSyntax item:
+ format.Append(item.TextToken.Text);
+ break;
+
+ case InterpolationSyntax item:
+ var expression = Visit(item.Expression);
+ if (expression == null)
+ return null;
+
+ format.Append($"{{{args.Count}{item.FormatClause?.ToString()}}}");
+ args.Add(ToCast(expression, typeof(object)));
+ break;
+
+ default:
+ return ToError(content);
+ }
+ }
+
+ return Expression.Call(
+ null,
+ typeof(string).GetMethod(nameof(string.Format), new[] { typeof(string), typeof(object[]) })!,
+ new Expression[]
+ {
+ Expression.Constant(format.ToString()),
+ Expression.NewArrayInit(typeof(object), args)
+ });
+ }
+ public override Expression? VisitTupleExpression(TupleExpressionSyntax node)
+ {
+ var args = ResolveParameters(node.Arguments);
+ if (args == null)
+ return null;
+
+ var initMethod = typeof(ValueTuple)
+ .GetMethods()
+ .SingleOrDefault(x => x.Name == nameof(ValueTuple.Create) && x.GetParameters().Length == args.Count);
+ if (initMethod == null)
+ return ToError(node);
+
+ return Expression.Call(null, initMethod.MakeGenericMethod(args.Select(x => x!.Type).ToArray()), args);
+ }
+
+ public override Expression? VisitIdentifierName(IdentifierNameSyntax node)
+ {
+ var name = node.Identifier.Text;
+
+ // Variables
+ if (_variables != null)
+ {
+ var (varType, _, varIndex) = _variables.FirstOrDefault(x => x.Name == name);
+ if (varType != null)
+ return Expression.Call(_variableContextParameter!, typeof(LambdaVariableContext).GetMethod(nameof(LambdaVariableContext.GetValue))!.MakeGenericMethod(varType), Expression.Constant(varIndex));
+ }
+
+ // Local parameter
+ var parameter = _nestedParameters?.LastOrDefault(x => x.Name == name && x != _thisParameter);
+ if (parameter != null)
+ return parameter;
+
+ // Parameter
+ parameter = _parameters.FirstOrDefault(x => x.Name == name && x != _thisParameter);
+
+ if (parameter != null)
+ return parameter;
+
+ // This
+ if (_thisParameter != null)
+ {
+ var expression = ResolveCustomMember(_thisParameter, name)
+ ?? ResolveMember(node, _thisParameter, name, false);
+ if (expression != null)
+ return expression;
+ }
+
+ // Static type
+ var type = ResolveType(node, name);
+ if (type != null)
+ return Expression.Constant(type);
+
+ // Unknown
+ return string.IsNullOrEmpty(name)
+ ? ToError(node, "Missing identifier.")
+ : ToError(node, $"Unknown identifier '{name}'.");
+ }
+ private Expression? DeclareVariable(SyntaxNode node, Expression expression, string name)
+ {
+ if (_variables?.Any(x => x.Name == name) == true
+ || _parameters.Any(x => x.Name == name)
+ || _nestedParameters?.Any(x => x.Name == name) == true)
+ {
+ return ToError(node, $"Variable '{name}' is already declared.");
+ }
+
+ var index = _nextVariableIndex++;
+
+ _variableContextParameter ??= Expression.Parameter(typeof(LambdaVariableContext), "__lvc_");
+ _variables ??= new List<(Type, string, int)>();
+ _variables.Add((expression.Type, name, index));
+
+ return Expression.Call(_variableContextParameter, typeof(LambdaVariableContext).GetMethod(nameof(LambdaVariableContext.SetValue))!, Expression.Constant(index), ToCast(expression, typeof(object)));
+ }
+
+ private Type? ResolveType(TypeSyntax type)
+ {
+ switch (type)
+ {
+ case NullableTypeSyntax nts:
+ {
+ var typeArgument = ResolveType(nts.ElementType);
+ return typeArgument != null
+ ? typeof(Nullable<>).MakeGenericType(typeArgument)
+ : null;
+ }
+
+ case PredefinedTypeSyntax pts:
+ return (SyntaxKind)pts.Keyword.RawKind switch
+ {
+ SyntaxKind.BoolKeyword => typeof(bool),
+ SyntaxKind.ByteKeyword => typeof(byte),
+ SyntaxKind.SByteKeyword => typeof(sbyte),
+ SyntaxKind.ShortKeyword => typeof(short),
+ SyntaxKind.UShortKeyword => typeof(ushort),
+ SyntaxKind.IntKeyword => typeof(int),
+ SyntaxKind.UIntKeyword => typeof(uint),
+ SyntaxKind.LongKeyword => typeof(long),
+ SyntaxKind.ULongKeyword => typeof(ulong),
+ SyntaxKind.DoubleKeyword => typeof(double),
+ SyntaxKind.FloatKeyword => typeof(float),
+ SyntaxKind.DecimalKeyword => typeof(decimal),
+ SyntaxKind.StringKeyword => typeof(string),
+ SyntaxKind.CharKeyword => typeof(char),
+ SyntaxKind.VoidKeyword => typeof(void),
+ SyntaxKind.ObjectKeyword => typeof(object),
+ _ => ToTypeError(type, null)
+ };
+
+ case ArrayTypeSyntax arrayType:
+ {
+ var elementType = ResolveType(arrayType.ElementType);
+ if (elementType == null)
+ return null;
+
+ if (arrayType.RankSpecifiers.Count == 0)
+ return elementType.MakeArrayType();
+
+ return ToTypeError(type, null);
+ }
+
+ case GenericNameSyntax genericName:
+ {
+ var elements = new List();
+ foreach (var item in genericName.TypeArgumentList.Arguments)
+ {
+ var elementType = ResolveType(item);
+ if (elementType == null)
+ return null;
+
+ elements.Add(elementType);
+ }
+
+ var genericType = ResolveType(type, genericName.Identifier.Text, elements.Count);
+ return genericType?.MakeGenericType(elements.ToArray());
+ }
+
+ case QualifiedNameSyntax { Right: IdentifierNameSyntax id } name:
+ {
+ var t = ResolveType(name, id.Identifier.Text);
+ if (t != null && t.Namespace != name.Left.ToString())
+ return ToTypeError(type, null);
+
+ return t;
+ }
+
+ case IdentifierNameSyntax name:
+ return ResolveType(name, name.Identifier.Text);
+
+ default:
+ return ToTypeError(type, null);
+ }
+ }
+ private Type? ResolveType(SyntaxNode relatedNode, string typeName, int genericArguments = 0)
+ {
+ if (genericArguments > 0)
+ typeName += "'" + genericArguments;
+
+ if (_options.ResultType is { } resultType && resultType != typeof(object))
+ {
+ resultType = Nullable.GetUnderlyingType(resultType) ?? resultType;
+ if (resultType.Name == typeName)
+ return resultType;
+ }
+
+ if (_options.IncludedTypesMap.TryGetValue(typeName, out var type) && type != null)
+ return type;
+
+ foreach (var parameter in _parameters)
+ if (parameter.Type.Name == typeName)
+ return parameter.Type;
+
+ return typeName switch
+ {
+ "TimeSpan" => typeof(TimeSpan),
+ "DateTime" => typeof(DateTime),
+ "DateTimeKind" => typeof(DateTimeKind),
+ "DayOfWeek" => typeof(DayOfWeek),
+ "StringComparison" => typeof(StringComparison),
+ "StringSplitOptions" => typeof(StringSplitOptions),
+
+ "List'1" => typeof(List<>),
+ "IList'1" => typeof(IList<>),
+ "IEnumerable'1" => typeof(IEnumerable<>),
+
+ "CultureInfo" => typeof(CultureInfo),
+ //"Type" => typeof(Type),
+
+ "Math" => typeof(Math),
+ "Enumerable" => typeof(Enumerable),
+
+ _ => ToTypeError(relatedNode, typeName)
+ };
+ }
+ private Expression? ResolveMember(SyntaxNode node, Expression expression, string name, bool setErrorWhenNotFound = true)
+ {
+ var staticType = (expression as ConstantExpression)?.Value as Type;
+ var expressionType = staticType ?? expression.Type;
+
+ // From instance
+ for (var type = expressionType; type != null; type = type.BaseType)
+ {
+ var members = type.GetMember(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Public);
+ switch (members.Length)
+ {
+ case 1:
+ return Expression.MakeMemberAccess(staticType != null ? null : expression, members[0]);
+ case > 1:
+ return setErrorWhenNotFound ? ToError(node, $"More then one member with name {name}.") : null;
+ }
+
+ if (type == typeof(object))
+ break;
+ }
+
+ // From interface
+ if (staticType == null)
+ {
+ foreach (var interfaceType in expressionType.GetInterfaces())
+ {
+ var members = interfaceType.GetMember(name, BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public);
+ switch (members.Length)
+ {
+ case 1:
+ return Expression.MakeMemberAccess(expression, members[0]);
+ case > 1:
+ return setErrorWhenNotFound ? ToError(node, $"More then one member with name {name}.") : null;
+ }
+ }
+ }
+
+ return setErrorWhenNotFound ? ToError(node, $"Unknown member {name}.") : null;
+ }
+ private Expression? ResolveCustomMember(Expression expression, string name)
+ {
+ var resolver = _options.CustomPropertyResolver;
+ if (resolver == null)
+ return null;
+
+ _resolverContext ??= new MemberResolverContext(this);
+ _resolverContext.Switch(_extensionInstance, expression, name);
+
+ return resolver(_resolverContext);
+ }
+
+ private Expression? TryResolveLambda(LambdaExpressionSyntax node, Type[] parameterTypes, Type? resultType)
+ {
+ var simple = node as SimpleLambdaExpressionSyntax;
+ var parenthesized = node as ParenthesizedLambdaExpressionSyntax;
+ var parametersSyntax = simple != null
+ ? new[] { simple.Parameter }
+ : parenthesized!.ParameterList.Parameters.ToArray();
+ if (parametersSyntax.Length != parameterTypes.Length)
+ return null;
+
+ var nestedParametersStartIndex = -1;
+ var nestedVariableStartIndex = -1;
+ ParameterExpression[]? parameters = null;
+
+ _nestedParameters ??= new List();
+ try
+ {
+ // Parameters
+ parameters = new ParameterExpression[parameterTypes.Length];
+ for (var i = 0; i < parameters.Length; i++)
+ {
+ var name = parametersSyntax[i].Identifier.Text;
+ if (string.IsNullOrEmpty(name))
+ return null;
+
+ if (parameterTypes[i].IsGenericParameter) // Generic is not supported
+ return null;
+
+ parameters[i] = Expression.Parameter(parameterTypes[i], name);
+ }
+
+ nestedParametersStartIndex = _nestedParameters.Count;
+ nestedVariableStartIndex = _nextVariableIndex;
+ _nestedParameters.AddRange(parameters);
+
+ // Body
+ var bodyNode = simple?.Block ?? (SyntaxNode?)parenthesized?.Block
+ ?? simple?.ExpressionBody ?? parenthesized?.ExpressionBody;
+ var body = Visit(bodyNode);
+ if (body == null)
+ return null;
+
+ // Result
+ return Expression.Lambda(body, parameters);
+ }
+ finally
+ {
+ if (parameters != null && nestedParametersStartIndex >= 0)
+ for (var i = nestedParametersStartIndex + parameters.Length - 1; i >= nestedParametersStartIndex; i--)
+ _nestedParameters.RemoveAt(i);
+
+ if (_variables != null)
+ while (_variables.Count > 0 && _variables[_variables.Count - 1].Index >= nestedVariableStartIndex)
+ _variables.RemoveAt(_variables.Count - 1);
+ }
+ }
+ private IList? ResolveParameters(IEnumerable? node)
+ {
+ if (node == null)
+ return Array.Empty();
+
+ var parameters = new List();
+
+ foreach (var arg in node)
+ {
+ var argExpression = Visit(arg);
+ if (argExpression == null)
+ return null;
+
+ parameters.Add(argExpression);
+ }
+
+ return parameters;
+ }
+ private Expression? ResolveItemCall(SyntaxNode relatedNode, Expression instanceExpression, IList parameters)
+ {
+ var instanceType = instanceExpression.Type;
+ if (instanceType.IsArray)
+ return Expression.ArrayAccess(instanceExpression, parameters);
+
+ // Select method override
+ var methods = GetIndexers(instanceType);
+
+ if (methods.Count > 0 && TryResolveMethodCall(relatedNode, instanceExpression, parameters, methods, out var expression))
+ return expression;
+
+ return ToError(relatedNode, "Indexer not found for this arguments.");
+ }
+ private Expression? ResolveCustomKeywords(SyntaxNode node, string methodName, IList parameters)
+ {
+ // ReSharper disable StringLiteralTypo
+ if (methodName is not ("typeis" or "typeas" or "typecast")
+ || parameters.Count != 2
+ || parameters[0] is not ConstantExpression { Value: string typeName })
+ {
+ return null;
+ }
+
+ var runtimeType = Type.GetType(typeName);
+ if (runtimeType == null)
+ return ToError(node, $"Runtime type '{typeName}' not found.");
+
+ var expression = parameters[1];
+
+ return methodName switch
+ {
+ "typeis" => ToIsOperator(expression, Expression.Constant(runtimeType)),
+ "typeas" => ToAsOperator(node, expression, Expression.Constant(runtimeType)),
+ "typecast" => ToCastOperator(node, expression, Expression.Constant(runtimeType), false),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ // ReSharper restore StringLiteralTypo
+ }
+
+ private bool EnsureTheSameTypes(SyntaxNode node, ref Expression e1, ref Expression e2)
+ {
+ // Null mismatch
+ if (e1 is ConstantExpression c && c.Type == typeof(object) && c.Value == null)
+ {
+ var type = e2.Type;
+ if (type.IsValueType && !IsNullableType(type))
+ type = typeof(Nullable<>).MakeGenericType(type);
+ e1 = Expression.Convert(e1, type);
+ }
+ else if (e2 is ConstantExpression c2 && c2.Type == typeof(object) && c2.Value == null)
+ {
+ var type = e1.Type;
+ if (type.IsValueType && !IsNullableType(type))
+ type = typeof(Nullable<>).MakeGenericType(type);
+ e2 = Expression.Convert(e2, type);
+ }
+
+ // Nullable mismatch
+ var t1 = e1.Type;
+ var t2 = e2.Type;
+
+ if (t1 == t2)
+ return true;
+
+ if (Nullable.GetUnderlyingType(t1) != null)
+ {
+ if (Nullable.GetUnderlyingType(t2) == null && t2.IsValueType)
+ e2 = Expression.Convert(e2, typeof(Nullable<>).MakeGenericType(t2));
+ }
+ else if (Nullable.GetUnderlyingType(t2) != null && t1.IsValueType)
+ e1 = Expression.Convert(e1, typeof(Nullable<>).MakeGenericType(t1));
+
+ // Numeric mismatch
+ if (TypeUtils.IsNumericType(t1) && TypeUtils.IsNumericType(t2))
+ {
+ var c1 = Type.GetTypeCode(t1);
+ var c2 = Type.GetTypeCode(t2);
+
+ if (c1 is TypeCode.Decimal or TypeCode.Double && c2 is TypeCode.Decimal or TypeCode.Double)
+ {
+ ToError(node, "Can not apply operator to decimal and double types.");
+ return false;
+ }
+
+ if (c1 > c2)
+ e2 = ToCast(e2, t1);
+ else
+ e1 = ToCast(e1, t2);
+ }
+
+ // Cast
+ if (t1.IsAssignableFrom(t2))
+ e2 = ToCast(e2, t1);
+ else if (t2.IsAssignableFrom(t1))
+ e1 = ToCast(e1, t2);
+
+ return true;
+ }
+ private bool EnsureArgumentType(SyntaxNode node, Type parameterType, ref Expression argument)
+ {
+ var argumentType = argument.Type;
+ if (parameterType == argumentType)
+ return true;
+
+ // Numeric mismatch
+ if (TypeUtils.IsNumericType(argumentType) && TypeUtils.IsNumericType(parameterType))
+ {
+ var argumentCode = Type.GetTypeCode(argumentType);
+ var parameterCode = Type.GetTypeCode(parameterType);
+
+ if (argumentCode is TypeCode.Decimal or TypeCode.Double && parameterCode is TypeCode.Decimal or TypeCode.Double)
+ return false;
+
+ if (argumentCode < parameterCode)
+ {
+ argument = ToCast(argument, parameterType);
+ return true;
+ }
+
+ return false;
+ }
+
+ // Missing type cast
+ if (parameterType.IsAssignableFrom(argumentType))
+ {
+ argument = ToCast(argument, parameterType);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void Push(Expression expression) => _tmp = expression;
+ private Expression? Pop(SyntaxNode node)
+ {
+ var r = _tmp;
+ _tmp = null;
+ return r ?? ToError(node, "Invalid syntax.");
+ }
+ private Expression? TryPop()
+ {
+ var r = _tmp;
+ _tmp = null;
+ return r;
+ }
+
+ private Expression? ToError(SyntaxNode node, string? message = null)
+ {
+ var code = node.ToFullString();
+ if (string.IsNullOrEmpty(code))
+ {
+ node = node.Parent!;
+ code = node.ToFullString();
+ }
+
+ var location = node.GetLocation().GetLineSpan().StartLinePosition;
+
+ FirstError = $"{message ?? $"Unsupported expression of type {node.GetType().Name}."}{Environment.NewLine}at ({location}): {code}";
+ return null;
+ }
+ private Type? ToTypeError(SyntaxNode node, string? typeName)
+ {
+ FirstError = $"Unknown type '{typeName ?? node.ToString()}'.";
+ return null;
+ }
+
+ private static Expression ToIsNotNull(Expression expression)
+ {
+ if (Nullable.GetUnderlyingType(expression.Type) is { })
+ return Expression.MakeMemberAccess(expression, expression.Type.GetProperty("HasValue")!);
+
+ if (expression.Type.IsValueType)
+ return Expression.Constant(true);
+
+ return Expression.NotEqual(expression, Expression.Constant(null, expression.Type));
+ }
+ private static Expression ToCast(Expression expression, Type type) => expression.Type != type ? Expression.Convert(expression, type) : expression;
+ private static Expression CallWhenNotNull(Expression instance, MethodInfo method)
+ {
+ if (Nullable.GetUnderlyingType(instance.Type) is { })
+ return new ConditionalAccessExpression(instance, Expression.Call(Expression.MakeMemberAccess(instance, instance.Type.GetProperty("Value")!), method));
+
+ return instance.Type.IsValueType
+ ? Expression.Call(instance, method)
+ : new ConditionalAccessExpression(instance, Expression.Call(instance, method));
+ }
+ private static Expression ToIsOperator(Expression left, Expression right)
+ {
+ var expressionType = left.Type;
+
+ if (expressionType.IsValueType && !IsNullableType(expressionType))
+ {
+ var castType = (Type)((ConstantExpression)right).Value;
+ castType = Nullable.GetUnderlyingType(castType) ?? castType;
+ expressionType = Nullable.GetUnderlyingType(expressionType) ?? expressionType;
+
+ return Expression.MakeBinary(ExpressionType.Equal, Expression.Constant(expressionType), Expression.Constant(castType));
+ }
+
+ var leftType = Expression.Call(left, typeof(object).GetMethod("GetType")!);
+
+ return Expression.AndAlso(
+ ToIsNotNull(left),
+ Expression.Call(right, typeof(Type).GetMethod("IsAssignableFrom")!, leftType));
+ }
+ private Expression? ToAsOperator(SyntaxNode node, Expression left, Expression right)
+ {
+ var castOperation = ToCastOperator(node, left, right, true);
+ if (castOperation == null)
+ return null;
+
+ var condition = ToIsOperator(left, right);
+
+ return Expression.Condition(condition, castOperation, Expression.Constant(null, castOperation.Type));
+ }
+ private Expression? ToCastOperator(SyntaxNode node, Expression left, Expression right, bool usedByAsOperator)
+ {
+ var castType = (Type)((ConstantExpression)right).Value;
+ var expressionType = left.Type;
+
+ if (usedByAsOperator && castType.IsValueType && !IsNullableType(castType))
+ return ToError(node, "The as operator must be used with a reference type or nullable type");
+
+ if (castType.IsAssignableFrom(expressionType) || expressionType.IsAssignableFrom(castType))
+ return Expression.Convert(left, castType);
+
+ return ToError(node, $"Cannot convert value type '{left.Type}' to '{right.Type}' using build-in conversion.");
+ }
+
+ private static IList GetIndexers(Type instanceType) => GetMethods(instanceType, "get_Item", BindingFlags.Instance);
+ private static IList GetMethods(Type instanceType, string name, BindingFlags additionalFlags)
+ {
+ var members = new List();
+ var names = new HashSet();
+
+ for (var type = instanceType; type != null; type = type.BaseType)
+ {
+ var nextMembers = type.GetMethods(BindingFlags.Public | additionalFlags);
+
+ // ReSharper disable once ForCanBeConvertedToForeach
+ // ReSharper disable once LoopCanBeConvertedToQuery
+ for (var i = 0; i < nextMembers.Length; i++)
+ {
+ var item = nextMembers[i];
+ if (item.Name == name && names.Add(item.ToString()))
+ members.Add(item);
+ }
+
+ if (type == typeof(object))
+ break;
+ }
+
+ foreach (var type in instanceType.GetInterfaces())
+ {
+ var nextMembers = type.GetMethods(BindingFlags.Public | additionalFlags);
+
+ // ReSharper disable once ForCanBeConvertedToForeach
+ // ReSharper disable once LoopCanBeConvertedToQuery
+ for (var i = 0; i < nextMembers.Length; i++)
+ {
+ var item = nextMembers[i];
+ if (item.Name == name && names.Add(item.ToString()))
+ members.Add(item);
+ }
+ }
+
+ return members;
+ }
+ private static IList GetExtensionMethods(Type instanceType, string name)
+ {
+ var members = new List();
+
+ // Extensions
+ if (TypeUtils.ContainsGenericDefinition(instanceType, typeof(IEnumerable<>)))
+ {
+ var nextMembers = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static);
+
+ // ReSharper disable once ForCanBeConvertedToForeach
+ // ReSharper disable once LoopCanBeConvertedToQuery
+ for (var i = 0; i < nextMembers.Length; i++)
+ {
+ var item = nextMembers[i];
+ if (item.Name == name && item.GetCustomAttribute() != null)
+ {
+ var thisParameter = item.GetParameters().FirstOrDefault();
+ if (thisParameter == null)
+ continue;
+
+ if (!IsMatchingParameterType(thisParameter.ParameterType, instanceType))
+ continue;
+
+ members.Add(item);
+ }
+ }
+ }
+
+ return members;
+ }
+ private static MemberInfo? GetAssignMember(Type type, string name)
+ {
+ for (; type != null!; type = type.BaseType!)
+ {
+ var member = (MemberInfo?)type.GetProperty(name) ?? type.GetField(name);
+ if (member != null)
+ return member;
+ }
+
+ return null;
+ }
+ private static bool TryExtractGenericArguments(Type parameterWithGeneric, Type argumentType, IList<(string, Type)>? argumentTypes)
+ {
+ if (parameterWithGeneric.IsGenericParameter)
+ {
+ argumentTypes?.Add((parameterWithGeneric.Name, argumentType));
+ return true;
+ }
+
+ if (!parameterWithGeneric.IsGenericType)
+ return parameterWithGeneric == argumentType;
+
+ if (parameterWithGeneric.IsInterface)
+ {
+ var definition = parameterWithGeneric.IsGenericTypeDefinition
+ ? parameterWithGeneric
+ : parameterWithGeneric.GetGenericTypeDefinition();
+
+ if (argumentType != definition && (argumentType.IsGenericType && argumentType.GetGenericTypeDefinition() != definition || argumentType.IsArray))
+ {
+ var interfaceType = argumentType.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == definition);
+ if (interfaceType == null)
+ return false;
+
+ argumentType = interfaceType;
+ }
+ }
+ else if (parameterWithGeneric.IsClass)
+ {
+ while (argumentType.Name != parameterWithGeneric.Name && argumentType != typeof(object))
+ argumentType = argumentType.BaseType!;
+
+ if (argumentType == typeof(object))
+ return false;
+ }
+
+ if (parameterWithGeneric.Name != argumentType.Name)
+ return argumentType.BaseType is { } baseType && TryExtractGenericArguments(parameterWithGeneric, baseType, argumentTypes);
+
+ var pp = parameterWithGeneric.GetGenericArguments();
+ var ap = argumentType.GetGenericArguments();
+ var result = true;
+
+ for (var i = 0; i < pp.Length; i++)
+ result &= TryExtractGenericArguments(pp[i], ap[i], argumentTypes);
+
+ return result;
+ }
+
+ private bool TryResolveMethodCall(SyntaxNode relatedNode, Expression? instanceExpression, IList arguments, IEnumerable candidates, out Expression? expression)
+ {
+ expression = null;
+
+ // Select best method
+ MethodCallInfo? bestMethod = null;
+ List? ambiguous = null;
+
+ foreach (var item in candidates)
+ {
+ var info = ToMatchingMethod(item);
+ if (info == null || !HasMatchingParameters(info.Parameters, info.Arguments))
+ continue;
+
+ if (bestMethod == null)
+ {
+ bestMethod = info;
+ continue;
+ }
+
+ var newBestMethod = GetBestMatchingMethod(bestMethod, info);
+ if (newBestMethod == null)
+ {
+ ambiguous ??= new List();
+ ambiguous.Add(info);
+ ambiguous.Add(bestMethod);
+ }
+
+ bestMethod = newBestMethod;
+ }
+
+ if (ambiguous?.Count > 0)
+ {
+ if (bestMethod == null || ambiguous.Any(x => GetBestMatchingMethod(bestMethod, x) != bestMethod))
+ {
+ expression = ToError(relatedNode, "Ambiguous method call.");
+ return true;
+ }
+ }
+
+ if (bestMethod == null)
+ return false;
+
+ expression = Expression.Call(instanceExpression, bestMethod.Method, bestMethod.Arguments);
+ return true;
+
+ MethodCallInfo? ToMatchingMethod(MethodInfo x)
+ {
+ var info = new MethodCallInfo(x, arguments);
+ ref var method = ref info.Method;
+ var methodParameters = info.Parameters;
+ var methodArguments = info.Arguments;
+
+ if (methodParameters.Length < methodArguments.Count)
+ return null;
+
+ // Try extract arguments
+ Type[]? genericParameters = null;
+ Type?[]? genericArguments = null;
+
+ if (method.IsGenericMethodDefinition)
+ {
+ genericParameters = method.GetGenericArguments();
+ genericArguments = new Type[genericParameters.Length];
+
+ var argumentTypes = new List<(string, Type)>();
+
+ for (var i = 0; i < methodArguments.Count; i++)
+ if (methodArguments[i] is not DelayLambdaExpression)
+ TryExtractGenericArguments(methodParameters[i].ParameterType, methodArguments[i].Type, argumentTypes);
+
+ for (var i = 0; i < genericParameters.Length; i++)
+ {
+ var type = genericParameters[i];
+ var fullType = argumentTypes.FirstOrDefault(y => y.Item1 == type.Name).Item2;
+ genericArguments[i] = fullType;
+ }
+ }
+
+ if (method.IsGenericMethodDefinition && genericArguments!.All(y => y != null))
+ {
+ method = method.MakeGenericMethod(genericArguments!);
+ methodParameters = method.GetParameters();
+ }
+
+ // Parse lambda
+ for (var i = 0; i < methodArguments.Count; i++)
+ if (methodArguments[i] is DelayLambdaExpression dl)
+ {
+ // Resolve parameters
+ var lambdaType = methodParameters[i].ParameterType;
+ if (!lambdaType.IsGenericType)
+ return null;
+
+ Type? resultType = null;
+ var lambdaParameters = lambdaType.GetGenericArguments();
+
+ for (var j = 0; j < lambdaParameters.Length; j++)
+ {
+ var item = lambdaParameters[j];
+ if (item.IsGenericParameter)
+ {
+ for (var k = 0; k < genericParameters!.Length; k++)
+ if (genericParameters[k].Name == item.Name)
+ {
+ if (genericArguments![k] != null)
+ lambdaParameters[j] = genericArguments[k]!;
+ break;
+ }
+ }
+ }
+
+ // var invokeMethod = lambdaType.GetMethod("Invoke")!;
+ if (lambdaType.Name.StartsWith("Func`"))
+ {
+ resultType = lambdaParameters.Last();
+ lambdaParameters = lambdaParameters.Take(lambdaParameters.Length - 1).ToArray();
+ }
+
+ // Expression
+ var expression = TryResolveLambda(dl.Node, lambdaParameters, resultType);
+ if (expression == null)
+ return null;
+
+ methodArguments[i] = expression;
+
+ // Extract generics
+ if (method.IsGenericMethodDefinition)
+ {
+ var argumentTypes = new List<(string, Type)>();
+ TryExtractGenericArguments(methodParameters[i].ParameterType, methodArguments[i].Type, argumentTypes);
+
+ for (var j = 0; j < genericArguments!.Length; j++)
+ {
+ if (genericArguments[j] != null)
+ continue;
+
+ var type = genericParameters![i];
+ var fullType = argumentTypes.FirstOrDefault(y => y.Item1 == type.Name).Item2;
+ genericArguments[j] = fullType;
+ }
+ }
+ }
+
+
+ // Make
+ if (method.IsGenericMethodDefinition)
+ {
+ if (genericArguments!.Any(y => y == null))
+ return null;
+
+ method = method.MakeGenericMethod(genericArguments!);
+ methodParameters = method.GetParameters();
+ }
+
+ // Cast method arguments
+ for (var i = 0; i < methodArguments.Count; i++)
+ {
+ var parameterType = methodParameters[i].ParameterType;
+ var argumentType = methodArguments[i].Type;
+
+ if (parameterType != argumentType)
+ {
+ var argument = methodArguments[i];
+ if (!EnsureArgumentType(relatedNode, parameterType, ref argument))
+ return null;
+
+ methodArguments[i] = argument;
+ }
+ }
+
+ // Default arguments
+ for (var i = methodArguments.Count; i < methodParameters.Length; i++)
+ {
+ if (!methodParameters[i].HasDefaultValue)
+ return null;
+
+ var defaultValue = methodParameters[i].DefaultValue;
+ var parameterType = methodParameters[i].ParameterType;
+
+ if (defaultValue == null && parameterType.IsValueType)
+ defaultValue = Activator.CreateInstance(parameterType);
+
+ methodArguments.Add(Expression.Constant(defaultValue, parameterType));
+ }
+
+ return info;
+ }
+ }
+ private static MethodCallInfo? GetBestMatchingMethod(MethodCallInfo method1, MethodCallInfo method2)
+ {
+ var args = method1.RawArguments;
+
+ for (var i = 0; i < args.Count; i++)
+ {
+ var argType = args[i].Type;
+ var t1 = method1.Parameters[method1.HasParams ? Math.Min(i, method1.Parameters.Length - 1) : i].ParameterType;
+ var t2 = method2.Parameters[method2.HasParams ? Math.Min(i, method2.Parameters.Length - 1) : i].ParameterType;
+
+ if (GetBestMatchingType(argType, t1, t2) is { } best)
+ return best == t1 ? method1 : method2;
+ }
+
+ if (method1.Method.IsGenericMethod.CompareTo(method2.Method.IsGenericMethod) is var genericDiff && genericDiff != 0)
+ return genericDiff < 0 ? method1 : method2;
+
+ if (method1.HasParams.CompareTo(method2.HasParams) is var paramsDiff && paramsDiff != 0)
+ return paramsDiff < 0 ? method1 : method2;
+
+ if (method1.Parameters.Length.CompareTo(method2.Parameters.Length) is var parametersCountDiff && parametersCountDiff != 0)
+ return parametersCountDiff > 0 ? method1 : method2;
+
+ return null;
+ }
+ private static Type? GetBestMatchingType(Type argType, Type type1, Type type2)
+ {
+ if (type1 == type2)
+ return null;
+
+ if (argType == type1 || argType == type2)
+ return argType;
+
+ // Assignable
+ var a1 = type1.IsAssignableFrom(argType);
+ var a2 = type2.IsAssignableFrom(argType);
+
+ if (a1 && !a2)
+ return type1;
+ if (a2 && !a1)
+ return type2;
+
+ // Matching
+ var m1 = IsMatchingParameterType(type2, type1);
+ var m2 = IsMatchingParameterType(type1, type2);
+
+ if (m1 && !m2)
+ return type1;
+ if (m2 && !m1)
+ return type2;
+
+ // Sign
+ var s1 = IsUnsignedType(type1);
+ var s2 = IsUnsignedType(type2);
+
+ if (s1 == false && s2 == true)
+ return type1;
+ if (s2 == false && s1 == true)
+ return type2;
+
+ return null;
+ }
+
+ private static bool IsNullableType(Type type) => Nullable.GetUnderlyingType(type) != null;
+ private static bool HasMatchingParameters(IList parameters, IList arguments)
+ {
+ if (parameters.Count < arguments.Count)
+ return false;
+
+ for (var i = 0; i < arguments.Count; i++)
+ {
+ var ept = arguments[i].Type;
+ var mpt = parameters[i].ParameterType;
+
+ if (!IsMatchingParameterType(mpt, ept))
+ return false;
+ }
+
+ return true;
+ }
+ private static bool IsMatchingParameterType(Type parameterType, Type argumentType)
+ {
+ if (!parameterType.IsAssignableFrom(argumentType))
+ {
+ // Numeric mismatch
+ if (TypeUtils.IsNumericType(argumentType) && TypeUtils.IsNumericType(parameterType))
+ {
+ var argumentCode = Type.GetTypeCode(argumentType);
+ var parameterCode = Type.GetTypeCode(parameterType);
+
+ if (argumentCode is TypeCode.Decimal or TypeCode.Double && parameterCode is TypeCode.Decimal or TypeCode.Double)
+ return false;
+
+ return argumentCode < parameterCode;
+ }
+
+ // Generic
+ return TryExtractGenericArguments(parameterType, argumentType, null);
+ }
+
+ return true;
+ }
+ private static bool? IsUnsignedType(Type type)
+ {
+ var code = Type.GetTypeCode(type);
+ return code is >= TypeCode.Byte and <= TypeCode.UInt64
+ ? code is TypeCode.Byte or TypeCode.UInt16 or TypeCode.UInt32 or TypeCode.UInt64
+ : null;
+ }
+
+ private static Expression WrapWithTypeInfo(Expression expression, object typeInfo)
+ {
+ s_typeInfoWrapper ??= typeof(ExpressionBuilder).GetMethod(nameof(TypeInfoWrapper), BindingFlags.Static | BindingFlags.NonPublic)!;
+ var method = s_typeInfoWrapper.MakeGenericMethod(expression.Type);
+ return Expression.Call(method, expression, Expression.Constant(typeInfo));
+ }
+ private static object? ExtractTypeInfo(Expression? expression)
+ {
+ if (expression is MethodCallExpression { Method.IsGenericMethod: true } mc && mc.Method.GetGenericMethodDefinition() == s_typeInfoWrapper)
+ return ((ConstantExpression)mc.Arguments[1]).Value;
+
+ return null;
+ }
+ private static T TypeInfoWrapper(T value, object typeInfo) => value;
+
+ private class MethodCallInfo
+ {
+ public MethodInfo Method;
+
+ public ParameterInfo[] Parameters { get; }
+ public bool HasParams { get; }
+
+ public IList RawArguments { get; }
+ public List Arguments { get; }
+
+ public MethodCallInfo(MethodInfo method, IList rawArguments)
+ {
+ Method = method;
+ Parameters = method.GetParameters();
+ HasParams = Parameters.Length > 0 && Parameters[Parameters.Length - 1].IsDefined(typeof(ParamArrayAttribute), false);
+
+ RawArguments = rawArguments;
+ Arguments = rawArguments.ToList();
+ }
+
+
+ public override string ToString() => Method.ToString();
+ }
+ private class MemberResolverContext : IExpressionMemberResolverContext
+ {
+ private readonly ExpressionBuilder _visitor;
+ private string? _memberFullPath;
+
+ public Expression Instance { get; private set; } = null!;
+ public object? InstanceTypeInfo { get; private set; }
+
+ public string MemberName { get; private set; } = null!;
+ public string? MemberFullPath
+ {
+ get
+ {
+ if (_memberFullPath == null)
+ {
+ var names = new List(2) { MemberName };
+ var isOk = true;
+ var i = Instance;
+
+ while (isOk)
+ {
+ if (i is ParameterExpression p)
+ {
+ names.Add(p.Name);
+ break;
+ }
+
+ if (i is MemberExpression ma)
+ {
+ names.Add(ma.Member.Name);
+ i = ma.Expression;
+ continue;
+ }
+
+ isOk = false;
+ }
+
+ if (isOk)
+ names.Reverse();
+
+ _memberFullPath = isOk
+ ? string.Join(".", names)
+ : string.Empty;
+ }
+
+ return _memberFullPath != string.Empty
+ ? _memberFullPath
+ : null;
+ }
+ }
+
+ public MemberResolverContext(ExpressionBuilder visitor) => _visitor = visitor;
+
+
+ public void Switch(Expression? expressionInstance, Expression instance, string memberName)
+ {
+ Instance = instance;
+ InstanceTypeInfo = ExtractTypeInfo(instance) ?? (instance is ParameterExpression ? ExtractTypeInfo(expressionInstance) : null);
+
+ MemberName = memberName;
+ _memberFullPath = null;
+ }
+
+ public ParameterExpression GetParameter(string name) => _visitor._parameters.First(x => x.Name == name);
+ public Expression IncludeTypeInfo(Expression expression, object typeInfo) => WrapWithTypeInfo(expression, typeInfo);
+ }
+ private class DelayLambdaExpression : Expression
+ {
+ public LambdaExpressionSyntax Node { get; }
+
+ public DelayLambdaExpression(LambdaExpressionSyntax node)
+ {
+ Node = node;
+ }
+ }
+ private class ConditionalAccessExpression : Expression
+ {
+ private readonly Type _type;
+
+ private Expression Instance { get; }
+ private Expression Member { get; }
+
+ public override Type Type => _type;
+ public override bool CanReduce => true;
+ public override ExpressionType NodeType => ExpressionType.Extension;
+
+ public ConditionalAccessExpression(Expression instance, Expression member)
+ {
+ Instance = instance;
+ Member = member;
+
+ _type = member.Type;
+
+ if (_type.IsValueType && Nullable.GetUnderlyingType(_type) == null)
+ _type = typeof(Nullable<>).MakeGenericType(_type);
+ }
+
+
+ public override Expression Reduce()
+ {
+ var member = Member;
+ if (_type != member.Type)
+ member = Convert(Member, _type);
+
+ return Condition(
+ ToIsNotNull(Instance),
+ member,
+ Constant(null, _type));
+ }
+ public override string ToString()
+ {
+ var instance = Instance.ToString();
+ var member = Member.ToString();
+
+ if (member.StartsWith(instance + "."))
+ return $"{instance}?{member.Substring(instance.Length)}";
+
+ return $"({instance != null} ? {member} : default)";
+ }
+ }
+
+ private class LambdaVariableContext
+ {
+ private readonly object[] _values;
+
+ public LambdaVariableContext(int count) => _values = new object[count];
+
+
+ public T GetValue(int index) => (T)_values[index];
+ public void SetValue(int index, object value) => _values[index] = value;
+ }
+}
diff --git a/src/TagBites.Expressions/Expressions/Visitors/IdentifierDetector.cs b/src/TagBites.Expressions/Expressions/Visitors/IdentifierDetector.cs
new file mode 100644
index 0000000..a701266
--- /dev/null
+++ b/src/TagBites.Expressions/Expressions/Visitors/IdentifierDetector.cs
@@ -0,0 +1,31 @@
+using System.Linq.Expressions;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace TagBites.Expressions;
+
+internal class IdentifierDetector : ExpressionBuilder
+{
+ public IList Identifiers { get; } = new List();
+ public IList UnknownIdentifiers { get; } = new List();
+
+ public IdentifierDetector(ExpressionParserOptions options)
+ : base(options)
+ { }
+
+
+ public override Expression VisitIdentifierName(IdentifierNameSyntax node)
+ {
+ var name = node.Identifier.Text;
+ var result = base.VisitIdentifierName(node);
+
+ if (result != null)
+ Identifiers.Add(name);
+ else
+ {
+ UnknownIdentifiers.Add(name);
+ result = Expression.Constant(null, typeof(object));
+ }
+
+ return result;
+ }
+}
diff --git a/src/TagBites.Expressions/Expressions/Visitors/ReflectionDetector.cs b/src/TagBites.Expressions/Expressions/Visitors/ReflectionDetector.cs
new file mode 100644
index 0000000..8147db5
--- /dev/null
+++ b/src/TagBites.Expressions/Expressions/Visitors/ReflectionDetector.cs
@@ -0,0 +1,28 @@
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace TagBites.Expressions;
+
+internal class ReflectionDetector : ExpressionVisitor
+{
+ public bool HasReflectionCall { get; private set; }
+
+
+ protected override Expression VisitMethodCall(MethodCallExpression node)
+ {
+ HasReflectionCall |= node.Object != null
+ && (typeof(Type).IsAssignableFrom(node.Object.Type) || typeof(MemberInfo).IsAssignableFrom(node.Object.Type))
+ && node.Method.Name != nameof(Type.IsAssignableFrom);
+
+ return base.VisitMethodCall(node);
+ }
+
+ protected override Expression VisitMember(MemberExpression node)
+ {
+ HasReflectionCall |= (typeof(Type).IsAssignableFrom(node.Member.DeclaringType) || typeof(MemberInfo).IsAssignableFrom(node.Member.DeclaringType))
+ && node.Member.Name != "Name"
+ && node.Member.Name != "IsValueType";
+
+ return base.VisitMember(node);
+ }
+}
diff --git a/src/TagBites.Expressions/TagBites.Expressions.csproj b/src/TagBites.Expressions/TagBites.Expressions.csproj
new file mode 100644
index 0000000..e8aec67
--- /dev/null
+++ b/src/TagBites.Expressions/TagBites.Expressions.csproj
@@ -0,0 +1,28 @@
+
+
+
+
+ netstandard2.1
+ true
+
+
+
+
+ TagBites
+ enable
+
+
+
+
+
+
+
+
+
+
+ True
+ \
+
+
+
+
diff --git a/src/TagBites.Expressions/Utils/JetBrainsAnnotations.cs b/src/TagBites.Expressions/Utils/JetBrainsAnnotations.cs
new file mode 100644
index 0000000..59cae38
--- /dev/null
+++ b/src/TagBites.Expressions/Utils/JetBrainsAnnotations.cs
@@ -0,0 +1,153 @@
+#nullable enable
+
+// ReSharper disable UnusedType.Global
+// ReSharper disable UnusedMember.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable IntroduceOptionalParameters.Global
+// ReSharper disable MemberCanBeProtected.Global
+// ReSharper disable InconsistentNaming
+// ReSharper disable CheckNamespace
+
+namespace JetBrains.Annotations;
+
+///
+/// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library),
+/// so this symbol will be ignored by usage-checking inspections.
+/// You can use and
+/// to configure how this attribute is applied.
+///
+///
+///
+/// [UsedImplicitly]
+/// public class TypeConverter {}
+///
+/// public class SummaryData
+/// {
+/// [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
+/// public SummaryData() {}
+/// }
+///
+/// [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.Default)]
+/// public interface IService {}
+///
+///
+[AttributeUsage(AttributeTargets.All)]
+internal sealed class UsedImplicitlyAttribute : Attribute
+{
+ public ImplicitUseTargetFlags TargetFlags { get; }
+
+ public ImplicitUseKindFlags UseKindFlags { get; }
+ public UsedImplicitlyAttribute()
+ : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { }
+
+ public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags)
+ : this(useKindFlags, ImplicitUseTargetFlags.Default) { }
+
+ public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags)
+ : this(ImplicitUseKindFlags.Default, targetFlags) { }
+
+ public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags)
+ {
+ UseKindFlags = useKindFlags;
+ TargetFlags = targetFlags;
+ }
+}
+
+///
+/// Can be applied to attributes, type parameters, and parameters of a type assignable from .
+/// When applied to an attribute, the decorated attribute behaves the same as .
+/// When applied to a type parameter or to a parameter of type ,
+/// indicates that the corresponding type is used implicitly.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter | AttributeTargets.Parameter)]
+internal sealed class MeansImplicitUseAttribute : Attribute
+{
+ [UsedImplicitly]
+ public ImplicitUseTargetFlags TargetFlags { get; }
+
+ [UsedImplicitly]
+ public ImplicitUseKindFlags UseKindFlags { get; }
+ public MeansImplicitUseAttribute()
+ : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { }
+
+ public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags)
+ : this(useKindFlags, ImplicitUseTargetFlags.Default) { }
+
+ public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags)
+ : this(ImplicitUseKindFlags.Default, targetFlags) { }
+
+ public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags)
+ {
+ UseKindFlags = useKindFlags;
+ TargetFlags = targetFlags;
+ }
+}
+
+///
+/// Specifies the details of implicitly used symbol when it is marked
+/// with or .
+///
+[Flags]
+internal enum ImplicitUseKindFlags
+{
+ Default = Access | Assign | InstantiatedWithFixedConstructorSignature,
+ /// Only entity marked with attribute considered used.
+ Access = 1,
+ /// Indicates implicit assignment to a member.
+ Assign = 2,
+ ///
+ /// Indicates implicit instantiation of a type with fixed constructor signature.
+ /// That means any unused constructor parameters won't be reported as such.
+ ///
+ InstantiatedWithFixedConstructorSignature = 4,
+ /// Indicates implicit instantiation of a type.
+ InstantiatedNoFixedConstructorSignature = 8
+}
+
+///
+/// Specifies what is considered to be used implicitly when marked
+/// with or .
+///
+[Flags]
+internal enum ImplicitUseTargetFlags
+{
+ Default = Itself,
+ Itself = 1,
+ /// Members of the type marked with the attribute are considered used.
+ Members = 2,
+ /// Inherited entities are considered used.
+ WithInheritors = 4,
+ /// Entity marked with the attribute and all its members considered used.
+ WithMembers = Itself | Members
+}
+
+///
+/// This attribute is intended to mark publicly available API,
+/// which should not be removed and so is treated as used.
+///
+[MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)]
+[AttributeUsage(AttributeTargets.All, Inherited = false)]
+internal sealed class PublicAPIAttribute : Attribute
+{
+ public string? Comment { get; }
+ public PublicAPIAttribute() { }
+ public PublicAPIAttribute(string comment)
+ {
+ Comment = comment;
+ }
+}
+
+///
+/// Indicates that the return value of the method invocation must be used.
+///
+[AttributeUsage(AttributeTargets.Method)]
+internal sealed class MustUseReturnValueAttribute : Attribute
+{
+ public string? Justification { get; }
+ public MustUseReturnValueAttribute() { }
+ public MustUseReturnValueAttribute(string justification)
+ {
+ Justification = justification;
+ }
+}
diff --git a/src/TagBites.Expressions/Utils/TypeUtils.cs b/src/TagBites.Expressions/Utils/TypeUtils.cs
new file mode 100644
index 0000000..ec62838
--- /dev/null
+++ b/src/TagBites.Expressions/Utils/TypeUtils.cs
@@ -0,0 +1,143 @@
+using System.Reflection;
+using System.Text;
+
+namespace TagBites.Utils;
+
+internal static class TypeUtils
+{
+ #region Types
+
+ public static bool IsNumericType(Type type)
+ {
+ if (type.IsEnum)
+ return false;
+
+ var code = GetTypeCodeWithNullable(type);
+ return code >= TypeCode.SByte && code <= TypeCode.Decimal;
+ }
+ private static TypeCode GetTypeCodeWithNullable(Type type)
+ {
+ if (type.IsEnum)
+ type = Enum.GetUnderlyingType(type);
+
+ return Type.GetTypeCode(type);
+ }
+
+ public static string? GetTypeAlias(Type type)
+ {
+ return type.FullName switch
+ {
+ "System.Boolean" => "bool",
+
+ "System.Byte" => "byte",
+ "System.SByte" => "sbyte",
+ "System.Int16" => "short",
+ "System.UInt16" => "ushort",
+ "System.Int32" => "int",
+ "System.UInt32" => "uint",
+ "System.Int64" => "long",
+ "System.UInt64" => "ulong",
+
+ "System.Single" => "float",
+ "System.Double" => "double",
+ "System.Decimal" => "decimal",
+
+ "System.Char" => "char",
+ "System.String" => "string",
+
+ "System.Object" => "object",
+ "System.Void" => "void",
+
+ _ => null
+ };
+ }
+ public static string GetFriendlyTypeName(this Type type)
+ {
+ if (!type.IsGenericType)
+ return GetTypeAlias(type) ?? type.Name;
+
+ if (type.IsValueType)
+ {
+ var nullableType = Nullable.GetUnderlyingType(type);
+ if (nullableType != null)
+ return GetFriendlyTypeName(nullableType) + "?";
+ }
+
+ var types = type.GetGenericArguments();
+
+ var sb = new StringBuilder();
+ sb.Append(type.Name.Substring(0, type.Name.LastIndexOf('`')));
+ sb.Append('<');
+ for (var i = 0; i < types.Length; i++)
+ {
+ if (i > 0)
+ sb.Append(',');
+
+ sb.Append(GetFriendlyTypeName(types[i]));
+ }
+ sb.Append('>');
+
+ return sb.ToString();
+ }
+
+ #endregion
+
+ #region Generics
+
+ public static bool ContainsGenericDefinition(Type type, Type genericTypeDefinition)
+ {
+ return GetGenericArguments(type, genericTypeDefinition).Length > 0;
+ }
+ public static Type[] GetGenericArguments(Type type, Type genericTypeDefinition)
+ {
+ var ti = type;
+
+ if (ti.IsGenericTypeDefinition && type == genericTypeDefinition)
+ return ti.GenericTypeArguments;
+
+ if (ti.IsInterface)
+ {
+ if (ti.IsGenericType && type.GetGenericTypeDefinition() == genericTypeDefinition)
+ return ti.GenericTypeArguments;
+ }
+ else
+ {
+ for (var it = ti; it != null; it = it.BaseType)
+ if (it.IsGenericType && it.GetGenericTypeDefinition() == genericTypeDefinition)
+ return it.GenericTypeArguments;
+ }
+
+ foreach (var item in ti.GetInterfaces())
+ {
+ if (item.IsGenericType && item.GetGenericTypeDefinition() == genericTypeDefinition)
+ return item.GenericTypeArguments;
+ }
+
+ return Array.Empty();
+ }
+
+ #endregion
+
+ #region Properties
+
+ public static PropertyInfo? GetProperty(object obj, string name, bool nonPublic)
+ {
+ return GetProperty(obj.GetType(), name, nonPublic, false);
+ }
+ public static PropertyInfo? GetProperty(Type type, string name, bool nonPublic, bool isStatic)
+ {
+ while (type != typeof(object) && type != null!)
+ {
+ var ti = type.GetTypeInfo();
+ var property = ti.GetDeclaredProperty(name);
+ if (property != null && property.GetMethod != null && (isStatic == property.GetMethod.IsStatic) && (nonPublic || property.GetMethod.IsPublic) && property.GetIndexParameters().Length == 0)
+ return property;
+
+ type = ti.BaseType!;
+ }
+
+ return null;
+ }
+
+ #endregion
+}
diff --git a/tests/TagBites.Expressions.Tests/ExpressionParserTests.cs b/tests/TagBites.Expressions.Tests/ExpressionParserTests.cs
new file mode 100644
index 0000000..483dc88
--- /dev/null
+++ b/tests/TagBites.Expressions.Tests/ExpressionParserTests.cs
@@ -0,0 +1,716 @@
+using System.Collections.ObjectModel;
+using System.Linq.Expressions;
+
+namespace TagBites.Expressions.Tests;
+
+public class ExpressionParserTests
+{
+ [Theory]
+ [InlineData("1 + 2", 3)]
+ [InlineData("1 + +2", 3)]
+ [InlineData("1 - 2", -1)]
+ [InlineData("1 - -2", 3)]
+ [InlineData("1 * 2", 2)]
+ [InlineData("4 / 2", 2)]
+ [InlineData("1d / 2d", 0.5)]
+ [InlineData("1.5d * 2d", 3d)]
+ [InlineData("5 % 2", 1)]
+ public void MathOperators(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 << 2", 4)]
+ [InlineData("2 >> 1", 1)]
+ [InlineData("1 | 2 | 4", 7)]
+ [InlineData("7 & 2", 2)]
+ [InlineData("7 ^ 2", 5)]
+ public void BitwiseOperators(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 == 2", false)]
+ [InlineData("2 == 2", true)]
+ [InlineData("1 != 2", true)]
+ [InlineData("2 != 2", false)]
+ [InlineData("1 < 2", true)]
+ [InlineData("2 < 1", false)]
+ [InlineData("1 <= 2", true)]
+ [InlineData("2 <= 1", false)]
+ [InlineData("1 > 2", false)]
+ [InlineData("2 > 1", true)]
+ [InlineData("1 >= 2", false)]
+ [InlineData("2 >= 1", true)]
+ [InlineData("!true", false)]
+ [InlineData("!false", true)]
+ [InlineData("!(1 == 2)", true)]
+ public void LogicalOperators(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("(double)1", 1d)]
+ [InlineData("(int)2.5", 2)]
+ [InlineData("(float)2.5", 2.5f)]
+ [InlineData("(double)2.5m", 2.5)]
+ public void CastOperators(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 < 2 ? 1 : 2", 1)]
+ [InlineData("1 > 2 ? 1 : 2", 2)]
+ [InlineData("1 == 2 ? 1 : null", null)]
+ [InlineData("1 == 1 ? 1 : null", 1)]
+ [InlineData("1 == 2 ? null : 1", 1)]
+ [InlineData("1 == 1 ? null : 1", null)]
+ public void ConditionalOperator(string script, object? expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 switch { 1 => 10, 2 => 20, _ => 0 }", 10)]
+ [InlineData("2 switch { 1 => 10, 2 => 20, _ => 0 }", 20)]
+ [InlineData("3 switch { 1 => 10, 2 => 20, _ => 0 }", 0)]
+ [InlineData("n switch { 1 => 10, 2 => 20, _ => 0 }", 10)]
+ public void Switch(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(int), "n"),
+ }
+ };
+
+ ExecuteAndTest(script, options, expectedResult, 1);
+ }
+
+ [Theory]
+ [InlineData(@"""a"" + ""b""", "ab")]
+ [InlineData(@"""a"" < ""b""", true)]
+ [InlineData(@"""b"" < ""a""", false)]
+ [InlineData(@"""a"" <= ""b""", true)]
+ [InlineData(@"""b"" <= ""a""", false)]
+ [InlineData(@"""a"" > ""b""", false)]
+ [InlineData(@"""b"" > ""a""", true)]
+ [InlineData(@"""a"" >= ""b""", false)]
+ [InlineData(@"""b"" >= ""a""", true)]
+ [InlineData(@"""a"" == ""a""", true)]
+ [InlineData(@"""a"" == ""b""", false)]
+ [InlineData(@"""a"" != ""a""", false)]
+ [InlineData(@"""b"" != ""a""", true)]
+ [InlineData(@"""a"" + 1", "a1")]
+ [InlineData(@"1 + ""a""", "1a")]
+ [InlineData(@"'b' + ""a""", "ba")]
+ [InlineData(@"(1 == 2 ? 1 : null) + ""a""", "a")]
+ [InlineData(@"(1 == 1 ? 1 : null) + ""a""", "1a")]
+ [InlineData("$\"{\"a\"}.{\"b\"}\"", "a.b")]
+ [InlineData("$\"{1.23:0}x{2.34:00}\"", "1x02")]
+ public void StringOperators(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("new DateTime(1992, 8, 7) < new DateTime(2021, 8, 14)", true)]
+ [InlineData("new List() != null", true)]
+ public void NewOperator(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("new int[] { 1, 2, 3 }[0]", 1)]
+ [InlineData("new [] { 1, 2, 3 }[0]", 1)]
+ public void Array(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 + 2.1", 3.1)]
+ [InlineData("1 + 2L", 3L)]
+ [InlineData("2 < 1d", false)]
+ [InlineData("2 < 1m", false)]
+ [InlineData("2 < 1L", false)]
+ [InlineData("2 == 2m", true)]
+ [InlineData("1 / 2d", 0.5)]
+ public void ImplicitCast(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("new DateTime(2021, 8, 14).Day", 14)]
+ [InlineData("new DateTime(2021, 8, 14).Date.Day", 14)]
+ [InlineData("DateTime.MinValue < new DateTime(2021, 8, 14)", true)]
+ public void AccessMember(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData(@"new DateTime(2021, 8, 14).ToString(""yyyy"")", "2021")]
+ [InlineData(@"new DateTime(2021, 8, 14).ToString(""yyyy"").Length", 4)]
+ public void Invocation(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("v", 10)]
+ [InlineData("v + v", 20)]
+ [InlineData(@"v.ToString() + ""-"" + t", "10-ten")]
+ [InlineData("int.Parse(v.ToString().Substring(0,1))", 1)]
+ [InlineData("m.TenTimes.One + m.One", 11)]
+ public void Arguments(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(int), "v"),
+ (typeof(string), "t"),
+ (typeof(TestModel), "m")
+ }
+ };
+
+ ExecuteAndTest(script, options, expectedResult, 10, "ten", new TestModel());
+ }
+
+ [Theory]
+ [InlineData("((TimeSpan?)TimeSpan.FromMinutes(2))?.TotalMinutes", 2d)]
+ [InlineData("((TimeSpan?)TimeSpan.FromMinutes(2)).Value.TotalMinutes", 2d)]
+ [InlineData("nv.Value", 5)]
+ [InlineData("m?.TenTimes != null", true)]
+ [InlineData("m?.TenTimes?.One", 10)]
+ [InlineData("nv + m.TenTimes?.TenTimes.One + m.TenTimes?.One", 115)]
+ [InlineData("(1 < 2 ? (int?)1 : 2).Value", 1)]
+ [InlineData("(m?.TenTimes.One ?? nv).Value", 10)]
+ public void ConditionalOperators(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(TestModel), "m"),
+ (typeof(int?), "nv"),
+ }
+ };
+
+ ExecuteAndTest(script, options, expectedResult, new TestModel(), 5);
+ }
+
+ [Theory]
+ [InlineData("list[0]", 1)]
+ [InlineData("array[0]", 1)]
+ [InlineData("list?[0] ?? 0", 1)]
+ [InlineData("(list.Count == array.Length ? list : (IList)array)?[0] ?? 0", 1)]
+ public void ItemOperator(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(IList), "list"),
+ (typeof(int[]), "array"),
+ }
+ };
+
+ var list = new List { 1 };
+ ExecuteAndTest(script, options, expectedResult, list, list.ToArray());
+ }
+
+ [Theory]
+ [InlineData("m.ReturnForExactType(2)", 2)]
+ [InlineData("m.ReturnForExactType((object)2)", 2)]
+ [InlineData("m.ReturnForExactType(2)", 0L)]
+ [InlineData("m.GetExactOrDefault(2)", 0L)]
+ [InlineData("m.GetExactOrDefault(2, 1)", 1L)]
+ [InlineData("m.GetExactOrDefault(2L)", 2L)]
+ [InlineData("m.ReturnIt(2)", 2)]
+ [InlineData("m.ReturnIt(2)", 2L)]
+ public void GenericMethods(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters = { (typeof(TestModel), "m") }
+ };
+
+ ExecuteAndTest(script, options, expectedResult, new TestModel());
+ }
+
+ [Theory]
+ [InlineData("Math.Min(2, 2)", 2)]
+ [InlineData("Math.Min(2L, 2L)", 2L)]
+ [InlineData("Math.Min(2d, 2d)", 2d)]
+ [InlineData("Math.Min(2, 2L)", 2L)]
+ [InlineData("Math.Min(2L, 2)", 2L)]
+ [InlineData("Math.Min(2, 2d)", 2d)]
+ [InlineData("Math.Min(2d, 2)", 2d)]
+ [InlineData("Math.Min(2d, 2f)", 2d)]
+ public void ImplicitCastOnMethodCall(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("Math.Min(2d, 2m)")]
+ [InlineData("Math.Min(2m, 2d)")]
+ public void AmbiguousMethodCall(string script) => Assert.ThrowsAny(() => ExpressionParser.Parse(script, null));
+
+ [Theory]
+ [InlineData("m is TestModel", true)]
+ [InlineData("m is object", true)]
+ [InlineData("!(m is TestModel)", false)]
+ [InlineData("m as TestModel != null", true)]
+ [InlineData("(int?)1 is int", true)]
+ [InlineData("(int?)null is int", false)]
+ [InlineData("(int?)1 as int?", 1)]
+ [InlineData("(int?)null as int?", null)]
+ [InlineData("((ITestModel)m) as TestModel != null", true)]
+ public void TypeCheck(string script, object? expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ IncludedTypes =
+ {
+ typeof(ITestModel)
+ },
+ Parameters =
+ {
+ (typeof(TestModel), "m"),
+ (typeof(int?), "nv"),
+ }
+ };
+
+ ExecuteAndTest(script, options, expectedResult, new TestModel(), 5);
+ }
+
+ [Theory]
+ [InlineData("(int?)null is null", true)]
+ [InlineData("(int?)1 is null", false)]
+ [InlineData("(int?)1 is not null", true)]
+ [InlineData("(int?)null is not null", false)]
+ [InlineData("(int?)1 is { }", true)]
+ [InlineData("(int?)null is { }", false)]
+ [InlineData("(int?)1 is not { }", false)]
+ [InlineData("(int?)null is not { }", true)]
+ public void PatternNullCheck(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("(int?)1 is int", true)]
+ [InlineData("(int?)null is int", false)]
+ [InlineData("true is int", false)]
+ public void PatternTypeCheck(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 is 1", true)]
+ [InlineData("1 is 0", false)]
+ [InlineData("(int?)1 is 1", true)]
+ [InlineData("(int?)1 is 0", false)]
+ public void PatternConstCheck(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 is 1", true)]
+ [InlineData("1 is 0 or 1", true)]
+ [InlineData("1 is 1 and > 0", true)]
+ public void PatternOrAnd(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 is > 0", true)]
+ [InlineData("1 is >= 0", true)]
+ [InlineData("1 is < 0", false)]
+ [InlineData("1 is <= 0", false)]
+ public void PatternRelation(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 is not 0", true)]
+ public void PatternUnaryOperator(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("1 is (1)", true)]
+ [InlineData("1 is (2 or 1)", true)]
+ [InlineData("1 is (1 or 2)", true)]
+ [InlineData("1 is not 0 and (1 or 2)", true)]
+ [InlineData("1 is not (1 or 2)", false)]
+ [InlineData("1 is not (1 or 2 or not 3)", false)]
+ [InlineData("(int?)1 is not (null)", true)]
+ [InlineData("(int?)1 is (null)", false)]
+ [InlineData("(int?)1 is (null or 1)", true)]
+ public void PatternParenthesized(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData("(int?)1 is { } a", true)]
+ [InlineData("(int?)1 is int a && a == 1", true)]
+ [InlineData("(int?)1 is not int a && a == 1", false)]
+ [InlineData("(int?)1 is not int a || a == 1", true)]
+ [InlineData("(int?)1 is int a && list.Sum(x => x + a) == 9", true)]
+ [InlineData("(int?)1 is int a && list.Sum(x => x is int x2 ? x2 + a : 0) == 9", true)]
+ public void PatternVar(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ IncludedTypes =
+ {
+ typeof(ITestModel)
+ },
+ Parameters =
+ {
+ (typeof(IList), "list"),
+ (typeof(TestModel), "m"),
+ (typeof(int?), "nv"),
+ }
+ };
+
+ ExecuteAndTest(script, options, expectedResult, new List { 1, 2, 3 }, new TestModel(), 5);
+ }
+
+ [Theory]
+ [InlineData(@"""a"" is { Length: > 0 }", true)]
+ [InlineData(@"""a"" is { Length: < 0 }", false)]
+ [InlineData(@"s is { X: 1 }", true)]
+ [InlineData(@"s is { X: 1, Y: 2 }", true)]
+ [InlineData(@"s is { X: 1, Y: 3 }", false)]
+ [InlineData(@"s is { X: 3, Y: 2 }", false)]
+ [InlineData(@"s is TestStruct { X: 1, Y: 2 } a && a.X + a.Y == 3", true)]
+ [InlineData(@"m is { NullChild: null }", true)]
+ [InlineData(@"m is { NullChild: not null }", false)]
+ [InlineData(@"m is { NullChild: not { } }", true)]
+ [InlineData(@"m is { NullChild: { } }", false)]
+ [InlineData(@"m is { NullChild: null, TenTimes: { } }", true)]
+ [InlineData(@"m is { NullChild: null, TenTimes: not { } }", false)]
+ [InlineData(@"m is TestModel { NullChild: null, TenTimes: { } } a && a.TenTimes.Value == 10", true)]
+ public void PatternProperty(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(TestStruct), "s"),
+ (typeof(TestModel), "m")
+ }
+ };
+ ExecuteAndTest(script, options, expectedResult, new TestStruct { X = 1, Y = 2 }, new TestModel());
+ }
+
+ [Theory]
+ [InlineData("(1, 2).Item1", 1)]
+ [InlineData("(1, 2).Item2", 2)]
+ [InlineData("(n, n + 1, a + a).Item1", 1)]
+ [InlineData("(n, n + 1, a + a).Item2", 2)]
+ [InlineData("(n, n + 1, a + a).Item3", "aa")]
+ [InlineData("(A: 1, B: 2).Item1", 1)]
+ [InlineData("(A: 1, B: 2).Item2", 2)]
+ public void Tuple(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters = {
+ (typeof(int), "n"),
+ (typeof(string), "a")
+ }
+ };
+
+ ExecuteAndTest(script, options, expectedResult, 1, "a");
+ }
+
+ [Theory]
+ [InlineData("TenTimes.One + One + v", 16)]
+ public void ThisParameter(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(TestModel), "m"),
+ (typeof(int), "v")
+ },
+ UseFirstParameterAsThis = true
+ };
+
+ ExecuteAndTest(script, options, expectedResult, new TestModel(), 5);
+ }
+
+ [Theory]
+ [InlineData("One + Two + TenTimes.One + TwentyTimes.One + TwentyTimes.Two + TwentyTimes.TwentyTimes.One + TwentyTimes.TwentyTimes.Two", 1 + 2 + 10 + 20 + 20 * 2 + 20 * 20 + 20 * 20 * 2)]
+ [InlineData("m.Two + m.TwentyTimes.Two", 2 + 20 * 2)]
+ public void DynamicParameterBinding(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(TestModel), "this"),
+ (typeof(TestModel), "m")
+ },
+ UseFirstParameterAsThis = true,
+ CustomPropertyResolver = context =>
+ {
+ if (context.Instance.Type == typeof(TestModel) && TestModel.GetMemberType(context.MemberName) is { } type)
+ return Expression.Convert(
+ Expression.Call(context.Instance, typeof(TestModel).GetMethod("GetValue")!, Expression.Constant(context.MemberName)),
+ type);
+
+ return null;
+ },
+ };
+
+ ExecuteAndTest(script, options, expectedResult, new TestModel(), new TestModel());
+ }
+
+ [Theory]
+ [InlineData("list.First()", 1)]
+ [InlineData("list.FirstOrDefault()", 1)]
+ [InlineData("list.Count()", 3)]
+ [InlineData("list.Min()", 1)]
+ [InlineData("list.Max()", 3)]
+ [InlineData("list.Sum()", 6)]
+ [InlineData("list.First(x => x > 2)", 3)]
+ [InlineData("list.Where(x => x > 2).Count()", 1)]
+ [InlineData("list.Where((x, i) => x > 1 && i > 1).Count()", 1)]
+ [InlineData("array.First(x => x > 2)", 3)]
+ [InlineData("models.First(x => x.Value > 1).Value", 10)]
+ [InlineData("listOfLists.Select(x => x.Select(y => y * 2).Max()).Sum()", 3 * 2 + 6 * 2)]
+ [InlineData("listOfLists.Select(x => x.Select(y => y * 2).Select(x => x * 2).Max()).Sum()", 3 * 2 * 2 + 6 * 2 * 2)]
+ [InlineData("list.Sum(x => x + n)", 9)]
+ public void Lambda(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(IList), "list"),
+ (typeof(int[]), "array"),
+ (typeof(IList>), "listOfLists"),
+ (typeof(IList), "models"),
+ (typeof(int), "n")
+ }
+ };
+ var args = new object[]
+ {
+ new List { 1, 2, 3 },
+ new [] { 1, 2, 3 },
+ new List> { new List { 1, 2, 3 }, new List { 4, 5, 6 } },
+ new List { new (), new (10), new (100) },
+ 1
+ };
+ //var w= new List> { new List { 1, 2, 3 }, new List { 4, 5, 6 } }.Select(x=>x.Select(y=>y.))
+ ExecuteAndTest(script, options, expectedResult, args);
+ }
+
+ [Theory]
+ [InlineData("new TestModel().Value", 1)]
+ [InlineData("new TestModel(5).Value", 5)]
+ [InlineData("new TestModel { Field1 = 1, Field2 = 2 }.Field1", 1)]
+ [InlineData("new TestModel { Field1 = 1, Field2 = 2 }.Field2", 2)]
+ [InlineData("new TestModel { Field1 = 0, Field2 = 0 }.Value", 1)]
+ [InlineData("new TestModel(5) { Field1 = 1, Field2 = 2 }.Value", 5)]
+ public void ObjectCreation(string script, object expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ IncludedTypes =
+ {
+ typeof(TestModel)
+ }
+ };
+ ExecuteAndTest(script, options, expectedResult);
+ }
+
+ [Theory]
+ [InlineData("new [] { 1, 2, 3 }.Select(x => (x, x + 1).Item2).Sum()", 9)]
+ public void Complex(string script, object expectedResult) => ExecuteAndTest(script, expectedResult);
+
+ [Theory]
+ [InlineData(@"new TestModel(1)", typeof(TestModel))]
+ public void ResolveTypeUsingResult(string script, Type type)
+ {
+ var options = new ExpressionParserOptions
+ {
+ AllowRuntimeCast = true,
+ ResultType = type
+ };
+ var result = Execute(script, options);
+ Assert.IsType(type, result);
+ }
+
+ [Theory]
+ [InlineData(@"typeis(""System.String,System.Private.CoreLib"", ""a"")", true)]
+ [InlineData(@"typeis(""System.String,System.Private.CoreLib"", m)", false)]
+ [InlineData(@"typeis(""System.String,System.Private.CoreLib"", null)", false)]
+ [InlineData(@"typeas(""System.String,System.Private.CoreLib"", ""a"")", "a")]
+ [InlineData(@"typeas(""System.String,System.Private.CoreLib"", null)", null)]
+ [InlineData(@"typecast(""System.String,System.Private.CoreLib"", null)", null)]
+ [InlineData(@"typecast(""System.String,System.Private.CoreLib"", ""a"")", "a")]
+ public void RuntimeCast(string script, object? expectedResult)
+ {
+ var options = new ExpressionParserOptions
+ {
+ AllowRuntimeCast = true,
+ IncludedTypes =
+ {
+ typeof(TestModel),
+ typeof(ITestModel),
+ },
+ Parameters = { (typeof(TestModel), "m") }
+ };
+ ExecuteAndTest(script, options, expectedResult, new TestModel());
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("1 + * 2")]
+ [InlineData("list[]")]
+ [InlineData("list[")]
+ [InlineData("1 + (")]
+ [InlineData("{ 1 }")]
+ [InlineData("( 1")]
+ [InlineData("list.Func_abs()")]
+ [InlineData("list.Min(")]
+ [InlineData("list.Min(x=>y)")]
+ [InlineData("abc")]
+ [InlineData("v + abs")]
+ [InlineData("1 switch { \"a\" => 2, _ => 0 }")]
+ [InlineData("1 switch { 1 => \"a\", _ => 0 }")]
+ [InlineData("1 switch { 1 => 'a', _ => \"a\" }")]
+ [InlineData("2d == 2m")]
+ [InlineData("2d + 2m")]
+ public void InvalidSyntax(string? script)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(TestModel), "this"),
+ (typeof(TestModel), "m"),
+ (typeof(IList), "list"),
+ (typeof(int), "v")
+ },
+ UseFirstParameterAsThis = true,
+ };
+
+ Assert.ThrowsAny(() => ExpressionParser.Parse(script!, options));
+ }
+
+ [Theory]
+ [InlineData("2.GetType().Name", "Int32", false)]
+ [InlineData("2.GetType().IsValueType", true, false)]
+ [InlineData("2.GetType().IsAssignableFrom(2.GetType())", true, false)]
+ [InlineData("2.GetType().FullName", null, false)]
+ [InlineData("2.GetType().Assembly", null, false)]
+ [InlineData(@"""a"".GetType().GetProperties().Length > 0", null, false)]
+ [InlineData("2.GetType().FullName", "System.Int32", true)]
+ [InlineData(@"""a"".GetType().GetProperties().Length > 0", true, true)]
+ public void ReflectionCallTest(string script, object? expectedResult, bool allowReflection)
+ {
+ var options = new ExpressionParserOptions { AllowReflection = allowReflection };
+ var shouldParse = expectedResult is not null;
+
+ Assert.Equal(shouldParse, ExpressionParser.TryParse(script, options, out var expression, out _));
+
+ if (expectedResult != null)
+ {
+ var result = expression!.Compile().DynamicInvoke();
+ Assert.Equal(expectedResult, result);
+ }
+ }
+
+ private static void ExecuteAndTest(string script, object? expectedResult, params object?[] args)
+ {
+ ExecuteAndTest(script, null, expectedResult, args);
+ }
+ private static void ExecuteAndTest(string script, ExpressionParserOptions? options, object? expectedResult, params object?[] args)
+ {
+ var result = Execute(script, options, args);
+
+ Assert.Equal(expectedResult, result);
+ }
+ private static object? Execute(string script, ExpressionParserOptions? options, params object?[] args)
+ {
+ var expression = ExpressionParser.Parse(script, options);
+ var expressionDelegate = expression.Compile();
+ return expressionDelegate.DynamicInvoke(args);
+ }
+
+ // ReSharper disable NotAccessedField.Local
+ // ReSharper disable UnusedMember.Local
+ // ReSharper disable MemberCanBePrivate.Local
+ private class ITestModel
+ {
+
+ }
+ private class TestModel : ITestModel
+ {
+ private TestModel? _child;
+ private TestModel? _dynamiChild;
+
+ public int Value => One;
+ public int One { get; }
+ public TestModel TenTimes => _child ??= new TestModel(One * 10);
+
+ public int Field1 { get; set; }
+ public int Field2 { get; set; }
+
+ public TestModel? NullChild => null;
+
+ public TestModel(int value = 1) => One = value;
+
+
+ public object? GetValue(string member)
+ {
+ return member switch
+ {
+ "Two" => One * 2,
+ "TwentyTimes" => _dynamiChild ??= new TestModel(One * 20),
+ _ => null
+ };
+ }
+ public static Type? GetMemberType(string member)
+ {
+ return member switch
+ {
+ "Two" => typeof(int),
+ "TwentyTimes" => typeof(TestModel),
+ _ => null
+ };
+ }
+
+ public T? ReturnForExactType(object v) => v is T v1 ? v1 : default;
+ public T ReturnIt(T value) => value;
+
+ public T? GetExactOrDefault(object v, T? defaultValue = default) => v is T v1 ? v1 : defaultValue;
+ }
+ private struct TestStruct
+ {
+ public int X;
+ public int Y;
+ }
+
+ private class RuntimeDefinedType
+ {
+ public Dictionary Properties { get; } = new();
+ }
+ private class RuntimeDefinedTypeInstance
+ {
+ public RuntimeDefinedType Type { get; }
+ private readonly Dictionary _values = new();
+
+ public object? this[string propertyName]
+ {
+ get => _values.GetValueOrDefault(propertyName);
+ set
+ {
+ var type = Type.Properties.GetValueOrDefault(propertyName);
+ if (type == null || value?.GetType() is { } t && !type.IsAssignableFrom(t))
+ throw new ArgumentException();
+
+ _values[propertyName] = value;
+ }
+ }
+
+ public RuntimeDefinedTypeInstance(RuntimeDefinedType type) => Type = type;
+
+
+ public T? GetTypedValue(string propertyName) => this[propertyName] is T t ? t : default;
+ }
+ private class RuntimeDefinedTypeInstanceCollection : Collection
+ {
+ public RuntimeDefinedType Type { get; }
+
+ public RuntimeDefinedTypeInstanceCollection(RuntimeDefinedType type) => Type = type;
+
+
+ protected override void InsertItem(int index, RuntimeDefinedTypeInstance item)
+ {
+ if (item.Type != Type)
+ throw new ArgumentException();
+
+ base.InsertItem(index, item);
+ }
+ protected override void SetItem(int index, RuntimeDefinedTypeInstance item)
+ {
+ if (item.Type != Type)
+ throw new ArgumentException();
+
+ base.SetItem(index, item);
+ }
+ }
+
+ // ReSharper restore NotAccessedField.Local
+ // ReSharper restore UnusedMember.Local
+ // ReSharper restore MemberCanBePrivate.Local
+}
diff --git a/tests/TagBites.Expressions.Tests/SampleTests.cs b/tests/TagBites.Expressions.Tests/SampleTests.cs
new file mode 100644
index 0000000..8435bef
--- /dev/null
+++ b/tests/TagBites.Expressions.Tests/SampleTests.cs
@@ -0,0 +1,83 @@
+namespace TagBites.Expressions.Tests;
+
+public class SampleTests
+{
+ [Fact]
+ public void BasicUseTest()
+ {
+ var expression = "new [] { 1, 2, 3 }.Select(x => (x, x + 1).Item2).Sum()";
+ var func = ExpressionParser.Parse(expression, null).Compile();
+
+ Assert.Equal(9, func.DynamicInvoke());
+ }
+
+ [Fact]
+ public void SimpleTest()
+ {
+ var func = Parse("(a + b) / (double)b");
+ Assert.Equal(2.5d, func(3, 2));
+
+ func = Parse("a switch { 1 => b, 2 => b * 2, _ => b + a }");
+ Assert.Equal(2, func(1, 2));
+ Assert.Equal(4, func(2, 2));
+ Assert.Equal(5, func(3, 2));
+
+ static Func Parse(string expression)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(int), "a"),
+ (typeof(int), "b")
+ },
+ ResultCastType = typeof(double)
+ };
+ var lambda = ExpressionParser.Parse(expression, options);
+ return (Func)lambda.Compile();
+ }
+ }
+
+ [Fact]
+ public void TypeTest()
+ {
+ var m = new TestModel { X = 1, Y = 2 };
+
+ var func = Parse("X + Y");
+ Assert.Equal(3, func(m));
+
+ func = Parse("X + Nested.X");
+ Assert.Equal(3, func(m));
+
+ func = Parse("X + new TestModel { X = 1, Y = 2 }.Y");
+ Assert.Equal(3, func(m));
+
+ func = Parse("X + (X == 1 ? Nested.X : Nested.Y)");
+ Assert.Equal(3, func(m));
+ Assert.Equal(7, func(new TestModel { X = 2, Y = 3 }));
+
+ static Func Parse(string expression)
+ {
+ var options = new ExpressionParserOptions
+ {
+ Parameters =
+ {
+ (typeof(TestModel), "this")
+ },
+ UseFirstParameterAsThis = true,
+ ResultType = typeof(int)
+ };
+ var lambda = ExpressionParser.Parse(expression, options);
+ return (Func)lambda.Compile();
+ }
+ }
+
+ private class TestModel
+ {
+ private TestModel? _nested;
+
+ public int X { get; set; }
+ public int Y { get; set; }
+ public TestModel Nested => _nested ??= new TestModel { X = Y, Y = X + Y };
+ }
+}
diff --git a/tests/TagBites.Expressions.Tests/TagBites.Expressions.Tests.csproj b/tests/TagBites.Expressions.Tests/TagBites.Expressions.Tests.csproj
new file mode 100644
index 0000000..de20cea
--- /dev/null
+++ b/tests/TagBites.Expressions.Tests/TagBites.Expressions.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net8.0
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+