From 2591eceb1aaeb195d6ae97f53f29c7cc26aeb68a Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Wed, 28 Oct 2020 04:49:43 -0300 Subject: [PATCH] Add codefix for setup of ref/out methods This is one of the most annoying scenarios when mocking APIs that have ref/out arguments. Figuring out the callback parameters, setting up the right output values in an untyped collection of some sort, it's terrible. So rather than that, offer a built-in way to generate a delegate that can be used to directly set up and implement the method, with full typed arguments including ref/out annotations, just like you would if you implemented the interface manually (which you likely do when things get complicated because of this issue!). For easier troubleshooting a VSIX project is also provided so you can more easily debug the completion provider. Setting up tests for this will be more complicated than for built-in analyzers, pending further investigation. --- Moq.sln | 8 +- src/Directory.Build.props | 1 + src/Directory.Packages.props | 5 +- .../CustomDelegateCodeFix.cs | 89 +++++++++---- .../CustomDelegateCompletion.cs | 126 ++++++++++++++++++ src/Moq.CodeAnalysis/Moq.CodeAnalysis.csproj | 3 +- src/Moq.CodeAnalysis/Resources.Designer.cs | 10 ++ src/Moq.CodeAnalysis/Resources.resx | 5 + src/Moq.Sdk/SetupScopeAttribute.cs | 19 +++ src/Moq.Vsix/Moq.Vsix.csproj | 55 ++++++++ src/Moq.Vsix/source.extension.vsixmanifest | 24 ++++ src/Moq/SetupExtension.cs | 5 + 12 files changed, 319 insertions(+), 31 deletions(-) rename src/{Moq.CodeFix => Moq.CodeAnalysis}/CustomDelegateCodeFix.cs (79%) create mode 100644 src/Moq.CodeAnalysis/CustomDelegateCompletion.cs create mode 100644 src/Moq.Sdk/SetupScopeAttribute.cs create mode 100644 src/Moq.Vsix/Moq.Vsix.csproj create mode 100644 src/Moq.Vsix/source.extension.vsixmanifest diff --git a/Moq.sln b/Moq.sln index a5b1359f..9d8d228f 100644 --- a/Moq.sln +++ b/Moq.sln @@ -39,7 +39,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\release.yml = .github\workflows\release.yml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moq.CodeAnalysis.UnitTests", "src\Moq.CodeAnalysis.UnitTests\Moq.CodeAnalysis.UnitTests.csproj", "{7EFC63B5-6978-40C9-8FEF-4FEEB4019B4C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.CodeAnalysis.UnitTests", "src\Moq.CodeAnalysis.UnitTests\Moq.CodeAnalysis.UnitTests.csproj", "{7EFC63B5-6978-40C9-8FEF-4FEEB4019B4C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Vsix", "src\Moq.Vsix\Moq.Vsix.csproj", "{EB67F50E-322F-4B22-9B30-EC80AE7CBE88}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,6 +89,10 @@ Global {7EFC63B5-6978-40C9-8FEF-4FEEB4019B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EFC63B5-6978-40C9-8FEF-4FEEB4019B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EFC63B5-6978-40C9-8FEF-4FEEB4019B4C}.Release|Any CPU.Build.0 = Release|Any CPU + {EB67F50E-322F-4B22-9B30-EC80AE7CBE88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB67F50E-322F-4B22-9B30-EC80AE7CBE88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB67F50E-322F-4B22-9B30-EC80AE7CBE88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB67F50E-322F-4B22-9B30-EC80AE7CBE88}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 0e08f270..da6597fc 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -32,6 +32,7 @@ https://pkg.kzu.io/index.json;https://api.nuget.org/v3/index.json;$(RestoreSources) $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\nugetizer\bin'));$(RestoreSources) $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\stunts\bin'));$(RestoreSources) + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\ThisAssembly\bin'));$(RestoreSources) $(MSBuildThisFileDirectory)Moq.snk 002400000480000094000000060200000024000052534131000400000100010051155fd0ee280be78d81cc979423f1129ec5dd28edce9cd94fd679890639cad54c121ebdb606f8659659cd313d3b3db7fa41e2271158dd602bb0039a142717117fa1f63d93a2d288a1c2f920ec05c4858d344a45d48ebd31c1368ab783596b382b611d8c92f9c1b3d338296aa21b12f3bc9f34de87756100c172c52a24bad2db diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e08b2eb0..a71cb368 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -8,7 +8,8 @@ - + + @@ -65,6 +66,8 @@ + + \ No newline at end of file diff --git a/src/Moq.CodeFix/CustomDelegateCodeFix.cs b/src/Moq.CodeAnalysis/CustomDelegateCodeFix.cs similarity index 79% rename from src/Moq.CodeFix/CustomDelegateCodeFix.cs rename to src/Moq.CodeAnalysis/CustomDelegateCodeFix.cs index d02d40f3..98e43f0c 100644 --- a/src/Moq.CodeFix/CustomDelegateCodeFix.cs +++ b/src/Moq.CodeAnalysis/CustomDelegateCodeFix.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System; +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -6,20 +7,19 @@ using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Simplification; +using Superpower.Parsers; using CS = Microsoft.CodeAnalysis.CSharp.Syntax; -using VB = Microsoft.CodeAnalysis.VisualBasic.Syntax; using CSFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using VB = Microsoft.CodeAnalysis.VisualBasic.Syntax; using VBFactory = Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory; -using Microsoft.CodeAnalysis.Simplification; -using System; -using Moq.Properties; namespace Moq { /// /// Generates code for custom delegates used for ref/out mocking. /// - [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = "CustomDelegate")] + [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = nameof(CustomDelegateCodeFix))] public class CustomDelegateCodeFix : CodeFixProvider { /// @@ -36,6 +36,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var document = context.Document; var span = context.Span; var root = await document.GetSyntaxRootAsync(context.CancellationToken); + if (root == null) + return; var token = root.FindToken(span.Start); if (!token.Span.IntersectsWith(span)) @@ -48,7 +50,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var semantic = await document.GetSemanticModelAsync(context.CancellationToken); var node = root.FindNode(span); - if (node == null) + if (node == null || semantic == null) return; if (node.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.Argument)) @@ -66,16 +68,16 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) return; var compilation = await document.Project.GetCompilationAsync(); - var scope = compilation.GetTypeByMetadataName(typeof(SetupScopeAttribute).FullName); - if (scope == null) + var scope = compilation?.GetTypeByMetadataName(typeof(SetupScopeAttribute).FullName); + if (compilation == null || scope == null) return; // We only generate for [SetupScope] annotated methods. // TODO: should integrate seamlessly with recursive mocks - if (!setupSymbol.CandidateSymbols.Any(c => c.GetAttributes().Any(a => a.AttributeClass == scope))) + if (!setupSymbol.CandidateSymbols.Any(c => c.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, scope)))) return; - IMethodSymbol targetMethod = null; + IMethodSymbol? targetMethod = null; // CS1593 case if (node is CS.LambdaExpressionSyntax) @@ -119,7 +121,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) } /// - public sealed override FixAllProvider GetFixAllProvider() => null; + public sealed override FixAllProvider? GetFixAllProvider() => null; class SetupDelegateCodeAction : CodeAction { @@ -134,7 +136,10 @@ public SetupDelegateCodeAction(Document document, SyntaxNode setup, IMethodSymbo this.symbol = symbol; } - public override string Title => Strings.CustomDelegateCodeFix.TitleFormat(symbol.Name); + public override string? EquivalenceKey => symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + + "(" + string.Join(",", symbol.Parameters.Select(x => x.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) + ")"; + + public override string Title => ThisAssembly.Strings.CustomDelegateCodeFix.TitleFormat(symbol.Name); protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) { @@ -160,10 +165,13 @@ protected override async Task GetChangedDocumentAsync(CancellationToke node = FindSetup(root); } else - { + { var tempDoc = document.WithSyntaxRoot(generator.ReplaceNode(root, @delegate, signature)); tempDoc = await Simplifier.ReduceAsync(tempDoc); var tempRoot = await tempDoc.GetSyntaxRootAsync(cancellationToken); + if (tempRoot == null) + return document; + var className = generator.GetName(@class); var tempClass = tempRoot.DescendantNodes().First(x => generator.GetDeclarationKind(x) == DeclarationKind.Class && @@ -175,14 +183,21 @@ protected override async Task GetChangedDocumentAsync(CancellationToke { // Generate the delegate name using full Type+Member name. var semantic = await document.GetSemanticModelAsync(cancellationToken); - var mock = semantic.GetSymbolInfo( - setup.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.InvocationExpression) ? - (SyntaxNode)((setup as CS.InvocationExpressionSyntax)?.Expression as CS.MemberAccessExpressionSyntax)?.Expression : - ((setup as VB.InvocationExpressionSyntax)?.Expression as VB.MemberAccessExpressionSyntax)?.Expression); - - if (mock.Symbol != null && - mock.Symbol.Kind == SymbolKind.Local || - mock.Symbol.Kind == SymbolKind.Field) + if (semantic == null) + return document; + + SyntaxNode? memberNode = setup.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.InvocationExpression) ? + ((setup as CS.InvocationExpressionSyntax)?.Expression as CS.MemberAccessExpressionSyntax)?.Expression : + ((setup as VB.InvocationExpressionSyntax)?.Expression as VB.MemberAccessExpressionSyntax)?.Expression; + + if (memberNode == null) + return document; + + var mock = semantic.GetSymbolInfo(memberNode); + + if (mock.Symbol != null && + (mock.Symbol.Kind == SymbolKind.Local || + mock.Symbol.Kind == SymbolKind.Field)) { var type = mock.Symbol.Kind == SymbolKind.Local ? ((ILocalSymbol)mock.Symbol).Type : @@ -200,10 +215,11 @@ protected override async Task GetChangedDocumentAsync(CancellationToke root = generator.ReplaceNode(root, node, generator.WithTypeArguments(node, generator.IdentifierName(delegateName))); // Find the updated setup node = FindSetup(root); + if (node == null || node.Parent == null) + return document; // Detect recursive mock access and wrap in a Func - if ( - node.Parent.ChildNodes() + if (node.Parent.ChildNodes() .OfType() .Where(list => !list.Arguments.Select(arg => arg.Expression).OfType().Any()) .SelectMany(list => list.DescendantNodes().OfType()) @@ -226,12 +242,15 @@ protected override async Task GetChangedDocumentAsync(CancellationToke generator.ValueReturningLambdaExpression(expression)); // Find the updated setup node = FindSetup(root); + if (node == null || node.Parent == null) + return document; } // If there is no Returns, generate one - if (node.Parent.Parent.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.ExpressionStatement)) + if (node.Parent?.Parent != null && + node.Parent.Parent.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.ExpressionStatement)) { - var returns = generator.InvocationExpression( + var returns = (CS.InvocationExpressionSyntax)generator.InvocationExpression( generator.MemberAccessExpression( node.Parent.WithTrailingTrivia( node.Parent.Parent.GetLeadingTrivia().Add(CSFactory.Whitespace("\t"))), @@ -242,15 +261,29 @@ protected override async Task GetChangedDocumentAsync(CancellationToke // Replace the parent InvocationExpression with the returning one. root = generator.ReplaceNode(root, node.Parent, returns); + + // Find the updated setup + node = FindSetup(root); + + var statement = node.Ancestors().OfType().FirstOrDefault(); + if (statement != null && + (statement.GetTrailingTrivia().Count == 0 || + !statement.GetTrailingTrivia().Any(t => t.Token == statement.SemicolonToken))) + { + root = generator.ReplaceNode(root, statement, statement + .WithSemicolonToken(CSFactory.Token(Microsoft.CodeAnalysis.CSharp.SyntaxKind.SemicolonToken) + .WithTrailingTrivia(statement.GetTrailingTrivia().Insert(0, CSFactory.ElasticCarriageReturnLineFeed)))); + } } - else if (node.Parent.Parent.IsKind(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.ExpressionStatement)) + else if (node.Parent?.Parent != null && + node.Parent.Parent.IsKind(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.ExpressionStatement)) { var lambda = VBFactory.MultiLineFunctionLambdaExpression( VBFactory.FunctionLambdaHeader().WithParameterList( VBFactory.ParameterList( VBFactory.SeparatedList( symbol.Parameters.Select(prm => (VB.ParameterSyntax)generator.ParameterDeclaration(prm))))), - VBFactory.List(new VB.StatementSyntax[] + VBFactory.List(new VB.StatementSyntax[] { VBFactory.ThrowStatement( VBFactory.ObjectCreationExpression( diff --git a/src/Moq.CodeAnalysis/CustomDelegateCompletion.cs b/src/Moq.CodeAnalysis/CustomDelegateCompletion.cs new file mode 100644 index 00000000..eabff636 --- /dev/null +++ b/src/Moq.CodeAnalysis/CustomDelegateCompletion.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Tags; +using Microsoft.CodeAnalysis.Text; + +namespace Moq.CodeAnalysis +{ + [ExportCompletionProvider(nameof(CustomDelegateCompletion), LanguageNames.CSharp)] + public class CustomDelegateCompletion : CompletionProvider + { + static readonly CompletionItemRules rules = CompletionItemRules.Create(selectionBehavior: CompletionItemSelectionBehavior.SoftSelection); + + public override Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) + { + if (!item.Tags.Contains(nameof(CustomDelegateCompletion))) + return base.GetDescriptionAsync(document, item, cancellationToken); + + return Task.FromResult(CompletionDescription.FromText(ThisAssembly.Strings.CustomDelegateCompletion.Description)); + } + + public override Task GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) + { + File.AppendAllText(Path.Combine(Path.GetTempPath(), nameof(CustomDelegateCompletion) + ".txt"), nameof(GetChangeAsync)); + return base.GetChangeAsync(document, item, commitKey, cancellationToken); + } + + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + if (context.Document.SupportsSemanticModel != true) + return; + + var position = context.Position; + var document = context.Document; + var cancellation = context.CancellationToken; + + var root = await document.GetSyntaxRootAsync(cancellation).ConfigureAwait(false); + if (root == null) + return; + + var span = context.CompletionListSpan; + var token = root.FindToken(span.Start); + if (token.Parent == null) + return; + + var node = token.Parent.AncestorsAndSelf().FirstOrDefault(a => a.FullSpan.Contains(span)); + if (node == null) + return; + + if (node.AncestorsAndSelf().OfType().FirstOrDefault() is not InvocationExpressionSyntax invocation) + return; + + var semantic = await document.GetSemanticModelAsync(cancellation).ConfigureAwait(false); + if (semantic == null) + return; + + var scope = semantic.Compilation.GetTypeByMetadataName(typeof(SetupScopeAttribute).FullName); + if (scope == null) + return; + + bool IsSetupScope(ISymbol? symbol) => symbol is IMethodSymbol && + symbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, scope)); + + if (!IsSetupScope(semantic.GetSymbolInfo(invocation, cancellation).Symbol) && + !semantic.GetSymbolInfo(invocation, cancellation).CandidateSymbols.Any(IsSetupScope)) + return; + + if (invocation.Expression is not MemberAccessExpressionSyntax member) + return; + + var symbol = semantic.GetSymbolInfo(member.Expression, cancellation).Symbol; + var target = symbol as ITypeSymbol ?? (symbol as ILocalSymbol)?.Type; + + if (symbol == null || target == null) + return; + + var start = invocation.ArgumentList.Span.Start + 1; + var length = span.End - start; + // Wrong length, shouldn't happen, but bail just in case. + if (length < 0) + return; + + var existing = (await document.GetTextAsync(cancellation).ConfigureAwait(false)) + .GetSubText(new TextSpan(start, length)).ToString(); + + // In this case, completion would already have the right items, no need to annotate. + if (existing.StartsWith(symbol.Name + ".")) + return; + + // List all the members of the target type that have ref/out parameter + var members = target.GetMembers().OfType() + .Where(m => m.Parameters.Any(p => p.RefKind == RefKind.Ref || p.RefKind == RefKind.Out)).ToArray(); + + foreach (var candidate in members) + { + context.AddItem(CompletionItem.Create( + displayText: symbol.Name + "." + candidate.Name, + sortText: symbol.Name + "." + candidate.Name, + filterText: symbol.Name + "." + candidate.Name, + tags: ImmutableArray.Create(WellKnownTags.Method).Add(nameof(CustomDelegateCompletion)), + rules: rules, + inlineDescription: "Setup via delegate")); + } + } + + public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options) + { + if (trigger.Kind == CompletionTriggerKind.Invoke || + trigger.Kind == CompletionTriggerKind.InvokeAndCommitIfUnique) + return true; + + if (trigger.Kind == CompletionTriggerKind.Insertion && + (trigger.Character == '.' || trigger.Character == '(')) + return true; + + return base.ShouldTriggerCompletion(text, caretPosition, trigger, options); + } + } +} diff --git a/src/Moq.CodeAnalysis/Moq.CodeAnalysis.csproj b/src/Moq.CodeAnalysis/Moq.CodeAnalysis.csproj index 2259c571..1bbf6bf4 100644 --- a/src/Moq.CodeAnalysis/Moq.CodeAnalysis.csproj +++ b/src/Moq.CodeAnalysis/Moq.CodeAnalysis.csproj @@ -12,6 +12,7 @@ + @@ -21,7 +22,7 @@ - + diff --git a/src/Moq.CodeAnalysis/Resources.Designer.cs b/src/Moq.CodeAnalysis/Resources.Designer.cs index 7f6d3eec..267d1f9f 100644 --- a/src/Moq.CodeAnalysis/Resources.Designer.cs +++ b/src/Moq.CodeAnalysis/Resources.Designer.cs @@ -69,6 +69,16 @@ internal static string CustomDelegateCodeFix_TitleFormat { } } + /// + /// Looks up a localized string similar to A subsequent code fix for this completion will allow you to generate a + ///delegate to set up this method directly with the right Returns signature.. + /// + internal static string CustomDelegateCompletion_Description { + get { + return ResourceManager.GetString("CustomDelegateCompletion_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Project '{0}' does not reference the required Moq assembly.. /// diff --git a/src/Moq.CodeAnalysis/Resources.resx b/src/Moq.CodeAnalysis/Resources.resx index ff64c698..175b642b 100644 --- a/src/Moq.CodeAnalysis/Resources.resx +++ b/src/Moq.CodeAnalysis/Resources.resx @@ -120,6 +120,11 @@ Setup mock with typed delegate for {0} + + A subsequent code fix for this completion will allow you to generate a +delegate to set up this method directly with the right Returns signature. + Description for completion + Project '{0}' does not reference the required Moq assembly. diff --git a/src/Moq.Sdk/SetupScopeAttribute.cs b/src/Moq.Sdk/SetupScopeAttribute.cs new file mode 100644 index 00000000..e15b2f4f --- /dev/null +++ b/src/Moq.Sdk/SetupScopeAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace Moq +{ + /// + /// Annotates a method that creates an implicit . + /// + /// + /// This allows a method like Setup that receives a lambda, to have + /// its parameter considered as a setup lambda, so that executing it does + /// not cause the actual mock behavior to run, and instead it turns on + /// an automatic so that behaviors can adapt to + /// the invocation dynamically. + /// + [AttributeUsage(AttributeTargets.Method)] + public class SetupScopeAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Moq.Vsix/Moq.Vsix.csproj b/src/Moq.Vsix/Moq.Vsix.csproj new file mode 100644 index 00000000..8d380d78 --- /dev/null +++ b/src/Moq.Vsix/Moq.Vsix.csproj @@ -0,0 +1,55 @@ + + + + + net472 + Moq.Vsix + Moq.Vsix + + + + false + false + true + true + false + false + + + + + + + + + + Program + $(DevEnvDir)devenv.exe + /rootsuffix Exp + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Moq.Vsix/source.extension.vsixmanifest b/src/Moq.Vsix/source.extension.vsixmanifest new file mode 100644 index 00000000..07747510 --- /dev/null +++ b/src/Moq.Vsix/source.extension.vsixmanifest @@ -0,0 +1,24 @@ + + + + + Moq Completion Tester + This is only needed to make troubleshooting and debuging custom completions more convenient. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Moq/SetupExtension.cs b/src/Moq/SetupExtension.cs index 64494c68..a9e41dd0 100644 --- a/src/Moq/SetupExtension.cs +++ b/src/Moq/SetupExtension.cs @@ -20,11 +20,13 @@ public static class SetupExtension /// is active and affects all invocations to any mocks called within the block. /// /// + [SetupScope] public static IDisposable Setup(this T mock) => new SetupScope(); /// /// Sets up the mock with the given void method call. /// + [SetupScope] public static ISetup Setup(this T mock, Action action) { using (new SetupScope()) @@ -37,6 +39,7 @@ public static ISetup Setup(this T mock, Action action) /// /// Sets up the mock with the given function. /// + [SetupScope] public static TResult Setup(this T mock, Func function) { using (new SetupScope()) @@ -50,6 +53,7 @@ public static TResult Setup(this T mock, Func function) /// access and set ref/out arguments. A code fix will automatically /// generate a delegate with the right signature when using this overload. /// + [SetupScope] public static ISetup Setup(this object mock, TDelegate member) => new DefaultSetup(member as Delegate ?? throw new ArgumentException(ThisAssembly.Strings.Setup.DelegateExpected)); @@ -61,6 +65,7 @@ public static ISetup Setup(this object mock, TDelegate mem /// and pass in the method group directly instead. A code fix will automatically /// generate a delegate with the right signature when using this overload. /// + [SetupScope] public static ISetup Setup(this object mock, Func memberFunction) { using (new SetupScope())