mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
ae6c386ed5
## Problem
A `yield` expression in a classic `for` loop initializer can lose the parentheses required around an `in` operator in its argument. This turns valid JavaScript into output that cannot be parsed, in both default and minified code generation.
For example, this input is valid:
```js
function *g(o) {
for (yield (1 in o); false;);
}
```
Previously, codegen emitted:
```js
// Default output
function* g(o) {
for (yield 1 in o; false;);
}
// Minified output
function*g(o){for(yield 1 in o;false;);}
```
The unparenthesized `in` is not allowed at this position in a classic `for` initializer. The same restriction applies to the argument of `yield*`, and can also be lost through nested yields or an assignment inside the yield argument.
## Change
`ForStatement` already prints its initializer with `Context::FORBID_IN`. However, `YieldExpression::gen_expr` ignored its incoming context and always printed its argument with `Context::empty()`.
Preserve `FORBID_IN` when printing the argument of an unwrapped yield expression. This lets the existing expression printers retain the required grouping:
```js
// Default output
function* g(o) {
for (yield (1 in o); false;);
}
// Minified output
function*g(o){for(yield(1 in o);false;);}
```
Only the relevant `FORBID_IN` flag is forwarded; the argument's existing precedence remains `Precedence::Yield`. Both `yield` and `yield*` use this path.
## Parentheses around yield
When precedence requires parentheses around the entire yield expression, those parentheses already allow `in` in the argument. Clear the restriction in that case to avoid redundant inner grouping:
```js
// Input
function *g(o) {
for ((yield (1 in o)) + 1; false;);
}
// Default output
function* g(o) {
for ((yield 1 in o) + 1; false;);
}
// Minified output
function*g(o){for((yield 1 in o)+1;false;);}
```
Outside a restricted initializer, ordinary yield expressions continue to omit unnecessary parentheses: `yield (1 in o)` prints as `yield 1 in o`.
1136 lines
45 KiB
Rust
1136 lines
45 KiB
Rust
use oxc_allocator::Allocator;
|
||
use oxc_ast::{ast::*, builder::AstBuilder};
|
||
use oxc_codegen::{Codegen, CodegenOptions, IndentChar};
|
||
use oxc_span::SPAN;
|
||
|
||
use crate::tester::{
|
||
test, test_minify, test_minify_same, test_options, test_same, test_same_ignore_parse_errors,
|
||
test_unambiguous, test_with_parse_options,
|
||
};
|
||
|
||
#[test]
|
||
fn cases() {
|
||
test_same_ignore_parse_errors("class C {\n\t@foo static accessor A = @bar class {};\n}\n");
|
||
test_same_ignore_parse_errors("function foo(@foo x = @bar class {}) {}\n");
|
||
test_same_ignore_parse_errors("function foo(@foo ...rest) {}\n");
|
||
}
|
||
|
||
#[test]
|
||
fn decl() {
|
||
test_same("const [foo, ...bar] = qux;\n");
|
||
test_same("const { foo, ...bar } = qux;\n");
|
||
test_minify("const [foo] = bar", "const[foo]=bar;");
|
||
test_minify("const {foo} = bar", "const{foo}=bar;");
|
||
test_minify("const foo = bar", "const foo=bar;");
|
||
}
|
||
|
||
#[test]
|
||
fn module_decl() {
|
||
test("export * as foo from 'foo'", "export * as foo from \"foo\";\n");
|
||
test("import x from './foo.js' with {}", "import x from \"./foo.js\" with {};\n");
|
||
test("import {} from './foo.js' with {}", "import {} from \"./foo.js\" with {};\n");
|
||
test("import {} from './a\"b.mjs';", "import {} from \"./a\\\"b.mjs\";\n");
|
||
test("import {} from './a\\nb.mjs';", "import {} from \"./a\\nb.mjs\";\n");
|
||
test("export * from './foo.js' with {}", "export * from \"./foo.js\" with {};\n");
|
||
test(
|
||
"export { default } from './foo.js' with { type: 'json' }",
|
||
"export { default } from \"./foo.js\" with { type: \"json\" };\n",
|
||
);
|
||
test("export {} from './foo.js' with {}", "export {} from \"./foo.js\" with {};\n");
|
||
test_minify("export { '☿' } from 'mod';", "export{\"☿\"}from\"mod\";");
|
||
test_minify("export { '☿' as '☿' } from 'mod';", "export{\"☿\"}from\"mod\";");
|
||
test_minify(
|
||
"import x from './foo.custom' with { 'type': 'json' }",
|
||
"import x from\"./foo.custom\"with{\"type\":\"json\"};",
|
||
);
|
||
test_minify("import {} from './a\"b.mjs';", "import{}from'./a\"b.mjs';");
|
||
test_minify(
|
||
"export { default } from './foo.js' with { type: 'json' }",
|
||
"export{default}from\"./foo.js\"with{type:\"json\"};",
|
||
);
|
||
|
||
test_minify("export default {a:1}", "export default{a:1};");
|
||
test_minify("export default [1]", "export default[1];");
|
||
test_minify("export default (a,b)", "export default(a,b);");
|
||
test_minify("export default 5", "export default 5;");
|
||
test_minify("export default foo", "export default foo;");
|
||
test_minify("export default function(){}", "export default function(){}");
|
||
|
||
// `as` next to a string export/import name needs no space.
|
||
test_minify("export { foo as \"name\" }", "export{foo as\"name\"};");
|
||
test_minify("import { \"y\" as z } from \"m\"", "import{\"y\"as z}from\"m\";");
|
||
test_minify("export * as \"ns\" from \"m\"", "export*as\"ns\" from\"m\";");
|
||
test_minify("export { foo as bar }", "export{foo as bar};");
|
||
}
|
||
|
||
#[test]
|
||
fn quoted_import_names() {
|
||
test(r#"import { "foo" as foo } from "m";"#, "import { foo } from \"m\";\n");
|
||
test(r#"import { "a\u0062" as ab } from "m";"#, "import { ab } from \"m\";\n");
|
||
test(r#"import { "π" as π } from "m";"#, "import { π } from \"m\";\n");
|
||
test(r#"import { "type" as type } from "m";"#, "import { type } from \"m\";\n");
|
||
|
||
test_same("import { \"foo-bar\" as foo } from \"m\";\n");
|
||
test_same("import { \"\" as foo } from \"m\";\n");
|
||
test_same("import { \"default\" as foo } from \"m\";\n");
|
||
test_same("import { \"foo\" as bar } from \"m\";\n");
|
||
test(
|
||
r#"import { "foo" as foo, "bar" as bar, baz } from "m";"#,
|
||
"import { foo, bar, baz } from \"m\";\n",
|
||
);
|
||
test(r#"import { foo as foo, bar } from "m";"#, "import { foo, bar } from \"m\";\n");
|
||
|
||
test_minify(r#"import { "foo" as foo } from "m";"#, r#"import{foo}from"m";"#);
|
||
test_minify(r#"import { "a\u0062" as ab } from "m";"#, r#"import{ab}from"m";"#);
|
||
test_minify(r#"import { foo as foo, bar } from "m";"#, r#"import{foo,bar}from"m";"#);
|
||
}
|
||
|
||
#[test]
|
||
fn export_type() {
|
||
test_same("export type {} from \"mod\";\n");
|
||
test_same("export type { Foo } from \"mod\";\n");
|
||
test_same("export { type Foo, type Bar } from \"mod\";\n");
|
||
}
|
||
|
||
#[test]
|
||
fn expr() {
|
||
test("new (foo()).bar();", "new (foo()).bar();\n");
|
||
test_minify("x in new Error()", "x in new Error;");
|
||
test(
|
||
"new function() { let a = foo?.bar().baz; return a; }();",
|
||
"new function() {\n\tlet a = foo?.bar().baz;\n\treturn a;\n}();\n",
|
||
);
|
||
test(
|
||
"new class { foo() { let a = foo?.bar().baz; return a; } }();",
|
||
"new class {\n\tfoo() {\n\t\tlet a = foo?.bar().baz;\n\t\treturn a;\n\t}\n}();\n",
|
||
);
|
||
|
||
test("1000000000000000128.0.toFixed(0)", "0xde0b6b3a7640080.toFixed(0);\n");
|
||
test_minify("1000000000000000128.0.toFixed(0)", "0xde0b6b3a7640080.toFixed(0);");
|
||
|
||
test_minify("throw 'foo'", "throw`foo`;");
|
||
test_minify("function a() { return 'foo' }", "function a(){return`foo`}");
|
||
test_minify("function a() { return class {} }", "function a(){return class{}}");
|
||
test_minify(
|
||
"function a() { return async function foo() {} }",
|
||
"function a(){return async function foo(){}}",
|
||
);
|
||
test_minify_same("function a(){return super()}");
|
||
test_minify_same("function a(){return new.target}");
|
||
test_minify_same("throw await 1;");
|
||
test_minify_same("await import(``);");
|
||
|
||
test("delete 2e308", "delete (0, Infinity);\n");
|
||
test_minify("delete 2e308", "delete(1/0);");
|
||
|
||
test_minify_same(r#"({"http://a\r\" \n<'b:b@c\r\nd/e?f":{}});"#);
|
||
test_minify_same("new(import(``),function(){});");
|
||
|
||
// A `new` callee containing a call must keep parentheses.
|
||
test_same("new (f())();\n");
|
||
test_same("new (g?.())();\n");
|
||
test_same("new (f.g?.h)();\n");
|
||
test_same("new (f?.g.h)();\n");
|
||
test("new (f().g)();", "new (f()).g();\n");
|
||
test_same("new (f?.().g)();\n");
|
||
test_same("new (f()?.g)();\n");
|
||
test_same("new (f?.()?.g)();\n");
|
||
test("new (f()`g`)();", "new (f())`g`();\n"); // #22961
|
||
test_same("new (import(\"foo\"))();\n");
|
||
test("new (import(\"foo\").bar)();", "new (import(\"foo\")).bar();\n");
|
||
test("new (import(\"foo\")`bar`)();", "new (import(\"foo\"))`bar`();\n");
|
||
test("new (a`b`)();", "new a`b`();\n");
|
||
test("new (f().g.h)();", "new (f()).g.h();\n");
|
||
test("new (f[g()])();", "new f[g()]();\n");
|
||
test("new (new f())();", "new new f()();\n");
|
||
|
||
// The base of `**` must be an UpdateExpression, so a unary/await base keeps parens.
|
||
test_same("(-a) ** b;\n");
|
||
test_same("(typeof a) ** b;\n");
|
||
test_same("(await a) ** b;\n");
|
||
// Only the base needs wrapping; the `**` right operand may be a UnaryExpression.
|
||
test("(await a) ** (await b);", "(await a) ** await b;\n");
|
||
}
|
||
|
||
#[test]
|
||
fn private_in() {
|
||
test(
|
||
"class Foo { #test; bar() { if (!(#test in Foo)) { } } }",
|
||
"class Foo {\n\t#test;\n\tbar() {\n\t\tif (!(#test in Foo)) {}\n\t}\n}\n",
|
||
);
|
||
test(
|
||
"class Foo { #test; bar() { #field in {} << 0 } }",
|
||
"class Foo {\n\t#test;\n\tbar() {\n\t\t#field in {} << 0;\n\t}\n}\n",
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn private_in_binary_right() {
|
||
fn wrap_case_equal_lower_precedence(op: &str) -> (String, String, String) {
|
||
let minified_op =
|
||
if matches!(op, "in" | "instanceof") { format!(" {op} ") } else { op.to_string() };
|
||
(
|
||
format!("class C {{ #x; test(a, b) {{ return #x in (a {op} b); }} }}"),
|
||
format!("class C {{\n\t#x;\n\ttest(a, b) {{\n\t\treturn #x in (a {op} b);\n\t}}\n}}\n"),
|
||
format!("class C{{#x;test(a,b){{return#x in (a{minified_op}b)}}}}"),
|
||
)
|
||
}
|
||
fn wrap_case_higher_precedence(op: &str) -> (String, String, String) {
|
||
(
|
||
format!("class C {{ #x; test(a, b) {{ return #x in (a {op} b); }} }}"),
|
||
format!("class C {{\n\t#x;\n\ttest(a, b) {{\n\t\treturn #x in a {op} b;\n\t}}\n}}\n"),
|
||
format!("class C{{#x;test(a,b){{return#x in a{op}b}}}}"),
|
||
)
|
||
}
|
||
|
||
for (source, expected, expected_minified) in [
|
||
"instanceof",
|
||
"in",
|
||
"<",
|
||
"<=",
|
||
">",
|
||
">=",
|
||
"==",
|
||
"!=",
|
||
"===",
|
||
"!==",
|
||
"&",
|
||
"^",
|
||
"|",
|
||
"&&",
|
||
"||",
|
||
"??",
|
||
"=",
|
||
]
|
||
.map(wrap_case_equal_lower_precedence)
|
||
{
|
||
test(&source, &expected);
|
||
test_minify(&source, &expected_minified);
|
||
}
|
||
|
||
for (source, expected, expected_minified) in
|
||
["<<", ">>", ">>>", "+", "-", "*", "/", "%", "**"].map(wrap_case_higher_precedence)
|
||
{
|
||
test(&source, &expected);
|
||
test_minify(&source, &expected_minified);
|
||
}
|
||
|
||
for (rhs, minified_rhs) in [("#x in a", "#x in a"), ("a ? a : b", "a?a:b"), ("a, b", "a,b")] {
|
||
let source = format!("class C {{ #x; test(a, b) {{ return #x in ({rhs}); }} }}");
|
||
test(
|
||
&source,
|
||
&format!("class C {{\n\t#x;\n\ttest(a, b) {{\n\t\treturn #x in ({rhs});\n\t}}\n}}\n"),
|
||
);
|
||
test_minify(&source, &format!("class C{{#x;test(a,b){{return#x in ({minified_rhs})}}}}"));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn private_in_binary_left() {
|
||
fn wrap_case_equal_higher_precedence(op: &str) -> (String, String, String) {
|
||
(
|
||
format!("class C {{ #x; test(o) {{ return (#x in o) {op} 1; }} }}"),
|
||
format!("class C {{\n\t#x;\n\ttest(o) {{\n\t\treturn (#x in o) {op} 1;\n\t}}\n}}\n"),
|
||
format!("class C{{#x;test(o){{return(#x in o){op}1}}}}"),
|
||
)
|
||
}
|
||
fn wrap_case_equal_lower_precedence(op: &str) -> (String, String, String) {
|
||
(
|
||
format!("class C {{ #x; test(o) {{ return (#x in o) {op} 1; }} }}"),
|
||
format!("class C {{\n\t#x;\n\ttest(o) {{\n\t\treturn #x in o {op} 1;\n\t}}\n}}\n"),
|
||
format!("class C{{#x;test(o){{return#x in o{op}1}}}}"),
|
||
)
|
||
}
|
||
|
||
for (source, expected, expected_minified) in
|
||
["+", "-", "*", "/", "%", "**", "<<", ">>", ">>>"].map(wrap_case_equal_higher_precedence)
|
||
{
|
||
test(&source, &expected);
|
||
test_minify(&source, &expected_minified);
|
||
}
|
||
|
||
for (source, expected, expected_minified) in
|
||
["<", "<=", ">", ">=", "==", "!=", "===", "!==", "&", "^", "|", "&&", "||", "??"]
|
||
.map(wrap_case_equal_lower_precedence)
|
||
{
|
||
test(&source, &expected);
|
||
test_minify(&source, &expected_minified);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn class() {
|
||
test(
|
||
"export default class Foo { @x @y accessor #aDef = 1 }",
|
||
"export default class Foo {\n\t@x @y accessor #aDef = 1;\n}\n",
|
||
);
|
||
test(
|
||
"export class Test2 {\n@decorator\nproperty: ((arg: any) => any) | undefined;\n}",
|
||
"export class Test2 {\n\t@decorator property: ((arg: any) => any) | undefined;\n}\n",
|
||
);
|
||
test_same("export @dec1 @dec2 class C {}\n");
|
||
test_same("export default @dec1 @dec2 class {}\n");
|
||
test_minify("class { static [computed] }", "class{static[computed]}");
|
||
}
|
||
|
||
#[test]
|
||
fn for_stmt() {
|
||
test("for (let x = 0; x < 10; x++) {}", "for (let x = 0; x < 10; x++) {}\n");
|
||
test("for (;;) {}", "for (;;) {}\n");
|
||
test("for (let x = 1;;) {}", "for (let x = 1;;) {}\n");
|
||
test("for (;true;) {}", "for (; true;) {}\n");
|
||
test("for (;;i++) {}", "for (;; i++) {}\n");
|
||
|
||
test("for (using x = 1;;) {}", "for (using x = 1;;) {}\n");
|
||
|
||
// `in` expression
|
||
test("for (x = (y in z) || y;;);", "for (x = (y in z) || y;;);\n");
|
||
test("for (x = (y in z) || y || y;;);", "for (x = (y in z) || y || y;;);\n");
|
||
test("for (x = y || y || (y in z);;);", "for (x = y || y || (y in z);;);\n");
|
||
test(
|
||
"for (x = (y in z) || y || (y in z) || y || (y in z);;);",
|
||
"for (x = (y in z) || y || (y in z) || y || (y in z);;);\n",
|
||
);
|
||
test(
|
||
"for (var a = 1 || (2 in {}) in { x: 1 }) count++;",
|
||
"for (var a = 1 || (2 in {}) in { x: 1 }) count++;\n",
|
||
);
|
||
|
||
// A `for...of` head may not start with `let` or `async`; `for...in` may.
|
||
test("for ((let) of x);", "for ((let) of x);\n");
|
||
test("for ((async) of x);", "for ((async) of x);\n");
|
||
test("for (let in x);", "for (let in x);\n");
|
||
test("for (async in x);", "for (async in x);\n");
|
||
// A for-of head whose leading token is `let` is wrapped wherever it appears,
|
||
// not only as a bare identifier; a bare `async` is wrapped but `async.x` is fine.
|
||
test("for ((let.x) of x);", "for ((let.x) of x);\n");
|
||
test("for ((let.x[0]) of x);", "for ((let.x[0]) of x);\n");
|
||
test("for ((let[0]) of x);", "for ((let)[0] of x);\n");
|
||
test("for ((async.x) of x);", "for (async.x of x);\n");
|
||
test("for (((let).x) of x);", "for ((let.x) of x);\n");
|
||
// A leading `let` outside a for-of head is untouched.
|
||
test("let.x;", "let.x;\n");
|
||
// A nested for-of inside a `for` init is still wrapped correctly.
|
||
test(
|
||
"for (function f(){ for ((async) of xs); };;);",
|
||
"for (function f() {\n\tfor ((async) of xs);\n};;);\n",
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn do_while_stmt() {
|
||
test("do ; while (true)", "do;\nwhile (true);\n");
|
||
test_minify("do ; while (true)", "do;while(true);");
|
||
test_minify("do break; while (true)", "do break;while(true);");
|
||
test_minify("do continue; while (true)", "do continue;while(true);");
|
||
test_minify("do debugger; while (true)", "do debugger;while(true);");
|
||
test_minify("do for(x in y); while (true)", "do for(x in y);while(true);");
|
||
test_minify("do for(x of y); while (true)", "do for(x of y);while(true);");
|
||
test_minify("do for(;;); while (true)", "do for(;;);while(true);");
|
||
test_minify("do if (test) {} while (true)", "do if(test){}while(true);");
|
||
test_minify("do foo:; while (true)", "do foo:;while(true);");
|
||
test_minify("function a() { do return; while (true) }", "function a(){do return;while(true)}");
|
||
test_minify("do switch(test){} while (true)", "do switch(test){}while(true);");
|
||
test_minify("do throw x; while (true)", "do throw x;while(true);");
|
||
test_minify("do with(x); while (true)", "do with(x);while(true);");
|
||
test_minify("do try{} catch{} while (true)", "do try{}catch{}while(true);");
|
||
test_minify(
|
||
"try { x } catch (err) /* v8 ignore next */ { y }",
|
||
"try{x}catch(err)/* v8 ignore next */{y}",
|
||
);
|
||
test_minify("do do ; while(true) while (true)", "do do;while(true);while(true);");
|
||
}
|
||
|
||
#[test]
|
||
fn if_stmt() {
|
||
test(
|
||
"function f() { if (foo) return foo; else if (bar) return foo; }",
|
||
"function f() {\n\tif (foo) return foo;\n\telse if (bar) return foo;\n}\n",
|
||
);
|
||
test_minify(
|
||
"function f() { if (foo) return foo; else if (bar) return foo; }",
|
||
"function f(){if(foo)return foo;else if(bar)return foo}",
|
||
);
|
||
// `else` only needs a space before an identifier-like body.
|
||
test_minify("if(x)a();else b()", "if(x)a();else b();");
|
||
test_minify("if(x)a();else++b", "if(x)a();else++b;");
|
||
test_minify("if(x)a();else[b]", "if(x)a();else[b];");
|
||
test_minify("if(x)a();else`b`", "if(x)a();else`b`;");
|
||
test_minify("if(x)a();else debugger", "if(x)a();else debugger;");
|
||
}
|
||
|
||
#[test]
|
||
fn shorthand() {
|
||
test("let _ = { x }", "let _ = { x };\n");
|
||
test("let { x } = y", "let { x } = y;\n");
|
||
test("({ x: (x) })", "({ x });\n");
|
||
test("({ x } = y)", "({x} = y);\n");
|
||
// https://github.com/tc39/test262/blob/05c45a4c430ab6fee3e0c7f0d47d8a30d8876a6d/test/language/expressions/object/__proto__-permitted-dup-shorthand.js
|
||
test("var obj = { __proto__, __proto__, };", "var obj = {\n\t__proto__,\n\t__proto__\n};\n");
|
||
test("var obj = { __proto__: __proto__, };", "var obj = { __proto__: __proto__ };\n");
|
||
}
|
||
|
||
#[test]
|
||
fn unicode_escape() {
|
||
test("console.log('你好');", "console.log(\"你好\");\n");
|
||
test("console.log('こんにちは');", "console.log(\"こんにちは\");\n");
|
||
test("console.log('안녕하세요');", "console.log(\"안녕하세요\");\n");
|
||
test("console.log('🧑🤝🧑');", "console.log(\"🧑🤝🧑\");\n");
|
||
test("console.log(\"\\uD800\\uD801\")", "console.log(\"\\ud800\\ud801\");\n");
|
||
}
|
||
|
||
#[test]
|
||
fn regex() {
|
||
fn test_all(source: &str, expect: &str, minify: &str) {
|
||
test(source, expect);
|
||
test_minify(source, minify);
|
||
}
|
||
test_all("/regex/giv", "/regex/giv;\n", "/regex/giv;");
|
||
test_all(
|
||
r"/(.)(.)(.)(.)(.)(.)(.)(.)\8\8/",
|
||
"/(.)(.)(.)(.)(.)(.)(.)(.)\\8\\8/;\n",
|
||
"/(.)(.)(.)(.)(.)(.)(.)(.)\\8\\8/;",
|
||
);
|
||
|
||
test_all(
|
||
r"/\n\cM\0\x41\u{1f600}\./u",
|
||
"/\\n\\cM\\0\\x41\\u{1f600}\\./u;\n",
|
||
"/\\n\\cM\\0\\x41\\u{1f600}\\./u;",
|
||
);
|
||
test_all(r"/\n\cM\0\x41\./u", "/\\n\\cM\\0\\x41\\./u;\n", "/\\n\\cM\\0\\x41\\./u;");
|
||
test_all(
|
||
r"/\n\cM\0\x41\u1234\./",
|
||
"/\\n\\cM\\0\\x41\\u1234\\./;\n",
|
||
"/\\n\\cM\\0\\x41\\u1234\\./;",
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn comma() {
|
||
test("[1, 2, 3]", "[\n\t1,\n\t2,\n\t3\n];\n");
|
||
test("[1, 2, 3,]", "[\n\t1,\n\t2,\n\t3\n];\n");
|
||
test("[,]", "[,];\n");
|
||
test("[,,]", "[, ,];\n");
|
||
test("[,1]", "[, 1];\n");
|
||
|
||
test_minify("1, 2, 3", "1,2,3;");
|
||
test_minify("1, a = b, 3", "1,a=b,3;");
|
||
test_minify("1, (2, 3), 4", "1,2,3,4;");
|
||
}
|
||
|
||
#[test]
|
||
fn assignment() {
|
||
test("(let[0] = 100);", "(let)[0] = 100;\n");
|
||
test("[a, ...rest] = arr;", "[a, ...rest] = arr;\n");
|
||
test("[...rest] = arr;", "[...rest] = arr;\n");
|
||
test("({a, ...rest} = obj);", "({a, ...rest} = obj);\n");
|
||
test("({...rest} = obj);", "({...rest} = obj);\n");
|
||
test_minify("a = b ? c : d", "a=b?c:d;");
|
||
test_minify("[a,b] = (1, 2)", "[a,b]=(1,2);");
|
||
// `{a,b}` is a block, must wrap the whole expression to be an assignment expression
|
||
test_minify("({a,b} = (1, 2))", "({a,b}=(1,2));");
|
||
test_minify("function* foo() { a *= yield b }", "function*foo(){a*=yield b}");
|
||
test_minify("a /= () => {}", "a/=()=>{};");
|
||
test_minify("a %= async () => {}", "a%=async()=>{};");
|
||
test_minify("a -= (1, 2)", "a-=(1,2);");
|
||
test_minify("({ x: x = flag1 = true } = {})", "({x=flag1=true}={});");
|
||
|
||
test_minify("({ 0: x } = foo);", "({0:x}=foo);");
|
||
test_minify("({ [0]: x } = foo);", "({[0]:x}=foo);");
|
||
test_minify("({ a: x } = foo);", "({a:x}=foo);");
|
||
test_minify("({ [a.b]: x } = foo);", "({[a.b]:x}=foo);");
|
||
|
||
test_minify(r#"({"my-key": value} = obj);"#, r#"({"my-key":value}=obj);"#);
|
||
test_minify(
|
||
r#"({["computed"]: a, "literal": b} = obj);"#,
|
||
r#"({["computed"]:a,"literal":b}=obj);"#,
|
||
);
|
||
test_minify(r#"let {"test-key": testKey} = obj;"#, r#"let{"test-key":testKey}=obj;"#);
|
||
|
||
test_minify(r#"({ "test-key": key });"#, r#"({"test-key":key});"#);
|
||
test_minify(r#"(class { "test-key" = key });"#, r#"(class{"test-key"=key});"#);
|
||
}
|
||
|
||
#[test]
|
||
fn r#yield() {
|
||
test("function * foo() { yield * 1 }", "function* foo() {\n\tyield* 1;\n}\n");
|
||
test_minify("function *foo() { yield }", "function*foo(){yield}");
|
||
test_minify("function *foo() { yield * a ? b : c }", "function*foo(){yield*a?b:c}");
|
||
test_minify("function *foo() { yield * yield * a }", "function*foo(){yield*yield*a}");
|
||
test_minify("function *foo() { yield * () => {} }", "function*foo(){yield*()=>{}}");
|
||
test_minify("function *foo() { yield * async () => {} }", "function*foo(){yield*async()=>{}}");
|
||
test_minify("function *foo() { yield a ? b : c }", "function*foo(){yield a?b:c}");
|
||
test_minify("function *foo() { yield yield a }", "function*foo(){yield yield a}");
|
||
test_minify("function *foo() { yield () => {} }", "function*foo(){yield()=>{}}");
|
||
test_minify("function *foo() { yield async () => {} }", "function*foo(){yield async()=>{}}");
|
||
test_minify(
|
||
"function *foo() { yield { a } = [ b ] = c ? b : d }",
|
||
"function*foo(){yield{a}=[b]=c?b:d}",
|
||
);
|
||
test_minify("function *foo() { yield (a, b) }", "function*foo(){yield(a,b)}");
|
||
test_minify("function *foo() { yield a, b }", "function*foo(){yield a,b}");
|
||
}
|
||
|
||
#[test]
|
||
fn arrow() {
|
||
test_minify("x => a, b", "x=>a,b;");
|
||
test_minify("x => (a, b)", "x=>(a,b);");
|
||
test_minify("x => (a => b)", "x=>(a=>b);");
|
||
test_minify("x => y => a, b", "x=>y=>a,b;");
|
||
test_minify("x => y => (a = b)", "x=>y=>a=b;");
|
||
test_minify("x => y => z => a = b, c", "x=>y=>z=>a=b,c;");
|
||
test_minify("x => y => z => a = (b, c)", "x=>y=>z=>a=(b,c);");
|
||
test_minify("x => ({} + 0)", "x=>({})+0;");
|
||
}
|
||
|
||
#[test]
|
||
fn conditional() {
|
||
test_minify("a ? b : c", "a?b:c;");
|
||
test_minify("a ? (b, c) : (d, e)", "a?(b,c):(d,e);");
|
||
test_minify("a ? b : c ? b : c", "a?b:c?b:c;");
|
||
test_minify("(a ? b : c) ? b : c", "(a?b:c)?b:c;");
|
||
test_minify("a, b ? c : d", "a,b?c:d;");
|
||
test_minify("(a, b) ? c : d", "(a,b)?c:d;");
|
||
test_minify("a = b ? c : d", "a=b?c:d;");
|
||
test_minify("(a = b) ? c : d", "(a=b)?c:d;");
|
||
}
|
||
|
||
#[test]
|
||
fn coalesce() {
|
||
test_minify("a ?? b", "a??b;");
|
||
test_minify("a ?? b ?? c ?? d", "a??b??c??d;");
|
||
test_minify("a ?? (b ?? (c ?? d))", "a??b??c??d;");
|
||
test_minify("(a ?? (b ?? (c ?? d)))", "a??b??c??d;");
|
||
test_minify("a, b ?? c", "a,b??c;");
|
||
test_minify("(a, b) ?? c", "(a,b)??c;");
|
||
test_minify("a, b ?? c, d", "a,b??c,d;");
|
||
test_minify("a, b ?? (c, d)", "a,b??(c,d);");
|
||
test_minify("a = b ?? c", "a=b??c;");
|
||
test_minify("a ?? (b = c)", "a??(b=c);");
|
||
test_minify("(a | b) ?? (c | d)", "a|b??c|d;");
|
||
}
|
||
|
||
#[test]
|
||
fn logical_or() {
|
||
test_minify("a || b || c", "a||b||c;");
|
||
test_minify("(a || (b || c)) || d", "a||b||c||d;");
|
||
test_minify("a || (b || (c || d))", "a||b||c||d;");
|
||
test_minify("a || b && c", "a||b&&c;");
|
||
test_minify("(a || b) && c", "(a||b)&&c;");
|
||
test_minify("a, b || c, d", "a,b||c,d;");
|
||
test_minify("(a, b) || (c, d)", "(a,b)||(c,d);");
|
||
test_minify("(a && b) || (c && d)", "a&&b||c&&d;");
|
||
test_minify("a && b || c && d", "a&&b||c&&d;");
|
||
}
|
||
|
||
#[test]
|
||
fn logical_and() {
|
||
test_minify("a && b && c", "a&&b&&c;");
|
||
test_minify("a && ((b && c) && d)", "a&&b&&c&&d;");
|
||
test_minify("((a && b) && c) && d", "a&&b&&c&&d;");
|
||
test_minify("(a || b) && (c || d)", "(a||b)&&(c||d);");
|
||
test_minify("a, b && c, d", "a,b&&c,d;");
|
||
test_minify("(a, b) && (c, d)", "(a,b)&&(c,d);");
|
||
test_minify("a || b && c || d", "a||b&&c||d;");
|
||
}
|
||
|
||
#[test]
|
||
fn bitwise_or() {
|
||
test_minify("a | b | c", "a|b|c;");
|
||
test_minify("(a | b) | c", "a|b|c;");
|
||
test_minify("a | (b | c)", "a|(b|c);");
|
||
test_minify("a | b ^ c", "a|b^c;");
|
||
test_minify("a | (b ^ c)", "a|b^c;");
|
||
test_minify("a | (b && c)", "a|(b&&c);");
|
||
test_minify("a | b && c", "a|b&&c;");
|
||
test_minify("(a ^ b) | (c ^ d)", "a^b|c^d;");
|
||
test_minify("(a, b) | (c, d)", "(a,b)|(c,d);");
|
||
test_minify("a, b | c, d", "a,b|c,d;");
|
||
}
|
||
|
||
#[test]
|
||
fn bitwise_xor() {
|
||
test_minify("a ^ b ^ c", "a^b^c;");
|
||
test_minify("(a ^ b) ^ c", "a^b^c;");
|
||
test_minify("a ^ (b ^ c)", "a^(b^c);");
|
||
test_minify("a | b & c", "a|b&c;");
|
||
test_minify("a | (b & c)", "a|b&c;");
|
||
test_minify("a | (b || c)", "a|(b||c);");
|
||
test_minify("a | b || c", "a|b||c;");
|
||
test_minify("(a, b) ^ (c, d)", "(a,b)^(c,d);");
|
||
test_minify("(a | b) ^ (c | d)", "(a|b)^(c|d);");
|
||
test_minify("a, b ^ c, d", "a,b^c,d;");
|
||
}
|
||
|
||
#[test]
|
||
fn bitwise_and() {
|
||
test_minify("a & b & c", "a&b&c;");
|
||
test_minify("((a & b) & c) & d", "a&b&c&d;");
|
||
test_minify("a & (b & (c & d))", "a&(b&(c&d));");
|
||
test_minify("a & b == c", "a&b==c;");
|
||
test_minify("a & (b == c)", "a&b==c;");
|
||
test_minify("a == b & c", "a==b&c;");
|
||
test_minify("(a == b) & c", "a==b&c;");
|
||
test_minify("a ^ b & c", "a^b&c;");
|
||
test_minify("(a ^ b) & c", "(a^b)&c;");
|
||
test_minify("(a, b) & (c, d)", "(a,b)&(c,d);");
|
||
test_minify("a, b & c, d", "a,b&c,d;");
|
||
}
|
||
|
||
#[test]
|
||
fn equality() {
|
||
test_minify("a == b != c === d !== e", "a==b!=c===d!==e;");
|
||
test_minify("a == (b != (c === (d !== e)))", "a==(b!=(c===(d!==e)));");
|
||
test_minify("(((a == b) != c) === d) !== e", "a==b!=c===d!==e;");
|
||
test_minify("a > b == c < d", "a>b==c<d;");
|
||
test_minify("(a > b) == (c < d)", "a>b==c<d;");
|
||
test_minify("a | b == c & d", "a|b==c&d;");
|
||
test_minify("(a | b) == (c & d)", "(a|b)==(c&d);");
|
||
test_minify("a, b == c , d", "a,b==c,d;");
|
||
test_minify("(a, b) == (c , d)", "(a,b)==(c,d);");
|
||
}
|
||
|
||
#[test]
|
||
fn vite_special_comments() {
|
||
test(
|
||
"new URL(/* @vite-ignore */ 'non-existent', import.meta.url)",
|
||
"new URL(\n\t/* @vite-ignore */\n\t\"non-existent\",\n\timport.meta.url\n);\n",
|
||
);
|
||
test(
|
||
"const importPromise = import(\n/* @vite-ignore */\nbase + '.js'\n);",
|
||
"const importPromise = import(\n\t/* @vite-ignore */\n\tbase + \".js\"\n);\n",
|
||
);
|
||
test(
|
||
"import(/* @vite-ignore */ module1Url).then((module1) => {\nself.postMessage(module.default + module1.msg1 + import.meta.env.BASE_URL)})",
|
||
"import(\n\t/* @vite-ignore */\n\tmodule1Url\n).then((module1) => {\n\tself.postMessage(module.default + module1.msg1 + import.meta.env.BASE_URL);\n});\n",
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn import_phase() {
|
||
test_minify("import.defer('foo')", "import.defer(`foo`);");
|
||
test_minify("import.source('foo')", "import.source(`foo`);");
|
||
test("import.defer('foo')", "import.defer(\"foo\");\n");
|
||
test("import.source('foo')", "import.source(\"foo\");\n");
|
||
}
|
||
|
||
// <https://github.com/javascript-compiler-hints/compiler-notations-spec/blob/main/pure-notation-spec.md#semantics>
|
||
#[test]
|
||
fn pure_comment() {
|
||
test_same("/* @__PURE__ */ pureOperation();\n");
|
||
test_same("/* @__PURE__ */ new PureConsutrctor();\n");
|
||
test("/* @__PURE__ */\npureOperation();\n", "/* @__PURE__ */ pureOperation();\n");
|
||
test_same("/* @__PURE__ The comment may contain additional text */ pureOperation();\n");
|
||
test_same(
|
||
"/* #__PURE__ -- @preserve */ pureOperation();\n", // rolldown#9408
|
||
);
|
||
test("const foo /* #__PURE__ */ = pureOperation();", "const foo = pureOperation();\n"); // INVALID: "=" not allowed after annotation
|
||
|
||
test("/* @__PURE__ */ (foo());", "/* @__PURE__ */ foo();\n");
|
||
test("/* @__PURE__ */ (new Foo());\n", "/* @__PURE__ */ new Foo();\n");
|
||
test("/*#__PURE__*/ (foo(), bar());", "/*#__PURE__*/ foo(), bar();\n"); // INVALID, there is a comma expression in the parentheses
|
||
|
||
test_same("/* @__PURE__ */ a.b().c.d();\n");
|
||
// PURE applies to the innermost call; codegen wraps to keep the annotation on the call.
|
||
test("/* @__PURE__ */ a().b;", "(/* @__PURE__ */ a()).b;\n");
|
||
test_same("(/* @__PURE__ */ a()).b;\n");
|
||
|
||
// More
|
||
test_same("/* @__PURE__ */ a() || b;\n");
|
||
test_same("/* @__PURE__ */ a() && b;\n");
|
||
test_same("/* @__PURE__ */ a() ?? b;\n");
|
||
test_same("/* @__PURE__ */ a() ? b : c;\n");
|
||
test_same("/* @__PURE__ */ a.b();\n");
|
||
test_same("/* @__PURE__ */ a?.b();\n");
|
||
test_same("true && /* @__PURE__ */ noEffect();\n");
|
||
test_same("false || /* @__PURE__ */ noEffect();\n");
|
||
}
|
||
|
||
#[test]
|
||
fn unapplied_annotation_comments() {
|
||
test_same("/* #__PURE__ */ function foo() {}\n");
|
||
test_same("/* #__NO_SIDE_EFFECTS__ */ value;\n");
|
||
|
||
// A misplaced `@__NO_SIDE_EFFECTS__` comment sharing the call site's `attached_to`
|
||
// must be preserved for downstream consumers to warn about, without replacing the
|
||
// valid pure-call annotation.
|
||
test(
|
||
"/* @__PURE__ */ /* @__NO_SIDE_EFFECTS__ */ pureOperation();\n",
|
||
"/* @__NO_SIDE_EFFECTS__ */ /* @__PURE__ */ pureOperation();\n",
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn pife() {
|
||
test_same("foo((() => 0));\n");
|
||
test_minify_same("foo((()=>0));");
|
||
test_same("(() => 0)();\n");
|
||
test_minify_same("(()=>0)();");
|
||
test_same("foo((function() {\n\treturn 0;\n}));\n");
|
||
test_minify_same("foo((function(){return0}));");
|
||
test_same("(function() {\n\treturn 0;\n})();\n");
|
||
test_minify_same("(function(){return0})();");
|
||
}
|
||
|
||
// followup from https://github.com/oxc-project/oxc/pull/6422
|
||
#[test]
|
||
fn in_expr_in_sequence_in_for_loop_init() {
|
||
test(
|
||
"for (l = ('foo' in bar), i; i < 10; i += 1) {}",
|
||
"for (l = (\"foo\" in bar), i; i < 10; i += 1) {}\n",
|
||
);
|
||
|
||
test(
|
||
"for (('hidden' in a) && (m = a.hidden), r = 0; s > r; r++) {}",
|
||
"for ((\"hidden\" in a) && (m = a.hidden), r = 0; s > r; r++) {}\n",
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn in_expr_in_yield_expression() {
|
||
for (keyword, prefix) in [("yield", "yield "), ("yield*", "yield*")] {
|
||
for (init, expected, minified) in [
|
||
(
|
||
format!("{keyword} (1 in o)"),
|
||
format!("{keyword} (1 in o)"),
|
||
format!("{keyword}(1 in o)"),
|
||
),
|
||
(
|
||
format!("x = {keyword} (1 in o)"),
|
||
format!("x = {keyword} (1 in o)"),
|
||
format!("x={keyword}(1 in o)"),
|
||
),
|
||
(
|
||
format!("{keyword} yield (1 in o)"),
|
||
format!("{keyword} yield (1 in o)"),
|
||
format!("{prefix}yield(1 in o)"),
|
||
),
|
||
(
|
||
format!("{keyword} (x = (1 in o))"),
|
||
format!("{keyword} x = (1 in o)"),
|
||
format!("{prefix}x=(1 in o)"),
|
||
),
|
||
// Parentheses around the yield expression allow `in` in its argument.
|
||
(
|
||
format!("({keyword} (1 in o)) + 1"),
|
||
format!("({keyword} 1 in o) + 1"),
|
||
format!("({prefix}1 in o)+1"),
|
||
),
|
||
] {
|
||
let source = format!("function *g(o) {{ for ({init}; false;); }}");
|
||
test(&source, &format!("function* g(o) {{\n\tfor ({expected}; false;);\n}}\n"));
|
||
test_minify(&source, &format!("function*g(o){{for({minified};false;);}}"));
|
||
crate::test_idempotency(&source);
|
||
crate::test_idempotency_options(
|
||
&source,
|
||
&CodegenOptions { minify: true, ..CodegenOptions::default() },
|
||
);
|
||
}
|
||
|
||
let source = format!("function *g(o) {{ {keyword} (1 in o); }}");
|
||
test(&source, &format!("function* g(o) {{\n\t{keyword} 1 in o;\n}}\n"));
|
||
test_minify(&source, &format!("function*g(o){{{prefix}1 in o}}"));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn in_expr_in_arrow_function_expression() {
|
||
test("() => ('foo' in bar)", "() => \"foo\" in bar;\n");
|
||
test("() => 'foo' in bar", "() => \"foo\" in bar;\n");
|
||
test("() => { ('foo' in bar) }", "() => {\n\t\"foo\" in bar;\n};\n");
|
||
|
||
// A parenthesized arrow resets FORBID_IN, so its concise body / param default
|
||
// does not need extra parentheses around `in` (was `(() => (a in b))`).
|
||
test("for (x = (() => a in b);;);", "for (x = (() => a in b);;);\n");
|
||
test("for (x = ((a = b in c) => 1);;);", "for (x = ((a = b in c) => 1);;);\n");
|
||
}
|
||
|
||
#[test]
|
||
fn big_int() {
|
||
test("9007199254740991n;", "9007199254740991n;\n");
|
||
test("-9007199254740991n;", "-9007199254740991n;\n");
|
||
test("-90_0719_92547_40991n;", "-9007199254740991n;\n");
|
||
test("+9007199254740991n;", "+9007199254740991n;\n");
|
||
test("1000n", "1000n;\n");
|
||
test("-15n", "-15n;\n");
|
||
|
||
test("100_000_000n;", "100000000n;\n");
|
||
test("10000000000000000n;", "10000000000000000n;\n");
|
||
test("0n;", "0n;\n");
|
||
test("+0n;", "+0n;\n");
|
||
test("-0n;", "-0n;\n");
|
||
|
||
test("0x1_0n;", "16n;\n");
|
||
test("0x10n;", "16n;\n");
|
||
|
||
test("0b1_01n;", "5n;\n");
|
||
test("0b101n;", "5n;\n");
|
||
test("0b101_101n;", "45n;\n");
|
||
test("0b10_1n", "5n;\n");
|
||
|
||
test("0o13n;", "11n;\n");
|
||
test("0o7n", "7n;\n");
|
||
|
||
test("0x2_0n", "32n;\n");
|
||
test("0xfabn", "4011n;\n");
|
||
test("0xaef_en;", "44798n;\n");
|
||
test("0xaefen;", "44798n;\n");
|
||
|
||
test("function a() { return 1n }", "function a() {\n\treturn 1n;\n}\n");
|
||
test_minify("function a() { return 1n }", "function a(){return 1n}");
|
||
}
|
||
|
||
#[test]
|
||
#[ignore = "Minify bigint is not implemented."]
|
||
fn big_int_minify() {
|
||
test_minify("9007199254740991n", "9007199254740991n;");
|
||
test_minify("-9007199254740991n;", "-9007199254740991n;");
|
||
test_minify("-90_0719_92547_40991n;", "-9007199254740991n;");
|
||
test_minify("+9007199254740991n;", "+9007199254740991n;");
|
||
test_minify("1000n", "1000n;");
|
||
test_minify("-15n", "-15n;");
|
||
|
||
test_minify("100_000_000n;", "100000000n;");
|
||
test_minify("10000000000000000n;", "0x2386f26fc10000n;");
|
||
test_minify("0n;", "0n;");
|
||
test_minify("+0n;", "+0n;");
|
||
test_minify("-0n;", "-0n;");
|
||
|
||
test_minify("0x1_0n;", "16n;");
|
||
test_minify("0x10n;", "16n;");
|
||
|
||
test_minify("0b1_01n;", "5n;");
|
||
test_minify("0b101n;", "5n;");
|
||
test_minify("0b101_101n;", "45n;");
|
||
test_minify("0b10_1n", "5n;");
|
||
|
||
test_minify("0o13n;", "11n;");
|
||
test_minify("0o7n", "7n;");
|
||
|
||
test_minify("0x2_0n", "32n;");
|
||
test_minify("0xfabn", "4011n;");
|
||
test_minify("0xaef_en;", "44798n;");
|
||
test_minify("0xaefen;", "44798n;");
|
||
}
|
||
|
||
#[test]
|
||
fn directive() {
|
||
let single_quote = CodegenOptions { single_quote: true, ..CodegenOptions::default() };
|
||
test_options("\"'\"", "\"'\";\n", single_quote.clone());
|
||
test_options("'\"'", "'\"';\n", single_quote);
|
||
let double_quote = CodegenOptions { single_quote: false, ..CodegenOptions::default() };
|
||
test_options("\"'\"", "\"'\";\n", double_quote.clone());
|
||
test_options("'\"'", "'\"';\n", double_quote.clone());
|
||
test_options(r#""'\"""#, "\"'\\\"\";\n", double_quote);
|
||
}
|
||
|
||
#[test]
|
||
fn directive_prologue_boundary() {
|
||
// A parenthesized string expression statement has to keep its parentheses while it sits at the
|
||
// end of the directive prologue, or it re-parses as a directive.
|
||
|
||
// No directives before it
|
||
test_same("(\"use strict\");\nfoo();\n");
|
||
// After real directives - printed bare, this would switch the program to strict mode
|
||
test_same("\"use asm\";\n(\"use strict\");\nfoo();\n");
|
||
// Indented, in a function body
|
||
test_same("function f() {\n\t(\"x\");\n}\n");
|
||
test_same("function f() {\n\t\"use strict\";\n\t(\"x\");\n}\n");
|
||
// A TS module block has a directive prologue too
|
||
test_same("module Foo {\n\t(\"x\");\n}\n");
|
||
// A class static block does not, so there is nothing to protect against there
|
||
test(
|
||
"class C {\n\tstatic {\n\t\t(\"x\");\n\t}\n}\n",
|
||
"class C {\n\tstatic {\n\t\t\"x\";\n\t}\n}\n",
|
||
);
|
||
|
||
// Only the statement which closes the prologue needs the parentheses - a string statement
|
||
// after it cannot be a directive
|
||
test_same("(\"a\");\n\"b\";\n");
|
||
test("foo();\n(\"a\");\n", "foo();\n\"a\";\n");
|
||
test("\"use strict\";\nfoo();\n(\"a\");\n", "\"use strict\";\nfoo();\n\"a\";\n");
|
||
}
|
||
|
||
#[test]
|
||
fn directive_prologue_boundary_minify() {
|
||
// Minified output has the same hazard. A string usually prints as a template literal, which
|
||
// cannot be a directive, but only where that is the shortest form - one backtick in the string
|
||
// is enough to make quotes shorter, and then nothing else would stop it printing as a
|
||
// directive. So the template literal is printed whatever the contents.
|
||
test_minify("(\"`\");", "`\\``;");
|
||
// `${` costs a backtick the same as a backtick does
|
||
test_minify("(\"${}\");", "`\\${}`;");
|
||
// Strings which would have chosen a template literal anyway are unaffected
|
||
test_minify("(\"use strict\");", "`use strict`;");
|
||
// After real directives
|
||
test_minify("\"use asm\"; (\"`\");", "\"use asm\";`\\``;");
|
||
// A newline pays for a backtick, so quotes were never shorter for this one
|
||
test_minify("(\"\\n`\");", "`\n\\``;");
|
||
// Only the statement which closes the prologue is at risk
|
||
test_minify("foo(); (\"`\");", "foo();\"`\";");
|
||
}
|
||
|
||
#[test]
|
||
fn getter_setter() {
|
||
test_minify("({ get [foo]() {} })", "({get[foo](){}});");
|
||
test_minify("({ set [foo](v) {} })", "({set[foo](v){}});");
|
||
}
|
||
|
||
#[test]
|
||
fn string() {
|
||
// `${` only escaped when quote is backtick
|
||
test("let x = \"${}\";", "let x = \"${}\";\n");
|
||
test_minify("let x = \"${}\";", "let x=\"${}\";");
|
||
test("let x = '\"\"${}';", "let x = \"\\\"\\\"${}\";\n");
|
||
test_minify("let x = '\"\"${}';", "let x='\"\"${}';");
|
||
test("let x = '\"\"\\'\\'${}';", "let x = \"\\\"\\\"''${}\";\n");
|
||
test_minify("let x = '\"\"\\'\\'${}';", "let x=`\"\"''\\${}`;");
|
||
test_minify("let x = '\\'\\'\\'\"\"\"${}';", "let x=`'''\"\"\"\\${}`;");
|
||
|
||
// Lossy replacement character
|
||
test("let x = \"<EFBFBD>\\u{FFFD}\";", "let x = \"<EFBFBD><EFBFBD>\";\n");
|
||
test_minify("let x = \"<EFBFBD>\\u{FFFD}\";", "let x=`<60><>`;");
|
||
test(
|
||
"let x = \"<EFBFBD> <20><><EFBFBD> \\u{FFFD} \\u{FFFD}\\u{FFFD}\\u{FFFD} <20>\";",
|
||
"let x = \"<EFBFBD> <20><><EFBFBD> <20> <20><><EFBFBD> <20>\";\n",
|
||
);
|
||
test_minify(
|
||
"let x = \"<EFBFBD> <20><><EFBFBD> \\u{FFFD} \\u{FFFD}\\u{FFFD}\\u{FFFD} <20>\";",
|
||
"let x=`<60> <20><><EFBFBD> <20> <20><><EFBFBD> <20>`;",
|
||
);
|
||
// Lone surrogates
|
||
test(
|
||
"let x = \"\\uD800 \\uDBFF \\uDC00 \\uDFFF\";",
|
||
"let x = \"\\ud800 \\udbff \\udc00 \\udfff\";\n",
|
||
);
|
||
test_minify(
|
||
"let x = \"\\uD800 \\uDBFF \\uDC00 \\uDFFF\";",
|
||
"let x=`\\ud800 \\udbff \\udc00 \\udfff`;",
|
||
);
|
||
test("let x = \"\\uD800\u{41}\";", "let x = \"\\ud800A\";\n");
|
||
test_minify("let x = \"\\uD800\u{41}\";", "let x=`\\ud800A`;");
|
||
// Invalid pairs
|
||
test(
|
||
"let x = \"\\uD800\\uDBFF \\uDC00\\uDFFF\";",
|
||
"let x = \"\\ud800\\udbff \\udc00\\udfff\";\n",
|
||
);
|
||
test_minify(
|
||
"let x = \"\\uD800\\uDBFF \\uDC00\\uDFFF\";",
|
||
"let x=`\\ud800\\udbff \\udc00\\udfff`;",
|
||
);
|
||
// Lone surrogates and lossy replacement characters
|
||
test(
|
||
"let x = \"<EFBFBD><EFBFBD>\\u{FFFD}\\u{FFFD}\\uD800\\uDBFF<EFBFBD><EFBFBD>\\u{FFFD}\\u{FFFD}\\uDC00\\uDFFF<EFBFBD><EFBFBD>\\u{FFFD}\\u{FFFD}\";",
|
||
"let x = \"<EFBFBD><EFBFBD><EFBFBD><EFBFBD>\\ud800\\udbff<EFBFBD><EFBFBD><EFBFBD><EFBFBD>\\udc00\\udfff<EFBFBD><EFBFBD><EFBFBD><EFBFBD>\";\n",
|
||
);
|
||
test_minify(
|
||
"let x = \"<EFBFBD><EFBFBD>\\u{FFFD}\\u{FFFD}\\uD800\\uDBFF<EFBFBD><EFBFBD>\\u{FFFD}\\u{FFFD}\\uDC00\\uDFFF<EFBFBD><EFBFBD>\\u{FFFD}\\u{FFFD}\";",
|
||
"let x=`<60><><EFBFBD><EFBFBD>\\ud800\\udbff<EFBFBD><EFBFBD><EFBFBD><EFBFBD>\\udc00\\udfff<EFBFBD><EFBFBD><EFBFBD><EFBFBD>`;",
|
||
);
|
||
|
||
test_minify(
|
||
r#";'eval("\'\\vstr\\ving\\v\'") === "\\vstr\\ving\\v"'"#,
|
||
r#";`eval("'\\vstr\\ving\\v'") === "\\vstr\\ving\\v"`;"#,
|
||
);
|
||
test_minify(r#"foo("\n")"#, "foo(`\n`);");
|
||
|
||
// https://github.com/oxc-project/oxc/issues/22342
|
||
test_minify(
|
||
r#"Object.defineProperty(exports, "getInclusionReasons", { enumerable: true });"#,
|
||
r#"Object.defineProperty(exports,"getInclusionReasons",{enumerable:true});"#,
|
||
);
|
||
test_minify(
|
||
r#"Reflect.defineProperty(exports, "getInclusionReasons", { enumerable: true });"#,
|
||
r#"Reflect.defineProperty(exports,"getInclusionReasons",{enumerable:true});"#,
|
||
);
|
||
test_minify(
|
||
r#"exports["has-dash"] = a; module.exports["__esModule"] = true;"#,
|
||
r#"exports["has-dash"]=a;module.exports["__esModule"]=true;"#,
|
||
);
|
||
test_minify(r#"obj["not-exports"] = a;"#, "obj[`not-exports`]=a;");
|
||
|
||
// require() should preserve string quotes for cjs-module-lexer compatibility
|
||
test_minify(
|
||
r#"__exportStar(require("./decorators"), exports);"#,
|
||
r#"__exportStar(require("./decorators"),exports);"#,
|
||
);
|
||
test_minify(r#"var a = require("./foo");"#, r#"var a=require("./foo");"#);
|
||
test_minify(r#"require("./foo");"#, r#"require("./foo");"#);
|
||
// Non-require calls should still use backtick optimization
|
||
test_minify(r#"foo("./bar")"#, "foo(`./bar`);");
|
||
// Dynamic require is not affected
|
||
test_minify("require(foo);", "require(foo);");
|
||
// Single-quoted require
|
||
test_minify(r"require('./foo');", r#"require("./foo");"#);
|
||
|
||
// `cjs-module-lexer` re-export detection requires `"default"` and
|
||
// `"__esModule"` to stay as plain string literals in equality comparisons.
|
||
test_minify(
|
||
r#"function f(key) { if (key === "default" || key === "__esModule") return; }"#,
|
||
r#"function f(key){if(key==="default"||key==="__esModule")return}"#,
|
||
);
|
||
test_minify(r#"a = x !== "default""#, r#"a=x!=="default";"#);
|
||
test_minify(r#"a = x == "__esModule""#, r#"a=x=="__esModule";"#);
|
||
test_minify(r#"a = x != "default""#, r#"a=x!="default";"#);
|
||
// Magic string on the left side of an equality is also preserved.
|
||
test_minify(r#"a = "default" === x"#, r#"a="default"===x;"#);
|
||
// Other strings in equality comparisons are unaffected.
|
||
test_minify(r#"a = x === "foo""#, "a=x===`foo`;");
|
||
// The two magic strings are unaffected when not in equality comparisons.
|
||
test_minify(r#"a = "default""#, "a=`default`;");
|
||
}
|
||
|
||
#[test]
|
||
fn print_string() {
|
||
fn print(value: &str, options: CodegenOptions) -> String {
|
||
let mut codegen = Codegen::new().with_options(options);
|
||
codegen.print_string(value);
|
||
codegen.into_source_text()
|
||
}
|
||
|
||
assert_eq!(print("hello \"world\"", CodegenOptions::default()), r#""hello \"world\"""#);
|
||
assert_eq!(
|
||
print("hello 'world'", CodegenOptions { single_quote: true, ..Default::default() }),
|
||
r"'hello \'world\''"
|
||
);
|
||
assert_eq!(print("line\n\u{00a0}🦄", CodegenOptions::default()), "\"line\\n\\xA0🦄\"");
|
||
assert_eq!(print("\"\"''", CodegenOptions::minify()), r#""\"\"''""#);
|
||
assert_eq!(print("\"\"''${", CodegenOptions::minify()), r#""\"\"''${""#);
|
||
}
|
||
|
||
#[test]
|
||
fn v8_intrinsics() {
|
||
let parse_opts = oxc_parser::ParseOptions {
|
||
allow_v8_intrinsics: true,
|
||
..oxc_parser::ParseOptions::default()
|
||
};
|
||
|
||
test_with_parse_options(
|
||
"const p = %DebugPrint('hi')",
|
||
"const p = %DebugPrint(\"hi\");\n",
|
||
parse_opts,
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn indentation() {
|
||
// Test default - tabs with width 1
|
||
test_options(
|
||
"if (true) {\nif (nested) {\nconsole.log('test');\n}\n}",
|
||
"if (true) {\n\tif (nested) {\n\t\tconsole.log(\"test\");\n\t}\n}\n",
|
||
CodegenOptions::default(),
|
||
);
|
||
|
||
// Test tabs with width 2
|
||
test_options(
|
||
"if (true) {\nif (nested) {\nconsole.log('test');\n}\n}",
|
||
"if (true) {\n\t\tif (nested) {\n\t\t\t\tconsole.log(\"test\");\n\t\t}\n}\n",
|
||
CodegenOptions {
|
||
indent_char: IndentChar::Tab,
|
||
indent_width: 2,
|
||
..CodegenOptions::default()
|
||
},
|
||
);
|
||
|
||
// Test spaces with width 2
|
||
test_options(
|
||
"if (true) {\nif (nested) {\nconsole.log('test');\n}\n}",
|
||
"if (true) {\n if (nested) {\n console.log(\"test\");\n }\n}\n",
|
||
CodegenOptions {
|
||
indent_char: IndentChar::Space,
|
||
indent_width: 2,
|
||
..CodegenOptions::default()
|
||
},
|
||
);
|
||
|
||
// Test spaces with width 4
|
||
test_options(
|
||
"if (true) {\nif (nested) {\nconsole.log('test');\n}\n}",
|
||
"if (true) {\n if (nested) {\n console.log(\"test\");\n }\n}\n",
|
||
CodegenOptions {
|
||
indent_char: IndentChar::Space,
|
||
indent_width: 4,
|
||
..CodegenOptions::default()
|
||
},
|
||
);
|
||
|
||
// Test initial indent with 1
|
||
test_options(
|
||
"let foo = 1;",
|
||
"\tlet foo = 1;\n",
|
||
CodegenOptions { initial_indent: 1, ..CodegenOptions::default() },
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn template_literal_escape_when_building_ast() {
|
||
let allocator = Allocator::default();
|
||
let ast = AstBuilder::new(&allocator);
|
||
|
||
// Create a template literal with special characters that need escaping:
|
||
// backtick, ${, and backslash
|
||
// Use `template_element_escape_raw` to automatically escape the raw field
|
||
let cooked = "hello`world${foo}\\bar";
|
||
let value = TemplateElementValue {
|
||
raw: Str::from_str_in(cooked, &ast),
|
||
cooked: Some(Str::from_str_in(cooked, &ast)),
|
||
};
|
||
let element = TemplateElement::new_escape_raw(SPAN, value, true, &ast);
|
||
let template_literal = TemplateLiteral::new(SPAN, [element], [], &ast);
|
||
|
||
let expr = Expression::new_template_literal(
|
||
SPAN,
|
||
template_literal.quasis,
|
||
template_literal.expressions,
|
||
&ast,
|
||
);
|
||
let stmt = Statement::new_expression_statement(SPAN, expr, &ast);
|
||
let program = Program::new(SPAN, oxc_span::SourceType::mjs(), "", [], None, [], [stmt], &ast);
|
||
|
||
let result = Codegen::new().build(&program).code;
|
||
// The raw value should have been escaped by template_element with escape_raw: true
|
||
// backtick, ${, and backslash are all escaped
|
||
assert_eq!(result, "`hello\\`world\\${foo}\\\\bar`;\n");
|
||
}
|
||
|
||
/// ECMAScript Annex B.1.1 HTML-like Comments
|
||
#[test]
|
||
fn html_comments() {
|
||
test_unambiguous(
|
||
"<!-- HTML comment\nconsole.log(\"test\");\n",
|
||
"<!-- HTML comment\nconsole.log(\"test\");\n",
|
||
);
|
||
test_unambiguous(
|
||
"console.log(\"test\");\n--> HTML comment\n",
|
||
"console.log(\"test\");\n--> HTML comment\n",
|
||
);
|
||
test_unambiguous(
|
||
"const test = '<!-- Hello World! -->';\n",
|
||
"const test = \"<!-- Hello World! -->\";\n",
|
||
);
|
||
test_unambiguous(
|
||
"const test = 'a'; <!-- comment\nconsole.log('test');\n",
|
||
"const test = \"a\";\nconsole.log(\"test\");\n",
|
||
);
|
||
test_unambiguous("const x = 1;\n--> comment\n", "const x = 1;\n--> comment\n");
|
||
test_unambiguous(
|
||
"<!-- comment 1\nconst x = 1;\n<!-- comment 2\nconst y = 2;\n",
|
||
"<!-- comment 1\nconst x = 1;\n<!-- comment 2\nconst y = 2;\n",
|
||
);
|
||
// `<!--` comments out rest of line - everything after is a comment
|
||
test_unambiguous(
|
||
"const test = 'a'; <!-- Test --> console.log('not executed'); //\n",
|
||
"const test = \"a\";\n",
|
||
);
|
||
// Injection: `<!--` comments out rest of line, but code on NEXT line executes
|
||
test_unambiguous(
|
||
"const test = 'a'; <!--\nconsole.log('injection');\n",
|
||
"const test = \"a\";\nconsole.log(\"injection\");\n",
|
||
);
|
||
// `-->` at start of line is also a comment
|
||
test_unambiguous(
|
||
"const x = 1;\n--> comment\nconst y = 2;\n",
|
||
"const x = 1;\n--> comment\nconst y = 2;\n",
|
||
);
|
||
}
|