From c5c99f4fdc84c99469d20d9687dcb7f21409e4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 17:28:53 +0100 Subject: [PATCH 1/5] 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 --- js/test.ts | 2 +- src/graph.rs | 113 +++++++++++++++++++++++++- tests/specs/graph/cjs/file_export.txt | 2 +- 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/js/test.ts b/js/test.ts index ad75f3fcf..6bc6ab050 100644 --- a/js/test.ts +++ b/js/test.ts @@ -724,7 +724,7 @@ Deno.test({ ); }, Error, - "The module's source code could not be parsed", + "at file:///a/test.js:", ); }, }); diff --git a/src/graph.rs b/src/graph.rs index 6c957fb54..5948949de 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -562,14 +562,119 @@ impl std::error::Error for ModuleErrorKind { } } +/// Reformats a parse diagnostic message into a nicer format with +/// `-->` location arrows, line numbers, and carets. +/// +/// Input format (from ParseDiagnostic::Display): +/// `{message} at {specifier}:{line}:{col}\n\n {source_line}\n {underline}` +/// +/// Output format: +/// `{message}\n at {specifier}:{line}:{col}\n\n {line_num} | {source_line}\n {carets}` +fn format_parse_diagnostic( + f: &mut fmt::Formatter<'_>, + diagnostic: &str, +) -> fmt::Result { + // Try to parse the diagnostic format: "{message} at {location}\n\n {snippet}" + if let Some(at_pos) = diagnostic.find(" at ") { + let message = &diagnostic[..at_pos]; + + // Find the end of the location line (first newline after " at ") + let after_at = &diagnostic[at_pos + 4..]; + let (location, rest) = if let Some(newline_pos) = after_at.find('\n') { + (&after_at[..newline_pos], &after_at[newline_pos..]) + } else { + (after_at, "") + }; + + // Parse location to extract line number + // Location format: {specifier}:{line}:{col} + let line_num = location + .rsplit(':') + .nth(1) + .and_then(|s| s.parse::().ok()); + + write!(f, "{message}\n at {location}")?; + + if !rest.is_empty() { + // The rest contains the source snippet, indented with " " + // Reformat with line numbers + let lines: Vec<&str> = rest.lines().collect(); + + // Find source line and underline (skip empty lines) + let mut source_line = None; + let mut underline = None; + for line in &lines { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + if source_line.is_none() { + source_line = Some(trimmed); + } else if underline.is_none() { + underline = Some(trimmed); + } + } + + if let (Some(src), Some(ul)) = (source_line, underline) { + writeln!(f)?; + if let Some(num) = line_num { + let num_str = num.to_string(); + let padding = " ".repeat(num_str.len()); + writeln!(f, "{padding} |")?; + writeln!(f, "{num_str} | {src}")?; + // Calculate indent: we need to align the underline under the source + // The underline in the original starts at the same column as the + // source, so we need to figure out the offset + let src_start = source_line + .and_then(|s| { + lines + .iter() + .find(|l| l.trim() == s) + .map(|l| l.len() - l.trim_start().len()) + }) + .unwrap_or(2); + let ul_start = underline + .and_then(|u| { + lines + .iter() + .find(|l| l.trim() == u) + .map(|l| l.len() - l.trim_start().len()) + }) + .unwrap_or(2); + // The offset of the underline relative to the source + let offset = if ul_start >= src_start { + ul_start - src_start + } else { + 0 + }; + write!( + f, + "{padding} | {}{}", + " ".repeat(offset), + ul + )?; + } else { + writeln!(f)?; + writeln!(f, " {src}")?; + write!(f, " {ul}")?; + } + } + } + + Ok(()) + } else { + // Fallback: just write the diagnostic as-is + write!(f, "{}", diagnostic) + } +} + impl fmt::Display for ModuleErrorKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Load { err, .. } => err.fmt(f), - Self::Parse { diagnostic, .. } => write!( - f, - "The module's source code could not be parsed: {diagnostic}" - ), + Self::Parse { diagnostic, .. } => { + format_parse_diagnostic(f, &diagnostic.to_string()) + } Self::WasmParse { specifier, err, .. } => write!( f, "The Wasm module could not be parsed: {err}\n Specifier: {specifier}" diff --git a/tests/specs/graph/cjs/file_export.txt b/tests/specs/graph/cjs/file_export.txt index 39fef25f9..efea1a10f 100644 --- a/tests/specs/graph/cjs/file_export.txt +++ b/tests/specs/graph/cjs/file_export.txt @@ -12,7 +12,7 @@ export class Test {} "modules": [ { "specifier": "file:///file.cjs", - "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 ~~~~~~" + "error": "'import', and 'export' cannot be used outside of module code\n at file:///file.cjs:1:1\n |\n1 | export class Test {}\n | ~~~~~~" }, { "kind": "esm", From ea56c53a1a622393b11cd22e175a4b89daed046b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 17:47:42 +0100 Subject: [PATCH 2/5] refactor: move location after snippet, clean up token descriptions - Location line now appears after the code snippet instead of before - String literal token descriptions simplified (e.g. 'string literal ("./foo.js", "./foo.js")' -> "./foo.js") Co-Authored-By: Claude Opus 4.6 --- src/graph.rs | 46 +++++++++++++++++++++------ tests/specs/graph/cjs/file_export.txt | 2 +- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 5948949de..c56ca2d2f 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -562,21 +562,48 @@ impl std::error::Error for ModuleErrorKind { } } +/// Cleans up SWC token descriptions in error messages. +/// e.g. `'string literal ("./foo.js", "./foo.js")'` -> `"./foo.js"` +fn clean_parse_message(message: &str) -> String { + // Replace `'string literal ("{value}", "{value}")'` with `"{value}"` + let mut result = message.to_string(); + while let Some(start) = result.find("'string literal (\"") { + let after = &result[start + 18..]; // skip `'string literal ("` + if let Some(quote_end) = after.find('"') { + let value = &after[..quote_end]; + // Find the closing `)'` + if let Some(end) = result[start..].find(")'") { + let full_end = start + end + 2; + result = format!( + "{}\"{}\"{}", + &result[..start], + value, + &result[full_end..] + ); + continue; + } + } + break; + } + result +} + /// Reformats a parse diagnostic message into a nicer format with -/// `-->` location arrows, line numbers, and carets. +/// line numbers and carets, with the location shown after the snippet. /// /// Input format (from ParseDiagnostic::Display): /// `{message} at {specifier}:{line}:{col}\n\n {source_line}\n {underline}` /// /// Output format: -/// `{message}\n at {specifier}:{line}:{col}\n\n {line_num} | {source_line}\n {carets}` +/// `{message}\n |\nN | {source_line}\n | {carets}\n at {location}` fn format_parse_diagnostic( f: &mut fmt::Formatter<'_>, diagnostic: &str, ) -> fmt::Result { // Try to parse the diagnostic format: "{message} at {location}\n\n {snippet}" if let Some(at_pos) = diagnostic.find(" at ") { - let message = &diagnostic[..at_pos]; + let raw_message = &diagnostic[..at_pos]; + let message = clean_parse_message(raw_message); // Find the end of the location line (first newline after " at ") let after_at = &diagnostic[at_pos + 4..]; @@ -593,7 +620,7 @@ fn format_parse_diagnostic( .nth(1) .and_then(|s| s.parse::().ok()); - write!(f, "{message}\n at {location}")?; + write!(f, "{message}")?; if !rest.is_empty() { // The rest contains the source snippet, indented with " " @@ -622,9 +649,7 @@ fn format_parse_diagnostic( let padding = " ".repeat(num_str.len()); writeln!(f, "{padding} |")?; writeln!(f, "{num_str} | {src}")?; - // Calculate indent: we need to align the underline under the source - // The underline in the original starts at the same column as the - // source, so we need to figure out the offset + // Calculate indent: align the underline under the source let src_start = source_line .and_then(|s| { lines @@ -641,13 +666,12 @@ fn format_parse_diagnostic( .map(|l| l.len() - l.trim_start().len()) }) .unwrap_or(2); - // The offset of the underline relative to the source let offset = if ul_start >= src_start { ul_start - src_start } else { 0 }; - write!( + writeln!( f, "{padding} | {}{}", " ".repeat(offset), @@ -656,11 +680,13 @@ fn format_parse_diagnostic( } else { writeln!(f)?; writeln!(f, " {src}")?; - write!(f, " {ul}")?; + writeln!(f, " {ul}")?; } } } + write!(f, " at {location}")?; + Ok(()) } else { // Fallback: just write the diagnostic as-is diff --git a/tests/specs/graph/cjs/file_export.txt b/tests/specs/graph/cjs/file_export.txt index efea1a10f..1e1407fb6 100644 --- a/tests/specs/graph/cjs/file_export.txt +++ b/tests/specs/graph/cjs/file_export.txt @@ -12,7 +12,7 @@ export class Test {} "modules": [ { "specifier": "file:///file.cjs", - "error": "'import', and 'export' cannot be used outside of module code\n at file:///file.cjs:1:1\n |\n1 | export class Test {}\n | ~~~~~~" + "error": "'import', and 'export' cannot be used outside of module code\n |\n1 | export class Test {}\n | ~~~~~~\n at file:///file.cjs:1:1" }, { "kind": "esm", From dd86fcdcbeb31c46f71b501221434402c356dde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 17:54:50 +0100 Subject: [PATCH 3/5] fmt --- src/graph.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index c56ca2d2f..14b35ff68 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -574,12 +574,8 @@ fn clean_parse_message(message: &str) -> String { // Find the closing `)'` if let Some(end) = result[start..].find(")'") { let full_end = start + end + 2; - result = format!( - "{}\"{}\"{}", - &result[..start], - value, - &result[full_end..] - ); + result = + format!("{}\"{}\"{}", &result[..start], value, &result[full_end..]); continue; } } @@ -671,12 +667,7 @@ fn format_parse_diagnostic( } else { 0 }; - writeln!( - f, - "{padding} | {}{}", - " ".repeat(offset), - ul - )?; + writeln!(f, "{padding} | {}{}", " ".repeat(offset), ul)?; } else { writeln!(f)?; writeln!(f, " {src}")?; From ae31ded38138b378e6b79c648fc53512cd2f2fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 18:15:19 +0100 Subject: [PATCH 4/5] clippy --- src/graph.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 14b35ff68..5cdb442aa 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -662,11 +662,7 @@ fn format_parse_diagnostic( .map(|l| l.len() - l.trim_start().len()) }) .unwrap_or(2); - let offset = if ul_start >= src_start { - ul_start - src_start - } else { - 0 - }; + let offset = ul_start.saturating_sub(src_start); writeln!(f, "{padding} | {}{}", " ".repeat(offset), ul)?; } else { writeln!(f)?; From 781f672d31241f383d182e8c12417575ba858ee2 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Thu, 12 Mar 2026 14:24:14 -0400 Subject: [PATCH 5/5] update --- tests/specs/ecosystem/mrii/rocket_io/0_1_3.test | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/specs/ecosystem/mrii/rocket_io/0_1_3.test b/tests/specs/ecosystem/mrii/rocket_io/0_1_3.test index 67df08ff5..2dea87264 100644 --- a/tests/specs/ecosystem/mrii/rocket_io/0_1_3.test +++ b/tests/specs/ecosystem/mrii/rocket_io/0_1_3.test @@ -77,10 +77,10 @@ error: Error: [ERR_PACKAGE_PATH_NOT_EXPORTED] Package subpath './build/esm/socke at ext:deno_cli_tsc/97_ts_host.js:749:49 at spanned (ext:deno_cli_tsc/97_ts_host.js:16:12) at Object.host. [as resolveModuleNameLiterals] (ext:deno_cli_tsc/97_ts_host.js:749:14) - at resolveModuleNamesWorker (ext:deno_cli_tsc/00_typescript.js:127677:20) - at resolveNamesReusingOldState (ext:deno_cli_tsc/00_typescript.js:127813:14) - at resolveModuleNamesReusingOldState (ext:deno_cli_tsc/00_typescript.js:127769:12) - at processImportedModules (ext:deno_cli_tsc/00_typescript.js:129227:118) - at findSourceFileWorker (ext:deno_cli_tsc/00_typescript.js:129034:7) - at findSourceFile (ext:deno_cli_tsc/00_typescript.js:128886:20) + at resolveModuleNamesWorker (ext:deno_cli_tsc/00_typescript.js:127688:20) + at resolveNamesReusingOldState (ext:deno_cli_tsc/00_typescript.js:127824:14) + at resolveModuleNamesReusingOldState (ext:deno_cli_tsc/00_typescript.js:127780:12) + at processImportedModules (ext:deno_cli_tsc/00_typescript.js:129238:118) + at findSourceFileWorker (ext:deno_cli_tsc/00_typescript.js:129045:7) + at findSourceFile (ext:deno_cli_tsc/00_typescript.js:128897:20)