mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(minifier): implement folding indexOf and lastIndexOf string fns (#6435)
This commit is contained in:
parent
5ee1ef3d92
commit
a4f57a42e8
2 changed files with 282 additions and 20 deletions
|
|
@ -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,
|
||||
};
|
||||
#[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,
|
||||
};
|
||||
|
||||
if let Some(transformed_value) = transformed_value {
|
||||
self.changed = true;
|
||||
*node = ctx.ast.expression_from_string_literal(transformed_value);
|
||||
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<Expression<'a>> {
|
||||
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: <https://github.com/google/closure-compiler/blob/master/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java>
|
||||
|
|
@ -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');");
|
||||
|
|
|
|||
|
|
@ -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<f64>,
|
||||
) -> 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::<String>().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<f64>,
|
||||
) -> 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::<String>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue