camc314 committed 2026-08-26 10:33:48 +00:00
1 parent 7cec5f3f69
commit d5163d0faa
5 files changed
+133 -79

No files matched your search

-2
View File
@@ -129,8 +129,6 @@ impl<'a, C: Config> ParserImpl<'a, C> {
} else {
(None, None)
};
// `const foo /* #__PURE__ */ = bar()` - pure comment before `=` cannot be applied
self.lexer.trivia_builder.mark_current_pure_comment_not_applied();
let init = self.eat(Kind::Eq).then(|| self.parse_assignment_expression_or_higher());
let decl = VariableDeclarator::new(
self.end_span(start),
+22 -34
View File
@@ -744,6 +744,10 @@ impl<'a, C: Config> ParserImpl<'a, C> {
/// Section 13.3 Left-Hand-Side Expression
pub(crate) fn parse_lhs_expression_or_higher(&mut self) -> Expression<'a> {
self.with_pure_comments(Self::parse_lhs_expression_or_higher_impl)
}
fn parse_lhs_expression_or_higher_impl(&mut self) -> Expression<'a> {
let start = self.cur_start();
let mut in_optional_chain = false;
// `MemberExpression`
@@ -778,6 +782,20 @@ impl<'a, C: Config> ParserImpl<'a, C> {
}
}
fn with_pure_comments(
&mut self,
parse: impl FnOnce(&mut Self) -> Expression<'a>,
) -> Expression<'a> {
let pure_comments = self.lexer.trivia_builder.previous_token_pure_comments();
let mut expr = parse(self);
if let Some(comments) = pure_comments
&& Self::set_pure_on_call_or_new_expr(&mut expr)
{
self.lexer.trivia_builder.mark_pure_comments_applied(comments);
}
expr
}
fn map_to_chain_expression(&self, span: Span, expr: Expression<'a>) -> Expression<'a> {
match expr {
match_member_expression!(Expression) => {
@@ -1235,7 +1253,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
// which parses the JSX element.)
Kind::LAngle if !self.source_type.is_jsx() => {
if self.is_ts {
self.parse_ts_type_assertion()
self.with_pure_comments(Self::parse_ts_type_assertion)
} else {
self.parse_jsx_in_non_jsx_error()
}
@@ -1257,7 +1275,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
return self.parse_jsx_expression();
}
if self.is_ts {
return self.parse_ts_type_assertion();
return self.with_pure_comments(Self::parse_ts_type_assertion);
}
self.parse_jsx_in_non_jsx_error()
}
@@ -1277,13 +1295,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
let start = self.cur_start();
let operator = map_unary_operator(self.cur_kind());
self.bump_any();
let pure_comment_index = self.lexer.trivia_builder.previous_token_has_pure_comment();
let mut argument = self.parse_simple_unary_expression(self.cur_start());
if let Some(index) = pure_comment_index
&& !Self::set_pure_on_call_or_new_expr(&mut argument)
{
self.lexer.trivia_builder.mark_pure_comment_not_applied(index);
}
let argument = self.parse_simple_unary_expression(self.cur_start());
Expression::new_unary_expression(self.end_span(start), operator, argument, self)
}
@@ -1298,13 +1310,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
let lhs = if self.ctx.has_in() && self.at(Kind::PrivateIdentifier) {
self.parse_private_in_expression(lhs_start, lhs_precedence)
} else {
let has_pure_comment =
self.lexer.trivia_builder.previous_token_has_pure_comment().is_some();
let mut expr = self.parse_unary_expression_or_higher(lhs_start);
if has_pure_comment {
Self::set_pure_on_call_or_new_expr(&mut expr);
}
expr
self.parse_unary_expression_or_higher(lhs_start)
};
self.parse_binary_expression_rest(lhs_start, lhs, lhs_parenthesized, lhs_precedence)
@@ -1485,7 +1491,6 @@ impl<'a, C: Config> ParserImpl<'a, C> {
) -> Expression<'a> {
let has_no_side_effects_comment =
self.lexer.trivia_builder.previous_token_has_no_side_effects_comment();
let pure_comment_index = self.lexer.trivia_builder.previous_token_has_pure_comment();
// [+Yield] YieldExpression
if self.is_yield_expression() {
return self.parse_yield_expression();
@@ -1549,12 +1554,6 @@ impl<'a, C: Config> ParserImpl<'a, C> {
let mut expr =
self.parse_conditional_expression_rest(start, lhs, allow_return_type_in_arrow_function);
if let Some(index) = pure_comment_index
&& !Self::set_pure_on_call_or_new_expr(&mut expr)
{
self.lexer.trivia_builder.mark_pure_comment_not_applied(index);
}
if has_no_side_effects_comment {
Self::set_pure_on_function_expr(&mut expr);
}
@@ -1572,17 +1571,6 @@ impl<'a, C: Config> ParserImpl<'a, C> {
new_expr.pure = true;
true
}
Expression::BinaryExpression(binary_expr) => {
Self::set_pure_on_call_or_new_expr(&mut binary_expr.left)
}
Expression::LogicalExpression(logical_expr) => {
Self::set_pure_on_call_or_new_expr(&mut logical_expr.left)
}
Expression::ConditionalExpression(conditional_expr) => {
Self::set_pure_on_call_or_new_expr(&mut conditional_expr.test)
}
// Recurse through member-access chains: `/* #__PURE__ */ foo().a.b.c`
// applies PURE to the underlying call/new (Rollup/esbuild semantics).
expr @ match_member_expression!(Expression) => {
Self::set_pure_on_call_or_new_expr(expr.to_member_expression_mut().object_mut())
}
-9
View File
@@ -134,7 +134,6 @@ impl<'a, C: Config> ParserImpl<'a, C> {
) -> Statement<'a> {
let has_no_side_effects_comment =
self.lexer.trivia_builder.previous_token_has_no_side_effects_comment();
let pure_comment_index = self.lexer.trivia_builder.previous_token_has_pure_comment();
let mut stmt = match self.cur_kind() {
Kind::LCurly => self.parse_block_statement(),
@@ -196,14 +195,6 @@ impl<'a, C: Config> ParserImpl<'a, C> {
_ => self.parse_expression_or_labeled_statement(),
};
// `/* #__PURE__ */ function foo() {}` - pure comment before non-expression statements cannot be applied.
// Expression statements handle pure comments internally in `parse_assignment_expression_or_higher_impl`.
if let Some(index) = pure_comment_index
&& !matches!(stmt, Statement::ExpressionStatement(_))
{
self.lexer.trivia_builder.mark_pure_comment_not_applied(index);
}
if has_no_side_effects_comment {
Self::set_pure_on_function_stmt(&mut stmt);
}
+6 -6
View File
@@ -53,7 +53,7 @@ pub struct LexerCheckpoint<'a> {
token: Token,
errors_snapshot: ErrorSnapshot<'a>,
tokens_len: usize,
pure_comment: Option<usize>,
pure_comments: Option<(usize, usize)>,
has_no_side_effects_comment: bool,
}
@@ -198,7 +198,7 @@ impl<'a, C: Config> Lexer<'a, C> {
token: self.token,
errors_snapshot,
tokens_len: self.tokens.len(),
pure_comment: self.trivia_builder.pure_comment,
pure_comments: self.trivia_builder.previous_token_pure_comments(),
has_no_side_effects_comment: self.trivia_builder.has_no_side_effects_comment,
}
}
@@ -216,7 +216,7 @@ impl<'a, C: Config> Lexer<'a, C> {
token: self.token,
errors_snapshot,
tokens_len: self.tokens.len(),
pure_comment: self.trivia_builder.pure_comment,
pure_comments: self.trivia_builder.previous_token_pure_comments(),
has_no_side_effects_comment: self.trivia_builder.has_no_side_effects_comment,
}
}
@@ -231,7 +231,7 @@ impl<'a, C: Config> Lexer<'a, C> {
self.tokens.truncate(checkpoint.tokens_len);
self.source.set_position(checkpoint.source_position);
self.token = checkpoint.token;
self.trivia_builder.pure_comment = checkpoint.pure_comment;
self.trivia_builder.set_pure_comments(checkpoint.pure_comments);
self.trivia_builder.has_no_side_effects_comment = checkpoint.has_no_side_effects_comment;
}
@@ -476,7 +476,7 @@ impl<'a, C: Config> Lexer<'a, C> {
/// Whitespace and line terminators are skipped
#[inline] // Make sure is inlined into `next_token`
fn read_next_token(&mut self) -> Kind {
self.trivia_builder.pure_comment = None;
self.trivia_builder.clear_pure_comments();
self.trivia_builder.has_no_side_effects_comment = false;
let end_pos = self.source.end();
@@ -544,7 +544,7 @@ impl<'a, C: Config> Lexer<'a, C> {
#[inline]
fn read_next_jsx_attribute_value(&mut self) -> Kind {
self.trivia_builder.pure_comment = None;
self.trivia_builder.clear_pure_comments();
self.trivia_builder.has_no_side_effects_comment = false;
let end_pos = self.source.end();
+105 -28
View File
@@ -29,8 +29,8 @@ pub struct TriviaBuilder<'a> {
/// Previous token, used to attach trailing comments to its end.
previous_token: Token,
/// Index of the pure comment in `comments` vec, or `None` if no pure comment for the current token.
pub(super) pure_comment: Option<usize>,
/// Range of comments preceding the current token that contains pure comments.
pure_comments: Option<(usize, usize)>,
pub(super) has_no_side_effects_comment: bool,
}
@@ -46,30 +46,32 @@ impl<'a> TriviaBuilder<'a> {
saw_newline: true,
saw_newline_for_comment: true,
previous_token,
pure_comment: None,
pure_comments: None,
has_no_side_effects_comment: false,
}
}
pub fn previous_token_has_pure_comment(&self) -> Option<usize> {
self.pure_comment
pub fn previous_token_pure_comments(&self) -> Option<(usize, usize)> {
self.pure_comments
}
pub fn previous_token_has_no_side_effects_comment(&self) -> bool {
self.has_no_side_effects_comment
}
pub fn mark_pure_comment_not_applied(&mut self, index: usize) {
if let Some(comment) = self.comments.get_mut(index) {
debug_assert!(comment.is_pure());
comment.content = CommentContent::PureNotApplied;
}
pub(super) fn set_pure_comments(&mut self, pure_comments: Option<(usize, usize)>) {
self.pure_comments = pure_comments;
}
/// Mark the current token's pure comment (if any) as not applied.
pub fn mark_current_pure_comment_not_applied(&mut self) {
if let Some(index) = self.pure_comment {
self.mark_pure_comment_not_applied(index);
pub(super) fn clear_pure_comments(&mut self) {
self.pure_comments = None;
}
pub fn mark_pure_comments_applied(&mut self, (start, end): (usize, usize)) {
for comment in &mut self.comments[start..end] {
if comment.content == CommentContent::PureNotApplied {
comment.content = CommentContent::Pure;
}
}
}
@@ -251,10 +253,14 @@ impl<'a> TriviaBuilder<'a> {
)
}
/// Update `pure_comment` / `has_no_side_effects_comment` to point to the comment at `index`.
/// Update annotation state for the comment at `index`.
fn set_annotation_flags(&mut self, comment: &Comment, index: usize) {
if comment.is_pure() {
self.pure_comment = Some(index);
if comment.content == CommentContent::PureNotApplied {
if let Some((_, end)) = &mut self.pure_comments {
*end = index + 1;
} else {
self.pure_comments = Some((index, index + 1));
}
} else if comment.is_no_side_effects() {
self.has_no_side_effects_comment = true;
}
@@ -269,7 +275,13 @@ impl<'a> TriviaBuilder<'a> {
{
// Duplicate from parser lookahead/rewind — update annotation flags
// to point to the existing comment.
self.set_annotation_flags(&comment, self.comments.len() - 1);
if let Ok(index) = self
.comments
.binary_search_by_key(&comment.span.start, |existing| existing.span.start)
&& self.comments[index].span == comment.span
{
self.set_annotation_flags(&comment, index);
}
return;
}
@@ -430,7 +442,7 @@ impl<'a> TriviaBuilder<'a> {
if start < bytes.len() && bytes[start..].starts_with(b"__") {
let rest = &bytes[start + 2..];
if rest.starts_with(b"PURE__") {
comment.content = CommentContent::Pure;
comment.content = CommentContent::PureNotApplied;
return;
} else if rest.starts_with(b"NO_SIDE_EFFECTS__") {
comment.content = CommentContent::NoSideEffects;
@@ -517,6 +529,14 @@ mod test {
ret.program.comments.iter().copied().collect::<Vec<_>>()
}
fn get_comments_typescript(source_text: &str) -> Vec<Comment> {
let allocator = Allocator::default();
let source_type = SourceType::default().with_typescript(true);
let ret = Parser::new(&allocator, source_text, source_type).parse();
assert!(ret.diagnostics.is_empty());
ret.program.comments.iter().copied().collect::<Vec<_>>()
}
#[test]
fn comment_attachments() {
let source_text = "
@@ -889,21 +909,79 @@ function bar() {}";
"const foo /* #__PURE__ */ = pureOperation();",
// Pure comment before object literal (triggers parser lookahead/rewind for arrow detection)
"export const X = /* @__PURE__ */ { a: 1 };",
"foo /* #__PURE__ */ = pureOperation();",
"foo /* #__PURE__ */ + pureOperation();",
"foo /* #__PURE__ */ && pureOperation();",
"foo /* #__PURE__ */ in pureOperation();",
"foo /* #__PURE__ */ as T;",
"foo /* #__PURE__ */ satisfies T;",
"foo /* #__PURE__ */ ? bar : baz;",
"foo /* #__PURE__ */, bar;",
"({ x /*#__PURE__*/: sideEffect() });",
"true ? x /*#__PURE__*/ : sideEffect();",
"foo /* #__PURE__ */++;",
"foo /* #__PURE__ */--;",
"foo /* #__PURE__ */.bar;",
"foo /* #__PURE__ */[bar];",
"foo /* #__PURE__ */();",
"foo /* #__PURE__ */?.bar;",
"foo /* #__PURE__ */!;",
r"foo /* #__PURE__ */ `bar`;",
"42 /* #__PURE__ */ + foo;",
"(foo) /* #__PURE__ */ + bar;",
"[foo] /* #__PURE__ */ + bar;",
"foo\n/* #__PURE__ */ + bar;",
"foo // #__PURE__\n+ bar;",
"(a = foo /* #__PURE__ */ + bar)",
];
for source_text in cases {
let comments = get_comments(source_text);
let comments = get_comments_typescript(source_text);
assert_eq!(comments[0].content, CommentContent::PureNotApplied, "{source_text}");
}
}
#[test]
fn pure_comment_applied_after_lookahead() {
// `export const X = /* @__PURE__ */ foo()` triggers arrow-function lookahead
// due to the `{`-ambiguity path. The pure comment must still be correctly
// applied to the call expression after the parser rewinds.
let source_text = "export const X = /* @__PURE__ */ foo();";
// The ordinary comment is read during arrow-function lookahead before the parser rewinds.
// Re-lexing the pure comment must recover its index rather than the later comment's index.
let source_text = "export const X = /* @__PURE__ */ foo(/* comment */);";
let comments = get_comments(source_text);
assert_eq!(comments.len(), 2);
assert_eq!(comments[0].content, CommentContent::Pure, "{source_text}");
assert_eq!(comments[1].content, CommentContent::None, "{source_text}");
}
#[test]
fn pure_comment_applied() {
let cases = [
"/* #__PURE__ */ foo();",
"/* #__PURE__ */ (foo)();",
"/* #__PURE__ */ (new Foo());",
"a + /* #__PURE__ */ <T>foo(), bar;",
"x = /* #__PURE__ */ pureOperation() || y;",
"y || (x = /* #__PURE__ */ pureOperation());",
"y || /* #__PURE__ */ pureOperation();",
"function f() {} /* #__PURE__ */ (foo)();",
"/*#__PURE__*/ [foo][0]()",
"if (cond) /*#__PURE__*/ (foo)();",
"foo++\n/*#__PURE__*/ (bar)();",
"function f() {} /*#__PURE__*/ [foo][0]();",
];
for source_text in cases {
let comments = get_comments_typescript(source_text);
assert_eq!(comments[0].content, CommentContent::Pure, "{source_text}");
}
}
#[test]
fn multiple_pure_comments_applied() {
let source_text = "export const X = /*#__PURE__*/ /* comment */ /*@__PURE__*/ foo();";
let comments = get_comments(source_text);
assert_eq!(comments.len(), 3);
assert_eq!(comments[0].content, CommentContent::Pure);
assert_eq!(comments[1].content, CommentContent::None);
assert_eq!(comments[2].content, CommentContent::Pure);
}
#[test]
@@ -927,9 +1005,8 @@ function bar() {}";
}
#[test]
fn pure_comment_not_applied_marks_correct_comment() {
fn pure_comments_are_applied_independently() {
// The first pure comment is invalid (before `foo`), the second is valid (before `bar()`).
// `mark_pure_comment_not_applied` must retag the first comment, not the second.
let source_text = "/*#__PURE__*/ foo + /*#__PURE__*/ bar()";
let comments = get_comments(source_text);
assert_eq!(
@@ -974,9 +1051,9 @@ function bar() {}";
("/* @vite-xxx */", CommentContent::Vite),
("/* webpackChunkName: 'my-chunk-name' */", CommentContent::Webpack),
("/* webpack */", CommentContent::None),
("/* @__PURE__ */", CommentContent::Pure),
("/* @__PURE__ */", CommentContent::PureNotApplied),
("/* @__NO_SIDE_EFFECTS__ */", CommentContent::NoSideEffects),
("/* #__PURE__ */", CommentContent::Pure),
("/* #__PURE__ */", CommentContent::PureNotApplied),
("/* #__NO_SIDE_EFFECTS__ */", CommentContent::NoSideEffects),
("/* @__KEY__ */", CommentContent::PropertyKey),
("/* #__KEY__ */", CommentContent::PropertyKey),