Skip to content

Commit

Permalink
feat(minifier): minimize computed property access
Browse files Browse the repository at this point in the history
  • Loading branch information
sapphi-red committed Dec 28, 2024
1 parent 37c9959 commit 4c78078
Show file tree
Hide file tree
Showing 3 changed files with 311 additions and 10 deletions.
277 changes: 277 additions & 0 deletions crates/oxc_minifier/src/ast_passes/convert_to_dotted_properties.rs
Original file line number Diff line number Diff line change
@@ -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.
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/ConvertToDottedProperties.java>
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 <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/ConvertToDottedPropertiesTest.java>
#[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;
",
);
}
}
24 changes: 24 additions & 0 deletions crates/oxc_minifier/src/ast_passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
}
}

Expand All @@ -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 {
Expand All @@ -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>(
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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);
}
Expand Down
Loading

0 comments on commit 4c78078

Please sign in to comment.