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
35 changes: 35 additions & 0 deletions src/DiffEngine.Tests/InlinePatcherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,41 @@ public async Task ASnapshotBeforeAVerbatimStringOpeningOnAnEscapedQuoteIsStillFo
const string q3 = "\"\"\"";
const string q4 = "\"\"\"\"";

/// <summary>
/// A retire is anchored the same way a set is. It used to delete whichever call sat nearest
/// the recorded line, and that line stops being true the moment anything above it is edited -
/// so a stale hint retired the snapshot in the test next door and reported Applied.
/// </summary>
[Test]
public async Task RemoveTakesTheCallTheAnchorNamesRatherThanTheNearest()
{
var source = Method(
" await A().Snapshot(\"one\");\n" +
" await B().Snapshot(\"two\");");

// Hint on the second call, anchor on the first
var status = TryApply(source, 6, InlinePatchMode.Remove, "\"one\"", "", out var newSource, out _);

await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(newSource).DoesNotContain("\"one\"");
// The other test's snapshot is left alone
await Assert.That(newSource).Contains("Snapshot(\"two\")");
}

/// <summary>
/// And an anchor that matches nothing is reported rather than resolved to the nearest call.
/// </summary>
[Test]
public async Task RemoveReportsWhenTheAnchorIsGone()
{
var source = Method(" await A().Snapshot(\"two\");");

var status = TryApply(source, 5, InlinePatchMode.Remove, "\"one\"", "", out _, out var reason);

await Assert.That(status).IsEqualTo(PatchStatus.NotFound);
await Assert.That(reason).Contains("still the one the test run saw");
}

/// <summary>
/// A hint that has gone stale and now points into another member. The recorded line is tried
/// first so two snapshots in one member stay apart, but it is only evidence while it is still
Expand Down
90 changes: 87 additions & 3 deletions src/DiffEngine/Inline/InlinePatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public static PatchStatus TryApply(

if (mode == InlinePatchMode.Remove)
{
return TryRemove(source, scan, lineStarts, lineHint, memberLine, ref newSource, ref failReason);
return TryRemove(language, source, scan, lineStarts, lineHint, memberLine, originalExpression, originalValue, eol, ref newSource, ref failReason);
}

var fileUnit = DetectIndentUnit(source, scan, lineStarts);
Expand Down Expand Up @@ -406,17 +406,24 @@ static PatchStatus TryAppend(
/// blank line is left behind.
/// </summary>
static PatchStatus TryRemove(
SourceLanguage language,
string source,
SourceScan scan,
List<int> lineStarts,
int lineHint,
int? memberLine,
string? originalExpression,
string? originalValue,
string eol,
ref string newSource,
ref string failReason)
{
if (!TryFindCall(source, scan, lineStarts, lineHint, memberLine, snapshotName, false, out var nameStart, out var openParen))
var anchored = !string.IsNullOrEmpty(originalExpression) || originalValue != null;
if (!TryFindAnchoredCall(language, source, scan, lineStarts, lineHint, memberLine, originalExpression, originalValue, eol, out var nameStart, out var openParen))
{
failReason = $"Could not find a {methodName} call near line {lineHint}. The source may have changed since the test run. Re-run the test.";
failReason = anchored
? $"Could not find a {methodName} call near line {lineHint} whose expected argument is still the one the test run saw. The source may have changed since the test run. Re-run the test."
: $"Could not find a {methodName} call near line {lineHint}. The source may have changed since the test run. Re-run the test.";
return PatchStatus.NotFound;
}

Expand Down Expand Up @@ -546,6 +553,83 @@ static string LeadingWhitespace(string source, List<int> lineStarts, int offset)

static readonly string[] snapshotName = [methodName];

/// <summary>
/// The call the anchor names, rather than whichever one sits nearest the hint.
/// <para>
/// Set and Append locate by content for a reason - the same literal is just as likely to be in
/// the test next door - and a Remove has exactly the same problem with none of the protection.
/// It deleted the nearest call to a line number that stops being true as soon as anything
/// above it is edited, so a stale hint retired somebody else's snapshot and reported Applied.
/// </para>
/// <para>
/// With no anchor there is nothing to match on and nearest-to-the-hint is all there is, which
/// is the case for a producer whose language withholds CallerArgumentExpression and sends no
/// value either.
/// </para>
/// </summary>
static bool TryFindAnchoredCall(
SourceLanguage language,
string source,
SourceScan scan,
List<int> lineStarts,
int lineHint,
int? memberLine,
string? originalExpression,
string? originalValue,
string eol,
out int nameStart,
out int openParen)
{
if (string.IsNullOrEmpty(originalExpression) &&
originalValue == null)
{
return TryFindCall(source, scan, lineStarts, lineHint, memberLine, snapshotName, false, out nameStart, out openParen);
}

// ReSharper disable once RedundantSuppressNullableWarningExpression
var needle = string.IsNullOrEmpty(originalExpression) ? null : NormalizeTo(originalExpression!, eol);
var previous = originalValue == null ? null : SourceLanguage.NormalizeNewlines(originalValue);

foreach (var (candidateName, candidateParen) in FindCalls(source, scan, lineStarts, lineHint, memberLine, snapshotName, false))
{
if (!TryReadArguments(source, scan, candidateParen, out var expected))
{
continue;
}

if (needle != null)
{
if (!expected.Matches(source, needle))
{
continue;
}
}
else
{
if (expected.IsAbsent ||
expected.BlockedByName)
{
continue;
}

var argument = source.Substring(expected.Start, expected.End - expected.Start);
if (!language.TryParse(argument, out var value) ||
value != previous)
{
continue;
}
}

nameStart = candidateName;
openParen = candidateParen;
return true;
}

nameStart = -1;
openParen = -1;
return false;
}

static bool TryFindCall(string source, SourceScan scan, List<int> lineStarts, int lineHint, int? memberLine, out int openParen) =>
TryFindCall(source, scan, lineStarts, lineHint, memberLine, snapshotName, false, out _, out openParen);

Expand Down
Loading