Skip to content

Commit

Permalink
feat(minifier): remove unnecessary "use strict"
Browse files Browse the repository at this point in the history
  • Loading branch information
sapphi-red committed Dec 28, 2024
1 parent 37c9959 commit e88cb55
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,14 @@ mod test {
fn test_nospace(source_text: &str, expected: &str) {
let allocator = Allocator::default();
let mut pass = super::PeepholeFoldConstants::new();
tester::test_impl(&allocator, source_text, expected, &mut pass, true);
tester::test_impl(
&allocator,
source_text,
expected,
&mut pass,
oxc_span::SourceType::mjs(),
true,
);
}

fn test_same(source_text: &str) {
Expand Down
90 changes: 90 additions & 0 deletions crates/oxc_minifier/src/ast_passes/remove_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ impl<'a> CompressorPass<'a> for RemoveSyntax {
}

impl<'a> Traverse<'a> for RemoveSyntax {
fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
Self::drop_use_strict_directives_in_program(program, ctx);
}

fn enter_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) {
Self::drop_use_strict_directives_in_function_body(body, ctx);
}

fn exit_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) {
Self::drop_use_strict_directives_if_function_is_empty(body, ctx);
}

fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, _ctx: &mut TraverseCtx<'a>) {
stmts.retain(|stmt| {
!(matches!(stmt, Statement::EmptyStatement(_))
Expand Down Expand Up @@ -89,6 +101,38 @@ impl<'a> RemoveSyntax {
let Some(ident) = obj.get_identifier_reference() else { return false };
ident.name == "console"
}

/// Drop `"use strict";` directives if the input is strict mode (e.g. written in ESM).
fn drop_use_strict_directives_in_program(
program: &mut Program<'a>,
_ctx: &mut TraverseCtx<'a>,
) {
if program.source_type.is_strict() {
program.directives.retain(|directive| !directive.is_use_strict());
}
}

/// Drop `"use strict";` directives if the parent scope is already strict mode.
fn drop_use_strict_directives_in_function_body(
body: &mut FunctionBody<'a>,
ctx: &mut TraverseCtx<'a>,
) {
let current_scope_id = ctx.current_scope_id();
let Some(parent_scope_id) = ctx.scopes().get_parent_id(current_scope_id) else { return };
if ctx.scopes().get_flags(parent_scope_id).is_strict_mode() {
body.directives.retain(|directive| !directive.is_use_strict());
}
}

/// Drop `"use strict";` directives if the function is empty.
fn drop_use_strict_directives_if_function_is_empty(
body: &mut FunctionBody<'a>,
_ctx: &mut TraverseCtx<'a>,
) {
if body.statements.is_empty() {
body.directives.retain(|directive| !directive.is_use_strict());
}
}
}

#[cfg(test)]
Expand All @@ -103,6 +147,23 @@ mod test {
tester::test(&allocator, source_text, expected, &mut pass);
}

fn test_script(source_text: &str, expected: &str) {
let allocator = Allocator::default();
let mut pass = super::RemoveSyntax::new(CompressOptions::all_true());
tester::test_impl(
&allocator,
source_text,
expected,
&mut pass,
oxc_span::SourceType::cjs(),
true,
);
}

fn test_script_same(source_text: &str) {
test_script(source_text, source_text);
}

#[test]
fn parens() {
test("(((x)))", "x");
Expand All @@ -118,4 +179,33 @@ mod test {
fn drop_debugger() {
test("debugger", "");
}

#[test]
fn use_strict() {
test("'use strict';", "");

test_script(
"'use strict'; function foo() { 'use strict'; alert(1); }",
"'use strict'; function foo() { alert(1); }",
);
test_script(
"'use strict'; const foo = () => { 'use strict'; alert(1); }",
"'use strict'; const foo = () => { alert(1); }",
);
test_script_same("function foo() { 'use strict'; alert(1); }");
test_script(
"function foo() { 'use strict'; return function foo() { 'use strict'; alert(1); }; } ",
"function foo() { 'use strict'; return function foo() { alert(1); }; } ",
);
test_script(
"class Foo { foo() { 'use strict'; alert(1); } } ",
"class Foo { foo() { alert(1); } } ",
);
test_script(
"const Foo = class { foo() { 'use strict'; alert(1); } } ",
"const Foo = class { foo() { alert(1); } } ",
);

test_script("function foo() { 'use strict';}", "function foo() {}");
}
}
9 changes: 5 additions & 4 deletions crates/oxc_minifier/src/tester.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,29 @@ pub fn test<'a, P: CompressorPass<'a>>(
expected: &'a str,
pass: &mut P,
) {
test_impl(allocator, source_text, expected, pass, false);
test_impl(allocator, source_text, expected, pass, SourceType::mjs(), false);
}

pub fn test_impl<'a, P: CompressorPass<'a>>(
allocator: &'a Allocator,
source_text: &'a str,
expected: &'a str,
pass: &mut P,
source_type: SourceType,
remove_whitespace: bool,
) {
let result = run(allocator, source_text, Some(pass), remove_whitespace);
let expected = run::<P>(allocator, expected, None, remove_whitespace);
let result = run(allocator, source_text, Some(pass), source_type, remove_whitespace);
let expected = run::<P>(allocator, expected, None, source_type, remove_whitespace);
assert_eq!(result, expected, "\nfor source\n{source_text}\nexpect\n{expected}\ngot\n{result}");
}

fn run<'a, P: CompressorPass<'a>>(
allocator: &'a Allocator,
source_text: &'a str,
pass: Option<&mut P>,
source_type: SourceType,
remove_whitespace: bool,
) -> String {
let source_type = SourceType::mjs();
let mut program = Parser::new(allocator, source_text, source_type).parse().program;

if let Some(pass) = pass {
Expand Down
6 changes: 3 additions & 3 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
| Oxc | ESBuild | Oxc | ESBuild |
Original | minified | minified | gzip | gzip | Fixture
-------------------------------------------------------------------------------------
72.14 kB | 23.74 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js
72.14 kB | 23.72 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

Expand All @@ -17,11 +17,11 @@ Original | minified | minified | gzip | gzip | Fixture

1.25 MB | 656.81 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 | 735.31 kB | 724.14 kB | 180.98 kB | 181.07 kB | victory.js

3.20 MB | 1.01 MB | 1.01 MB | 332.27 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.36 MB | 2.31 MB | 494.94 kB | 488.28 kB | antd.js

10.95 MB | 3.51 MB | 3.49 MB | 910.93 kB | 915.50 kB | typescript.js

2 changes: 1 addition & 1 deletion tasks/minsize/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pub fn run() -> Result<(), io::Error> {
}

fn minify_twice(file: &TestFile) -> String {
let source_type = SourceType::from_path(&file.file_name).unwrap();
let source_type = SourceType::from_path(&file.file_name).unwrap().with_script(true);
let options = MinifierOptions {
mangle: Some(MangleOptions::default()),
compress: CompressOptions::default(),
Expand Down

0 comments on commit e88cb55

Please sign in to comment.