es-toolkit-eslint-plugin 0.1.0 → 0.1.2

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.
@@ -1,20 +1,46 @@
1
1
  import { createRule } from '../utils/create-rule.js';
2
2
  const filterBoolean = 'CallExpression[callee.property.name="filter"][arguments.length=1][arguments.0.name="Boolean"]';
3
+ // `.filter(x => …)` with a single single-parameter arrow predicate; its body is checked below.
4
+ const filterArrow = 'CallExpression[callee.property.name="filter"][arguments.length=1][arguments.0.type="ArrowFunctionExpression"][arguments.0.params.length=1][arguments.0.params.0.type="Identifier"]';
5
+ const isParam = (node, param) => node.type === 'Identifier' && node.name === param;
3
6
  export const preferCompact = createRule({
4
7
  name: 'prefer-compact',
5
8
  meta: {
6
9
  type: 'suggestion',
7
10
  docs: {
8
- description: 'Prefer `compact` from es-toolkit over `.filter(Boolean)`.',
11
+ description: 'Prefer `compact` from es-toolkit over a truthiness filter.',
9
12
  },
10
13
  schema: [],
11
14
  messages: {
12
- preferCompact: 'Prefer `compact` from es-toolkit instead of `.filter(Boolean)`.',
15
+ preferCompact: 'Prefer `compact` from es-toolkit instead of filtering by truthiness.',
13
16
  },
14
17
  },
15
18
  create(context) {
19
+ const report = (node) => context.report({ node, messageId: 'preferCompact' });
16
20
  return {
17
- [filterBoolean]: (node) => context.report({ node, messageId: 'preferCompact' }),
21
+ [filterBoolean]: report,
22
+ [filterArrow](node) {
23
+ const arrow = node.arguments[0];
24
+ if (arrow.type !== 'ArrowFunctionExpression')
25
+ return;
26
+ const [param] = arrow.params;
27
+ if (param.type !== 'Identifier')
28
+ return;
29
+ const { body } = arrow;
30
+ // The predicate keeps only truthy values: `x => !!x` or `x => Boolean(x)`.
31
+ const isDoubleBang = body.type === 'UnaryExpression'
32
+ && body.operator === '!'
33
+ && body.argument.type === 'UnaryExpression'
34
+ && body.argument.operator === '!'
35
+ && isParam(body.argument.argument, param.name);
36
+ const isBooleanCall = body.type === 'CallExpression'
37
+ && body.callee.type === 'Identifier'
38
+ && body.callee.name === 'Boolean'
39
+ && body.arguments.length === 1
40
+ && isParam(body.arguments[0], param.name);
41
+ if (isDoubleBang || isBooleanCall)
42
+ report(node);
43
+ },
18
44
  };
19
45
  },
20
46
  });
@@ -1,7 +1,10 @@
1
1
  import { createRule } from '../utils/create-rule.js';
2
2
  // `new Promise(resolve => setTimeout(resolve, ms))` — a single-param executor whose
3
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"]';
4
+ const expressionExecutor = '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
+ // The block-bodied form `new Promise(resolve => { setTimeout(resolve, ms); })` — a single
6
+ // statement that is the same two-argument `setTimeout`.
7
+ const blockExecutor = 'NewExpression[callee.name="Promise"][arguments.length=1] > ArrowFunctionExpression[params.length=1][params.0.type="Identifier"][body.type="BlockStatement"][body.body.length=1][body.body.0.type="ExpressionStatement"][body.body.0.expression.callee.name="setTimeout"][body.body.0.expression.arguments.length=2][body.body.0.expression.arguments.0.type="Identifier"]';
5
8
  export const preferDelay = createRule({
6
9
  name: 'prefer-delay',
7
10
  meta: {
@@ -15,18 +18,29 @@ export const preferDelay = createRule({
15
18
  },
16
19
  },
17
20
  create(context) {
21
+ // Confirm the timeout callback IS the promise's own `resolve` (a pure delay).
22
+ const isPureDelay = (param, timeout) => {
23
+ const [callback] = timeout.arguments;
24
+ return (param.type === 'Identifier'
25
+ && callback.type === 'Identifier'
26
+ && param.name === callback.name);
27
+ };
28
+ const reportPromise = (node) => context.report({ node: node.parent, messageId: 'preferDelay' });
18
29
  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')
30
+ [expressionExecutor](node) {
31
+ if (node.body.type === 'CallExpression'
32
+ && isPureDelay(node.params[0], node.body)) {
33
+ reportPromise(node);
34
+ }
35
+ },
36
+ [blockExecutor](node) {
37
+ if (node.body.type !== 'BlockStatement')
24
38
  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' });
39
+ const [statement] = node.body.body;
40
+ if (statement.type === 'ExpressionStatement'
41
+ && statement.expression.type === 'CallExpression'
42
+ && isPureDelay(node.params[0], statement.expression)) {
43
+ reportPromise(node);
30
44
  }
31
45
  },
32
46
  };
@@ -1,7 +1,9 @@
1
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"]`;
2
+ const objectKeysLength = (path) => `[${path}.property.name="length"][${path}.object.callee.object.name="Object"][${path}.object.callee.property.name="keys"]`;
3
3
  // `Object.keys(obj).length === 0`, in either operand order.
4
4
  const keysLengthZero = (zero, keys) => `BinaryExpression[operator=/^===?$/][${zero}.value=0]${objectKeysLength(keys)}`;
5
+ // `!Object.keys(obj).length` — a falsy length means no keys.
6
+ const notKeysLength = `UnaryExpression[operator="!"]${objectKeysLength('argument')}`;
5
7
  export const preferIsEmpty = createRule({
6
8
  name: 'prefer-is-empty',
7
9
  meta: {
@@ -19,6 +21,7 @@ export const preferIsEmpty = createRule({
19
21
  return {
20
22
  [keysLengthZero('right', 'left')]: report,
21
23
  [keysLengthZero('left', 'right')]: report,
24
+ [notKeysLength]: report,
22
25
  };
23
26
  },
24
27
  });
@@ -1,22 +1,29 @@
1
1
  import { createRule } from '../utils/create-rule.js';
2
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"])))`;
3
+ // `Math.random() * X`. A `.length` multiplier is left to `prefer-sample`.
4
+ const product = `BinaryExpression[operator="*"]:has(> ${random}):not(:has(> MemberExpression[property.name="length"]))`;
5
+ // Each idiom truncates that product to an int: `Math.floor`/`Math.trunc`, `~~`, or `| 0`.
6
+ const truncCall = `CallExpression[callee.object.name="Math"][callee.property.name=/^(floor|trunc)$/][arguments.length=1]:has(> ${product})`;
7
+ const doubleTilde = `UnaryExpression[operator="~"]:has(> UnaryExpression[operator="~"]:has(> ${product}))`;
8
+ const bitwiseOr = `BinaryExpression[operator="|"][right.value=0]:has(> ${product})`;
5
9
  export const preferRandomInt = createRule({
6
10
  name: 'prefer-random-int',
7
11
  meta: {
8
12
  type: 'suggestion',
9
13
  docs: {
10
- description: 'Prefer `randomInt` from es-toolkit over `Math.floor(Math.random() * …)`.',
14
+ description: 'Prefer `randomInt` from es-toolkit over truncating `Math.random() * …` to an int.',
11
15
  },
12
16
  schema: [],
13
17
  messages: {
14
- preferRandomInt: 'Prefer `randomInt` from es-toolkit instead of `Math.floor(Math.random() * …)`.',
18
+ preferRandomInt: 'Prefer `randomInt` from es-toolkit instead of truncating `Math.random() * …` to an int.',
15
19
  },
16
20
  },
17
21
  create(context) {
22
+ const report = (node) => context.report({ node, messageId: 'preferRandomInt' });
18
23
  return {
19
- [floorRandom]: (node) => context.report({ node, messageId: 'preferRandomInt' }),
24
+ [truncCall]: report,
25
+ [doubleTilde]: report,
26
+ [bitwiseOr]: report,
20
27
  };
21
28
  },
22
29
  });
@@ -1,6 +1,12 @@
1
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"])';
2
+ const random = 'CallExpression[callee.object.name="Math"][callee.property.name="random"]';
3
+ const length = 'MemberExpression[property.name="length"]';
4
+ // A computed access `arr[<index>]` whose index draws on `Math.random()` and an array `.length`.
5
+ const randomIndexAccess = (index) => `MemberExpression[computed=true]${index}:has(${random}):has(${length})`;
6
+ // The index truncates to an int via `Math.floor`/`Math.trunc`, `~~`, or `| 0`.
7
+ const truncCall = '[property.callee.object.name="Math"][property.callee.property.name=/^(floor|trunc)$/]';
8
+ const doubleTilde = '[property.type="UnaryExpression"][property.operator="~"][property.argument.type="UnaryExpression"][property.argument.operator="~"]';
9
+ const bitwiseOr = '[property.type="BinaryExpression"][property.operator="|"][property.right.value=0]';
4
10
  export const preferSample = createRule({
5
11
  name: 'prefer-sample',
6
12
  meta: {
@@ -14,8 +20,11 @@ export const preferSample = createRule({
14
20
  },
15
21
  },
16
22
  create(context) {
23
+ const report = (node) => context.report({ node, messageId: 'preferSample' });
17
24
  return {
18
- [randomIndex]: (node) => context.report({ node, messageId: 'preferSample' }),
25
+ [randomIndexAccess(truncCall)]: report,
26
+ [randomIndexAccess(doubleTilde)]: report,
27
+ [randomIndexAccess(bitwiseOr)]: report,
19
28
  };
20
29
  },
21
30
  });
@@ -4,16 +4,19 @@ Prefer [`compact`](https://es-toolkit.dev/reference/array/compact.html) from es-
4
4
  over `.filter(Boolean)`.
5
5
 
6
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.
7
+ exactly what a truthiness filter does, but named and (in TypeScript) better typed.
8
8
 
9
9
  ## Rule details
10
10
 
11
- This rule reports — it does **not** auto-fix — a single-argument `.filter(Boolean)` call.
11
+ This rule reports — it does **not** auto-fix — a single-argument truthiness filter:
12
+ `.filter(Boolean)`, `.filter(x => !!x)`, or `.filter(x => Boolean(x))`.
12
13
 
13
14
  ### ❌ Incorrect
14
15
 
15
16
  ```js
16
17
  arr.filter(Boolean);
18
+ arr.filter(x => !!x);
19
+ arr.filter(x => Boolean(x));
17
20
  items.filter(Boolean).map(x => x.id);
18
21
  ```
19
22
 
@@ -26,11 +29,16 @@ compact(arr);
26
29
 
27
30
  // Not a falsy filter — left untouched:
28
31
  arr.filter(x => x);
32
+ arr.filter(x => !!y); // filters by a different value
33
+ arr.filter(x => Boolean(x.foo)); // filters by a property, not the element
29
34
  arr.map(Boolean);
30
35
  ```
31
36
 
32
37
  ## Limitations
33
38
 
39
+ - **Arrow predicate is identity-checked.** `x => !!x` and `x => Boolean(x)` are reported only
40
+ when the negated/wrapped value is the predicate's own parameter, so `x => !!y` and
41
+ `x => Boolean(x.foo)` are left alone.
34
42
  - **Syntactic match.** A shadowed local `Boolean` could produce a false positive. This is
35
43
  rare in practice.
36
44
  - **TypeScript bonus.** Beyond readability, `compact` narrows nullable element types, while
@@ -9,12 +9,16 @@ hand-rolled `new Promise((resolve) => setTimeout(resolve, ms))` sleep idiom.
9
9
  ## Rule details
10
10
 
11
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.
12
+ executor immediately calls `setTimeout` with that same parameter as the callback, whether the
13
+ executor has an expression body or a single-statement block body.
13
14
 
14
15
  ### ❌ Incorrect
15
16
 
16
17
  ```js
17
18
  new Promise(resolve => setTimeout(resolve, 1000));
19
+ new Promise(resolve => {
20
+ setTimeout(resolve, ms);
21
+ });
18
22
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
19
23
  ```
20
24
 
@@ -28,15 +32,17 @@ await delay(1000);
28
32
  // Not a pure delay — left untouched:
29
33
  new Promise(resolve => setTimeout(doWork, 1000)); // runs work, not just a delay
30
34
  new Promise(resolve => {
31
- setTimeout(resolve, ms); // block-bodied executor, see Limitations
35
+ doWork();
36
+ setTimeout(resolve, ms); // does more than wait, see Limitations
32
37
  });
33
38
  ```
34
39
 
35
40
  ## Limitations
36
41
 
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.
42
+ - **Single-statement executors only.** A block body that does more than the one `setTimeout`
43
+ (e.g. `new Promise((resolve) => { doWork(); setTimeout(resolve, ms); })`), arrow-only forms,
44
+ and wrapped forms like `setTimeout(() => resolve(), ms)` are not reported, to keep the rule
45
+ precise. Function-expression executors (`function (resolve) { … }`) are also not flagged.
40
46
  - **Identity-checked.** The rule reports only when `setTimeout`'s callback is the promise's
41
47
  own `resolve` parameter, so `setTimeout(doWork, ms)` inside a `new Promise` is left alone.
42
48
  - **No auto-fix by design.** The fix requires adding an `import { delay } from 'es-toolkit'`.
@@ -8,14 +8,15 @@ allocating an intermediate keys array.
8
8
 
9
9
  ## Rule details
10
10
 
11
- This rule reports — it does **not** auto-fix — an equality comparison of
12
- `Object.keys(...).length` against `0`, in either operand order.
11
+ This rule reports — it does **not** auto-fix — a check that `Object.keys(...).length` is
12
+ zero: an equality comparison against `0` in either operand order, or the `!`-negated length.
13
13
 
14
14
  ### ❌ Incorrect
15
15
 
16
16
  ```js
17
17
  Object.keys(obj).length === 0;
18
18
  0 === Object.keys(obj).length;
19
+ !Object.keys(obj).length;
19
20
  ```
20
21
 
21
22
  ### ✅ Correct
@@ -8,14 +8,17 @@ formula; `randomInt` names the intent and removes the off-by-one foot-guns.
8
8
 
9
9
  ## Rule details
10
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.
11
+ This rule reports — it does **not** auto-fix — a `Math.random() * …` product truncated to an
12
+ integer, whether by `Math.floor(...)`, `Math.trunc(...)`, `~~(...)`, or `… | 0`. A `.length`
13
+ multiplier is left to [`prefer-sample`](./prefer-sample.md) instead.
14
14
 
15
15
  ### ❌ Incorrect
16
16
 
17
17
  ```js
18
18
  Math.floor(Math.random() * 6);
19
+ Math.trunc(Math.random() * 6);
20
+ ~~(Math.random() * 6);
21
+ (Math.random() * 6) | 0;
19
22
  Math.floor(Math.random() * (max - min + 1)) + min;
20
23
  ```
21
24
 
@@ -28,7 +31,8 @@ randomInt(0, 6);
28
31
 
29
32
  // Not this rule's concern — left untouched:
30
33
  Math.random(); // a float in [0, 1)
31
- Math.round(Math.random() * n); // rounds, not floors
34
+ Math.round(Math.random() * n); // rounds, not truncates
35
+ Math.ceil(Math.random() * n); // changes the bounds
32
36
  arr[Math.floor(Math.random() * arr.length)]; // see prefer-sample
33
37
  ```
34
38
 
@@ -37,7 +41,8 @@ arr[Math.floor(Math.random() * arr.length)]; // see prefer-sample
37
41
  - **Bounds differ.** The inclusive idiom `Math.floor(Math.random() * (max - min + 1)) + min`
38
42
  yields `[min, max]`, whereas `randomInt(min, max)` yields `[min, max)`. Adjust the upper
39
43
  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).
44
+ - **Truncation only.** `Math.floor`, `Math.trunc`, `~~`, and `| 0` are reported (all truncate
45
+ toward zero for the non-negative `Math.random() * n` range). `Math.round(...)` and
46
+ `Math.ceil(...)` are **not** — they change the distribution or bounds.
42
47
  - **Syntactic match.** A shadowed local `Math` could produce a false positive.
43
48
  - **No auto-fix by design.** The fix requires adding an `import { randomInt } from 'es-toolkit'`.
@@ -8,13 +8,17 @@ out the same idea by hand and repeats the array.
8
8
 
9
9
  ## Rule details
10
10
 
11
- This rule reports — it does **not** auto-fix — a computed access whose index is
12
- `Math.floor(Math.random() * …length)`.
11
+ This rule reports — it does **not** auto-fix — a computed access whose index truncates
12
+ `Math.random() * …length` to an integer, whether by `Math.floor`/`Math.trunc`, `~~`, or
13
+ `… | 0`.
13
14
 
14
15
  ### ❌ Incorrect
15
16
 
16
17
  ```js
17
18
  arr[Math.floor(Math.random() * arr.length)];
19
+ arr[Math.trunc(Math.random() * arr.length)];
20
+ arr[~~(Math.random() * arr.length)];
21
+ arr[(Math.random() * arr.length) | 0];
18
22
  items[Math.floor(Math.random() * items.length)];
19
23
  ```
20
24
 
@@ -35,5 +39,7 @@ arr[i];
35
39
  - **Object identity is not checked.** The selector matches `a[Math.floor(Math.random() *
36
40
  b.length)]`, so the rare case where the indexed array and the `.length` array differ would
37
41
  also be reported.
42
+ - **Truncation only.** The index must truncate toward zero (`Math.floor`, `Math.trunc`, `~~`,
43
+ or `| 0`); `Math.round`/`Math.ceil` indices are not reported.
38
44
  - **Syntactic match.** A shadowed local `Math` could produce a false positive.
39
45
  - **No auto-fix by design.** The fix requires adding an `import { sample } from 'es-toolkit'`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "es-toolkit-eslint-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "ESLint plugin that suggests es-toolkit utilities in place of verbose JS/TS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,20 +32,18 @@
32
32
  "url": "https://github.com/syxov/es-toolkit-eslint-plugin/issues"
33
33
  },
34
34
  "homepage": "https://github.com/syxov/es-toolkit-eslint-plugin#readme",
35
- "dependencies": {
36
- "@typescript-eslint/utils": "^8.61.1"
37
- },
38
35
  "peerDependencies": {
36
+ "@typescript-eslint/utils": "^8.61.1",
39
37
  "eslint": ">=10"
40
38
  },
41
39
  "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",
40
+ "@types/node": "^26.0.0",
41
+ "@typescript-eslint/parser": "^8.61.1",
42
+ "@typescript-eslint/rule-tester": "^8.61.1",
43
+ "eslint": "^10.5.0",
46
44
  "prettier": "^3.8.4",
47
45
  "typescript": "^6.0.3",
48
- "vitest": "^3.0.0"
46
+ "vitest": "^4.1.9"
49
47
  },
50
48
  "scripts": {
51
49
  "build": "tsc",