Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax> verifyReplacements =
FindAndBuildVerifyReplacements(allInvocations, semanticModel, mockSymbol, cancellationToken);

Dictionary<AssignmentExpressionSyntax, InvocationExpressionSyntax> propertyVerifyReplacements =
FindAndBuildPropertyVerifyReplacements(compilationUnit, semanticModel, mockSymbol, cancellationToken);

Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax> clearReplacements =
FindAndBuildClearReceivedCallsReplacements(allInvocations, semanticModel, mockSymbol, cancellationToken);

Expand All @@ -83,6 +86,7 @@
nodesToReplace.AddRange(clearReplacements.Keys);
nodesToReplace.AddRange(raiseReplacements.Keys);
nodesToReplace.AddRange(whenDoReplacements.Keys);
nodesToReplace.AddRange(propertyVerifyReplacements.Keys);

compilationUnit = compilationUnit.ReplaceNodes(
nodesToReplace,
Expand Down Expand Up @@ -116,10 +120,17 @@
}
}

if (original is AssignmentExpressionSyntax assignment &&
raiseReplacements.TryGetValue(assignment, out InvocationExpressionSyntax? raiseReplacement))
if (original is AssignmentExpressionSyntax assignment)
{
return raiseReplacement;
if (raiseReplacements.TryGetValue(assignment, out InvocationExpressionSyntax? raiseReplacement))
{
return raiseReplacement;
}

if (propertyVerifyReplacements.TryGetValue(assignment, out InvocationExpressionSyntax? propVerifyReplacement))
{
return propVerifyReplacement;
}
}

return original;
Expand All @@ -132,7 +143,7 @@
compilationUnit = compilationUnit.AddUsings(usingDirective);
}

if (verifyReplacements.Count > 0)
if (verifyReplacements.Count > 0 || propertyVerifyReplacements.Count > 0)
{
bool hasVerifyUsing = compilationUnit.Usings.Any(u => u.Name?.ToString() == "Mockolate.Verify");
if (!hasVerifyUsing)
Expand Down Expand Up @@ -577,6 +588,124 @@
return result;
}

/// <summary>
/// Translates property-style verifications:
/// <list type="bullet">
/// <item><c>_ = sub.Received().Prop</c> → <c>sub.Mock.Verify.Prop.Got().AtLeastOnce()</c></item>
/// <item><c>sub.Received().Prop = v</c> → <c>sub.Mock.Verify.Prop.Set(v).AtLeastOnce()</c></item>
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML doc example for property sets shows sub.Mock.Verify.Prop.Set(v)..., but the implementation wraps non-matcher values in It.Is(...) (e.g., Set(It.Is("x"))). Consider updating the example/comment to reflect the actual output so the documentation matches behavior.

Suggested change
/// <item><c>sub.Received().Prop = v</c> → <c>sub.Mock.Verify.Prop.Set(v).AtLeastOnce()</c></item>
/// <item><c>sub.Received().Prop = "x"</c> → <c>sub.Mock.Verify.Prop.Set(It.Is("x")).AtLeastOnce()</c></item>

Copilot uses AI. Check for mistakes.
/// </list>
/// <c>Received(n)</c> / <c>DidNotReceive()</c> map to <c>Exactly(n)</c>/<c>Once()</c>/<c>Never()</c> in the same way as method-style.
/// </summary>
private static Dictionary<AssignmentExpressionSyntax, InvocationExpressionSyntax> FindAndBuildPropertyVerifyReplacements(
CompilationUnitSyntax compilationUnit,
SemanticModel? semanticModel,
ISymbol? mockSymbol,
CancellationToken cancellationToken)
{
if (semanticModel is null || mockSymbol is null)
{
return [];
}

Dictionary<AssignmentExpressionSyntax, InvocationExpressionSyntax> result = [];

foreach (AssignmentExpressionSyntax assignment in compilationUnit.DescendantNodes().OfType<AssignmentExpressionSyntax>())
{
if (!assignment.IsKind(SyntaxKind.SimpleAssignmentExpression))
{
continue;
}

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FindAndBuildPropertyVerifyReplacements rewrites any matching SimpleAssignmentExpression regardless of where the assignment appears. Since assignment expressions can be used as sub-expressions (not just statements), this code fix could change semantics or produce non-compiling code when the assignment’s value is used. Consider restricting rewrites to assignments whose parent is an ExpressionStatementSyntax (and, for the get case, a discard assignment used as a statement) to ensure only verification-style usages are migrated.

Suggested change
if (assignment.Parent is not ExpressionStatementSyntax)
{
continue;
}

Copilot uses AI. Check for mistakes.
// Got pattern: `_ = sub.Received().Prop`
if (assignment.Left is IdentifierNameSyntax { Identifier.Text: "_", } &&
TryExtractReceivedPropertyAccess(assignment.Right, semanticModel, mockSymbol, cancellationToken,
out ExpressionSyntax? gotMockReceiver, out SimpleNameSyntax? gotPropertyName,
out string? gotReceivedMethod, out ArgumentListSyntax? gotReceivedArgs))
{
result[assignment] = BuildPropertyVerifyChain(gotMockReceiver, gotPropertyName, "Got",
SyntaxFactory.ArgumentList(), gotReceivedMethod, gotReceivedArgs)
.WithTriviaFrom(assignment);
continue;
}

// Set pattern: `sub.Received().Prop = value`
if (TryExtractReceivedPropertyAccess(assignment.Left, semanticModel, mockSymbol, cancellationToken,
out ExpressionSyntax? setMockReceiver, out SimpleNameSyntax? setPropertyName,
out string? setReceivedMethod, out ArgumentListSyntax? setReceivedArgs))
{
ArgumentListSyntax setArgs = SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(assignment.Right.WithoutTrivia())));

result[assignment] = BuildPropertyVerifyChain(setMockReceiver, setPropertyName, "Set",
setArgs, setReceivedMethod, setReceivedArgs)
.WithTriviaFrom(assignment);
}
Comment thread
vbreuss marked this conversation as resolved.
Outdated
}

return result;
}

private static bool TryExtractReceivedPropertyAccess(ExpressionSyntax expression,
SemanticModel semanticModel, ISymbol mockSymbol, CancellationToken cancellationToken,
out ExpressionSyntax? mockReceiver, out SimpleNameSyntax? propertyName,
out string? receivedMethod, out ArgumentListSyntax? receivedArgs)

Check warning on line 651 in Source/Mockolate.Migration.Analyzers.CodeFixers/NSubstituteCodeFixProvider.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Method has 8 parameters, which is greater than the 7 authorized.

See more on https://sonarcloud.io/project/issues?id=aweXpect_Mockolate.Migration&issues=AZ3jajBhIQccHbajVlt2&open=AZ3jajBhIQccHbajVlt2&pullRequest=51
{
mockReceiver = null;
propertyName = null;
receivedMethod = null;
receivedArgs = null;

if (expression is not MemberAccessExpressionSyntax propertyAccess ||
propertyAccess.Expression is not InvocationExpressionSyntax receivedInvocation ||
receivedInvocation.Expression is not MemberAccessExpressionSyntax receivedAccess)
{
return false;
}

string method = receivedAccess.Name.Identifier.Text;
if (method is not ("Received" or "DidNotReceive"))
{
return false;
}

if (!IsTrackedMockReceiver(receivedAccess.Expression, semanticModel, mockSymbol, cancellationToken))
{
return false;
}

mockReceiver = receivedAccess.Expression;
propertyName = propertyAccess.Name;
receivedMethod = method;
receivedArgs = receivedInvocation.ArgumentList;
return true;
}

private static InvocationExpressionSyntax BuildPropertyVerifyChain(ExpressionSyntax? mockReceiver,
SimpleNameSyntax? propertyName, string accessor, ArgumentListSyntax accessorArgs,
string? receivedMethod, ArgumentListSyntax? receivedArgs)
{
MemberAccessExpressionSyntax mockAccess = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
mockReceiver!,
SyntaxFactory.IdentifierName("Mock"));
MemberAccessExpressionSyntax verifyMember = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
mockAccess,
SyntaxFactory.IdentifierName("Verify"));
MemberAccessExpressionSyntax propAccess = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
verifyMember,
propertyName!.WithoutTrivia());
MemberAccessExpressionSyntax accessorAccess = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
propAccess,
SyntaxFactory.IdentifierName(accessor));
InvocationExpressionSyntax accessorCall = SyntaxFactory.InvocationExpression(accessorAccess, accessorArgs);

bool isNegative = receivedMethod == "DidNotReceive";
return BuildVerifySuffix(accessorCall, isNegative, receivedArgs ?? SyntaxFactory.ArgumentList());
}

private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax> FindAndBuildVerifyReplacements(
IReadOnlyList<InvocationExpressionSyntax> allInvocations,
SemanticModel? semanticModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,40 @@ public void Test()
}
""");

[Fact]
public async Task DidNotReceivePropertySet_RewritesToVerifySetNever()
=> await Verifier.VerifyCodeFixAsync(
"""
using NSubstitute;

public interface IFoo { string Name { get; set; } }

public class Tests
{
public void Test()
{
var sub = [|Substitute.For<IFoo>()|];
sub.DidNotReceive().Name = "x";
}
}
""",
"""
using NSubstitute;
using Mockolate;
using Mockolate.Verify;

public interface IFoo { string Name { get; set; } }

public class Tests
{
public void Test()
{
var sub = IFoo.CreateMock();
sub.Mock.Verify.Name.Set("x").Never();
}
}
""");

[Fact]
Comment thread
vbreuss marked this conversation as resolved.
Outdated
public async Task DidNotReceiveWithAnyArgs_AddsAnyParametersAndNever()
=> await Verifier.VerifyCodeFixAsync(
Expand Down Expand Up @@ -179,6 +213,74 @@ public void Test()
}
""");

[Fact]
public async Task ReceivedPropertyGet_RewritesToVerifyGot()
=> await Verifier.VerifyCodeFixAsync(
"""
using NSubstitute;

public interface IFoo { string Name { get; set; } }

public class Tests
{
public void Test()
{
var sub = [|Substitute.For<IFoo>()|];
_ = sub.Received().Name;
}
}
""",
"""
using NSubstitute;
using Mockolate;
using Mockolate.Verify;

public interface IFoo { string Name { get; set; } }

public class Tests
{
public void Test()
{
var sub = IFoo.CreateMock();
sub.Mock.Verify.Name.Got().AtLeastOnce();
}
}
""");

[Fact]
public async Task ReceivedPropertySet_RewritesToVerifySet()
=> await Verifier.VerifyCodeFixAsync(
"""
using NSubstitute;

public interface IFoo { string Name { get; set; } }

public class Tests
{
public void Test()
{
var sub = [|Substitute.For<IFoo>()|];
sub.Received().Name = "x";
}
}
""",
"""
using NSubstitute;
using Mockolate;
using Mockolate.Verify;

public interface IFoo { string Name { get; set; } }

public class Tests
{
public void Test()
{
var sub = IFoo.CreateMock();
sub.Mock.Verify.Name.Set("x").AtLeastOnce();
Comment thread
vbreuss marked this conversation as resolved.
Outdated
}
}
""");

[Fact]
public async Task ReceivedWithAnyArgs_AddsAnyParameters()
=> await Verifier.VerifyCodeFixAsync(
Expand Down
Loading