feat(parser)!: rename panicked to fatal_error in ParserReturn (#26382)

`ParserReturn` contains a property `panicked`.

The name `panicked` is misleading. The parser did _not_ panic. When `true`, it means that the parser encountered a fatal error and is unable to parse the source, because it contains an unrecoverable syntax error.

Rename this property to `fatal_error`, to better describe what it means.

This is a breaking change, but one that's trivial to fix in downstream code - the property doesn't change its meaning or behavior, just its name.

All the changes in this PR in other crates are just reacting to this change and making everywhere use the term "fatal error" instead of "panicked", so the codebase is consistent.
This commit is contained in:
overlookmotel committed 2026-09-07 09:41:52 +00:00
1 parent 0a81d293f9
commit 66744f0eb9
22 files changed
+78 -69

No files matched your search

+3 -2
View File
@@ -204,11 +204,12 @@ unsafe fn parse_raw_impl(
})
.with_config(RuntimeParserConfig::new(true))
.parse();
let ParserReturn { program: parsed_program, diagnostics, mut tokens, panicked, .. } =
let ParserReturn { program: parsed_program, diagnostics, mut tokens, fatal_error, .. } =
parser_ret;
let program = allocator.alloc(parsed_program);
let mut parsing_failed = panicked || (!diagnostics.is_empty() && !ignore_non_fatal_errors);
let mut parsing_failed =
fatal_error || (!diagnostics.is_empty() && !ignore_non_fatal_errors);
// Check for semantic errors.
// If `ignore_non_fatal_errors` is `true`, skip running semantic, as any errors will be ignored anyway.
+2 -2
View File
@@ -77,7 +77,7 @@ let mut errors = Vec::new();
// Step 1: Parsing
// Parse the TSX file into an AST. The root AST node is a `Program` struct.
let ParserReturn { program, errors: parser_errors, panicked, .. } =
let ParserReturn { program, diagnostics: parser_errors, fatal_error, .. } =
Parser::new(&allocator, source_text, source_type).parse();
errors.extend(parser_errors);
@@ -85,7 +85,7 @@ errors.extend(parser_errors);
// parser could recover from errors, `program` will be a valid AST and
// `errors` will be populated. We can still perform semantic analysis in
// such cases (if we want).
if panicked {
if fatal_error {
for error in &errors {
eprintln!("{error:?}");
panic!("Parsing failed.");
+2 -2
View File
@@ -229,7 +229,7 @@ fn format_program_with_session<'a>(
/// so a program fed to [`format_program`] matches the text-in path.
/// Use this when you need to control parse-error handling
/// or isolate parsing from formatting (perf/allocation measurement, error-tolerant harnesses).
/// Inspect the returned `ParserReturn` (`errors` / `panicked`) and pass `&ret.program` to [`format_program`].
/// Inspect the returned `ParserReturn` (`errors` / `fatal_error`) and pass `&ret.program` to [`format_program`].
pub fn parse_for_format<'a>(
allocator: &'a Allocator,
source_text: &'a str,
@@ -253,7 +253,7 @@ pub fn parse_for_format<'a>(
/// Parse `source_text` and promote the `Program` to the arena lifetime.
///
/// NOTE: Reject ANY parse diagnostic, not only `panicked`: we format valid code only, by design.
/// NOTE: Reject ANY parse diagnostic, not only `fatal_error`: we format valid code only, by design.
/// A recovered AST may be an unfaithful "fix" of the source
/// (e.g. invalid modifiers are reported but not all of them are representable),
/// so formatting it can silently rewrite what the user wrote.
+1 -1
View File
@@ -225,7 +225,7 @@ fn run_formatter(
// This deliberately DIVERGES from production oxfmt,
// whose fail-loud `format()` would report a diagnostic for these fixtures instead of formatting.
let ret = parse_for_format(&allocator, source_text, source_type);
if ret.panicked {
if ret.fatal_error {
return None;
}
+2 -2
View File
@@ -66,7 +66,7 @@ pub fn parse_json<'a>(
// we retry without the wrap and accept the result only when it contains no statements.
// i.e. `source` is comments / whitespace only.
// This lets comment-only JSON files round-trip without changing the normal path's cost.
if !ret.diagnostics.is_empty() || ret.panicked {
if !ret.diagnostics.is_empty() || ret.fatal_error {
if let Some(parsed) = try_parse_comments_only(allocator, source, options) {
validate_comments_for_variant(variant, parsed.comments, false)?;
return Ok(parsed);
@@ -147,7 +147,7 @@ fn try_parse_comments_only<'a>(
let ret =
Parser::new(allocator, bare_source, SourceType::default()).with_options(options).parse();
if !ret.diagnostics.is_empty() || ret.panicked || !ret.program.body.is_empty() {
if !ret.diagnostics.is_empty() || ret.fatal_error || !ret.program.body.is_empty() {
return None;
}
+1 -1
View File
@@ -420,7 +420,7 @@ impl<'a> Fixer<'a> {
})
.parse();
debug_assert!(
parse_result.diagnostics.is_empty() && !parse_result.panicked,
parse_result.diagnostics.is_empty() && !parse_result.fatal_error,
"Linter fixer produced invalid syntax.\n\nInput code: \n```\n{source_text}\n```\n\nFixed code: \n```\n{output}\n```\n\nParse errors: {:?}",
parse_result.diagnostics
);
+1 -1
View File
@@ -256,7 +256,7 @@ mod test {
fn collect(opts: MangleOptionsKeepNames, source_text: &str) -> FxHashSet<String> {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let ret = SemanticBuilder::new().with_build_nodes(true).build(&ret.program);
assert!(ret.diagnostics.is_empty(), "{source_text}");
@@ -7,7 +7,7 @@ use oxc_span::SourceType;
fn test(source_text: &str, expected: bool) {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else {
@@ -68,7 +68,7 @@ fn test_with_ctx_and_functions_option(
) {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else {
@@ -8,7 +8,7 @@ use oxc_span::SourceType;
fn test(source: &str, pure_functions: &[&str], expected: bool) {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source}");
assert!(!ret.fatal_error, "{source}");
assert!(ret.diagnostics.is_empty(), "{source}");
let Some(Statement::ExpressionStatement(stmt)) = ret.program.body.first() else {
@@ -85,7 +85,7 @@ fn test_ts(source_text: &str, expected: bool) {
let ctx = Ctx::default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::tsx()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else {
@@ -118,7 +118,7 @@ fn test_with_target(source_text: &str, target: &str, expected: bool) {
fn test_with_ctx(source_text: &str, ctx: &Ctx, expected: bool) {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else {
@@ -132,7 +132,7 @@ fn test_in_function(source_text: &str, expected: bool) {
let ctx = Ctx::default();
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let Some(Statement::FunctionDeclaration(stmt)) = &ret.program.body.first() else {
@@ -164,7 +164,7 @@ fn test_assign_target_with_global_variables(
};
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else {
@@ -54,7 +54,7 @@ impl MayHaveSideEffectsContext<'_> for Ctx {
fn test(source_text: &str, expected: bool) {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let stmt = ret.program.body.first().unwrap();
@@ -65,7 +65,7 @@ fn test(source_text: &str, expected: bool) {
fn test_in_function(source_text: &str, expected: bool) {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let Some(Statement::FunctionDeclaration(stmt)) = &ret.program.body.first() else {
@@ -32,7 +32,7 @@ fn test_with_global_variables(
) {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let global_reference_checker = GlobalReferenceChecker { global_variable_names };
+1 -1
View File
@@ -154,7 +154,7 @@ fn run_with_iterations(
let ret = Parser::new(&allocator, source_text, source_type)
.with_options(ParseOptions { allow_return_outside_function: true, ..Default::default() })
.parse();
assert!(!ret.panicked, "{source_text}");
assert!(!ret.fatal_error, "{source_text}");
assert!(ret.diagnostics.is_empty(), "{source_text}");
let mut program = ret.program;
let iterations = options.map_or(0, |options| {
+3 -3
View File
@@ -41,12 +41,12 @@ export const Counter: React.FC = () => {
let ParserReturn {
program, // AST
diagnostics, // Syntax errors
panicked, // Parser encountered an error it couldn't recover from
fatal_error, // Parser encountered an error it couldn't recover from
..
} = Parser::new(&allocator, source_text, source_type).parse();
if panicked {
return Err("Parser panicked".to_string());
if fatal_error {
return Err("Parser encountered a fatal error".to_string());
}
if !diagnostics.is_empty() {
+30 -22
View File
@@ -145,23 +145,25 @@ pub(crate) const MAX_LEN: usize = if size_of::<usize>() >= 8 {
/// enabled](https://docs.rs/oxc_semantic/latest/oxc_semantic/struct.SemanticBuilder.html#method.with_check_syntax_error)
///
/// ## Errors
/// Oxc's [`Parser`] is able to recover from some syntax errors and continue parsing. When this
/// happens,
///
/// Oxc's [`Parser`] is able to recover from some syntax errors and continue parsing.
/// When this happens:
///
/// 1. [`diagnostics`] will be non-empty
/// 2. [`program`] will contain a full AST
/// 3. [`panicked`] will be false
/// 3. [`fatal_error`] will be false
///
/// When the parser cannot recover, it will abort and terminate parsing early. [`program`] will
/// be empty and [`panicked`] will be `true`.
/// When the parser cannot recover, it will abort and terminate parsing early.
/// [`program`] will be empty and [`fatal_error`] will be `true`.
///
/// [`program`]: ParserReturn::program
/// [`diagnostics`]: ParserReturn::diagnostics
/// [`panicked`]: ParserReturn::panicked
/// [`fatal_error`]: ParserReturn::fatal_error
#[non_exhaustive]
pub struct ParserReturn<'a> {
/// The parsed AST.
///
/// Will be empty (e.g. no statements, directives, etc) if the parser panicked.
/// Will be empty (e.g. no statements, directives, etc) if the parser encountered a fatal error.
///
/// ## Validity
/// It is possible for the AST to be present and semantically invalid. This will happen if
@@ -190,15 +192,18 @@ pub struct ParserReturn<'a> {
/// Tokens are only collected when tokens are enabled in [`ParserConfig`].
pub tokens: ArenaVec<'a, Token>,
/// Whether the parser panicked and terminated early.
/// Whether the parser encountered a fatal error and terminated early.
///
/// This will be `false` if parsing was successful, or if parsing was able to recover from a
/// syntax error. When `true`, [`program`] will be empty and [`diagnostics`] will contain at least
/// one error.
/// If `false`, either parsing was successful, or parsing was able to recover from a syntax error.
/// [`diagnostics`] may still contain errors, indicating that the program is not syntactically valid,
/// but parser was able to recover enough to produce an AST.
///
/// If `true`, parser was unable to parse the source.
/// [`program`] will be empty and [`diagnostics`] will contain at least one error.
///
/// [`program`]: ParserReturn::program
/// [`diagnostics`]: ParserReturn::diagnostics
pub panicked: bool,
pub fatal_error: bool,
/// Whether the file is [flow](https://flow.org).
pub is_flow_language: bool,
@@ -698,10 +703,10 @@ impl<'a, C: ParserConfig> ParserImpl<'a, C> {
#[inline]
pub fn parse(mut self) -> ParserReturn<'a> {
let mut program = self.parse_program();
let mut panicked = false;
let mut has_fatal_error = false;
if let Some(fatal_error) = self.fatal_error.take() {
panicked = true;
has_fatal_error = true;
self.errors.truncate(fatal_error.errors_len);
if !self.lexer.errors.is_empty() && self.cur_kind().is_eof() {
// Noop
@@ -717,7 +722,7 @@ impl<'a, C: ParserConfig> ParserImpl<'a, C> {
self.check_unfinished_errors();
if let Some(overlong_error) = self.overlong_error() {
panicked = true;
has_fatal_error = true;
self.lexer.errors.clear();
self.errors.clear();
self.error(overlong_error);
@@ -765,8 +770,11 @@ impl<'a, C: ParserConfig> ParserImpl<'a, C> {
}
}
let tokens =
if panicked { ArenaVec::new_in(&self.ast) } else { self.lexer.finalize_tokens() };
let tokens = if has_fatal_error {
ArenaVec::new_in(&self.ast)
} else {
self.lexer.finalize_tokens()
};
program.comments = self.lexer.trivia_builder.comments;
@@ -776,7 +784,7 @@ impl<'a, C: ParserConfig> ParserImpl<'a, C> {
diagnostics: errors,
irregular_whitespaces,
tokens,
panicked,
fatal_error: has_fatal_error,
is_flow_language,
}
}
@@ -1255,7 +1263,7 @@ mod test {
// Parsing should fail
assert!(ret.program.is_empty());
assert!(ret.panicked);
assert!(ret.fatal_error);
assert_eq!(ret.diagnostics.len(), 1);
assert_eq!(
ret.diagnostics.first().unwrap().to_string(),
@@ -1281,7 +1289,7 @@ mod test {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, &source, SourceType::default()).parse();
assert!(!ret.panicked);
assert!(!ret.fatal_error);
assert!(ret.diagnostics.is_empty());
assert_eq!(ret.program.body.len(), 2);
}
@@ -1296,7 +1304,7 @@ mod test {
.with_config(config::TokensParserConfig)
.parse();
assert!(!ret.panicked);
assert!(!ret.fatal_error);
assert_eq!(ret.diagnostics.len(), 1);
assert_eq!(ret.diagnostics.first().unwrap().to_string(), "Unterminated string");
@@ -1326,7 +1334,7 @@ mod test {
.with_config(config::TokensParserConfig)
.parse();
assert!(ret.panicked);
assert!(ret.fatal_error);
assert!(ret.tokens.is_empty());
}
}
+1 -1
View File
@@ -111,7 +111,7 @@ fn codegen_impl(
// Only print fixtures which parse cleanly. Oxc's parser recovers from errors, but a recovered
// AST is not something the JS side can be expected to reproduce.
if ret.panicked || !ret.diagnostics.is_empty() {
if ret.fatal_error || !ret.diagnostics.is_empty() {
return None;
}
+4 -4
View File
@@ -35,7 +35,7 @@ pub struct Driver {
pub check_semantic: bool,
pub allow_return_outside_function: bool,
// results
pub panicked: bool,
pub fatal_error: bool,
pub errors: Diagnostics,
pub printed: String,
pub source_type: Option<SourceType>,
@@ -78,14 +78,14 @@ impl CompilerInterface for Driver {
}
fn after_parse(&mut self, parser_return: &mut ParserReturn) -> ControlFlow<()> {
let ParserReturn { program, panicked, diagnostics, .. } = parser_return;
self.panicked = *panicked;
let ParserReturn { program, fatal_error, diagnostics, .. } = parser_return;
self.fatal_error = *fatal_error;
self.source_type = Some(program.source_type);
self.check_ast_nodes(program);
if self.check_comments(&program.comments) {
return ControlFlow::Break(());
}
if (diagnostics.is_empty() || !*panicked) && program.source_type.is_unambiguous() {
if (diagnostics.is_empty() || !*fatal_error) && program.source_type.is_unambiguous() {
self.errors.push(OxcDiagnostic::error("SourceType must not be unambiguous."));
}
// Make sure serialization doesn't crash; also for code coverage.
+1 -1
View File
@@ -151,7 +151,7 @@ fn parser_stream(
) -> Option<(Vec<Span>, Option<OxcDiagnostic>)> {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, code, source_type).with_config(TokensParserConfig).parse();
if ret.panicked {
if ret.fatal_error {
return None;
}
+4 -4
View File
@@ -122,8 +122,8 @@ impl CoverageResult {
fn parsed(&self) -> bool {
match &self.result {
TestResult::ParseError(_, panicked) | TestResult::CorrectError(_, panicked) => {
!panicked
TestResult::ParseError(_, is_fatal) | TestResult::CorrectError(_, is_fatal) => {
!is_fatal
}
_ => true,
}
@@ -261,8 +261,8 @@ pub fn snapshot_results(name: &str, test_root: &Path, results: &[CoverageResult]
for r in &failed_positives {
let path = normalize_path(Path::new("tasks/coverage").join(&r.path));
match &r.result {
TestResult::ParseError(error, panicked) => {
let label = if *panicked { "Panicked" } else { "Expect to Parse" };
TestResult::ParseError(error, is_fatal) => {
let label = if *is_fatal { "Fatal error" } else { "Expect to Parse" };
writeln!(out, "{label}: {path}").unwrap();
out.push_str(error);
out.push('\n'); // Blank line after error content
+10 -10
View File
@@ -66,13 +66,13 @@ fn run_parser(
.with_source_code(NamedSource::new(path_str.clone(), Arc::clone(&source_arc)));
handler.render_report(&mut output, error.as_ref()).unwrap();
}
TestResult::ParseError(output, driver.panicked)
TestResult::ParseError(output, driver.fatal_error)
}
}
fn evaluate_result(result: TestResult, should_fail: bool) -> TestResult {
match (result, should_fail) {
(TestResult::ParseError(err, panicked), true) => TestResult::CorrectError(err, panicked),
(TestResult::ParseError(err, is_fatal), true) => TestResult::CorrectError(err, is_fatal),
(TestResult::Passed, true) => TestResult::IncorrectlyPassed,
(result, _) => result,
}
@@ -219,7 +219,7 @@ fn run_parser_typescript_unit(
.with_source_code(NamedSource::new(path_str.clone(), Arc::clone(&source_arc)));
handler.render_report(&mut output, error.as_ref()).unwrap();
}
TestResult::ParseError(output, driver.panicked)
TestResult::ParseError(output, driver.fatal_error)
}
pub fn run_parser_typescript(files: &[TypeScriptFile]) -> Vec<CoverageResult> {
@@ -771,7 +771,7 @@ fn run_estree_test262_impl(
.with_config(parser_config)
.parse();
if ret.panicked || !ret.diagnostics.is_empty() {
if ret.fatal_error || !ret.diagnostics.is_empty() {
let error = ret
.diagnostics
.first()
@@ -779,7 +779,7 @@ fn run_estree_test262_impl(
return CoverageResult {
path: test_file.path.clone(),
should_fail: false,
result: TestResult::ParseError(error, ret.panicked),
result: TestResult::ParseError(error, ret.fatal_error),
};
}
@@ -845,15 +845,15 @@ fn run_estree_acorn_jsx_impl(
.with_config(parser_config)
.parse();
if ret.panicked || !ret.diagnostics.is_empty() {
if ret.fatal_error || !ret.diagnostics.is_empty() {
let error = ret
.diagnostics
.first()
.map_or_else(|| "Panicked".to_string(), ToString::to_string);
let result = if test_file.should_fail {
TestResult::CorrectError(error, ret.panicked)
TestResult::CorrectError(error, ret.fatal_error)
} else {
TestResult::ParseError(error, ret.panicked)
TestResult::ParseError(error, ret.fatal_error)
};
return CoverageResult {
path: test_file.path.clone(),
@@ -1005,7 +1005,7 @@ fn run_estree_typescript_impl(
.with_config(parser_config)
.parse();
if ret.panicked || !ret.diagnostics.is_empty() {
if ret.fatal_error || !ret.diagnostics.is_empty() {
let error = ret
.diagnostics
.first()
@@ -1013,7 +1013,7 @@ fn run_estree_typescript_impl(
return CoverageResult {
path: test_file.path.clone(),
should_fail: false,
result: TestResult::ParseError(error, ret.panicked),
result: TestResult::ParseError(error, ret.fatal_error),
};
}
@@ -217,7 +217,7 @@ fn generate_for_file(f: &TypeScriptFile, header: &str, kind: BaselineKind) -> St
let ret = Parser::new(&allocator, &unit.content, unit.source_type)
.with_config(TokensParserConfig)
.parse();
if ret.panicked {
if ret.fatal_error {
Vec::new()
} else {
let token_ends: Vec<u32> =
@@ -241,7 +241,7 @@ fn generate_for_file(f: &TypeScriptFile, header: &str, kind: BaselineKind) -> St
}
BaselineKind::Types => {
let ret = Parser::new(&allocator, &unit.content, unit.source_type).parse();
if ret.panicked {
if ret.fatal_error {
Vec::new()
} else {
let mut walker = SymbolWalker {