public static bool IsMatch(this Expression expression, out Match match) { if (expression is MatchExpression matchExpression) { match = matchExpression.Match; return(true); } using (var observer = MatcherObserver.Activate()) { Expression.Lambda <Action>(expression).CompileUsingExpressionCompiler().Invoke(); return(observer.TryGetLastMatch(out match)); } }
public override Expression <Action <T> > ReconstructExpression <T>(Action <T> action, object[] ctorArgs = null) { using (var matcherObserver = MatcherObserver.Activate()) { // Create the root recording proxy: var root = (T)CreateProxy(typeof(T), ctorArgs, matcherObserver, out var rootRecorder); Exception error = null; try { // Execute the delegate. The root recorder will automatically "mock" return values // and so build a chain of recorders, whereby each one records a single invocation // in a method chain `o.X.Y.Z`: action.Invoke(root); } catch (Exception ex) { // Something went wrong. We don't return this error right away. We want to // rebuild the expression tree as far as possible for diagnostic purposes. error = ex; } // Start the expression tree with a parameter of type `T`: var actionParameters = action.GetMethodInfo().GetParameters(); var actionParameterName = actionParameters[actionParameters.Length - 1].Name; var rootExpression = Expression.Parameter(typeof(T), actionParameterName); Expression body = rootExpression; // Then step through one recorded invocation at a time: for (var recorder = rootRecorder; recorder != null; recorder = recorder.Next) { var invocation = recorder.Invocation; if (invocation != null) { body = Expression.Call(body, invocation.Method, GetArgumentExpressions(invocation, recorder.Matches.ToArray())); } else { // A recorder was set up, but it recorded no invocation. This means // that the invocation could not be intercepted: throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, Resources.UnsupportedExpressionWithHint, $"{actionParameterName} => {body.ToStringFixed()}...", Resources.NextMemberNonInterceptable)); } } // Now we've either got no error and a completely reconstructed expression, or // we have an error and a partially reconstructed expression which we can use for // diagnostic purposes: if (error == null) { return(Expression.Lambda <Action <T> >(body.Apply(UpgradePropertyAccessorMethods.Rewriter), rootExpression)); } else { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, Resources.UnsupportedExpressionWithHint, $"{actionParameterName} => {body.ToStringFixed()}...", error.Message)); } } Expression[] GetArgumentExpressions(Invocation invocation, Match[] matches) { // First, let's pretend that all arguments are constant values: var parameterTypes = invocation.Method.GetParameterTypes(); var parameterCount = parameterTypes.Count; var expressions = new Expression[parameterCount]; for (int i = 0; i < parameterCount; ++i) { expressions[i] = Expression.Constant(invocation.Arguments[i], parameterTypes[i]); } // Now let's override the above constant expressions with argument matchers, if available: if (matches.Length > 0) { int matchIndex = 0; for (int argumentIndex = 0; matchIndex < matches.Length && argumentIndex < expressions.Length; ++argumentIndex) { // We are assuming that by default matchers return `default(T)`. If a matcher was used, // it will have left behind a `default(T)` argument, possibly coerced to the parameter type. // Therefore, we attempt to reproduce such coercions using `Convert.ChangeType`: Type defaultValueType = matches[matchIndex].RenderExpression.Type; object defaultValue = defaultValueType.GetDefaultValue(); try { defaultValue = Convert.ChangeType(defaultValue, parameterTypes[argumentIndex]); } catch { // Never mind, we tried. } if (!object.Equals(invocation.Arguments[argumentIndex], defaultValue)) { // This parameter has a non-`default` value. We therefore assume that it isn't // a value that was produced by a matcher. (See explanation in comment above.) continue; } if (parameterTypes[argumentIndex].IsAssignableFrom(defaultValue?.GetType() ?? defaultValueType)) { // We found a potential match. (Matcher type is assignment-compatible to parameter type.) if (matchIndex < matches.Length - 1 && !(argumentIndex < expressions.Length - 1 || CanDistribute(matchIndex + 1, argumentIndex + 1))) { // We get here if there are more matchers to distribute, // but we either: // * ran out of parameters to distribute over, or // * the remaining matchers can't be distributed over the remaining parameters. // In this case, we bail out, which will lead to an exception being thrown. break; } // The remaining matchers can be distributed over the remaining parameters, // so we can use up this matcher: expressions[argumentIndex] = new MatchExpression(matches[matchIndex]); ++matchIndex; } } if (matchIndex < matches.Length) { // If we get here, we can be almost certain that matchers weren't distributed properly // across the invocation's parameters. We could hope for the best and just leave it // at that; however, it's probably better to let client code know, so it can be either // adjusted or reported to Moq. throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, Resources.MatcherAssignmentFailedDuringExpressionReconstruction, matches.Length, $"{invocation.Method.DeclaringType.GetFormattedName()}.{invocation.Method.Name}")); } bool CanDistribute(int msi, int asi) { var match = matches[msi]; var matchType = match.RenderExpression.Type; for (int ai = asi; ai < expressions.Length; ++ai) { if (parameterTypes[ai].IsAssignableFrom(matchType) && CanDistribute(msi + 1, ai + 1)) { return(true); } } return(false); } } // Finally, add explicit type casts (aka `Convert` nodes) where necessary: for (int i = 0; i < expressions.Length; ++i) { var argument = expressions[i]; var parameterType = parameterTypes[i]; if (argument.Type == parameterType) { continue; } // nullable type coercion: if (Nullable.GetUnderlyingType(parameterType) != null && Nullable.GetUnderlyingType(argument.Type) == null) { expressions[i] = Expression.Convert(argument, parameterType); } // boxing of value types (i.e. where a value-typed value is assigned to a reference-typed parameter): else if (argument.Type.IsValueType && !parameterType.IsValueType) { expressions[i] = Expression.Convert(argument, parameterType); } // if types don't match exactly and aren't assignment compatible: else if (argument.Type != parameterType && !parameterType.IsAssignableFrom(argument.Type)) { expressions[i] = Expression.Convert(argument, parameterType); } } return(expressions); } }