diff --git a/crates/oxc_ast/src/ast/comment.rs b/crates/oxc_ast/src/ast/comment.rs index a54e2595128e4..cddf0146e9262 100644 --- a/crates/oxc_ast/src/ast/comment.rs +++ b/crates/oxc_ast/src/ast/comment.rs @@ -125,6 +125,17 @@ impl Comment { || source_text.contains("@preserve") } + /// `#__PURE__` Notation Specification + /// + /// + #[inline] // inline because code path is hot. + pub fn is_pure(&self, source_text: &str) -> bool { + source_text[(self.span.start + 2) as usize..] + .trim_ascii_start() + .strip_prefix(['@', '#']) + .is_some_and(|s| s.starts_with("__PURE__")) + } + /// Gets the span of the comment content. pub fn content_span(&self) -> Span { match self.kind { diff --git a/crates/oxc_ast/src/ast_impl/js.rs b/crates/oxc_ast/src/ast_impl/js.rs index a1d5ad4d7588a..a5b5c0457d219 100644 --- a/crates/oxc_ast/src/ast_impl/js.rs +++ b/crates/oxc_ast/src/ast_impl/js.rs @@ -167,6 +167,15 @@ impl<'a> Expression<'a> { expr } + /// Remove nested parentheses from this expression. + pub fn without_parentheses_mut(&mut self) -> &mut Self { + let mut expr = self; + while let Expression::ParenthesizedExpression(paran_expr) = expr { + expr = &mut paran_expr.expression; + } + expr + } + /// Returns `true` if this [`Expression`] is an [`IdentifierReference`] with specified `name`. pub fn is_specific_id(&self, name: &str) -> bool { match self.get_inner_expression() { diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index 5f2b655774473..b1ba1bd32b7e3 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -1430,12 +1430,15 @@ impl GenExpr for CallExpression<'_> { let is_statement = p.start_of_stmt == p.code_len(); let is_export_default = p.start_of_default_export == p.code_len(); let mut wrap = precedence >= Precedence::New || ctx.intersects(Context::FORBID_CALL); - if precedence >= Precedence::Postfix && p.has_annotation_comment(self.span.start) { + let pure = p.options.print_annotation_comments() && self.pure; + if precedence >= Precedence::Postfix && pure { wrap = true; } p.wrap(wrap, |p| { - p.print_annotation_comments(self.span.start); + if pure { + p.print_str("/* @__PURE__ */ "); + } if is_export_default { p.start_of_default_export = p.code_len(); } else if is_statement { @@ -2198,11 +2201,14 @@ impl GenExpr for ChainExpression<'_> { impl GenExpr for NewExpression<'_> { fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) { let mut wrap = precedence >= self.precedence(); - if precedence >= Precedence::Postfix && p.has_annotation_comment(self.span.start) { + let pure = p.options.print_annotation_comments() && self.pure; + if precedence >= Precedence::Postfix && pure { wrap = true; } p.wrap(wrap, |p| { - p.print_annotation_comments(self.span.start); + if pure { + p.print_str("/* @__PURE__ */ "); + } p.print_space_before_identifier(); p.add_source_mapping(self.span); p.print_str("new"); diff --git a/crates/oxc_codegen/tests/integration/esbuild.rs b/crates/oxc_codegen/tests/integration/esbuild.rs index 14f8ec35f7759..4c13cba602673 100644 --- a/crates/oxc_codegen/tests/integration/esbuild.rs +++ b/crates/oxc_codegen/tests/integration/esbuild.rs @@ -552,7 +552,7 @@ fn test_pure_comment() { test("new (function() {})", "new function() {}();\n"); test("new (function() {})()", "new function() {}();\n"); - test("/*@__PURE__*/new (function() {})()", "/*@__PURE__*/ new function() {}();\n"); + test("/*@__PURE__*/new (function() {})()", "/* @__PURE__ */ new function() {}();\n"); test("export default (function() { foo() })", "export default (function() {\n\tfoo();\n});\n"); test( @@ -561,7 +561,7 @@ fn test_pure_comment() { ); test( "export default /*@__PURE__*/(function() { foo() })()", - "export default /*@__PURE__*/ (function() {\n\tfoo();\n})();\n", + "export default /* @__PURE__ */ (function() {\n\tfoo();\n})();\n", ); } diff --git a/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap b/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap index 001226ce07331..9735cf3c2c80f 100644 --- a/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap +++ b/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap @@ -175,13 +175,13 @@ isFunction(options) : options ---------- -isFunction(options) ? /*#__PURE__*/ (() => extend({ name: options.name }, extraOptions, { setup: options }))() : options; +isFunction(options) ? /* @__PURE__ */ (() => extend({ name: options.name }, extraOptions, { setup: options }))() : options; ########## 10 isFunction(options) ? /*#__PURE__*/ (() => extend({ name: options.name }, extraOptions, { setup: options }))() : options; ---------- -isFunction(options) ? /*#__PURE__*/ (() => extend({ name: options.name }, extraOptions, { setup: options }))() : options; +isFunction(options) ? /* @__PURE__ */ (() => extend({ name: options.name }, extraOptions, { setup: options }))() : options; ########## 11 @@ -194,18 +194,18 @@ const obj = { const p = /*#__PURE__*/ Promise.resolve(); ---------- -const obj = { props: /*#__PURE__*/ extend({}, TransitionPropsValidators, { +const obj = { props: /* @__PURE__ */ extend({}, TransitionPropsValidators, { tag: String, moveClass: String }) }; -const p = /*#__PURE__*/ Promise.resolve(); +const p = /* @__PURE__ */ Promise.resolve(); ########## 12 const staticCacheMap = /*#__PURE__*/ new WeakMap() ---------- -const staticCacheMap = /*#__PURE__*/ new WeakMap(); +const staticCacheMap = /* @__PURE__ */ new WeakMap(); ########## 13 @@ -217,7 +217,7 @@ const builtInSymbols = new Set( ---------- const builtInSymbols = new Set( - /*#__PURE__*/ Object.getOwnPropertyNames(Symbol).filter((key) => key !== 'arguments' && key !== 'caller') + /* @__PURE__ */ Object.getOwnPropertyNames(Symbol).filter((key) => key !== 'arguments' && key !== 'caller') ); ########## 14 @@ -267,7 +267,7 @@ const defineSSRCustomElement = () => { ---------- const defineSSRCustomElement = () => { - return /* @__PURE__ */ /* @__NO_SIDE_EFFECTS__ */ /* #__NO_SIDE_EFFECTS__ */ defineCustomElement(options, extraOptions, hydrate); + return /* @__PURE__ */ defineCustomElement(options, extraOptions, hydrate); }; ########## 20 @@ -276,7 +276,7 @@ const defineSSRCustomElement = () => { React.forwardRef((props, ref) => {}); ---------- -const Component = /* #__PURE__*/ React.forwardRef((props, ref) => {}); +const Component = /* @__PURE__ */ React.forwardRef((props, ref) => {}); ########## 21 @@ -347,18 +347,18 @@ let at_yes = /* @__PURE__ */ foo(bar); let at_no = /* @__PURE__ */ foo(bar()); let new_at_yes = /* @__PURE__ */ new foo(bar); let new_at_no = /* @__PURE__ */ new foo(bar()); -let nospace_at_yes = /*@__PURE__*/ foo(bar); -let nospace_at_no = /*@__PURE__*/ foo(bar()); -let nospace_new_at_yes = /*@__PURE__*/ new foo(bar); -let nospace_new_at_no = /*@__PURE__*/ new foo(bar()); -let num_yes = /* #__PURE__ */ foo(bar); -let num_no = /* #__PURE__ */ foo(bar()); -let new_num_yes = /* #__PURE__ */ new foo(bar); -let new_num_no = /* #__PURE__ */ new foo(bar()); -let nospace_num_yes = /*#__PURE__*/ foo(bar); -let nospace_num_no = /*#__PURE__*/ foo(bar()); -let nospace_new_num_yes = /*#__PURE__*/ new foo(bar); -let nospace_new_num_no = /*#__PURE__*/ new foo(bar()); +let nospace_at_yes = /* @__PURE__ */ foo(bar); +let nospace_at_no = /* @__PURE__ */ foo(bar()); +let nospace_new_at_yes = /* @__PURE__ */ new foo(bar); +let nospace_new_at_no = /* @__PURE__ */ new foo(bar()); +let num_yes = /* @__PURE__ */ foo(bar); +let num_no = /* @__PURE__ */ foo(bar()); +let new_num_yes = /* @__PURE__ */ new foo(bar); +let new_num_no = /* @__PURE__ */ new foo(bar()); +let nospace_num_yes = /* @__PURE__ */ foo(bar); +let nospace_num_no = /* @__PURE__ */ foo(bar()); +let nospace_new_num_yes = /* @__PURE__ */ new foo(bar); +let nospace_new_num_no = /* @__PURE__ */ new foo(bar()); let dot_yes = /* @__PURE__ */ foo(sideEffect()).dot(bar); let dot_no = /* @__PURE__ */ foo(sideEffect()).dot(bar()); let new_dot_yes = /* @__PURE__ */ new foo(sideEffect()).dot(bar); @@ -383,14 +383,14 @@ let new_nested_no = [ /* @__PURE__ */ new foo(bar()), 2 ]; -let single_at_yes = /* @__PURE__*/ foo(bar); -let single_at_no = /* @__PURE__*/ foo(bar()); -let new_single_at_yes = /* @__PURE__*/ new foo(bar); -let new_single_at_no = /* @__PURE__*/ new foo(bar()); -let single_num_yes = /* #__PURE__*/ foo(bar); -let single_num_no = /* #__PURE__*/ foo(bar()); -let new_single_num_yes = /* #__PURE__*/ new foo(bar); -let new_single_num_no = /* #__PURE__*/ new foo(bar()); +let single_at_yes = /* @__PURE__ */ foo(bar); +let single_at_no = /* @__PURE__ */ foo(bar()); +let new_single_at_yes = /* @__PURE__ */ new foo(bar); +let new_single_at_no = /* @__PURE__ */ new foo(bar()); +let single_num_yes = /* @__PURE__ */ foo(bar); +let single_num_no = /* @__PURE__ */ foo(bar()); +let new_single_num_yes = /* @__PURE__ */ new foo(bar); +let new_single_num_no = /* @__PURE__ */ new foo(bar()); let bad_no = foo(bar); let new_bad_no = new foo(bar); let parens_no = foo(bar); diff --git a/crates/oxc_codegen/tests/integration/tester.rs b/crates/oxc_codegen/tests/integration/tester.rs index 69f89835feadb..83eea6dbc0592 100644 --- a/crates/oxc_codegen/tests/integration/tester.rs +++ b/crates/oxc_codegen/tests/integration/tester.rs @@ -7,6 +7,10 @@ pub fn test(source_text: &str, expected: &str) { test_options(source_text, expected, CodegenOptions::default()); } +pub fn test_same(source_text: &str) { + test(source_text, source_text); +} + pub fn test_options(source_text: &str, expected: &str, options: CodegenOptions) { test_options_with_source_type(source_text, expected, SourceType::jsx(), options); } diff --git a/crates/oxc_codegen/tests/integration/unit.rs b/crates/oxc_codegen/tests/integration/unit.rs index c7673fe7b3e97..7c7c326b53f95 100644 --- a/crates/oxc_codegen/tests/integration/unit.rs +++ b/crates/oxc_codegen/tests/integration/unit.rs @@ -1,6 +1,6 @@ use oxc_codegen::CodegenOptions; -use crate::tester::{test, test_minify, test_minify_same, test_options}; +use crate::tester::{test, test_minify, test_minify_same, test_options, test_same}; #[test] fn decl() { @@ -356,6 +356,29 @@ fn vite_special_comments() { ); } +// +#[test] +fn pure_comment() { + test_same("/* @__PURE__ */ pureOperation();\n"); + test_same("/* @__PURE__ */ new PureConsutrctor();\n"); + test("/* @__PURE__ */\npureOperation();\n", "/* @__PURE__ */ pureOperation();\n"); + test( + "/* @__PURE__ The comment may contain additional text */ pureOperation();\n", + "/* @__PURE__ */ pureOperation();\n", + ); + test("const foo /* #__PURE__ */ = pureOperation();", "const foo = pureOperation();\n"); // INVALID: "=" not allowed after annotation + + test_same("/* #__PURE__ */ function foo() {}\n"); // FIXME: can be removed. // INVALID: Only allowed for calls + + test("/* @__PURE__ */ (foo());", "/* @__PURE__ */ foo();\n"); + test("/* @__PURE__ */ (new Foo());\n", "/* @__PURE__ */ new Foo();\n"); + test("/*#__PURE__*/ (foo(), bar());", "foo(), bar();\n"); // INVALID, there is a comma expression in the parentheses + + test_same("/* @__PURE__ */ a.b().c.d();\n"); + test("/* @__PURE__ */ a().b;", "a().b;\n"); // INVALID, it does not end with a call + test_same("(/* @__PURE__ */ a()).b;\n"); +} + // followup from https://github.com/oxc-project/oxc/pull/6422 #[test] fn in_expr_in_sequence_in_for_loop_init() { diff --git a/crates/oxc_parser/src/js/expression.rs b/crates/oxc_parser/src/js/expression.rs index c11848a0b8383..7d36f9a44d928 100644 --- a/crates/oxc_parser/src/js/expression.rs +++ b/crates/oxc_parser/src/js/expression.rs @@ -1102,6 +1102,7 @@ impl<'a> ParserImpl<'a> { &mut self, allow_return_type_in_arrow_function: bool, ) -> Result> { + let has_pure_comment = self.lexer.trivia_builder.previous_token_has_pure_comment(); // [+Yield] YieldExpression if self.is_yield_expression() { return self.parse_yield_expression(); @@ -1141,7 +1142,29 @@ impl<'a> ParserImpl<'a> { ); } - self.parse_conditional_expression_rest(span, lhs, allow_return_type_in_arrow_function) + let mut expr = + self.parse_conditional_expression_rest(span, lhs, allow_return_type_in_arrow_function)?; + + if has_pure_comment { + Self::set_pure_on_call_or_new_expr(&mut expr); + } + + Ok(expr) + } + + fn set_pure_on_call_or_new_expr(expr: &mut Expression<'a>) { + match &mut expr.without_parentheses_mut() { + Expression::CallExpression(call_expr) => { + call_expr.pure = true; + } + Expression::NewExpression(new_expr) => { + new_expr.pure = true; + } + Expression::BinaryExpression(binary_expr) => { + Self::set_pure_on_call_or_new_expr(&mut binary_expr.left); + } + _ => {} + } } fn parse_assignment_expression_recursive( diff --git a/crates/oxc_parser/src/lexer/comment.rs b/crates/oxc_parser/src/lexer/comment.rs index 143d5ae660f87..1aea27cce39c2 100644 --- a/crates/oxc_parser/src/lexer/comment.rs +++ b/crates/oxc_parser/src/lexer/comment.rs @@ -36,7 +36,7 @@ impl<'a> Lexer<'a> { if next_byte != LS_OR_PS_FIRST { // `\r` or `\n` self.trivia_builder - .add_line_comment(self.token.start, self.source.offset_of(pos)); + .add_line_comment(self.token.start, self.source.offset_of(pos), self.source.whole()); // SAFETY: Safe to consume `\r` or `\n` as both are ASCII pos = unsafe { pos.add(1) }; // We've found the end. Do not continue searching. @@ -51,7 +51,7 @@ impl<'a> Lexer<'a> { if matches!(next2, LS_BYTES_2_AND_3 | PS_BYTES_2_AND_3) { // Irregular line break self.trivia_builder - .add_line_comment(self.token.start, self.source.offset_of(pos)); + .add_line_comment(self.token.start, self.source.offset_of(pos), self.source.whole()); // Advance `pos` to after this char. // 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. @@ -70,7 +70,7 @@ impl<'a> Lexer<'a> { } }, handle_eof: { - self.trivia_builder.add_line_comment(self.token.start, self.offset()); + self.trivia_builder.add_line_comment(self.token.start, self.offset(), self.source.whole()); return Kind::Skip; }, }; @@ -146,7 +146,7 @@ impl<'a> Lexer<'a> { }, }; - self.trivia_builder.add_block_comment(self.token.start, self.offset()); + self.trivia_builder.add_block_comment(self.token.start, self.offset(), self.source.whole()); Kind::Skip } @@ -166,7 +166,11 @@ impl<'a> Lexer<'a> { if let Some(index) = finder.find(remaining) { // SAFETY: `pos + index + 2` is end of `*/`, so a valid `SourcePosition` self.source.set_position(unsafe { pos.add(index + 2) }); - self.trivia_builder.add_block_comment(self.token.start, self.offset()); + self.trivia_builder.add_block_comment( + self.token.start, + self.offset(), + self.source.whole(), + ); Kind::Skip } else { self.source.advance_to_end(); diff --git a/crates/oxc_parser/src/lexer/mod.rs b/crates/oxc_parser/src/lexer/mod.rs index d0a9ed7ce563a..fb51caba5ce82 100644 --- a/crates/oxc_parser/src/lexer/mod.rs +++ b/crates/oxc_parser/src/lexer/mod.rs @@ -312,6 +312,7 @@ impl<'a> Lexer<'a> { /// Read each char and set the current token /// Whitespace and line terminators are skipped fn read_next_token(&mut self) -> Kind { + self.trivia_builder.has_pure_comment = false; loop { let offset = self.offset(); self.token.start = offset; diff --git a/crates/oxc_parser/src/lexer/trivia_builder.rs b/crates/oxc_parser/src/lexer/trivia_builder.rs index 1bf2705924a60..93996a6c4c344 100644 --- a/crates/oxc_parser/src/lexer/trivia_builder.rs +++ b/crates/oxc_parser/src/lexer/trivia_builder.rs @@ -21,6 +21,8 @@ pub struct TriviaBuilder { /// Previous token kind, used to indicates comments are trailing from what kind previous_kind: Kind, + + pub(super) has_pure_comment: bool, } impl Default for TriviaBuilder { @@ -31,21 +33,26 @@ impl Default for TriviaBuilder { processed: 0, saw_newline: true, previous_kind: Kind::Undetermined, + has_pure_comment: false, } } } impl TriviaBuilder { + pub fn previous_token_has_pure_comment(&self) -> bool { + self.has_pure_comment + } + pub fn add_irregular_whitespace(&mut self, start: u32, end: u32) { self.irregular_whitespaces.push(Span::new(start, end)); } - pub fn add_line_comment(&mut self, start: u32, end: u32) { - self.add_comment(Comment::new(start, end, CommentKind::Line)); + pub fn add_line_comment(&mut self, start: u32, end: u32, source_text: &str) { + self.add_comment(Comment::new(start, end, CommentKind::Line), source_text); } - pub fn add_block_comment(&mut self, start: u32, end: u32) { - self.add_comment(Comment::new(start, end, CommentKind::Block)); + pub fn add_block_comment(&mut self, start: u32, end: u32, source_text: &str) { + self.add_comment(Comment::new(start, end, CommentKind::Block), source_text); } // For block comments only. This function is not called after line comments because the lexer skips @@ -105,7 +112,10 @@ impl TriviaBuilder { !self.saw_newline && !matches!(self.previous_kind, Kind::Eq | Kind::LParen) } - fn add_comment(&mut self, comment: Comment) { + fn add_comment(&mut self, comment: Comment, source_text: &str) { + if comment.is_pure(source_text) { + self.has_pure_comment = true; + } // The comments array is an ordered vec, only add the comment if its not added before, // to avoid situations where the parser needs to rewind and tries to reinsert the comment. if let Some(last_comment) = self.comments.last() { diff --git a/crates/oxc_parser/src/lib.rs b/crates/oxc_parser/src/lib.rs index 45ca5835e8d35..5fcb7293e66dd 100644 --- a/crates/oxc_parser/src/lib.rs +++ b/crates/oxc_parser/src/lib.rs @@ -343,7 +343,7 @@ use parser_parse::UniquePromise; struct ParserImpl<'a> { options: ParseOptions, - lexer: Lexer<'a>, + pub(crate) lexer: Lexer<'a>, /// SourceType: JavaScript or TypeScript, Script or Module, jsx support? source_type: SourceType,