@e18e/eslint-plugin 0.0.1
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 +95 -0
- package/lib/configs/modernization.d.ts +2 -0
- package/lib/configs/modernization.js +17 -0
- package/lib/configs/module-replacements.d.ts +2 -0
- package/lib/configs/module-replacements.js +8 -0
- package/lib/configs/performance-improvements.d.ts +2 -0
- package/lib/configs/performance-improvements.js +9 -0
- package/lib/configs/recommended.d.ts +2 -0
- package/lib/configs/recommended.js +18 -0
- package/lib/main.d.ts +3 -0
- package/lib/main.js +48 -0
- package/lib/rules/no-indexof-equality.d.ts +2 -0
- package/lib/rules/no-indexof-equality.js +90 -0
- package/lib/rules/prefer-array-at.d.ts +2 -0
- package/lib/rules/prefer-array-at.js +58 -0
- package/lib/rules/prefer-array-fill.d.ts +2 -0
- package/lib/rules/prefer-array-fill.js +120 -0
- package/lib/rules/prefer-array-from-map.d.ts +2 -0
- package/lib/rules/prefer-array-from-map.js +57 -0
- package/lib/rules/prefer-array-to-reversed.d.ts +2 -0
- package/lib/rules/prefer-array-to-reversed.js +42 -0
- package/lib/rules/prefer-array-to-sorted.d.ts +2 -0
- package/lib/rules/prefer-array-to-sorted.js +43 -0
- package/lib/rules/prefer-array-to-spliced.d.ts +2 -0
- package/lib/rules/prefer-array-to-spliced.js +43 -0
- package/lib/rules/prefer-exponentiation-operator.d.ts +2 -0
- package/lib/rules/prefer-exponentiation-operator.js +42 -0
- package/lib/rules/prefer-includes.d.ts +2 -0
- package/lib/rules/prefer-includes.js +131 -0
- package/lib/rules/prefer-nullish-coalescing.d.ts +2 -0
- package/lib/rules/prefer-nullish-coalescing.js +131 -0
- package/lib/rules/prefer-object-has-own.d.ts +2 -0
- package/lib/rules/prefer-object-has-own.js +71 -0
- package/lib/rules/prefer-optimized-indexof.d.ts +2 -0
- package/lib/rules/prefer-optimized-indexof.js +90 -0
- package/lib/rules/prefer-settimeout-args.d.ts +2 -0
- package/lib/rules/prefer-settimeout-args.js +175 -0
- package/lib/rules/prefer-spread-syntax.d.ts +2 -0
- package/lib/rules/prefer-spread-syntax.js +109 -0
- package/lib/rules/prefer-timer-args.d.ts +2 -0
- package/lib/rules/prefer-timer-args.js +176 -0
- package/lib/rules/prefer-url-canparse.d.ts +2 -0
- package/lib/rules/prefer-url-canparse.js +139 -0
- package/lib/test/setup.d.ts +1 -0
- package/lib/test/setup.js +10 -0
- package/lib/utils/ast.d.ts +15 -0
- package/lib/utils/ast.js +47 -0
- package/lib/utils/typescript.d.ts +14 -0
- package/lib/utils/typescript.js +6 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 e18e
|
|
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,95 @@
|
|
|
1
|
+
# @e18e/eslint-plugin
|
|
2
|
+
|
|
3
|
+
> The official e18e ESLint plugin for modernizing JavaScript/TypeScript code and improving performance.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> This is an experimental, unpublished project for now. Once we have settled on the scope, we will publish it and announce it to start getting community feedback.
|
|
7
|
+
|
|
8
|
+
This plugin focuses on applying the e18e community's best practices and advise to JavaScript/TypeScript codebases.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
There are a few categories of rules in this plugin:
|
|
13
|
+
|
|
14
|
+
- Modernization - New syntax and APIs which improve code readability and performance
|
|
15
|
+
- Module replacements - Community recommended alternatives to popular libraries, focused on performance and size
|
|
16
|
+
- Performance improvements - Patterns that can be optimized for better runtime performance
|
|
17
|
+
|
|
18
|
+
Each of these can be enabled individually, or you can use the recommended configuration to enable all rules.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install --save-dev @e18e/eslint-plugin
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Add the plugin to your `eslint.config.js`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import e18e from '@e18e/eslint-plugin';
|
|
32
|
+
|
|
33
|
+
export default [
|
|
34
|
+
// Use the recommended configuration (includes all categories)
|
|
35
|
+
e18e.configs.recommended,
|
|
36
|
+
|
|
37
|
+
// Or use specific category configurations
|
|
38
|
+
e18e.configs.modernization,
|
|
39
|
+
e18e.configs.moduleReplacements,
|
|
40
|
+
e18e.configs.performanceImprovements,
|
|
41
|
+
|
|
42
|
+
// Or configure rules manually
|
|
43
|
+
{
|
|
44
|
+
plugins: {
|
|
45
|
+
e18e
|
|
46
|
+
},
|
|
47
|
+
rules: {
|
|
48
|
+
'e18e/prefer-array-at': 'error',
|
|
49
|
+
'e18e/prefer-array-fill': 'error',
|
|
50
|
+
'e18e/prefer-includes': 'error'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Rules
|
|
57
|
+
|
|
58
|
+
**Legend:**
|
|
59
|
+
- ✅ = Yes / Enabled
|
|
60
|
+
- ✖️ = No / Disabled
|
|
61
|
+
- 💡 = Has suggestions (requires user confirmation for fixes)
|
|
62
|
+
|
|
63
|
+
### Modernization
|
|
64
|
+
|
|
65
|
+
| Rule | Description | Recommended | Fixable | Requires Types |
|
|
66
|
+
|------|-------------|-------------|---------|----------------|
|
|
67
|
+
| [prefer-array-at](./src/rules/prefer-array-at.ts) | Prefer `Array.prototype.at()` over length-based indexing | ✅ | ✅ | ✖️ |
|
|
68
|
+
| [prefer-array-fill](./src/rules/prefer-array-fill.ts) | Prefer `Array.prototype.fill()` over `Array.from()` or `map()` with constant values | ✅ | ✅ | ✖️ |
|
|
69
|
+
| [prefer-includes](./src/rules/prefer-includes.ts) | Prefer `.includes()` over `indexOf()` comparisons for arrays and strings | ✅ | ✅ | ✖️ |
|
|
70
|
+
| [prefer-array-to-reversed](./src/rules/prefer-array-to-reversed.ts) | Prefer `Array.prototype.toReversed()` over copying and reversing arrays | ✅ | ✅ | ✖️ |
|
|
71
|
+
| [prefer-array-to-sorted](./src/rules/prefer-array-to-sorted.ts) | Prefer `Array.prototype.toSorted()` over copying and sorting arrays | ✅ | ✅ | ✖️ |
|
|
72
|
+
| [prefer-array-to-spliced](./src/rules/prefer-array-to-spliced.ts) | Prefer `Array.prototype.toSpliced()` over copying and splicing arrays | ✅ | ✅ | ✖️ |
|
|
73
|
+
| [prefer-exponentiation-operator](./src/rules/prefer-exponentiation-operator.ts) | Prefer the exponentiation operator `**` over `Math.pow()` | ✅ | ✅ | ✖️ |
|
|
74
|
+
| [prefer-nullish-coalescing](./src/rules/prefer-nullish-coalescing.ts) | Prefer nullish coalescing operator (`??` and `??=`) over verbose null checks | ✅ | ✅ | ✖️ |
|
|
75
|
+
| [prefer-object-has-own](./src/rules/prefer-object-has-own.ts) | Prefer `Object.hasOwn()` over `Object.prototype.hasOwnProperty.call()` and `obj.hasOwnProperty()` | ✅ | ✅ | ✖️ |
|
|
76
|
+
| [prefer-spread-syntax](./src/rules/prefer-spread-syntax.ts) | Prefer spread syntax over `Array.concat()`, `Array.from()`, `Object.assign({}, ...)`, and `Function.apply()` | ✅ | ✅ | ✖️ |
|
|
77
|
+
| [prefer-url-canparse](./src/rules/prefer-url-canparse.ts) | Prefer `URL.canParse()` over try-catch blocks for URL validation | ✅ | 💡 | ✖️ |
|
|
78
|
+
|
|
79
|
+
### Module replacements
|
|
80
|
+
|
|
81
|
+
| Rule | Description | Recommended | Fixable | Requires Types |
|
|
82
|
+
|------|-------------|-------------|---------|----------------|
|
|
83
|
+
| ban-dependencies | Ban dependencies in favor of lighter alternatives | ✅ | ✖️ | ✖️ |
|
|
84
|
+
|
|
85
|
+
### Performance improvements
|
|
86
|
+
|
|
87
|
+
| Rule | Description | Recommended | Fixable | Requires Types |
|
|
88
|
+
|------|-------------|-------------|---------|----------------|
|
|
89
|
+
| [no-indexof-equality](./src/rules/no-indexof-equality.ts) | Prefer `startsWith()` for strings and direct array access over `indexOf()` equality checks | ✖️ | ✅ | ✅ |
|
|
90
|
+
| [prefer-array-from-map](./src/rules/prefer-array-from-map.ts) | Prefer `Array.from(iterable, mapper)` over `[...iterable].map(mapper)` to avoid intermediate array allocation | ✅ | ✅ | ✖️ |
|
|
91
|
+
| [prefer-timer-args](./src/rules/prefer-timer-args.ts) | Prefer passing function and arguments directly to `setTimeout`/`setInterval` instead of wrapping in an arrow function or using `bind` | ✅ | ✅ | ✖️ |
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const modernization = (plugin) => ({
|
|
2
|
+
plugins: {
|
|
3
|
+
e18e: plugin
|
|
4
|
+
},
|
|
5
|
+
rules: {
|
|
6
|
+
'e18e/prefer-array-at': 'error',
|
|
7
|
+
'e18e/prefer-array-fill': 'error',
|
|
8
|
+
'e18e/prefer-includes': 'error',
|
|
9
|
+
'e18e/prefer-array-to-reversed': 'error',
|
|
10
|
+
'e18e/prefer-array-to-sorted': 'error',
|
|
11
|
+
'e18e/prefer-array-to-spliced': 'error',
|
|
12
|
+
'e18e/prefer-nullish-coalescing': 'error',
|
|
13
|
+
'e18e/prefer-object-has-own': 'error',
|
|
14
|
+
'e18e/prefer-spread-syntax': 'error',
|
|
15
|
+
'e18e/prefer-url-canparse': 'error'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { modernization } from './modernization.js';
|
|
2
|
+
import { moduleReplacements } from './module-replacements.js';
|
|
3
|
+
import { performanceImprovements } from './performance-improvements.js';
|
|
4
|
+
export const recommended = (plugin) => {
|
|
5
|
+
const modernizationConfig = modernization(plugin);
|
|
6
|
+
const moduleReplacementsConfig = moduleReplacements(plugin);
|
|
7
|
+
const performanceImprovementsConfig = performanceImprovements(plugin);
|
|
8
|
+
return {
|
|
9
|
+
plugins: {
|
|
10
|
+
e18e: plugin
|
|
11
|
+
},
|
|
12
|
+
rules: {
|
|
13
|
+
...modernizationConfig.rules,
|
|
14
|
+
...moduleReplacementsConfig.rules,
|
|
15
|
+
...performanceImprovementsConfig.rules
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
};
|
package/lib/main.d.ts
ADDED
package/lib/main.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { recommended } from './configs/recommended.js';
|
|
2
|
+
import { modernization } from './configs/modernization.js';
|
|
3
|
+
import { moduleReplacements } from './configs/module-replacements.js';
|
|
4
|
+
import { performanceImprovements } from './configs/performance-improvements.js';
|
|
5
|
+
import { preferArrayAt } from './rules/prefer-array-at.js';
|
|
6
|
+
import { preferArrayFill } from './rules/prefer-array-fill.js';
|
|
7
|
+
import { preferArrayFromMap } from './rules/prefer-array-from-map.js';
|
|
8
|
+
import { preferIncludes } from './rules/prefer-includes.js';
|
|
9
|
+
import { preferArrayToReversed } from './rules/prefer-array-to-reversed.js';
|
|
10
|
+
import { preferArrayToSorted } from './rules/prefer-array-to-sorted.js';
|
|
11
|
+
import { preferArrayToSpliced } from './rules/prefer-array-to-spliced.js';
|
|
12
|
+
import { preferExponentiationOperator } from './rules/prefer-exponentiation-operator.js';
|
|
13
|
+
import { preferNullishCoalescing } from './rules/prefer-nullish-coalescing.js';
|
|
14
|
+
import { preferObjectHasOwn } from './rules/prefer-object-has-own.js';
|
|
15
|
+
import { preferSpreadSyntax } from './rules/prefer-spread-syntax.js';
|
|
16
|
+
import { preferUrlCanParse } from './rules/prefer-url-canparse.js';
|
|
17
|
+
import { noIndexOfEquality } from './rules/no-indexof-equality.js';
|
|
18
|
+
import { preferTimerArgs } from './rules/prefer-timer-args.js';
|
|
19
|
+
import { rules as dependRules } from 'eslint-plugin-depend';
|
|
20
|
+
const plugin = {
|
|
21
|
+
meta: {
|
|
22
|
+
name: '@e18e/eslint-plugin',
|
|
23
|
+
namespace: 'e18e'
|
|
24
|
+
},
|
|
25
|
+
configs: {},
|
|
26
|
+
rules: {
|
|
27
|
+
'prefer-array-at': preferArrayAt,
|
|
28
|
+
'prefer-array-fill': preferArrayFill,
|
|
29
|
+
'prefer-array-from-map': preferArrayFromMap,
|
|
30
|
+
'prefer-includes': preferIncludes,
|
|
31
|
+
'prefer-array-to-reversed': preferArrayToReversed,
|
|
32
|
+
'prefer-array-to-sorted': preferArrayToSorted,
|
|
33
|
+
'prefer-array-to-spliced': preferArrayToSpliced,
|
|
34
|
+
'prefer-exponentiation-operator': preferExponentiationOperator,
|
|
35
|
+
'prefer-nullish-coalescing': preferNullishCoalescing,
|
|
36
|
+
'prefer-object-has-own': preferObjectHasOwn,
|
|
37
|
+
'prefer-spread-syntax': preferSpreadSyntax,
|
|
38
|
+
'prefer-url-canparse': preferUrlCanParse,
|
|
39
|
+
'no-indexof-equality': noIndexOfEquality,
|
|
40
|
+
'prefer-timer-args': preferTimerArgs,
|
|
41
|
+
...dependRules
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
plugin.configs.recommended = recommended(plugin);
|
|
45
|
+
plugin.configs.modernization = modernization(plugin);
|
|
46
|
+
plugin.configs.moduleReplacements = moduleReplacements(plugin);
|
|
47
|
+
plugin.configs.performanceImprovements = performanceImprovements(plugin);
|
|
48
|
+
export default plugin;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getTypedParserServices } from '../utils/typescript.js';
|
|
2
|
+
export const noIndexOfEquality = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: 'suggestion',
|
|
5
|
+
docs: {
|
|
6
|
+
description: 'Prefer optimized alternatives to `indexOf()` equality checks',
|
|
7
|
+
recommended: false
|
|
8
|
+
},
|
|
9
|
+
fixable: 'code',
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
preferDirectAccess: 'Use direct array access `{{array}}[{{index}}] === {{item}}` instead of `indexOf() === {{index}}`',
|
|
13
|
+
preferStartsWith: 'Use `.startsWith()` instead of `indexOf() === 0` for strings'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const sourceCode = context.sourceCode;
|
|
18
|
+
const services = getTypedParserServices(context);
|
|
19
|
+
const checker = services.program.getTypeChecker();
|
|
20
|
+
return {
|
|
21
|
+
BinaryExpression(node) {
|
|
22
|
+
if (node.operator !== '===' && node.operator !== '==') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let indexOfCall;
|
|
26
|
+
let compareIndex;
|
|
27
|
+
if (node.left.type === 'CallExpression' &&
|
|
28
|
+
node.right.type === 'Literal' &&
|
|
29
|
+
typeof node.right.value === 'number' &&
|
|
30
|
+
node.right.value >= 0) {
|
|
31
|
+
indexOfCall = node.left;
|
|
32
|
+
compareIndex = node.right.value;
|
|
33
|
+
}
|
|
34
|
+
else if (node.right.type === 'CallExpression' &&
|
|
35
|
+
node.left.type === 'Literal' &&
|
|
36
|
+
typeof node.left.value === 'number' &&
|
|
37
|
+
node.left.value >= 0) {
|
|
38
|
+
indexOfCall = node.right;
|
|
39
|
+
compareIndex = node.left.value;
|
|
40
|
+
}
|
|
41
|
+
if (!indexOfCall || compareIndex === undefined) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (indexOfCall.callee.type !== 'MemberExpression' ||
|
|
45
|
+
indexOfCall.callee.property.type !== 'Identifier' ||
|
|
46
|
+
indexOfCall.callee.property.name !== 'indexOf') {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (indexOfCall.arguments.length !== 1) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const objectNode = indexOfCall.callee.object;
|
|
53
|
+
const searchArg = indexOfCall.arguments[0];
|
|
54
|
+
const type = services.getTypeAtLocation(objectNode);
|
|
55
|
+
if (!type) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const objectText = sourceCode.getText(objectNode);
|
|
59
|
+
const searchText = sourceCode.getText(searchArg);
|
|
60
|
+
const stringType = checker.getStringType();
|
|
61
|
+
if (checker.isTypeAssignableTo(type, stringType)) {
|
|
62
|
+
if (compareIndex === 0) {
|
|
63
|
+
context.report({
|
|
64
|
+
node,
|
|
65
|
+
messageId: 'preferStartsWith',
|
|
66
|
+
fix(fixer) {
|
|
67
|
+
return fixer.replaceText(node, `${objectText}.startsWith(${searchText})`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (checker.isArrayType(type)) {
|
|
74
|
+
context.report({
|
|
75
|
+
node,
|
|
76
|
+
messageId: 'preferDirectAccess',
|
|
77
|
+
data: {
|
|
78
|
+
array: objectText,
|
|
79
|
+
item: searchText,
|
|
80
|
+
index: String(compareIndex)
|
|
81
|
+
},
|
|
82
|
+
fix(fixer) {
|
|
83
|
+
return fixer.replaceText(node, `${objectText}[${compareIndex}] === ${searchText}`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const preferArrayAt = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description: 'Prefer Array.prototype.at() over length-based indexing',
|
|
6
|
+
recommended: true
|
|
7
|
+
},
|
|
8
|
+
fixable: 'code',
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
preferAt: 'Use .at(-1) instead of [{{array}}.length - 1]'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
const sourceCode = context.sourceCode;
|
|
16
|
+
return {
|
|
17
|
+
MemberExpression(node) {
|
|
18
|
+
if (!node.computed || !node.property) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (node.property.type !== 'BinaryExpression') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const propertyExpr = node.property;
|
|
25
|
+
if (propertyExpr.operator !== '-') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (propertyExpr.right.type !== 'Literal' ||
|
|
29
|
+
propertyExpr.right.value !== 1) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (propertyExpr.left.type !== 'MemberExpression') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const leftMember = propertyExpr.left;
|
|
36
|
+
if (leftMember.property.type !== 'Identifier' ||
|
|
37
|
+
leftMember.property.name !== 'length') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const arrayText = sourceCode.getText(node.object);
|
|
41
|
+
const lengthArrayText = sourceCode.getText(leftMember.object);
|
|
42
|
+
if (arrayText !== lengthArrayText) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
messageId: 'preferAt',
|
|
48
|
+
data: {
|
|
49
|
+
array: arrayText
|
|
50
|
+
},
|
|
51
|
+
fix(fixer) {
|
|
52
|
+
return fixer.replaceText(node, `${arrayText}.at(-1)`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
function isConstantCallback(func) {
|
|
2
|
+
return (func.params.length === 0 &&
|
|
3
|
+
(func.body.type !== 'BlockStatement' ||
|
|
4
|
+
(func.body.body.length === 1 &&
|
|
5
|
+
func.body.body[0]?.type === 'ReturnStatement')));
|
|
6
|
+
}
|
|
7
|
+
function getCallbackValueText(func, sourceCode) {
|
|
8
|
+
if (func.body.type === 'BlockStatement') {
|
|
9
|
+
const returnStmt = func.body.body[0];
|
|
10
|
+
if (returnStmt?.type === 'ReturnStatement' && returnStmt.argument) {
|
|
11
|
+
return sourceCode.getText(returnStmt.argument);
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return sourceCode.getText(func.body);
|
|
16
|
+
}
|
|
17
|
+
export const preferArrayFill = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'suggestion',
|
|
20
|
+
docs: {
|
|
21
|
+
description: 'Prefer Array.prototype.fill() over Array.from or map with constant values',
|
|
22
|
+
recommended: true
|
|
23
|
+
},
|
|
24
|
+
fixable: 'code',
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
preferFillArrayFrom: 'Use Array.from({length: {{length}}}).fill({{value}}) instead of Array.from with a constant callback',
|
|
28
|
+
preferFillSpreadMap: 'Use Array({{length}}).fill({{value}}) instead of spread Array with map'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
const sourceCode = context.sourceCode;
|
|
33
|
+
return {
|
|
34
|
+
CallExpression(node) {
|
|
35
|
+
// Check for Array.from({length: n}, () => value)
|
|
36
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
37
|
+
node.callee.object.type === 'Identifier' &&
|
|
38
|
+
node.callee.object.name === 'Array' &&
|
|
39
|
+
node.callee.property.type === 'Identifier' &&
|
|
40
|
+
node.callee.property.name === 'from' &&
|
|
41
|
+
node.arguments.length === 2) {
|
|
42
|
+
const firstArg = node.arguments[0];
|
|
43
|
+
const secondArg = node.arguments[1];
|
|
44
|
+
// Check if first arg is {length: n}
|
|
45
|
+
if (firstArg?.type === 'ObjectExpression' &&
|
|
46
|
+
firstArg.properties.length === 1 &&
|
|
47
|
+
firstArg.properties[0]?.type === 'Property' &&
|
|
48
|
+
firstArg.properties[0].key.type === 'Identifier' &&
|
|
49
|
+
firstArg.properties[0].key.name === 'length') {
|
|
50
|
+
// Check if second arg is a constant callback
|
|
51
|
+
if (secondArg &&
|
|
52
|
+
(secondArg.type === 'ArrowFunctionExpression' ||
|
|
53
|
+
secondArg.type === 'FunctionExpression') &&
|
|
54
|
+
isConstantCallback(secondArg)) {
|
|
55
|
+
const lengthValue = firstArg.properties[0].value;
|
|
56
|
+
const lengthText = sourceCode.getText(lengthValue);
|
|
57
|
+
const valueText = getCallbackValueText(secondArg, sourceCode);
|
|
58
|
+
if (!valueText) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
context.report({
|
|
62
|
+
node,
|
|
63
|
+
messageId: 'preferFillArrayFrom',
|
|
64
|
+
data: {
|
|
65
|
+
length: lengthText,
|
|
66
|
+
value: valueText
|
|
67
|
+
},
|
|
68
|
+
fix(fixer) {
|
|
69
|
+
return fixer.replaceText(node, `Array.from({length: ${lengthText}}).fill(${valueText})`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Check for [...Array(n)].map(() => value)
|
|
76
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
77
|
+
node.callee.property.type === 'Identifier' &&
|
|
78
|
+
node.callee.property.name === 'map' &&
|
|
79
|
+
node.callee.object.type === 'ArrayExpression' &&
|
|
80
|
+
node.callee.object.elements.length === 1 &&
|
|
81
|
+
node.arguments.length === 1) {
|
|
82
|
+
const spreadElement = node.callee.object.elements[0];
|
|
83
|
+
const callback = node.arguments[0];
|
|
84
|
+
if (spreadElement?.type === 'SpreadElement' &&
|
|
85
|
+
spreadElement.argument.type === 'CallExpression' &&
|
|
86
|
+
spreadElement.argument.callee.type === 'Identifier' &&
|
|
87
|
+
spreadElement.argument.callee.name === 'Array' &&
|
|
88
|
+
spreadElement.argument.arguments.length === 1) {
|
|
89
|
+
const arrayArg = spreadElement.argument.arguments[0];
|
|
90
|
+
// Check if callback is a constant function
|
|
91
|
+
if (callback &&
|
|
92
|
+
(callback.type === 'ArrowFunctionExpression' ||
|
|
93
|
+
callback.type === 'FunctionExpression') &&
|
|
94
|
+
isConstantCallback(callback)) {
|
|
95
|
+
if (!arrayArg) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const lengthText = sourceCode.getText(arrayArg);
|
|
99
|
+
const valueText = getCallbackValueText(callback, sourceCode);
|
|
100
|
+
if (!valueText) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
context.report({
|
|
104
|
+
node,
|
|
105
|
+
messageId: 'preferFillSpreadMap',
|
|
106
|
+
data: {
|
|
107
|
+
length: lengthText,
|
|
108
|
+
value: valueText
|
|
109
|
+
},
|
|
110
|
+
fix(fixer) {
|
|
111
|
+
return fixer.replaceText(node, `Array(${lengthText}).fill(${valueText})`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const preferArrayFromMap = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description: 'Prefer Array.from(iterable, mapper) over [...iterable].map(mapper) to avoid intermediate array allocation',
|
|
6
|
+
recommended: true
|
|
7
|
+
},
|
|
8
|
+
fixable: 'code',
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
preferArrayFrom: 'Use Array.from({{iterable}}, {{mapper}}) instead of [...{{iterable}}].map({{mapper}}) to avoid creating an intermediate array'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
const sourceCode = context.sourceCode;
|
|
16
|
+
return {
|
|
17
|
+
CallExpression(node) {
|
|
18
|
+
// Check if this is a .map() call
|
|
19
|
+
if (node.callee.type !== 'MemberExpression') {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (node.callee.property.type !== 'Identifier' ||
|
|
23
|
+
node.callee.property.name !== 'map') {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Check if .map() is being called on an array literal with spread
|
|
27
|
+
if (node.callee.object.type !== 'ArrayExpression') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const arrayExpr = node.callee.object;
|
|
31
|
+
// Check if the array has exactly one element and it's a spread element
|
|
32
|
+
if (arrayExpr.elements.length !== 1 ||
|
|
33
|
+
arrayExpr.elements[0]?.type !== 'SpreadElement') {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Check if map has exactly one argument (the mapper function)
|
|
37
|
+
if (node.arguments.length !== 1) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const spreadElement = arrayExpr.elements[0];
|
|
41
|
+
const iterableText = sourceCode.getText(spreadElement.argument);
|
|
42
|
+
const mapperText = sourceCode.getText(node.arguments[0]);
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
messageId: 'preferArrayFrom',
|
|
46
|
+
data: {
|
|
47
|
+
iterable: iterableText,
|
|
48
|
+
mapper: mapperText
|
|
49
|
+
},
|
|
50
|
+
fix(fixer) {
|
|
51
|
+
return fixer.replaceText(node, `Array.from(${iterableText}, ${mapperText})`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getArrayFromCopyPattern } from '../utils/ast.js';
|
|
2
|
+
export const preferArrayToReversed = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: 'suggestion',
|
|
5
|
+
docs: {
|
|
6
|
+
description: 'Prefer Array.prototype.toReversed() over copying and reversing arrays',
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
fixable: 'code',
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
preferToReversed: 'Use {{array}}.toReversed() instead of copying and reversing'
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
const sourceCode = context.sourceCode;
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (node.callee.type !== 'MemberExpression' ||
|
|
20
|
+
node.callee.property.type !== 'Identifier' ||
|
|
21
|
+
node.callee.property.name !== 'reverse') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const reverseCallee = node.callee.object;
|
|
25
|
+
const arrayNode = getArrayFromCopyPattern(reverseCallee);
|
|
26
|
+
if (arrayNode) {
|
|
27
|
+
const arrayText = sourceCode.getText(arrayNode);
|
|
28
|
+
context.report({
|
|
29
|
+
node,
|
|
30
|
+
messageId: 'preferToReversed',
|
|
31
|
+
data: {
|
|
32
|
+
array: arrayText
|
|
33
|
+
},
|
|
34
|
+
fix(fixer) {
|
|
35
|
+
return fixer.replaceText(node, `${arrayText}.toReversed()`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|