Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
  • Loading branch information
kzu committed Oct 29, 2020
commit 2591eceb1aaeb195d6ae97f53f29c7cc26aeb68a
8 changes: 7 additions & 1 deletion Moq.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<RestoreSources>https://pkg.kzu.io/index.json;https://api.nuget.org/v3/index.json;$(RestoreSources)</RestoreSources>
<RestoreSources Condition="Exists('$(MSBuildThisFileDirectory)..\..\nugetizer\bin\')">$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\nugetizer\bin'));$(RestoreSources)</RestoreSources>
<RestoreSources Condition="Exists('$(MSBuildThisFileDirectory)..\..\stunts\bin\')">$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\stunts\bin'));$(RestoreSources)</RestoreSources>
<RestoreSources Condition="Exists('$(MSBuildThisFileDirectory)..\..\ThisAssembly\bin\')">$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\ThisAssembly\bin'));$(RestoreSources)</RestoreSources>

<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)Moq.snk</AssemblyOriginatorKeyFile>
<PublicKey>002400000480000094000000060200000024000052534131000400000100010051155fd0ee280be78d81cc979423f1129ec5dd28edce9cd94fd679890639cad54c121ebdb606f8659659cd313d3b3db7fa41e2271158dd602bb0039a142717117fa1f63d93a2d288a1c2f920ec05c4858d344a45d48ebd31c1368ab783596b382b611d8c92f9c1b3d338296aa21b12f3bc9f34de87756100c172c52a24bad2db</PublicKey>
Expand Down
5 changes: 4 additions & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
<ItemGroup Label="Core">
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.0.0" />
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" />
<PackageVersion Include="ThisAssembly" Version="1.0.0-rc" />
<PackageVersion Include="ThisAssembly" Version="1.0.0-rc.1" />
<PackageVersion Update="ThisAssembly" Version="42.42.42" Condition="Exists('$(MSBuildThisFileDirectory)..\..\ThisAssembly\bin\')" />
<PackageVersion Include="NuGetizer" Version="0.4.9" />
<PackageVersion Update="NuGetizer" Version="42.42.42" Condition="Exists('$(MSBuildThisFileDirectory)..\..\nugetizer\bin\')" />
<PackageVersion Include="Stunts" Version="$(StuntsVersion)" />
Expand Down Expand Up @@ -65,6 +66,8 @@
<PackageVersion Include="Castle.Core" Version="4.4.1" />
<PackageVersion Include="Superpower" Version="2.3.0" />
<PackageVersion Include="Ben.Demystifier" Version="0.1.4" />

<PackageVersion Include="Microsoft.VSSDK.BuildTools" Version="16.7.3069" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
using System.Collections.Immutable;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
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
{
/// <summary>
/// Generates code for custom delegates used for ref/out mocking.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = "CustomDelegate")]
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = nameof(CustomDelegateCodeFix))]
public class CustomDelegateCodeFix : CodeFixProvider
{
/// <inheritdoc />
Expand All @@ -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))
Expand All @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -119,7 +121,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
}

/// <inheritdoc />
public sealed override FixAllProvider GetFixAllProvider() => null;
public sealed override FixAllProvider? GetFixAllProvider() => null;

class SetupDelegateCodeAction : CodeAction
{
Expand All @@ -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<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
{
Expand All @@ -160,10 +165,13 @@ protected override async Task<Document> 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 &&
Expand All @@ -175,14 +183,21 @@ protected override async Task<Document> 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 :
Expand All @@ -200,10 +215,11 @@ protected override async Task<Document> 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<TDelegate>
if (
node.Parent.ChildNodes()
if (node.Parent.ChildNodes()
.OfType<CS.ArgumentListSyntax>()
.Where(list => !list.Arguments.Select(arg => arg.Expression).OfType<CS.LambdaExpressionSyntax>().Any())
.SelectMany(list => list.DescendantNodes().OfType<CS.MemberAccessExpressionSyntax>())
Expand All @@ -226,12 +242,15 @@ protected override async Task<Document> 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"))),
Expand All @@ -242,15 +261,29 @@ protected override async Task<Document> 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<CS.ExpressionStatementSyntax>().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(
Expand Down
Loading