diff --git a/crates/oxc_ecmascript/src/lib.rs b/crates/oxc_ecmascript/src/lib.rs index ebc3612fa1d13..1c3d635c7623a 100644 --- a/crates/oxc_ecmascript/src/lib.rs +++ b/crates/oxc_ecmascript/src/lib.rs @@ -11,6 +11,7 @@ mod string_char_at; mod string_char_code_at; mod string_index_of; mod string_last_index_of; +mod string_substring; mod string_to_big_int; mod string_to_number; mod to_big_int; @@ -30,6 +31,7 @@ pub use self::{ private_bound_identifiers::PrivateBoundIdentifiers, prop_name::PropName, string_char_at::StringCharAt, string_char_code_at::StringCharCodeAt, string_index_of::StringIndexOf, string_last_index_of::StringLastIndexOf, - string_to_big_int::StringToBigInt, string_to_number::StringToNumber, to_big_int::ToBigInt, - to_boolean::ToBoolean, to_int_32::ToInt32, to_number::ToNumber, to_string::ToJsString, + string_substring::StringSubstring, string_to_big_int::StringToBigInt, + string_to_number::StringToNumber, to_big_int::ToBigInt, to_boolean::ToBoolean, + to_int_32::ToInt32, to_number::ToNumber, to_string::ToJsString, }; diff --git a/crates/oxc_ecmascript/src/string_substring.rs b/crates/oxc_ecmascript/src/string_substring.rs new file mode 100644 index 0000000000000..1518e688ba667 --- /dev/null +++ b/crates/oxc_ecmascript/src/string_substring.rs @@ -0,0 +1,37 @@ +use crate::ToInt32; + +pub trait StringSubstring { + /// `String.prototype.substring ( start , end ] )` + /// + fn substring(&self, start: Option, end: Option) -> String; +} + +impl StringSubstring for &str { + #[expect(clippy::cast_sign_loss)] + fn substring(&self, start: Option, end: Option) -> String { + let start = start.map_or(0, |x| x.to_int_32().max(0) as usize); + let end = end.map_or(usize::MAX, |x| x.to_int_32().max(0) as usize); + let start = start.min(self.len()); + let end = end.min(self.len()); + if start > end { + return String::new(); + } + self.chars().skip(start).take(end - start).collect() + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_prototype_last_index_of() { + use super::StringSubstring; + assert_eq!("foo".substring(Some(1.0), None), "oo"); + assert_eq!("foo".substring(Some(1.0), Some(2.0)), "o"); + assert_eq!("foo".substring(Some(1.0), Some(1.0)), ""); + assert_eq!("foo".substring(Some(1.0), Some(0.0)), ""); + assert_eq!("foo".substring(Some(0.0), Some(0.0)), ""); + assert_eq!("foo".substring(Some(0.0), Some(1.0)), "f"); + assert_eq!("abc".substring(Some(0.0), Some(2.0)), "ab"); + assert_eq!("abcde".substring(Some(0.0), Some(2.0)), "ab"); + } +} 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 00a845a2d8048..94238776cd42e 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 @@ -2,7 +2,7 @@ use cow_utils::CowUtils; use oxc_ast::ast::*; use oxc_ecmascript::{ constant_evaluation::ConstantEvaluation, StringCharAt, StringCharCodeAt, StringIndexOf, - StringLastIndexOf, + StringLastIndexOf, StringSubstring, }; use oxc_traverse::{Traverse, TraverseCtx}; @@ -76,7 +76,12 @@ impl PeepholeReplaceKnownMethods { ctx, ), // TODO: Implement the rest of the string methods - "substring" | "slice" => None, + "substring" | "slice" => Self::try_fold_string_substring_or_slice( + call_expr.span, + call_expr, + string_lit, + ctx, + ), "charAt" => { Self::try_fold_string_char_at(call_expr.span, call_expr, string_lit, ctx) } @@ -131,6 +136,53 @@ impl PeepholeReplaceKnownMethods { ))); } + fn try_fold_string_substring_or_slice<'a>( + span: Span, + call_expr: &CallExpression<'a>, + string_lit: &StringLiteral<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if call_expr.arguments.len() > 2 { + return None; + } + + let start_idx = if let Some(v) = call_expr.arguments.first() { + let val = match v { + Argument::SpreadElement(_) => None, + _ => Ctx(ctx).get_side_free_number_value(v.to_expression()), + }?; + Some(val) + } else { + None + }; + let end_idx = if let Some(v) = call_expr.arguments.get(1) { + let val = match v { + Argument::SpreadElement(_) => None, + _ => Ctx(ctx).get_side_free_number_value(v.to_expression()), + }?; + Some(val) + } else { + None + }; + + #[expect(clippy::cast_precision_loss)] + if start_idx.is_some_and(|start| start > string_lit.value.len() as f64 || start < 0.0) + || end_idx.is_some_and(|end| end > string_lit.value.len() as f64 || end < 0.0) + { + return None; + } + + if let (Some(start), Some(end)) = (start_idx, end_idx) { + if start > end { + return None; + } + }; + + return Some(ctx.ast.expression_from_string_literal( + ctx.ast.string_literal(span, string_lit.value.as_str().substring(start_idx, end_idx)), + )); + } + fn try_fold_string_char_at<'a>( span: Span, call_expr: &CallExpression<'a>, @@ -393,7 +445,6 @@ mod test { } #[test] - #[ignore] fn test_fold_string_substring() { fold("x = 'abcde'.substring(0,2)", "x = 'ab'"); fold("x = 'abcde'.substring(1,2)", "x = 'b'"); @@ -412,7 +463,6 @@ mod test { } #[test] - #[ignore] fn test_fold_string_slice() { fold("x = 'abcde'.slice(0,2)", "x = 'ab'"); fold("x = 'abcde'.slice(1,2)", "x = 'b'");