Skip to content

Commit 7aecae2

Browse files
committed
fix(unicorn/prefer-string-slice): make substring autofix safe
1 parent f58957c commit 7aecae2

File tree

2 files changed

+42
-21
lines changed

2 files changed

+42
-21
lines changed

crates/oxc_linter/src/rules/unicorn/prefer_string_slice.rs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use oxc_ast::{AstKind, ast::MemberExpression};
1+
use oxc_ast::{
2+
AstKind,
3+
ast::{Argument, Expression, MemberExpression},
4+
};
25
use oxc_diagnostics::OxcDiagnostic;
36
use oxc_macros::declare_oxc_lint;
47
use oxc_span::Span;
@@ -54,11 +57,16 @@ impl Rule for PreferStringSlice {
5457
return;
5558
}
5659
let method_name = v.property.name.as_str();
57-
let has_unsafe_substr_arguments = method_name == "substr"
58-
&& (call_expr.arguments.len() >= 2
59-
|| call_expr.arguments.iter().any(oxc_ast::ast::Argument::is_spread));
60+
let has_spread_arguments = call_expr.arguments.iter().any(Argument::is_spread);
61+
let has_unsafe_arguments = match method_name {
62+
"substr" => has_spread_arguments || call_expr.arguments.len() >= 2,
63+
"substring" => {
64+
has_spread_arguments || !is_safe_substring_arguments(&call_expr.arguments)
65+
}
66+
_ => unreachable!(),
67+
};
6068

61-
if has_unsafe_substr_arguments {
69+
if has_unsafe_arguments {
6270
ctx.diagnostic(prefer_string_slice_diagnostic(v.property.span, method_name));
6371
return;
6472
}
@@ -71,6 +79,35 @@ impl Rule for PreferStringSlice {
7179
}
7280
}
7381

82+
fn is_safe_substring_arguments(arguments: &[Argument<'_>]) -> bool {
83+
match arguments {
84+
[] => true,
85+
[start] => get_non_negative_integer_argument(start).is_some(),
86+
[start, end] => {
87+
let Some(start_value) = get_non_negative_integer_argument(start) else {
88+
return false;
89+
};
90+
let Some(end_value) = get_non_negative_integer_argument(end) else {
91+
return false;
92+
};
93+
start_value <= end_value
94+
}
95+
_ => false,
96+
}
97+
}
98+
99+
fn get_non_negative_integer_argument(argument: &Argument<'_>) -> Option<f64> {
100+
if !argument.is_expression() {
101+
return None;
102+
}
103+
104+
let Expression::NumericLiteral(number) = argument.to_expression().get_inner_expression() else {
105+
return None;
106+
};
107+
108+
if number.value >= 0.0 && number.value.fract() == 0.0 { Some(number.value) } else { None }
109+
}
110+
74111
#[test]
75112
fn test() {
76113
use crate::tester::Tester;
@@ -202,7 +239,6 @@ fn test() {
202239
// (r#""foo".substring(-1, -5)"#, r#""foo".slice(0, 0)"#),
203240
// (r#""foo".substring(-1, 2)"#, r#""foo".slice(0, 2)"#),
204241
// (r#""foo".substring(length)"#, r#""foo".slice(Math.max(0, length))"#),
205-
(r#""foo".substring("fo".length)"#, r#""foo".slice("fo".length)"#), // spellchecker:disable-line
206242
// TODO: Get this passing.
207243
// (r#""foo".substring(0, length)"#, r#""foo".slice(0, Math.max(0, length))"#),
208244
// (r#""foo".substring(length, 0)"#, r#""foo".slice(0, Math.max(0, length))"#),

crates/oxc_linter/src/snapshots/unicorn_prefer_string_slice.snap

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -219,56 +219,48 @@ source: crates/oxc_linter/src/tester.rs
219219
1"foo".substring(2, 1)
220220
· ─────────
221221
╰────
222-
help: Replace `substring` with `slice`.
223222

224223
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
225224
╭─[prefer_string_slice.tsx:1:7]
226225
1"foo".substring(-1, -5)
227226
· ─────────
228227
╰────
229-
help: Replace `substring` with `slice`.
230228

231229
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
232230
╭─[prefer_string_slice.tsx:1:7]
233231
1"foo".substring(-1, 2)
234232
· ─────────
235233
╰────
236-
help: Replace `substring` with `slice`.
237234

238235
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
239236
╭─[prefer_string_slice.tsx:1:7]
240237
1"foo".substring(length)
241238
· ─────────
242239
╰────
243-
help: Replace `substring` with `slice`.
244240

245241
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
246242
╭─[prefer_string_slice.tsx:1:10]
247243
1"foobar".substring("foo".length)
248244
· ─────────
249245
╰────
250-
help: Replace `substring` with `slice`.
251246

252247
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
253248
╭─[prefer_string_slice.tsx:1:7]
254249
1"foo".substring(0, length)
255250
· ─────────
256251
╰────
257-
help: Replace `substring` with `slice`.
258252

259253
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
260254
╭─[prefer_string_slice.tsx:1:7]
261255
1"foo".substring(length, 0)
262256
· ─────────
263257
╰────
264-
help: Replace `substring` with `slice`.
265258

266259
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
267260
╭─[prefer_string_slice.tsx:1:5]
268261
1foo.substring(start)
269262
· ─────────
270263
╰────
271-
help: Replace `substring` with `slice`.
272264

273265
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
274266
╭─[prefer_string_slice.tsx:1:7]
@@ -282,7 +274,6 @@ source: crates/oxc_linter/src/tester.rs
282274
1foo.substring(start, end)
283275
· ─────────
284276
╰────
285-
help: Replace `substring` with `slice`.
286277

287278
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
288279
╭─[prefer_string_slice.tsx:1:7]
@@ -296,7 +287,6 @@ source: crates/oxc_linter/src/tester.rs
296287
1foo.substring(1, 2, 3)
297288
· ─────────
298289
╰────
299-
help: Replace `substring` with `slice`.
300290

301291
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substr()
302292
╭─[prefer_string_slice.tsx:2:40]
@@ -323,7 +313,6 @@ source: crates/oxc_linter/src/tester.rs
323313
· ─────────
324314
3/* 9 */ (( /* 10 */ bar /* 11 */ )) /* 12 */,
325315
╰────
326-
help: Replace `substring` with `slice`.
327316

328317
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substr()
329318
╭─[prefer_string_slice.tsx:1:5]
@@ -360,28 +349,24 @@ source: crates/oxc_linter/src/tester.rs
360349
1foo.substring((10, 1), 0)
361350
· ─────────
362351
╰────
363-
help: Replace `substring` with `slice`.
364352

365353
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
366354
╭─[prefer_string_slice.tsx:1:5]
367355
1foo.substring(0, (10, 1))
368356
· ─────────
369357
╰────
370-
help: Replace `substring` with `slice`.
371358

372359
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
373360
╭─[prefer_string_slice.tsx:1:5]
374361
1foo.substring(0, await 1)
375362
· ─────────
376363
╰────
377-
help: Replace `substring` with `slice`.
378364

379365
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
380366
╭─[prefer_string_slice.tsx:1:5]
381367
1foo.substring((10, bar))
382368
· ─────────
383369
╰────
384-
help: Replace `substring` with `slice`.
385370

386371
eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substr()
387372
╭─[prefer_string_slice.tsx:2:35]

0 commit comments

Comments
 (0)