From 72904e159f9dd8135af428490329ce953febac3f Mon Sep 17 00:00:00 2001 From: Tomasz Malik Date: Thu, 18 Jan 2024 00:04:15 +0100 Subject: [PATCH] chore: code moved from the TagBites.UI repository --- .editorconfig | 48 + .github/workflows/build-and-test.yml | 19 + .github/workflows/publish-preview.yml | 21 + .github/workflows/publish.yml | 21 + .gitignore | 14 + Directory.Build.props | 63 + Directory.Build.targets | 21 + LICENSE | 21 + Package.props | 17 + README.md | 86 + TagBites.Expressions.sln | 49 + Version.props | 23 + .../Expressions/ExpressionParser.cs | 82 + .../Expressions/ExpressionParserException.cs | 8 + .../Expressions/ExpressionParserOptions.cs | 57 + .../IExpressionMemberResolverContext.cs | 17 + .../Expressions/Visitors/ExpressionBuilder.cs | 2099 +++++++++++++++++ .../Visitors/IdentifierDetector.cs | 31 + .../Visitors/ReflectionDetector.cs | 28 + .../TagBites.Expressions.csproj | 28 + .../Utils/JetBrainsAnnotations.cs | 153 ++ src/TagBites.Expressions/Utils/TypeUtils.cs | 143 ++ .../ExpressionParserTests.cs | 716 ++++++ .../TagBites.Expressions.Tests/SampleTests.cs | 83 + .../TagBites.Expressions.Tests.csproj | 32 + 25 files changed, 3880 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/build-and-test.yml create mode 100644 .github/workflows/publish-preview.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .gitignore create mode 100644 Directory.Build.props create mode 100644 Directory.Build.targets create mode 100644 LICENSE create mode 100644 Package.props create mode 100644 README.md create mode 100644 TagBites.Expressions.sln create mode 100644 Version.props create mode 100644 src/TagBites.Expressions/Expressions/ExpressionParser.cs create mode 100644 src/TagBites.Expressions/Expressions/ExpressionParserException.cs create mode 100644 src/TagBites.Expressions/Expressions/ExpressionParserOptions.cs create mode 100644 src/TagBites.Expressions/Expressions/IExpressionMemberResolverContext.cs create mode 100644 src/TagBites.Expressions/Expressions/Visitors/ExpressionBuilder.cs create mode 100644 src/TagBites.Expressions/Expressions/Visitors/IdentifierDetector.cs create mode 100644 src/TagBites.Expressions/Expressions/Visitors/ReflectionDetector.cs create mode 100644 src/TagBites.Expressions/TagBites.Expressions.csproj create mode 100644 src/TagBites.Expressions/Utils/JetBrainsAnnotations.cs create mode 100644 src/TagBites.Expressions/Utils/TypeUtils.cs create mode 100644 tests/TagBites.Expressions.Tests/ExpressionParserTests.cs create mode 100644 tests/TagBites.Expressions.Tests/SampleTests.cs create mode 100644 tests/TagBites.Expressions.Tests/TagBites.Expressions.Tests.csproj 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 + + + + + + + +