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 + + + + + + + +