diff --git a/crates/oxc_minifier/src/ast_passes/convert_to_dotted_properties.rs b/crates/oxc_minifier/src/ast_passes/convert_to_dotted_properties.rs new file mode 100644 index 0000000000000..af7f1d1539173 --- /dev/null +++ b/crates/oxc_minifier/src/ast_passes/convert_to_dotted_properties.rs @@ -0,0 +1,277 @@ +use oxc_ast::ast::*; +use oxc_span::GetSpan; +use oxc_syntax::identifier::is_identifier_name; +use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx}; + +use crate::CompressorPass; + +/// Converts property accesses from quoted string or bracket access syntax to dot or unquoted string +/// syntax, where possible. Dot syntax is more compact. +/// +pub struct ConvertToDottedProperties { + pub(crate) changed: bool, +} + +impl<'a> CompressorPass<'a> for ConvertToDottedProperties { + fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { + self.changed = false; + traverse_mut_with_ctx(self, program, ctx); + } +} + +impl<'a> Traverse<'a> for ConvertToDottedProperties { + fn enter_member_expression( + &mut self, + expr: &mut MemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.convert_computed_member_expression(expr, ctx); + } +} + +impl<'a> ConvertToDottedProperties { + pub fn new() -> Self { + Self { changed: false } + } + + fn convert_computed_member_expression( + &mut self, + expr: &mut MemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let MemberExpression::ComputedMemberExpression(computed_expr) = expr { + if let Expression::StringLiteral(key) = &computed_expr.expression { + if is_identifier_name(&key.value) { + let property = ctx.ast.identifier_name(key.span, key.value.clone()); + *expr = ctx.ast.member_expression_static( + computed_expr.span(), + ctx.ast.move_expression(&mut computed_expr.object), + property, + computed_expr.optional, + ); + self.changed = true; + } + } + } + } +} + +/// Port from +#[cfg(test)] +mod test { + use oxc_allocator::Allocator; + + use crate::tester; + + fn test(source_text: &str, expected: &str) { + let allocator = Allocator::default(); + let mut pass = super::ConvertToDottedProperties::new(); + tester::test(&allocator, source_text, expected, &mut pass); + } + + fn test_same(source_text: &str) { + test(source_text, source_text); + } + + #[test] + fn test_convert() { + test("a['p']", "a.p"); + test("a['_p_']", "a._p_"); + test("a['_']", "a._"); + test("a['$']", "a.$"); + test("a.b.c['p']", "a.b.c.p"); + test("a.b['c'].p", "a.b.c.p"); + test("a['p']();", "a.p();"); + test("a()['p']", "a().p"); + // ASCII in Unicode is safe. + test("a['\\u0041A']", "a.AA"); + } + + #[test] + fn test_do_not_convert() { + test_same("a[0]"); + test_same("a['']"); + test_same("a[' ']"); + test_same("a[',']"); + test_same("a[';']"); + test_same("a[':']"); + test_same("a['.']"); + test_same("a['0']"); + test_same("a['p ']"); + test_same("a['p' + '']"); + test_same("a[p]"); + test_same("a[P]"); + test_same("a[$]"); + test_same("a[p()]"); + // test_same("a['default']"); + // Ignorable control characters are ok in Java identifiers, but not in JS. + test_same("a['A\\u0004']"); + // upper case lower half of o from phonetic extensions set. + // valid in Safari, not in Firefox, IE. + // test_same("a['\\u1d17A']"); + // Latin capital N with tilde - nice if we handled it, but for now let's + // only allow simple Latin (aka ASCII) to be converted. + // test_same("a['\\u00d1StuffAfter']"); + } + + #[test] + fn test_already_dotted() { + test_same("a.b"); + test_same("var a = {b: 0};"); + } + + #[test] + fn test_quoted_props() { + test_same("({'':0})"); + test_same("({'1.0':0})"); + test_same("({'\\u1d17A':0})"); + test_same("({'a\\u0004b':0})"); + } + + #[test] + fn test5746867() { + test_same("var a = { '$\\\\' : 5 };"); + test_same("var a = { 'x\\\\u0041$\\\\' : 5 };"); + } + + #[test] + fn test_optional_chaining() { + test("data?.['name']", "data?.name"); + test("data?.['name']?.['first']", "data?.name?.first"); + test("data['name']?.['first']", "data.name?.first"); + test_same("a?.[0]"); + test_same("a?.['']"); + test_same("a?.[' ']"); + test_same("a?.[',']"); + test_same("a?.[';']"); + test_same("a?.[':']"); + test_same("a?.['.']"); + test_same("a?.['0']"); + test_same("a?.['p ']"); + test_same("a?.['p' + '']"); + test_same("a?.[p]"); + test_same("a?.[P]"); + test_same("a?.[$]"); + test_same("a?.[p()]"); + // test_same("a?.['default']"); + } + + #[test] + #[ignore] + fn test_computed_property_or_field() { + test("const test1 = {['prop1']:87};", "const test1 = {prop1:87};"); + test( + "const test1 = {['prop1']:87,['prop2']:bg,['prop3']:'hfd'};", + "const test1 = {prop1:87,prop2:bg,prop3:'hfd'};", + ); + test( + "o = {['x']: async function(x) { return await x + 1; }};", + "o = {x:async function (x) { return await x + 1; }};", + ); + test("o = {['x']: function*(x) {}};", "o = {x: function*(x) {}};"); + test( + "o = {['x']: async function*(x) { return await x + 1; }};", + "o = {x:async function*(x) { return await x + 1; }};", + ); + test("class C {'x' = 0; ['y'] = 1;}", "class C { x= 0;y= 1;}"); + test("class C {'m'() {} }", "class C {m() {}}"); + + test("const o = {'b'() {}, ['c']() {}};", "const o = {b: function() {}, c:function(){}};"); + test("o = {['x']: () => this};", "o = {x: () => this};"); + + test("const o = {get ['d']() {}};", "const o = {get d() {}};"); + test("const o = { set ['e'](x) {}};", "const o = { set e(x) {}};"); + test( + "class C {'m'() {} ['n']() {} 'x' = 0; ['y'] = 1;}", + "class C {m() {} n() {} x= 0;y= 1;}", + ); + test( + "const o = { get ['d']() {}, set ['e'](x) {}};", + "const o = {get d() {}, set e(x){}};", + ); + test( + "const o = {['a']: 1,'b'() {}, ['c']() {}, get ['d']() {}, set ['e'](x) {}};", + "const o = {a: 1,b: function() {}, c: function() {}, get d() {}, set e(x) {}};", + ); + + // test static keyword + test( + r" + class C { + 'm'(){} + ['n'](){} + static 'x' = 0; + static ['y'] = 1;} + ", + r" + class C { + m(){} + n(){} + static x = 0; + static y= 1;} + ", + ); + test( + r" + window['MyClass'] = class { + static ['Register'](){} + }; + ", + r" + window.MyClass = class { + static Register(){} + }; + ", + ); + test( + r" + class C { + 'method'(){} + async ['method1'](){} + *['method2'](){} + static ['smethod'](){} + static async ['smethod1'](){} + static *['smethod2'](){}} + ", + r" + class C { + method(){} + async method1(){} + *method2(){} + static smethod(){} + static async smethod1(){} + static *smethod2(){}} + ", + ); + + test_same("const o = {[fn()]: 0}"); + test_same("const test1 = {[0]:87};"); + test_same("const test1 = {['default']:87};"); + test_same("class C { ['constructor']() {} }"); + test_same("class C { ['constructor'] = 0 }"); + } + + #[test] + #[ignore] + fn test_computed_property_with_default_value() { + test("const {['o']: o = 0} = {};", "const {o:o = 0} = {};"); + } + + #[test] + fn test_continue_optional_chaining() { + test("const opt1 = window?.a?.['b'];", "const opt1 = window?.a?.b;"); + + test("const opt2 = window?.a['b'];", "const opt2 = window?.a.b;"); + test( + r" + const chain = + window['a'].x.y.b.x.y['c'].x.y?.d.x.y['e'].x.y + ['f-f'].x.y?.['g-g'].x.y?.['h'].x.y['i'].x.y; + ", + r" + const chain = window.a.x.y.b.x.y.c.x.y?.d.x.y.e.x.y + ['f-f'].x.y?.['g-g'].x.y?.h.x.y.i.x.y; + ", + ); + } +} diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index 459265a14fe03..c8d936fc67036 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -3,6 +3,7 @@ use oxc_ast::ast::*; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx}; mod collapse_variable_declarations; +mod convert_to_dotted_properties; mod exploit_assigns; mod normalize; mod peephole_fold_constants; @@ -14,6 +15,7 @@ mod remove_syntax; mod statement_fusion; pub use collapse_variable_declarations::CollapseVariableDeclarations; +pub use convert_to_dotted_properties::ConvertToDottedProperties; pub use exploit_assigns::ExploitAssigns; pub use normalize::Normalize; pub use peephole_fold_constants::PeepholeFoldConstants; @@ -73,6 +75,7 @@ pub struct LatePeepholeOptimizations { x4_peephole_substitute_alternate_syntax: PeepholeSubstituteAlternateSyntax, x5_peephole_replace_known_methods: PeepholeReplaceKnownMethods, x6_peephole_fold_constants: PeepholeFoldConstants, + x7_convert_to_dotted_properties: ConvertToDottedProperties, } impl LatePeepholeOptimizations { @@ -88,6 +91,7 @@ impl LatePeepholeOptimizations { ), x5_peephole_replace_known_methods: PeepholeReplaceKnownMethods::new(), x6_peephole_fold_constants: PeepholeFoldConstants::new(), + x7_convert_to_dotted_properties: ConvertToDottedProperties::new(), } } @@ -99,6 +103,7 @@ impl LatePeepholeOptimizations { self.x4_peephole_substitute_alternate_syntax.changed = false; self.x5_peephole_replace_known_methods.changed = false; self.x6_peephole_fold_constants.changed = false; + self.x7_convert_to_dotted_properties.changed = false; } fn changed(&self) -> bool { @@ -109,6 +114,7 @@ impl LatePeepholeOptimizations { || self.x4_peephole_substitute_alternate_syntax.changed || self.x5_peephole_replace_known_methods.changed || self.x6_peephole_fold_constants.changed + || self.x7_convert_to_dotted_properties.changed } pub fn run_in_loop<'a>( @@ -183,6 +189,14 @@ impl<'a> Traverse<'a> for LatePeepholeOptimizations { self.x6_peephole_fold_constants.exit_expression(expr, ctx); } + fn enter_member_expression( + &mut self, + expr: &mut MemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.x7_convert_to_dotted_properties.enter_member_expression(expr, ctx); + } + fn enter_call_expression(&mut self, expr: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { self.x4_peephole_substitute_alternate_syntax.enter_call_expression(expr, ctx); } @@ -204,6 +218,7 @@ pub struct PeepholeOptimizations { x4_peephole_replace_known_methods: PeepholeReplaceKnownMethods, x5_peephole_remove_dead_code: PeepholeRemoveDeadCode, x6_peephole_fold_constants: PeepholeFoldConstants, + x7_convert_to_dotted_properties: ConvertToDottedProperties, } impl PeepholeOptimizations { @@ -217,6 +232,7 @@ impl PeepholeOptimizations { x4_peephole_replace_known_methods: PeepholeReplaceKnownMethods::new(), x5_peephole_remove_dead_code: PeepholeRemoveDeadCode::new(), x6_peephole_fold_constants: PeepholeFoldConstants::new(), + x7_convert_to_dotted_properties: ConvertToDottedProperties::new(), } } } @@ -262,6 +278,14 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { self.x6_peephole_fold_constants.exit_expression(expr, ctx); } + fn enter_member_expression( + &mut self, + expr: &mut MemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.x7_convert_to_dotted_properties.enter_member_expression(expr, ctx); + } + fn enter_call_expression(&mut self, expr: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { self.x3_peephole_substitute_alternate_syntax.enter_call_expression(expr, ctx); } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 0d863febd2b80..d209035aa7dd4 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -3,25 +3,25 @@ Original | minified | minified | gzip | gzip | Fixture ------------------------------------------------------------------------------------- 72.14 kB | 23.74 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js -173.90 kB | 60.22 kB | 59.82 kB | 19.49 kB | 19.33 kB | moment.js +173.90 kB | 60.21 kB | 59.82 kB | 19.48 kB | 19.33 kB | moment.js -287.63 kB | 90.61 kB | 90.07 kB | 32.19 kB | 31.95 kB | jquery.js +287.63 kB | 90.57 kB | 90.07 kB | 32.17 kB | 31.95 kB | jquery.js -342.15 kB | 118.76 kB | 118.14 kB | 44.54 kB | 44.37 kB | vue.js +342.15 kB | 118.74 kB | 118.14 kB | 44.53 kB | 44.37 kB | vue.js -544.10 kB | 72.04 kB | 72.48 kB | 26.18 kB | 26.20 kB | lodash.js +544.10 kB | 72.00 kB | 72.48 kB | 26.18 kB | 26.20 kB | lodash.js 555.77 kB | 273.90 kB | 270.13 kB | 91.19 kB | 90.80 kB | d3.js -1.01 MB | 461.13 kB | 458.89 kB | 126.91 kB | 126.71 kB | bundle.min.js +1.01 MB | 461.08 kB | 458.89 kB | 126.90 kB | 126.71 kB | bundle.min.js -1.25 MB | 656.81 kB | 646.76 kB | 164.16 kB | 163.73 kB | three.js +1.25 MB | 656.77 kB | 646.76 kB | 164.16 kB | 163.73 kB | three.js -2.14 MB | 735.33 kB | 724.14 kB | 180.99 kB | 181.07 kB | victory.js +2.14 MB | 727.32 kB | 724.14 kB | 180.32 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 332.27 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 332.26 kB | 331.56 kB | echarts.js -6.69 MB | 2.36 MB | 2.31 MB | 495.04 kB | 488.28 kB | antd.js +6.69 MB | 2.32 MB | 2.31 MB | 493.17 kB | 488.28 kB | antd.js -10.95 MB | 3.51 MB | 3.49 MB | 910.93 kB | 915.50 kB | typescript.js +10.95 MB | 3.51 MB | 3.49 MB | 910.45 kB | 915.50 kB | typescript.js