es-toolkit-eslint-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +44 -0
- package/dist/rules/prefer-clamp.d.ts +3 -0
- package/dist/rules/prefer-clamp.js +25 -0
- package/dist/rules/prefer-compact.d.ts +3 -0
- package/dist/rules/prefer-compact.js +20 -0
- package/dist/rules/prefer-delay.d.ts +3 -0
- package/dist/rules/prefer-delay.js +34 -0
- package/dist/rules/prefer-is-empty.d.ts +3 -0
- package/dist/rules/prefer-is-empty.js +24 -0
- package/dist/rules/prefer-is-equal.d.ts +3 -0
- package/dist/rules/prefer-is-equal.js +22 -0
- package/dist/rules/prefer-last.d.ts +3 -0
- package/dist/rules/prefer-last.js +21 -0
- package/dist/rules/prefer-random-int.d.ts +3 -0
- package/dist/rules/prefer-random-int.js +22 -0
- package/dist/rules/prefer-range.d.ts +3 -0
- package/dist/rules/prefer-range.js +37 -0
- package/dist/rules/prefer-sample.d.ts +3 -0
- package/dist/rules/prefer-sample.js +21 -0
- package/dist/rules/prefer-uniq.d.ts +3 -0
- package/dist/rules/prefer-uniq.js +25 -0
- package/dist/utils/create-rule.d.ts +4 -0
- package/dist/utils/create-rule.js +2 -0
- package/docs/rules/prefer-clamp.md +41 -0
- package/docs/rules/prefer-compact.md +38 -0
- package/docs/rules/prefer-delay.md +42 -0
- package/docs/rules/prefer-is-empty.md +40 -0
- package/docs/rules/prefer-is-equal.md +41 -0
- package/docs/rules/prefer-last.md +41 -0
- package/docs/rules/prefer-random-int.md +43 -0
- package/docs/rules/prefer-range.md +42 -0
- package/docs/rules/prefer-sample.md +39 -0
- package/docs/rules/prefer-uniq.md +41 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aliaksandr Zasim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# es-toolkit-eslint-plugin
|
|
2
|
+
|
|
3
|
+
An ESLint plugin that actively suggests [es-toolkit](https://es-toolkit.dev) utilities in
|
|
4
|
+
place of verbose hand-written JS/TS.
|
|
5
|
+
|
|
6
|
+
Rules are **report-only** (no auto-fix) and lean on
|
|
7
|
+
[esquery](https://eslint.org/docs/latest/extend/selectors) selectors to keep detection
|
|
8
|
+
declarative and minimal.
|
|
9
|
+
|
|
10
|
+
> [!WARNING]
|
|
11
|
+
> **Early stage.** This project is at a very early stage and under active development.
|
|
12
|
+
> False positives are possible.
|
|
13
|
+
> Try it out and [file issues](https://github.com/syxov/es-toolkit-eslint-plugin/issues).
|
|
14
|
+
|
|
15
|
+
> Requires **ESLint 10** (flat config).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pnpm add -D es-toolkit-eslint-plugin
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// eslint.config.js
|
|
27
|
+
import esToolkit from 'es-toolkit-eslint-plugin';
|
|
28
|
+
|
|
29
|
+
export default [
|
|
30
|
+
{
|
|
31
|
+
files: ['**/*.{js,ts}'],
|
|
32
|
+
plugins: { 'es-toolkit': esToolkit },
|
|
33
|
+
rules: {
|
|
34
|
+
'es-toolkit/prefer-clamp': 'error',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or extend the bundled config:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import esToolkit from 'es-toolkit-eslint-plugin';
|
|
44
|
+
|
|
45
|
+
export default [esToolkit.configs.recommended];
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Rules
|
|
49
|
+
|
|
50
|
+
| Rule | Description |
|
|
51
|
+
| ------------------------------------------------------ | -------------------------------------------------------- |
|
|
52
|
+
| [`prefer-clamp`](docs/rules/prefer-clamp.md) | Prefer `clamp` over nested `Math.min`/`Math.max`. |
|
|
53
|
+
| [`prefer-compact`](docs/rules/prefer-compact.md) | Prefer `compact` over `.filter(Boolean)`. |
|
|
54
|
+
| [`prefer-delay`](docs/rules/prefer-delay.md) | Prefer `delay` over `new Promise` + `setTimeout`. |
|
|
55
|
+
| [`prefer-is-empty`](docs/rules/prefer-is-empty.md) | Prefer `isEmpty` over `Object.keys(obj).length === 0`. |
|
|
56
|
+
| [`prefer-is-equal`](docs/rules/prefer-is-equal.md) | Prefer `isEqual` over comparing `JSON.stringify` output. |
|
|
57
|
+
| [`prefer-last`](docs/rules/prefer-last.md) | Prefer `last` over `arr[arr.length - 1]`. |
|
|
58
|
+
| [`prefer-random-int`](docs/rules/prefer-random-int.md) | Prefer `randomInt` over `Math.floor(Math.random() * …)`. |
|
|
59
|
+
| [`prefer-range`](docs/rules/prefer-range.md) | Prefer `range` over manual index-array construction. |
|
|
60
|
+
| [`prefer-sample`](docs/rules/prefer-sample.md) | Prefer `sample` over random-index array access. |
|
|
61
|
+
| [`prefer-uniq`](docs/rules/prefer-uniq.md) | Prefer `uniq` over `new Set` round-trips. |
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
pnpm install
|
|
67
|
+
pnpm test # vitest + RuleTester
|
|
68
|
+
pnpm build # tsc -> dist/
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
declare const plugin: {
|
|
2
|
+
meta: {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
};
|
|
6
|
+
rules: {
|
|
7
|
+
'prefer-clamp': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferClamp", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
'prefer-compact': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferCompact", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
13
|
+
'prefer-delay': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferDelay", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
14
|
+
name: string;
|
|
15
|
+
};
|
|
16
|
+
'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferIsEmpty", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
'prefer-is-equal': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferIsEqual", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
'prefer-last': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferLast", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
23
|
+
name: string;
|
|
24
|
+
};
|
|
25
|
+
'prefer-random-int': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferRandomInt", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
'prefer-range': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferRange", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
29
|
+
name: string;
|
|
30
|
+
};
|
|
31
|
+
'prefer-sample': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferSample", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
32
|
+
name: string;
|
|
33
|
+
};
|
|
34
|
+
'prefer-uniq': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferUniq", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
35
|
+
name: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
configs: {};
|
|
39
|
+
};
|
|
40
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { preferClamp } from './rules/prefer-clamp.js';
|
|
2
|
+
import { preferCompact } from './rules/prefer-compact.js';
|
|
3
|
+
import { preferDelay } from './rules/prefer-delay.js';
|
|
4
|
+
import { preferIsEmpty } from './rules/prefer-is-empty.js';
|
|
5
|
+
import { preferIsEqual } from './rules/prefer-is-equal.js';
|
|
6
|
+
import { preferLast } from './rules/prefer-last.js';
|
|
7
|
+
import { preferRandomInt } from './rules/prefer-random-int.js';
|
|
8
|
+
import { preferRange } from './rules/prefer-range.js';
|
|
9
|
+
import { preferSample } from './rules/prefer-sample.js';
|
|
10
|
+
import { preferUniq } from './rules/prefer-uniq.js';
|
|
11
|
+
const plugin = {
|
|
12
|
+
meta: { name: 'es-toolkit-eslint-plugin', version: '1.0.0' },
|
|
13
|
+
rules: {
|
|
14
|
+
'prefer-clamp': preferClamp,
|
|
15
|
+
'prefer-compact': preferCompact,
|
|
16
|
+
'prefer-delay': preferDelay,
|
|
17
|
+
'prefer-is-empty': preferIsEmpty,
|
|
18
|
+
'prefer-is-equal': preferIsEqual,
|
|
19
|
+
'prefer-last': preferLast,
|
|
20
|
+
'prefer-random-int': preferRandomInt,
|
|
21
|
+
'prefer-range': preferRange,
|
|
22
|
+
'prefer-sample': preferSample,
|
|
23
|
+
'prefer-uniq': preferUniq,
|
|
24
|
+
},
|
|
25
|
+
configs: {},
|
|
26
|
+
};
|
|
27
|
+
plugin.configs = {
|
|
28
|
+
recommended: {
|
|
29
|
+
plugins: { 'es-toolkit': plugin },
|
|
30
|
+
rules: {
|
|
31
|
+
'es-toolkit/prefer-clamp': 'error',
|
|
32
|
+
'es-toolkit/prefer-compact': 'error',
|
|
33
|
+
'es-toolkit/prefer-delay': 'error',
|
|
34
|
+
'es-toolkit/prefer-is-empty': 'error',
|
|
35
|
+
'es-toolkit/prefer-is-equal': 'error',
|
|
36
|
+
'es-toolkit/prefer-last': 'error',
|
|
37
|
+
'es-toolkit/prefer-random-int': 'error',
|
|
38
|
+
'es-toolkit/prefer-range': 'error',
|
|
39
|
+
'es-toolkit/prefer-sample': 'error',
|
|
40
|
+
'es-toolkit/prefer-uniq': 'error',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
export default plugin;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
const mathCall = (method) => `CallExpression[callee.object.name="Math"][callee.property.name="${method}"][arguments.length=2]`;
|
|
3
|
+
// A clamp is an outer Math call wrapping the opposite Math call as a direct argument:
|
|
4
|
+
// Math.min(Math.max(value, lower), upper) or Math.max(Math.min(value, upper), lower).
|
|
5
|
+
const clampPattern = (outer, inner) => `${mathCall(outer)}:has(> ${mathCall(inner)})`;
|
|
6
|
+
export const preferClamp = createRule({
|
|
7
|
+
name: 'prefer-clamp',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Prefer `clamp` from es-toolkit over nested `Math.min`/`Math.max`.',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: {
|
|
15
|
+
preferClamp: 'Prefer `clamp` from es-toolkit instead of nesting `Math.min`/`Math.max`.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const report = (node) => context.report({ node, messageId: 'preferClamp' });
|
|
20
|
+
return {
|
|
21
|
+
[clampPattern('min', 'max')]: report,
|
|
22
|
+
[clampPattern('max', 'min')]: report,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
const filterBoolean = 'CallExpression[callee.property.name="filter"][arguments.length=1][arguments.0.name="Boolean"]';
|
|
3
|
+
export const preferCompact = createRule({
|
|
4
|
+
name: 'prefer-compact',
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'suggestion',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Prefer `compact` from es-toolkit over `.filter(Boolean)`.',
|
|
9
|
+
},
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
preferCompact: 'Prefer `compact` from es-toolkit instead of `.filter(Boolean)`.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
[filterBoolean]: (node) => context.report({ node, messageId: 'preferCompact' }),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
// `new Promise(resolve => setTimeout(resolve, ms))` — a single-param executor whose
|
|
3
|
+
// expression body is a two-argument `setTimeout` whose first argument is an identifier.
|
|
4
|
+
const promisifiedTimeout = 'NewExpression[callee.name="Promise"][arguments.length=1] > ArrowFunctionExpression[params.length=1][params.0.type="Identifier"][body.callee.name="setTimeout"][body.arguments.length=2][body.arguments.0.type="Identifier"]';
|
|
5
|
+
export const preferDelay = createRule({
|
|
6
|
+
name: 'prefer-delay',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Prefer `delay` from es-toolkit over `new Promise` + `setTimeout`.',
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
preferDelay: 'Prefer `delay` from es-toolkit instead of wrapping `setTimeout` in a `new Promise`.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
[promisifiedTimeout](node) {
|
|
20
|
+
const [param] = node.params;
|
|
21
|
+
const { body } = node;
|
|
22
|
+
// Confirm the timeout callback IS the promise's own `resolve` (a pure delay).
|
|
23
|
+
if (body.type !== 'CallExpression')
|
|
24
|
+
return;
|
|
25
|
+
const [callback] = body.arguments;
|
|
26
|
+
if (param.type === 'Identifier'
|
|
27
|
+
&& callback.type === 'Identifier'
|
|
28
|
+
&& param.name === callback.name) {
|
|
29
|
+
context.report({ node: node.parent, messageId: 'preferDelay' });
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
const objectKeysLength = (side) => `[${side}.property.name="length"][${side}.object.callee.object.name="Object"][${side}.object.callee.property.name="keys"]`;
|
|
3
|
+
// `Object.keys(obj).length === 0`, in either operand order.
|
|
4
|
+
const keysLengthZero = (zero, keys) => `BinaryExpression[operator=/^===?$/][${zero}.value=0]${objectKeysLength(keys)}`;
|
|
5
|
+
export const preferIsEmpty = createRule({
|
|
6
|
+
name: 'prefer-is-empty',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Prefer `isEmpty` from es-toolkit over `Object.keys(obj).length === 0`.',
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
preferIsEmpty: 'Prefer `isEmpty` from es-toolkit instead of `Object.keys(obj).length === 0`.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const report = (node) => context.report({ node, messageId: 'preferIsEmpty' });
|
|
19
|
+
return {
|
|
20
|
+
[keysLengthZero('right', 'left')]: report,
|
|
21
|
+
[keysLengthZero('left', 'right')]: report,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
const stringify = (side) => `[${side}.callee.object.name="JSON"][${side}.callee.property.name="stringify"]`;
|
|
3
|
+
// `JSON.stringify(a) === JSON.stringify(b)` (or `!==`) is a fragile deep-equality check.
|
|
4
|
+
const stringifyComparison = `BinaryExpression[operator=/^[!=]==?$/]${stringify('left')}${stringify('right')}`;
|
|
5
|
+
export const preferIsEqual = createRule({
|
|
6
|
+
name: 'prefer-is-equal',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Prefer `isEqual` from es-toolkit over comparing `JSON.stringify` results.',
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
preferIsEqual: 'Prefer `isEqual` from es-toolkit instead of comparing `JSON.stringify` output.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
[stringifyComparison]: (node) => context.report({ node, messageId: 'preferIsEqual' }),
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
// `arr[arr.length - 1]`: a computed access whose index is `<something>.length - 1`.
|
|
3
|
+
const lastIndex = 'MemberExpression[computed=true][property.operator="-"][property.right.value=1][property.left.property.name="length"]';
|
|
4
|
+
export const preferLast = createRule({
|
|
5
|
+
name: 'prefer-last',
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Prefer `last` from es-toolkit over `arr[arr.length - 1]`.',
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
preferLast: 'Prefer `last` from es-toolkit instead of indexing with `arr.length - 1`.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
[lastIndex]: (node) => context.report({ node, messageId: 'preferLast' }),
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
const random = 'CallExpression[callee.object.name="Math"][callee.property.name="random"]';
|
|
3
|
+
// `Math.floor(Math.random() * X)`. A `.length` multiplier is left to `prefer-sample`.
|
|
4
|
+
const floorRandom = `CallExpression[callee.object.name="Math"][callee.property.name="floor"][arguments.length=1]:has(> BinaryExpression[operator="*"]:has(> ${random}):not(:has(> MemberExpression[property.name="length"])))`;
|
|
5
|
+
export const preferRandomInt = createRule({
|
|
6
|
+
name: 'prefer-random-int',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Prefer `randomInt` from es-toolkit over `Math.floor(Math.random() * …)`.',
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
preferRandomInt: 'Prefer `randomInt` from es-toolkit instead of `Math.floor(Math.random() * …)`.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
[floorRandom]: (node) => context.report({ node, messageId: 'preferRandomInt' }),
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
// `[...Array(n).keys()]` — a spread of the index iterator of a freshly sized array.
|
|
3
|
+
const spreadArrayKeys = 'ArrayExpression[elements.length=1][elements.0.type="SpreadElement"][elements.0.argument.type="CallExpression"][elements.0.argument.callee.property.name="keys"][elements.0.argument.callee.object.callee.name="Array"]';
|
|
4
|
+
// `Array.from({ length: n }, (_, i) => i)` — the mapper returning its index is verified below.
|
|
5
|
+
const arrayFromLength = 'CallExpression[callee.object.name="Array"][callee.property.name="from"][arguments.length=2][arguments.0.type="ObjectExpression"][arguments.0.properties.0.key.name="length"][arguments.1.params.length=2][arguments.1.body.type="Identifier"]';
|
|
6
|
+
export const preferRange = createRule({
|
|
7
|
+
name: 'prefer-range',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Prefer `range` from es-toolkit over manual index-array construction.',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: {
|
|
15
|
+
preferRange: 'Prefer `range` from es-toolkit instead of building an index array by hand.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const report = (node) => context.report({ node, messageId: 'preferRange' });
|
|
20
|
+
return {
|
|
21
|
+
[spreadArrayKeys]: report,
|
|
22
|
+
[arrayFromLength](node) {
|
|
23
|
+
const mapper = node.arguments[1];
|
|
24
|
+
if (mapper.type !== 'ArrowFunctionExpression'
|
|
25
|
+
&& mapper.type !== 'FunctionExpression')
|
|
26
|
+
return;
|
|
27
|
+
const index = mapper.params[1];
|
|
28
|
+
// Only a mapper that returns its own index parameter yields `range(n)`.
|
|
29
|
+
if (index?.type === 'Identifier'
|
|
30
|
+
&& mapper.body.type === 'Identifier'
|
|
31
|
+
&& mapper.body.name === index.name) {
|
|
32
|
+
report(node);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
// `arr[Math.floor(Math.random() * arr.length)]` — a random index into an array's length.
|
|
3
|
+
const randomIndex = 'MemberExpression[computed=true][property.callee.object.name="Math"][property.callee.property.name="floor"]:has(CallExpression[callee.object.name="Math"][callee.property.name="random"]):has(MemberExpression[property.name="length"])';
|
|
4
|
+
export const preferSample = createRule({
|
|
5
|
+
name: 'prefer-sample',
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Prefer `sample` from es-toolkit over random-index array access.',
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
preferSample: 'Prefer `sample` from es-toolkit instead of indexing with a random index.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
[randomIndex]: (node) => context.report({ node, messageId: 'preferSample' }),
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRule } from '../utils/create-rule.js';
|
|
2
|
+
const set = 'NewExpression[callee.name="Set"][arguments.length=1]';
|
|
3
|
+
// Both `[...new Set(arr)]` and `Array.from(new Set(arr))` deduplicate an array.
|
|
4
|
+
const spreadSet = 'ArrayExpression[elements.length=1][elements.0.type="SpreadElement"][elements.0.argument.type="NewExpression"][elements.0.argument.callee.name="Set"][elements.0.argument.arguments.length=1]';
|
|
5
|
+
const arrayFromSet = `CallExpression[callee.object.name="Array"][callee.property.name="from"][arguments.length=1]:has(> ${set})`;
|
|
6
|
+
export const preferUniq = createRule({
|
|
7
|
+
name: 'prefer-uniq',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Prefer `uniq` from es-toolkit over `new Set` round-trips.',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: {
|
|
15
|
+
preferUniq: 'Prefer `uniq` from es-toolkit instead of round-tripping through `new Set`.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const report = (node) => context.report({ node, messageId: 'preferUniq' });
|
|
20
|
+
return {
|
|
21
|
+
[spreadSet]: report,
|
|
22
|
+
[arrayFromSet]: report,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const createRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# prefer-clamp
|
|
2
|
+
|
|
3
|
+
Prefer [`clamp`](https://es-toolkit.dev/reference/math/clamp.html) from es-toolkit over
|
|
4
|
+
nested `Math.min`/`Math.max`.
|
|
5
|
+
|
|
6
|
+
`clamp(value, minimum, maximum)` is exactly `Math.min(Math.max(value, minimum), maximum)`,
|
|
7
|
+
so the nested form is more verbose and harder to read than the named utility.
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
This rule reports — it does **not** auto-fix — any two-bound clamp idiom where a `Math.min`
|
|
12
|
+
call wraps a `Math.max` call (or vice versa) as a direct argument.
|
|
13
|
+
|
|
14
|
+
### ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
Math.min(Math.max(value, lower), upper);
|
|
18
|
+
Math.max(Math.min(value, upper), lower);
|
|
19
|
+
Math.min(upper, Math.max(value, lower)); // argument order does not matter
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### ✅ Correct
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { clamp } from 'es-toolkit';
|
|
26
|
+
|
|
27
|
+
clamp(value, lower, upper);
|
|
28
|
+
|
|
29
|
+
// Not a clamp — left untouched:
|
|
30
|
+
Math.min(a, b);
|
|
31
|
+
Math.min(Math.min(a, b), c);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Limitations
|
|
35
|
+
|
|
36
|
+
- **Syntactic match.** `Math` is matched by name, so a shadowed local variable named
|
|
37
|
+
`Math` could produce a false positive. This is rare in practice.
|
|
38
|
+
- **Two-bound form only.** The single-bound `Math.min(a, b)` / `Math.max(a, b)` form is
|
|
39
|
+
intentionally not reported — it is too common and ambiguous to flag reliably.
|
|
40
|
+
- **No auto-fix by design.** The fix requires adding an `import { clamp } from 'es-toolkit'`
|
|
41
|
+
and choosing argument order, so the rule only reports.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# prefer-compact
|
|
2
|
+
|
|
3
|
+
Prefer [`compact`](https://es-toolkit.dev/reference/array/compact.html) from es-toolkit
|
|
4
|
+
over `.filter(Boolean)`.
|
|
5
|
+
|
|
6
|
+
`compact(arr)` removes all falsy values (`false`, `null`, `0`, `''`, `undefined`, `NaN`) —
|
|
7
|
+
exactly what `arr.filter(Boolean)` does, but named and (in TypeScript) better typed.
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
This rule reports — it does **not** auto-fix — a single-argument `.filter(Boolean)` call.
|
|
12
|
+
|
|
13
|
+
### ❌ Incorrect
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
arr.filter(Boolean);
|
|
17
|
+
items.filter(Boolean).map(x => x.id);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### ✅ Correct
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { compact } from 'es-toolkit';
|
|
24
|
+
|
|
25
|
+
compact(arr);
|
|
26
|
+
|
|
27
|
+
// Not a falsy filter — left untouched:
|
|
28
|
+
arr.filter(x => x);
|
|
29
|
+
arr.map(Boolean);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Limitations
|
|
33
|
+
|
|
34
|
+
- **Syntactic match.** A shadowed local `Boolean` could produce a false positive. This is
|
|
35
|
+
rare in practice.
|
|
36
|
+
- **TypeScript bonus.** Beyond readability, `compact` narrows nullable element types, while
|
|
37
|
+
`filter(Boolean)` often leaves them un-narrowed.
|
|
38
|
+
- **No auto-fix by design.** The fix requires adding an `import { compact } from 'es-toolkit'`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# prefer-delay
|
|
2
|
+
|
|
3
|
+
Prefer [`delay`](https://es-toolkit.dev/reference/promise/delay.html) from es-toolkit over
|
|
4
|
+
wrapping `setTimeout` in a `new Promise`.
|
|
5
|
+
|
|
6
|
+
`delay(ms)` returns a promise that resolves after `ms` milliseconds — the same thing as the
|
|
7
|
+
hand-rolled `new Promise((resolve) => setTimeout(resolve, ms))` sleep idiom.
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
This rule reports — it does **not** auto-fix — a `new Promise` whose single-parameter
|
|
12
|
+
executor immediately calls `setTimeout` with that same parameter as the callback.
|
|
13
|
+
|
|
14
|
+
### ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
new Promise(resolve => setTimeout(resolve, 1000));
|
|
18
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### ✅ Correct
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { delay } from 'es-toolkit';
|
|
25
|
+
|
|
26
|
+
await delay(1000);
|
|
27
|
+
|
|
28
|
+
// Not a pure delay — left untouched:
|
|
29
|
+
new Promise(resolve => setTimeout(doWork, 1000)); // runs work, not just a delay
|
|
30
|
+
new Promise(resolve => {
|
|
31
|
+
setTimeout(resolve, ms); // block-bodied executor, see Limitations
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Limitations
|
|
36
|
+
|
|
37
|
+
- **Expression-body executor only.** The block-bodied form
|
|
38
|
+
`new Promise((resolve) => { setTimeout(resolve, ms); })` and wrapped forms like
|
|
39
|
+
`setTimeout(() => resolve(), ms)` are not reported, to keep the rule precise.
|
|
40
|
+
- **Identity-checked.** The rule reports only when `setTimeout`'s callback is the promise's
|
|
41
|
+
own `resolve` parameter, so `setTimeout(doWork, ms)` inside a `new Promise` is left alone.
|
|
42
|
+
- **No auto-fix by design.** The fix requires adding an `import { delay } from 'es-toolkit'`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# prefer-is-empty
|
|
2
|
+
|
|
3
|
+
Prefer [`isEmpty`](https://es-toolkit.dev/reference/predicate/isEmpty.html) from es-toolkit
|
|
4
|
+
over `Object.keys(obj).length === 0`.
|
|
5
|
+
|
|
6
|
+
`isEmpty(obj)` answers "does this object have no own enumerable keys?" directly, without
|
|
7
|
+
allocating an intermediate keys array.
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
This rule reports — it does **not** auto-fix — an equality comparison of
|
|
12
|
+
`Object.keys(...).length` against `0`, in either operand order.
|
|
13
|
+
|
|
14
|
+
### ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
Object.keys(obj).length === 0;
|
|
18
|
+
0 === Object.keys(obj).length;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### ✅ Correct
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { isEmpty } from 'es-toolkit';
|
|
25
|
+
|
|
26
|
+
isEmpty(obj);
|
|
27
|
+
|
|
28
|
+
// Not in scope — left untouched:
|
|
29
|
+
Object.keys(obj).length === 1;
|
|
30
|
+
Object.values(obj).length === 0;
|
|
31
|
+
arr.length === 0;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Limitations
|
|
35
|
+
|
|
36
|
+
- **`Object.keys` only.** `Object.values(...)`/`Object.entries(...)` length checks and bare
|
|
37
|
+
`arr.length === 0` / `str.length === 0` are intentionally **not** flagged — they are too
|
|
38
|
+
common and clear to rewrite reliably.
|
|
39
|
+
- **Syntactic match.** A shadowed local `Object` could produce a false positive.
|
|
40
|
+
- **No auto-fix by design.** The fix requires adding an `import { isEmpty } from 'es-toolkit'`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# prefer-is-equal
|
|
2
|
+
|
|
3
|
+
Prefer [`isEqual`](https://es-toolkit.dev/reference/predicate/isEqual.html) from es-toolkit
|
|
4
|
+
over comparing `JSON.stringify` output.
|
|
5
|
+
|
|
6
|
+
Comparing `JSON.stringify(a) === JSON.stringify(b)` is a common but fragile deep-equality
|
|
7
|
+
check: it depends on key order, silently drops `undefined`/functions, and throws on circular
|
|
8
|
+
references. `isEqual(a, b)` performs a real structural comparison.
|
|
9
|
+
|
|
10
|
+
## Rule details
|
|
11
|
+
|
|
12
|
+
This rule reports — it does **not** auto-fix — an equality/inequality comparison whose both
|
|
13
|
+
operands are `JSON.stringify(...)` calls.
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
JSON.stringify(a) === JSON.stringify(b);
|
|
19
|
+
JSON.stringify(x) !== JSON.stringify(y);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### ✅ Correct
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { isEqual } from 'es-toolkit';
|
|
26
|
+
|
|
27
|
+
isEqual(a, b);
|
|
28
|
+
|
|
29
|
+
// Not a stringify-based comparison — left untouched:
|
|
30
|
+
JSON.stringify(a) === b;
|
|
31
|
+
JSON.parse(a) === JSON.parse(b);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Limitations
|
|
35
|
+
|
|
36
|
+
- **Both operands must be `JSON.stringify`.** Comparing one stringified value against
|
|
37
|
+
something else is not reported.
|
|
38
|
+
- **Key-order caveat is the point.** `JSON.stringify` equality is order-sensitive and
|
|
39
|
+
lossy — that fragility is exactly why this rule flags it in favor of `isEqual`.
|
|
40
|
+
- **No auto-fix by design.** The fix requires adding an `import { isEqual } from 'es-toolkit'`
|
|
41
|
+
(and inverting with `!` for the `!==` case).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# prefer-last
|
|
2
|
+
|
|
3
|
+
Prefer [`last`](https://es-toolkit.dev/reference/array/last.html) from es-toolkit over
|
|
4
|
+
`arr[arr.length - 1]`.
|
|
5
|
+
|
|
6
|
+
`last(arr)` reads as "the last element"; `arr[arr.length - 1]` repeats the array and forces
|
|
7
|
+
the reader to compute the index.
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
This rule reports — it does **not** auto-fix — a computed member access whose index is
|
|
12
|
+
`<something>.length - 1`.
|
|
13
|
+
|
|
14
|
+
### ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
arr[arr.length - 1];
|
|
18
|
+
items[items.length - 1];
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### ✅ Correct
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { last } from 'es-toolkit';
|
|
25
|
+
|
|
26
|
+
last(arr);
|
|
27
|
+
|
|
28
|
+
// Not a last-element access — left untouched:
|
|
29
|
+
arr[arr.length - 2];
|
|
30
|
+
arr[i - 1];
|
|
31
|
+
arr.at(-1); // the native equivalent, also fine
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Limitations
|
|
35
|
+
|
|
36
|
+
- **Object identity is not checked.** The selector matches the shape `a[b.length - 1]`, so
|
|
37
|
+
the rare case where the indexed array and the `.length` array differ would also be
|
|
38
|
+
reported.
|
|
39
|
+
- **Native alternative.** `arr.at(-1)` is a concise built-in; this rule suggests es-toolkit's
|
|
40
|
+
`last` to keep call sites consistent with the rest of the toolkit.
|
|
41
|
+
- **No auto-fix by design.** The fix requires adding an `import { last } from 'es-toolkit'`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# prefer-random-int
|
|
2
|
+
|
|
3
|
+
Prefer [`randomInt`](https://es-toolkit.dev/reference/math/randomInt.html) from es-toolkit
|
|
4
|
+
over `Math.floor(Math.random() * …)`.
|
|
5
|
+
|
|
6
|
+
The `Math.floor(Math.random() * n)` family is the classic hand-rolled random-integer
|
|
7
|
+
formula; `randomInt` names the intent and removes the off-by-one foot-guns.
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
This rule reports — it does **not** auto-fix — `Math.floor(...)` applied to a multiplication
|
|
12
|
+
that contains `Math.random()`. A `.length` multiplier is left to
|
|
13
|
+
[`prefer-sample`](./prefer-sample.md) instead.
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
Math.floor(Math.random() * 6);
|
|
19
|
+
Math.floor(Math.random() * (max - min + 1)) + min;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### ✅ Correct
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { randomInt } from 'es-toolkit';
|
|
26
|
+
|
|
27
|
+
randomInt(0, 6);
|
|
28
|
+
|
|
29
|
+
// Not this rule's concern — left untouched:
|
|
30
|
+
Math.random(); // a float in [0, 1)
|
|
31
|
+
Math.round(Math.random() * n); // rounds, not floors
|
|
32
|
+
arr[Math.floor(Math.random() * arr.length)]; // see prefer-sample
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Limitations
|
|
36
|
+
|
|
37
|
+
- **Bounds differ.** The inclusive idiom `Math.floor(Math.random() * (max - min + 1)) + min`
|
|
38
|
+
yields `[min, max]`, whereas `randomInt(min, max)` yields `[min, max)`. Adjust the upper
|
|
39
|
+
bound when rewriting.
|
|
40
|
+
- **`floor` only.** The `Math.round(...)` variant is not reported (it has a non-uniform
|
|
41
|
+
distribution and is a different intent).
|
|
42
|
+
- **Syntactic match.** A shadowed local `Math` could produce a false positive.
|
|
43
|
+
- **No auto-fix by design.** The fix requires adding an `import { randomInt } from 'es-toolkit'`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# prefer-range
|
|
2
|
+
|
|
3
|
+
Prefer [`range`](https://es-toolkit.dev/reference/math/range.html) from es-toolkit over
|
|
4
|
+
hand-built index arrays.
|
|
5
|
+
|
|
6
|
+
`range(n)` produces `[0, 1, …, n - 1]`. The manual equivalents — `[...Array(n).keys()]` and
|
|
7
|
+
`Array.from({ length: n }, (_, i) => i)` — are noisier ways to say the same thing.
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
This rule reports — it does **not** auto-fix — two index-array idioms:
|
|
12
|
+
|
|
13
|
+
- `[...Array(n).keys()]`
|
|
14
|
+
- `Array.from({ length: n }, (_, i) => i)` (a mapper that returns its own index)
|
|
15
|
+
|
|
16
|
+
### ❌ Incorrect
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
[...Array(n).keys()];
|
|
20
|
+
Array.from({ length: n }, (_, i) => i);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### ✅ Correct
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { range } from 'es-toolkit';
|
|
27
|
+
|
|
28
|
+
range(n);
|
|
29
|
+
|
|
30
|
+
// Not a plain index range — left untouched:
|
|
31
|
+
Array.from({ length: n }); // array of holes, not 0…n-1
|
|
32
|
+
Array.from({ length: n }, (_, i) => i * 2); // transformed → range(n).map(...)
|
|
33
|
+
[...arr.keys()]; // keys of an existing array
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Limitations
|
|
37
|
+
|
|
38
|
+
- **Identity-mapper only.** Only the mapper that returns its index parameter (→ `range(n)`)
|
|
39
|
+
is reported; transformed mappers and `range`'s `start`/`step` variants are not.
|
|
40
|
+
- **Syntactic match.** A shadowed local `Array` could produce a false positive. This is
|
|
41
|
+
rare in practice.
|
|
42
|
+
- **No auto-fix by design.** The fix requires adding an `import { range } from 'es-toolkit'`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# prefer-sample
|
|
2
|
+
|
|
3
|
+
Prefer [`sample`](https://es-toolkit.dev/reference/array/sample.html) from es-toolkit over
|
|
4
|
+
random-index array access.
|
|
5
|
+
|
|
6
|
+
`sample(arr)` returns a random element; `arr[Math.floor(Math.random() * arr.length)]` spells
|
|
7
|
+
out the same idea by hand and repeats the array.
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
This rule reports — it does **not** auto-fix — a computed access whose index is
|
|
12
|
+
`Math.floor(Math.random() * …length)`.
|
|
13
|
+
|
|
14
|
+
### ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
arr[Math.floor(Math.random() * arr.length)];
|
|
18
|
+
items[Math.floor(Math.random() * items.length)];
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### ✅ Correct
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { sample } from 'es-toolkit';
|
|
25
|
+
|
|
26
|
+
sample(arr);
|
|
27
|
+
|
|
28
|
+
// Not a random element pick — left untouched:
|
|
29
|
+
arr[Math.floor(Math.random() * 3)]; // fixed range, see prefer-random-int
|
|
30
|
+
arr[i];
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Limitations
|
|
34
|
+
|
|
35
|
+
- **Object identity is not checked.** The selector matches `a[Math.floor(Math.random() *
|
|
36
|
+
b.length)]`, so the rare case where the indexed array and the `.length` array differ would
|
|
37
|
+
also be reported.
|
|
38
|
+
- **Syntactic match.** A shadowed local `Math` could produce a false positive.
|
|
39
|
+
- **No auto-fix by design.** The fix requires adding an `import { sample } from 'es-toolkit'`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# prefer-uniq
|
|
2
|
+
|
|
3
|
+
Prefer [`uniq`](https://es-toolkit.dev/reference/array/uniq.html) from es-toolkit over
|
|
4
|
+
round-tripping an array through `new Set`.
|
|
5
|
+
|
|
6
|
+
`[...new Set(arr)]` and `Array.from(new Set(arr))` exist only to deduplicate; `uniq(arr)`
|
|
7
|
+
states that intent directly.
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
This rule reports — it does **not** auto-fix — an array literal spreading a single
|
|
12
|
+
`new Set(x)`, or `Array.from(new Set(x))` with no mapper.
|
|
13
|
+
|
|
14
|
+
### ❌ Incorrect
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
[...new Set(arr)];
|
|
18
|
+
Array.from(new Set(arr));
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### ✅ Correct
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { uniq } from 'es-toolkit';
|
|
25
|
+
|
|
26
|
+
uniq(arr);
|
|
27
|
+
|
|
28
|
+
// Not a dedupe round-trip — left untouched:
|
|
29
|
+
new Set(arr);
|
|
30
|
+
[...arr];
|
|
31
|
+
Array.from(new Set(arr), x => x * 2); // a map, not plain uniq
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Limitations
|
|
35
|
+
|
|
36
|
+
- **Syntactic match.** `Set`/`Array` are matched by name, so shadowed locals could produce
|
|
37
|
+
a false positive. This is rare in practice.
|
|
38
|
+
- **Single-argument `Set` only.** `[...new Set()]` (empty) is not reported.
|
|
39
|
+
- **`Array.from(set, mapFn)`** is intentionally skipped — the second argument transforms
|
|
40
|
+
the result, so it is not a plain `uniq`.
|
|
41
|
+
- **No auto-fix by design.** The fix requires adding an `import { uniq } from 'es-toolkit'`.
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "es-toolkit-eslint-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint plugin that suggests es-toolkit utilities in place of verbose JS/TS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"docs",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"eslint",
|
|
21
|
+
"eslintplugin",
|
|
22
|
+
"eslint-plugin",
|
|
23
|
+
"es-toolkit"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Aliaksandr Zasim",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/syxov/es-toolkit-eslint-plugin.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/syxov/es-toolkit-eslint-plugin/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/syxov/es-toolkit-eslint-plugin#readme",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@typescript-eslint/utils": "^8.61.1"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"eslint": ">=10"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
44
|
+
"@typescript-eslint/rule-tester": "^8.0.0",
|
|
45
|
+
"eslint": "^10.0.0",
|
|
46
|
+
"prettier": "^3.8.4",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"vitest": "^3.0.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"format": "prettier --write . --experimental-cli --log-level=error",
|
|
55
|
+
"release": "pnpm publish --access public"
|
|
56
|
+
}
|
|
57
|
+
}
|