diff --git a/js/test.ts b/js/test.ts index ad75f3fc..6bc6ab05 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 6c957fb5..5cdb442a 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -562,14 +562,132 @@ 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 +/// 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 |\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 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..]; + 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}")?; + + 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: align the underline under the source + 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); + let offset = ul_start.saturating_sub(src_start); + writeln!(f, "{padding} | {}{}", " ".repeat(offset), ul)?; + } else { + writeln!(f)?; + writeln!(f, " {src}")?; + writeln!(f, " {ul}")?; + } + } + } + + write!(f, " at {location}")?; + + 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/ecosystem/mrii/rocket_io/0_1_3.test b/tests/specs/ecosystem/mrii/rocket_io/0_1_3.test index 67df08ff..2dea8726 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) diff --git a/tests/specs/graph/cjs/file_export.txt b/tests/specs/graph/cjs/file_export.txt index 39fef25f..1e1407fb 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 |\n1 | export class Test {}\n | ~~~~~~\n at file:///file.cjs:1:1" }, { "kind": "esm",