Skip to content

fix(parser): preserve fatal error across top-level await reparse#23677

Open
ammubhave wants to merge 1 commit into
oxc-project:mainfrom
ammubhave:fix-23673
Open

fix(parser): preserve fatal error across top-level await reparse#23677
ammubhave wants to merge 1 commit into
oxc-project:mainfrom
ammubhave:fix-23673

Conversation

@ammubhave

Copy link
Copy Markdown
Contributor

Summary

Fixes #23673. oxfmt panicked with string must start with a quote (oxc_formatter/src/utils/string.rs) on input like await(E);import. The root cause is in the parser: a fatal parse error was silently discarded, so a file that should be rejected was reported as parsed successfully with a malformed AST.

Problem

await(E);import is invalid — the trailing import is incomplete, which is a fatal error. On its own (import, or x;import) the parser reports it correctly. But await(E) is a top-level await: in unambiguous mode the parser first parses await as an identifier, records a checkpoint, and once the import proves the file is a module, reparses that statement with await enabled (reparse_potential_top_level_awaits).

That reparse calls rewind(checkpoint), and rewind restores all saved state — including self.fatal_error. The checkpoint was taken before the import, so rewinding reset fatal_error back to None, erasing it. The parser then returned no diagnostics and a malformed ImportDeclaration whose source is a synthetic StringLiteral { span: 0..0, value: "", raw: None }. Because oxc_formatter::format only runs when parsing reports no diagnostics, this malformed program reached the formatter, which sliced the empty source text and tripped the assertion (and would index-underflow in release).

Fix

Stash self.fatal_error before the reparse loop and restore it afterward. Reparsing only re-derives the AST for already-valid await statements, so a fatal error from a later statement is unrelated to them and must outlive the reparse. A fatal error newly surfaced by reparsing comes earlier in source order and takes priority; otherwise the first-pass error is restored.

This cannot cause false positives: the reparse only triggers after an import/export parsed successfully (parsing never stopped earlier), so any surviving fatal_error necessarily came from a statement after the reparsed awaits — which the reparse cannot fix anyway. It also fixes the class of bug generally, not just the empty-import-source symptom.

Example

await(E);import

Before: parsed "successfully" → oxfmt panics. After: Expected frombut foundEOF``, and valid files like await(E); import x from "y"; still format normally.

Testing

  • Added tasks/coverage/misc/fail/oxc-23673.js (negative parser fixture).
  • cargo coverage -- parser: 100% AST-parsed, no regressions; the only snapshot change is the new fixture counted as a correctly-failing test (Negative Passed: 149 → 150).
  • cargo test -p oxc_parser, clippy, and rustfmt pass.

AI usage disclosure: this change was investigated and drafted with the help of Claude Code, then reviewed and tested by the contributor.

🤖 Generated with Claude Code

@ammubhave ammubhave marked this pull request as ready for review June 21, 2026 04:18
@codspeed-hq

codspeed-hq Bot commented Jun 21, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 62 untouched benchmarks
⏩ 9 skipped benchmarks1


Comparing ammubhave:fix-23673 (104abe6) with main (36009dd)

Open in CodSpeed

Footnotes

  1. 9 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Sysix

Sysix commented Jun 21, 2026

Copy link
Copy Markdown
Member

this will fix #23681 too

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.

Panic string must start with a quote in src/utils/string.rs

2 participants