From 609361df502ec252c7b1db6745bf2e83461ab173 Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Thu, 10 Oct 2024 20:42:14 +0100 Subject: [PATCH] feat(minifier): implement folding `indexOf` and `lastIndexOf` string fns --- .../peephole_replace_known_methods/mod.rs | 159 +++++++++++++++--- .../string_methods.rs | 90 ++++++++++ 2 files changed, 228 insertions(+), 21 deletions(-) create mode 100644 crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods/string_methods.rs diff --git a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods/mod.rs b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods/mod.rs index 057b9f4ae603f5..44060d38143731 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods/mod.rs @@ -1,6 +1,9 @@ use cow_utils::CowUtils; use oxc_ast::ast::*; use oxc_traverse::{Traverse, TraverseCtx}; +use string_methods::{evaluate_string_index_of, evaluate_string_last_index_of}; + +mod string_methods; use crate::CompressorPass; @@ -41,25 +44,139 @@ impl PeepholeReplaceKnownMethods { let Expression::StaticMemberExpression(member) = &call_expr.callee else { return }; if let Expression::StringLiteral(string_lit) = &member.object { - if call_expr.arguments.len() == 0 { - 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, - }; - - if let Some(transformed_value) = transformed_value { - self.changed = true; - *node = ctx.ast.expression_from_string_literal(transformed_value); + 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, + ), + "substr" => self.try_fold_string_substr(node, ctx), + "substring" | "slice" => self.try_fold_string_substring_or_slice(node, ctx), + "charAt" => self.try_fold_string_char_at(node, ctx), + "charCodeAt" => self.try_fold_string_char_code_at(node, ctx), + "replace" => self.try_fold_string_replace(node, ctx), + "replaceAll" => self.try_fold_string_replace_all(node, ctx), + _ => None, + }; + + if let Some(replacement) = replacement { + self.changed = true; + *node = replacement; } } } + + fn try_fold_string_index_of<'a>( + &mut self, + span: Span, + call_expr: &CallExpression<'a>, + member: &StaticMemberExpression<'a>, + string_lit: &StringLiteral<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let search_value = match call_expr.arguments.first() { + Some(Argument::StringLiteral(string_lit)) => Some(string_lit.value.as_str()), + None => None, + _ => return None, + }; + + let search_start_index = match call_expr.arguments.get(1) { + Some(Argument::NumericLiteral(numeric_lit)) => Some(numeric_lit.value), + None => None, + _ => return None, + }; + + let result = if member.property.name.as_str() == "indexOf" { + evaluate_string_index_of(&string_lit.value, search_value, search_start_index) + } else { + evaluate_string_last_index_of(&string_lit.value, search_value, search_start_index) + }; + + #[expect(clippy::cast_precision_loss)] + return Some(ctx.ast.expression_from_numeric_literal(ctx.ast.numeric_literal( + span, + result as f64, + result.to_string(), + NumberBase::Decimal, + ))); + } + + fn try_fold_string_substr<'a>( + &mut self, + _node: &mut Expression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // TODO + None + } + + fn try_fold_string_substring_or_slice<'a>( + &mut self, + _node: &mut Expression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // TODO + None + } + + fn try_fold_string_char_at<'a>( + &mut self, + _node: &mut Expression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // TODO + None + } + + fn try_fold_string_char_code_at<'a>( + &mut self, + _node: &mut Expression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // TODO + None + } + + fn try_fold_string_replace<'a>( + &mut self, + _node: &mut Expression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // TODO + None + } + + fn try_fold_string_replace_all<'a>( + &mut self, + _node: &mut Expression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // TODO + None + } } /// Port from: @@ -88,7 +205,6 @@ mod test { } #[test] - #[ignore] fn test_string_index_of() { fold("x = 'abcdef'.indexOf('g')", "x = -1"); fold("x = 'abcdef'.indexOf('b')", "x = 1"); @@ -102,11 +218,12 @@ mod test { // Both elements must be strings. Don't do anything if either one is not // string. - fold("x = 'abc1def'.indexOf(1)", "x = 3"); - fold("x = 'abcNaNdef'.indexOf(NaN)", "x = 3"); - fold("x = 'abcundefineddef'.indexOf(undefined)", "x = 3"); - fold("x = 'abcnulldef'.indexOf(null)", "x = 3"); - fold("x = 'abctruedef'.indexOf(true)", "x = 3"); + // TODO: cast first arg to a string, and fold if possible. + // fold("x = 'abc1def'.indexOf(1)", "x = 3"); + // fold("x = 'abcNaNdef'.indexOf(NaN)", "x = 3"); + // fold("x = 'abcundefineddef'.indexOf(undefined)", "x = 3"); + // fold("x = 'abcnulldef'.indexOf(null)", "x = 3"); + // fold("x = 'abctruedef'.indexOf(true)", "x = 3"); // The following test case fails with JSC_PARSE_ERROR. Hence omitted. // fold_same("x = 1.indexOf('bcd');"); diff --git a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods/string_methods.rs b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods/string_methods.rs new file mode 100644 index 00000000000000..d4fca6ff7e7da8 --- /dev/null +++ b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods/string_methods.rs @@ -0,0 +1,90 @@ +#[expect(clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss)] +pub(super) fn evaluate_string_index_of<'a>( + string: &'a str, + search_value: Option<&'a str>, + from_index: Option, +) -> isize { + let from_index = from_index.map_or(0.0, |x| x.floor().max(0.0)) as usize; + + let Some(search_value) = search_value else { + return -1; + }; + let result = string.chars().skip(from_index).collect::().find(search_value); + result.map(|index| index + from_index).map_or(-1, |index| index as isize) +} + +#[expect(clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss)] +pub(super) fn evaluate_string_last_index_of<'a>( + string: &'a str, + search_value: Option<&'a str>, + from_index: Option, +) -> isize { + let Some(search_value) = search_value else { return -1 }; + + let from_index = + from_index.map_or(usize::MAX, |x| x.floor().max(0.0) as usize + search_value.len()); + + string + .chars() + .take(from_index) + .collect::() + .rfind(search_value) + .map_or(-1, |index| index as isize) +} + +#[cfg(test)] +mod test { + #[test] + fn test_evaluate_string_index_of() { + use super::evaluate_string_index_of; + + assert_eq!(evaluate_string_index_of("test test test", Some("t"), Some(0.0)), 0); + assert_eq!(evaluate_string_index_of("test test test", Some("t"), Some(1.0)), 3); + assert_eq!(evaluate_string_index_of("test test test", Some("t"), Some(4.0)), 5); + assert_eq!(evaluate_string_index_of("test test test", Some("t"), Some(4.1)), 5); + assert_eq!(evaluate_string_index_of("test test test", Some("t"), Some(0.0)), 0); + assert_eq!(evaluate_string_index_of("test test test", Some("t"), Some(-1.0)), 0); + assert_eq!(evaluate_string_index_of("test test test", Some("t"), Some(-1.0)), 0); + assert_eq!(evaluate_string_index_of("test test test", Some("t"), Some(-1.1)), 0); + assert_eq!( + evaluate_string_index_of("test test test", Some("t"), Some(-1_073_741_825.0)), + 0 + ); + assert_eq!(evaluate_string_index_of("test test test", Some("e"), Some(0.0)), 1); + assert_eq!(evaluate_string_index_of("test test test", Some("s"), Some(0.0)), 2); + + assert_eq!(evaluate_string_index_of("test test test", Some("test"), Some(4.0)), 5); + assert_eq!(evaluate_string_index_of("test test test", Some("test"), Some(5.0)), 5); + assert_eq!(evaluate_string_index_of("test test test", Some("test"), Some(6.0)), 10); + assert_eq!(evaluate_string_index_of("test test test", Some("test"), Some(0.0)), 0); + assert_eq!(evaluate_string_index_of("test test test", Some("test"), Some(-1.0)), 0); + assert_eq!(evaluate_string_index_of("test test test", Some("not found"), Some(-1.0)), -1); + assert_eq!(evaluate_string_index_of("test test test", Some("test"), Some(-1.0)), 0); + assert_eq!( + evaluate_string_index_of("test test test", Some("test"), Some(-1_073_741_825.0)), + 0 + ); + assert_eq!(evaluate_string_index_of("test test test", Some("test"), Some(0.0)), 0); + assert_eq!(evaluate_string_index_of("test test test", Some("notpresent"), Some(0.0)), -1); + assert_eq!(evaluate_string_index_of("test test test", None, Some(0.0)), -1); + } + + #[test] + fn test_evaluate_string_last_index_of() { + use super::evaluate_string_last_index_of; + assert_eq!(evaluate_string_last_index_of("test test test", Some("test"), Some(15.0)), 10); + assert_eq!(evaluate_string_last_index_of("test test test", Some("test"), Some(14.0)), 10); + assert_eq!(evaluate_string_last_index_of("test test test", Some("test"), Some(10.0)), 10); + assert_eq!(evaluate_string_last_index_of("test test test", Some("test"), Some(9.0)), 5); + assert_eq!(evaluate_string_last_index_of("test test test", Some("test"), Some(6.0)), 5); + assert_eq!(evaluate_string_last_index_of("test test test", Some("test"), Some(5.0)), 5); + assert_eq!(evaluate_string_last_index_of("test test test", Some("test"), Some(4.0)), 0); + assert_eq!(evaluate_string_last_index_of("test test test", Some("test"), Some(0.0)), 0); + assert_eq!( + evaluate_string_last_index_of("test test test", Some("notpresent"), Some(0.0)), + -1 + ); + assert_eq!(evaluate_string_last_index_of("test test test", None, Some(1.0)), -1); + assert_eq!(evaluate_string_last_index_of("abcdef", Some("b"), None), 1); + } +}