Skip to content

fix(psl): preserve schema line endings during reformat#5814

Open
barobaonguyen wants to merge 1 commit into
prisma:mainfrom
barobaonguyen:bounty/8548-prisma-format-line-endings
Open

fix(psl): preserve schema line endings during reformat#5814
barobaonguyen wants to merge 1 commit into
prisma:mainfrom
barobaonguyen:bounty/8548-prisma-format-line-endings

Conversation

@barobaonguyen
Copy link
Copy Markdown

Summary

Fixes prisma/prisma#8548prisma format emitted LF for every line of its output but added a CRLF as the trailing newline when the input used CRLF anywhere, leaving Windows users with mixed line endings (CRLF only on the last line).

Per maintainer guidance from @SevInf (prisma/prisma#8548 comment), this fix lives in the Rust formatter (psl/schema-ast/src/reformat.rs) rather than the TS CLI, and the line-ending decision is part of the renderer state — no post-process string replace.

Changes

  • New helper NewlineType::detect(&str) -> NewlineType in psl/schema-ast/src/ast/newline_type.rs — first newline in the input wins; mixed-ending inputs fall back to LF per maintainer note.
  • Renderer now carries a line_ending: NewlineType field; end_line() writes the configured separator natively (not LF + later replace).
  • reformat() auto-detects the input line ending and delegates to a new sibling reformat_with_line_ending() that exposes explicit control. The trailing-newline guard appends the configured ending too.

How tested

Added psl/psl/tests/reformat/reformat.rs::line_endings covering:

  • LF input → LF-only output (every line + trailing newline).
  • CRLF input → CRLF on every line including the trailing newline.
  • Mixed-ending input → LF fallback.
  • Explicit reformat_with_line_ending LF / CRLF.
  • The exact issue-8548 regression: LF body must not have a CRLF trailing newline.

⚠ Local Rust toolchain was not available, so cargo test -p psl line_endings could not be run on my machine before opening the PR. Marking this draft so CI verifies. Happy to iterate based on CI feedback and review notes.

Differs from prior PRs

  • Lives in this repo, not prisma/prisma TS CLI (#29245, #29299, #29373, #29460, #29475, #29561, #29569, #29570 — closed for wrong target).
  • Threads the line-ending decision through Renderer state, so each emitted line writes the configured separator rather than the formatter emitting LF and then string-replacing post-hoc (the rejection reason for fix(psl): preserve schema line endings during reformat #5812).

Closes prisma/prisma#8548

/claim prisma/prisma#8548

`prisma format` previously emitted LF for every line of its output but
ended the file with a CRLF when the input used CRLF anywhere. That left
Windows users with a stray `\r\n` on the last line of an otherwise-LF
file (or vice versa) and broke editors that disallow mixed line endings.

Fix it natively in the renderer instead of post-processing the output
(the maintainer rejected post-processing in prisma#5812). The reformatter now:

* Detects the input's line-ending style (first newline wins; mixed
  inputs fall back to LF) via a new `NewlineType::detect` helper.
* Threads the chosen ending through `Renderer` so each `end_line` writes
  the configured separator. The trailing-newline guard in `reformat` is
  updated to check for and append the configured ending too.
* Exposes a `reformat_with_line_ending` entry point alongside the
  existing `reformat` for callers that want to pin a specific style.

Closes prisma/prisma#8548
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Review Change Stack

Summary by CodeRabbit

Release Notes

  • New Features

    • The schema reformatter now preserves the input file's line-ending style (LF or CRLF) instead of converting all line endings to a single format.
  • Tests

    • Added test coverage for line-ending preservation, including LF, CRLF, mixed line endings, and regression cases.

Walkthrough

This PR implements line-ending preservation in schema reformatting. The formatter now detects whether input uses LF or CRLF line endings and preserves that style in the output, with mixed line endings defaulting to LF. A regression test ensures the output correctly terminates with the detected line ending.

Changes

Line-ending preservation in schema reformatting

Layer / File(s) Summary
Line-ending detection contract
psl/schema-ast/src/ast/newline_type.rs
NewlineType::detect() scans input for the first newline byte and identifies CRLF (Windows) or LF (Unix) style, defaulting to Unix when no newline is found.
Renderer configurable line-ending support
psl/schema-ast/src/renderer.rs
Renderer gains a line_ending: NewlineType field, constructor updated to accept line ending configuration, and end_line() modified to append the configured line ending instead of hardcoded \n.
Reformat API with line-ending preservation
psl/schema-ast/src/reformat.rs, psl/schema-ast/src/lib.rs
New public reformat_with_line_ending(input, indent_width, line_ending) function accepts explicit line ending; existing reformat() detects input style via NewlineType::detect() and delegates to preserve the original line ending. Both functions are publicly re-exported.
Line-ending preservation tests
psl/psl/tests/reformat/reformat.rs
New line_endings test module validates LF preservation, CRLF preservation, mixed line-ending default to LF, and regression case ensuring pure-LF input does not end with CRLF.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: preserving schema line endings during reformatting, which directly addresses the core issue.
Description check ✅ Passed The description is well-related to the changeset, providing clear context about the bug fix, implementation details, test coverage, and how it differs from prior attempts.
Linked Issues check ✅ Passed The PR fully addresses issue #8548 by detecting and preserving line endings throughout the reformat pipeline, ensuring consistent line endings instead of mixed LF/CRLF output.
Out of Scope Changes check ✅ Passed All changes directly support the line-ending preservation objective: NewlineType::detect, Renderer modifications, reformat refactoring, and comprehensive tests are all within scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@barobaonguyen barobaonguyen marked this pull request as ready for review May 23, 2026 01:01
Copilot AI review requested due to automatic review settings May 23, 2026 01:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates the PSL reformatter to preserve (or explicitly control) output line endings (LF vs CRLF), addressing a regression where output could end with a different newline style than the body.

Changes:

  • Add NewlineType::detect() and thread NewlineType through Renderer so all emitted newlines are consistent.
  • Introduce reformat_with_line_ending() and have reformat() auto-detect the input’s line-ending style.
  • Add regression tests ensuring LF/CRLF consistency and correct trailing newline behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
psl/schema-ast/src/renderer.rs Renderer now emits newlines using a configured NewlineType instead of always \n.
psl/schema-ast/src/reformat.rs Adds auto-detection and a public API to force line endings; ensures trailing newline matches chosen ending.
psl/schema-ast/src/lib.rs Re-exports reformat_with_line_ending as part of the public API surface.
psl/schema-ast/src/ast/newline_type.rs Adds NewlineType::detect() to infer newline style from an input string.
psl/psl/tests/reformat/reformat.rs Adds regression tests for line-ending preservation and consistency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +36 to +47
pub fn detect(input: &str) -> NewlineType {
let bytes = input.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b'\n' {
if i > 0 && bytes[i - 1] == b'\r' {
return NewlineType::Windows;
}
return NewlineType::Unix;
}
}
NewlineType::Unix
}
Comment on lines +13 to +14
/// The output preserves the line-ending style detected from `input` (LF or
/// CRLF). Mixed-ending inputs fall back to LF. See `NewlineType::detect`.
Comment on lines +1284 to +1294
#[test]
fn mixed_input_defaults_to_lf() {
// First line uses LF, later lines use CRLF: the first newline wins
// and the reformatter falls back to LF for the whole output.
let input = format!(
"{}\n{}\r\n{}\r\n{}\r\n",
SCHEMA_LINES[0], SCHEMA_LINES[1], SCHEMA_LINES[2], SCHEMA_LINES[3]
);
let out = reformat(&input);
assert!(!out.contains('\r'), "mixed input should normalize to LF: {out:?}");
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@psl/psl/tests/reformat/reformat.rs`:
- Around line 1285-1294: Add a second test variant that mirrors
mixed_input_defaults_to_lf but with CRLF on the first line(s) and LF later to
ensure normalization behavior is symmetric; duplicate the test logic in a new
function (e.g., mixed_input_with_crlf_first) using the same SCHEMA_LINES
sequence but build input so the first newline is "\r\n" and later lines use "\n"
(or construct format with CRLF-first then LF), call reformat(&input) and assert
no '\r' in out exactly like the original test to lock the mixed-ending contract
from both directions.

In `@psl/schema-ast/src/ast/newline_type.rs`:
- Around line 36-47: The detect function currently returns on the first newline
and misclassifies mixed CRLF+LF input as Windows; update NewlineType::detect to
scan the entire byte slice and track two booleans (e.g., seen_crlf and seen_lf)
while iterating bytes, setting seen_crlf when a newline has a preceding '\r' and
seen_lf when a newline does not; after the loop return NewlineType::Unix if both
flags are set (mixed -> prefer LF), otherwise return Windows if only seen_crlf,
Unix if seen_lf, or Unix by default—adjust the logic inside detect accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3c5ba2f3-013b-4cd4-86aa-32c56f7f3c0a

📥 Commits

Reviewing files that changed from the base of the PR and between 3c6e192 and d1a01a9.

📒 Files selected for processing (5)
  • psl/psl/tests/reformat/reformat.rs
  • psl/schema-ast/src/ast/newline_type.rs
  • psl/schema-ast/src/lib.rs
  • psl/schema-ast/src/reformat.rs
  • psl/schema-ast/src/renderer.rs

Comment on lines +1285 to +1294
fn mixed_input_defaults_to_lf() {
// First line uses LF, later lines use CRLF: the first newline wins
// and the reformatter falls back to LF for the whole output.
let input = format!(
"{}\n{}\r\n{}\r\n{}\r\n",
SCHEMA_LINES[0], SCHEMA_LINES[1], SCHEMA_LINES[2], SCHEMA_LINES[3]
);
let out = reformat(&input);
assert!(!out.contains('\r'), "mixed input should normalize to LF: {out:?}");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

mixed_input_defaults_to_lf only validates one mixed ordering.

This case checks LF-first mixing only. Add a CRLF-first-then-LF variant to lock the mixed-ending contract from both directions.

Test addition
 #[test]
 fn mixed_input_defaults_to_lf() {
@@
     let out = reformat(&input);
     assert!(!out.contains('\r'), "mixed input should normalize to LF: {out:?}");
 }
+
+#[test]
+fn mixed_input_crlf_first_also_defaults_to_lf() {
+    let input = format!(
+        "{}\r\n{}\n{}\r\n{}\r\n",
+        SCHEMA_LINES[0], SCHEMA_LINES[1], SCHEMA_LINES[2], SCHEMA_LINES[3]
+    );
+    let out = reformat(&input);
+    assert!(!out.contains('\r'), "mixed input should normalize to LF: {out:?}");
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@psl/psl/tests/reformat/reformat.rs` around lines 1285 - 1294, Add a second
test variant that mirrors mixed_input_defaults_to_lf but with CRLF on the first
line(s) and LF later to ensure normalization behavior is symmetric; duplicate
the test logic in a new function (e.g., mixed_input_with_crlf_first) using the
same SCHEMA_LINES sequence but build input so the first newline is "\r\n" and
later lines use "\n" (or construct format with CRLF-first then LF), call
reformat(&input) and assert no '\r' in out exactly like the original test to
lock the mixed-ending contract from both directions.

Comment on lines +36 to +47
pub fn detect(input: &str) -> NewlineType {
let bytes = input.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b'\n' {
if i > 0 && bytes[i - 1] == b'\r' {
return NewlineType::Windows;
}
return NewlineType::Unix;
}
}
NewlineType::Unix
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mixed-ending detection does not actually guarantee LF fallback.

Line 36 returns on the first newline only, so CRLF-first mixed input is classified as Windows instead of LF. That contradicts the mixed-input contract in Lines 34-35 and can reintroduce inconsistent behavior.

Suggested fix
 pub fn detect(input: &str) -> NewlineType {
     let bytes = input.as_bytes();
+    let mut saw_lf = false;
+    let mut saw_crlf = false;
+
     for (i, &b) in bytes.iter().enumerate() {
         if b == b'\n' {
             if i > 0 && bytes[i - 1] == b'\r' {
-                return NewlineType::Windows;
+                saw_crlf = true;
+            } else {
+                saw_lf = true;
             }
-            return NewlineType::Unix;
+
+            if saw_lf && saw_crlf {
+                return NewlineType::Unix;
+            }
         }
     }
-    NewlineType::Unix
+
+    if saw_crlf {
+        NewlineType::Windows
+    } else {
+        NewlineType::Unix
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn detect(input: &str) -> NewlineType {
let bytes = input.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b'\n' {
if i > 0 && bytes[i - 1] == b'\r' {
return NewlineType::Windows;
}
return NewlineType::Unix;
}
}
NewlineType::Unix
}
pub fn detect(input: &str) -> NewlineType {
let bytes = input.as_bytes();
let mut saw_lf = false;
let mut saw_crlf = false;
for (i, &b) in bytes.iter().enumerate() {
if b == b'\n' {
if i > 0 && bytes[i - 1] == b'\r' {
saw_crlf = true;
} else {
saw_lf = true;
}
if saw_lf && saw_crlf {
return NewlineType::Unix;
}
}
}
if saw_crlf {
NewlineType::Windows
} else {
NewlineType::Unix
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@psl/schema-ast/src/ast/newline_type.rs` around lines 36 - 47, The detect
function currently returns on the first newline and misclassifies mixed CRLF+LF
input as Windows; update NewlineType::detect to scan the entire byte slice and
track two booleans (e.g., seen_crlf and seen_lf) while iterating bytes, setting
seen_crlf when a newline has a preceding '\r' and seen_lf when a newline does
not; after the loop return NewlineType::Unix if both flags are set (mixed ->
prefer LF), otherwise return Windows if only seen_crlf, Unix if seen_lf, or Unix
by default—adjust the logic inside detect accordingly.

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.

prisma format ends the file with a single CRLF on windows

3 participants