diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index bc36e0c1e25ce..c677317ef3b3a 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -684,7 +684,7 @@ impl Gen for VariableDeclaration<'_> { VariableDeclarationKind::AwaitUsing => "await using", }); if !self.declarations.is_empty() { - p.print_hard_space(); + p.print_soft_space(); } p.print_list(&self.declarations, ctx); } @@ -1228,6 +1228,7 @@ impl Gen for IdentifierName<'_> { impl Gen for BindingIdentifier<'_> { fn gen(&self, p: &mut Codegen, _ctx: Context) { let name = p.get_binding_identifier_name(self); + p.print_space_before_identifier(); p.add_source_mapping_for_name(self.span, name); p.print_str(name); } @@ -1414,9 +1415,33 @@ impl Gen for StringLiteral<'_> { fn gen(&self, p: &mut Codegen, _ctx: Context) { p.add_source_mapping(self.span); let s = self.value.as_str(); - p.wrap_quote(|p, quote| { - print_unquoted_str(s, quote, p); - }); + + let quote = if p.options.minify { + let mut single_cost: u32 = 0; + let mut double_cost: u32 = 0; + for b in s.as_bytes() { + match b { + b'\'' => { + single_cost += 1; + } + b'"' => { + double_cost += 1; + } + _ => {} + } + } + if double_cost > single_cost { + b'\'' + } else { + b'"' + } + } else { + p.quote + }; + + p.print_ascii_byte(quote); + print_unquoted_str(s, quote, p); + p.print_ascii_byte(quote); } } @@ -1470,7 +1495,7 @@ impl GenExpr for StaticMemberExpression<'_> { impl GenExpr for PrivateFieldExpression<'_> { fn gen_expr(&self, p: &mut Codegen, _precedence: Precedence, ctx: Context) { - self.object.print_expr(p, Precedence::Prefix, ctx.intersection(Context::FORBID_CALL)); + self.object.print_expr(p, Precedence::Postfix, ctx.intersection(Context::FORBID_CALL)); if self.optional { p.print_str("?"); } @@ -1640,18 +1665,22 @@ impl Gen for ObjectProperty<'_> { PropertyKind::Init => false, PropertyKind::Get => { p.add_source_mapping(self.span); - p.print_str("get "); + p.print_str("get"); + p.print_soft_space(); true } PropertyKind::Set => { p.add_source_mapping(self.span); - p.print_str("set "); + p.print_str("set"); + p.print_soft_space(); true } }; if self.method || is_accessor { if func.r#async { - p.print_str("async "); + p.print_space_before_identifier(); + p.print_str("async"); + p.print_soft_space(); } if func.generator { p.print_str("*"); @@ -2216,23 +2245,29 @@ impl GenExpr for NewExpression<'_> { p.print_annotation_comments(self.span.start); p.print_space_before_identifier(); p.add_source_mapping(self.span); - p.print_str("new "); + p.print_str("new"); + p.print_soft_space(); self.callee.print_expr(p, Precedence::New, Context::FORBID_CALL); - p.print_ascii_byte(b'('); - let has_comment = (self.span.end > 0 && p.has_comment(self.span.end - 1)) - || self.arguments.iter().any(|item| p.has_comment(item.span().start)); - if has_comment { - p.indent(); - p.print_list_with_comments(&self.arguments, ctx); - // Handle `/* comment */);` - if self.span.end > 0 && !p.print_expr_comments(self.span.end - 1) { - p.print_soft_newline(); + + // Omit the "()" when minifying, but only when safe to do so + if !p.options.minify || !self.arguments.is_empty() || precedence >= Precedence::Postfix + { + p.print_ascii_byte(b'('); + let has_comment = (self.span.end > 0 && p.has_comment(self.span.end - 1)) + || self.arguments.iter().any(|item| p.has_comment(item.span().start)); + if has_comment { + p.indent(); + p.print_list_with_comments(&self.arguments, ctx); + // Handle `/* comment */);` + if self.span.end > 0 && !p.print_expr_comments(self.span.end - 1) { + p.print_soft_newline(); + } + p.dedent(); + } else { + p.print_list(&self.arguments, ctx); } - p.dedent(); - } else { - p.print_list(&self.arguments, ctx); + p.print_ascii_byte(b')'); } - p.print_ascii_byte(b')'); }); } } diff --git a/crates/oxc_codegen/src/lib.rs b/crates/oxc_codegen/src/lib.rs index f9c8e9e609821..c26c14f61b84e 100644 --- a/crates/oxc_codegen/src/lib.rs +++ b/crates/oxc_codegen/src/lib.rs @@ -600,22 +600,27 @@ impl<'a> Codegen<'a> { candidates.push(format!("0x{:x}", num as u128)); } + // create `1e-2` if s.starts_with(".0") { - // create `1e-2` if let Some((i, _)) = s[1..].bytes().enumerate().find(|(_, c)| *c != b'0') { let len = i + 1; // `+1` to include the dot. let digits = &s[len..]; candidates.push(format!("{digits}e-{}", digits.len() + len - 1)); } - } else if s.ends_with('0') { - // create 1e2 + } + + // create 1e2 + if s.ends_with('0') { if let Some((len, _)) = s.bytes().rev().enumerate().find(|(_, c)| *c != b'0') { candidates.push(format!("{}e{len}", &s[0..s.len() - len])); } - } else if let Some((integer, point, exponent)) = + } + + // `1.2e101` -> ("1", "2", "101") + // `1.3415205933077406e300` -> `13415205933077406e284;` + if let Some((integer, point, exponent)) = s.split_once('.').and_then(|(a, b)| b.split_once('e').map(|e| (a, e.0, e.1))) { - // `1.2e101` -> ("1", "2", "101") candidates.push(format!( "{integer}{point}e{}", exponent.parse::().unwrap() - point.len() as isize @@ -636,13 +641,6 @@ impl<'a> Codegen<'a> { } } - #[inline] - fn wrap_quote(&mut self, mut f: F) { - self.print_ascii_byte(self.quote); - f(self, self.quote); - self.print_ascii_byte(self.quote); - } - fn add_source_mapping(&mut self, span: Span) { if span == SPAN { return; diff --git a/crates/oxc_codegen/tests/integration/esbuild.rs b/crates/oxc_codegen/tests/integration/esbuild.rs index 444205e38744a..bfa3503cf266c 100644 --- a/crates/oxc_codegen/tests/integration/esbuild.rs +++ b/crates/oxc_codegen/tests/integration/esbuild.rs @@ -178,12 +178,12 @@ fn test_new() { test("new (import('foo')[bar])", "new (import(\"foo\"))[bar]();\n"); test("new (import('foo'))[bar]", "new (import(\"foo\"))[bar]();\n"); - // test_minify("new x", "new x;"); - // test_minify("new x.y", "new x.y;"); - // test_minify("(new x).y", "new x().y;"); - // test_minify("new x().y", "new x().y;"); - // test_minify("new x() + y", "new x+y;"); - // test_minify("new x() ** 2", "new x**2;"); + test_minify("new x", "new x;"); + test_minify("new x.y", "new x.y;"); + test_minify("(new x).y", "new x().y;"); + test_minify("new x().y", "new x().y;"); + test_minify("new x() + y", "new x+y;"); + test_minify("new x() ** 2", "new x**2;"); // Test preservation of Webpack-specific comments test( @@ -298,6 +298,8 @@ fn test_call() { fn test_member() { test("x.y[z]", "x.y[z];\n"); test("((x+1).y+1)[z]", "((x + 1).y + 1)[z];\n"); + + test_minify("1.3415205933077406e300", "13415205933077406e284;"); } #[test] diff --git a/crates/oxc_codegen/tests/integration/snapshots/minify.snap b/crates/oxc_codegen/tests/integration/snapshots/minify.snap index 106343bda7138..59be2866aae0a 100644 --- a/crates/oxc_codegen/tests/integration/snapshots/minify.snap +++ b/crates/oxc_codegen/tests/integration/snapshots/minify.snap @@ -11,43 +11,43 @@ function foo(x: T, y: string, ...restOfParams: Omit): return x; } ---------- -function foo(x:T,y:string,...restOfParams:Omit): T{return x} +function foo(x:T,y:string,...restOfParams:Omit): T{return x} ########## 2 let x: string[] = ['abc', 'def', 'ghi']; ---------- -let x:string[]=['abc','def','ghi']; +let x:string[]=["abc","def","ghi"]; ########## 3 let x: Array = ['abc', 'def', 'ghi',]; ---------- -let x:Array=['abc','def','ghi']; +let x:Array=["abc","def","ghi"]; ########## 4 let x: [string, number] = ['abc', 123]; ---------- -let x:[string,number]=['abc',123]; +let x:[string,number]=["abc",123]; ########## 5 let x: string | number = 'abc'; ---------- -let x:string|number='abc'; +let x:string|number="abc"; ########## 6 let x: string & number = 'abc'; ---------- -let x:string&number='abc'; +let x:string&number="abc"; ########## 7 let x: typeof String = 'string'; ---------- -let x:typeof String='string'; +let x:typeof String="string"; ########## 8 let x: keyof string = 'length'; ---------- -let x:keyof string='length'; +let x:keyof string="length"; ########## 9 let x: keyof typeof String = 'length'; ---------- -let x:keyof typeof String='length'; +let x:keyof typeof String="length"; ########## 10 let x: string['length'] = 123; ---------- -let x:string['length']=123; +let x:string["length"]=123; ########## 11 function isString(value: unknown): asserts value is string { if (typeof value !== 'string') { @@ -55,19 +55,19 @@ function isString(value: unknown): asserts value is string { } } ---------- -function isString(value:unknown): asserts value is string{if(typeof value!=='string'){throw new Error('Not a string')}} +function isString(value:unknown): asserts value is string{if(typeof value!=="string"){throw new Error("Not a string")}} ########## 12 import type { Foo } from 'foo'; ---------- -import type{Foo}from'foo'; +import type{Foo}from"foo"; ########## 13 import { Foo, type Bar } from 'foo'; ---------- -import{Foo,type Bar}from'foo'; +import{Foo,type Bar}from"foo"; ########## 14 export { Foo, type Bar } from 'foo'; ---------- -export{Foo,type Bar}from'foo'; +export{Foo,type Bar}from"foo"; ########## 15 type A = { [K in keyof T as K extends string ? B : K ]: T[K] } ---------- @@ -75,7 +75,7 @@ type A={[K in keyof T as K extends string ? B : K]:T[K]}; ########## 16 class A {readonly type = 'frame'} ---------- -class A{readonly type='frame'} +class A{readonly type="frame"} ########## 17 let foo: { (t: T): void } ---------- @@ -196,4 +196,4 @@ export { default, /* …, */ } from "module-name"; export { default as name16 } from "module-name"; ---------- -import defaultExport from'module-name';import*as name from'module-name';import{export1}from'module-name';import{export1 as alias1}from'module-name';import{default as alias}from'module-name';import{export1,export2}from'module-name';import{export1,export2 as alias2}from'module-name';import{'string name' as alias}from'module-name';import defaultExport,{export1}from'module-name';import defaultExport,*as name from'module-name';import'module-name';import{}from"mod";export let name1,name2;export const name3=1,name4=2;export function functionName(){}export class ClassName{}export function*generatorFunctionName(){}export const {name5,name2:bar}=o;export const [name6,name7]=array;export{name8,name81};export{variable1 as name9,variable2 as name10,name82};export{variable1 as 'string name'};export{name1 as default1};export*from'module-name';export*as name11 from'module-name';export{name12,nameN}from'module-name';export{import1 as name13,import2 as name14,name15}from'module-name';export{default}from'module-name';export{default as name16}from'module-name'; +import defaultExport from"module-name";import*as name from"module-name";import{export1}from"module-name";import{export1 as alias1}from"module-name";import{default as alias}from"module-name";import{export1,export2}from"module-name";import{export1,export2 as alias2}from"module-name";import{"string name" as alias}from"module-name";import defaultExport,{export1}from"module-name";import defaultExport,*as name from"module-name";import"module-name";import{}from"mod";export let name1,name2;export const name3=1,name4=2;export function functionName(){}export class ClassName{}export function*generatorFunctionName(){}export const{name5,name2:bar}=o;export const[name6,name7]=array;export{name8,name81};export{variable1 as name9,variable2 as name10,name82};export{variable1 as "string name"};export{name1 as default1};export*from"module-name";export*as name11 from"module-name";export{name12,nameN}from"module-name";export{import1 as name13,import2 as name14,name15}from"module-name";export{default}from"module-name";export{default as name16}from"module-name"; diff --git a/crates/oxc_codegen/tests/integration/unit.rs b/crates/oxc_codegen/tests/integration/unit.rs index 1dc91547b92e9..ed017b5f85e31 100644 --- a/crates/oxc_codegen/tests/integration/unit.rs +++ b/crates/oxc_codegen/tests/integration/unit.rs @@ -2,6 +2,13 @@ use oxc_codegen::CodegenOptions; use crate::tester::{test, test_minify, test_minify_same, test_options}; +#[test] +fn decl() { + 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"); @@ -13,7 +20,7 @@ fn module_decl() { #[test] fn expr() { test("new (foo()).bar();", "new (foo()).bar();\n"); - test_minify("x in new Error()", "x in new Error();"); + test_minify("x in new Error()", "x in new Error;"); test("1000000000000000128.0.toFixed(0)", "0xde0b6b3a7640080.toFixed(0);\n"); test_minify("1000000000000000128.0.toFixed(0)", "0xde0b6b3a7640080.toFixed(0);"); @@ -398,3 +405,9 @@ fn directive() { test_options("'\"'", "'\"';\n", double_quote.clone()); test_options(r#""'\"""#, "\"'\\\"\";\n", double_quote); } + +#[test] +fn getter_setter() { + test_minify("({ get [foo]() {} })", "({get[foo](){}});"); + test_minify("({ set [foo]() {} })", "({set[foo](){}});"); +} diff --git a/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs b/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs index 569621a393e35..c9493729c7d51 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs @@ -17,20 +17,28 @@ pub enum ValueType { } impl ValueType { + pub fn is_undefined(self) -> bool { + self == Self::Undefined + } + + pub fn is_null(self) -> bool { + self == Self::Null + } + pub fn is_string(self) -> bool { - matches!(self, Self::String) + self == Self::String } pub fn is_number(self) -> bool { - matches!(self, Self::Number) + self == Self::Number } pub fn is_bigint(self) -> bool { - matches!(self, Self::BigInt) + self == Self::BigInt } pub fn is_boolean(self) -> bool { - matches!(self, Self::Boolean) + self == Self::Boolean } } diff --git a/crates/oxc_linter/src/rules/import/named.rs b/crates/oxc_linter/src/rules/import/named.rs index 72033bf068038..c3859b7bc4093 100644 --- a/crates/oxc_linter/src/rules/import/named.rs +++ b/crates/oxc_linter/src/rules/import/named.rs @@ -77,7 +77,8 @@ declare_oxc_lint!( /// import { SomeNonsenseThatDoesntExist } from 'react' /// ``` Named, - correctness + nursery // There are race conditions in the runtime which may cause the module to + // not find any exports from `exported_bindings_from_star_export`. ); impl Rule for Named { diff --git a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs index 58332ac0e3543..f79336b4b5d2d 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs @@ -1,6 +1,6 @@ use oxc_ast::ast::*; use oxc_ecmascript::{ - constant_evaluation::{ConstantEvaluation, ValueType}, + constant_evaluation::{ConstantEvaluation, ConstantValue, ValueType}, side_effects::MayHaveSideEffects, }; use oxc_span::{GetSpan, SPAN}; @@ -53,6 +53,7 @@ impl<'a> Traverse<'a> for PeepholeFoldConstants { } // TODO: return tryFoldGetProp(subtree); Expression::LogicalExpression(e) => Self::try_fold_logical_expression(e, ctx), + Expression::ChainExpression(e) => Self::try_fold_optional_chain(e, ctx), // TODO: tryFoldGetElem // TODO: tryFoldAssign _ => None, @@ -106,6 +107,20 @@ impl<'a, 'b> PeepholeFoldConstants { } } + fn try_fold_optional_chain( + chain_expr: &mut ChainExpression<'a>, + ctx: Ctx<'a, 'b>, + ) -> Option> { + let member_expr = chain_expr.expression.as_member_expression()?; + if !member_expr.optional() { + return None; + } + let object = member_expr.object(); + let ty = ValueType::from(object); + (ty.is_null() || ty.is_undefined()) + .then(|| ctx.value_to_expr(chain_expr.span, ConstantValue::Undefined)) + } + /// Try to fold a AND / OR node. /// /// port from [closure-compiler](https://github.com/google/closure-compiler/blob/09094b551915a6487a980a783831cba58b5739d1/src/com/google/javascript/jscomp/PeepholeFoldConstants.java#L587) @@ -1149,6 +1164,24 @@ mod test { test_same("void x()"); } + #[test] + fn test_fold_opt_chain() { + // can't fold when optional part may execute + test_same("a = x?.y"); + test_same("a = x?.()"); + + // fold args of optional call + test("x = foo() ?. (true && bar())", "x = foo() ?.(bar())"); + test("a() ?. (1 ?? b())", "a() ?. (1)"); + + // test("({a})?.a.b.c.d()?.x.y.z", "a.b.c.d()?.x.y.z"); + + test("x = undefined?.y", "x = void 0"); + test("x = null?.y", "x = void 0"); + test("x = undefined?.[foo]", "x = void 0"); + test("x = null?.[foo]", "x = void 0"); + } + #[test] fn test_fold_bitwise_op() { test("x = 1 & 1", "x = 1"); diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 862dd78370982..cae51a72ab199 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -1,27 +1,27 @@ | Oxc | ESBuild | Oxc | ESBuild | Original | minified | minified | gzip | gzip | Fixture ------------------------------------------------------------------------------------- -72.14 kB | 24.05 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js +72.14 kB | 24.04 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js -173.90 kB | 61.60 kB | 59.82 kB | 19.55 kB | 19.33 kB | moment.js +173.90 kB | 61.60 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js -287.63 kB | 92.61 kB | 90.07 kB | 32.27 kB | 31.95 kB | jquery.js +287.63 kB | 92.60 kB | 90.07 kB | 32.27 kB | 31.95 kB | jquery.js -342.15 kB | 121.77 kB | 118.14 kB | 44.58 kB | 44.37 kB | vue.js +342.15 kB | 121.56 kB | 118.14 kB | 44.64 kB | 44.37 kB | vue.js -544.10 kB | 73.37 kB | 72.48 kB | 26.13 kB | 26.20 kB | lodash.js +544.10 kB | 73.32 kB | 72.48 kB | 26.13 kB | 26.20 kB | lodash.js -555.77 kB | 276.22 kB | 270.13 kB | 91.15 kB | 90.80 kB | d3.js +555.77 kB | 276.08 kB | 270.13 kB | 91.14 kB | 90.80 kB | d3.js -1.01 MB | 467.14 kB | 458.89 kB | 126.74 kB | 126.71 kB | bundle.min.js +1.01 MB | 467.00 kB | 458.89 kB | 126.74 kB | 126.71 kB | bundle.min.js -1.25 MB | 662.66 kB | 646.76 kB | 164.00 kB | 163.73 kB | three.js +1.25 MB | 661.59 kB | 646.76 kB | 163.94 kB | 163.73 kB | three.js -2.14 MB | 740.54 kB | 724.14 kB | 181.37 kB | 181.07 kB | victory.js +2.14 MB | 740.48 kB | 724.14 kB | 181.35 kB | 181.07 kB | victory.js -3.20 MB | 1.02 MB | 1.01 MB | 332.09 kB | 331.56 kB | echarts.js +3.20 MB | 1.02 MB | 1.01 MB | 332.01 kB | 331.56 kB | echarts.js -6.69 MB | 2.39 MB | 2.31 MB | 495.63 kB | 488.28 kB | antd.js +6.69 MB | 2.39 MB | 2.31 MB | 495.62 kB | 488.28 kB | antd.js -10.95 MB | 3.55 MB | 3.49 MB | 909.67 kB | 915.50 kB | typescript.js +10.95 MB | 3.54 MB | 3.49 MB | 909.71 kB | 915.50 kB | typescript.js