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

/// <summary>
/// A chain the test ended by hand. Snapshot returns the SettingsTask and GetAwaiter does not,
/// so appending after the end of the chain produced source that does not compile - and
/// reported Applied while doing it, which leaves the snapshot recorded as accepted.
/// </summary>
[Test]
[Arguments("GetAwaiter().GetResult()")]
[Arguments("ConfigureAwait(false)")]
[Arguments("ToTask()")]
public async Task AppendGoesInFrontOfAChainTerminator(string tail)
{
var source = Method($" await Verify(x).{tail};");

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

await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(reason).IsEmpty();
// In front of the terminator, so the chain the Snapshot is appended to is still a chain
await Assert.That(newSource).Contains("Snapshot(");
await Assert.That(newSource.IndexOf("Snapshot(", StringComparison.Ordinal))
.IsLessThan(newSource.IndexOf(tail.Split('(')[0], StringComparison.Ordinal));
}

/// <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
20 changes: 20 additions & 0 deletions src/DiffEngine/Inline/CsLanguage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ public override bool TryParse(string expression, [NotNullWhen(true)] out string?

internal override char NameSeparator => ':';

/// <summary>
/// The calls a C# verify chain ends with when it stops being a verify chain: awaiting it by
/// hand, blocking on it, or converting it. Each is a real member of SettingsTask - checked
/// against Verify's source rather than guessed - and none of them returns one.
/// <para>
/// So a Snapshot appended after any of them is not merely bad style. SettingsTask.ToTask sets
/// its task field, and CurrentSettings then throws "This SettingsTask instance has already
/// been converted to a Task and can no longer be modified" - so where such a patch compiles at
/// all it fails at run time, and where it does not compile the patch still reported Applied
/// and the snapshot was recorded as accepted.
/// </para>
/// </summary>
internal override string[] ChainTerminators =>
[
"GetAwaiter",
"GetResult",
"ConfigureAwait",
"ToTask"
];

internal override bool IsIdentifierChar(char ch) =>
char.IsLetterOrDigit(ch) || ch == '_';

Expand Down
2 changes: 1 addition & 1 deletion src/DiffEngine/Inline/FsLanguage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public override bool TryParse(string expression, [NotNullWhen(true)] out string?
/// test ends the chain with ToTask. Snapshot returns the SettingsTask and ToTask does not, so
/// an appended call goes in front of it rather than after it.
/// </summary>
internal override string? ChainTerminator => "ToTask";
internal override string[] ChainTerminators => ["ToTask"];

/// <summary>
/// The F# compiler does not implement <see cref="CallerArgumentExpressionAttribute"/> - it
Expand Down
22 changes: 17 additions & 5 deletions src/DiffEngine/Inline/InlinePatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -472,13 +472,13 @@ static PatchStatus TryRemove(
/// <summary>
/// Walks the calls chained onto an invocation and returns where a call should be appended:
/// the end of the chain, or the point in front of the language's
/// <see cref="SourceLanguage.ChainTerminator"/> when the chain ends in one.
/// <see cref="SourceLanguage.ChainTerminators"/> when the chain ends in one.
/// <paramref name="found"/> is set when one of them is a call to <paramref name="name"/>.
/// </summary>
static int WalkChain(string source, SourceScan scan, int index, string name, out bool found)
{
found = false;
var terminator = scan.Language.ChainTerminator;
var terminators = scan.Language.ChainTerminators;
// Where the chain was before the terminating call, which is where an appended one goes:
// in front of the terminator, and behind the whitespace and line break that introduced it
var beforeTerminator = -1;
Expand Down Expand Up @@ -514,9 +514,8 @@ static int WalkChain(string source, SourceScan scan, int index, string name, out
found = true;
}

if (terminator != null &&
beforeTerminator < 0 &&
IsCall(source, nameStart, cursor, terminator))
if (beforeTerminator < 0 &&
IsTerminator(source, nameStart, cursor, terminators))
{
beforeTerminator = index;
}
Expand All @@ -527,6 +526,19 @@ static int WalkChain(string source, SourceScan scan, int index, string name, out
return beforeTerminator < 0 ? index : beforeTerminator;
}

static bool IsTerminator(string source, int nameStart, int nameEnd, string[] terminators)
{
foreach (var terminator in terminators)
{
if (IsCall(source, nameStart, nameEnd, terminator))
{
return true;
}
}

return false;
}

static bool IsCall(string source, int nameStart, int nameEnd, string name) =>
nameEnd - nameStart == name.Length &&
string.CompareOrdinal(source, nameStart, name, 0, name.Length) == 0;
Expand Down
11 changes: 8 additions & 3 deletions src/DiffEngine/Inline/SourceLanguage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,15 @@ public static SourceLanguage ForFile(string path)
internal abstract char NameSeparator { get; }

/// <summary>
/// A chained call that a Snapshot call has to be appended in front of rather than after, or
/// null when the end of the chain is always the insertion point.
/// Chained calls that a Snapshot call has to be appended in front of rather than after, or
/// empty when the end of the chain is always the insertion point.
/// <para>
/// These are the calls that turn the verify chain into something that is no longer one -
/// awaiting it, blocking on it, converting it - so a Snapshot appended after one is appended
/// to the wrong type and the file stops compiling.
/// </para>
/// </summary>
internal virtual string? ChainTerminator => null;
internal virtual string[] ChainTerminators => [];

/// <summary>
/// Whether a patch from this language carries the source text of the expected argument, which
Expand Down
Loading