Skip to content

Commit

Permalink
chore(minifier): add boilerplate for StatementFusion and `ExploitAs…
Browse files Browse the repository at this point in the history
…signs` (#5914)
  • Loading branch information
Boshen committed Sep 20, 2024
1 parent 943bd76 commit 9b3cc36
Show file tree
Hide file tree
Showing 5 changed files with 519 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ use oxc_traverse::{Traverse, TraverseCtx};

use crate::{CompressOptions, CompressorPass};

/// Collapse variable declarations (TODO: and assignments).
/// Collapse variable declarations.
///
/// `var a; var b = 1; var c = 2` => `var a, b = 1; c = 2`
/// TODO: `a = null; b = null;` => `a = b = null`
pub struct CollapseVariableDeclarations {
options: CompressOptions,
}
Expand Down
295 changes: 295 additions & 0 deletions crates/oxc_minifier/src/ast_passes/exploit_assigns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
use oxc_traverse::Traverse;

use crate::CompressorPass;

/// Tries to chain assignments together.
///
/// <https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/ExploitAssigns.java>
pub struct ExploitAssigns;

impl<'a> CompressorPass<'a> for ExploitAssigns {}

impl<'a> Traverse<'a> for ExploitAssigns {}

impl ExploitAssigns {
pub fn new() -> Self {
Self {}
}
}

#[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::ExploitAssigns::new();
tester::test(&allocator, source_text, expected, &mut pass);
}

fn test_same(source_text: &str) {
test(source_text, source_text);
}

#[test]
#[ignore]
fn test_expr_exploitation_types() {
test("a = true; b = true", "b = a = true");
test("a = !0; b = !0", "b = a = !0");
test("a = !1; b = !1", "b = a = !1");
test("a = void 0; b = void 0", "b = a = void 0");
test("a = -Infinity; b = -Infinity", "b = a = -Infinity");
}

#[test]
#[ignore]
fn test_expr_exploitation_types2() {
test("a = !0; b = !0", "b = a = !0");
}

#[test]
#[ignore]
fn nullish_coalesce() {
test("a = null; a ?? b;", "(a = null)??b");
test("a = true; if (a ?? a) { foo(); }", "if ((a = true) ?? a) { foo() }");
test("a = !0; if (a ?? a) { foo(); }", "if ((a = !0) ?? a) { foo() }");
}

#[test]
#[ignore]
fn test_expr_exploitation() {
test("a = null; b = null; var c = b", "var c = b = a = null");
test("a = null; b = null", "b = a = null");
test("a = undefined; b = undefined", "b = a = undefined");
test("a = 0; b = 0", "b=a=0");
test("a = 'foo'; b = 'foo'", "b = a = \"foo\"");
test("a = c; b = c", "b=a=c");

test_same("a = 0; b = 1");
test_same("a = \"foo\"; b = \"foox\"");

test("a = null; a && b;", "(a = null)&&b");
test("a = null; a || b;", "(a = null)||b");

test("a = null; a ? b : c;", "(a = null) ? b : c");

test("a = null; this.foo = null;", "this.foo = a = null");
test("function f(){ a = null; return null; }", "function f(){return a = null}");

test("a = true; if (a) { foo(); }", "if (a = true) { foo() }");
test("a = true; if (a && a) { foo(); }", "if ((a = true) && a) { foo() }");
test("a = false; if (a) { foo(); }", "if (a = false) { foo() }");

test("a = !0; if (a) { foo(); }", "if (a = !0) { foo() }");
test("a = !0; if (a && a) { foo(); }", "if ((a = !0) && a) { foo() }");
test("a = !1; if (a) { foo(); }", "if (a = !1) { foo() }");

test_same("a = this.foo; a();");
test("a = b; b = a;", "b = a = b");
test_same("a = b; a.c = a");
test("this.foo = null; this.bar = null;", "this.bar = this.foo = null");
test(
"this.foo = null; this.bar = null; this.baz = this.bar",
"this.baz = this.bar = this.foo = null",
);
test(
"this.foo = null; this.bar = null; this.baz = this?.bar",
"this.bar = this.foo = null; this.baz = this?.bar;",
);
test("this.foo = null; a = null;", "a = this.foo = null");
test("this.foo = null; a = this.foo;", "a = this.foo = null");
test_same("this.foo = null; a = this?.foo;");
test("a.b.c=null; a=null;", "a = a.b.c = null");
test_same("a = null; a.b.c = null");
test("(a=b).c = null; this.b = null;", "this.b = (a=b).c = null");
test_same("if(x) a = null; else b = a");
}

#[test]
#[ignore]
fn test_let_const_assignment() {
test("a = null; b = null; let c = b", "let c = b = a = null");
}

#[test]
#[ignore]
fn test_block_scope() {
test("{ a = null; b = null; c = b }", "{ c = b = a = null }");

// TODO (simranarora) What should we have as the intended behavior with block scoping?
test(
"a = null; b = null; { c = b; }",
// "{ c = b = a = null; }
"b = a = null; { c = b; }",
);
}

#[test]
#[ignore]
fn test_exploit_in_arrow_function() {
test("() => { a = null; return null; }", "() => { return a = null }");
}

#[test]
#[ignore]
fn test_nested_expr_exploitation() {
test(
"this.foo = null; this.bar = null; this.baz = null;",
"this.baz = this.bar = this.foo = null",
);

test(
"a = 3; this.foo = a; this.bar = a; this.baz = 3;",
"this.baz = this.bar = this.foo = a = 3",
);
test(
"a = 3; this.foo = a; this.bar = this.foo; this.baz = a;",
"this.baz = this.bar = this.foo = a = 3",
);
// recursively optimize assigns until optional chaining on RHS
test(
"a = 3; this.foo = a; this.bar = this?.foo; this.baz = a;",
"this.foo = a = 3; this.bar = this?.foo; this.baz = a;",
);
test(
"a = 3; this.foo = a; this.bar = 3; this.baz = this.foo;",
"this.baz = this.bar = this.foo = a = 3",
);
// recursively optimize assigns until optional chaining on RHS
test(
"a = 3; this.foo = a; this.bar = 3; this.baz = this?.foo;",
"this.bar = this.foo = a = 3; this.baz = this?.foo;",
);
// test(
// "a = 3; this.foo = a; a = 3; this.bar = 3; " + "a = 3; this.baz = this.foo;",
// "this.baz = a = this.bar = a = this.foo = a = 3",
// );
// recursively optimize assigns until optional chaining on RHS
// test(
// lines("a = 3; this.foo = a; a = 3; this.bar = 3; a = 3; this.baz = this?.foo;"),
// lines("a = this.bar = a = this.foo = a = 3; this.baz = this?.foo;"),
// );

// test(
// "a = 4; this.foo = a; a = 3; this.bar = 3; " + "a = 3; this.baz = this.foo;",
// "this.foo = a = 4; a = this.bar = a = 3; this.baz = this.foo",
// );
// recursively optimize assigns until optional chaining on RHS
// test(
// lines("a = 4; this.foo = a; a = 3; this.bar = 3; a = 3; this.baz = this?.foo;"),
// lines("this.foo = a = 4;", "a = this.bar = a = 3;", "this.baz = this?.foo;"),
// );

// test(
// "a = 3; this.foo = a; a = 4; this.bar = 3; " + "a = 3; this.baz = this.foo;",
// "this.foo = a = 3; a = 4; a = this.bar = 3; this.baz = this.foo",
// );
// test(
// lines("a = 3; this.foo = a; a = 4; this.bar = 3; ", "a = 3; this.baz = this?.foo;"),
// lines("this.foo = a = 3;", "a = 4;", "a = this.bar = 3;", "this.baz = this?.foo;"),
// );
// test(
// "a = 3; this.foo = a; a = 3; this.bar = 3; " + "a = 4; this.baz = this.foo;",
// "this.bar = a = this.foo = a = 3; a = 4; this.baz = this.foo",
// );
// test(
// lines("a = 3; this.foo = a; a = 3; this.bar = 3; a = 4; this.baz = this?.foo;"),
// lines("this.bar = a = this.foo = a = 3;", "a = 4;", "this.baz = this?.foo;"),
// );
}

#[test]
#[ignore]
fn test_bug1840071() {
// Some external properties are implemented as setters. Let's
// make sure that we don't collapse them inappropriately.
test("a.b = a.x; if (a.x) {}", "if (a.b = a.x) {}");
test_same("a.b = a?.x; if (a?.x) {}");
test_same("a.b = a.x; if (a.b) {}");
test("a.b = a.c = a.x; if (a.x) {}", "if (a.b = a.c = a.x) {}");
test_same("a.b = a.c = a?.x; if (a?.x) {}");

test_same("a.b = a.c = a.x; if (a.c) {}");
test_same("a.b = a.c = a.x; if (a.b) {}");
}

#[test]
#[ignore]
fn test_bug2072343() {
test_same("a = a.x;a = a.x");
test_same("a = a.x;b = a.x");
test("b = a.x;a = a.x", "a = b = a.x");
test_same("b = a?.x;a = a?.x");
test_same("a.x = a;a = a.x");
test_same("a.b = a.b.x;a.b = a.b.x");
test_same("a.y = a.y.x;b = a.y;c = a.y.x");
test("a = a.x;b = a;c = a.x", "b = a = a.x;c = a.x");
test("b = a.x;a = b;c = a.x", "a = b = a.x;c = a.x");
}

#[test]
#[ignore]
fn test_bad_collapse_into_call() {
// Can't collapse this, because if we did, 'foo' would be called
// in the wrong 'this' context.
test_same("this.foo = function() {}; this.foo();");
}

#[test]
#[ignore]
fn test_bad_collapse() {
test_same("this.$e$ = []; this.$b$ = null;");
}

#[test]
#[ignore]
fn test_issue1017() {
test_same("x = x.parentNode.parentNode; x = x.parentNode.parentNode;");
}

#[test]
#[ignore]
fn test_destructuring_lhs_array_ideal_behaviours() {
test_same("a => { ([a] = a); return a; }"); // `a` is being reassigned.
test_same("a => { ([b] = a); return a; }"); // Evaluating `b` could side-effect `a`.
test_same("a => { ([a = foo()] = a); return a; }"); // `foo` may be invoked.
test_same("(a, b) => { (a = [a] = b); return b; }"); // Evaluating `a` could side-effect `b`.
}

#[test]
#[ignore]
fn test_destructuring_lhs_array_backoff_behaviours() {
// TODO(b/123102446): We really like to collapse some of these chained assignments.

test_same("(a, b) => { ([a] = a = b); return b; }"); // The middle `a` is redundant.
test_same("(a, b) => { ([a] = a = b); return a; }"); // The middle `a` is redundant.
test(
"(a, b) => { (a = [a] = b); return a; }", // The final `a` is redundant.
"(a, b) => { return (a = [a] = b); }",
);
}

#[test]
#[ignore]
fn test_destructuring_lhs_object_ideal_behaviours() {
test_same("a => { ({a} = a); return a; }"); // `a` is being reassigned.
test_same("a => { ({b} = a); return a; }"); // Evaluating `b` could side-effect `a`.
test_same("a => { ({a = foo()} = a); return a; }"); // `foo` may be invoked.
test_same("(a, b) => { (a = {a} = b); return b; }"); // Evaluating `a` could side-effect `b`.
}

#[test]
#[ignore]
fn test_destructuring_lhs_object_backoff_behaviours() {
// TODO(b/123102446): We really like to collapse some of these chained assignments.

test_same("(a, b) => { ({a} = a = b); return b; }"); // The middle `a` is redundant.
test_same("(a, b) => { ({a} = a = b); return a; }"); // The middle `a` is redundant.
test(
"(a, b) => { (a = {a} = b); return a; }", // The final `a` is redundant.
"(a, b) => { return (a = {a} = b); }",
);
}
}
4 changes: 4 additions & 0 deletions crates/oxc_minifier/src/ast_passes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
mod collapse_variable_declarations;
mod exploit_assigns;
mod peephole_fold_constants;
mod peephole_minimize_conditions;
mod peephole_remove_dead_code;
mod peephole_substitute_alternate_syntax;
mod remove_syntax;
mod statement_fusion;

pub use collapse_variable_declarations::CollapseVariableDeclarations;
pub use exploit_assigns::ExploitAssigns;
pub use peephole_fold_constants::PeepholeFoldConstants;
pub use peephole_minimize_conditions::PeepholeMinimizeConditions;
pub use peephole_remove_dead_code::PeepholeRemoveDeadCode;
pub use peephole_substitute_alternate_syntax::PeepholeSubstituteAlternateSyntax;
pub use remove_syntax::RemoveSyntax;
pub use statement_fusion::StatementFusion;

use oxc_ast::ast::Program;
use oxc_semantic::{ScopeTree, SymbolTable};
Expand Down
Loading

0 comments on commit 9b3cc36

Please sign in to comment.