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

/// <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
/// inside that member - and a declaration between the two says it is not. Trying it anyway
/// rewrote the other test's identical snapshot and left this one as it was.
/// </summary>
[Test]
public async Task AStaleHintDoesNotReachIntoTheNextMember()
{
var source =
"class Tests\n" +
"{\n" +
" async Task First()\n" +
" {\n" +
" await Snapshot(\"dup\");\n" +
" }\n" +
"\n" +
" async Task Second()\n" +
" {\n" +
" await Snapshot(\"dup\");\n" +
" }\n" +
"}";

// Line 10 is Second's snapshot; the patch came from First
var status = TryApply(source, 10, InlinePatchMode.Set, "\"dup\"", "new", out var newSource, out _, memberName: "First");

await Assert.That(status).IsEqualTo(PatchStatus.Applied);
var patched = newSource.Split('\n');
await Assert.That(patched[4]).Contains("\"new\"");
await Assert.That(patched[9]).Contains("\"dup\"");
}

/// <summary>
/// A CRLF file holding a literal whose own lines are LF. The anchor is normalised to the
/// file's dominant ending before the search, which is right for writing and wrong for
Expand Down
48 changes: 47 additions & 1 deletion src/DiffEngine/Inline/InlinePatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,14 @@ static bool TryFindCall(
lineHint = Clamp(lineHint, lineCount);
var floor = memberLine is null ? 1 : Clamp(memberLine.Value, lineCount);
var origin = memberLine is null ? lineHint : floor;
if (lineHint >= floor)
// The recorded line is tried first so that two snapshots in one member stay apart. It is
// only evidence about this member while it is still inside it, though, and a declaration
// between the two says it is not: the hint went stale, something above it moved, and it
// now points into the test next door. Trying it anyway rewrote that test's snapshot and
// left this one alone, which is the failure the member name exists to prevent
if (lineHint >= floor &&
(memberLine is null ||
!DeclarationBetween(source, scan, lineStarts, floor, lineHint)))
{
foreach (var call in CallsOnLine(source, scan, lineStarts, lineHint, names, byPrefix))
{
Expand Down Expand Up @@ -642,6 +649,45 @@ static bool TryFindCall(
}
}

/// <summary>
/// Whether a declaration sits after <paramref name="afterLine"/> and at or before
/// <paramref name="uptoLine"/>. Cheap because it only ever runs over the span between a
/// member's declaration and the recorded line.
/// </summary>
static bool DeclarationBetween(string source, SourceScan scan, List<int> lineStarts, int afterLine, int uptoLine)
{
if (uptoLine <= afterLine ||
afterLine >= lineStarts.Count)
{
return false;
}

var start = lineStarts[afterLine];
var end = uptoLine < lineStarts.Count ? lineStarts[uptoLine] : source.Length;
for (var index = start; index < end; index++)
{
if (!scan.IsIdentifierChar(source[index]) ||
!scan.IsCode(index) ||
!StartsToken(source, scan, index))
{
continue;
}

if (scan.IsDeclaration(index))
{
return true;
}

while (index + 1 < end &&
scan.IsIdentifierChar(source[index + 1]))
{
index++;
}
}

return false;
}

static int Clamp(int line, int lineCount) =>
Math.Min(Math.Max(line, 1), lineCount);

Expand Down
Loading