Skip to content

Commit c5c99f4

Browse files
bartlomiejuclaude
andcommitted
fix: improve parse error display format
Reformats syntax error messages to use a structured format with location on its own line, pipe margins, line numbers, and carets instead of the previous flat single-line format. Before: The module's source code could not be parsed: Expected ',' got 'x' at file:///a.js:1:13 import from "./foo.js" ~~~~~~~~~~ After: Expected ',' got 'x' at file:///a.js:1:13 | 1 | import from "./foo.js" | ~~~~~~~~~~ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8687b2b commit c5c99f4

File tree

3 files changed

+111
-6
lines changed

3 files changed

+111
-6
lines changed

js/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ Deno.test({
724724
);
725725
},
726726
Error,
727-
"The module's source code could not be parsed",
727+
"at file:///a/test.js:",
728728
);
729729
},
730730
});

src/graph.rs

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -562,14 +562,119 @@ impl std::error::Error for ModuleErrorKind {
562562
}
563563
}
564564

565+
/// Reformats a parse diagnostic message into a nicer format with
566+
/// `-->` location arrows, line numbers, and carets.
567+
///
568+
/// Input format (from ParseDiagnostic::Display):
569+
/// `{message} at {specifier}:{line}:{col}\n\n {source_line}\n {underline}`
570+
///
571+
/// Output format:
572+
/// `{message}\n at {specifier}:{line}:{col}\n\n {line_num} | {source_line}\n {carets}`
573+
fn format_parse_diagnostic(
574+
f: &mut fmt::Formatter<'_>,
575+
diagnostic: &str,
576+
) -> fmt::Result {
577+
// Try to parse the diagnostic format: "{message} at {location}\n\n {snippet}"
578+
if let Some(at_pos) = diagnostic.find(" at ") {
579+
let message = &diagnostic[..at_pos];
580+
581+
// Find the end of the location line (first newline after " at ")
582+
let after_at = &diagnostic[at_pos + 4..];
583+
let (location, rest) = if let Some(newline_pos) = after_at.find('\n') {
584+
(&after_at[..newline_pos], &after_at[newline_pos..])
585+
} else {
586+
(after_at, "")
587+
};
588+
589+
// Parse location to extract line number
590+
// Location format: {specifier}:{line}:{col}
591+
let line_num = location
592+
.rsplit(':')
593+
.nth(1)
594+
.and_then(|s| s.parse::<usize>().ok());
595+
596+
write!(f, "{message}\n at {location}")?;
597+
598+
if !rest.is_empty() {
599+
// The rest contains the source snippet, indented with " "
600+
// Reformat with line numbers
601+
let lines: Vec<&str> = rest.lines().collect();
602+
603+
// Find source line and underline (skip empty lines)
604+
let mut source_line = None;
605+
let mut underline = None;
606+
for line in &lines {
607+
let trimmed = line.trim();
608+
if trimmed.is_empty() {
609+
continue;
610+
}
611+
if source_line.is_none() {
612+
source_line = Some(trimmed);
613+
} else if underline.is_none() {
614+
underline = Some(trimmed);
615+
}
616+
}
617+
618+
if let (Some(src), Some(ul)) = (source_line, underline) {
619+
writeln!(f)?;
620+
if let Some(num) = line_num {
621+
let num_str = num.to_string();
622+
let padding = " ".repeat(num_str.len());
623+
writeln!(f, "{padding} |")?;
624+
writeln!(f, "{num_str} | {src}")?;
625+
// Calculate indent: we need to align the underline under the source
626+
// The underline in the original starts at the same column as the
627+
// source, so we need to figure out the offset
628+
let src_start = source_line
629+
.and_then(|s| {
630+
lines
631+
.iter()
632+
.find(|l| l.trim() == s)
633+
.map(|l| l.len() - l.trim_start().len())
634+
})
635+
.unwrap_or(2);
636+
let ul_start = underline
637+
.and_then(|u| {
638+
lines
639+
.iter()
640+
.find(|l| l.trim() == u)
641+
.map(|l| l.len() - l.trim_start().len())
642+
})
643+
.unwrap_or(2);
644+
// The offset of the underline relative to the source
645+
let offset = if ul_start >= src_start {
646+
ul_start - src_start
647+
} else {
648+
0
649+
};
650+
write!(
651+
f,
652+
"{padding} | {}{}",
653+
" ".repeat(offset),
654+
ul
655+
)?;
656+
} else {
657+
writeln!(f)?;
658+
writeln!(f, " {src}")?;
659+
write!(f, " {ul}")?;
660+
}
661+
}
662+
}
663+
664+
Ok(())
665+
} else {
666+
// Fallback: just write the diagnostic as-is
667+
write!(f, "{}", diagnostic)
668+
}
669+
}
670+
565671
impl fmt::Display for ModuleErrorKind {
566672
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
567673
match self {
568674
Self::Load { err, .. } => err.fmt(f),
569-
Self::Parse { diagnostic, .. } => write!(
570-
f,
571-
"The module's source code could not be parsed: {diagnostic}"
572-
),
675+
Self::Parse { diagnostic, .. } => {
676+
format_parse_diagnostic(f, &diagnostic.to_string())
677+
}
573678
Self::WasmParse { specifier, err, .. } => write!(
574679
f,
575680
"The Wasm module could not be parsed: {err}\n Specifier: {specifier}"

tests/specs/graph/cjs/file_export.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class Test {}
1212
"modules": [
1313
{
1414
"specifier": "file:///file.cjs",
15-
"error": "The module's source code could not be parsed: 'import', and 'export' cannot be used outside of module code at file:///file.cjs:1:1\n\n export class Test {}\n ~~~~~~"
15+
"error": "'import', and 'export' cannot be used outside of module code\n at file:///file.cjs:1:1\n |\n1 | export class Test {}\n | ~~~~~~"
1616
},
1717
{
1818
"kind": "esm",

0 commit comments

Comments
 (0)