Skip to content

Commit 41117a6

Browse files
fix: normalize CRLF line endings in sameAs diff output (#73)
* fix: normalize CRLF line endings in sameAs diff output Strip trailing \r from each line in splitLinesForDiff so that strings with CRLF (\r\n) line endings produce readable unified diff output. Previously, the raw \r caused terminal cursor to jump to column 0, overwriting the -/+ diff prefixes and making the output unreadable. Fixes #72 Co-authored-by: Kazik Pogoda <morisil@users.noreply.github.com> * improved test cases * test fix --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Kazik Pogoda <morisil@users.noreply.github.com>
1 parent ce676fd commit 41117a6

2 files changed

Lines changed: 77 additions & 5 deletions

File tree

src/commonMain/kotlin/SameAs.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,22 @@ public infix fun String?.sameAsXml(
7171
*
7272
* The standard [String.lines] function adds a trailing empty string when the string ends with \n.
7373
* This helper removes that trailing empty string to get the actual line content.
74+
*
75+
* Trailing `\r` characters are stripped from each line to normalize CRLF (`\r\n`) line endings
76+
* to LF, so that diff output is readable in terminals and CRLF/LF differences do not garble
77+
* the `-`/`+` prefixes via carriage-return overwrite.
7478
*/
7579
private fun String.splitLinesForDiff(): List<String> {
7680
if (isEmpty()) return emptyList()
7781
val lines = lines()
7882
// If string ends with newline, lines() adds ONE trailing empty string - remove only that one
79-
return if (lines.size > 1 && endsWith('\n') && lines.last().isEmpty()) {
83+
val trimmedLines = if (lines.size > 1 && endsWith('\n') && lines.last().isEmpty()) {
8084
lines.dropLast(1)
8185
} else {
8286
lines
8387
}
88+
// Normalize CRLF line endings: strip trailing \r so diff output is not garbled in terminals
89+
return trimmedLines.map { it.trimEnd('\r') }
8490
}
8591

8692
/**

src/commonTest/kotlin/SameAsTest.kt

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class SameAsTest {
3636
fun `should pass on equal HTML strings using sameAsHtml`() {
3737
// given
3838
val html = """
39-
<html>
39+
<html lang="en">
4040
<body>
4141
<h1>Hello World</h1>
4242
<p>This is a <strong>test</strong> paragraph.</p>
@@ -57,7 +57,7 @@ class SameAsTest {
5757
fun `should fail and report difference on different HTML strings using sameAsHtml`() {
5858
// given
5959
val actual = /* language=html */ """
60-
<html>
60+
<html lang="en">
6161
<body>
6262
<h1>Hello World</h1>
6363
<p>Updated paragraph.</p>
@@ -71,7 +71,7 @@ class SameAsTest {
7171
""".trimIndent()
7272

7373
val expected = """
74-
<html>
74+
<html lang="en">
7575
<body>
7676
<h1>Hello World</h1>
7777
<p>Original paragraph.</p>
@@ -94,7 +94,7 @@ class SameAsTest {
9494
--- expected
9595
+++ actual
9696
@@ -1,10 +1,10 @@
97-
<html>
97+
<html lang="en">
9898
<body>
9999
<h1>Hello World</h1>
100100
- <p>Original paragraph.</p>
@@ -2329,4 +2329,70 @@ class SameAsTest {
23292329
""".trimIndent()
23302330
}
23312331

2332+
@Test
2333+
fun `should pass on identical strings with CRLF line endings`() {
2334+
val string = "unchanged line\r\nexpected content\r\n"
2335+
string sameAs string
2336+
}
2337+
2338+
@Test
2339+
fun `should produce readable diff without raw CR for CRLF strings differing in content`() {
2340+
// given
2341+
val expected = "unchanged line\r\nexpected content\r\n"
2342+
val actual = "unchanged line\r\nactual content\r\n"
2343+
2344+
// when
2345+
val error = assertFailsWith<AssertionError> {
2346+
actual sameAs expected
2347+
}
2348+
2349+
// then
2350+
error.message sameAs """
2351+
--- expected
2352+
+++ actual
2353+
@@ -1,2 +1,2 @@
2354+
unchanged line
2355+
-expected content
2356+
+actual content
2357+
2358+
""".trimIndent()
2359+
assert('\r' !in error.message!!)
2360+
}
2361+
2362+
@Test
2363+
fun `should handle mixed CRLF and LF line endings in baseline-like content`() {
2364+
// Content with mixed line endings: LF for source sections, CRLF for JS output
2365+
// check xemantic-typescript-compiler where this bug was initially discovered
2366+
// given
2367+
fun makeBaseline(jsLine: String): String = buildString {
2368+
append("//// [example.ts]\n")
2369+
append("export const x = 1;\n")
2370+
append("\n")
2371+
append("//// [example.js]\r\n")
2372+
append("$jsLine\r\n")
2373+
}
2374+
2375+
val expected = makeBaseline("export const x = 1;")
2376+
val actual = makeBaseline("export const x = 2;")
2377+
2378+
// when
2379+
val error = assertFailsWith<AssertionError> {
2380+
actual sameAs expected
2381+
}
2382+
2383+
// then
2384+
error.message sameAs """
2385+
--- expected
2386+
+++ actual
2387+
@@ -2,4 +2,4 @@
2388+
export const x = 1;
2389+
2390+
//// [example.js]
2391+
-export const x = 1;
2392+
+export const x = 2;
2393+
2394+
""".trimIndent()
2395+
assert('\r' !in error.message!!)
2396+
}
2397+
23322398
}

0 commit comments

Comments
 (0)