Skip to content

Keep a string literal's own line endings when printing it - #87

Merged
Mpdreamz merged 1 commit into
nullean:mainfrom
kfrancis:fix/85-interpolated-raw-string-line-endings
Sep 2, 2026
Merged

Mpdreamz merged 1 commit into
nullean:mainfrom
kfrancis:fix/85-interpolated-raw-string-line-endings

Conversation

@kfrancis

@kfrancis kfrancis commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #85.

What was wrong

InterpolatedStringExpression is the one multi-line literal Curb prints as a verbatim run rather than as a token. TokenPrinter.EmitVerbatimRange split that run at every \n, dropped the \r, and re-issued each break as a LiteralLine — that is, with the configured end_of_line.

Those newlines are not layout. They are characters of the value the literal builds, and they are part of the token's own text. Rewriting them changed the program, and the re-parse comparer was right to report it:

A.cs: formatting changed token 12: '$"""
' became '$"""
'

Both sides print the same because the only difference is an invisible \r.

Every other multi-line literal survives because Roslyn hands it over as a single token, which TokenPrinter.Print emits as one text leaf. So """ and @"" were fine while $""" was not — which is the split the issue observed. It is not raw-string-specific either: $@"one\ntwo" fails identically.

The failure only fires when the literal's endings differ from the resolved end_of_line, which is why the issue saw 1 file out of 10 fail and the other 9 sit green.

The fix

Give the document IR a literal line that reproduces the source's own ending, and use it for string content only.

File Change
Doc.cs B on a LineType.Literal line now selects the ending: ConfiguredEnding / LfEnding / CrLfEnding
DocArena.cs SourceLine(bool crLf) emits one
DocPrinter.cs PrintLine honours it; the HasVerbatimOrRawString forced-reparse guard is gone (see below)
TokenPrinter.cs EmitVerbatimRange(..., preserveLineEndings)
Printers.Expressions.cs VerbatimContent passes true — the only caller that does
DocDumper.cs dumps literalline lf / literalline crlf

Comments, doc comments and disabled #if branches keep normalising their endings, which is what they want — that is what bought efcore its fixed points.

Removing the forced re-parse

DocPrinter.Print forced a round-trip parse on every mixed-ending file containing @" or """, for exactly this rewrite. With the rewrite gone the guard has nothing to find, so it goes too. On Curb's own sources with LF input and end_of_line = crlf, forced re-parses drop from 64 of 144 to 0, with byte-identical output.

This is a deliberate scope addition rather than part of the minimal fix. It reverts cleanly as its own hunk if you would rather keep it — Fires_when_line_endings_are_being_rewritten comes back with it.

Verification

  • The issue's repro formats to itself and exits 0. Layout newlines become CRLF; the literal's stay LF.
  • Unit suite: 1205 passing. Two failures (Renders_the_arena_as_an_indented_s_expression, and the write-failure test that needs C:\repo) reproduce on unmodified main and are unrelated.
  • Old vs. new binary over 144 repo files, LF source with end_of_line = crlf: byte-identical output.
  • curb check over elastic/docs-builder (1,285 files, 41 containing $"""): 0 failed, 0 unparsable.
  • Conformance against dotnet format whitespace, all four CI legs:
Configuration Result
reflow off 1285/1285 (100.00%)
--reflow 1285/1285 (100.00%)
--width 120 1285/1285 (100.00%)
--reflow --preserve 1285/1285 (100.00%)

Tests added

  • CoreOptionTests: three end_of_line cases pinning that an interpolated raw string, an interpolated verbatim string, and a plain raw string each keep their own endings while the layout around them converts.
  • RoundTripRiskTests: SourceLine beats the configured ending in both directions, and the risk flag no longer fires merely because a file holds a multi-line literal.

An interpolated string is printed as one verbatim run rather than as a
token, so `TokenPrinter.EmitVerbatimRange` split it at every newline and
re-issued each break with the configured `end_of_line`. Those newlines are
not layout: they are characters of the value the literal builds, and they
are part of the token's own text. Rewriting them changed the program, and
the re-parse comparer correctly reported it as `formatting changed token N`
— identical-looking on both sides, because the only difference is a `\r`.

Every other multi-line literal survives because Roslyn hands it over as a
single token, which `TokenPrinter.Print` emits as one text leaf. `"""` and
`@""` were therefore fine and `$"""` and `$@""` were not, which is what
issue nullean#85 observed.

`Doc` gains three sentinels for `B` on a literal line — the configured
ending, LF, or CRLF — and `DocArena.SourceLine` emits one carrying the
ending the source had at that point. `EmitVerbatimRange` uses it behind
`preserveLineEndings`, which only the interpolated-string printer sets:
comments, doc comments and disabled `#if` branches want their endings
normalised with everything else, and keep it.

That removes the reason `DocPrinter.Print` forced a round-trip parse on
every mixed-ending file containing `@"` or `"""`. With the rewrite gone
there is nothing for the second parse to find, so the guard goes with it —
64 of 144 forced re-parses became 0 on curb's own sources, with output
byte-identical.

Conformance on elastic/docs-builder (1,285 files) holds at 100% in all four
configurations: reflow off, reflow on, width 120, and preserving.

@Mpdreamz Mpdreamz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, first PR not from me!

Thank you for patching this hole in curbs coverage :)

@Mpdreamz
Mpdreamz merged commit dce0893 into nullean:main Sep 2, 2026
3 checks passed
@Mpdreamz

Mpdreamz commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

This is now up on nuget 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Round-trip verification fails on multi-line interpolated raw string literals ($"""), exit 3 / CURB0002

2 participants