private static TCall SetupSetImpl <T, TCall>( Mock <T> mock, Action <T> setterExpression, Func <Mock, Expression, MethodInfo, Expression[], TCall> callFactory) where T : class where TCall : MethodCall { using (var context = new FluentMockContext()) { setterExpression(mock.Object); var last = context.LastInvocation; if (last == null) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, Resources.SetupOnNonOverridableMember, string.Empty)); } var setter = last.Invocation.Method; if (!setter.IsPropertySetter()) { throw new ArgumentException(Resources.SetupNotSetter); } // No need to call ThrowIfCantOverride as non-overridable would have thrown above already. // Get the variable name as used in the actual delegate :) // because of delegate currying, look at the last parameter for the Action's backing method, not the first var setterExpressionParameters = setterExpression.Method.GetParameters(); var parameterName = setterExpressionParameters[setterExpressionParameters.Length - 1].Name; var x = Expression.Parameter(last.Invocation.Method.DeclaringType, parameterName); var arguments = last.Invocation.Arguments; var parameters = setter.GetParameters(); var values = new Expression[arguments.Length]; if (last.Match == null) { // Length == 1 || Length == 2 (Indexer property) for (int i = 0; i < arguments.Length; i++) { values[i] = GetValueExpression(arguments[i], parameters[i].ParameterType); } var lambda = Expression.Lambda( typeof(Action <>).MakeGenericType(x.Type), Expression.Call(x, last.Invocation.Method, values), x); return(callFactory(last.Mock, lambda, last.Invocation.Method, values)); } else { var matchers = new Expression[arguments.Length]; var valueIndex = arguments.Length - 1; var propertyType = setter.GetParameters()[valueIndex].ParameterType; // If the value matcher is not equal to the property // type (i.e. prop is int?, but you use It.IsAny<int>()) // add a cast. if (last.Match.RenderExpression.Type != propertyType) { values[valueIndex] = Expression.Convert(last.Match.RenderExpression, propertyType); } else { values[valueIndex] = last.Match.RenderExpression; } matchers[valueIndex] = new MatchExpression(last.Match); if (arguments.Length == 2) { // TODO: what about multi-index setters? // Add the index value for the property indexer values[0] = GetValueExpression(arguments[0], parameters[0].ParameterType); // TODO: No matcher supported now for the index matchers[0] = values[0]; } var lambda = Expression.Lambda( typeof(Action <>).MakeGenericType(x.Type), Expression.Call(x, last.Invocation.Method, values), x); return(callFactory(last.Mock, lambda, last.Invocation.Method, matchers)); } } }
private void ToStringMatch(MatchExpression node) { ToString(node.Match.RenderExpression); return; }
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); } }
private static StringBuilder AppendExpression(this StringBuilder builder, MatchExpression expression) { return(builder.AppendExpression(expression.Match.RenderExpression)); }
private static SetupSetImplResult SetupSetImpl(Mock mock, Delegate setterExpression) { Mock target; Invocation invocation; AmbientObserver.Matches matches; using (var observer = AmbientObserver.Activate()) { setterExpression.DynamicInvoke(mock.Object); if (!observer.LastIsInvocation(out target, out invocation, out matches)) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, Resources.SetupOnNonVirtualMember, string.Empty)); } } var setter = invocation.Method; if (!setter.IsPropertySetter()) { throw new ArgumentException(Resources.SetupNotSetter); } // No need to call ThrowIfCantOverride as non-overridable would have thrown above already. // Get the variable name as used in the actual delegate :) // because of delegate currying, look at the last parameter for the Action's backing method, not the first var setterExpressionParameters = setterExpression.GetMethodInfo().GetParameters(); var parameterName = setterExpressionParameters[setterExpressionParameters.Length - 1].Name; var x = Expression.Parameter(invocation.Method.DeclaringType, parameterName); var arguments = invocation.Arguments; var parameters = setter.GetParameters(); var values = new Expression[arguments.Length]; if (matches.Count == 0) { // Length == 1 || Length == 2 (Indexer property) for (int i = 0; i < arguments.Length; i++) { values[i] = GetValueExpression(arguments[i], parameters[i].ParameterType); } var lambda = Expression.Lambda( typeof(Action <>).MakeGenericType(x.Type), Expression.Call(x, invocation.Method, values), x); return(new SetupSetImplResult(target, lambda, invocation.Method, values)); } else { // TODO: Use all observed matchers, not just the last one! var lastMatch = matches[matches.Count - 1]; var matchers = new Expression[arguments.Length]; var valueIndex = arguments.Length - 1; var propertyType = setter.GetParameters()[valueIndex].ParameterType; // If the value matcher is not equal to the property // type (i.e. prop is int?, but you use It.IsAny<int>()) // add a cast. if (lastMatch.RenderExpression.Type != propertyType) { values[valueIndex] = Expression.Convert(lastMatch.RenderExpression, propertyType); } else { values[valueIndex] = lastMatch.RenderExpression; } matchers[valueIndex] = new MatchExpression(lastMatch); for (int i = 0; i < arguments.Length - 1; i++) { // Add the index value for the property indexer values[i] = GetValueExpression(arguments[i], parameters[i].ParameterType); // TODO: No matcher supported now for the index matchers[i] = values[i]; } var lambda = Expression.Lambda( typeof(Action <>).MakeGenericType(x.Type), Expression.Call(x, invocation.Method, values), x); return(new SetupSetImplResult(target, lambda, invocation.Method, matchers)); } }