Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@
//! : ^^
//! 6 | }
//! `----
//! x Expected ';', '}' or <eof>
//! ,-[5:1]
//! 2 | // https://github.com/microsoft/TypeScript/issues/55555
//! 3 |
//! 4 | async function test() {
//! 5 | for await (await using of of of) {};
//! : ^|^
//! : `-- This is the expression part of an expression statement
//! 6 | }
//! `----
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@
//! : ^^
//! 6 | }
//! `----
//! x Expected ';', '}' or <eof>
//! ,-[5:1]
//! 2 | // https://github.com/microsoft/TypeScript/issues/55555
//! 3 |
//! 4 | async function test() {
//! 5 | for await (await using of of of) {};
//! : ^|^
//! : `-- This is the expression part of an expression statement
//! 6 | }
//! `----
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@
//! : ^^
//! 6 | }
//! `----
//! x Expected ';', '}' or <eof>
//! ,-[5:1]
//! 2 | // https://github.com/microsoft/TypeScript/issues/55555
//! 3 |
//! 4 | {
//! 5 | for (await using of of of) {};
//! : ^|^
//! : `-- This is the expression part of an expression statement
//! 6 | }
//! `----
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@
//! : ^^
//! 6 | }
//! `----
//! x Expected ';', '}' or <eof>
//! ,-[5:1]
//! 2 | // https://github.com/microsoft/TypeScript/issues/55555
//! 3 |
//! 4 | {
//! 5 | for (await using of of of) {};
//! : ^|^
//! : `-- This is the expression part of an expression statement
//! 6 | }
//! `----
54 changes: 50 additions & 4 deletions crates/swc_ecma_parser/src/parser/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,15 @@ impl<I: Tokens> Parser<I> {
let mut elems = Vec::with_capacity(8);

while !self.input().is(Token::RBracket) {
// Recovery: check for EOF to prevent infinite loop
if self.input().is(Token::Eof) {
self.emit_err(
self.input().cur_span(),
SyntaxError::Expected("]".into(), "eof".into()),
);
break;
}

if self.input().is(Token::Comma) {
expect!(self, Token::Comma);
elems.push(None);
Expand All @@ -500,15 +509,31 @@ impl<I: Tokens> Parser<I> {
elems.push(self.allow_in_expr(|p| p.parse_expr_or_spread()).map(Some)?);

if !self.input().is(Token::RBracket) {
expect!(self, Token::Comma);
// Recovery: if not a comma, emit error but continue
if !self.input_mut().eat(Token::Comma) {
// If EOF, break out to avoid infinite loop
if self.input().is(Token::Eof) {
self.emit_err(
self.input().cur_span(),
SyntaxError::Expected("]".into(), "eof".into()),
);
break;
}
// Emit error for missing comma but continue parsing
let span = self.input().cur_span();
let cur = self.input_mut().dump_cur();
self.emit_err(span, SyntaxError::Expected(",".into(), cur));
Comment on lines +522 to +525
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After emitting an error for a missing comma, the parser continues without consuming the unexpected token. This could potentially lead to issues if parse_expr_or_spread on the next iteration doesn't consume the token or make progress. While the tests may pass for common cases, consider adding explicit token consumption after the error to make the recovery more robust and prevent potential infinite loops in edge cases. For example, you could add a check to consume the unexpected token before continuing the loop.

Suggested change
// Emit error for missing comma but continue parsing
let span = self.input().cur_span();
let cur = self.input_mut().dump_cur();
self.emit_err(span, SyntaxError::Expected(",".into(), cur));
// Emit error for missing comma but continue parsing.
// Also consume the unexpected token to ensure forward progress.
let span = self.input().cur_span();
let cur = self.input_mut().dump_cur();
self.emit_err(span, SyntaxError::Expected(",".into(), cur));
// Consume the unexpected token to avoid potential infinite loops
self.input_mut().bump();

Copilot uses AI. Check for mistakes.
}

if self.input().is(Token::RBracket) {
let prev_span = self.input().prev_span();
self.state_mut().trailing_commas.insert(start, prev_span);
}
}
}

expect!(self, Token::RBracket);
// Recovery: use expect_or_recover to allow continuing even without ]
expect_or_recover!(self, Token::RBracket);

let span = self.span(start);
Ok(ArrayLit { span, elems }.into())
Expand Down Expand Up @@ -919,10 +944,30 @@ impl<I: Tokens> Parser<I> {
let mut expr_or_spreads = Vec::with_capacity(2);

while !p.input().is(Token::RParen) {
// Recovery: check for EOF to prevent infinite loop
if p.input().is(Token::Eof) {
p.emit_err(
p.input().cur_span(),
SyntaxError::Expected(")".into(), "eof".into()),
);
break;
}

if first {
first = false;
} else {
expect!(p, Token::Comma);
// Recovery: if not a comma, emit error but continue
if !p.input_mut().eat(Token::Comma) {
// Check if we're at a closing paren or EOF
if p.input().is(Token::RParen) || p.input().is(Token::Eof) {
break;
}
// Emit error for missing comma
let span = p.input().cur_span();
let cur = p.input_mut().dump_cur();
p.emit_err(span, SyntaxError::Expected(",".into(), cur));
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After emitting an error for a missing comma, the parser continues without consuming the unexpected token. This could potentially lead to issues if parse_expr_or_spread on the next iteration doesn't consume the token or make progress. While the tests may pass for common cases, consider adding explicit token consumption after the error to make the recovery more robust and prevent potential infinite loops in edge cases. For example, you could add a check to consume the unexpected token before continuing the loop.

Suggested change
p.emit_err(span, SyntaxError::Expected(",".into(), cur));
p.emit_err(span, SyntaxError::Expected(",".into(), cur));
// Recovery: consume the unexpected token to ensure progress
p.input_mut().bump();
continue;

Copilot uses AI. Check for mistakes.
}

// Handle trailing comma.
if p.input().is(Token::RParen) {
if is_dynamic_import && !p.input().syntax().import_attributes() {
Expand All @@ -936,7 +981,8 @@ impl<I: Tokens> Parser<I> {
expr_or_spreads.push(p.allow_in_expr(|p| p.parse_expr_or_spread())?);
}

expect!(p, Token::RParen);
// Recovery: use expect_or_recover for closing paren
expect_or_recover!(p, Token::RParen);
Ok(expr_or_spreads)
})
}
Expand Down
19 changes: 19 additions & 0 deletions crates/swc_ecma_parser/src/parser/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ macro_rules! expect {
}};
}

/// Like `expect!`, but recovers from the error by emitting it and continuing.
/// Returns `true` if the token was found and consumed, `false` if it was
/// missing.
macro_rules! expect_or_recover {
($p:expr, $t:expr) => {{
if $p.input_mut().eat($t) {
true
} else {
let span = $p.input().cur_span();
let cur = $p.input_mut().dump_cur();
$p.emit_err(
span,
$crate::error::SyntaxError::Expected(format!("{:?}", $t), cur),
);
false
}
}};
}

macro_rules! unexpected {
($p:expr, $expected:literal) => {{
let got = $p.input_mut().dump_cur();
Expand Down
22 changes: 21 additions & 1 deletion crates/swc_ecma_parser/src/parser/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,30 @@ impl<I: Tokens> Parser<I> {
let mut props = Vec::with_capacity(8);

while !p.input_mut().eat(Token::RBrace) {
// Recovery: check for EOF to prevent infinite loop
if p.input().is(Token::Eof) {
p.emit_err(
p.input().cur_span(),
SyntaxError::Expected("}".into(), "eof".into()),
);
break;
}

props.push(parse_prop(p)?);

if !p.input().is(Token::RBrace) {
expect!(p, Token::Comma);
// Recovery: if not a comma, emit error but continue
if !p.input_mut().eat(Token::Comma) {
// Check if we're at a closing brace or EOF
if p.input().is(Token::RBrace) || p.input().is(Token::Eof) {
continue;
}
// Emit error for missing comma
let span = p.input().cur_span();
let cur = p.input_mut().dump_cur();
p.emit_err(span, SyntaxError::Expected(",".into(), cur));
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After emitting an error for a missing comma, the parser continues without consuming the unexpected token. This could potentially lead to issues if parse_prop on the next iteration doesn't consume the token or make progress. While the tests may pass for common cases, consider adding explicit token consumption after the error to make the recovery more robust and prevent potential infinite loops in edge cases. For example, you could add a check to consume the unexpected token before continuing the loop.

Suggested change
p.emit_err(span, SyntaxError::Expected(",".into(), cur));
p.emit_err(span, SyntaxError::Expected(",".into(), cur));
// Consume the unexpected token to ensure forward progress
p.input_mut().bump();

Copilot uses AI. Check for mistakes.
}

if p.input().is(Token::RBrace) {
trailing_comma = Some(p.input().prev_span());
}
Expand Down
44 changes: 28 additions & 16 deletions crates/swc_ecma_parser/src/parser/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,8 @@ impl<I: Tokens> Parser<I> {
} else {
None
};
expect!(self, Token::LParen);
// Recovery: use expect_or_recover for opening paren
expect_or_recover!(self, Token::LParen);

let head = self.do_inside_of_context(Context::ForLoopInit, |p| {
if await_token.is_some() {
Expand All @@ -499,7 +500,8 @@ impl<I: Tokens> Parser<I> {
}
})?;

expect!(self, Token::RParen);
// Recovery: use expect_or_recover for closing paren
expect_or_recover!(self, Token::RParen);

let body = self
.do_inside_of_context(
Expand Down Expand Up @@ -574,7 +576,8 @@ impl<I: Tokens> Parser<I> {
self.assert_and_bump(Token::If);
let if_token = self.input().prev_span();

expect!(self, Token::LParen);
// Recovery: use expect_or_recover for opening paren
expect_or_recover!(self, Token::LParen);

let test = self
.do_outside_of_context(Context::IgnoreElseClause, |p| {
Expand All @@ -591,7 +594,8 @@ impl<I: Tokens> Parser<I> {
)
})?;

expect!(self, Token::RParen);
// Recovery: use expect_or_recover for closing paren
expect_or_recover!(self, Token::RParen);

let cons = {
// Prevent stack overflow
Expand Down Expand Up @@ -717,9 +721,10 @@ impl<I: Tokens> Parser<I> {

self.assert_and_bump(Token::While);

expect!(self, Token::LParen);
// Recovery: use expect_or_recover for parens
expect_or_recover!(self, Token::LParen);
let test = self.allow_in_expr(|p| p.parse_expr())?;
expect!(self, Token::RParen);
expect_or_recover!(self, Token::RParen);

let body = self
.do_inside_of_context(
Expand Down Expand Up @@ -779,12 +784,13 @@ impl<I: Tokens> Parser<I> {
)
.map(Box::new)?;

expect!(self, Token::While);
expect!(self, Token::LParen);
// Recovery: use expect_or_recover for while and parens
expect_or_recover!(self, Token::While);
expect_or_recover!(self, Token::LParen);

let test = self.allow_in_expr(|p| p.parse_expr())?;

expect!(self, Token::RParen);
expect_or_recover!(self, Token::RParen);

// We *may* eat semicolon.
let _ = self.eat_general_semi();
Expand Down Expand Up @@ -920,14 +926,16 @@ impl<I: Tokens> Parser<I> {

self.assert_and_bump(Token::Switch);

expect!(self, Token::LParen);
// Recovery: use expect_or_recover for parens
expect_or_recover!(self, Token::LParen);
let discriminant = self.allow_in_expr(|p| p.parse_expr())?;
expect!(self, Token::RParen);
expect_or_recover!(self, Token::RParen);

let mut cases = Vec::new();
let mut span_of_previous_default = None;

expect!(self, Token::LBrace);
// Recovery: use expect_or_recover for opening brace
expect_or_recover!(self, Token::LBrace);

self.do_inside_of_context(Context::IsBreakAllowed, |p| {
while {
Expand All @@ -948,11 +956,15 @@ impl<I: Tokens> Parser<I> {

None
};
expect!(p, Token::Colon);
// Recovery: use expect_or_recover for colon after case/default
expect_or_recover!(p, Token::Colon);

while {
let cur = p.input().cur();
!(cur == Token::Case || cur == Token::Default || cur == Token::RBrace)
!(cur == Token::Case
|| cur == Token::Default
|| cur == Token::RBrace
|| cur == Token::Eof)
} {
cons.push(
p.do_outside_of_context(Context::TopLevel, Self::parse_stmt_list_item)?,
Expand All @@ -969,8 +981,8 @@ impl<I: Tokens> Parser<I> {
Ok(())
})?;

// eof or rbrace
expect!(self, Token::RBrace);
// eof or rbrace - Recovery: use expect_or_recover
expect_or_recover!(self, Token::RBrace);

Ok(SwitchStmt {
span: self.span(switch_start),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let arr = [1, 2, 3
let x = 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
x Expression expected
,-[$DIR/tests/errors/missing_array_bracket/input.js:2:1]
1 | let arr = [1, 2, 3
2 | let x = 1;
: ^
`----
x Expected ',', got 'let'
,-[$DIR/tests/errors/missing_array_bracket/input.js:2:1]
1 | let arr = [1, 2, 3
2 | let x = 1;
: ^^^
`----
x `let` cannot be used as an identifier in strict mode
,-[$DIR/tests/errors/missing_array_bracket/input.js:2:1]
1 | let arr = [1, 2, 3
2 | let x = 1;
: ^^^
`----
x Expected ',', got 'ident'
,-[$DIR/tests/errors/missing_array_bracket/input.js:2:1]
1 | let arr = [1, 2, 3
2 | let x = 1;
: ^
`----
x Expected ',', got ';'
,-[$DIR/tests/errors/missing_array_bracket/input.js:2:1]
1 | let arr = [1, 2, 3
2 | let x = 1;
: ^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let arr = [1 2 3];
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
x Expected ',', got 'numeric literal'
,-[$DIR/tests/errors/missing_comma_array/input.js:1:1]
1 | let arr = [1 2 3];
: ^
`----
x Expected ',', got 'numeric literal'
,-[$DIR/tests/errors/missing_comma_array/input.js:1:1]
1 | let arr = [1 2 3];
: ^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let obj = {a: 1 b: 2};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
x Expected ',', got 'ident'
,-[$DIR/tests/errors/missing_comma_object/input.js:1:1]
1 | let obj = {a: 1 b: 2};
: ^
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
for let i = 0; i < 10; i++ {
console.log(i);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
x Expected '(', got 'let'
,-[$DIR/tests/errors/missing_paren_for/input.js:1:1]
1 | for let i = 0; i < 10; i++ {
: ^^^
2 | console.log(i);
`----
x Expected ')', got '{'
,-[$DIR/tests/errors/missing_paren_for/input.js:1:1]
1 | for let i = 0; i < 10; i++ {
: ^
2 | console.log(i);
`----
Loading
Loading