From bf0cc9bb3a60138e53d17cf6a96fcb6cc214bd2f Mon Sep 17 00:00:00 2001 From: aoright <102943475+aoright@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:17:51 +0800 Subject: [PATCH 1/2] fix(parser): support HTML close comment on first line --- core/parser/src/parser/cursor/buffered_lexer/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/parser/src/parser/cursor/buffered_lexer/mod.rs b/core/parser/src/parser/cursor/buffered_lexer/mod.rs index 67ba7aee596..3f1f13bd18a 100644 --- a/core/parser/src/parser/cursor/buffered_lexer/mod.rs +++ b/core/parser/src/parser/cursor/buffered_lexer/mod.rs @@ -134,9 +134,11 @@ where let previous_index = self.write_index.checked_sub(1).unwrap_or(PEEK_BUF_SIZE - 1); - if let Some(ref token) = self.peeked[previous_index] - && token.kind() == &TokenKind::LineTerminator - { + let previous_token = self.peeked[previous_index].as_ref(); + let is_line_term = previous_token.map_or(false, |token| token.kind() == &TokenKind::LineTerminator); + let is_start = previous_token.is_none(); + + if is_line_term || is_start { // We don't want to have multiple contiguous line terminators in the buffer, since // they have no meaning. let next = loop { From c1b7013d7f0607a966ab7de09bfa40fe4e3756a3 Mon Sep 17 00:00:00 2001 From: aoright <102943475+aoright@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:44:28 +0800 Subject: [PATCH 2/2] fix(parser): preserve line terminators during ASI while supporting first-line HTML close comments Fixes a parser regression where checking previous_token.is_none() in BufferedLexer::fill() evaluated to true whenever the peeked ring buffer cleared, causing line terminators in normal statement positions to be skipped and breaking Automatic Semicolon Insertion (ASI). Replace buffer exhaustion check with an explicit start_of_file state so that only genuine start of input handles Annex B HTML close comments, while preserving line terminators for statement parsing. Signed-off-by: aoright <102943475+aoright@users.noreply.github.com> --- .../src/parser/cursor/buffered_lexer/mod.rs | 27 ++++++++-- .../src/parser/cursor/buffered_lexer/tests.rs | 54 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/core/parser/src/parser/cursor/buffered_lexer/mod.rs b/core/parser/src/parser/cursor/buffered_lexer/mod.rs index 3f1f13bd18a..f932adc3fa6 100644 --- a/core/parser/src/parser/cursor/buffered_lexer/mod.rs +++ b/core/parser/src/parser/cursor/buffered_lexer/mod.rs @@ -30,6 +30,7 @@ pub(super) struct BufferedLexer { read_index: usize, write_index: usize, last_linear_pos: LinearPosition, + start_of_file: bool, } impl From> for BufferedLexer @@ -53,6 +54,7 @@ where read_index: 0, write_index: 0, last_linear_pos: LinearPosition::default(), + start_of_file: true, } } } @@ -135,10 +137,11 @@ where let previous_index = self.write_index.checked_sub(1).unwrap_or(PEEK_BUF_SIZE - 1); let previous_token = self.peeked[previous_index].as_ref(); - let is_line_term = previous_token.map_or(false, |token| token.kind() == &TokenKind::LineTerminator); - let is_start = previous_token.is_none(); + let is_line_term = + previous_token.is_some_and(|token| token.kind() == &TokenKind::LineTerminator); - if is_line_term || is_start { + if is_line_term { + self.start_of_file = false; // We don't want to have multiple contiguous line terminators in the buffer, since // they have no meaning. let next = loop { @@ -155,6 +158,24 @@ where } }; + self.peeked[self.write_index] = next; + } else if self.start_of_file { + self.start_of_file = false; + // At the start of the file, HTML close comments (`-->`) are allowed by Annex B. + // Whitespace and single-line block comments may precede them. + let next = loop { + self.lexer.skip_html_close(interner)?; + let next = self.lexer.next_no_skip(interner)?; + if let Some(ref token) = next { + match token.kind() { + TokenKind::Comment => self.lexer.skip_html_close(interner)?, + _ => break next, + } + } else { + break None; + } + }; + self.peeked[self.write_index] = next; } else { self.peeked[self.write_index] = self.lexer.next(interner)?; diff --git a/core/parser/src/parser/cursor/buffered_lexer/tests.rs b/core/parser/src/parser/cursor/buffered_lexer/tests.rs index 7dc8c58ef5b..e803e06a1b2 100644 --- a/core/parser/src/parser/cursor/buffered_lexer/tests.rs +++ b/core/parser/src/parser/cursor/buffered_lexer/tests.rs @@ -287,3 +287,57 @@ fn issue_1768() { assert!(cur.peek(3, true, interner).unwrap().is_none()); } + +#[test] +#[cfg(feature = "annex-b")] +fn html_close_comment_first_line() { + let mut cur = BufferedLexer::from(&b"--> comment\nx"[..]); + let interner = &mut Interner::default(); + + assert_eq!( + *cur.peek(0, true, interner) + .unwrap() + .expect("Token expected") + .kind(), + TokenKind::identifier(interner.get_or_intern_static("x", utf16!("x"))) + ); +} + +#[test] +#[cfg(feature = "annex-b")] +fn html_close_comment_first_line_with_spaces() { + let mut cur = BufferedLexer::from(&b" --> comment\nx"[..]); + let interner = &mut Interner::default(); + + assert_eq!( + *cur.peek(0, true, interner) + .unwrap() + .expect("Token expected") + .kind(), + TokenKind::identifier(interner.get_or_intern_static("x", utf16!("x"))) + ); +} + +#[test] +#[cfg(feature = "annex-b")] +fn html_close_comment_first_line_with_block_comments() { + let mut cur = BufferedLexer::from(&b"/* comment */ /* another */ --> comment\nx"[..]); + let interner = &mut Interner::default(); + + assert_eq!( + *cur.peek(0, true, interner) + .unwrap() + .expect("Token expected") + .kind(), + TokenKind::identifier(interner.get_or_intern_static("x", utf16!("x"))) + ); +} + +#[test] +#[cfg(feature = "annex-b")] +fn html_close_comment_first_line_eof() { + let mut cur = BufferedLexer::from(&b"--> comment"[..]); + let interner = &mut Interner::default(); + + assert!(cur.peek(0, true, interner).unwrap().is_none()); +}