Keep a string literal's own line endings when printing it - #87
Merged
Mpdreamz merged 1 commit intoSep 2, 2026
Merged
Conversation
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
approved these changes
Sep 2, 2026
Mpdreamz
left a comment
Contributor
There was a problem hiding this comment.
Awesome, first PR not from me!
Thank you for patching this hole in curbs coverage :)
Contributor
|
This is now up on nuget 👍 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #85.
What was wrong
InterpolatedStringExpressionis the one multi-line literal Curb prints as a verbatim run rather than as a token.TokenPrinter.EmitVerbatimRangesplit that run at every\n, dropped the\r, and re-issued each break as aLiteralLine— that is, with the configuredend_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:
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.Printemits 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.
Doc.csBon aLineType.Literalline now selects the ending:ConfiguredEnding/LfEnding/CrLfEndingDocArena.csSourceLine(bool crLf)emits oneDocPrinter.csPrintLinehonours it; theHasVerbatimOrRawStringforced-reparse guard is gone (see below)TokenPrinter.csEmitVerbatimRange(..., preserveLineEndings)Printers.Expressions.csVerbatimContentpassestrue— the only caller that doesDocDumper.csliteralline lf/literalline crlfComments, doc comments and disabled
#ifbranches keep normalising their endings, which is what they want — that is what bought efcore its fixed points.Removing the forced re-parse
DocPrinter.Printforced 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 andend_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_rewrittencomes back with it.Verification
Renders_the_arena_as_an_indented_s_expression, and the write-failure test that needsC:\repo) reproduce on unmodifiedmainand are unrelated.end_of_line = crlf: byte-identical output.curb checkover elastic/docs-builder (1,285 files, 41 containing$"""): 0 failed, 0 unparsable.dotnet format whitespace, all four CI legs:--reflow--width 120--reflow --preserveTests added
CoreOptionTests: threeend_of_linecases 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:SourceLinebeats the configured ending in both directions, and the risk flag no longer fires merely because a file holds a multi-line literal.