diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index 20a66970bc6e1..7069fe582db3c 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -39,3 +39,51 @@ pub trait CompressorPass<'a>: Traverse<'a> { fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>); } + +#[cfg(test)] +mod test { + use super::*; + use oxc_allocator::Allocator; + use oxc_ast::ast::Statement; + use oxc_parser::Parser; + use oxc_semantic::SemanticBuilder; + use oxc_span::SourceType; + + #[derive(Default)] + pub struct Tester; + + fn build<'a>(allocator: &'a Allocator, source_text: &'a str) -> (TraverseCtx<'a>, Program<'a>) { + let source_type = SourceType::mjs(); + + let program = Parser::new(allocator, source_text, source_type).parse().program; + let (symbols, scopes) = + SemanticBuilder::new().build(&program).semantic.into_symbol_table_and_scope_tree(); + + (TraverseCtx::new(scopes, symbols, allocator), program) + } + + fn get_string_literal(source_text: &str) -> Option { + let allocator = Allocator::default(); + let (ctx, program) = build(&allocator, source_text); + + let Some(Statement::ExpressionStatement(expr_stmt)) = program.body.first() else { + return None; + }; + + ctx.get_string_literal(&expr_stmt.expression).map(Into::into) + } + + #[test] + fn test_get_string_literal() { + assert_eq!(get_string_literal("`abc`"), Some("abc".to_string())); + assert_ne!(get_string_literal("`a${b}`"), Some("ab".to_string())); + assert_eq!(get_string_literal("`${null}`"), Some("null".to_string())); + assert_eq!(get_string_literal("`${undefined}`"), Some("undefined".to_string())); + assert_eq!(get_string_literal("`${{}}123`"), Some("[object Object]123".to_string())); + assert_eq!(get_string_literal("`a${1}${true}${NaN}0`"), Some("a1trueNaN0".to_string())); + + // assert_eq!(get_string_literal("`${1,2}`"), Some("2".to_string())); + // assert_eq!(get_string_literal("`${[]}${[1,2]}`"), Some("1,2".to_string())); + // assert_eq!(get_string_literal("`${new Set()}`"), Some("[object Set]".to_string())); + } +} diff --git a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs index c89667e708e7a..232d2b5d13118 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs @@ -3,7 +3,7 @@ use oxc_ast::ast::*; use oxc_ecmascript::{StringCharAt, StringIndexOf, StringLastIndexOf}; use oxc_traverse::{Traverse, TraverseCtx}; -use crate::CompressorPass; +use crate::{node_util::NodeUtil, CompressorPass}; /// Minimize With Known Methods /// @@ -40,62 +40,56 @@ impl PeepholeReplaceKnownMethods { ) { let Expression::CallExpression(call_expr) = node else { return }; - let Expression::StaticMemberExpression(member) = &call_expr.callee else { return }; - if let Expression::StringLiteral(string_lit) = &member.object { - #[expect(clippy::match_same_arms)] - let replacement = match member.property.name.as_str() { - "toLowerCase" | "toUpperCase" | "trim" => { - let transformed_value = - match member.property.name.as_str() { - "toLowerCase" => Some(ctx.ast.string_literal( - call_expr.span, - string_lit.value.cow_to_lowercase(), - )), - "toUpperCase" => Some(ctx.ast.string_literal( - call_expr.span, - string_lit.value.cow_to_uppercase(), - )), - "trim" => Some( - ctx.ast.string_literal(call_expr.span, string_lit.value.trim()), - ), - _ => None, - }; - - transformed_value.map(|transformed_value| { - ctx.ast.expression_from_string_literal(transformed_value) - }) - } - "indexOf" | "lastIndexOf" => Self::try_fold_string_index_of( - call_expr.span, - call_expr, - member, - string_lit, - ctx, - ), - // TODO: Implement the rest of the string methods - "substr" => None, - "substring" | "slice" => None, - "charAt" => { - Self::try_fold_string_char_at(call_expr.span, call_expr, string_lit, ctx) - } - "charCodeAt" => None, - "replace" => None, - "replaceAll" => None, - _ => None, - }; - - if let Some(replacement) = replacement { - self.changed = true; - *node = replacement; + let Some(mem_expr) = call_expr.callee.as_member_expression() else { return }; + let Some(string_lit) = ctx.get_string_literal(mem_expr.object()) else { return }; + let Some(method_name) = mem_expr.static_property_name() else { return }; + + #[expect(clippy::match_same_arms)] + let replacement = match method_name { + "toLowerCase" | "toUpperCase" | "trim" => { + let transformed_value = match method_name { + "toLowerCase" => { + Some(ctx.ast.string_literal(call_expr.span, string_lit.cow_to_lowercase())) + } + "toUpperCase" => { + Some(ctx.ast.string_literal(call_expr.span, string_lit.cow_to_uppercase())) + } + "trim" => Some(ctx.ast.string_literal(call_expr.span, string_lit.trim())), + _ => None, + }; + + transformed_value.map(|transformed_value| { + ctx.ast.expression_from_string_literal(transformed_value) + }) } + "indexOf" | "lastIndexOf" => Self::try_fold_string_index_of( + call_expr.span, + &string_lit, + method_name, + call_expr, + ctx, + ), + // TODO: Implement the rest of the string methods + "substr" => None, + "substring" | "slice" => None, + "charAt" => Self::try_fold_string_char_at(call_expr.span, &string_lit, call_expr, ctx), + "charCodeAt" => None, + "replace" => None, + "replaceAll" => None, + _ => None, + }; + + if let Some(replacement) = replacement { + self.changed = true; + *node = replacement; } } fn try_fold_string_index_of<'a>( span: Span, + string_lit: &str, + method_name: &str, call_expr: &CallExpression<'a>, - member: &StaticMemberExpression<'a>, - string_lit: &StringLiteral<'a>, ctx: &mut TraverseCtx<'a>, ) -> Option> { let search_value = match call_expr.arguments.first() { @@ -110,11 +104,9 @@ impl PeepholeReplaceKnownMethods { _ => return None, }; - let result = match member.property.name.as_str() { - "indexOf" => string_lit.value.as_str().index_of(search_value, search_start_index), - "lastIndexOf" => { - string_lit.value.as_str().last_index_of(search_value, search_start_index) - } + let result = match method_name { + "indexOf" => string_lit.index_of(search_value, search_start_index), + "lastIndexOf" => string_lit.last_index_of(search_value, search_start_index), _ => unreachable!(), }; @@ -129,8 +121,8 @@ impl PeepholeReplaceKnownMethods { fn try_fold_string_char_at<'a>( span: Span, + string_lit: &str, call_expr: &CallExpression<'a>, - string_lit: &StringLiteral<'a>, ctx: &mut TraverseCtx<'a>, ) -> Option> { let char_at_index: Option = match call_expr.arguments.first() { @@ -147,11 +139,7 @@ impl PeepholeReplaceKnownMethods { _ => return None, }; - let result = &string_lit - .value - .as_str() - .char_at(char_at_index) - .map_or(String::new(), |v| v.to_string()); + let result = string_lit.char_at(char_at_index).map_or(String::new(), |v| v.to_string()); return Some(ctx.ast.expression_from_string_literal(ctx.ast.string_literal(span, result))); } @@ -217,7 +205,7 @@ mod test { fold_same("x = 'abcdef'.indexOf([1,2])"); // Template Strings - fold_same("x = `abcdef`.indexOf('b')"); + fold("x = `abcdef`.indexOf('b')", "x = 1;"); fold_same("x = `Hello ${name}`.indexOf('a')"); fold_same("x = tag `Hello ${name}`.indexOf('a')"); } @@ -431,7 +419,7 @@ mod test { // fold("x = '\\ud834\udd1e'.charAt(1)", "x = '\\udd1e'"); // Template strings - fold_same("x = `abcdef`.charAt(0)"); + fold("x = `abcdef`.charAt(0)", "x = 'a'"); fold_same("x = `abcdef ${abc}`.charAt(0)"); } @@ -546,7 +534,7 @@ mod test { fold("'A'.toUpperCase()", "'A'"); fold("'aBcDe'.toUpperCase()", "'ABCDE'"); - fold_same("`abc`.toUpperCase()"); + fold("`abc`.toUpperCase()", "'ABC';"); fold_same("`a ${bc}`.toUpperCase()"); /* @@ -578,7 +566,7 @@ mod test { fold("'a'.toLowerCase()", "'a'"); fold("'aBcDe'.toLowerCase()", "'abcde'"); - fold_same("`ABC`.toLowerCase()"); + fold("`ABC`.toLowerCase()", "'abc'"); fold_same("`A ${BC}`.toLowerCase()"); /* diff --git a/crates/oxc_minifier/src/node_util/mod.rs b/crates/oxc_minifier/src/node_util/mod.rs index f01a90ccb4b90..d9a77419324c4 100644 --- a/crates/oxc_minifier/src/node_util/mod.rs +++ b/crates/oxc_minifier/src/node_util/mod.rs @@ -116,6 +116,15 @@ pub trait NodeUtil<'a> { expr.to_big_int() } + /// Retrieve the literal value of a string, such as `abc` or "abc". + fn get_string_literal(&self, expr: &Expression<'a>) -> Option> { + match expr { + Expression::StringLiteral(lit) => Some(Cow::Borrowed(lit.value.as_str())), + Expression::TemplateLiteral(_) => Some(self.get_string_value(expr)?), + _ => None, + } + } + /// Port from [closure-compiler](https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/NodeUtil.java#L234) /// Gets the value of a node as a String, or `None` if it cannot be converted. When it returns a /// String, this method effectively emulates the `String()` JavaScript cast function.