From a6b6b2d63d9c71358068577a1c2d8038cbee4817 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 | 114 +++++++++-- .../string_methods.rs | 190 ++++++++++++++++++ 2 files changed, 283 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..6134a5628eef66 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::StringUtils; + +mod string_methods; use crate::CompressorPass; @@ -41,25 +44,94 @@ 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); + #[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" => None, + "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, + 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" { + StringUtils::evaluate_string_index_of( + &string_lit.value, + search_value, + search_start_index, + ) + } else { + StringUtils::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, + ))); + } } /// Port from: @@ -88,7 +160,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 +173,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..48bb421928ee95 --- /dev/null +++ b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods/string_methods.rs @@ -0,0 +1,190 @@ +use oxc_syntax::number::ToJsInt32; + +pub(super) struct StringUtils; + +impl StringUtils { + #[expect(clippy::cast_possible_wrap, clippy::cast_sign_loss)] + pub(super) fn evaluate_string_index_of( + string: &str, + search_value: Option<&str>, + from_index: Option, + ) -> isize { + let from_index = from_index.map_or(0, |x| x.to_js_int_32().max(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_sign_loss)] + pub(super) fn evaluate_string_last_index_of( + string: &str, + search_value: Option<&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.to_js_int_32().max(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::StringUtils; + + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("t"), Some(0.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("t"), Some(1.0)), + 3 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("t"), Some(4.0)), + 5 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("t"), Some(4.1)), + 5 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("t"), Some(0.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("t"), Some(-1.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("t"), Some(-1.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("t"), Some(-1.1)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of( + "test test test", + Some("t"), + Some(-1_073_741_825.0) + ), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("e"), Some(0.0)), + 1 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("s"), Some(0.0)), + 2 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("test"), Some(4.0)), + 5 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("test"), Some(5.0)), + 5 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("test"), Some(6.0)), + 10 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("test"), Some(0.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("test"), Some(-1.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("not found"), Some(-1.0)), + -1 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("test"), Some(-1.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of( + "test test test", + Some("test"), + Some(-1_073_741_825.0) + ), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("test"), Some(0.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_index_of("test test test", Some("notpresent"), Some(0.0)), + -1 + ); + assert_eq!(StringUtils::evaluate_string_index_of("test test test", None, Some(0.0)), -1); + } + + #[test] + fn test_evaluate_string_last_index_of() { + use super::StringUtils; + assert_eq!( + StringUtils::evaluate_string_last_index_of("test test test", Some("test"), Some(15.0)), + 10 + ); + assert_eq!( + StringUtils::evaluate_string_last_index_of("test test test", Some("test"), Some(14.0)), + 10 + ); + assert_eq!( + StringUtils::evaluate_string_last_index_of("test test test", Some("test"), Some(10.0)), + 10 + ); + assert_eq!( + StringUtils::evaluate_string_last_index_of("test test test", Some("test"), Some(9.0)), + 5 + ); + assert_eq!( + StringUtils::evaluate_string_last_index_of("test test test", Some("test"), Some(6.0)), + 5 + ); + assert_eq!( + StringUtils::evaluate_string_last_index_of("test test test", Some("test"), Some(5.0)), + 5 + ); + assert_eq!( + StringUtils::evaluate_string_last_index_of("test test test", Some("test"), Some(4.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_last_index_of("test test test", Some("test"), Some(0.0)), + 0 + ); + assert_eq!( + StringUtils::evaluate_string_last_index_of( + "test test test", + Some("notpresent"), + Some(0.0) + ), + -1 + ); + assert_eq!( + StringUtils::evaluate_string_last_index_of("test test test", None, Some(1.0)), + -1 + ); + assert_eq!(StringUtils::evaluate_string_last_index_of("abcdef", Some("b"), None), 1); + } +}