Skip to content

Commit

Permalink
Add a fixer for the PosInfoMoq1005 rule.
Browse files Browse the repository at this point in the history
  • Loading branch information
GillesTourreau committed Oct 25, 2024
1 parent b71c04f commit bd18baa
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 1 deletion.
Binary file added docs/Design/PosInfoMoq1005-Fixer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions docs/Design/PosInfoMoq1005.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ public void SetNameOfCustomer()

To fix a violation of this rule, use the `SetupSet<TProperty>()` method with the type of the mocked property as the generic argument.

### Visual Studio fixer
A Visual Studio fixer exists to set explicitly the generic argument of the `SetupSet<T>()` method with the property type
in the current document, project or solution.

![Visual Studio rule fixer](PosInfoMoq1005-Fixer.png)

## When to suppress warnings

Do not suppress a warning from this rule. Using the `SetupSet<T>()` method ensures that the delegate argument in the `Callback()`
Expand Down
2 changes: 1 addition & 1 deletion src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace PosInformatique.Moq.Analyzers
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class SetupSetAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor UseSetupSetWithGenericArgumentRule = new DiagnosticDescriptor(
internal static readonly DiagnosticDescriptor UseSetupSetWithGenericArgumentRule = new DiagnosticDescriptor(
"PosInfoMoq1005",
"Defines the generic argument of the SetupSet() method with the type of the mocked property",
"Defines the generic argument of the SetupSet() method with the type of the mocked property",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//-----------------------------------------------------------------------
// <copyright file="SetGenericArgumentSetupSetCodeFixProvider.cs" company="P.O.S Informatique">
// Copyright (c) P.O.S Informatique. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace PosInformatique.Moq.Analyzers
{
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SetGenericArgumentSetupSetCodeFixProvider))]
[Shared]
public class SetGenericArgumentSetupSetCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(SetupSetAnalyzer.UseSetupSetWithGenericArgumentRule.Id); }
}

public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

if (root is null)
{
return;
}

// Gets the location where is the issue in the code.
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;

// Gets the syntax node where is located the issue in the code.
var node = root.FindNode(diagnosticSpan, getInnermostNodeForTie: true);

if (node is not IdentifierNameSyntax identifierNameSyntax)
{
return;
}

context.RegisterCodeFix(
CodeAction.Create(
title: "Set the generic argument with the mocked property type to the SetupSet() method.",
createChangedDocument: cancellationToken => AddMockBehiavorStrictArgumentAsync(context.Document, identifierNameSyntax, cancellationToken),
equivalenceKey: "Set the generic argument with the mocked property type to the SetupSet() method."),
diagnostic);

return;
}

private static async Task<Document> AddMockBehiavorStrictArgumentAsync(Document document, IdentifierNameSyntax oldIdentifierNameSyntax, CancellationToken cancellationToken)
{
var semanticModel = await document.GetSemanticModelAsync();

if (semanticModel is null)
{
return document;
}

var moqSymbols = MoqSymbols.FromCompilation(semanticModel.Compilation);

if (moqSymbols is null)
{
return document;
}

// Retrieve the invocation expression
if (oldIdentifierNameSyntax.Parent is not MemberAccessExpressionSyntax memberAccessExpressionSyntax)
{
return document;
}

if (memberAccessExpressionSyntax.Parent is not InvocationExpressionSyntax invocationExpressionSyntax)
{
return document;
}

// Gets the chained members from the lambda expression (to determine the type of the last property in the members chain).
var expressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, semanticModel);

var chainedMembers = expressionAnalyzer.ExtractChainedMembersInvocationFromLambdaExpression(invocationExpressionSyntax, cancellationToken);

if (chainedMembers is null)
{
return document;
}

// Update the IdentifierNameSyntax with a GenericName (which contains the type of the property).
var propertyType = SyntaxFactory.ParseTypeName(chainedMembers.ReturnType.ToDisplayString());

var newIdentifierNameSyntax = SyntaxFactory.GenericName(
oldIdentifierNameSyntax.Identifier,
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList(propertyType)));

var oldRoot = await document.GetSyntaxRootAsync(cancellationToken);

if (oldRoot is null)
{
return document;
}

var newRoot = oldRoot.ReplaceNode(oldIdentifierNameSyntax, newIdentifierNameSyntax);

return document.WithSyntaxRoot(newRoot);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//-----------------------------------------------------------------------
// <copyright file="SetGenericArgumentSetupSetCodeFixProviderTest.cs" company="P.O.S Informatique">
// Copyright (c) P.O.S Informatique. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace PosInformatique.Moq.Analyzers.Tests
{
using Verifier = MoqCSharpCodeFixVerifier<SetupSetAnalyzer, SetGenericArgumentSetupSetCodeFixProvider>;

public class SetGenericArgumentSetupSetCodeFixProviderTest
{
[Fact]
public async Task SetupSet_Fix()
{
var source = @"
namespace ConsoleApplication1
{
using Moq;
using System;
public class TestClass
{
public void TestMethod()
{
var mock1 = new Mock<I>();
mock1.[|SetupSet|](i => i.TestPropertyInt32 = 1234);
mock1.[|SetupSet|](i => i.TestPropertyString = ""Foobard"");
// No changes
mock1.SetupSet<int>(i => i.TestPropertyInt32 = 1234);
mock1.SetupSet<string>(i => i.TestPropertyString = ""Foobard"");
}
}
public interface I
{
int TestPropertyInt32 { get; set; }
string TestPropertyString { get; set; }
}
}";

var expectedFixedSource =
@"
namespace ConsoleApplication1
{
using Moq;
using System;
public class TestClass
{
public void TestMethod()
{
var mock1 = new Mock<I>();
mock1.SetupSet<int>(i => i.TestPropertyInt32 = 1234);
mock1.SetupSet<string>(i => i.TestPropertyString = ""Foobard"");
// No changes
mock1.SetupSet<int>(i => i.TestPropertyInt32 = 1234);
mock1.SetupSet<string>(i => i.TestPropertyString = ""Foobard"");
}
}
public interface I
{
int TestPropertyInt32 { get; set; }
string TestPropertyString { get; set; }
}
}";

await Verifier.VerifyCodeFixAsync(source, expectedFixedSource);
}
}
}

0 comments on commit bd18baa

Please sign in to comment.