fix(formatter_core): report error position releative to CRLF source (#27777)

e.g. CSS, YAML formatter replace CRLF beforehand, so when reports error, need to restore positon.
This commit is contained in:
leaysgur committed 2026-10-16 04:04:06 +00:00
1 parent c78e80edc5
commit 5b2acb0427
7 files changed
+58 -9

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { format } from "../../dist/index.js";
describe("Line ending handling", () => {
// Some entries parse a newline-normalized copy, but error labels must point into the original source
it.each([
["a.css", "a {}\nb {"],
["a.css", "\uFEFFa {}\nb {"],
["a.yaml", "a: 1\nb: ["],
])("reports error labels in %s against the CRLF source", async (filename, source) => {
const lf = await format(filename, source);
const crlf = await format(filename, source.replaceAll("\n", "\r\n"));
// Each input has 1 line break before the error
expect(crlf.errors[0].labels).toStrictEqual(
// oxlint-disable no-map-spread
lf.errors[0].labels.map((l) => ({ ...l, start: l.start + 1, end: l.end + 1 })),
);
});
});
+1 -1
View File
@@ -112,7 +112,7 @@ A type/fn that fits none of them belongs in a consumer crate.
Admission: the printing phase consumes it. "Shared by all languages" is NOT a reason on its own.
- (2) `source/`: Source-side access mechanics (`SourceText`, `SpanCursor`, `rebase_labels`)
- (2) `source/`: Source-side access mechanics (`SourceText`, `SpanCursor`, `rebase_labels`, `restore_crlf_labels`)
Admission: the structure makes no output decision by itself; language differences arrive as data (offsets, the item type), never as parameters encoding grammar or policy.
+1 -1
View File
@@ -52,7 +52,7 @@ pub use session::embedded::{
dispatch_fragment_ir, dispatch_ir, map_text_in_ir, push_text_with_literal_lines,
};
pub use session::{FormatSession, InputKind, SessionServices, StringEmbedder, TailwindSorter};
pub use source::{SourceText, SpanCursor, rebase_labels};
pub use source::{SourceText, SpanCursor, rebase_labels, restore_crlf_labels};
pub use write::Format;
pub use write::arguments::{Argument, Arguments};
pub use write::buffer::ScratchBuffer;
+28 -1
View File
@@ -1,4 +1,4 @@
use oxc_span::LabeledSpan;
use oxc_span::{LabeledSpan, Span};
/// Moves `labels` pointing into `tail`, a suffix of `source`, onto `source`.
///
@@ -11,3 +11,30 @@ pub fn rebase_labels(labels: &mut [LabeledSpan], source: &str, tail: &str) {
label.set_span_offset(label.offset().saturating_add(offset));
}
}
/// Moves `labels` pointing into `normalize_newlines(source, ['\r'])` onto `source`.
///
/// Entries that normalize line endings before parsing collapse each `\r\n` into one byte,
/// so a label shifts right by the CRLFs before it.
pub fn restore_crlf_labels(labels: &mut [LabeledSpan], source: &str) {
let restore = |offset: u32| {
let mut offset = offset as usize;
for (pos, _) in source.match_indices("\r\n") {
if pos >= offset {
break;
}
offset += 1;
}
u32::try_from(offset).unwrap_or(u32::MAX)
};
for label in labels {
let span = label.span();
let span = Span::new(restore(span.start), restore(span.end));
let text = label.label().map(ToString::to_string);
*label = if label.primary() {
LabeledSpan::new_primary_with_span(text, span)
} else {
LabeledSpan::new_with_span(text, span)
};
}
}
+2 -2
View File
@@ -3,7 +3,7 @@
//!
//! - [`SourceText`]: offset-keyed byte/slice access (WHERE)
//! - [`SpanCursor`]: span-ordered drain over a sorted item slice (WHO is next)
//! - [`rebase_labels`]: diagnostic labels moved from a trimmed tail back onto the source
//! - [`rebase_labels`] / [`restore_crlf_labels`]: diagnostic labels moved from a trimmed / newline-normalized copy back onto the source
//!
//! Admission: the structure decides nothing about the output.
//! Unlike `spec/`, which encodes shared formatting behavior as pure functions.
@@ -17,5 +17,5 @@ mod labels;
mod text;
pub use cursor::SpanCursor;
pub use labels::rebase_labels;
pub use labels::{rebase_labels, restore_crlf_labels};
pub use text::SourceText;
+1
View File
@@ -117,6 +117,7 @@ pub fn parse_for_format<'a>(
let PreparedSource { source, parse_source, front_matter } = prepare_source(allocator, stripped);
let (stylesheet, comments) =
parse_stylesheet(allocator, parse_source, options, root).map_err(|mut err| {
oxc_formatter_core::restore_crlf_labels(&mut err.labels, stripped);
oxc_formatter_core::rebase_labels(&mut err.labels, source_text, stripped);
err
})?;
+6 -4
View File
@@ -117,12 +117,14 @@ fn parse_root<'a>(
// NOTE: Normalize line endings BEFORE parsing, unlike other `oxc_formatter_xxx`.
// For YAML formatter, the printer slices verbatim text from the source in many places.
// YAML is also unusual in that line breaks and whitespace have meaning.
let source_text = oxc_formatter_core::normalize_newlines(source_text, ['\r']);
let source: &'a str = allocator.alloc_str(&source_text);
let normalized = oxc_formatter_core::normalize_newlines(source_text, ['\r']);
let source: &'a str = allocator.alloc_str(&normalized);
let root = Parser::new(allocator, source).parse().map_err(|error| {
OxcDiagnostic::error(format!("Syntax error: {}", error.kind))
.with_label(to_span(error.span))
let mut err = OxcDiagnostic::error(format!("Syntax error: {}", error.kind))
.with_label(to_span(error.span));
oxc_formatter_core::restore_crlf_labels(&mut err.labels, source_text);
err
})?;
let root = allocator.alloc(root);