Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,8 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
mockAccess,
methodNameSyntax);
replacement = SyntaxFactory.InvocationExpression(methodAccess, transformedArgs)
.WithTriviaFrom(invocation);
.WithTriviaFrom(invocation)
.WithLeadingTrivia(BuildNestedMockTodoTrivia(invocation, navChain));
}

result[invocation] = replacement;
Expand Down Expand Up @@ -624,6 +625,7 @@ private static Dictionary<InvocationExpressionSyntax, MemberAccessExpressionSynt
SimpleNameSyntax propertyNameSyntax = lambdaMemberAccess.Name;

MemberAccessExpressionSyntax replacement;
ExpressionSyntax? navChainForTodo = null;
if (navigationChain.Count == 0)
{
// Direct setup: mock.Mock.Setup.Property
Expand Down Expand Up @@ -664,9 +666,16 @@ private static Dictionary<InvocationExpressionSyntax, MemberAccessExpressionSynt
SyntaxKind.SimpleMemberAccessExpression,
setupAccess,
propertyNameSyntax);
navChainForTodo = navChain;
}

MemberAccessExpressionSyntax withTrivia = replacement.WithTriviaFrom(invocation);
if (navChainForTodo is not null)
{
withTrivia = withTrivia.WithLeadingTrivia(BuildNestedMockTodoTrivia(invocation, navChainForTodo));
}

result[invocation] = replacement.WithTriviaFrom(invocation);
result[invocation] = withTrivia;
}

return result;
Expand Down Expand Up @@ -730,6 +739,7 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
SimpleNameSyntax propertyNameSyntax = lambdaMemberAccess.Name;

ExpressionSyntax propertyAccess;
ExpressionSyntax? navChainForTodo = null;
if (navigationChain.Count == 0)
{
MemberAccessExpressionSyntax mockAccess = SyntaxFactory.MemberAccessExpression(
Expand Down Expand Up @@ -768,6 +778,7 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
SyntaxKind.SimpleMemberAccessExpression,
setupAccess,
propertyNameSyntax);
navChainForTodo = navChain;
}

InvocationExpressionSyntax replacement;
Expand Down Expand Up @@ -795,6 +806,11 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
.WithTriviaFrom(invocation);
}

if (navChainForTodo is not null)
{
replacement = replacement.WithLeadingTrivia(BuildNestedMockTodoTrivia(invocation, navChainForTodo));
}

result[invocation] = replacement;
}

Expand Down Expand Up @@ -836,14 +852,17 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
// because Mockolate infers them during code generation.
// Reuse the original dot token to preserve leading trivia (e.g. newline + indent
// when .Callback is written on its own line).
// Only set trailing trivia from invocation — applying full WithTriviaFrom would
// clobber leading trivia on rebuiltReceiver's first token, which may carry a
// nested-mock TODO comment attached to the migrated setup.
InvocationExpressionSyntax replacement = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
rebuiltReceiver!,
memberAccess.OperatorToken,
SyntaxFactory.IdentifierName("Do")),
invocation.ArgumentList)
.WithTriviaFrom(invocation);
.WithTrailingTrivia(invocation.GetTrailingTrivia());

result[invocation] = replacement;
}
Expand Down Expand Up @@ -955,6 +974,7 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
methodParameterCount);

InvocationExpressionSyntax baseInvocation;
ExpressionSyntax? navChainForTodo = null;
if (navigationChain.Count == 0)
{
MemberAccessExpressionSyntax mockAccess = SyntaxFactory.MemberAccessExpression(
Expand Down Expand Up @@ -995,6 +1015,7 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
verifyAccess,
methodNameSyntax);
baseInvocation = SyntaxFactory.InvocationExpression(methodAccess, transformedArgs);
navChainForTodo = navChain;
}

InvocationExpressionSyntax replacement;
Expand All @@ -1020,6 +1041,11 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
.WithTriviaFrom(invocation);
}

if (navChainForTodo is not null)
{
replacement = replacement.WithLeadingTrivia(BuildNestedMockTodoTrivia(invocation, navChainForTodo));
}

result[invocation] = replacement;
}

Expand Down Expand Up @@ -1145,6 +1171,11 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
.WithTriviaFrom(invocation)
: atLeastOnceFallback.WithTriviaFrom(invocation);

if (navigationChain.Count > 0)
{
replacement = replacement.WithLeadingTrivia(BuildNestedMockTodoTrivia(invocation, receiver));
}

result[invocation] = replacement;
}

Expand Down Expand Up @@ -1279,6 +1310,11 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
.WithTriviaFrom(invocation)
: atLeastOnceFallback.WithTriviaFrom(invocation);

if (navigationChain.Count > 0)
{
replacement = replacement.WithLeadingTrivia(BuildNestedMockTodoTrivia(invocation, receiver));
}

result[invocation] = replacement;
}

Expand Down Expand Up @@ -1796,6 +1832,24 @@ private static UsingDirectiveSyntax BuildUsingDirective(CompilationUnitSyntax co
return SyntaxFactory.UsingDirective(name);
}

private static SyntaxTriviaList BuildNestedMockTodoTrivia(SyntaxNode anchor, ExpressionSyntax navigationRoot)
{
SyntaxTriviaList existing = anchor.GetLeadingTrivia();
SyntaxTrivia indent = existing.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia));
string endOfLine = DetectLineEnding(anchor.SyntaxTree.GetRoot());
return existing
.Add(SyntaxFactory.Comment(
$"// TODO(MockolateM001): register the nested '{navigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)"))
.Add(SyntaxFactory.EndOfLine(endOfLine))
.Add(indent);
}

private static string DetectLineEnding(SyntaxNode root) =>
root.DescendantTrivia(descendIntoTrivia: true)
.Where(t => t.IsKind(SyntaxKind.EndOfLineTrivia))
.Select(t => t.ToFullString())
.FirstOrDefault(s => s.Length > 0) ?? "\n";

private static TypeSyntax? GetTypeArgumentFromSemanticModel(
SemanticModel? semanticModel,
ExpressionSyntax expressionSyntax,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ private static Dictionary<SyntaxNode, SyntaxNode> FindAndBuildSetupReplacements(

if (outerReplacement is not null)
{
outerReplacement = ApplySetupTrivia(outerReplacement, outerInvocation,
outerReplacement = ApplyTodoTrivia(outerReplacement, outerInvocation,
isNested ? targetMemberAccess.Expression : null, callInfoTodoNeeded);
result[outerInvocation] = outerReplacement;
}
Expand Down Expand Up @@ -363,7 +363,7 @@ private static Dictionary<SyntaxNode, SyntaxNode> FindAndBuildSetupReplacements(

if (outerPropertyReplacement is not null)
{
outerPropertyReplacement = ApplySetupTrivia(outerPropertyReplacement, outerInvocation,
outerPropertyReplacement = ApplyTodoTrivia(outerPropertyReplacement, outerInvocation,
isNestedProperty ? targetPropertyAccess.Expression : null, propertyCallInfoTodoNeeded);
result[outerInvocation] = outerPropertyReplacement;
}
Expand Down Expand Up @@ -427,28 +427,30 @@ private static (InvocationExpressionSyntax effectiveOuter, bool callInfoTodoNeed
}

/// <summary>
/// Combines the nested-mock and CallInfo TODO comments onto a single setup replacement when both apply.
/// Either flag may be inactive; if neither is active, the replacement is returned untouched.
/// Layers the nested-mock and/or CallInfo TODO comments onto a migrated replacement. Called from
/// every rewrite path that can produce these TODOs — setup-call, setup-property, method verify,
/// property verify, and event raise. Either flag may be inactive; if neither is active, the
/// replacement is returned untouched.
/// </summary>
private static InvocationExpressionSyntax ApplySetupTrivia(InvocationExpressionSyntax replacement,
InvocationExpressionSyntax outerInvocation, ExpressionSyntax? nestedNavigationRoot, bool callInfoTodoNeeded)
private static InvocationExpressionSyntax ApplyTodoTrivia(InvocationExpressionSyntax replacement,
SyntaxNode anchor, ExpressionSyntax? nestedNavigationRoot, bool callInfoTodoNeeded)
{
if (nestedNavigationRoot is null && !callInfoTodoNeeded)
{
return replacement;
}

SyntaxTriviaList trivia = outerInvocation.GetLeadingTrivia();
SyntaxTriviaList trivia = anchor.GetLeadingTrivia();
if (nestedNavigationRoot is not null)
{
trivia = AppendTodoComment(trivia, outerInvocation,
$"// TODO: register the nested '{nestedNavigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)");
trivia = AppendTodoComment(trivia, anchor,
$"// TODO(MockolateM002): register the nested '{nestedNavigationRoot}' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)");
}

if (callInfoTodoNeeded)
{
trivia = AppendTodoComment(trivia, outerInvocation,
"// TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo");
trivia = AppendTodoComment(trivia, anchor,
"// TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo");
}

return replacement.WithLeadingTrivia(trivia);
Expand All @@ -475,7 +477,7 @@ private static InvocationExpressionSyntax BuildSimpleOuter(ExpressionSyntax setu
/// </summary>
private static SyntaxTriviaList BuildCallInfoTodoTrivia(SyntaxNode anchor) =>
AppendTodoComment(anchor.GetLeadingTrivia(), anchor,
"// TODO: review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo");
"// TODO(MockolateM002): review CallInfo usage manually — Mockolate's Do/Returns take typed parameters, not CallInfo");

private static SyntaxTriviaList AppendTodoComment(SyntaxTriviaList existingLeading, SyntaxNode anchor,
string commentText)
Expand Down Expand Up @@ -823,8 +825,14 @@ private static Dictionary<AssignmentExpressionSyntax, InvocationExpressionSyntax
raiseMember,
eventAccess.Name.WithoutTrivia());

result[assignment] = SyntaxFactory.InvocationExpression(raiseEventName, raiseArgs)
InvocationExpressionSyntax raiseReplacement = SyntaxFactory.InvocationExpression(raiseEventName, raiseArgs)
.WithTriviaFrom(assignment);
if (eventAccess.Expression is MemberAccessExpressionSyntax nestedEventReceiver)
{
raiseReplacement = ApplyTodoTrivia(raiseReplacement, assignment, nestedEventReceiver, callInfoTodoNeeded: false);
}

result[assignment] = raiseReplacement;
}

return result;
Expand Down Expand Up @@ -973,9 +981,15 @@ private static Dictionary<AssignmentExpressionSyntax, InvocationExpressionSyntax
if (assignment.Left is IdentifierNameSyntax { Identifier.Text: "_", } &&
TryExtractReceivedPropertyAccess(assignment.Right, semanticModel, mockSymbol, cancellationToken) is { } got)
{
result[assignment] = BuildPropertyVerifyChain(got.MockReceiver, got.PropertyName, "Got",
InvocationExpressionSyntax gotChain = BuildPropertyVerifyChain(got.MockReceiver, got.PropertyName, "Got",
SyntaxFactory.ArgumentList(), got.ReceivedMethod, got.ReceivedArgs)
.WithTriviaFrom(assignment);
if (got.MockReceiver is MemberAccessExpressionSyntax gotNestedReceiver)
{
gotChain = ApplyTodoTrivia(gotChain, assignment, gotNestedReceiver, callInfoTodoNeeded: false);
}

result[assignment] = gotChain;
continue;
}

Expand All @@ -985,9 +999,15 @@ private static Dictionary<AssignmentExpressionSyntax, InvocationExpressionSyntax
ArgumentListSyntax setArgs = BuildPropertyVerifySetArgs(
assignment.Right, semanticModel, cancellationToken);

result[assignment] = BuildPropertyVerifyChain(set.MockReceiver, set.PropertyName, "Set",
InvocationExpressionSyntax setChain = BuildPropertyVerifyChain(set.MockReceiver, set.PropertyName, "Set",
setArgs, set.ReceivedMethod, set.ReceivedArgs)
.WithTriviaFrom(assignment);
if (set.MockReceiver is MemberAccessExpressionSyntax setNestedReceiver)
{
setChain = ApplyTodoTrivia(setChain, assignment, setNestedReceiver, callInfoTodoNeeded: false);
}

result[assignment] = setChain;
}
}

Expand Down Expand Up @@ -1130,7 +1150,13 @@ private static Dictionary<InvocationExpressionSyntax, InvocationExpressionSyntax
bool isNegative = receivedMethod is "DidNotReceive" or "DidNotReceiveWithAnyArgs";
InvocationExpressionSyntax suffix = BuildVerifySuffix(verifyTarget, isNegative, receiverCall.ArgumentList);

result[outerInvocation] = suffix.WithTriviaFrom(outerInvocation);
InvocationExpressionSyntax replacement = suffix.WithTriviaFrom(outerInvocation);
if (mockReceiver is MemberAccessExpressionSyntax nestedReceiver)
{
replacement = ApplyTodoTrivia(replacement, outerInvocation, nestedReceiver, callInfoTodoNeeded: false);
}

result[outerInvocation] = replacement;
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,45 @@ public void Test()
}
}
""");

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

public interface IBar { bool Bar(string x); }
public interface IFoo { IBar Child { get; } }

public class Tests
{
public void Test()
{
var mock = [|new Mock<IFoo>()|];
mock.Setup(m => m.Child.Bar(It.IsAny<string>()))
.Returns(true)
.Callback<string>(x => { });
}
}
""",
"""
using Moq;
using Mockolate;

public interface IBar { bool Bar(string x); }
public interface IFoo { IBar Child { get; } }

public class Tests
{
public void Test()
{
var mock = IFoo.CreateMock();
// TODO(MockolateM001): register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)
mock.Child.Mock.Bar(It.IsAny<string>())
.Returns(true)
.Do(x => { });
}
}
""");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ public class Tests
public void Test()
{
var mock = IFoo.CreateMock();
// TODO(MockolateM001): register the nested 'mock.Child.GrandChild' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)
mock.Child.GrandChild.Mock.Bar(It.IsAny<string>()).Returns(true);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ public class Tests
public void Test()
{
var mock = IFoo.CreateMock();
// TODO(MockolateM001): register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)
mock.Child.Mock.Verify.Name.Got().Exactly(2);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ public class Tests
public void Test()
{
var mock = IFoo.CreateMock();
// TODO(MockolateM001): register the nested 'mock.Bar.Baz' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)
mock.Bar.Baz.Mock.Setup.Name.Returns("baz");
}
}
Expand Down Expand Up @@ -593,6 +594,7 @@ public class Tests
public void Test()
{
var mock = IFoo.CreateMock();
// TODO(MockolateM001): register the nested 'mock.Bar' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)
mock.Bar.Mock.Setup.Name.InitializeWith("value");
}
}
Expand Down Expand Up @@ -628,6 +630,7 @@ public class Tests
public void Test()
{
var mock = IFoo.CreateMock();
// TODO(MockolateM001): register the nested 'mock.Bar' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)
mock.Bar.Mock.Setup.Name.Register();
}
}
Expand Down Expand Up @@ -807,6 +810,7 @@ public class Tests
public void Test()
{
var mock = IFoo.CreateMock();
// TODO(MockolateM001): register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)
mock.Child.Mock.GetCount()
.Returns(1)
.Throws<InvalidOperationException>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public class Tests
public void Test()
{
var mock = IFoo.CreateMock();
// TODO(MockolateM001): register the nested 'mock.Child' chain explicitly in the mock setup (Mockolate doesn't auto-mock recursively)
mock.Child.Mock.Verify.MyEvent.Subscribed().Never();
}
}
Expand Down
Loading
Loading