Skip to content

Commit d51a197

Browse files
authored
prefer-spread: Check String#split('') (#1489)
1 parent 1675118 commit d51a197

File tree

7 files changed

+259
-6
lines changed

7 files changed

+259
-6
lines changed

docs/rules/prefer-spread.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Prefer the spread operator over `Array.from(…)`, `Array#concat(…)` and `Array#slice()`
1+
# Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#slice()` and `String#split('')`
22

33
Enforces the use of [the spread operator (`...`)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) over
44

@@ -18,6 +18,12 @@ Enforces the use of [the spread operator (`...`)](https://developer.mozilla.org/
1818

1919
Variables named `arrayBuffer`, `blob`, `buffer`, `file`, and `this` are ignored.
2020

21+
- `String#split('')`
22+
23+
Split a string into an array of characters.
24+
25+
Note: [The suggestion fix may get different result](https://stackoverflow.com/questions/4547609/how-to-get-character-array-from-a-string/34717402#34717402).
26+
2127
This rule is partly fixable.
2228

2329
## Fail
@@ -34,6 +40,10 @@ const array = array1.concat(array2);
3440
const copy = array.slice();
3541
```
3642

43+
```js
44+
const characters = string.split('');
45+
```
46+
3747
## Pass
3848

3949
```js
@@ -52,6 +62,10 @@ const tail = array.slice(1);
5262
const copy = [...array];
5363
```
5464

65+
```js
66+
const characters = [...string];
67+
```
68+
5569
## With the `unicorn/no-useless-spread` rule
5670

5771
Some cases are fixed using extra spread syntax. Therefore we recommend enabling the [`unicorn/no-useless-spread`](./no-useless-spread.md) rule to fix it.

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ Each rule has emojis denoting:
231231
| [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) | Prefer `Reflect.apply()` over `Function#apply()`. || 🔧 | |
232232
| [prefer-regexp-test](docs/rules/prefer-regexp-test.md) | Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`. || 🔧 | |
233233
| [prefer-set-has](docs/rules/prefer-set-has.md) | Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. || 🔧 | 💡 |
234-
| [prefer-spread](docs/rules/prefer-spread.md) | Prefer the spread operator over `Array.from(…)`, `Array#concat(…)` and `Array#slice()`. || 🔧 | 💡 |
234+
| [prefer-spread](docs/rules/prefer-spread.md) | Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#slice()` and `String#split('')`. || 🔧 | 💡 |
235235
| [prefer-string-replace-all](docs/rules/prefer-string-replace-all.md) | Prefer `String#replaceAll()` over regex searches with the global flag. | | 🔧 | |
236236
| [prefer-string-slice](docs/rules/prefer-string-slice.md) | Prefer `String#slice()` over `String#substr()` and `String#substring()`. || 🔧 | |
237237
| [prefer-string-starts-ends-with](docs/rules/prefer-string-starts-ends-with.md) | Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`. || 🔧 | 💡 |

rules/prefer-spread.js

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,22 @@ const {
1515
const ERROR_ARRAY_FROM = 'array-from';
1616
const ERROR_ARRAY_CONCAT = 'array-concat';
1717
const ERROR_ARRAY_SLICE = 'array-slice';
18+
const ERROR_STRING_SPLIT = 'string-split';
1819
const SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE = 'argument-is-spreadable';
1920
const SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE = 'argument-is-not-spreadable';
2021
const SUGGESTION_CONCAT_TEST_ARGUMENT = 'test-argument';
2122
const SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS = 'spread-all-arguments';
23+
const SUGGESTION_USE_SPREAD = 'use-spread';
2224
const messages = {
2325
[ERROR_ARRAY_FROM]: 'Prefer the spread operator over `Array.from(…)`.',
2426
[ERROR_ARRAY_CONCAT]: 'Prefer the spread operator over `Array#concat(…)`.',
2527
[ERROR_ARRAY_SLICE]: 'Prefer the spread operator over `Array#slice()`.',
28+
[ERROR_STRING_SPLIT]: 'Prefer the spread operator over `String#split(\'\')`.',
2629
[SUGGESTION_CONCAT_ARGUMENT_IS_SPREADABLE]: 'First argument is an `array`.',
2730
[SUGGESTION_CONCAT_ARGUMENT_IS_NOT_SPREADABLE]: 'First argument is not an `array`.',
2831
[SUGGESTION_CONCAT_TEST_ARGUMENT]: 'Test first argument with `Array.isArray(…)`.',
2932
[SUGGESTION_CONCAT_SPREAD_ALL_ARGUMENTS]: 'Spread all unknown arguments`.',
33+
[SUGGESTION_USE_SPREAD]: 'Use `...` operator.',
3034
};
3135

3236
const arrayFromCallSelector = [
@@ -67,6 +71,11 @@ const ignoredSliceCallee = [
6771
'this',
6872
];
6973

74+
const stringSplitCallSelector = methodCallSelector({
75+
method: 'split',
76+
argumentsLength: 1,
77+
});
78+
7079
const isArrayLiteral = node => node.type === 'ArrayExpression';
7180
const isArrayLiteralHasTrailingComma = (node, sourceCode) => {
7281
if (node.elements.length === 0) {
@@ -281,7 +290,7 @@ function fixArrayFrom(node, sourceCode) {
281290
};
282291
}
283292

284-
function fixSlice(node, sourceCode) {
293+
function methodCallToSpread(node, sourceCode) {
285294
return function * (fixer) {
286295
// Fixed code always starts with `[`
287296
if (needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, '[')) {
@@ -291,7 +300,7 @@ function fixSlice(node, sourceCode) {
291300
yield fixer.insertTextBefore(node, '[...');
292301
yield fixer.insertTextAfter(node, ']');
293302

294-
// The array is already accessing `.slice`, there should not any case need add extra `()`
303+
// The array is already accessing `.slice` or `.split`, there should not any case need add extra `()`
295304

296305
yield * removeMethodCall(fixer, node, sourceCode);
297306
};
@@ -418,8 +427,49 @@ const create = context => {
418427
return {
419428
node: node.callee.property,
420429
messageId: ERROR_ARRAY_SLICE,
421-
fix: fixSlice(node, sourceCode),
430+
fix: methodCallToSpread(node, sourceCode),
431+
};
432+
},
433+
[stringSplitCallSelector](node) {
434+
const [separator] = node.arguments;
435+
if (!isLiteralValue(separator, '')) {
436+
return;
437+
}
438+
439+
const string = node.callee.object;
440+
const staticValue = getStaticValue(string, context.getScope());
441+
let hasSameResult = false;
442+
if (staticValue) {
443+
const {value} = staticValue;
444+
445+
if (typeof value !== 'string') {
446+
return;
447+
}
448+
449+
const resultBySplit = value.split('');
450+
const resultBySpread = [...value];
451+
452+
hasSameResult = resultBySplit.length === resultBySpread.length
453+
&& resultBySplit.every((character, index) => character === resultBySpread[index]);
454+
}
455+
456+
const problem = {
457+
node: node.callee.property,
458+
messageId: ERROR_STRING_SPLIT,
422459
};
460+
461+
if (hasSameResult) {
462+
problem.fix = methodCallToSpread(node, sourceCode);
463+
} else {
464+
problem.suggest = [
465+
{
466+
messageId: SUGGESTION_USE_SPREAD,
467+
fix: methodCallToSpread(node, sourceCode),
468+
},
469+
];
470+
}
471+
472+
return problem;
423473
},
424474
};
425475
};
@@ -429,7 +479,7 @@ module.exports = {
429479
meta: {
430480
type: 'suggestion',
431481
docs: {
432-
description: 'Prefer the spread operator over `Array.from(…)`, `Array#concat(…)` and `Array#slice()`.',
482+
description: 'Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#slice()` and `String#split(\'\')`.',
433483
},
434484
fixable: 'code',
435485
messages,

test/prefer-spread.mjs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,46 @@ test.snapshot({
339339
'array.slice(0.00, )',
340340
],
341341
});
342+
343+
// `String#slice('')`
344+
test.snapshot({
345+
valid: [
346+
'new foo.split("")',
347+
'split("")',
348+
'string[split]("")',
349+
'string.split',
350+
'string.split(1)',
351+
'string.split(..."")',
352+
'string.split(...[""])',
353+
'string.split("" + "")',
354+
'string.split(0)',
355+
'string.split(false)',
356+
'string.split(undefined)',
357+
'string.split(0n)',
358+
'string.split(null)',
359+
'string.split(/""/)',
360+
'string.split(``)',
361+
'const EMPTY_STRING = ""; string.split(EMPTY_STRING)',
362+
'string.split("", limit)',
363+
'"".split(string)',
364+
'string.split()',
365+
'string.notSplit("")',
366+
'const notString = 0; notString.split("")',
367+
],
368+
invalid: [
369+
'"string".split("")',
370+
'"string".split(\'\')',
371+
'unknown.split("")',
372+
'const characters = "string".split("")',
373+
'(( (( (( "string" )).split ))( (("")) ) ))',
374+
// Semicolon
375+
outdent`
376+
bar()
377+
foo.split("")
378+
`,
379+
'unknown.split("")',
380+
// Not result the same
381+
'"🦄".split("")',
382+
'const {length} = "🦄".split("")',
383+
],
384+
});

test/run-rules-on-codebase/lint.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ const eslint = new ESLint({
7070
'unicorn/prefer-module': 'off',
7171
},
7272
},
73+
{
74+
files: [
75+
'rules/prefer-spread.js',
76+
],
77+
rules: {
78+
// TODO[xo@>=0.45.0]: Enable this rule when `xo` updated `eslint-plugin-unicorn`
79+
'unicorn/prefer-spread': 'off',
80+
},
81+
},
7382
],
7483
},
7584
});

test/snapshots/prefer-spread.mjs.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2251,3 +2251,140 @@ Generated by [AVA](https://avajs.dev).
22512251
> 1 | array.slice(0.00, )␊
22522252
| ^^^^^ Prefer the spread operator over \`Array#slice()\`.␊
22532253
`
2254+
2255+
## Invalid #1
2256+
1 | "string".split("")
2257+
2258+
> Output
2259+
2260+
`␊
2261+
1 | [..."string"]␊
2262+
`
2263+
2264+
> Error 1/1
2265+
2266+
`␊
2267+
> 1 | "string".split("")␊
2268+
| ^^^^^ Prefer the spread operator over \`String#split('')\`.␊
2269+
`
2270+
2271+
## Invalid #2
2272+
1 | "string".split('')
2273+
2274+
> Output
2275+
2276+
`␊
2277+
1 | [..."string"]␊
2278+
`
2279+
2280+
> Error 1/1
2281+
2282+
`␊
2283+
> 1 | "string".split('')␊
2284+
| ^^^^^ Prefer the spread operator over \`String#split('')\`.␊
2285+
`
2286+
2287+
## Invalid #3
2288+
1 | unknown.split("")
2289+
2290+
> Error 1/1
2291+
2292+
`␊
2293+
> 1 | unknown.split("")␊
2294+
| ^^^^^ Prefer the spread operator over \`String#split('')\`.␊
2295+
2296+
--------------------------------------------------------------------------------␊
2297+
Suggestion 1/1: Use \`...\` operator.␊
2298+
1 | [...unknown]␊
2299+
`
2300+
2301+
## Invalid #4
2302+
1 | const characters = "string".split("")
2303+
2304+
> Output
2305+
2306+
`␊
2307+
1 | const characters = [..."string"]␊
2308+
`
2309+
2310+
> Error 1/1
2311+
2312+
`␊
2313+
> 1 | const characters = "string".split("")␊
2314+
| ^^^^^ Prefer the spread operator over \`String#split('')\`.␊
2315+
`
2316+
2317+
## Invalid #5
2318+
1 | (( (( (( "string" )).split ))( (("")) ) ))
2319+
2320+
> Output
2321+
2322+
`␊
2323+
1 | (( [...(( (( "string" )) ))] ))␊
2324+
`
2325+
2326+
> Error 1/1
2327+
2328+
`␊
2329+
> 1 | (( (( (( "string" )).split ))( (("")) ) ))␊
2330+
| ^^^^^ Prefer the spread operator over \`String#split('')\`.␊
2331+
`
2332+
2333+
## Invalid #6
2334+
1 | bar()
2335+
2 | foo.split("")
2336+
2337+
> Error 1/1
2338+
2339+
`␊
2340+
1 | bar()␊
2341+
> 2 | foo.split("")␊
2342+
| ^^^^^ Prefer the spread operator over \`String#split('')\`.␊
2343+
2344+
--------------------------------------------------------------------------------␊
2345+
Suggestion 1/1: Use \`...\` operator.␊
2346+
1 | bar()␊
2347+
2 | ;[...foo]␊
2348+
`
2349+
2350+
## Invalid #7
2351+
1 | unknown.split("")
2352+
2353+
> Error 1/1
2354+
2355+
`␊
2356+
> 1 | unknown.split("")␊
2357+
| ^^^^^ Prefer the spread operator over \`String#split('')\`.␊
2358+
2359+
--------------------------------------------------------------------------------␊
2360+
Suggestion 1/1: Use \`...\` operator.␊
2361+
1 | [...unknown]␊
2362+
`
2363+
2364+
## Invalid #8
2365+
1 | "🦄".split("")
2366+
2367+
> Error 1/1
2368+
2369+
`␊
2370+
> 1 | "🦄".split("")␊
2371+
| ^^^^^ Prefer the spread operator over \`String#split('')\`.␊
2372+
2373+
--------------------------------------------------------------------------------␊
2374+
Suggestion 1/1: Use \`...\` operator.␊
2375+
1 | [..."🦄"]␊
2376+
`
2377+
2378+
## Invalid #9
2379+
1 | const {length} = "🦄".split("")
2380+
2381+
> Error 1/1
2382+
2383+
`␊
2384+
> 1 | const {length} = "🦄".split("")␊
2385+
| ^^^^^ Prefer the spread operator over \`String#split('')\`.␊
2386+
2387+
--------------------------------------------------------------------------------␊
2388+
Suggestion 1/1: Use \`...\` operator.␊
2389+
1 | const {length} = [..."🦄"]␊
2390+
`
472 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)