mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
8ab883afaa
`@__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.
1014 lines
35 KiB
Rust
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");
|
|
}
|