diff --git a/crates/oxc_ast/src/ast/comment.rs b/crates/oxc_ast/src/ast/comment.rs index cddf0146e9262..a54e2595128e4 100644 --- a/crates/oxc_ast/src/ast/comment.rs +++ b/crates/oxc_ast/src/ast/comment.rs @@ -125,17 +125,6 @@ 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/js.rs b/crates/oxc_ast/src/ast/js.rs index d885bac350038..67a310e5ee7bb 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -1834,6 +1834,10 @@ pub struct ArrowFunctionExpression<'a> { #[estree(via = ArrowFunctionExpressionBody)] pub body: Box<'a, FunctionBody<'a>>, pub scope_id: Cell>, + /// `true` if the function is marked with a `/*#__NO_SIDE_EFFECTS__*/` comment + #[builder(default)] + #[estree(skip)] + pub pure: bool, } /// Generator Function Definitions diff --git a/crates/oxc_ast/src/generated/assert_layouts.rs b/crates/oxc_ast/src/generated/assert_layouts.rs index 59c090709b6d5..facfff7f064c2 100644 --- a/crates/oxc_ast/src/generated/assert_layouts.rs +++ b/crates/oxc_ast/src/generated/assert_layouts.rs @@ -555,6 +555,7 @@ const _: () = { assert!(offset_of!(ArrowFunctionExpression, return_type) == 32); assert!(offset_of!(ArrowFunctionExpression, body) == 40); assert!(offset_of!(ArrowFunctionExpression, scope_id) == 48); + assert!(offset_of!(ArrowFunctionExpression, pure) == 52); assert!(size_of::() == 32); assert!(align_of::() == 8); @@ -1943,7 +1944,7 @@ const _: () = { assert!(offset_of!(FunctionBody, directives) == 8); assert!(offset_of!(FunctionBody, statements) == 24); - assert!(size_of::() == 32); + assert!(size_of::() == 36); assert!(align_of::() == 4); assert!(offset_of!(ArrowFunctionExpression, span) == 0); assert!(offset_of!(ArrowFunctionExpression, expression) == 8); @@ -1953,6 +1954,7 @@ const _: () = { assert!(offset_of!(ArrowFunctionExpression, return_type) == 20); assert!(offset_of!(ArrowFunctionExpression, body) == 24); assert!(offset_of!(ArrowFunctionExpression, scope_id) == 28); + assert!(offset_of!(ArrowFunctionExpression, pure) == 32); assert!(size_of::() == 20); assert!(align_of::() == 4); diff --git a/crates/oxc_ast/src/generated/ast_builder.rs b/crates/oxc_ast/src/generated/ast_builder.rs index 42f030d533023..0e47722e01fd3 100644 --- a/crates/oxc_ast/src/generated/ast_builder.rs +++ b/crates/oxc_ast/src/generated/ast_builder.rs @@ -5589,6 +5589,7 @@ impl<'a> AstBuilder<'a> { return_type: return_type.into_in(self.allocator), body: body.into_in(self.allocator), scope_id: Default::default(), + pure: Default::default(), } } @@ -5635,9 +5636,9 @@ impl<'a> AstBuilder<'a> { ) } - /// Build an [`ArrowFunctionExpression`] with `scope_id`. + /// Build an [`ArrowFunctionExpression`] with `scope_id` and `pure`. /// - /// If you want the built node to be allocated in the memory arena, use [`AstBuilder::alloc_arrow_function_expression_with_scope_id`] instead. + /// If you want the built node to be allocated in the memory arena, use [`AstBuilder::alloc_arrow_function_expression_with_scope_id_and_pure`] instead. /// /// ## Parameters /// * `span`: The [`Span`] covering this node @@ -5648,8 +5649,9 @@ impl<'a> AstBuilder<'a> { /// * `return_type` /// * `body`: See `expression` for whether this arrow expression returns an expression. /// * `scope_id` + /// * `pure`: `true` if the function is marked with a `/*#__NO_SIDE_EFFECTS__*/` comment #[inline] - pub fn arrow_function_expression_with_scope_id( + pub fn arrow_function_expression_with_scope_id_and_pure( self, span: Span, expression: bool, @@ -5659,6 +5661,7 @@ impl<'a> AstBuilder<'a> { return_type: T3, body: T4, scope_id: ScopeId, + pure: bool, ) -> ArrowFunctionExpression<'a> where T1: IntoIn<'a, Option>>>, @@ -5675,12 +5678,13 @@ impl<'a> AstBuilder<'a> { return_type: return_type.into_in(self.allocator), body: body.into_in(self.allocator), scope_id: Cell::new(Some(scope_id)), + pure, } } - /// Build an [`ArrowFunctionExpression`] with `scope_id`, and store it in the memory arena. + /// Build an [`ArrowFunctionExpression`] with `scope_id` and `pure`, and store it in the memory arena. /// - /// Returns a [`Box`] containing the newly-allocated node. If you want a stack-allocated node, use [`AstBuilder::arrow_function_expression_with_scope_id`] instead. + /// Returns a [`Box`] containing the newly-allocated node. If you want a stack-allocated node, use [`AstBuilder::arrow_function_expression_with_scope_id_and_pure`] instead. /// /// ## Parameters /// * `span`: The [`Span`] covering this node @@ -5691,8 +5695,9 @@ impl<'a> AstBuilder<'a> { /// * `return_type` /// * `body`: See `expression` for whether this arrow expression returns an expression. /// * `scope_id` + /// * `pure`: `true` if the function is marked with a `/*#__NO_SIDE_EFFECTS__*/` comment #[inline] - pub fn alloc_arrow_function_expression_with_scope_id( + pub fn alloc_arrow_function_expression_with_scope_id_and_pure( self, span: Span, expression: bool, @@ -5702,6 +5707,7 @@ impl<'a> AstBuilder<'a> { return_type: T3, body: T4, scope_id: ScopeId, + pure: bool, ) -> Box<'a, ArrowFunctionExpression<'a>> where T1: IntoIn<'a, Option>>>, @@ -5710,7 +5716,7 @@ impl<'a> AstBuilder<'a> { T4: IntoIn<'a, Box<'a, FunctionBody<'a>>>, { Box::new_in( - self.arrow_function_expression_with_scope_id( + self.arrow_function_expression_with_scope_id_and_pure( span, expression, r#async, @@ -5719,6 +5725,7 @@ impl<'a> AstBuilder<'a> { return_type, body, scope_id, + pure, ), self.allocator, ) diff --git a/crates/oxc_ast/src/generated/derive_clone_in.rs b/crates/oxc_ast/src/generated/derive_clone_in.rs index e8453329e009c..c6c06004127a3 100644 --- a/crates/oxc_ast/src/generated/derive_clone_in.rs +++ b/crates/oxc_ast/src/generated/derive_clone_in.rs @@ -1923,6 +1923,7 @@ impl<'new_alloc> CloneIn<'new_alloc> for ArrowFunctionExpression<'_> { return_type: CloneIn::clone_in(&self.return_type, allocator), body: CloneIn::clone_in(&self.body, allocator), scope_id: Default::default(), + pure: CloneIn::clone_in(&self.pure, allocator), } } } diff --git a/crates/oxc_ast/src/generated/derive_content_eq.rs b/crates/oxc_ast/src/generated/derive_content_eq.rs index 6b90f0d4342f8..ff30c5314d026 100644 --- a/crates/oxc_ast/src/generated/derive_content_eq.rs +++ b/crates/oxc_ast/src/generated/derive_content_eq.rs @@ -1117,6 +1117,7 @@ impl ContentEq for ArrowFunctionExpression<'_> { && ContentEq::content_eq(&self.params, &other.params) && ContentEq::content_eq(&self.return_type, &other.return_type) && ContentEq::content_eq(&self.body, &other.body) + && ContentEq::content_eq(&self.pure, &other.pure) } } diff --git a/crates/oxc_codegen/src/comment.rs b/crates/oxc_codegen/src/comment.rs index dc43723f0b154..93a75c0708a19 100644 --- a/crates/oxc_codegen/src/comment.rs +++ b/crates/oxc_codegen/src/comment.rs @@ -18,19 +18,10 @@ impl Codegen<'_> { self.comments.contains_key(&start) } - pub(crate) fn has_annotation_comment(&self, start: u32) -> bool { - if !self.options.print_annotation_comments() { - return false; - } - self.comments.get(&start).is_some_and(|comments| { - comments.iter().any(|comment| self.is_annotation_comment(comment)) - }) - } - pub(crate) fn has_non_annotation_comment(&self, start: u32) -> bool { if self.options.print_annotation_comments() { self.comments.get(&start).is_some_and(|comments| { - comments.iter().any(|comment| !self.is_annotation_comment(comment)) + comments.iter().any(|comment| !self.is_pure_comment(comment)) }) } else { self.has_comment(start) @@ -40,7 +31,7 @@ impl Codegen<'_> { /// `#__PURE__` Notation Specification /// /// - fn is_annotation_comment(&self, comment: &Comment) -> bool { + fn is_pure_comment(&self, comment: &Comment) -> bool { let s = comment.content_span().source_text(self.source_text).trim_start(); if let Some(s) = s.strip_prefix(['@', '#']) { s.starts_with("__PURE__") || s.starts_with("__NO_SIDE_EFFECTS__") @@ -52,8 +43,7 @@ impl Codegen<'_> { /// Whether to keep leading comments. fn is_leading_comments(&self, comment: &Comment) -> bool { comment.preceded_by_newline - && (comment.is_jsdoc(self.source_text) - || (comment.is_line() && self.is_annotation_comment(comment))) + && (comment.is_jsdoc(self.source_text) && !self.is_pure_comment(comment)) && !comment.content_span().source_text(self.source_text).chars().all(|c| c == '*') // webpack comment `/*****/` } @@ -118,36 +108,11 @@ impl Codegen<'_> { } } - pub(crate) fn print_annotation_comments(&mut self, node_start: u32) { - if !self.options.print_annotation_comments() { - return; - } - - // If there is has annotation comments awaiting move to here, print them. - let start = self.start_of_annotation_comment.take().unwrap_or(node_start); - - let Some(comments) = self.comments.remove(&start) else { return }; - - for comment in comments { - if !self.is_annotation_comment(&comment) { - continue; - } - if comment.is_line() { - self.print_str("/*"); - self.print_str(comment.content_span().source_text(self.source_text)); - self.print_str("*/"); - } else { - self.print_str(comment.span.source_text(self.source_text)); - } - self.print_hard_space(); - } - } - pub(crate) fn print_expr_comments(&mut self, start: u32) -> bool { let Some(comments) = self.comments.remove(&start) else { return false }; let (annotation_comments, comments): (Vec<_>, Vec<_>) = - comments.into_iter().partition(|comment| self.is_annotation_comment(comment)); + comments.into_iter().partition(|comment| self.is_pure_comment(comment)); if !annotation_comments.is_empty() { self.comments.insert(start, annotation_comments); diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index 403f74c3b4e26..5a743258f5563 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -13,6 +13,10 @@ use crate::{ binary_expr_visitor::{BinaryExpressionVisitor, Binaryish, BinaryishOperator}, }; +const PURE_COMMENT: &str = "/* @__PURE__ */ "; +const NO_SIDE_EFFECTS_NEW_LINE_COMMENT: &str = "/* @__NO_SIDE_EFFECTS__ */\n"; +const NO_SIDE_EFFECTS_COMMENT: &str = "/* @__NO_SIDE_EFFECTS__ */ "; + /// Generate source code for an AST node. pub trait Gen: GetSpan { /// Generate code for an AST node. @@ -184,10 +188,20 @@ impl Gen for Statement<'_> { } Self::ExportDefaultDeclaration(decl) => { p.print_statement_comments(decl.span.start); + if let ExportDefaultDeclarationKind::FunctionDeclaration(func) = &decl.declaration { + if func.pure && p.options.print_annotation_comments() { + p.print_str(NO_SIDE_EFFECTS_NEW_LINE_COMMENT); + } + } decl.print(p, ctx); } Self::ExportNamedDeclaration(decl) => { p.print_statement_comments(decl.span.start); + if let Some(Declaration::FunctionDeclaration(func)) = &decl.declaration { + if func.pure && p.options.print_annotation_comments() { + p.print_str(NO_SIDE_EFFECTS_NEW_LINE_COMMENT); + } + } decl.print(p, ctx); } Self::TSExportAssignment(decl) => { @@ -206,6 +220,9 @@ impl Gen for Statement<'_> { } Self::FunctionDeclaration(decl) => { p.print_statement_comments(decl.span.start); + if decl.pure && p.options.print_annotation_comments() { + p.print_str(NO_SIDE_EFFECTS_NEW_LINE_COMMENT); + } p.print_indent(); decl.print(p, ctx); p.print_soft_newline(); @@ -689,15 +706,6 @@ impl Gen for VariableDeclaration<'_> { p.print_str("declare "); } - if p.options.print_annotation_comments() - && p.start_of_annotation_comment.is_none() - && matches!(self.kind, VariableDeclarationKind::Const) - && matches!(self.declarations.first(), Some(VariableDeclarator { init: Some(init), .. }) if init.is_function()) - && p.has_annotation_comment(self.span.start) - { - p.start_of_annotation_comment = Some(self.span.start); - } - p.print_str(match self.kind { VariableDeclarationKind::Const => "const", VariableDeclarationKind::Let => "let", @@ -730,7 +738,6 @@ impl Gen for VariableDeclarator<'_> { p.print_soft_space(); p.print_equal(); p.print_soft_space(); - p.print_annotation_comments(self.span.start); init.print_expr(p, Precedence::Comma, ctx); } } @@ -740,7 +747,6 @@ impl Gen for Function<'_> { fn r#gen(&self, p: &mut Codegen, ctx: Context) { let n = p.code_len(); let wrap = self.is_expression() && (p.start_of_stmt == n || p.start_of_default_export == n); - p.print_annotation_comments(self.span.start); p.wrap(wrap, |p| { p.print_space_before_identifier(); p.add_source_mapping(self.span); @@ -999,24 +1005,6 @@ impl Gen for ExportNamedDeclaration<'_> { fn r#gen(&self, p: &mut Codegen, ctx: Context) { p.add_source_mapping(self.span); p.print_indent(); - - if p.options.print_annotation_comments() { - match &self.declaration { - Some(Declaration::FunctionDeclaration(_)) => { - p.print_annotation_comments(self.span.start); - } - Some(Declaration::VariableDeclaration(var_decl)) - if matches!(var_decl.kind, VariableDeclarationKind::Const) => - { - if matches!(var_decl.declarations.first(), Some(VariableDeclarator { init: Some(init), .. }) if init.is_function()) - && p.has_annotation_comment(self.span.start) - { - p.start_of_annotation_comment = Some(self.span.start); - } - } - _ => {} - }; - } p.print_str("export"); if self.export_kind.is_type() { p.print_str(" type "); @@ -1161,8 +1149,8 @@ impl Gen for ExportDefaultDeclaration<'_> { impl Gen for ExportDefaultDeclarationKind<'_> { fn r#gen(&self, p: &mut Codegen, ctx: Context) { match self { - Self::FunctionDeclaration(fun) => { - fun.print(p, ctx); + Self::FunctionDeclaration(func) => { + func.print(p, ctx); p.print_soft_newline(); } Self::ClassDeclaration(class) => { @@ -1196,8 +1184,18 @@ impl GenExpr for Expression<'_> { Self::CallExpression(expr) => expr.print_expr(p, precedence, ctx), Self::ArrayExpression(expr) => expr.print(p, ctx), Self::ObjectExpression(expr) => expr.print_expr(p, precedence, ctx), - Self::FunctionExpression(expr) => expr.print(p, ctx), - Self::ArrowFunctionExpression(expr) => expr.print_expr(p, precedence, ctx), + Self::FunctionExpression(func) => { + if func.pure && p.options.print_annotation_comments() { + p.print_str(NO_SIDE_EFFECTS_COMMENT); + } + func.print(p, ctx); + } + Self::ArrowFunctionExpression(func) => { + if func.pure && p.options.print_annotation_comments() { + p.print_str(NO_SIDE_EFFECTS_COMMENT); + } + func.print_expr(p, precedence, ctx); + } Self::YieldExpression(expr) => expr.print_expr(p, precedence, ctx), Self::UpdateExpression(expr) => expr.print_expr(p, precedence, ctx), Self::UnaryExpression(expr) => expr.print_expr(p, precedence, ctx), @@ -1431,14 +1429,14 @@ 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); - let pure = p.options.print_annotation_comments() && self.pure; + let pure = self.pure && p.options.print_annotation_comments(); if precedence >= Precedence::Postfix && pure { wrap = true; } p.wrap(wrap, |p| { if pure { - p.print_str("/* @__PURE__ */ "); + p.print_str(PURE_COMMENT); } if is_export_default { p.start_of_default_export = p.code_len(); @@ -1688,7 +1686,6 @@ impl Gen for PropertyKey<'_> { impl GenExpr for ArrowFunctionExpression<'_> { fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) { p.wrap(precedence >= Precedence::Assign, |p| { - p.print_annotation_comments(self.span.start); if self.r#async { p.print_space_before_identifier(); p.add_source_mapping(self.span); @@ -2202,13 +2199,13 @@ 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(); - let pure = p.options.print_annotation_comments() && self.pure; + let pure = self.pure && p.options.print_annotation_comments(); if precedence >= Precedence::Postfix && pure { wrap = true; } p.wrap(wrap, |p| { if pure { - p.print_str("/* @__PURE__ */ "); + p.print_str(PURE_COMMENT); } p.print_space_before_identifier(); p.add_source_mapping(self.span); @@ -3865,12 +3862,11 @@ impl GenExpr for V8IntrinsicExpression<'_> { 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) { + if precedence >= Precedence::Postfix { wrap = true; } p.wrap(wrap, |p| { - p.print_annotation_comments(self.span.start); if is_export_default { p.start_of_default_export = p.code_len(); } else if is_statement { diff --git a/crates/oxc_codegen/src/lib.rs b/crates/oxc_codegen/src/lib.rs index 41fbc5b5fb9cb..012da904afe5c 100644 --- a/crates/oxc_codegen/src/lib.rs +++ b/crates/oxc_codegen/src/lib.rs @@ -115,19 +115,6 @@ pub struct Codegen<'a> { comments: CommentsMap, legal_comments: Vec, - /// Start of comment that needs to be moved to the before VariableDeclarator - /// - /// For example: - /// ```js - /// /* @__NO_SIDE_EFFECTS__ */ export const a = function() { - /// }, b = 10000; - /// ``` - /// Should be generated as: - /// ```js - /// export const /* @__NO_SIDE_EFFECTS__ */ a = function() { - /// }, b = 10000; - /// ``` - start_of_annotation_comment: Option, sourcemap_builder: Option, } @@ -179,7 +166,6 @@ impl<'a> Codegen<'a> { quote: b'"', print_comments, comments: CommentsMap::default(), - start_of_annotation_comment: None, legal_comments: vec![], sourcemap_builder: None, } diff --git a/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap b/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap index 9735cf3c2c80f..b1b4cb07ab829 100644 --- a/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap +++ b/crates/oxc_codegen/tests/integration/snapshots/pure_comments.snap @@ -15,14 +15,14 @@ x([ ]) ---------- x([ - /* #__NO_SIDE_EFFECTS__ */ function() {}, - /* #__NO_SIDE_EFFECTS__ */ function y() {}, - /* #__NO_SIDE_EFFECTS__ */ function* () {}, - /* #__NO_SIDE_EFFECTS__ */ function* y() {}, - /* #__NO_SIDE_EFFECTS__ */ async function() {}, - /* #__NO_SIDE_EFFECTS__ */ async function y() {}, - /* #__NO_SIDE_EFFECTS__ */ async function* () {}, - /* #__NO_SIDE_EFFECTS__ */ async function* y() {} + /* @__NO_SIDE_EFFECTS__ */ function() {}, + /* @__NO_SIDE_EFFECTS__ */ function y() {}, + /* @__NO_SIDE_EFFECTS__ */ function* () {}, + /* @__NO_SIDE_EFFECTS__ */ function* y() {}, + /* @__NO_SIDE_EFFECTS__ */ async function() {}, + /* @__NO_SIDE_EFFECTS__ */ async function y() {}, + /* @__NO_SIDE_EFFECTS__ */ async function* () {}, + /* @__NO_SIDE_EFFECTS__ */ async function* y() {} ]); ########## 1 @@ -37,12 +37,12 @@ x([ ]) ---------- x([ - /* #__NO_SIDE_EFFECTS__ */ (y) => y, - /* #__NO_SIDE_EFFECTS__ */ () => {}, - /* #__NO_SIDE_EFFECTS__ */ (y) => y, - /* #__NO_SIDE_EFFECTS__ */ async (y) => y, - /* #__NO_SIDE_EFFECTS__ */ async () => {}, - /* #__NO_SIDE_EFFECTS__ */ async (y) => y + /* @__NO_SIDE_EFFECTS__ */ (y) => y, + /* @__NO_SIDE_EFFECTS__ */ () => {}, + /* @__NO_SIDE_EFFECTS__ */ (y) => y, + /* @__NO_SIDE_EFFECTS__ */ async (y) => y, + /* @__NO_SIDE_EFFECTS__ */ async () => {}, + /* @__NO_SIDE_EFFECTS__ */ async (y) => y ]); ########## 2 @@ -57,12 +57,12 @@ x([ ]) ---------- x([ - /* #__NO_SIDE_EFFECTS__ */ (y) => y, - /* #__NO_SIDE_EFFECTS__ */ () => {}, - /* #__NO_SIDE_EFFECTS__ */ (y) => y, - /* #__NO_SIDE_EFFECTS__ */ async (y) => y, - /* #__NO_SIDE_EFFECTS__ */ async () => {}, - /* #__NO_SIDE_EFFECTS__ */ async (y) => y + /* @__NO_SIDE_EFFECTS__ */ (y) => y, + /* @__NO_SIDE_EFFECTS__ */ () => {}, + /* @__NO_SIDE_EFFECTS__ */ (y) => y, + /* @__NO_SIDE_EFFECTS__ */ async (y) => y, + /* @__NO_SIDE_EFFECTS__ */ async () => {}, + /* @__NO_SIDE_EFFECTS__ */ async (y) => y ]); ########## 3 @@ -77,13 +77,13 @@ async function c() {} async function* d() {} ---------- -// #__NO_SIDE_EFFECTS__ +/* @__NO_SIDE_EFFECTS__ */ function a() {} -// #__NO_SIDE_EFFECTS__ +/* @__NO_SIDE_EFFECTS__ */ function* b() {} -// #__NO_SIDE_EFFECTS__ +/* @__NO_SIDE_EFFECTS__ */ async function c() {} -// #__NO_SIDE_EFFECTS__ +/* @__NO_SIDE_EFFECTS__ */ async function* d() {} ########## 4 @@ -98,13 +98,13 @@ async function c() {} async function* d() {} ---------- -// #__NO_SIDE_EFFECTS__ +/* @__NO_SIDE_EFFECTS__ */ function a() {} -// #__NO_SIDE_EFFECTS__ +/* @__NO_SIDE_EFFECTS__ */ function* b() {} -// #__NO_SIDE_EFFECTS__ +/* @__NO_SIDE_EFFECTS__ */ async function c() {} -// #__NO_SIDE_EFFECTS__ +/* @__NO_SIDE_EFFECTS__ */ async function* d() {} ########## 5 @@ -114,10 +114,14 @@ async function* d() {} /* @__NO_SIDE_EFFECTS__ */ export async function c() {} /* @__NO_SIDE_EFFECTS__ */ export async function* d() {} ---------- -/* @__NO_SIDE_EFFECTS__ */ export function a() {} -/* @__NO_SIDE_EFFECTS__ */ export function* b() {} -/* @__NO_SIDE_EFFECTS__ */ export async function c() {} -/* @__NO_SIDE_EFFECTS__ */ export async function* d() {} +/* @__NO_SIDE_EFFECTS__ */ +export function a() {} +/* @__NO_SIDE_EFFECTS__ */ +export function* b() {} +/* @__NO_SIDE_EFFECTS__ */ +export async function c() {} +/* @__NO_SIDE_EFFECTS__ */ +export async function* d() {} ########## 6 /* @__NO_SIDE_EFFECTS__ */ export function a() {} @@ -126,10 +130,14 @@ async function* d() {} /* @__NO_SIDE_EFFECTS__ */ export async function* d() {} ---------- -/* @__NO_SIDE_EFFECTS__ */ export function a() {} -/* @__NO_SIDE_EFFECTS__ */ export function* b() {} -/* @__NO_SIDE_EFFECTS__ */ export async function c() {} -/* @__NO_SIDE_EFFECTS__ */ export async function* d() {} +/* @__NO_SIDE_EFFECTS__ */ +export function a() {} +/* @__NO_SIDE_EFFECTS__ */ +export function* b() {} +/* @__NO_SIDE_EFFECTS__ */ +export async function c() {} +/* @__NO_SIDE_EFFECTS__ */ +export async function* d() {} ########## 7 @@ -141,12 +149,12 @@ async function* d() {} /* #__NO_SIDE_EFFECTS__ */ export const c2 = () => {}, c3 = () => {} ---------- -export var v0 = function() {}, v1 = function() {}; -export let l0 = function() {}, l1 = function() {}; -export const c0 = /* #__NO_SIDE_EFFECTS__ */ function() {}, c1 = function() {}; -export var v2 = () => {}, v3 = () => {}; -export let l2 = () => {}, l3 = () => {}; -export const c2 = /* #__NO_SIDE_EFFECTS__ */ () => {}, c3 = () => {}; +export var v0 = /* @__NO_SIDE_EFFECTS__ */ function() {}, v1 = function() {}; +export let l0 = /* @__NO_SIDE_EFFECTS__ */ function() {}, l1 = function() {}; +export const c0 = /* @__NO_SIDE_EFFECTS__ */ function() {}, c1 = function() {}; +export var v2 = /* @__NO_SIDE_EFFECTS__ */ () => {}, v3 = () => {}; +export let l2 = /* @__NO_SIDE_EFFECTS__ */ () => {}, l3 = () => {}; +export const c2 = /* @__NO_SIDE_EFFECTS__ */ () => {}, c3 = () => {}; ########## 8 @@ -158,12 +166,12 @@ export const c2 = /* #__NO_SIDE_EFFECTS__ */ () => {}, c3 = () => {}; /* #__NO_SIDE_EFFECTS__ */ const c2 = () => {}, c3 = () => {} ---------- -var v0 = function() {}, v1 = function() {}; -let l0 = function() {}, l1 = function() {}; -const c0 = /* #__NO_SIDE_EFFECTS__ */ function() {}, c1 = function() {}; -var v2 = () => {}, v3 = () => {}; -let l2 = () => {}, l3 = () => {}; -const c2 = /* #__NO_SIDE_EFFECTS__ */ () => {}, c3 = () => {}; +var v0 = /* @__NO_SIDE_EFFECTS__ */ function() {}, v1 = function() {}; +let l0 = /* @__NO_SIDE_EFFECTS__ */ function() {}, l1 = function() {}; +const c0 = /* @__NO_SIDE_EFFECTS__ */ function() {}, c1 = function() {}; +var v2 = /* @__NO_SIDE_EFFECTS__ */ () => {}, v3 = () => {}; +let l2 = /* @__NO_SIDE_EFFECTS__ */ () => {}, l3 = () => {}; +const c2 = /* @__NO_SIDE_EFFECTS__ */ () => {}, c3 = () => {}; ########## 9 @@ -255,7 +263,7 @@ const defineSSRCustomElement = /* @__NO_SIDE_EFFECTS__ */ ( }; ---------- -const defineSSRCustomElement = /* #__NO_SIDE_EFFECTS__ */ /* @__NO_SIDE_EFFECTS__ */ (options, extraOptions) => { +const defineSSRCustomElement = /* @__NO_SIDE_EFFECTS__ */ (options, extraOptions) => { return /* @__PURE__ */ defineCustomElement(options, extraOptions, hydrate); }; diff --git a/crates/oxc_codegen/tests/integration/unit.rs b/crates/oxc_codegen/tests/integration/unit.rs index db7cbb3ab11cf..a052b64325fd5 100644 --- a/crates/oxc_codegen/tests/integration/unit.rs +++ b/crates/oxc_codegen/tests/integration/unit.rs @@ -370,7 +370,7 @@ fn pure_comment() { ); 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__ */ function foo() {}\n", "function foo() {}\n"); test("/* @__PURE__ */ (foo());", "/* @__PURE__ */ foo();\n"); test("/* @__PURE__ */ (new Foo());\n", "/* @__PURE__ */ new Foo();\n"); diff --git a/crates/oxc_parser/src/js/expression.rs b/crates/oxc_parser/src/js/expression.rs index 56f860376175d..881aaff4f7bbf 100644 --- a/crates/oxc_parser/src/js/expression.rs +++ b/crates/oxc_parser/src/js/expression.rs @@ -1141,21 +1141,33 @@ impl<'a> ParserImpl<'a> { &mut self, allow_return_type_in_arrow_function: bool, ) -> Result> { + let has_no_side_effects_comment = + self.lexer.trivia_builder.previous_token_has_no_side_effects_comment(); 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(); } // `() => {}`, `(x) => {}` - if let Some(arrow_expr) = self.try_parse_parenthesized_arrow_function_expression( + if let Some(mut arrow_expr) = self.try_parse_parenthesized_arrow_function_expression( allow_return_type_in_arrow_function, )? { + if has_no_side_effects_comment { + if let Expression::ArrowFunctionExpression(func) = &mut arrow_expr { + func.pure = true; + } + } return Ok(arrow_expr); } // `async x => {}` - if let Some(arrow_expr) = self + if let Some(mut arrow_expr) = self .try_parse_async_simple_arrow_function_expression(allow_return_type_in_arrow_function)? { + if has_no_side_effects_comment { + if let Expression::ArrowFunctionExpression(func) = &mut arrow_expr { + func.pure = true; + } + } return Ok(arrow_expr); } @@ -1165,12 +1177,18 @@ impl<'a> ParserImpl<'a> { // `x => {}` if lhs.is_identifier_reference() && kind == Kind::Arrow { - return self.parse_simple_arrow_function_expression( + let mut arrow_expr = self.parse_simple_arrow_function_expression( span, lhs, /* async */ false, allow_return_type_in_arrow_function, - ); + )?; + if has_no_side_effects_comment { + if let Expression::ArrowFunctionExpression(func) = &mut arrow_expr { + func.pure = true; + } + } + return Ok(arrow_expr); } if kind.is_assignment_operator() { @@ -1188,6 +1206,10 @@ impl<'a> ParserImpl<'a> { Self::set_pure_on_call_or_new_expr(&mut expr); } + if has_no_side_effects_comment { + Self::set_pure_on_function_expr(&mut expr); + } + Ok(expr) } @@ -1217,6 +1239,18 @@ impl<'a> ParserImpl<'a> { } } + pub(crate) fn set_pure_on_function_expr(expr: &mut Expression<'a>) { + match expr { + Expression::FunctionExpression(func) => { + func.pure = true; + } + Expression::ArrowFunctionExpression(func) => { + func.pure = true; + } + _ => {} + } + } + fn parse_assignment_expression_recursive( &mut self, span: Span, diff --git a/crates/oxc_parser/src/js/function.rs b/crates/oxc_parser/src/js/function.rs index 3ed1cadae7469..697ded9dcd93c 100644 --- a/crates/oxc_parser/src/js/function.rs +++ b/crates/oxc_parser/src/js/function.rs @@ -203,7 +203,6 @@ impl<'a> ParserImpl<'a> { ))); } } - Ok(Statement::FunctionDeclaration(decl)) } diff --git a/crates/oxc_parser/src/js/statement.rs b/crates/oxc_parser/src/js/statement.rs index c46f4b5dc9092..643de0c725cb1 100644 --- a/crates/oxc_parser/src/js/statement.rs +++ b/crates/oxc_parser/src/js/statement.rs @@ -82,6 +82,9 @@ impl<'a> ParserImpl<'a> { ) -> Result> { let start_span = self.start_span(); + let has_no_side_effects_comment = + self.lexer.trivia_builder.previous_token_has_no_side_effects_comment(); + if self.at(Kind::At) { self.eat_decorators()?; } @@ -90,7 +93,7 @@ impl<'a> ParserImpl<'a> { // 1. plain if check // 2. check current token // 3. peek token - match self.cur_kind() { + let mut stmt = match self.cur_kind() { Kind::LCurly => self.parse_block_statement(), Kind::Semicolon => Ok(self.parse_empty_statement()), Kind::If => self.parse_if_statement(), @@ -130,6 +133,48 @@ impl<'a> ParserImpl<'a> { self.parse_ts_declaration_statement(start_span) } _ => self.parse_expression_or_labeled_statement(), + }?; + + if has_no_side_effects_comment { + Self::set_pure_on_function_stmt(&mut stmt); + } + + Ok(stmt) + } + + fn set_pure_on_function_stmt(stmt: &mut Statement<'a>) { + match stmt { + Statement::FunctionDeclaration(func) => { + func.pure = true; + } + Statement::ExportDefaultDeclaration(decl) => match &mut decl.declaration { + ExportDefaultDeclarationKind::FunctionExpression(func) + | ExportDefaultDeclarationKind::FunctionDeclaration(func) => { + func.pure = true; + } + ExportDefaultDeclarationKind::ArrowFunctionExpression(func) => { + func.pure = true; + } + _ => {} + }, + Statement::ExportNamedDeclaration(decl) => match &mut decl.declaration { + Some(Declaration::FunctionDeclaration(func)) => { + func.pure = true; + } + Some(Declaration::VariableDeclaration(var_decl)) => { + if let Some(Some(expr)) = var_decl.declarations.first_mut().map(|d| &mut d.init) + { + Self::set_pure_on_function_expr(expr); + } + } + _ => {} + }, + Statement::VariableDeclaration(var_decl) => { + if let Some(Some(expr)) = var_decl.declarations.first_mut().map(|d| &mut d.init) { + Self::set_pure_on_function_expr(expr); + } + } + _ => {} } } diff --git a/crates/oxc_parser/src/lexer/mod.rs b/crates/oxc_parser/src/lexer/mod.rs index fb51caba5ce82..cd67a2c7b3300 100644 --- a/crates/oxc_parser/src/lexer/mod.rs +++ b/crates/oxc_parser/src/lexer/mod.rs @@ -313,6 +313,7 @@ impl<'a> Lexer<'a> { /// Whitespace and line terminators are skipped fn read_next_token(&mut self) -> Kind { self.trivia_builder.has_pure_comment = false; + self.trivia_builder.has_no_side_effects_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 93996a6c4c344..b6f55cc5290c3 100644 --- a/crates/oxc_parser/src/lexer/trivia_builder.rs +++ b/crates/oxc_parser/src/lexer/trivia_builder.rs @@ -23,6 +23,8 @@ pub struct TriviaBuilder { previous_kind: Kind, pub(super) has_pure_comment: bool, + + pub(super) has_no_side_effects_comment: bool, } impl Default for TriviaBuilder { @@ -34,6 +36,7 @@ impl Default for TriviaBuilder { saw_newline: true, previous_kind: Kind::Undetermined, has_pure_comment: false, + has_no_side_effects_comment: false, } } } @@ -43,6 +46,10 @@ impl TriviaBuilder { self.has_pure_comment } + pub fn previous_token_has_no_side_effects_comment(&self) -> bool { + self.has_no_side_effects_comment + } + pub fn add_irregular_whitespace(&mut self, start: u32, end: u32) { self.irregular_whitespaces.push(Span::new(start, end)); } @@ -113,9 +120,7 @@ impl TriviaBuilder { } fn add_comment(&mut self, comment: Comment, source_text: &str) { - if comment.is_pure(source_text) { - self.has_pure_comment = true; - } + self.parse_pure_comment(comment, source_text); // 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() { @@ -138,6 +143,26 @@ impl TriviaBuilder { self.comments.push(comment); } + + /// Parse `#__PURE__` and `#__NO_SIDE_EFFECTS__` Notation + /// + /// + #[inline] // inline because code path is hot. + fn parse_pure_comment(&mut self, comment: Comment, source_text: &str) { + let Some(s) = source_text[(comment.span.start + 2) as usize..] + .trim_ascii_start() + .strip_prefix(['@', '#']) + else { + return; + }; + let Some(s) = s.strip_prefix("__") else { return }; + if s.starts_with("PURE__") { + self.has_pure_comment = true; + } + if s.starts_with("NO_SIDE_EFFECTS__") { + self.has_no_side_effects_comment = true; + } + } } #[cfg(test)] diff --git a/crates/oxc_transformer/src/common/arrow_function_converter.rs b/crates/oxc_transformer/src/common/arrow_function_converter.rs index 562e10db2c49b..e078c3429f2a2 100644 --- a/crates/oxc_transformer/src/common/arrow_function_converter.rs +++ b/crates/oxc_transformer/src/common/arrow_function_converter.rs @@ -953,8 +953,8 @@ impl<'a> ArrowFunctionConverter<'a> { ); let statements = ctx.ast.vec1(ctx.ast.statement_expression(SPAN, init)); let body = ctx.ast.function_body(SPAN, ctx.ast.vec(), statements); - let init = ctx.ast.alloc_arrow_function_expression_with_scope_id( - SPAN, true, false, NONE, params, NONE, body, scope_id, + let init = ctx.ast.alloc_arrow_function_expression_with_scope_id_and_pure( + SPAN, true, false, NONE, params, NONE, body, scope_id, false, ); ctx.ast.variable_declarator( SPAN, diff --git a/crates/oxc_transformer/src/es2020/nullish_coalescing_operator.rs b/crates/oxc_transformer/src/es2020/nullish_coalescing_operator.rs index 7853ea1f5a41e..71d32c2e77a9e 100644 --- a/crates/oxc_transformer/src/es2020/nullish_coalescing_operator.rs +++ b/crates/oxc_transformer/src/es2020/nullish_coalescing_operator.rs @@ -158,7 +158,7 @@ impl<'a> NullishCoalescingOperator<'a, '_> { ctx.ast.vec1(ctx.ast.statement_expression(SPAN, new_expr)), ); let arrow_function = Expression::ArrowFunctionExpression( - ctx.ast.alloc_arrow_function_expression_with_scope_id( + ctx.ast.alloc_arrow_function_expression_with_scope_id_and_pure( SPAN, true, false, @@ -167,6 +167,7 @@ impl<'a> NullishCoalescingOperator<'a, '_> { NONE, body, current_scope_id, + false, ), ); // `(x) => x;` -> `((x) => x)();` diff --git a/crates/oxc_transformer/src/es2022/class_properties/constructor.rs b/crates/oxc_transformer/src/es2022/class_properties/constructor.rs index 37d5e679a1348..85dc9b64e860b 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/constructor.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/constructor.rs @@ -325,7 +325,7 @@ impl<'a> ClassProperties<'a, '_> { // `(..._args) => (super(..._args), , this)` let super_func = Expression::ArrowFunctionExpression( - ctx.ast.alloc_arrow_function_expression_with_scope_id( + ctx.ast.alloc_arrow_function_expression_with_scope_id_and_pure( SPAN, true, false, @@ -342,6 +342,7 @@ impl<'a> ClassProperties<'a, '_> { NONE, ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), body), super_func_scope_id, + false, ), ); diff --git a/crates/oxc_transformer/src/plugins/module_runner_transform.rs b/crates/oxc_transformer/src/plugins/module_runner_transform.rs index 47a37248143ca..d85ece64dcf37 100644 --- a/crates/oxc_transformer/src/plugins/module_runner_transform.rs +++ b/crates/oxc_transformer/src/plugins/module_runner_transform.rs @@ -1996,7 +1996,12 @@ console.log(bar);", let expected = " - const __vite_ssr_import_0__ = await __vite_ssr_import__('./f', {importedNames:['f']}); + const __vite_ssr_import_0__ = await __vite_ssr_import__( + './f', + /*;;*/ + /*;;*/ + { importedNames: ['f'] } + ); let x = 0; diff --git a/crates/oxc_transformer/src/utils/ast_builder.rs b/crates/oxc_transformer/src/utils/ast_builder.rs index 4945f169b0b83..1dcfd7da1cb79 100644 --- a/crates/oxc_transformer/src/utils/ast_builder.rs +++ b/crates/oxc_transformer/src/utils/ast_builder.rs @@ -65,10 +65,11 @@ pub fn wrap_statements_in_arrow_function_iife<'a>( let kind = FormalParameterKind::ArrowFormalParameters; let params = ctx.ast.alloc_formal_parameters(SPAN, kind, ctx.ast.vec(), NONE); let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), stmts); - let arrow = - Expression::ArrowFunctionExpression(ctx.ast.alloc_arrow_function_expression_with_scope_id( - SPAN, false, false, NONE, params, NONE, body, scope_id, - )); + let arrow = Expression::ArrowFunctionExpression( + ctx.ast.alloc_arrow_function_expression_with_scope_id_and_pure( + SPAN, false, false, NONE, params, NONE, body, scope_id, false, + ), + ); ctx.ast.expression_call(span, arrow, NONE, ctx.ast.vec(), false) } diff --git a/crates/oxc_traverse/src/generated/ancestor.rs b/crates/oxc_traverse/src/generated/ancestor.rs index 85ab216ee9b71..e0a16a82f7fe2 100644 --- a/crates/oxc_traverse/src/generated/ancestor.rs +++ b/crates/oxc_traverse/src/generated/ancestor.rs @@ -7393,6 +7393,8 @@ pub(crate) const OFFSET_ARROW_FUNCTION_EXPRESSION_BODY: usize = offset_of!(ArrowFunctionExpression, body); pub(crate) const OFFSET_ARROW_FUNCTION_EXPRESSION_SCOPE_ID: usize = offset_of!(ArrowFunctionExpression, scope_id); +pub(crate) const OFFSET_ARROW_FUNCTION_EXPRESSION_PURE: usize = + offset_of!(ArrowFunctionExpression, pure); #[repr(transparent)] #[derive(Clone, Copy, Debug)] @@ -7455,6 +7457,13 @@ impl<'a, 't> ArrowFunctionExpressionWithoutTypeParameters<'a, 't> { as *const Cell>) } } + + #[inline] + pub fn pure(self) -> &'t bool { + unsafe { + &*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PURE) as *const bool) + } + } } impl<'a, 't> GetAddress for ArrowFunctionExpressionWithoutTypeParameters<'a, 't> { @@ -7525,6 +7534,13 @@ impl<'a, 't> ArrowFunctionExpressionWithoutParams<'a, 't> { as *const Cell>) } } + + #[inline] + pub fn pure(self) -> &'t bool { + unsafe { + &*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PURE) as *const bool) + } + } } impl<'a, 't> GetAddress for ArrowFunctionExpressionWithoutParams<'a, 't> { @@ -7595,6 +7611,13 @@ impl<'a, 't> ArrowFunctionExpressionWithoutReturnType<'a, 't> { as *const Cell>) } } + + #[inline] + pub fn pure(self) -> &'t bool { + unsafe { + &*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PURE) as *const bool) + } + } } impl<'a, 't> GetAddress for ArrowFunctionExpressionWithoutReturnType<'a, 't> { @@ -7665,6 +7688,13 @@ impl<'a, 't> ArrowFunctionExpressionWithoutBody<'a, 't> { as *const Cell>) } } + + #[inline] + pub fn pure(self) -> &'t bool { + unsafe { + &*((self.0 as *const u8).add(OFFSET_ARROW_FUNCTION_EXPRESSION_PURE) as *const bool) + } + } } impl<'a, 't> GetAddress for ArrowFunctionExpressionWithoutBody<'a, 't> {