fix(parser): classify Unicode line breaks in block comments (#26536)

Block comments containing U+2028 or U+2029 were marked `SingleLineBlock` unless they also contained LF or CR. Switch to the multiline-comment scanner after either Unicode separator so downstream consumers receive the correct comment kind.

This prevents the formatter from dropping parentheses around a return argument with such a leading comment, which could change the returned value to `undefined`.
This commit is contained in:
camc314 committed 2026-09-10 15:28:49 +00:00
1 parent 75cd919ffb
commit d7713ad4cb
3 files changed
+45 -5

No files matched your search

@@ -1011,3 +1011,20 @@ fn test_comment_on_paren_protected_prologue_boundary() {
test_same("// leading comment\n(\"use strict\");\nfoo();\n");
test_same("\"use asm\";\n// leading comment\n(\"use strict\");\nfoo();\n");
}
#[test]
fn test_block_comment_line_terminators() {
for separator in ["\u{2028}", "\u{2029}", "\n", "\r", "\r\n"] {
let source = format!("function f(){{return (/* first{separator}second */ {{}});}}");
test(&source, "function f() {\n\treturn (/* first\n\tsecond */ {});\n}\n");
test_idempotency(&source);
}
for separator in ["", "\u{2027}", "\u{202a}", "\u{00a0}"] {
let source = format!("function f(){{return (/* first{separator}second */ {{}});}}");
test(
&source,
&format!("function f() {{\n\treturn (/* first{separator}second */ {{}});\n}}\n"),
);
test_idempotency(&source);
}
}
+5 -5
View File
@@ -82,9 +82,8 @@ impl<'a, C: Config> Lexer<'a, C> {
/// Section 12.4 Multi Line Comment
pub(super) fn skip_multi_line_comment(&mut self) -> Kind {
// If `is_on_new_line` is already set, go directly to faster search which only looks for `*/`
// We need to identify if comment contains line breaks or not
// (`CommentKind::Block` or `CommentKind::MultilineBlock`).
// (`CommentKind::SingleLineBlock` or `CommentKind::MultiLineBlock`).
// So we have to use the loop below for the first line of the comment even if
// `Token`'s `is_on_new_line` flag is already set.
// If the loop finds a line break before end of the comment, we then switch to
@@ -126,10 +125,11 @@ impl<'a, C: Config> Lexer<'a, C> {
if matches!(next2, LS_BYTES_2_AND_3 | PS_BYTES_2_AND_3) {
// Irregular line break
self.token.set_is_on_new_line(true);
// Ideally we'd go on to `skip_multi_line_comment_after_line_break` here,
// but irregular line breaks are rare anyway.
// SAFETY: Consuming this 3-byte UTF-8 char leaves a UTF-8 char boundary.
let after_line_break = unsafe { pos.add(3) };
return self.skip_multi_line_comment_after_line_break(after_line_break);
}
// Either way, continue searching.
// Some other Unicode char beginning with `0xE2`.
// Skip 3 bytes (macro skips 1 already, so skip 2 here), and continue searching.
// SAFETY: `0xE2` is always 1st byte of a 3-byte UTF-8 char,
// so consuming 3 bytes will place `pos` on next UTF-8 char boundary.
@@ -1100,6 +1100,29 @@ function bar() {}";
}
}
#[test]
fn block_comment_line_terminators() {
for (separator, kind) in [
("\u{2028}", CommentKind::MultiLineBlock),
("\u{2029}", CommentKind::MultiLineBlock),
("\n", CommentKind::MultiLineBlock),
("\r", CommentKind::MultiLineBlock),
("\r\n", CommentKind::MultiLineBlock),
("", CommentKind::SingleLineBlock),
("\u{2027}", CommentKind::SingleLineBlock),
("\u{202a}", CommentKind::SingleLineBlock),
("\u{00a0}", CommentKind::SingleLineBlock),
] {
// A newline before the comment must not affect its kind.
for prefix in ["", "let a;", "let a;\n"] {
let source = format!("{prefix}/* first{separator}second */ let b;");
let comments = get_comments(&source);
assert_eq!(comments.len(), 1, "{source:?}");
assert_eq!(comments[0].kind, kind, "{source:?}");
}
}
}
#[test]
fn comment_parsing() {
let data = [