Visitar URL original
fix(es/parser): Retry ambiguous Program parsing (#12142) · swc-project/swc@141a320 · GitHub
Skip to content

Commit 141a320

Browse files
authored
fix(es/parser): Retry ambiguous Program parsing (#12142)
**Description:** `parse_program` could classify a source as a Module too early when a top-level `await` token also had a valid Script interpretation. For example, in the following code, `await` should be parsed as an identifier and the second import argument should remain a normal call expression: ```js function await() {} import("./fixture.js", await(undefined)); ``` This PR updates unambiguous Program parsing to: - initially parse using the Module `Await` grammar; - retry as Script when `await` has a distinct valid Script interpretation and no definitive module syntax was found; - retain the Module result when imports, exports, or unambiguous module-only syntax determine the parse goal; - restore lexer diagnostics and token state when retrying; - defer publishing comments until a parse result is selected, preventing duplicated comments; - preserve the grammar boundaries of functions, parameters, class fields, and static blocks. The regular lexer/parser checkpoint remains unchanged. Non-ambiguous valid programs still use a single parsing pass; reparsing is limited to ambiguous or failed probes. Regression coverage includes: - ambiguous `await` unary, binary, call, member, assignment, arrow, and `for...of` forms; - TypeScript `as` and `satisfies` expressions; - nested function and class-field grammar contexts; - comment preservation across retries; - parser fixtures and resolver output for the dynamic-import case. **Related issue (if exists):** N/A
1 parent ed74223 commit 141a320

24 files changed

Lines changed: 855 additions & 73 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
swc_core: patch
3+
swc_ecma_parser: patch
4+
swc_ecma_transforms_base: patch
5+
---
6+
7+
fix(es/parser): Parse ambiguous top-level `await` expressions using the valid Program goal
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
await (()=>{
1+
await(()=>{
22
console.log("non-IIFE");
3-
}), await void console.log("IIFE");
3+
}), await(()=>{
4+
console.log("IIFE");
5+
})();
Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1 @@
1-
var async = (x)=>{
2-
console.log("async", x);
3-
};
4-
var await = (x)=>{
5-
console.log("await", x);
6-
};
7-
async(1);
8-
await 2;
1+
var async=x=>{console.log("async",x)};var await=x=>{console.log("await",x)};async(1);await(2);
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
var o = function(o) {
22
console.log("async", o);
33
};
4-
var a = function(o) {
4+
var n = function(o) {
55
console.log("await", o);
66
};
77
o(1);
8-
await 2;
8+
n(2);
Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1 @@
1-
var async = function(x) {
2-
console.log("async", x);
3-
};
4-
var await = function(x) {
5-
console.log("await", x);
6-
};
7-
async(1);
8-
await 2;
1+
var async=function(x){console.log("async",x)};var await=function(x){console.log("await",x)};async(1);await(2);
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
var o = function(o) {
22
console.log("async", o);
33
};
4-
var a = function(o) {
4+
var n = function(o) {
55
console.log("await", o);
66
};
77
o(1);
8-
await (2);
8+
n(2);

‎crates/swc_ecma_parser/src/lexer/capturing.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,30 @@ impl<I: Tokens> Tokens for Capturing<I> {
133133
self.inner.take_errors()
134134
}
135135

136+
fn has_errors(&self) -> bool {
137+
self.inner.has_errors()
138+
}
139+
140+
fn diagnostic_checkpoint_save(&self) -> (usize, usize) {
141+
self.inner.diagnostic_checkpoint_save()
142+
}
143+
144+
fn diagnostic_checkpoint_load(&mut self, checkpoint: (usize, usize)) {
145+
self.inner.diagnostic_checkpoint_load(checkpoint);
146+
}
147+
136148
fn take_script_module_errors(&mut self) -> Vec<Error> {
137149
self.inner.take_script_module_errors()
138150
}
139151

152+
fn set_defer_comments(&mut self, defer: bool) {
153+
self.inner.set_defer_comments(defer);
154+
}
155+
156+
fn finalize_comments(&mut self) {
157+
self.inner.finalize_comments();
158+
}
159+
140160
fn update_token_flags(&mut self, f: impl FnOnce(&mut TokenFlags)) {
141161
self.inner.update_token_flags(f);
142162
}

‎crates/swc_ecma_parser/src/lexer/mod.rs‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ pub struct Lexer<'a> {
208208
comments: Option<&'a dyn Comments>,
209209
/// [Some] if comment comment parsing is enabled. Otherwise [None]
210210
comments_buffer: Option<CommentsBuffer>,
211+
/// Whether comments should remain buffered while the parser may rewind.
212+
defer_comments: bool,
211213

212214
pub ctx: Context,
213215
input: StringInput<'a>,
@@ -312,6 +314,7 @@ impl<'a> Lexer<'a> {
312314
Lexer {
313315
comments,
314316
comments_buffer: comments.is_some().then(CommentsBuffer::new),
317+
defer_comments: false,
315318
ctx: Default::default(),
316319
input,
317320
start_pos,
@@ -1337,6 +1340,7 @@ impl<'a> Lexer<'a> {
13371340
if let Some(comments) = self.comments() {
13381341
let last = self.state.prev_hi;
13391342
let start_pos = self.start_pos();
1343+
let defer_comments = self.defer_comments;
13401344
let comments_buffer = self.comments_buffer_mut().unwrap();
13411345

13421346
// if the file had no tokens and no shebang, then treat any
@@ -1350,6 +1354,10 @@ impl<'a> Lexer<'a> {
13501354
// move the pending to the leading or trailing
13511355
comments_buffer.pending_to_comment(kind, last);
13521356

1357+
if defer_comments {
1358+
return;
1359+
}
1360+
13531361
// now fill the user's passed in comments
13541362
for comment in comments_buffer.take_comments() {
13551363
match comment.kind {
@@ -1364,6 +1372,28 @@ impl<'a> Lexer<'a> {
13641372
}
13651373
}
13661374

1375+
fn finalize_comments(&mut self) {
1376+
self.defer_comments = false;
1377+
1378+
let Some(comments) = self.comments() else {
1379+
return;
1380+
};
1381+
let Some(comments_buffer) = self.comments_buffer.as_mut() else {
1382+
return;
1383+
};
1384+
1385+
for comment in comments_buffer.take_comments() {
1386+
match comment.kind {
1387+
BufferedCommentKind::Leading => {
1388+
comments.add_leading(comment.pos, comment.comment);
1389+
}
1390+
BufferedCommentKind::Trailing => {
1391+
comments.add_trailing(comment.pos, comment.comment);
1392+
}
1393+
}
1394+
}
1395+
}
1396+
13671397
fn read_jsx_entity(&mut self) -> LexResult<(Wtf8Atom, String)> {
13681398
debug_assert!(self.syntax().jsx());
13691399

‎crates/swc_ecma_parser/src/lexer/state.rs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,37 @@ impl crate::input::Tokens for Lexer<'_> {
144144
errors
145145
}
146146

147+
#[inline]
148+
fn has_errors(&self) -> bool {
149+
!self.errors.is_empty()
150+
}
151+
152+
#[inline]
153+
fn diagnostic_checkpoint_save(&self) -> (usize, usize) {
154+
(self.errors.len(), self.module_errors.len())
155+
}
156+
157+
#[inline]
158+
fn diagnostic_checkpoint_load(&mut self, checkpoint: (usize, usize)) {
159+
self.errors.truncate(checkpoint.0);
160+
self.module_errors.truncate(checkpoint.1);
161+
}
162+
147163
#[inline]
148164
fn take_script_module_errors(&mut self) -> Vec<Error> {
149165
take(&mut self.module_errors)
150166
}
151167

168+
#[inline]
169+
fn set_defer_comments(&mut self, defer: bool) {
170+
self.defer_comments = defer;
171+
}
172+
173+
#[inline]
174+
fn finalize_comments(&mut self) {
175+
Lexer::finalize_comments(self);
176+
}
177+
152178
#[inline]
153179
fn end_pos(&self) -> BytePos {
154180
self.input.end_pos()

‎crates/swc_ecma_parser/src/parser/class_and_fn.rs‎

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -375,13 +375,15 @@ impl<I: Tokens> Parser<I> {
375375
}
376376
};
377377

378-
let (this_param, params) = p.do_inside_of_context(Context::InParameters, |p| {
379-
p.do_outside_of_context(Context::InFunction, |p| {
380-
if is_async {
381-
p.do_inside_of_context(Context::InAsync, parse_args_with_generator_ctx)
382-
} else {
383-
p.do_outside_of_context(Context::InAsync, parse_args_with_generator_ctx)
384-
}
378+
let (this_param, params) = p.without_async_arrow_param_await_collection(|p| {
379+
p.do_inside_of_context(Context::InParameters, |p| {
380+
p.do_outside_of_context(Context::InFunction, |p| {
381+
if is_async {
382+
p.do_inside_of_context(Context::InAsync, parse_args_with_generator_ctx)
383+
} else {
384+
p.do_outside_of_context(Context::InAsync, parse_args_with_generator_ctx)
385+
}
386+
})
385387
})
386388
})?;
387389

@@ -888,12 +890,18 @@ impl<I: Tokens> Parser<I> {
888890
let type_ann = self.try_parse_ts_type_ann()?;
889891

890892
self.do_inside_of_context(Context::IncludeInExpr.union(Context::InClassField), |p| {
891-
let value = if p.input().is(Token::Eq) {
892-
p.assert_and_bump(Token::Eq);
893-
Some(p.parse_assignment_expr()?)
894-
} else {
895-
None
896-
};
893+
// Class field initializers have their own Await grammar parameter and must
894+
// not inherit the unambiguous Program probe's top-level async context.
895+
let value = p.without_async_arrow_param_await_collection(|p| {
896+
p.do_outside_of_context(Context::InAsync, |p| {
897+
if p.input().is(Token::Eq) {
898+
p.assert_and_bump(Token::Eq);
899+
p.parse_assignment_expr().map(Some)
900+
} else {
901+
Ok(None)
902+
}
903+
})
904+
})?;
897905

898906
if declare && value.is_some() {
899907
p.emit_err(p.span(start), SyntaxError::TS1183);

0 commit comments

Comments
 (0)