From 4a29845d82ca63bf0da11dea067fdf50712e9aea Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:02:22 +0000 Subject: [PATCH] feat(minifier): add `ConvertToDottedProperties` (#8212) --- .../convert_to_dotted_properties.rs | 329 ++++++++++++++++++ crates/oxc_minifier/src/ast_passes/mod.rs | 8 +- .../peephole_substitute_alternate_syntax.rs | 295 ---------------- 3 files changed, 335 insertions(+), 297 deletions(-) create mode 100644 crates/oxc_minifier/src/ast_passes/convert_to_dotted_properties.rs 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..600abcbe685cb --- /dev/null +++ b/crates/oxc_minifier/src/ast_passes/convert_to_dotted_properties.rs @@ -0,0 +1,329 @@ +use oxc_ast::ast::*; +use oxc_syntax::identifier::is_identifier_name; +use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx}; + +use crate::{node_util::Ctx, 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, + in_fixed_loop: bool, +} + +impl<'a> CompressorPass<'a> for ConvertToDottedProperties { + fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { + self.changed = true; + traverse_mut_with_ctx(self, program, ctx); + } +} + +impl<'a> Traverse<'a> for ConvertToDottedProperties { + fn exit_property_key(&mut self, key: &mut PropertyKey<'a>, ctx: &mut TraverseCtx<'a>) { + if !self.in_fixed_loop { + self.try_compress_property_key(key, ctx); + } + } + + fn exit_member_expression( + &mut self, + expr: &mut MemberExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if !self.in_fixed_loop { + self.try_compress_computed_member_expression(expr, Ctx(ctx)); + } + } +} + +impl<'a> ConvertToDottedProperties { + pub fn new(in_fixed_loop: bool) -> Self { + Self { changed: false, in_fixed_loop } + } + + // https://github.com/swc-project/swc/blob/4e2dae558f60a9f5c6d2eac860743e6c0b2ec562/crates/swc_ecma_minifier/src/compress/pure/properties.rs + #[allow(clippy::cast_lossless)] + fn try_compress_property_key(&mut self, key: &mut PropertyKey<'a>, ctx: &mut TraverseCtx<'a>) { + let PropertyKey::StringLiteral(s) = key else { return }; + if match ctx.parent() { + Ancestor::ObjectPropertyKey(key) => *key.computed(), + Ancestor::BindingPropertyKey(key) => *key.computed(), + Ancestor::MethodDefinitionKey(key) => *key.computed(), + Ancestor::PropertyDefinitionKey(key) => *key.computed(), + Ancestor::AccessorPropertyKey(key) => *key.computed(), + _ => true, + } { + return; + } + if is_identifier_name(&s.value) { + self.changed = true; + *key = PropertyKey::StaticIdentifier( + ctx.ast.alloc_identifier_name(s.span, s.value.clone()), + ); + } else if (!s.value.starts_with('0') && !s.value.starts_with('+')) || s.value.len() <= 1 { + if let Ok(value) = s.value.parse::() { + self.changed = true; + *key = PropertyKey::NumericLiteral(ctx.ast.alloc_numeric_literal( + s.span, + value as f64, + None, + NumberBase::Decimal, + )); + } + } + } + + /// `foo['bar']` -> `foo.bar` + /// `foo?.['bar']` -> `foo?.bar` + fn try_compress_computed_member_expression( + &mut self, + expr: &mut MemberExpression<'a>, + ctx: Ctx<'a, '_>, + ) { + if let MemberExpression::ComputedMemberExpression(e) = expr { + let Expression::StringLiteral(s) = &e.expression else { return }; + if !is_identifier_name(&s.value) { + return; + } + let property = ctx.ast.identifier_name(s.span, s.value.clone()); + let object = ctx.ast.move_expression(&mut e.object); + *expr = MemberExpression::StaticMemberExpression( + ctx.ast.alloc_static_member_expression(e.span, object, property, e.optional), + ); + self.changed = true; + } + } +} + +#[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(false); + tester::test(&allocator, source_text, expected, &mut pass); + } + + fn test_same(source_text: &str) { + test(source_text, source_text); + } + + #[test] + fn test_object_key() { + test("({ '0': _, 'a': _ })", "({ 0: _, a: _ })"); + test_same("({ '1.1': _, '😊': _, 'a.a': _ })"); + } + + #[test] + fn test_computed_to_member_expression() { + test("x['true']", "x.true"); + test_same("x['😊']"); + } + + #[test] + fn test_convert_to_dotted_properties_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 always safe. + test("a['\\u0041A']", "a.AA"); + // This is safe for ES5+. (keywords cannot be used for ES3) + test("a['default']", "a.default"); + // This is safe for ES2015+. (\u1d17 was introduced in Unicode 3.1, ES2015+ uses Unicode 5.1+) + test("a['\\u1d17A']", "a.\u{1d17}A"); + // Latin capital N with tilde - this is safe for ES3+. + test("a['\\u00d1StuffAfter']", "a.\u{00d1}StuffAfter"); + } + + #[test] + fn test_convert_to_dotted_properties_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()]"); + // Ignorable control characters are ok in Java identifiers, but not in JS. + test_same("a['A\\u0004']"); + } + + #[test] + fn test_convert_to_dotted_properties_already_dotted() { + test_same("a.b"); + test_same("var a = {b: 0};"); + } + + #[test] + fn test_convert_to_dotted_properties_quoted_props() { + test_same("({'':0})"); + test_same("({'1.0':0})"); + test("({'\\u1d17A':0})", "({ \u{1d17}A: 0 })"); + test_same("({'a\\u0004b':0})"); + } + + #[test] + fn test5746867() { + test_same("var a = { '$\\\\' : 5 };"); + test_same("var a = { 'x\\\\u0041$\\\\' : 5 };"); + } + + #[test] + fn test_convert_to_dotted_properties_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()]"); + // This is safe for ES5+. (keywords cannot be used for ES3) + test("a?.['default']", "a?.default"); + } + + #[test] + #[ignore] + fn test_convert_to_dotted_properties_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_convert_to_dotted_properties_computed_property_with_default_value() { + test("const {['o']: o = 0} = {};", "const {o:o = 0} = {};"); + } + + #[test] + fn test_convert_to_dotted_properties_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 23925cc7fa156..adcd45daf8c29 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 minimize_exit_points; mod normalize; @@ -16,6 +17,7 @@ mod remove_unused_code; mod statement_fusion; pub use collapse_variable_declarations::CollapseVariableDeclarations; +pub use convert_to_dotted_properties::ConvertToDottedProperties; pub use exploit_assigns::ExploitAssigns; pub use minimize_exit_points::MinimizeExitPoints; pub use normalize::Normalize; @@ -44,6 +46,7 @@ pub struct PeepholeOptimizations { x6_peephole_substitute_alternate_syntax: PeepholeSubstituteAlternateSyntax, x7_peephole_replace_known_methods: PeepholeReplaceKnownMethods, x8_peephole_fold_constants: PeepholeFoldConstants, + x9_convert_to_dotted_properties: ConvertToDottedProperties, } impl PeepholeOptimizations { @@ -61,6 +64,7 @@ impl PeepholeOptimizations { ), x7_peephole_replace_known_methods: PeepholeReplaceKnownMethods::new(), x8_peephole_fold_constants: PeepholeFoldConstants::new(), + x9_convert_to_dotted_properties: ConvertToDottedProperties::new(in_fixed_loop), } } @@ -171,7 +175,7 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { } fn exit_property_key(&mut self, key: &mut PropertyKey<'a>, ctx: &mut TraverseCtx<'a>) { - self.x6_peephole_substitute_alternate_syntax.exit_property_key(key, ctx); + self.x9_convert_to_dotted_properties.exit_property_key(key, ctx); } fn exit_member_expression( @@ -179,7 +183,7 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { expr: &mut MemberExpression<'a>, ctx: &mut TraverseCtx<'a>, ) { - self.x6_peephole_substitute_alternate_syntax.exit_member_expression(expr, ctx); + self.x9_convert_to_dotted_properties.exit_member_expression(expr, ctx); } fn exit_catch_clause(&mut self, catch: &mut CatchClause<'a>, ctx: &mut TraverseCtx<'a>) { diff --git a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs index e8c3e1efdb9d2..db7c71378ea25 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs @@ -5,7 +5,6 @@ use oxc_semantic::IsGlobalReference; use oxc_span::{GetSpan, SPAN}; use oxc_syntax::{ es_target::ESTarget, - identifier::is_identifier_name, number::NumberBase, operator::{BinaryOperator, UnaryOperator}, }; @@ -84,18 +83,6 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax { self.in_define_export = false; } - fn exit_property_key(&mut self, key: &mut PropertyKey<'a>, ctx: &mut TraverseCtx<'a>) { - self.try_compress_property_key(key, ctx); - } - - fn exit_member_expression( - &mut self, - expr: &mut MemberExpression<'a>, - ctx: &mut TraverseCtx<'a>, - ) { - self.try_compress_computed_member_expression(expr, Ctx(ctx)); - } - fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { let ctx = Ctx(ctx); @@ -705,66 +692,6 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax { Self::array_literal(ctx.ast.vec(), ctx) } - // https://github.com/swc-project/swc/blob/4e2dae558f60a9f5c6d2eac860743e6c0b2ec562/crates/swc_ecma_minifier/src/compress/pure/properties.rs - #[allow(clippy::cast_lossless)] - fn try_compress_property_key(&mut self, key: &mut PropertyKey<'a>, ctx: &mut TraverseCtx<'a>) { - if self.in_fixed_loop { - return; - } - let PropertyKey::StringLiteral(s) = key else { return }; - if match ctx.parent() { - Ancestor::ObjectPropertyKey(key) => *key.computed(), - Ancestor::BindingPropertyKey(key) => *key.computed(), - Ancestor::MethodDefinitionKey(key) => *key.computed(), - Ancestor::PropertyDefinitionKey(key) => *key.computed(), - Ancestor::AccessorPropertyKey(key) => *key.computed(), - _ => true, - } { - return; - } - if is_identifier_name(&s.value) { - self.changed = true; - *key = PropertyKey::StaticIdentifier( - ctx.ast.alloc_identifier_name(s.span, s.value.clone()), - ); - } else if (!s.value.starts_with('0') && !s.value.starts_with('+')) || s.value.len() <= 1 { - if let Ok(value) = s.value.parse::() { - self.changed = true; - *key = PropertyKey::NumericLiteral(ctx.ast.alloc_numeric_literal( - s.span, - value as f64, - None, - NumberBase::Decimal, - )); - } - } - } - - /// `foo['bar']` -> `foo.bar` - /// `foo?.['bar']` -> `foo?.bar` - fn try_compress_computed_member_expression( - &mut self, - expr: &mut MemberExpression<'a>, - ctx: Ctx<'a, 'b>, - ) { - if self.in_fixed_loop { - return; - } - - if let MemberExpression::ComputedMemberExpression(e) = expr { - let Expression::StringLiteral(s) = &e.expression else { return }; - if !is_identifier_name(&s.value) { - return; - } - let property = ctx.ast.identifier_name(s.span, s.value.clone()); - let object = ctx.ast.move_expression(&mut e.object); - *expr = MemberExpression::StaticMemberExpression( - ctx.ast.alloc_static_member_expression(e.span, object, property, e.optional), - ); - self.changed = true; - } - } - fn compress_catch_clause(&mut self, catch: &mut CatchClause<'a>) { if catch.body.body.is_empty() && !self.in_fixed_loop @@ -1271,18 +1198,6 @@ mod test { test("`number` !== typeof foo", "'number' != typeof foo"); } - #[test] - fn test_object_key() { - test("({ '0': _, 'a': _ })", "({ 0: _, a: _ })"); - test_same("({ '1.1': _, '😊': _, 'a.a': _ })"); - } - - #[test] - fn test_computed_to_member_expression() { - test("x['true']", "x.true"); - test_same("x['😊']"); - } - #[test] fn optional_catch_binding() { test("try {} catch(e) {}", "try {} catch {}"); @@ -1298,214 +1213,4 @@ mod test { let code = "try {} catch(e) {}"; tester::test(&allocator, code, code, &mut pass); } - - /// Port from - mod convert_to_dotted_properties { - use super::{test, test_same}; - - #[test] - fn test_convert_to_dotted_properties_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 always safe. - test("a['\\u0041A']", "a.AA"); - // This is safe for ES5+. (keywords cannot be used for ES3) - test("a['default']", "a.default"); - // This is safe for ES2015+. (\u1d17 was introduced in Unicode 3.1, ES2015+ uses Unicode 5.1+) - test("a['\\u1d17A']", "a.\u{1d17}A"); - // Latin capital N with tilde - this is safe for ES3+. - test("a['\\u00d1StuffAfter']", "a.\u{00d1}StuffAfter"); - } - - #[test] - fn test_convert_to_dotted_properties_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()]"); - // Ignorable control characters are ok in Java identifiers, but not in JS. - test_same("a['A\\u0004']"); - } - - #[test] - fn test_convert_to_dotted_properties_already_dotted() { - test_same("a.b"); - test_same("var a = {b: 0};"); - } - - #[test] - fn test_convert_to_dotted_properties_quoted_props() { - test_same("({'':0})"); - test_same("({'1.0':0})"); - test("({'\\u1d17A':0})", "({ \u{1d17}A: 0 })"); - test_same("({'a\\u0004b':0})"); - } - - #[test] - fn test5746867() { - test_same("var a = { '$\\\\' : 5 };"); - test_same("var a = { 'x\\\\u0041$\\\\' : 5 };"); - } - - #[test] - fn test_convert_to_dotted_properties_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()]"); - // This is safe for ES5+. (keywords cannot be used for ES3) - test("a?.['default']", "a?.default"); - } - - #[test] - #[ignore] - fn test_convert_to_dotted_properties_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_convert_to_dotted_properties_computed_property_with_default_value() { - test("const {['o']: o = 0} = {};", "const {o:o = 0} = {};"); - } - - #[test] - fn test_convert_to_dotted_properties_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; - ", - ); - } - } }