From ccd5a3d81518cd7d56636855904bd2df84dfc005 Mon Sep 17 00:00:00 2001 From: Matthias Vill Date: Wed, 24 Jun 2026 23:17:58 +0200 Subject: [PATCH 1/2] Include relevant parts of ExpressionShortcuts This elemiminates all NRT-warnings in the solution --- source/Handlebars.Test/IssueTests.cs | 2 +- .../Handlebars/Compiler/CompilationContext.cs | 2 +- source/Handlebars/Compiler/FunctionBuilder.cs | 4 +- .../Handlebars/Compiler/HandlebarsCompiler.cs | 1 - .../ClosureExpressionMiddleware.cs | 3 +- .../Expression/BlockHelperFunctionBinder.cs | 5 +- .../Expression/BoolishConverter.cs | 4 +- .../Expression/DecoratorDefinition.cs | 2 +- .../Expression/FunctionBinderHelpers.cs | 4 +- .../Expression/HelperFunctionBinder.cs | 4 +- .../Translation/Expression/IteratorBinder.cs | 6 +- .../Translation/Expression/PartialBinder.cs | 7 +- .../Translation/Expression/PathBinder.cs | 3 +- .../Translation/Expression/StaticReplacer.cs | 3 +- .../Expression/SubExpressionVisitor.cs | 3 +- .../Expression/UnencodedStatementVisitor.cs | 3 +- .../ExpressionShortcuts/BlockBuilder.cs | 74 +++++++++ .../ExpressionContainer.cs | 56 +++++++ .../ExpressionContainerExtensions.cs | 69 ++++++++ .../ExpressionExtractorVisitor.cs | 79 +++++++++ .../ExpressionShortcuts.cs | 156 ++++++++++++++++++ .../ExpressionShortcuts/ExpressionUtils.cs | 119 +++++++++++++ source/Handlebars/ExpressionShortcuts/LICENSE | 21 +++ .../ParameterReplacerVisitor.cs | 27 +++ source/Handlebars/Handlebars.csproj | 3 +- 25 files changed, 623 insertions(+), 37 deletions(-) create mode 100644 source/Handlebars/ExpressionShortcuts/BlockBuilder.cs create mode 100644 source/Handlebars/ExpressionShortcuts/ExpressionContainer.cs create mode 100644 source/Handlebars/ExpressionShortcuts/ExpressionContainerExtensions.cs create mode 100644 source/Handlebars/ExpressionShortcuts/ExpressionExtractorVisitor.cs create mode 100644 source/Handlebars/ExpressionShortcuts/ExpressionShortcuts.cs create mode 100644 source/Handlebars/ExpressionShortcuts/ExpressionUtils.cs create mode 100644 source/Handlebars/ExpressionShortcuts/LICENSE create mode 100644 source/Handlebars/ExpressionShortcuts/ParameterReplacerVisitor.cs diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index 130efdc0..23cfa686 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -1285,7 +1285,7 @@ public void Issue661_ExplicitCrLfInTemplateIsPreservedVerbatim() { var handlebars = Handlebars.Create(); handlebars.RegisterHelper("Split", (context, arguments) => - ((string) arguments[0]).Split(((string) arguments[1])[0])); + ((string) arguments[0]!).Split(((string) arguments[1]!)[0])); var template = handlebars.Compile("{{#each (Split \"a;b;c\" ';')}}\r\n{{@Key}}:{{@Index}}:{{this}}\r\n{{/each}}"); diff --git a/source/Handlebars/Compiler/CompilationContext.cs b/source/Handlebars/Compiler/CompilationContext.cs index b85cd85f..25cd33be 100644 --- a/source/Handlebars/Compiler/CompilationContext.cs +++ b/source/Handlebars/Compiler/CompilationContext.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using Expressions.Shortcuts; +using HandlebarsDotNet.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs index f8f3e5b5..4c182b43 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using Expressions.Shortcuts; +using HandlebarsDotNet.ExpressionShortcuts; using HandlebarsDotNet.Polyfills; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/HandlebarsCompiler.cs b/source/Handlebars/Compiler/HandlebarsCompiler.cs index 398853be..ac64df75 100644 --- a/source/Handlebars/Compiler/HandlebarsCompiler.cs +++ b/source/Handlebars/Compiler/HandlebarsCompiler.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Expressions.Shortcuts; using HandlebarsDotNet.Compiler.Lexer; using HandlebarsDotNet.IO; using HandlebarsDotNet.PathStructure; diff --git a/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs b/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs index 0b1c297a..9f9e07b2 100644 --- a/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs +++ b/source/Handlebars/Compiler/Middlewares/ClosureExpressionMiddleware.cs @@ -2,8 +2,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; -using HandlebarsDotNet.Pools; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler.Middlewares { diff --git a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs index 8611597b..85b2eb58 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BlockHelperFunctionBinder.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; -using Expressions.Shortcuts; using HandlebarsDotNet.Decorators; +using HandlebarsDotNet.ExpressionShortcuts; using HandlebarsDotNet.Helpers; using HandlebarsDotNet.Helpers.BlockHelpers; using HandlebarsDotNet.PathStructure; using HandlebarsDotNet.Polyfills; using HandlebarsDotNet.Runtime; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs index 06f7bc28..c6cd9238 100644 --- a/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs +++ b/source/Handlebars/Compiler/Translation/Expression/BoolishConverter.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; using System.Linq.Expressions; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs b/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs index 50343553..56627bac 100644 --- a/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs +++ b/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq.Expressions; -using Expressions.Shortcuts; +using HandlebarsDotNet.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs b/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs index ab20ac55..2246a994 100644 --- a/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs +++ b/source/Handlebars/Compiler/Translation/Expression/FunctionBinderHelpers.cs @@ -3,11 +3,11 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using Expressions.Shortcuts; using HandlebarsDotNet.Collections; using HandlebarsDotNet.EqualityComparers; +using HandlebarsDotNet.ExpressionShortcuts; using HandlebarsDotNet.Runtime; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs index ea17cfb3..49302598 100644 --- a/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/HelperFunctionBinder.cs @@ -1,12 +1,10 @@ using System.Collections.Generic; using System.Linq.Expressions; -using Expressions.Shortcuts; using HandlebarsDotNet.Decorators; using HandlebarsDotNet.Helpers; -using HandlebarsDotNet.Helpers.BlockHelpers; using HandlebarsDotNet.PathStructure; using HandlebarsDotNet.Runtime; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs index b65b89f6..c2cff494 100644 --- a/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/IteratorBinder.cs @@ -1,9 +1,9 @@ using System.Linq.Expressions; -using Expressions.Shortcuts; +using HandlebarsDotNet.ExpressionShortcuts; using HandlebarsDotNet.ObjectDescriptors; using HandlebarsDotNet.PathStructure; using HandlebarsDotNet.Polyfills; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { @@ -127,7 +127,7 @@ public static void Iterate( if (!ObjectDescriptor.TryCreate(target, out var descriptor)) { - throw new HandlebarsRuntimeException($"Cannot create ObjectDescriptor for type {descriptor.DescribedType}"); + throw new HandlebarsRuntimeException($"Cannot create ObjectDescriptor for type {target.GetType()}"); } if (descriptor.Iterator == null) throw new HandlebarsRuntimeException($"Type {descriptor.DescribedType} does not support iteration"); diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index 6e20973f..f3f4d6b1 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq.Expressions; -using System.Text; -using Expressions.Shortcuts; using HandlebarsDotNet.IO; using HandlebarsDotNet.PathStructure; using HandlebarsDotNet.Polyfills; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { @@ -142,7 +139,7 @@ private static void WriteWithIndent(EncodedTextWriter writer, string? content, s } var pos = 0; - while (pos < content.Length) + while (pos < content!.Length) { var newlinePos = content.IndexOf('\n', pos); if (newlinePos < 0) diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 9db57165..2686a376 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -1,9 +1,8 @@ using System.Linq.Expressions; -using Expressions.Shortcuts; using HandlebarsDotNet.Helpers; using HandlebarsDotNet.PathStructure; using HandlebarsDotNet.Runtime; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs index ba8641ba..505d2c05 100644 --- a/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs +++ b/source/Handlebars/Compiler/Translation/Expression/StaticReplacer.cs @@ -1,6 +1,5 @@ using System.Linq.Expressions; -using Expressions.Shortcuts; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs index 48b69c46..abdfee1c 100644 --- a/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/SubExpressionVisitor.cs @@ -1,7 +1,6 @@ using System.Linq.Expressions; -using Expressions.Shortcuts; using HandlebarsDotNet.Helpers; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs index 72e6300b..dfc2e312 100644 --- a/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs +++ b/source/Handlebars/Compiler/Translation/Expression/UnencodedStatementVisitor.cs @@ -1,6 +1,5 @@ using System.Linq.Expressions; -using Expressions.Shortcuts; -using static Expressions.Shortcuts.ExpressionShortcuts; +using static HandlebarsDotNet.ExpressionShortcuts.ExpressionShortcuts; namespace HandlebarsDotNet.Compiler { diff --git a/source/Handlebars/ExpressionShortcuts/BlockBuilder.cs b/source/Handlebars/ExpressionShortcuts/BlockBuilder.cs new file mode 100644 index 00000000..acce4895 --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/BlockBuilder.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace HandlebarsDotNet.ExpressionShortcuts +{ + /// + /// Shortcut for + /// + internal class BlockBuilder: ExpressionContainer + { + private readonly Type? _returnType; + private readonly List _expressions; + private readonly HashSet _parameters; + + internal BlockBuilder(Type? returnType) : base(Expression.Empty()) + { + _returnType = returnType; + _expressions = new List(); + _parameters = new HashSet(); + } + + /// + public override Expression Expression => + _returnType == null + ? Expression.Block(_parameters, _expressions) + : Expression.Block(_returnType, _parameters, _expressions); + + /// + /// Adds parameter to + /// + public BlockBuilder Parameter(out ExpressionContainer parameter) + { + var expression = Expression.Parameter(typeof(T)); + parameter = ExpressionShortcuts.Arg(expression); + return Parameter(expression); + } + + /// + /// Adds parameter to + /// + public BlockBuilder Parameter(ParameterExpression e) + { + _parameters.Add(e); + return this; + } + + /// + /// Adds new "line" to + /// + public BlockBuilder Line(Expression e) + { + _expressions.Add(e); + return this; + } + + /// + /// Adds multiple new "lines" to + /// + public BlockBuilder Lines(IEnumerable e) + { + _expressions.AddRange(e); + return this; + } + + /// + /// Creates out of current . + /// + public Expression Lambda(IEnumerable parameters) where T : class + { + return Expression.Lambda(Expression, parameters); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionContainer.cs b/source/Handlebars/ExpressionShortcuts/ExpressionContainer.cs new file mode 100644 index 00000000..5f7cb49a --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/ExpressionContainer.cs @@ -0,0 +1,56 @@ +using System.Linq.Expressions; + +namespace HandlebarsDotNet.ExpressionShortcuts +{ + /// + /// Wrapper around of to provide addition functionality + /// + internal class ExpressionContainer + { + /// + /// + /// + /// + public ExpressionContainer(Expression expression) => Expression = expression; + + /// + /// Return the underling + /// + public virtual Expression Expression { get; } + + /// + /// + /// + /// + /// + public static implicit operator Expression(ExpressionContainer expressionContainer) => expressionContainer.Expression; + + /// + /// + /// + /// + /// + public static implicit operator ExpressionContainer(Expression expression) => new ExpressionContainer(expression); + } + + /// + /// Provides strongly typed container for . + /// + /// Used to trick C# compiler in cases like in order to pass value to target method. + /// Type of expected result value. + internal class ExpressionContainer : ExpressionContainer + { + /// + /// Used to trick C# compiler + /// + public static implicit operator T(ExpressionContainer _0) => default(T)!; + + /// + /// + /// + /// + public ExpressionContainer(Expression expression) : base(expression) + { + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionContainerExtensions.cs b/source/Handlebars/ExpressionShortcuts/ExpressionContainerExtensions.cs new file mode 100644 index 00000000..a49ffc65 --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/ExpressionContainerExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; + +namespace HandlebarsDotNet.ExpressionShortcuts +{ + internal static partial class ExpressionShortcuts + { + /// + /// Creates strongly typed representation of the + /// + /// + /// Property accessor expression + /// Expected type of resulting target + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Property(this ExpressionContainer instance, Expression> propertyAccessor) + { + return Property(instance.Expression, propertyAccessor); + } + + /// + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. + /// + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Call(this ExpressionContainer instance, Expression> invocationExpression) + { + return new ExpressionContainer(ExpressionUtils.ProcessCallLambda(invocationExpression, instance)); + } + + /// + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. + /// + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Call(this ExpressionContainer instance, Expression> invocationExpression) + { + return ExpressionShortcuts.Arg(ExpressionUtils.ProcessCallLambda(invocationExpression, instance)); + } + + /// + /// Creates assign . + /// Parameters are resolved based on actual passed parameters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Assign(this ExpressionContainer target, ExpressionContainer value) + { + return new ExpressionContainer(Expression.Assign(target, value)); + } + + /// + /// Creates assign . + /// Parameters are resolved based on actual passed parameters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Assign(this ExpressionContainer target, T value) + { + return new ExpressionContainer(Expression.Assign(target, Expression.Constant(value, typeof(T)))); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionExtractorVisitor.cs b/source/Handlebars/ExpressionShortcuts/ExpressionExtractorVisitor.cs new file mode 100644 index 00000000..a95300c8 --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/ExpressionExtractorVisitor.cs @@ -0,0 +1,79 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +namespace HandlebarsDotNet.ExpressionShortcuts +{ + internal class ExpressionExtractorVisitor : ExpressionVisitor + { + [return: NotNullIfNotNull("node")] + public override Expression? Visit(Expression? node) + { + if (node is LambdaExpression lambda) + { + return ExpressionUtils.ProcessCall(lambda.Body); + } + + return base.Visit(node); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + var dynamicInvoke = Expression.Lambda(node).Compile().DynamicInvoke(); + return ConvertToExpression(dynamicInvoke); + } + + protected override Expression VisitMember(MemberExpression node) + { + switch (node.Expression) + { + case ConstantExpression constant: + var constantValue = constant.Value; + var value = constantValue!.GetType().GetField(node.Member.Name)?.GetValue(constantValue); + if (value is ExpressionContainer expressionContainer) return expressionContainer.Expression; + if (value?.GetType() == node.Type) return ConvertToExpression(value); + + return Visit(Expression.Convert(Expression.Constant(value), node.Type)); + + default: + return base.VisitMember(node); + } + } + + protected override Expression VisitUnary(UnaryExpression node) + { + switch (node.NodeType) + { + case ExpressionType.ConvertChecked: + case ExpressionType.Convert: + { + if (!typeof(ExpressionContainer).IsAssignableFrom(node.Operand.Type)) + return node.Type == node.Operand.Type + ? node.Update(Visit(node.Operand)) + : node; + + var operand = Visit(node.Operand); + if (operand.Type == typeof(void)) return operand; + + if (typeof(ExpressionContainer).IsAssignableFrom(node.Type)) + { + return operand; + } + + return operand.Type != node.Type + ? Expression.Convert(operand, node.Type) + : operand; + } + + default: + return base.VisitUnary(node); + } + } + + private Expression ConvertToExpression(object? value) + { + if (value is ExpressionContainer expressionContainer) return expressionContainer.Expression; + if (value is Expression expression) return Visit(expression); + return Expression.Constant(value); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionShortcuts.cs b/source/Handlebars/ExpressionShortcuts/ExpressionShortcuts.cs new file mode 100644 index 00000000..25f4a6b4 --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/ExpressionShortcuts.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; + +namespace HandlebarsDotNet.ExpressionShortcuts +{ + /// + /// Stands for shortcuts. + /// + internal static partial class ExpressionShortcuts + { + /// + /// Creates strongly typed representation of the + /// + /// If is null returns result of + /// to wrap + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Arg(Expression? expression) => expression == null ? Null() : new ExpressionContainer(expression); + + /// + /// Creates strongly typed representation of the + /// + /// If is null returns result of + /// to wrap + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Arg(T? value) => value == null ? Null() : new ExpressionContainer(Expression.Constant(value, typeof(T))); + + /// + /// Creates strongly typed representation of the . + /// + /// If is null returns result of + /// to wrap + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Arg(Expression? expression) => expression == null ? Null() : new ExpressionContainer(expression); + + /// + /// Creates strongly typed representation of the and performs on it. + /// + /// If is null returns result of + /// to wrap + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Cast(Expression? expression) => expression == null ? Null() : new ExpressionContainer(Expression.Convert(expression, typeof(T))); + + /// + /// Creates strongly typed representation of the + /// + /// Variable name. Corresponds to type name if omitted. + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Parameter(string? name = null) + { + return new ExpressionContainer(Expression.Parameter(typeof(T), name ?? typeof(T).Name)); + } + + /// + /// Creates strongly typed representation of the + /// + /// Variable name. Corresponds to type name if omitted. + /// Property accessor expression + /// Expected type of resulting target + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ExpressionContainer Property(Expression instance, Expression> propertyLambda) + { + return Arg(ExpressionUtils.ProcessPropertyLambda(instance, propertyLambda)); + } + + /// + /// Creates strongly typed representation of the + /// + /// Items for the new array + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Array(IEnumerable items) + { + return Arg(Expression.NewArrayInit(typeof(T), items)); + } + + /// + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Call(Expression invocationExpression) + { + return new ExpressionContainer(ExpressionUtils.ProcessCallLambda(invocationExpression)); + } + + /// + /// Creates or based on . + /// Parameters are resolved based on actual passed parameters. + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer Call(Expression> invocationExpression) + { + return Arg(ExpressionUtils.ProcessCallLambda(invocationExpression)); + } + + /// + /// Creates . Parameters for constructor and constructor itself are resolved based . + /// + /// Expression used to invoke the method. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ExpressionContainer New(Expression> invocationExpression) + { + return Arg(ExpressionUtils.ProcessCallLambda(invocationExpression)); + } + + /// + /// Provides fluent interface for creation + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BlockBuilder Block(Type? returnType = null) + { + return new BlockBuilder(returnType); + } + + /// + /// Creates strongly typed representation of null. + /// + /// Expected type of resulting + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ExpressionContainer Null() + { + return Arg(Null(typeof(T))); + } + + /// + /// Creates strongly typed representation of null. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ExpressionContainer Null(Type type) + { + return new ExpressionContainer(Expression.Convert(Expression.Constant(null), type)); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionUtils.cs b/source/Handlebars/ExpressionShortcuts/ExpressionUtils.cs new file mode 100644 index 00000000..717ef732 --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/ExpressionUtils.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace HandlebarsDotNet.ExpressionShortcuts +{ + /// + /// + /// + internal static class ExpressionUtils + { + /// + /// Visits and replaces by performing match by + /// + private static IEnumerable ReplaceParameters(IEnumerable expressions, IList newValues) + { + return (newValues.Count != 0 + ? PerformReplacement() + : expressions)!; + + IEnumerable PerformReplacement() + { + var visitor = new ParameterReplacerVisitor(newValues); + return expressions.Where(o => o != null).Select(visitor.Visit)!; + } + } + + /// + /// Visits and replaces by performing match by + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(expression))] + private static Expression? ReplaceParameters(Expression? expression, params Expression?[] newValues) + { + var visitor = new ParameterReplacerVisitor(newValues); + return visitor.Visit(expression); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Expression ProcessPropertyLambda(Expression instance, LambdaExpression propertyLambda) + { + if (propertyLambda.Body is not MemberExpression member) + throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property."); + + if (member.Member is not PropertyInfo) + throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property."); + + return ReplaceParameters(ExtractArgument(member), instance); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Expression ProcessCallLambda(LambdaExpression propertyLambda, Expression? instance = null) + { + return ProcessCall(propertyLambda.Body, instance); + } + + internal static Expression ProcessCall(Expression propertyLambda, Expression? instance = null) + { + switch (propertyLambda) + { + case NewExpression newExpression: + return newExpression.Update(ExtractArguments(newExpression.Arguments)); + + case MethodCallExpression member: + var methodInfo = member.Method; + var parameters = instance != null ? new[] { instance } : new Expression[0]; + instance = ReplaceParameters(new[] {member.Object}, parameters).SingleOrDefault(); + IEnumerable methodCallArguments = member.Arguments; + methodCallArguments = ReplaceParameters(methodCallArguments, parameters).Select(ExtractArgument); + var memberObject = methodInfo.IsStatic + ? null : Expression.Convert(instance!, methodInfo.DeclaringType!); + + return Expression.Call(memberObject, methodInfo, methodCallArguments); + + case InvocationExpression invocationExpression: + return invocationExpression.Update( + invocationExpression.Expression, + ExtractArguments(invocationExpression.Arguments) + ); + + default: + return ReplaceParameters(ExtractArgument(propertyLambda), instance); + } + } + + private static IReadOnlyCollection ExtractArguments(IReadOnlyCollection expressions) + { + var result = new Expression[expressions.Count]; + if (expressions is IList list) + { + for (var index = 0; index < list.Count; index++) + { + result[index] = ExtractArgument(list[index]); + } + } + else + { + int index = 0; + foreach (var expr in expressions) + { + result[index++] = ExtractArgument(expr); + } + } + + return result; + } + + private static readonly ExpressionExtractorVisitor ExtractorVisitor = new ExpressionExtractorVisitor(); + + private static Expression ExtractArgument(Expression expr) + { + return ExtractorVisitor.Visit(expr); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/LICENSE b/source/Handlebars/ExpressionShortcuts/LICENSE new file mode 100644 index 00000000..9057436c --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Oleh Formaniuk + +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. \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ParameterReplacerVisitor.cs b/source/Handlebars/ExpressionShortcuts/ParameterReplacerVisitor.cs new file mode 100644 index 00000000..2ff26080 --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/ParameterReplacerVisitor.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace HandlebarsDotNet.ExpressionShortcuts +{ + internal sealed class ParameterReplacerVisitor : ExpressionVisitor + { + private readonly List _replacements; + + public ParameterReplacerVisitor(IEnumerable replacements) + { + _replacements = replacements.Where(o => o != null).ToList()!; + } + + protected override Expression VisitParameter(ParameterExpression node) + { + var replacement = _replacements.FirstOrDefault(o => o.Type == node.Type); + if (replacement == null || replacement == node) + { + return base.VisitParameter(node); + } + + return Visit(replacement)!; + } + } +} \ No newline at end of file diff --git a/source/Handlebars/Handlebars.csproj b/source/Handlebars/Handlebars.csproj index e15d5ba3..6358cd5c 100644 --- a/source/Handlebars/Handlebars.csproj +++ b/source/Handlebars/Handlebars.csproj @@ -41,8 +41,7 @@ - - + From 6d9fb8004558690a48962401a87e0323d925bf7f Mon Sep 17 00:00:00 2001 From: Matthias Vill Date: Tue, 11 Aug 2026 00:28:17 +0200 Subject: [PATCH 2/2] Simplify ExpressionShortcuts for our use-case --- .../Translation/Expression/PartialBinder.cs | 51 +++----- .../ExpressionContainer.cs | 15 +-- .../ExpressionContainerExtensions.cs | 21 ++-- .../ExpressionExtractorVisitor.cs | 79 ------------ .../ExpressionShortcuts.cs | 44 ++----- .../ExpressionShortcuts/ExpressionUtils.cs | 119 ------------------ .../ParameterReplacerVisitor.cs | 27 ---- .../ReplaceParameterVisitor.cs | 15 +++ .../UnpackExpressionContainerVisitor.cs | 66 ++++++++++ 9 files changed, 124 insertions(+), 313 deletions(-) delete mode 100644 source/Handlebars/ExpressionShortcuts/ExpressionExtractorVisitor.cs delete mode 100644 source/Handlebars/ExpressionShortcuts/ExpressionUtils.cs delete mode 100644 source/Handlebars/ExpressionShortcuts/ParameterReplacerVisitor.cs create mode 100644 source/Handlebars/ExpressionShortcuts/ReplaceParameterVisitor.cs create mode 100644 source/Handlebars/ExpressionShortcuts/UnpackExpressionContainerVisitor.cs diff --git a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs index f3f4d6b1..879719e8 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs @@ -30,26 +30,27 @@ protected override Expression VisitPartialExpression(PartialExpression pex) ? FunctionBuilder.Compile(new[] { pex.Fallback }, CompilationContext, out decorators) : null; - if (decorators.Count > 0) + var bindingContext = CompilationContext.Args.BindingContext; + var writer = CompilationContext.Args.EncodedWriter; + + var parentContext = bindingContext; + if (pex.Argument != null || partialBlockTemplate != null) { - var bindingContext = CompilationContext.Args.BindingContext; - var writer = CompilationContext.Args.EncodedWriter; + var value = pex.Argument != null + ? Arg(FunctionBuilder.Reduce(pex.Argument, CompilationContext, out _)) + : bindingContext.Property(o => o.Value); - var parentContext = bindingContext; - if (pex.Argument != null || partialBlockTemplate != null) - { - var value = pex.Argument != null - ? Arg(FunctionBuilder.Reduce(pex.Argument, CompilationContext, out _)) - : bindingContext.Property(o => o.Value); + var partialTemplate = Arg(partialBlockTemplate); + bindingContext = bindingContext.Call(o => o.CreateChildContext(value, partialTemplate)); + } - var partialTemplate = Arg(partialBlockTemplate); - bindingContext = bindingContext.Call(o => o.CreateChildContext(value, partialTemplate)); - } + var partialName = Cast(pex.PartialName); + var configuration = Arg(CompilationContext.Configuration); + var isBlock = Arg(pex.IsBlock); + var indent = Arg(pex.Indent); - var partialName = Cast(pex.PartialName); - var configuration = Arg(CompilationContext.Configuration); - var isBlock = Arg(pex.IsBlock); - var indent = Arg(pex.Indent); + if (decorators.Count > 0) + { var templateDelegate = FunctionBuilder.Compile( new [] { @@ -67,24 +68,6 @@ out _ } else { - var bindingContext = CompilationContext.Args.BindingContext; - var writer = CompilationContext.Args.EncodedWriter; - - if (pex.Argument != null || partialBlockTemplate != null) - { - var value = pex.Argument != null - ? Arg(FunctionBuilder.Reduce(pex.Argument, CompilationContext, out _)) - : bindingContext.Property(o => o.Value); - - var partialTemplate = Arg(partialBlockTemplate); - bindingContext = bindingContext.Call(o => o.CreateChildContext(value, partialTemplate)); - } - - var partialName = Cast(pex.PartialName); - var configuration = Arg(CompilationContext.Configuration); - var isBlock = Arg(pex.IsBlock); - var indent = Arg(pex.Indent); - return Call(() => InvokePartialWithFallback(partialName, bindingContext, writer, (ICompiledHandlebarsConfiguration) configuration, isBlock, indent) ); diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionContainer.cs b/source/Handlebars/ExpressionShortcuts/ExpressionContainer.cs index 5f7cb49a..8a3569db 100644 --- a/source/Handlebars/ExpressionShortcuts/ExpressionContainer.cs +++ b/source/Handlebars/ExpressionShortcuts/ExpressionContainer.cs @@ -8,7 +8,7 @@ namespace HandlebarsDotNet.ExpressionShortcuts internal class ExpressionContainer { /// - /// + /// This can be used to wrap a -Type expression or implement a derived type /// /// public ExpressionContainer(Expression expression) => Expression = expression; @@ -19,18 +19,11 @@ internal class ExpressionContainer public virtual Expression Expression { get; } /// - /// + /// Convenience for unwrapping ExpressionContainer /// /// /// public static implicit operator Expression(ExpressionContainer expressionContainer) => expressionContainer.Expression; - - /// - /// - /// - /// - /// - public static implicit operator ExpressionContainer(Expression expression) => new ExpressionContainer(expression); } /// @@ -41,12 +34,12 @@ internal class ExpressionContainer internal class ExpressionContainer : ExpressionContainer { /// - /// Used to trick C# compiler + /// Used to trick C# compiler - calls need to be removed through /// public static implicit operator T(ExpressionContainer _0) => default(T)!; /// - /// + /// Wrap an expression of Type /// /// public ExpressionContainer(Expression expression) : base(expression) diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionContainerExtensions.cs b/source/Handlebars/ExpressionShortcuts/ExpressionContainerExtensions.cs index a49ffc65..2eacf4d8 100644 --- a/source/Handlebars/ExpressionShortcuts/ExpressionContainerExtensions.cs +++ b/source/Handlebars/ExpressionShortcuts/ExpressionContainerExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq.Expressions; using System.Runtime.CompilerServices; @@ -17,7 +18,9 @@ internal static partial class ExpressionShortcuts [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ExpressionContainer Property(this ExpressionContainer instance, Expression> propertyAccessor) { - return Property(instance.Expression, propertyAccessor); + var visitor = new ReplaceParameterVisitor(propertyAccessor.Parameters[0], instance); + var combinedExpression = visitor.Visit(propertyAccessor.Body); + return new ExpressionContainer(combinedExpression); } /// @@ -30,7 +33,9 @@ public static ExpressionContainer Property(this ExpressionContainer(this ExpressionContainer instance, Expression> invocationExpression) { - return new ExpressionContainer(ExpressionUtils.ProcessCallLambda(invocationExpression, instance)); + var visitor = new ReplaceParameterVisitor(invocationExpression.Parameters[0], instance); + var combinedExpression = visitor.Visit(invocationExpression.Body); + return new ExpressionContainer(combinedExpression); } /// @@ -43,7 +48,9 @@ public static ExpressionContainer Call(this ExpressionContainer instance, [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ExpressionContainer Call(this ExpressionContainer instance, Expression> invocationExpression) { - return ExpressionShortcuts.Arg(ExpressionUtils.ProcessCallLambda(invocationExpression, instance)); + var visitor = new ReplaceParameterVisitor(invocationExpression.Parameters[0], instance); + var combinedExpression = visitor.Visit(invocationExpression.Body); + return new ExpressionContainer(combinedExpression); } /// @@ -51,9 +58,9 @@ public static ExpressionContainer Call(this ExpressionContainer in /// Parameters are resolved based on actual passed parameters. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Assign(this ExpressionContainer target, ExpressionContainer value) + public static ExpressionContainer Assign(this ExpressionContainer target, ExpressionContainer value) { - return new ExpressionContainer(Expression.Assign(target, value)); + return new ExpressionContainer(Expression.Assign(target, value)); } /// @@ -61,9 +68,9 @@ public static ExpressionContainer Assign(this ExpressionContainer target, /// Parameters are resolved based on actual passed parameters. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Assign(this ExpressionContainer target, T value) + public static ExpressionContainer Assign(this ExpressionContainer target, T value) { - return new ExpressionContainer(Expression.Assign(target, Expression.Constant(value, typeof(T)))); + return new ExpressionContainer(Expression.Assign(target, Expression.Constant(value, typeof(T)))); } } } \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionExtractorVisitor.cs b/source/Handlebars/ExpressionShortcuts/ExpressionExtractorVisitor.cs deleted file mode 100644 index a95300c8..00000000 --- a/source/Handlebars/ExpressionShortcuts/ExpressionExtractorVisitor.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; - -namespace HandlebarsDotNet.ExpressionShortcuts -{ - internal class ExpressionExtractorVisitor : ExpressionVisitor - { - [return: NotNullIfNotNull("node")] - public override Expression? Visit(Expression? node) - { - if (node is LambdaExpression lambda) - { - return ExpressionUtils.ProcessCall(lambda.Body); - } - - return base.Visit(node); - } - - protected override Expression VisitMethodCall(MethodCallExpression node) - { - var dynamicInvoke = Expression.Lambda(node).Compile().DynamicInvoke(); - return ConvertToExpression(dynamicInvoke); - } - - protected override Expression VisitMember(MemberExpression node) - { - switch (node.Expression) - { - case ConstantExpression constant: - var constantValue = constant.Value; - var value = constantValue!.GetType().GetField(node.Member.Name)?.GetValue(constantValue); - if (value is ExpressionContainer expressionContainer) return expressionContainer.Expression; - if (value?.GetType() == node.Type) return ConvertToExpression(value); - - return Visit(Expression.Convert(Expression.Constant(value), node.Type)); - - default: - return base.VisitMember(node); - } - } - - protected override Expression VisitUnary(UnaryExpression node) - { - switch (node.NodeType) - { - case ExpressionType.ConvertChecked: - case ExpressionType.Convert: - { - if (!typeof(ExpressionContainer).IsAssignableFrom(node.Operand.Type)) - return node.Type == node.Operand.Type - ? node.Update(Visit(node.Operand)) - : node; - - var operand = Visit(node.Operand); - if (operand.Type == typeof(void)) return operand; - - if (typeof(ExpressionContainer).IsAssignableFrom(node.Type)) - { - return operand; - } - - return operand.Type != node.Type - ? Expression.Convert(operand, node.Type) - : operand; - } - - default: - return base.VisitUnary(node); - } - } - - private Expression ConvertToExpression(object? value) - { - if (value is ExpressionContainer expressionContainer) return expressionContainer.Expression; - if (value is Expression expression) return Visit(expression); - return Expression.Constant(value); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionShortcuts.cs b/source/Handlebars/ExpressionShortcuts/ExpressionShortcuts.cs index 25f4a6b4..0ff12776 100644 --- a/source/Handlebars/ExpressionShortcuts/ExpressionShortcuts.cs +++ b/source/Handlebars/ExpressionShortcuts/ExpressionShortcuts.cs @@ -13,42 +13,38 @@ internal static partial class ExpressionShortcuts /// /// Creates strongly typed representation of the /// - /// If is null returns result of /// to wrap /// Expected type of resulting /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Arg(Expression? expression) => expression == null ? Null() : new ExpressionContainer(expression); + public static ExpressionContainer Arg(Expression expression) => new ExpressionContainer(expression); /// /// Creates strongly typed representation of the /// - /// If is null returns result of /// to wrap /// Expected type of resulting /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Arg(T? value) => value == null ? Null() : new ExpressionContainer(Expression.Constant(value, typeof(T))); + public static ExpressionContainer Arg(T value) => new ExpressionContainer(Expression.Constant(value, typeof(T))); /// /// Creates strongly typed representation of the . /// - /// If is null returns result of /// to wrap /// Expected type of resulting /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Arg(Expression? expression) => expression == null ? Null() : new ExpressionContainer(expression); + public static ExpressionContainer Arg(Expression expression) => new ExpressionContainer(expression); /// /// Creates strongly typed representation of the and performs on it. /// - /// If is null returns result of /// to wrap /// Expected type of resulting /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ExpressionContainer Cast(Expression? expression) => expression == null ? Null() : new ExpressionContainer(Expression.Convert(expression, typeof(T))); + public static ExpressionContainer Cast(Expression expression) => new ExpressionContainer(Expression.Convert(expression, typeof(T))); /// /// Creates strongly typed representation of the @@ -62,20 +58,6 @@ public static ExpressionContainer Parameter(string? name = null) return new ExpressionContainer(Expression.Parameter(typeof(T), name ?? typeof(T).Name)); } - /// - /// Creates strongly typed representation of the - /// - /// Variable name. Corresponds to type name if omitted. - /// Property accessor expression - /// Expected type of resulting target - /// Expected type of resulting - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ExpressionContainer Property(Expression instance, Expression> propertyLambda) - { - return Arg(ExpressionUtils.ProcessPropertyLambda(instance, propertyLambda)); - } - /// /// Creates strongly typed representation of the /// @@ -97,7 +79,7 @@ public static ExpressionContainer Array(IEnumerable items) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ExpressionContainer Call(Expression invocationExpression) { - return new ExpressionContainer(ExpressionUtils.ProcessCallLambda(invocationExpression)); + return new ExpressionContainer(UnpackExpressionContainerVisitor.Instance.Visit(invocationExpression.Body)); } /// @@ -109,7 +91,7 @@ public static ExpressionContainer Call(Expression invocationExpression) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ExpressionContainer Call(Expression> invocationExpression) { - return Arg(ExpressionUtils.ProcessCallLambda(invocationExpression)); + return new ExpressionContainer(UnpackExpressionContainerVisitor.Instance.Visit(invocationExpression.Body)); } /// @@ -120,7 +102,7 @@ public static ExpressionContainer Call(Expression> invocationExpre [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ExpressionContainer New(Expression> invocationExpression) { - return Arg(ExpressionUtils.ProcessCallLambda(invocationExpression)); + return new ExpressionContainer(UnpackExpressionContainerVisitor.Instance.Visit(invocationExpression.Body)); } /// @@ -140,17 +122,7 @@ public static BlockBuilder Block(Type? returnType = null) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ExpressionContainer Null() { - return Arg(Null(typeof(T))); - } - - /// - /// Creates strongly typed representation of null. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ExpressionContainer Null(Type type) - { - return new ExpressionContainer(Expression.Convert(Expression.Constant(null), type)); + return Arg(Expression.Constant(null, typeof(T))); } } } \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ExpressionUtils.cs b/source/Handlebars/ExpressionShortcuts/ExpressionUtils.cs deleted file mode 100644 index 717ef732..00000000 --- a/source/Handlebars/ExpressionShortcuts/ExpressionUtils.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; - -namespace HandlebarsDotNet.ExpressionShortcuts -{ - /// - /// - /// - internal static class ExpressionUtils - { - /// - /// Visits and replaces by performing match by - /// - private static IEnumerable ReplaceParameters(IEnumerable expressions, IList newValues) - { - return (newValues.Count != 0 - ? PerformReplacement() - : expressions)!; - - IEnumerable PerformReplacement() - { - var visitor = new ParameterReplacerVisitor(newValues); - return expressions.Where(o => o != null).Select(visitor.Visit)!; - } - } - - /// - /// Visits and replaces by performing match by - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [return: NotNullIfNotNull(nameof(expression))] - private static Expression? ReplaceParameters(Expression? expression, params Expression?[] newValues) - { - var visitor = new ParameterReplacerVisitor(newValues); - return visitor.Visit(expression); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Expression ProcessPropertyLambda(Expression instance, LambdaExpression propertyLambda) - { - if (propertyLambda.Body is not MemberExpression member) - throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property."); - - if (member.Member is not PropertyInfo) - throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property."); - - return ReplaceParameters(ExtractArgument(member), instance); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Expression ProcessCallLambda(LambdaExpression propertyLambda, Expression? instance = null) - { - return ProcessCall(propertyLambda.Body, instance); - } - - internal static Expression ProcessCall(Expression propertyLambda, Expression? instance = null) - { - switch (propertyLambda) - { - case NewExpression newExpression: - return newExpression.Update(ExtractArguments(newExpression.Arguments)); - - case MethodCallExpression member: - var methodInfo = member.Method; - var parameters = instance != null ? new[] { instance } : new Expression[0]; - instance = ReplaceParameters(new[] {member.Object}, parameters).SingleOrDefault(); - IEnumerable methodCallArguments = member.Arguments; - methodCallArguments = ReplaceParameters(methodCallArguments, parameters).Select(ExtractArgument); - var memberObject = methodInfo.IsStatic - ? null : Expression.Convert(instance!, methodInfo.DeclaringType!); - - return Expression.Call(memberObject, methodInfo, methodCallArguments); - - case InvocationExpression invocationExpression: - return invocationExpression.Update( - invocationExpression.Expression, - ExtractArguments(invocationExpression.Arguments) - ); - - default: - return ReplaceParameters(ExtractArgument(propertyLambda), instance); - } - } - - private static IReadOnlyCollection ExtractArguments(IReadOnlyCollection expressions) - { - var result = new Expression[expressions.Count]; - if (expressions is IList list) - { - for (var index = 0; index < list.Count; index++) - { - result[index] = ExtractArgument(list[index]); - } - } - else - { - int index = 0; - foreach (var expr in expressions) - { - result[index++] = ExtractArgument(expr); - } - } - - return result; - } - - private static readonly ExpressionExtractorVisitor ExtractorVisitor = new ExpressionExtractorVisitor(); - - private static Expression ExtractArgument(Expression expr) - { - return ExtractorVisitor.Visit(expr); - } - } -} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ParameterReplacerVisitor.cs b/source/Handlebars/ExpressionShortcuts/ParameterReplacerVisitor.cs deleted file mode 100644 index 2ff26080..00000000 --- a/source/Handlebars/ExpressionShortcuts/ParameterReplacerVisitor.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; - -namespace HandlebarsDotNet.ExpressionShortcuts -{ - internal sealed class ParameterReplacerVisitor : ExpressionVisitor - { - private readonly List _replacements; - - public ParameterReplacerVisitor(IEnumerable replacements) - { - _replacements = replacements.Where(o => o != null).ToList()!; - } - - protected override Expression VisitParameter(ParameterExpression node) - { - var replacement = _replacements.FirstOrDefault(o => o.Type == node.Type); - if (replacement == null || replacement == node) - { - return base.VisitParameter(node); - } - - return Visit(replacement)!; - } - } -} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/ReplaceParameterVisitor.cs b/source/Handlebars/ExpressionShortcuts/ReplaceParameterVisitor.cs new file mode 100644 index 00000000..7fd39b74 --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/ReplaceParameterVisitor.cs @@ -0,0 +1,15 @@ +using System.Linq.Expressions; + +namespace HandlebarsDotNet.ExpressionShortcuts +{ + internal sealed class ReplaceParameterVisitor(ParameterExpression parameter, Expression argument) + : UnpackExpressionContainerVisitor + { + protected override Expression VisitParameter(ParameterExpression node) + { + return ReferenceEquals(node, parameter) + ? argument + : base.VisitParameter(node); + } + } +} \ No newline at end of file diff --git a/source/Handlebars/ExpressionShortcuts/UnpackExpressionContainerVisitor.cs b/source/Handlebars/ExpressionShortcuts/UnpackExpressionContainerVisitor.cs new file mode 100644 index 00000000..cbc6048e --- /dev/null +++ b/source/Handlebars/ExpressionShortcuts/UnpackExpressionContainerVisitor.cs @@ -0,0 +1,66 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace HandlebarsDotNet.ExpressionShortcuts +{ + internal class UnpackExpressionContainerVisitor : ExpressionVisitor + { + public static readonly UnpackExpressionContainerVisitor Instance = new(); + + protected override Expression VisitMember(MemberExpression node) + { + if (node is + { + Member: FieldInfo fieldInfo, + Expression: {} nextedExpression + } + && typeof(ExpressionContainer).IsAssignableFrom(fieldInfo.FieldType)) + { + var nestedValue = ExtractFieldOrConstantValue(nextedExpression); + if (nestedValue != null + && fieldInfo.GetValue(nestedValue) is ExpressionContainer expressionContainer) + return expressionContainer.Expression; + } + + return base.VisitMember(node); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + // Did somebody use the implicit operator to convert ExpressionContainer to T? + if (node.NodeType is ExpressionType.ConvertChecked or ExpressionType.Convert + && typeof(ExpressionContainer).IsAssignableFrom(node.Operand.Type)) + { + var operand = Visit(node.Operand); + + return operand.Type != node.Type + ? Expression.Convert(operand, node.Type) + : operand; + } + + return base.VisitUnary(node); + } + + // The C# compiler actually creates closure-objects that put values as fields on an anonymous object -> unwrap + private static object? ExtractFieldOrConstantValue(Expression node) + { + switch (node) + { + case ConstantExpression constantExpression: + return constantExpression.Value; + case MemberExpression + { + Member: FieldInfo fieldInfo, + Expression: { } nestedExpression + }: + { + // In certain cases the compiler may use nested structures. + var nestedValue = ExtractFieldOrConstantValue(nestedExpression); + return nestedValue == null ? null : fieldInfo.GetValue(nestedValue); + } + default: + return null; + } + } + } +} \ No newline at end of file