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
59 changes: 59 additions & 0 deletions src/DiffEngine.Tests/InlinePatcherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,65 @@ public async Task AppendWithNoVerifyCall()
await Assert.That(reason).Contains("Could not find a Verify call");
}

// A project helper named Verify is not the entry point, and a stale hint must not drift onto one
[Test]
public async Task AppendSkipsAVerifyOnAnotherReceiver()
{
var source = Method(" Assert.Empty(ContentValidation.Verify(value));");

var status = InlinePatcher.TryApply(source, 5, InlinePatchMode.Append, null, "new", out _, out var reason);

await Assert.That(status).IsEqualTo(PatchStatus.NotFound);
await Assert.That(reason).Contains("Could not find a Verify call");
}

[Test]
public async Task AppendSkipsAVerifyOnAnInstance()
{
var source = Method(" mock.VerifyAll();");

var status = InlinePatcher.TryApply(source, 5, InlinePatchMode.Append, null, "new", out _, out var reason);

await Assert.That(status).IsEqualTo(PatchStatus.NotFound);
await Assert.That(reason).Contains("Could not find a Verify call");
}

// The entry point wrapping a helper of the same name: the outer call is the one to append to
[Test]
public async Task AppendPrefersTheEntryPointOverANestedHelper()
{
var source = Method(" await Verify(ContentValidation.Verify(value));");

var status = InlinePatcher.TryApply(source, 5, InlinePatchMode.Append, null, "new", out var newSource, out _);

await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(newSource).Contains(
" await Verify(ContentValidation.Verify(value))\n" +
" .Snapshot(\"\"\"");
}

[Test]
public async Task AppendToAVerifierQualifiedCall()
{
var source = Method(" await Verifier.Verify(value);");

var status = InlinePatcher.TryApply(source, 5, InlinePatchMode.Append, null, "new", out var newSource, out _);

await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(newSource).Contains("await Verifier.Verify(value)\n .Snapshot(\"\"\"");
}

[Test]
public async Task AppendToAThisQualifiedCall()
{
var source = Method(" await this.Verify(value);");

var status = InlinePatcher.TryApply(source, 5, InlinePatchMode.Append, null, "new", out var newSource, out _);

await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(newSource).Contains("await this.Verify(value)\n .Snapshot(\"\"\"");
}

[Test]
public async Task RemoveTakesTheWholeLine()
{
Expand Down
2 changes: 1 addition & 1 deletion src/DiffEngine/Inline/CsScan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public bool IsDeclaration(int nameStart)
/// The offset of the last character before <paramref name="index"/> that is neither
/// whitespace nor inside a comment, or -1 when there is none.
/// </summary>
int PreviousSignificant(int index)
public int PreviousSignificant(int index)
{
index--;
while (index >= 0 &&
Expand Down
54 changes: 54 additions & 0 deletions src/DiffEngine/Inline/InlinePatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ static class InlinePatcher
/// </summary>
const string verifyPrefix = "Verify";

/// <summary>
/// The only receiver a verify entry point is reached through. Every adapter exposes the entry
/// points unqualified (a static using, or inherited from VerifyBase) or on this one class.
/// </summary>
const string verifierType = "Verifier";

public static PatchStatus TryApply(
string source,
int lineHint,
Expand Down Expand Up @@ -494,6 +500,7 @@ static bool TryFindCall(
if (scan.IsCode(index) &&
StartsToken(source, index) &&
!scan.IsDeclaration(index) &&
!(byPrefix && IsForeignReceiver(source, scan, index)) &&
TrySkipToParen(source, scan, identifierEnd, out var paren))
{
yield return (index, paren);
Expand All @@ -509,6 +516,53 @@ static bool StartsToken(string source, int index) =>
index == 0 ||
!CsScan.IsIdentifierChar(source[index - 1]);

/// <summary>
/// True when the name is reached through a member access on anything other than the verify
/// entry point class. Only used for the prefix search, where the name is a guess at a verify
/// entry point rather than something already known to be one.
/// <para>
/// Verify is an ordinary enough name that a project has its own: ContentValidation.Verify,
/// validator.Verify, mock.VerifyAll. Those read exactly like an entry point to a token scan,
/// and appending a Snapshot call to one splices the snapshot into a call that never produced
/// it, in a test that may not even be the one the patch came from.
/// </para>
/// </summary>
static bool IsForeignReceiver(string source, CsScan scan, int nameStart)
{
var dot = scan.PreviousSignificant(nameStart);
if (dot < 0 ||
source[dot] != '.')
{
// Unqualified: a static using, or inherited from VerifyBase
return false;
}

var end = scan.PreviousSignificant(dot);
if (end >= 0 &&
source[end] == '?')
{
end = scan.PreviousSignificant(end);
}

if (end < 0 ||
!CsScan.IsIdentifierChar(source[end]))
{
// Not a plain receiver, so a literal, an indexer or a call result
return true;
}

var start = end;
while (start > 0 &&
CsScan.IsIdentifierChar(source[start - 1]))
{
start--;
}

var receiver = source.Substring(start, end - start + 1);
return receiver != verifierType &&
receiver != "this";
}

static bool TrySkipToParen(string source, CsScan scan, int index, out int paren)
{
paren = -1;
Expand Down
Loading