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
19 changes: 14 additions & 5 deletions src/DiffEngine.Tests/FsStringLiteralTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,21 @@ public async Task ParseRejects(string expression)
await Assert.That(parsed).IsFalse();
}

// The indent is stripped by ordinal prefix, so a content line less indented than the closing
// delimiter is not something this can read
// A triple-quoted literal that is not written to the layout convention. F# has no raw string
// form, so this is a perfectly good literal and its value is simply what it says - which is
// also what StripLayout returns for the same text, and the two have to agree or an
// OriginalValue anchor can never match.
[Test]
public async Task ParseRejectsMalformedIndent()
[Arguments("\"\"\"\n a\n \"\"\"", "\n a\n ")]
// The idiomatic hand-written shape: content starting on the opening line.
[Arguments("\"\"\"{\n \"a\": 1\n}\"\"\"", "{\n \"a\": 1\n}")]
public async Task ParseKeepsContentWithNoLayoutToStrip(string expression, string expected)
{
var parsed = FsStringLiteral.TryParse("\"\"\"\n a\n \"\"\"", out _);
await Assert.That(parsed).IsFalse();
var parsed = FsStringLiteral.TryParse(expression, out var value);
await Assert.That(parsed).IsTrue();
await Assert.That(value).IsEqualTo(expected);
// Whatever a producer sends as OriginalValue for this literal goes through StripLayout,
// so the two readers have to land in the same place
await Assert.That(FsStringLiteral.StripLayout(value!)).IsEqualTo(expected);
}
}
28 changes: 28 additions & 0 deletions src/DiffEngine.Tests/InlinePatcherFsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,34 @@ await Assert.That(newSource).IsEqualTo(
Test(" Verifier.Verify(15).Snapshot(\"new\").ToTask() |> Async.AwaitTask"));
}

/// <summary>
/// A hand-written triple-quoted literal, content starting on the opening line. F# has no raw
/// string, so the layout is only a convention and a literal not written to it still holds a
/// perfectly good value. Rejecting those made them unpatchable: the anchor an F# producer
/// sends is OriginalValue (FS0202 means there is no CallerArgumentExpression to send), and
/// what the parser read back had to be able to equal it.
/// </summary>
[Test]
public async Task ReplaceHandWrittenTripleQuotedLiteral()
{
var literal = "\"\"\"{\n \"a\": 1\n}\"\"\"";
var source = Test($" Verifier.Verify(15).Snapshot({literal}).ToTask() |> Async.AwaitTask");

var status = TryApply(
source,
5,
InlinePatchMode.Set,
null,
"new",
out var newSource,
out var reason,
originalValue: FsStringLiteral.StripLayout("{\n \"a\": 1\n}"));

await Assert.That(status).IsEqualTo(PatchStatus.Applied);
await Assert.That(reason).IsEmpty();
await Assert.That(newSource).IsEqualTo(
Test(" Verifier.Verify(15).Snapshot(\"new\").ToTask() |> Async.AwaitTask"));
}
// The same shape C# writes: the content indented under the call, with the first line and the
// closing delimiter's indentation there for the reader to take back off
[Test]
Expand Down
2 changes: 1 addition & 1 deletion src/DiffEngine/Inline/CsStringLiteral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ static bool TryScanLiteral(string text, int start, out string? value, out int en
var quotes = StringLiteral.QuoteRunLength(text, index);
if (quotes >= 3)
{
return StringLiteral.TryScanMultiLine(text, index, quotes, out value, out end);
return StringLiteral.TryScanMultiLine(text, index, quotes, true, out value, out end);
}

if (quotes == 2)
Expand Down
9 changes: 5 additions & 4 deletions src/DiffEngine/Inline/FsStringLiteral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ public static string StripLayout(string value) =>

/// <summary>
/// Parses an F# string literal expression back to the snapshot it holds: triple-quoted
/// ("""..."""), with the layout taken off, verbatim (@"...") and regular ("..."), which carry
/// their value as it is. Returns false for interpolated strings, byte strings, concatenations,
/// or any other expression. Newlines in the returned value are normalized to \n.
/// ("""..."""), with the layout taken off when it is written to that convention and
/// verbatim when it is not; verbatim (@"...") and regular ("..."), which carry their value as
/// it is. Returns false for interpolated strings, byte strings, concatenations, or any other
/// expression. Newlines in the returned value are normalized to \n.
/// </summary>
public static bool TryParse(string expression, [NotNullWhen(true)] out string? value) =>
StringLiteral.TryParse(expression, TryScanLiteral, out value);
Expand Down Expand Up @@ -131,7 +132,7 @@ static bool TryScanLiteral(string text, int start, out string? value, out int en
{
// F# reads the closing delimiter as exactly three quotes, so a longer run is content
// it cannot hold and never something this wrote
return StringLiteral.TryScanMultiLine(text, index, 3, out value, out end);
return StringLiteral.TryScanMultiLine(text, index, 3, false, out value, out end);
}

if (quotes == 2)
Expand Down
34 changes: 32 additions & 2 deletions src/DiffEngine/Inline/StringLiteral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,24 @@ public static int QuoteRunLength(string text, int index)
/// <summary>
/// Scans a multi-line literal opening at <paramref name="start"/> with a run of
/// <paramref name="quotes"/>, and returns what it holds with the layout taken off.
/// <para>
/// <paramref name="layoutRequired"/> is what separates the two languages. A C# raw string not
/// in the layout shape does not compile, so content that fails the strip is a literal this
/// never wrote and rejecting it is right. F#'s triple-quoted literal is plain verbatim text
/// and the layout is only a convention, so a hand-written one - content starting on the
/// opening line, which is the idiomatic shape - is a perfectly good literal whose value is
/// simply what it says. Rejecting those made them unpatchable: TryParse said "not a string
/// literal" while StripLayout, which is what a test library runs the value through, returned
/// the same text unchanged, so the producer's OriginalValue could never match.
/// </para>
/// </summary>
public static bool TryScanMultiLine(string text, int start, int quotes, out string? value, out int end)
public static bool TryScanMultiLine(
string text,
int start,
int quotes,
bool layoutRequired,
out string? value,
out int end)
{
value = null;
end = start;
Expand Down Expand Up @@ -224,7 +240,21 @@ public static bool TryScanMultiLine(string text, int start, int quotes, out stri
return true;
}

return TryStripLayout(SourceLanguage.NormalizeNewlines(content), out value);
var normalized = SourceLanguage.NormalizeNewlines(content);
if (TryStripLayout(normalized, out value))
{
return true;
}

if (layoutRequired)
{
return false;
}

// Not in the layout shape, so there is no layout to take off and the content is the
// value. The same answer StripLayout gives for the same text
value = normalized;
return true;
}

/// <summary>
Expand Down
Loading