Files
oxc-project__oxc/crates/oxc_codegen/tests/integration/comments.rs
Dunqing 8ab883afaa fix(codegen): preserve property key annotations (#25766)
`@__KEY__` and `#__KEY__` are currently treated as normal comments. Codegen can drop them before a later property-mangling pass. The property may then be renamed while the annotated string keeps its old name.

This classifies the marker as a property-key annotation. Codegen keeps it next to string and no-substitution template literals when annotation comments are enabled, and the property mangler reads that classification instead of scanning source text. Final output still follows the normal comment options, so whitespace-minified or annotation-disabled output removes the marker.

## Example

Input:

```js
const object = { _field: 1 };
const key = /* #__KEY__ */ "_field";
object[key];
```

Before, an intermediate codegen pass could remove the marker:

```js
const object = { _field: 1 };
const key = "_field";
object[key];
```

After, the marker survives for the later property-mangling pass.

Verified with the affected crate suites, `just minsize`, workspace lint, docs, AST generation, and independent review. `just ready` is only blocked locally by existing Node-version and terminal-color snapshot differences.

Implemented with AI assistance (OpenAI Codex). I reviewed and understand the changes and remain responsible for this contribution under the repository AI usage policy.
2026-08-17 09:23:04 +00:00

1014 lines
35 KiB
Rust

use crate::{
test_idempotency, test_idempotency_options,
tester::{test, test_same},
};
// A leading comment inside a `pife` arrow alternate of a `?:` must stay
// inside the paren wrap on every codegen pass; otherwise the parser re-
// anchors the shifted comment and the next pass drops it.
#[test]
fn test_comment_inside_pife_arrow_alternate_of_conditional() {
test(
"export const x = foo ? bar : (\n // explanatory comment\n (a, b) => a + b\n);",
"export const x = foo ? bar : (\n// explanatory comment\n(a, b) => a + b);\n",
);
test_idempotency(
"export const x = foo ? bar : (\n // explanatory comment\n (a, b) => a + b\n);",
);
}
#[test]
fn test_comment_inside_pife_function_alternate_of_conditional() {
test_idempotency(
"export const x = foo ? bar : (\n // explanatory comment\n function(c) { return c }\n);",
);
}
// A line comment between a conditional `:` and a plain (non-pife) alternate must
// be preserved. It was previously treated as a trailing comment of `:` and
// dropped, which broke codegen idempotency once a transform emits
// `? consequent : // comment\nalternate` (e.g. lowered optional chaining).
#[test]
fn test_line_comment_after_conditional_colon() {
test("x = cond ? a : // c\nb;", "x = cond ? a : // c\nb;\n");
test_idempotency("x = cond ? a : // c\nb;");
// Real-world shape: `a?.b() ?? c` with a leading comment, after lowering.
test_idempotency("x = (_a = a) === null || _a === void 0 ? void 0 : // c1\n// c2\n_a.b();");
}
#[test]
fn test_comments_before_expression_operands() {
// https://github.com/oxc-project/oxc/issues/21301
// https://github.com/oxc-project/oxc/issues/24324
test(
"const value = a ?? /* istanbul ignore next */ [];",
"const value = a ?? /* istanbul ignore next */ [];\n",
);
test(
"const value = a && /* istanbul ignore next */ otherValue;",
"const value = a && /* istanbul ignore next */ otherValue;\n",
);
test(
"const value = a ? /* istanbul ignore next */ first : second;",
"const value = a ? /* istanbul ignore next */ first : second;\n",
);
test(
"condition ?\n /* v8 ignore next */ uncovered() :\n coveredAlternate();",
"condition ? \n/* v8 ignore next */ uncovered() : coveredAlternate();\n",
);
test(
"const value = { aFunction: /* istanbul ignore next */ () => {} };",
"const value = { aFunction: /* istanbul ignore next */ () => {} };\n",
);
}
#[test]
fn test_comments_before_expression_operands_idempotency() {
test_idempotency("const value = a ?? /* istanbul ignore next */ [];");
test_idempotency("const value = a && /* istanbul ignore next */ otherValue;");
test_idempotency("const value = a ? /* istanbul ignore next */ first : second;");
test_idempotency("condition ?\n /* v8 ignore next */ uncovered() :\n coveredAlternate();");
test_idempotency("const value = { aFunction: /* istanbul ignore next */ () => {} };");
}
#[test]
fn test_property_key_annotations_before_literals() {
use oxc_codegen::{CodegenOptions, CommentOptions};
let annotation_only = CodegenOptions {
comments: CommentOptions { normal: false, ..CommentOptions::default() },
..CodegenOptions::default()
};
let source = r#"const key = /* @__KEY__ */ "_field";
const lineKey = // #__KEY__
"_field";
object[/* #__KEY__ */ `_field`];"#;
crate::tester::test_options(
source,
r#"const key = /* @__KEY__ */ "_field";
const lineKey = // #__KEY__
"_field";
object[/* #__KEY__ */ `_field`];
"#,
annotation_only.clone(),
);
crate::tester::test_options(
"use(/* @__KEY__ */ \"_field\");",
"use(\n\t/* @__KEY__ */\n\t\"_field\"\n);\n",
annotation_only.clone(),
);
let minify_with_annotations = CodegenOptions { minify: true, ..annotation_only.clone() };
crate::tester::test_options(
"(/* @__KEY__ */ \"_field\");",
"/* @__KEY__ */`_field`;",
minify_with_annotations.clone(),
);
test_idempotency_options(source, &annotation_only);
test_idempotency_options("(/* @__KEY__ */ \"_field\");", &minify_with_annotations);
crate::tester::test_options(
"const key = /* @__KEY__ */ \"_field\";",
"const key = \"_field\";\n",
CodegenOptions {
comments: CommentOptions { annotation: false, ..CommentOptions::default() },
..CodegenOptions::default()
},
);
}
// A mid-line comment group must not receive a full indent: `print_comments`
// used to inject `indent` tabs before any group whose first comment was not
// preceded by a newline, so indented emission sites (`key: /** c */ value`)
// grew indentation on every codegen pass (monitor-oxc caught this on
// webpack's `source: /** @type {string} */ (source)` shorthand collapse).
#[test]
fn test_comment_before_indented_object_property_value() {
test(
"function f(source) {\n\treturn {\n\t\tsource: /** @type {string} */ (source),\n\t\tother: 1\n\t};\n}",
"function f(source) {\n\treturn {\n\t\t/** @type {string} */ source,\n\t\tother: 1\n\t};\n}\n",
);
test_idempotency(
"function f(source) {\n\treturn {\n\t\tsource: /** @type {string} */ (source),\n\t\tother: 1\n\t};\n}",
);
}
// A comment group ending in a newline before a mid-expression operand must
// re-indent the operand: pass 1 otherwise leaves it at column 0 while the
// reparse anchors the group to the item and indents it (monitor-oxc dce
// caught this on storybook's `argument: /** @type {Expression} */ (argument)`
// with the group on its own lines).
#[test]
fn test_newline_comment_group_before_object_property_value() {
test(
"function f(argument) {\n\treturn {\n\t\ttype: 1,\n\t\targument:\n\t\t// c1\n\t\t/** @type {Expression} */\n\t\t(argument)\n\t};\n}",
"function f(argument) {\n\treturn {\n\t\ttype: 1,\n\t\t// c1\n\t\t/** @type {Expression} */\n\t\targument\n\t};\n}\n",
);
test_idempotency(
"function f(argument) {\n\treturn {\n\t\ttype: 1,\n\t\targument:\n\t\t// c1\n\t\t/** @type {Expression} */\n\t\t(argument)\n\t};\n}",
);
}
// An annotation before a parenthesized logical RHS anchors to the `(`, which
// `Binaryish::right()` strips — the lookup must check the unstripped span too.
// This is the reparse shape of a lowered optional chain (monitor-oxc
// transformer caught it on `a?.x || /* istanbul ignore next */ a?.y`).
#[test]
fn test_annotation_before_parenthesized_logical_rhs() {
test(
"x = a || /* istanbul ignore next */ (b ? c : d);",
"x = a || /* istanbul ignore next */ (b ? c : d);\n",
);
test_idempotency("x = a || /* istanbul ignore next */ (b ? c : d);");
// The comment can also anchor INSIDE the parens — the helpers probe
// every parenthesized layer, at all emission sites. It hoists before the
// `(`, where the reparse re-anchors it, so the rendering is stable.
test(
"x = a || (/* istanbul ignore next */ b ? c : d);",
"x = a || /* istanbul ignore next */ (b ? c : d);\n",
);
test_idempotency("x = a || (/* istanbul ignore next */ b ? c : d);");
test("y = { k: (/* lingui */ v) };", "y = { k: /* lingui */ v };\n");
test_idempotency("y = { k: (/* lingui */ v) };");
}
// In minify mode (comments kept, whitespace removed) a comment group glues to
// the next token with no space: `print_indent` never consumes the pending
// indent-as-space under minify, so an emitting consumer would make spacing
// depend on which printer consumed the group (`*/ x` vs `*/x`) — monitor-oxc
// whitespace caught this on webpack's `/** @type {string} */ (source)`.
#[test]
fn test_minify_comment_glue_idempotency() {
use oxc_codegen::CodegenOptions;
let minify_with_comments = CodegenOptions { minify: true, ..CodegenOptions::default() };
crate::tester::test_options(
"const x = { source: /** @type {string} */ (source), other: 1 };",
"const x={/** @type {string} */source,other:1};",
minify_with_comments.clone(),
);
test_idempotency_options(
"const x = { source: /** @type {string} */ (source), other: 1 };",
&minify_with_comments,
);
}
// Normal-comment groups must NOT be printed before a logical RHS: the minifier
// merges statements into logical right-hand sides (`if(a)x;if(b)x;` ->
// `if(a||(b,..))x`), which can anchor a removed statement's banner comments at
// the RHS span start; printing them mid-expression breaks minify idempotency
// (minifier_test262 `language/asi/S7.9_A5.8_T1.js`). Only annotation-bearing
// groups (coverage directives etc.) are printed.
#[test]
fn test_normal_comment_before_logical_rhs_not_printed() {
test("const value = a ?? /* plain comment */ [];", "const value = a ?? [];\n");
test("const value = a || //\n////////\n(b, c);", "const value = a || (b, c);\n");
}
#[test]
fn test_comment_before_template_literal_interpolation() {
// https://github.com/oxc-project/oxc/issues/22049
test(
"const value = `color: ${/* v8 ignore next -- @preserve */ ({ theme }) => theme.color}`;",
"const value = `color: ${/* v8 ignore next -- @preserve */ ({ theme }) => theme.color}`;\n",
);
test(
"const value = `color: ${\n /* v8 ignore next -- @preserve */\n ({ theme }) => theme.color\n};`;",
"const value = `color: ${\n/* v8 ignore next -- @preserve */\n({ theme }) => theme.color};`;\n",
);
test_idempotency(
"const value = `color: ${/* v8 ignore next -- @preserve */ ({ theme }) => theme.color}`;",
);
test_idempotency(
"const value = `color: ${\n /* v8 ignore next -- @preserve */\n ({ theme }) => theme.color\n};`;",
);
}
#[test]
fn test_comment_at_top_of_file() {
use oxc_allocator::Allocator;
use oxc_ast::CommentPosition;
use oxc_codegen::Codegen;
use oxc_parser::Parser;
use oxc_span::SourceType;
let source_type = SourceType::mjs();
let allocator = Allocator::default();
let mut ret = Parser::new(&allocator, "export{} /** comment */", source_type).parse();
// Move comment to top of the file.
ret.program.comments[0].attached_to = 0;
ret.program.comments[0].position = CommentPosition::Leading;
let code = Codegen::new().build(&ret.program).code;
assert_eq!(code, "/** comment */ export {};\n");
}
#[test]
fn unit() {
test_same("<div>{/* Hello */}</div>;\n");
// A comment-only JSX expression container must not leak a leading space onto
// the following statement's indent.
test_same("x = <div>{/* Hello */}</div>;\ny = 1;\n");
// https://github.com/oxc-project/oxc/issues/17266
test("console.log(<div x={/*before*/ x} />)", "console.log(<div x={/*before*/ x} />);\n");
test(
"console.log(<div x={/*before*/ \"y\"} />)",
"console.log(<div x={/*before*/ \"y\"} />);\n",
);
test("console.log(<div x={/*before*/ true} />)", "console.log(<div x={/*before*/ true} />);\n");
test("console.log(<div {/*before*/ ...x} />)", "console.log(<div {/*before*/ ...x} />);\n");
test("console.log(<div>{/*before*/ x}</div>)", "console.log(<div>{/*before*/ x}</div>);\n");
test("console.log(<>{/*before*/ x}</>)", "console.log(<>{/*before*/ x}</>);\n");
// https://lingui.dev/ref/macro#definemessage
test("const message = /*i18n*/{};", "const message = (/*i18n*/ {});\n");
test("function foo() { return /*i18n*/ {} }", "function foo() {\n\treturn (/*i18n*/ {});\n}\n");
test_same("export { /** @deprecated */ parseAst } from \"rolldown/parseAst\";\n");
test_same("export { /** @deprecated */ parseAst };\n");
test_same("export { parseAst as /** @deprecated */ b } from \"rolldown/parseAst\";\n");
test_same("export { parseAst as /** @deprecated */ b };\n");
}
pub mod misc_comments {
use crate::snapshot;
#[test]
fn comment() {
let cases = vec!["/** block1 */ /** block2 */\nfunction foo() {}\n"];
snapshot("misc_comments", &cases);
}
}
pub mod jsdoc {
use crate::snapshot;
#[test]
fn comment() {
let cases = vec![
r"
/**
* Top level
*
* @module
*/
/** This is a description of the foo function. */
function foo() {
}
/**
* Preserve newline
*/
/**
* Represents a book.
* @constructor
* @param {string} title - The title of the book.
* @param {string} author - The author of the book.
*/
function Book(title, author) {
}
/** Class representing a point. */
class Point {
/**
* Preserve newline
*/
/**
* Create a point.
* @param {number} x - The x value.
* @param {number} y - The y value.
*/
constructor(x, y) {
}
/**
* Get the x value.
* @return {number} The x value.
*/
getX() {
}
/**
* Get the y value.
* @return {number} The y value.
*/
getY() {
}
/**
* Convert a string containing two comma-separated numbers into a point.
* @param {string} str - The string containing two comma-separated numbers.
* @return {Point} A Point object.
*/
static fromString(str) {
}
}
/** Class representing a point. */
const Point = class {
}
/**
* Shirt module.
* @module my/shirt
*/
/** Button the shirt. */
exports.button = function() {
};
/** Unbutton the shirt. */
exports.unbutton = function() {
};
this.Book = function(title) {
/** The title of the book. */
this.title = title;
}
// https://github.com/oxc-project/oxc/issues/6006
export enum DefinitionKind {
/**
* Definition is a referenced variable.
*
* @example defineSomething(foo)
*/
Reference = 'Reference',
/**
* Definition is a `ObjectExpression`.
*
* @example defineSomething({ ... })
*/
Object = 'Object',
/**
* Definition is TypeScript interface.
*
* @example defineSomething<{ ... }>()
*/
TS = 'TS',
}
export type TSTypeLiteral = {
/**
* Comment
*/
foo: string
}
",
];
snapshot("jsdoc", &cases);
}
}
pub mod coverage {
use oxc_allocator::Allocator;
use oxc_codegen::{Codegen, CodegenOptions, CommentOptions};
use oxc_parser::Parser;
use oxc_span::SourceType;
use crate::snapshot;
fn codegen_after_removing_first_statement(source_text: &str) -> String {
codegen_after_removing_first_statement_with_options(source_text, CodegenOptions::default())
}
fn codegen_after_removing_first_statement_with_options(
source_text: &str,
options: CodegenOptions,
) -> String {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::ts()).parse();
assert!(ret.diagnostics.is_empty());
let mut program = ret.program;
program.body.remove(0);
Codegen::new().with_options(options).build(&program).code
}
#[test]
fn preserve_file_coverage_comment_when_anchor_is_removed() {
// https://github.com/oxc-project/oxc/issues/23667
for comment in [
"/* v8 ignore file */",
"// v8 ignore file",
"/* v8 ignore file -- @preserve */",
"/* istanbul ignore file */",
"// istanbul ignore file -- generated",
] {
let source_text =
format!("{comment}\nimport type {{ Foo }} from './types';\nexport default {{}};");
assert_eq!(
codegen_after_removing_first_statement(&source_text),
format!("{comment}\nexport default {{}};\n")
);
}
}
#[test]
fn preserve_file_coverage_comment_when_only_anchor_is_removed() {
for comment in ["/* v8 ignore file */", "// istanbul ignore file -- generated"] {
let source_text = format!("{comment}\nconst removed = 1;");
assert_eq!(
codegen_after_removing_first_statement(&source_text),
format!("{comment}\n")
);
}
}
#[test]
fn preserve_file_coverage_comment_after_hashbang_when_anchor_is_removed() {
let source_text =
"#!/usr/bin/env node\n/* v8 ignore file */\nconst removed = 1;\nsurvivor();";
assert_eq!(
codegen_after_removing_first_statement(source_text),
"#!/usr/bin/env node\n/* v8 ignore file */\nsurvivor();\n"
);
}
#[test]
fn respect_disabled_annotation_comments_when_anchor_is_removed() {
let source_text = "/* v8 ignore file */\nconst removed = 1;\nsurvivor();";
let options = CodegenOptions {
comments: CommentOptions { annotation: false, ..CommentOptions::default() },
..CodegenOptions::default()
};
assert_eq!(
codegen_after_removing_first_statement_with_options(source_text, options),
"survivor();\n"
);
}
#[test]
fn do_not_preserve_non_file_coverage_comment_when_anchor_is_removed() {
for comment in [
"/* v8 ignore next */",
"/* v8 ignore filename */",
"/* c8 ignore next */",
"/* istanbul ignore next */",
"/* node:coverage disable */",
] {
let source_text = format!("{comment}\nconst removed = 1;\nsurvivor();");
assert_eq!(codegen_after_removing_first_statement(&source_text), "survivor();\n");
}
}
#[test]
fn comment() {
let cases = vec![
"/* v8 ignore next */ x",
"/* v8 ignore next 2 */ x",
"/* v8 ignore start */ x",
"/* v8 ignore stop */ x",
"/* v8 ignore if */ x",
"/* v8 ignore else */ x",
"/* v8 ignore file */ x",
"/* c8 ignore next */ x",
"/* c8 ignore next 2 */x ",
"/* c8 ignore start */ x",
"/* c8 ignore stop */ x",
"/* node:coverage disable */ x",
"/* node:coverage enable */ x",
"/* node:coverage ignore next */ x",
"/* node:coverage ignore next 2 */ x",
"/* istanbul ignore if */ x",
"/* istanbul ignore else */ x",
"/* istanbul ignore next */ x",
"/* istanbul ignore file */ x",
"try { something(); }
/* istanbul ignore next */
catch(e) {
// should never happen
}
",
// Inline comment between catch param and body
"try { console.log('test'); }
catch (err) /* v8 ignore next */ { console.error(err); }",
// Multiple comments between catch param and body
"try { something(); }
catch (err) /* c8 ignore next */ /* istanbul ignore next */ { handle(err); }",
// Line comment between catch param and body.
// NOTE: Line comments after `)` are classified as trailing comments by the parser,
// so they are not preserved. Use block comments instead.
// See: https://github.com/oxc-project/oxc/pull/16167#discussion_r2567604139
"try { something(); }
catch (err) // v8 ignore next
{ handle(err); }",
// Coverage comment before ConditionalExpression alternate
// https://github.com/oxc-project/oxc/issues/20549
"const a = Math.random() ? 1 : /* istanbul ignore next */ 2;",
// Coverage comment between SwitchStatement cases
// https://github.com/oxc-project/oxc/issues/20549
"switch (Math.random()) {
case 0.5: break;
/* istanbul ignore next */
default: break;
}",
// Coverage comment before ObjectExpression property
// https://github.com/oxc-project/oxc/issues/21302
"const obj = {
a: () => 1,
/* v8 ignore next */
b: () => 2,
}",
// Coverage comment before single ObjectExpression property
"const obj = { /* v8 ignore next */ a: () => 1 }",
// Coverage comment before the first ObjectExpression property
"const obj = {
/* v8 ignore next */
a: () => 1,
b: () => 2,
}",
// Coverage comment before a SpreadElement in ObjectExpression
"const obj = {
a: 1,
/* v8 ignore next */
...rest,
}",
];
snapshot("coverage", &cases);
}
}
pub mod legal {
use oxc_codegen::{CodegenOptions, CommentOptions, LegalComment};
use crate::{codegen_options, snapshot, snapshot_options};
fn cases() -> Vec<&'static str> {
vec![
"/* @license */\n/* @license */\nfoo;bar;",
"/* @license */\n/* @preserve */\nfoo;bar;",
"/* @license */\n//! KEEP\nfoo;bar;",
"/* @license */\n/*! KEEP */\nfoo;bar;",
"/* @license *//*! KEEP */\nfoo;bar;",
"function test() {
/*
* @license
* Copyright notice 2
*/
bar;
}",
"function bar() { var foo; /*! #__NO_SIDE_EFFECTS__ */ function baz() { } }",
"function foo() {
(() => {
/**
* @preserve
*/
})();
/**
* @preserve
*/
}
/**
* @preserve
*/",
"/**
* @preserve
*/
",
// Issue #14953: legal comments above directives
"/*!\n * legal comment\n */\n\n\"use strict\";\n\nexport const foo = 'foo';",
]
}
#[test]
fn legal_inline_comment() {
snapshot("legal_inline_comments", &cases());
}
#[test]
fn legal_eof_comment() {
let options = CodegenOptions {
comments: CommentOptions { legal: LegalComment::Eof, ..CommentOptions::default() },
..CodegenOptions::default()
};
snapshot_options("legal_eof_comments", &cases(), &options);
}
#[test]
fn legal_eof_minify_comment() {
let options = CodegenOptions {
minify: true,
comments: CommentOptions { legal: LegalComment::Eof, ..CommentOptions::default() },
..CodegenOptions::default()
};
snapshot_options("legal_eof_minify_comments", &cases(), &options);
}
#[test]
fn legal_linked_comment() {
let options = CodegenOptions {
comments: CommentOptions {
legal: LegalComment::Linked(String::from("test.js")),
..CommentOptions::default()
},
..CodegenOptions::default()
};
snapshot_options("legal_linked_comments", &cases(), &options);
}
#[test]
fn legal_external_comment() {
let options = CodegenOptions {
comments: CommentOptions { legal: LegalComment::External, ..CommentOptions::default() },
..CodegenOptions::default()
};
let code = "/* @license */\n/* @preserve */\nfoo;\n";
let ret = codegen_options(code, &options);
assert_eq!(ret.code, "foo;\n");
assert_eq!(ret.legal_comments[0].content_span().source_text(code), " @license ");
assert_eq!(ret.legal_comments[1].content_span().source_text(code), " @preserve ");
}
}
pub mod pure {
use crate::snapshot;
#[test]
fn annotate_comment() {
let cases = vec![
r"
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() {},
])",
r"
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),
])",
r"
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),
])",
r"
// #__NO_SIDE_EFFECTS__
function a() {}
// #__NO_SIDE_EFFECTS__
function* b() {}
// #__NO_SIDE_EFFECTS__
async function c() {}
// #__NO_SIDE_EFFECTS__
async function* d() {}
",
r"
// #__NO_SIDE_EFFECTS__
function a() {}
// #__NO_SIDE_EFFECTS__
function* b() {}
// #__NO_SIDE_EFFECTS__
async function c() {}
// #__NO_SIDE_EFFECTS__
async function* d() {}
",
r"
/* @__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() {}",
r"/* @__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() {}
export default /* @__NO_SIDE_EFFECTS__ */ async function() {}
export default /* @__NO_SIDE_EFFECTS__ */ function() {}
",
// Only "c0" and "c2" should have "no side effects" (Rollup only respects "const" and only for the first one)
r"
/* #__NO_SIDE_EFFECTS__ */ export var v0 = function() {}, v1 = function() {}
/* #__NO_SIDE_EFFECTS__ */ export let l0 = function() {}, l1 = function() {}
/* #__NO_SIDE_EFFECTS__ */ export const c0 = function() {}, c1 = function() {}
/* #__NO_SIDE_EFFECTS__ */ export var v2 = () => {}, v3 = () => {}
/* #__NO_SIDE_EFFECTS__ */ export let l2 = () => {}, l3 = () => {}
/* #__NO_SIDE_EFFECTS__ */ export const c2 = () => {}, c3 = () => {}
",
// Only "c0" and "c2" should have "no side effects" (Rollup only respects "const" and only for the first one)
r"
/* #__NO_SIDE_EFFECTS__ */ var v0 = function() {}, v1 = function() {}
/* #__NO_SIDE_EFFECTS__ */ let l0 = function() {}, l1 = function() {}
/* #__NO_SIDE_EFFECTS__ */ const c0 = function() {}, c1 = function() {}
/* #__NO_SIDE_EFFECTS__ */ var v2 = () => {}, v3 = () => {}
/* #__NO_SIDE_EFFECTS__ */ let l2 = () => {}, l3 = () => {}
/* #__NO_SIDE_EFFECTS__ */ const c2 = () => {}, c3 = () => {}
",
r"
isFunction(options)
? // #8326: extend call and options.name access are considered side-effects
// by Rollup, so we have to wrap it in a pure-annotated IIFE.
/*#__PURE__*/ (() =>
extend({ name: options.name }, extraOptions, { setup: options }))()
: options
",
r"isFunction(options) ? /*#__PURE__*/ (() => extend({ name: options.name }, extraOptions, { setup: options }))() : options;
",
r"
const obj = {
props: /*#__PURE__*/ extend({}, TransitionPropsValidators, {
tag: String,
moveClass: String,
}),
};
const p = /*#__PURE__*/ Promise.resolve();
",
r"
const staticCacheMap = /*#__PURE__*/ new WeakMap()
",
r#"
const builtInSymbols = new Set(
/*#__PURE__*/
Object.getOwnPropertyNames(Symbol)
.filter(key => key !== "arguments" && key !== "caller")
)
"#,
"(/* @__PURE__ */ new Foo()).bar();\n",
"(/* @__PURE__ */ Foo()).bar();\n",
"(/* @__PURE__ */ new Foo())['bar']();\n",
"(/* @__PURE__ */ Foo())['bar']();\n",
// https://github.com/oxc-project/oxc/issues/4843
r"
/* #__NO_SIDE_EFFECTS__ */
const defineSSRCustomElement = /* @__NO_SIDE_EFFECTS__ */ (
options,
extraOptions,
) => {
return /* @__PURE__ */ defineCustomElement(options, extraOptions, hydrate);
};
",
// Range leading comments
r"
const defineSSRCustomElement = () => {
return /* @__PURE__ */ /* @__NO_SIDE_EFFECTS__ */ /* #__NO_SIDE_EFFECTS__ */ defineCustomElement(options, extraOptions, hydrate);
};
",
"
const Component = // #__PURE__
React.forwardRef((props, ref) => {});
",
// Copy from <https://github.com/rolldown-rs/rolldown/blob/v0.14.0/crates/rolldown/tests/esbuild/dce/remove_unused_pure_comment_calls/entry.js>
"
function bar() {}
let bare = foo(bar);
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 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);
let new_dot_no = /* @__PURE__ */ new foo(sideEffect()).dot(bar());
let nested_yes = [1, /* @__PURE__ */ foo(bar), 2];
let nested_no = [1, /* @__PURE__ */ foo(bar()), 2];
let new_nested_yes = [1, /* @__PURE__ */ new foo(bar), 2];
let new_nested_no = [1, /* @__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 bad_no = /* __PURE__ */ foo(bar);
let new_bad_no = /* __PURE__ */ new foo(bar);
let parens_no = (/* @__PURE__ */ foo)(bar);
let new_parens_no = new (/* @__PURE__ */ foo)(bar);
let exp_no = /* @__PURE__ */ foo() ** foo();
let new_exp_no = /* @__PURE__ */ new foo() ** foo();
",
"{ /* @__PURE__ */ (function() {})(); }",
"{ /* @__PURE__ */ (() => {})(); }",
"
void /* @__PURE__ */ function() {}();
typeof /* @__PURE__ */ function() {}();
! /* @__PURE__ */ function() {}();
delete /* @__PURE__ */ (() => {})();",
"const Foo = /* @__PURE__ */ (((() => {})()))",
"const Foo = /* @__PURE__ */ (() => { })() as unknown as { new (): any }",
"const Foo = /* @__PURE__ */ (() => {})() satisfies X",
"const Foo = /* @__PURE__ */ (() => {})()<X>",
"const Foo = /* @__PURE__ */ <Foo>(() => {})()!",
"const Foo = /* @__PURE__ */ <Foo>(() => {})()! as X satisfies Y",
// https://github.com/oxc-project/oxc/issues/17670 - annotation before parenthesized arrow function
r"/* @__NO_SIDE_EFFECTS__ */ ((options, extraOptions) => {
return defineCustomElement(options, extraOptions, hydrate);
})",
r"/* @__NO_SIDE_EFFECTS__ */ ((x) => x)",
r"/* @__NO_SIDE_EFFECTS__ */ (function() {})",
r"/* @__NO_SIDE_EFFECTS__ */ (function foo() {})",
];
snapshot("pure_comments", &cases);
}
}
pub mod options {
use oxc_codegen::{CodegenOptions, CommentOptions, LegalComment};
use crate::codegen_options;
#[test]
fn test() {
let code = "
//! Top Legal Comment
function foo() {
/** JSDoc Comment */
function bar() {
/* #__PURE__ */ x();
}
function baz() {
//! Function Legal Comment
}
x(/* Normal Comment */);
x(/** Call Expression Jsdoc Comment */ token);
}";
for normal in [true, false] {
for jsdoc in [true, false] {
for annotation in [true, false] {
for legal in [LegalComment::Inline, LegalComment::Eof, LegalComment::None] {
let options = CodegenOptions {
comments: CommentOptions {
normal,
jsdoc,
annotation,
legal: legal.clone(),
},
..CodegenOptions::default()
};
let printed = codegen_options(code, &options).code;
if normal {
assert!(printed.contains("Normal Comment"));
} else {
assert!(!printed.contains("Normal Comment"));
}
if jsdoc {
assert!(printed.contains("JSDoc Comment"));
assert!(printed.contains("Call Expression Jsdoc Comment"));
} else {
assert!(!printed.contains("JSDoc Comment"));
assert!(!printed.contains("Call Expression Jsdoc Comment"));
}
if annotation {
assert!(printed.contains("__PURE__"));
} else {
assert!(!printed.contains("__PURE__"));
}
if legal.is_none() {
assert!(!printed.contains("Top Legal Comment"));
assert!(!printed.contains("Function Legal Comment"));
} else {
assert!(printed.contains("Top Legal Comment"));
assert!(printed.contains("Function Legal Comment"));
}
}
}
}
}
}
}
#[test]
fn test_pure_comment_on_object_idempotency() {
test_idempotency("export const X = /* @__PURE__ */ { a: 1 };");
}
// Regression for monitor-oxc codegen idempotency:
// inline `/*!*/` between expression operands (parsed as a legal block comment
// because it starts with `!`) used to round-trip non-idempotently — pass 1
// emitted `\t/*!*/ }` (orphan flushed before `}`, trailing-edge converted
// the closing-brace indent into a single space) and pass 2 emitted
// `\t/*!*/}` (the comment now attaches to `}` so `FunctionBody`'s
// `clear_pending_indent_space()` runs). The fix forces orphan flushes to
// land on their own line, matching the pass-2 behaviour on pass 1.
#[test]
fn test_inline_legal_comment_in_function_body_is_idempotent() {
test_idempotency(
"function isPunctuator(ch) {\n\treturn ch === 33/*!*/ || ch === 37/*%*/;\n}\n",
);
}
#[test]
fn test_legal_comment_after_code_minify_with_comments_idempotency() {
use oxc_codegen::{CodegenOptions, CommentOptions};
test_idempotency_options(
"foo();/**
* @license MIT
**//*! #__NO_SIDE_EFFECTS__ */function bar(){}",
&CodegenOptions {
minify: true,
comments: CommentOptions::default(),
source_map_path: Some("test.js".into()),
..CodegenOptions::default()
},
);
}
#[test]
fn test_comment_inside_parenthesized_expression_with_pife_arrow() {
test_idempotency("export const x = foo ? bar : (\n // comment\n (a, b) => a + b\n);");
}
#[test]
fn test_comment_inside_double_parenthesized_pife_arrow() {
test_idempotency("const x = foo ? bar : ( ( ( a ) => a ) );");
}
// A leading comment on the statement which closes the directive prologue was dropped, because the
// paren-protecting path prints that statement itself instead of going through its printer.
#[test]
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");
}