@e18e/eslint-plugin 0.1.2 → 0.1.4
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/README.md +56 -1
- package/lib/configs/performance-improvements.js +4 -1
- package/lib/main.js +6 -0
- package/lib/rules/prefer-array-at.d.ts +4 -2
- package/lib/rules/prefer-array-at.js +8 -2
- package/lib/rules/prefer-array-some.d.ts +2 -0
- package/lib/rules/prefer-array-some.js +118 -0
- package/lib/rules/prefer-date-now.d.ts +2 -0
- package/lib/rules/prefer-date-now.js +71 -0
- package/lib/rules/prefer-regex-test.d.ts +4 -0
- package/lib/rules/prefer-regex-test.js +147 -0
- package/lib/rules/prefer-spread-syntax.js +4 -1
- package/lib/rules/prefer-timer-args.js +24 -17
- package/lib/utils/ast.d.ts +15 -2
- package/lib/utils/ast.js +48 -0
- package/lib/utils/typescript.d.ts +11 -0
- package/lib/utils/typescript.js +65 -2
- package/package.json +11 -9
package/README.md
CHANGED
|
@@ -75,18 +75,70 @@ Copying these rules into your `rules` object will achieve the same effect as usi
|
|
|
75
75
|
> Our type-aware rules depend on TypeScript ESLint's parser, which means they
|
|
76
76
|
> will not work with oxlint at this time.
|
|
77
77
|
|
|
78
|
+
## Linting `package.json`
|
|
79
|
+
|
|
80
|
+
Some rules (e.g. `ban-dependencies`) can be used against your `package.json`.
|
|
81
|
+
|
|
82
|
+
You can achieve this by using `@eslint/json` or `jsonc-eslint-parser`.
|
|
83
|
+
|
|
84
|
+
For example, with `@eslint/json` and `eslint.config.js`:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import e18e from '@e18e/eslint-plugin';
|
|
88
|
+
import json from '@eslint/json';
|
|
89
|
+
import {defineConfig} from 'eslint/config';
|
|
90
|
+
|
|
91
|
+
export default defineConfig([
|
|
92
|
+
{
|
|
93
|
+
files: ['package.json'],
|
|
94
|
+
language: 'json/json',
|
|
95
|
+
plugins: {
|
|
96
|
+
e18e,
|
|
97
|
+
json
|
|
98
|
+
},
|
|
99
|
+
extends: ['e18e/recommended'],
|
|
100
|
+
}
|
|
101
|
+
]);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Or with `jsonc-eslint-parser` and `eslint.config.js`:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import e18e from '@e18e/eslint-plugin';
|
|
108
|
+
import jsonParser from 'jsonc-eslint-parser';
|
|
109
|
+
import {defineConfig} from 'eslint/config';
|
|
110
|
+
|
|
111
|
+
export default defineConfig([
|
|
112
|
+
{
|
|
113
|
+
files: ['package.json'],
|
|
114
|
+
languageOptions: {
|
|
115
|
+
parser: jsonParser
|
|
116
|
+
},
|
|
117
|
+
plugins: {
|
|
118
|
+
e18e
|
|
119
|
+
},
|
|
120
|
+
extends: ['e18e/recommended'],
|
|
121
|
+
}
|
|
122
|
+
]);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Read more at the
|
|
126
|
+
[`@eslint/json` docs](https://github.com/eslint/json) and
|
|
127
|
+
[`jsonc-eslint-parser` docs](https://github.com/ota-meshi/jsonc-eslint-parser).
|
|
128
|
+
|
|
78
129
|
## Rules
|
|
79
130
|
|
|
80
131
|
**Legend:**
|
|
81
132
|
- ✅ = Yes / Enabled
|
|
82
133
|
- ✖️ = No / Disabled
|
|
83
134
|
- 💡 = Has suggestions (requires user confirmation for fixes)
|
|
135
|
+
- 🔶 = Optionally uses types (works without TypeScript but more powerful with it)
|
|
84
136
|
|
|
85
137
|
### Modernization
|
|
86
138
|
|
|
87
139
|
| Rule | Description | Recommended | Fixable | Requires Types |
|
|
88
140
|
|------|-------------|-------------|---------|----------------|
|
|
89
|
-
| [prefer-array-at](./src/rules/prefer-array-at.ts) | Prefer `Array.prototype.at()` over length-based indexing | ✅ | ✅ |
|
|
141
|
+
| [prefer-array-at](./src/rules/prefer-array-at.ts) | Prefer `Array.prototype.at()` over length-based indexing | ✅ | ✅ | 🔶 |
|
|
90
142
|
| [prefer-array-fill](./src/rules/prefer-array-fill.ts) | Prefer `Array.prototype.fill()` over `Array.from()` or `map()` with constant values | ✅ | ✅ | ✖️ |
|
|
91
143
|
| [prefer-includes](./src/rules/prefer-includes.ts) | Prefer `.includes()` over `indexOf()` comparisons for arrays and strings | ✅ | ✅ | ✖️ |
|
|
92
144
|
| [prefer-array-to-reversed](./src/rules/prefer-array-to-reversed.ts) | Prefer `Array.prototype.toReversed()` over copying and reversing arrays | ✅ | ✅ | ✖️ |
|
|
@@ -110,7 +162,10 @@ Copying these rules into your `rules` object will achieve the same effect as usi
|
|
|
110
162
|
|------|-------------|-------------|---------|----------------|
|
|
111
163
|
| [no-indexof-equality](./src/rules/no-indexof-equality.ts) | Prefer `startsWith()` for strings and direct array access over `indexOf()` equality checks | ✖️ | ✅ | ✅ |
|
|
112
164
|
| [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 | ✅ | ✅ | ✖️ |
|
|
165
|
+
| [prefer-array-some](./src/rules/prefer-array-some.ts) | Prefer `Array.some()` over `Array.find()` when checking for element existence | ✅ | ✅ | ✖️ |
|
|
113
166
|
| [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` | ✅ | ✅ | ✖️ |
|
|
167
|
+
| [prefer-date-now](./src/rules/prefer-date-now.ts) | Prefer `Date.now()` over `new Date().getTime()` and `+new Date()` | ✅ | ✅ | ✖️ |
|
|
168
|
+
| [prefer-regex-test](./src/rules/prefer-regex-test.ts) | Prefer `RegExp.test()` over `String.match()` and `RegExp.exec()` when only checking for match existence | ✅ | ✅ | 🔶 |
|
|
114
169
|
|
|
115
170
|
## License
|
|
116
171
|
|
|
@@ -4,6 +4,9 @@ export const performanceImprovements = (plugin) => ({
|
|
|
4
4
|
},
|
|
5
5
|
rules: {
|
|
6
6
|
'e18e/prefer-array-from-map': 'error',
|
|
7
|
-
'e18e/prefer-timer-args': 'error'
|
|
7
|
+
'e18e/prefer-timer-args': 'error',
|
|
8
|
+
'e18e/prefer-date-now': 'error',
|
|
9
|
+
'e18e/prefer-regex-test': 'error',
|
|
10
|
+
'e18e/prefer-array-some': 'error'
|
|
8
11
|
}
|
|
9
12
|
});
|
package/lib/main.js
CHANGED
|
@@ -16,6 +16,9 @@ import { preferSpreadSyntax } from './rules/prefer-spread-syntax.js';
|
|
|
16
16
|
import { preferUrlCanParse } from './rules/prefer-url-canparse.js';
|
|
17
17
|
import { noIndexOfEquality } from './rules/no-indexof-equality.js';
|
|
18
18
|
import { preferTimerArgs } from './rules/prefer-timer-args.js';
|
|
19
|
+
import { preferDateNow } from './rules/prefer-date-now.js';
|
|
20
|
+
import { preferRegexTest } from './rules/prefer-regex-test.js';
|
|
21
|
+
import { preferArraySome } from './rules/prefer-array-some.js';
|
|
19
22
|
import { rules as dependRules } from 'eslint-plugin-depend';
|
|
20
23
|
const plugin = {
|
|
21
24
|
meta: {
|
|
@@ -38,6 +41,9 @@ const plugin = {
|
|
|
38
41
|
'prefer-url-canparse': preferUrlCanParse,
|
|
39
42
|
'no-indexof-equality': noIndexOfEquality,
|
|
40
43
|
'prefer-timer-args': preferTimerArgs,
|
|
44
|
+
'prefer-date-now': preferDateNow,
|
|
45
|
+
'prefer-regex-test': preferRegexTest,
|
|
46
|
+
'prefer-array-some': preferArraySome,
|
|
41
47
|
...dependRules
|
|
42
48
|
}
|
|
43
49
|
};
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type MessageIds = 'preferAt';
|
|
3
|
+
export declare const preferArrayAt: TSESLint.RuleModule<MessageIds, []>;
|
|
4
|
+
export {};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { isArrayType, isStringType } from '../utils/typescript.js';
|
|
1
2
|
export const preferArrayAt = {
|
|
2
3
|
meta: {
|
|
3
4
|
type: 'suggestion',
|
|
4
5
|
docs: {
|
|
5
|
-
description: 'Prefer Array.prototype.at() over length-based indexing'
|
|
6
|
-
recommended: true
|
|
6
|
+
description: 'Prefer Array.prototype.at() over length-based indexing'
|
|
7
7
|
},
|
|
8
8
|
fixable: 'code',
|
|
9
9
|
schema: [],
|
|
@@ -11,6 +11,7 @@ export const preferArrayAt = {
|
|
|
11
11
|
preferAt: 'Use .at(-1) instead of [{{array}}.length - 1]'
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
defaultOptions: [],
|
|
14
15
|
create(context) {
|
|
15
16
|
const sourceCode = context.sourceCode;
|
|
16
17
|
return {
|
|
@@ -48,6 +49,11 @@ export const preferArrayAt = {
|
|
|
48
49
|
parent.left === node) {
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
52
|
+
// Check if the object supports .at() (array or string, when types are available)
|
|
53
|
+
if (!isArrayType(node.object, context) &&
|
|
54
|
+
!isStringType(node.object, context)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
51
57
|
context.report({
|
|
52
58
|
node,
|
|
53
59
|
messageId: 'preferAt',
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { isInBooleanContext, isNullish } from '../utils/ast.js';
|
|
2
|
+
function isFindCall(node) {
|
|
3
|
+
return (node.type === 'CallExpression' &&
|
|
4
|
+
node.callee.type === 'MemberExpression' &&
|
|
5
|
+
node.callee.property.type === 'Identifier' &&
|
|
6
|
+
node.callee.property.name === 'find' &&
|
|
7
|
+
node.arguments.length >= 1);
|
|
8
|
+
}
|
|
9
|
+
function reportFind(context, node, findCall, shouldNegate) {
|
|
10
|
+
if (findCall.callee.type !== 'MemberExpression') {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const sourceCode = context.sourceCode;
|
|
14
|
+
const arrayText = sourceCode.getText(findCall.callee.object);
|
|
15
|
+
const argsText = findCall.arguments
|
|
16
|
+
.map((arg) => sourceCode.getText(arg))
|
|
17
|
+
.join(', ');
|
|
18
|
+
const replacement = shouldNegate
|
|
19
|
+
? `!${arrayText}.some(${argsText})`
|
|
20
|
+
: `${arrayText}.some(${argsText})`;
|
|
21
|
+
context.report({
|
|
22
|
+
node,
|
|
23
|
+
messageId: 'preferArraySome',
|
|
24
|
+
fix(fixer) {
|
|
25
|
+
return fixer.replaceText(node, replacement);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function checkBinaryExpression(node, context) {
|
|
30
|
+
const { left, right, operator } = node;
|
|
31
|
+
if (left.type === 'PrivateIdentifier') {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let findCall;
|
|
35
|
+
let constantSide;
|
|
36
|
+
if (isFindCall(left)) {
|
|
37
|
+
findCall = left;
|
|
38
|
+
constantSide = right;
|
|
39
|
+
}
|
|
40
|
+
else if (isFindCall(right)) {
|
|
41
|
+
findCall = right;
|
|
42
|
+
constantSide = left;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const nullishType = isNullish(constantSide);
|
|
48
|
+
if (!nullishType) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (operator === '===' || operator === '!==') {
|
|
52
|
+
if (nullishType !== 'undefined') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const shouldNegate = operator === '===';
|
|
56
|
+
reportFind(context, node, findCall, shouldNegate);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function checkUnaryExpression(node, context) {
|
|
61
|
+
// !arr.find(fn) -> !arr.some(fn)
|
|
62
|
+
if (node.operator === '!' && isFindCall(node.argument)) {
|
|
63
|
+
reportFind(context, node, node.argument, true);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// !!arr.find(fn) -> arr.some(fn)
|
|
67
|
+
if (node.operator === '!' &&
|
|
68
|
+
node.argument.type === 'UnaryExpression' &&
|
|
69
|
+
node.argument.operator === '!' &&
|
|
70
|
+
isFindCall(node.argument.argument)) {
|
|
71
|
+
reportFind(context, node, node.argument.argument, false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export const preferArraySome = {
|
|
76
|
+
meta: {
|
|
77
|
+
type: 'suggestion',
|
|
78
|
+
docs: {
|
|
79
|
+
description: 'Prefer Array.some() over Array.find() when checking for element existence',
|
|
80
|
+
recommended: true
|
|
81
|
+
},
|
|
82
|
+
fixable: 'code',
|
|
83
|
+
schema: [],
|
|
84
|
+
messages: {
|
|
85
|
+
preferArraySome: 'Use Array.some() instead of Array.find() when checking for element existence'
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
create(context) {
|
|
89
|
+
return {
|
|
90
|
+
BinaryExpression(node) {
|
|
91
|
+
checkBinaryExpression(node, context);
|
|
92
|
+
},
|
|
93
|
+
UnaryExpression(node) {
|
|
94
|
+
// Skip inner ! if it's inside !! (the outer will handle it)
|
|
95
|
+
if (node.operator === '!' && node.parent) {
|
|
96
|
+
if (node.parent.type === 'UnaryExpression' &&
|
|
97
|
+
node.parent.operator === '!') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
checkUnaryExpression(node, context);
|
|
102
|
+
},
|
|
103
|
+
CallExpression(node) {
|
|
104
|
+
// Skip if handled by UnaryExpression or BinaryExpression
|
|
105
|
+
if (node.parent?.type === 'UnaryExpression' ||
|
|
106
|
+
node.parent?.type === 'BinaryExpression') {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!isFindCall(node)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (isInBooleanContext(node)) {
|
|
113
|
+
reportFind(context, node, node, false);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
function getDateNowReplacement(node) {
|
|
2
|
+
if (node.type !== 'NewExpression' || node.arguments.length !== 0) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'Date') {
|
|
6
|
+
return 'Date.now()';
|
|
7
|
+
}
|
|
8
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
9
|
+
node.callee.object.type === 'Identifier' &&
|
|
10
|
+
(node.callee.object.name === 'window' ||
|
|
11
|
+
node.callee.object.name === 'globalThis') &&
|
|
12
|
+
node.callee.property.type === 'Identifier' &&
|
|
13
|
+
node.callee.property.name === 'Date' &&
|
|
14
|
+
!node.callee.computed) {
|
|
15
|
+
return `${node.callee.object.name}.Date.now()`;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export const preferDateNow = {
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'suggestion',
|
|
22
|
+
docs: {
|
|
23
|
+
description: 'Prefer Date.now() over new Date().getTime() and +new Date()',
|
|
24
|
+
recommended: true
|
|
25
|
+
},
|
|
26
|
+
fixable: 'code',
|
|
27
|
+
schema: [],
|
|
28
|
+
messages: {
|
|
29
|
+
preferDateNow: 'Use Date.now() to avoid allocating a new Date object.'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
return {
|
|
34
|
+
// new Date().getTime()
|
|
35
|
+
CallExpression(node) {
|
|
36
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
37
|
+
node.callee.object.type === 'NewExpression' &&
|
|
38
|
+
node.callee.property.type === 'Identifier' &&
|
|
39
|
+
node.callee.property.name === 'getTime' &&
|
|
40
|
+
!node.callee.computed &&
|
|
41
|
+
node.arguments.length === 0) {
|
|
42
|
+
const replacement = getDateNowReplacement(node.callee.object);
|
|
43
|
+
if (replacement) {
|
|
44
|
+
context.report({
|
|
45
|
+
node,
|
|
46
|
+
messageId: 'preferDateNow',
|
|
47
|
+
fix(fixer) {
|
|
48
|
+
return fixer.replaceText(node, replacement);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
// +new Date()
|
|
55
|
+
UnaryExpression(node) {
|
|
56
|
+
if (node.operator === '+' && node.argument.type === 'NewExpression') {
|
|
57
|
+
const replacement = getDateNowReplacement(node.argument);
|
|
58
|
+
if (replacement) {
|
|
59
|
+
context.report({
|
|
60
|
+
node,
|
|
61
|
+
messageId: 'preferDateNow',
|
|
62
|
+
fix(fixer) {
|
|
63
|
+
return fixer.replaceText(node, replacement);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { tryGetTypedParserServices } from '../utils/typescript.js';
|
|
2
|
+
import { isInBooleanContext } from '../utils/ast.js';
|
|
3
|
+
function isRegExpLiteral(node) {
|
|
4
|
+
return (node.type === 'Literal' &&
|
|
5
|
+
'regex' in node &&
|
|
6
|
+
node.regex !== undefined &&
|
|
7
|
+
node.regex !== null);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a node is a `new RegExp(...)`
|
|
11
|
+
*/
|
|
12
|
+
function isRegExpConstructor(node) {
|
|
13
|
+
if (node.type !== 'NewExpression') {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const { callee } = node;
|
|
17
|
+
// new RegExp()
|
|
18
|
+
if (callee.type === 'Identifier' && callee.name === 'RegExp') {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
// new window.RegExp() or new globalThis.RegExp()
|
|
22
|
+
if (callee.type === 'MemberExpression' &&
|
|
23
|
+
callee.object.type === 'Identifier' &&
|
|
24
|
+
(callee.object.name === 'window' || callee.object.name === 'globalThis') &&
|
|
25
|
+
callee.property.type === 'Identifier' &&
|
|
26
|
+
callee.property.name === 'RegExp' &&
|
|
27
|
+
!callee.computed) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Checks if a node is a RegExp (literal or constructor)
|
|
34
|
+
*/
|
|
35
|
+
function isRegExp(node) {
|
|
36
|
+
return (node !== null &&
|
|
37
|
+
node !== undefined &&
|
|
38
|
+
(isRegExpLiteral(node) || isRegExpConstructor(node)));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Checks if a node resolves to a RegExp using TypeScript types (when available)
|
|
42
|
+
*/
|
|
43
|
+
function isRegExpByType(node, context) {
|
|
44
|
+
const services = tryGetTypedParserServices(context);
|
|
45
|
+
if (!services) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const type = services.getTypeAtLocation(node);
|
|
49
|
+
if (!type) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const checker = services.program.getTypeChecker();
|
|
53
|
+
const typeString = checker.typeToString(type);
|
|
54
|
+
return typeString === 'RegExp';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checks if a node resolves to a RegExp (literal, constructor, or by type)
|
|
58
|
+
*/
|
|
59
|
+
function resolvesToRegExp(node, context) {
|
|
60
|
+
if (isRegExpByType(node, context)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (isRegExp(node)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (node.type !== 'Identifier') {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const scope = context.sourceCode.getScope(node);
|
|
70
|
+
const variable = scope.references.find((ref) => ref.identifier === node)?.resolved;
|
|
71
|
+
if (!variable) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
for (const def of variable.defs) {
|
|
75
|
+
if (def.type === 'Variable' && def.node.type === 'VariableDeclarator') {
|
|
76
|
+
const init = def.node.init;
|
|
77
|
+
if (isRegExp(init)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
export const preferRegexTest = {
|
|
85
|
+
meta: {
|
|
86
|
+
type: 'suggestion',
|
|
87
|
+
docs: {
|
|
88
|
+
description: 'prefer `RegExp.test()` over `String.match()` and `RegExp.exec()` when only checking for match existence'
|
|
89
|
+
},
|
|
90
|
+
fixable: 'code',
|
|
91
|
+
messages: {
|
|
92
|
+
preferTest: 'Prefer `{{regex}}.test({{string}})` over `{{original}}` for boolean checks'
|
|
93
|
+
},
|
|
94
|
+
schema: []
|
|
95
|
+
},
|
|
96
|
+
defaultOptions: [],
|
|
97
|
+
create(context) {
|
|
98
|
+
return {
|
|
99
|
+
CallExpression(node) {
|
|
100
|
+
if (!isInBooleanContext(node)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { callee } = node;
|
|
104
|
+
if (callee.type !== 'MemberExpression') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const property = callee.property;
|
|
108
|
+
if (property.type !== 'Identifier' || node.arguments.length !== 1) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
let regexNode;
|
|
112
|
+
let stringNode;
|
|
113
|
+
if (property.name === 'match') {
|
|
114
|
+
// str.match(regex)
|
|
115
|
+
stringNode = callee.object;
|
|
116
|
+
regexNode = node.arguments[0];
|
|
117
|
+
}
|
|
118
|
+
else if (property.name === 'exec') {
|
|
119
|
+
// regex.exec(str)
|
|
120
|
+
regexNode = callee.object;
|
|
121
|
+
stringNode = node.arguments[0];
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!resolvesToRegExp(regexNode, context)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const sourceCode = context.sourceCode;
|
|
130
|
+
const regexText = sourceCode.getText(regexNode);
|
|
131
|
+
const stringText = sourceCode.getText(stringNode);
|
|
132
|
+
context.report({
|
|
133
|
+
node,
|
|
134
|
+
messageId: 'preferTest',
|
|
135
|
+
data: {
|
|
136
|
+
regex: regexText,
|
|
137
|
+
string: stringText,
|
|
138
|
+
original: sourceCode.getText(node)
|
|
139
|
+
},
|
|
140
|
+
fix(fixer) {
|
|
141
|
+
return fixer.replaceText(node, `${regexText}.test(${stringText})`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
@@ -30,9 +30,12 @@ export const preferSpreadSyntax = {
|
|
|
30
30
|
let messageId;
|
|
31
31
|
let replacement;
|
|
32
32
|
// array.concat()
|
|
33
|
+
// excluding Buffer.concat()
|
|
33
34
|
if (node.callee.property.type === 'Identifier' &&
|
|
34
35
|
node.callee.property.name === 'concat' &&
|
|
35
|
-
node.arguments.length > 0
|
|
36
|
+
node.arguments.length > 0 &&
|
|
37
|
+
!(node.callee.object.type === 'Identifier' &&
|
|
38
|
+
node.callee.object.name === 'Buffer')) {
|
|
36
39
|
const arrayText = sourceCode.getText(node.callee.object);
|
|
37
40
|
const argTexts = node.arguments.map((arg) => sourceCode.getText(arg));
|
|
38
41
|
const spreadParts = [arrayText, ...argTexts]
|
|
@@ -4,6 +4,22 @@ function isNullOrUndefined(node) {
|
|
|
4
4
|
}
|
|
5
5
|
return node.type === 'Identifier' && node.name === 'undefined';
|
|
6
6
|
}
|
|
7
|
+
function isTimerCall(node) {
|
|
8
|
+
if (node.callee.type === 'Identifier' &&
|
|
9
|
+
(node.callee.name === 'setTimeout' || node.callee.name === 'setInterval')) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
13
|
+
node.callee.object.type === 'Identifier' &&
|
|
14
|
+
(node.callee.object.name === 'window' ||
|
|
15
|
+
node.callee.object.name === 'globalThis') &&
|
|
16
|
+
node.callee.property.type === 'Identifier' &&
|
|
17
|
+
(node.callee.property.name === 'setTimeout' ||
|
|
18
|
+
node.callee.property.name === 'setInterval')) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
7
23
|
function isSafeArgument(arg) {
|
|
8
24
|
if (arg.type === 'SpreadElement') {
|
|
9
25
|
return arg.argument.type === 'Identifier';
|
|
@@ -75,18 +91,7 @@ export const preferTimerArgs = {
|
|
|
75
91
|
const sourceCode = context.sourceCode;
|
|
76
92
|
return {
|
|
77
93
|
CallExpression(node) {
|
|
78
|
-
|
|
79
|
-
const isTimerFunction = (node.callee.type === 'Identifier' &&
|
|
80
|
-
(node.callee.name === 'setTimeout' ||
|
|
81
|
-
node.callee.name === 'setInterval')) ||
|
|
82
|
-
(node.callee.type === 'MemberExpression' &&
|
|
83
|
-
node.callee.object.type === 'Identifier' &&
|
|
84
|
-
(node.callee.object.name === 'window' ||
|
|
85
|
-
node.callee.object.name === 'globalThis') &&
|
|
86
|
-
node.callee.property.type === 'Identifier' &&
|
|
87
|
-
(node.callee.property.name === 'setTimeout' ||
|
|
88
|
-
node.callee.property.name === 'setInterval'));
|
|
89
|
-
if (!isTimerFunction) {
|
|
94
|
+
if (!isTimerCall(node)) {
|
|
90
95
|
return;
|
|
91
96
|
}
|
|
92
97
|
if (node.arguments.length < 2) {
|
|
@@ -101,21 +106,23 @@ export const preferTimerArgs = {
|
|
|
101
106
|
let replacement = null;
|
|
102
107
|
// simple arrow functions, e.g. () => fn(args)
|
|
103
108
|
if (firstArg.type === 'ArrowFunctionExpression') {
|
|
104
|
-
const arrowFn = firstArg;
|
|
105
109
|
// skip if it is a block body
|
|
106
|
-
if (
|
|
110
|
+
if (firstArg.body.type === 'BlockStatement') {
|
|
107
111
|
return;
|
|
108
112
|
}
|
|
109
113
|
// skip if it has parameters
|
|
110
|
-
if (
|
|
114
|
+
if (firstArg.params.length > 0) {
|
|
111
115
|
return;
|
|
112
116
|
}
|
|
113
|
-
if (
|
|
117
|
+
if (firstArg.body.type !== 'CallExpression') {
|
|
114
118
|
return;
|
|
115
119
|
}
|
|
116
|
-
const callExpression =
|
|
120
|
+
const callExpression = firstArg.body;
|
|
117
121
|
const callee = callExpression.callee;
|
|
118
122
|
const callArgs = callExpression.arguments;
|
|
123
|
+
if (callee.type === 'MemberExpression') {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
119
126
|
if (!callArgs.every(isSafeArgument)) {
|
|
120
127
|
return;
|
|
121
128
|
}
|
package/lib/utils/ast.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import type { CallExpression, Node } from 'estree';
|
|
2
|
-
import type {
|
|
1
|
+
import type { CallExpression, Node, Expression } from 'estree';
|
|
2
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
3
|
+
import type { Rule, SourceCode } from 'eslint';
|
|
4
|
+
type AnyNode = (Node & Rule.NodeParentExtension) | TSESTree.Node;
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a node is in a boolean context (where the result is only used as truthy/falsy).
|
|
7
|
+
* e.g. if conditions, while loops, ternary tests, logical operators
|
|
8
|
+
*/
|
|
9
|
+
export declare function isInBooleanContext(node: AnyNode): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Checks if a node is undefined, null, or void 0.
|
|
12
|
+
* Returns the type of nullish value or false if not nullish.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isNullish(node: Expression): 'undefined' | 'null' | false;
|
|
3
15
|
/**
|
|
4
16
|
* Checks if a CallExpression is a copy operation that creates a shallow copy of an array.
|
|
5
17
|
* e.g. concat(), slice(), slice(0)
|
|
@@ -13,3 +25,4 @@ export declare function getArrayFromCopyPattern(node: Node): Node | null;
|
|
|
13
25
|
* Formats arguments from a CallExpression as a comma-separated string.
|
|
14
26
|
*/
|
|
15
27
|
export declare function formatArguments(args: CallExpression['arguments'], sourceCode: SourceCode): string;
|
|
28
|
+
export {};
|
package/lib/utils/ast.js
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a node is in a boolean context (where the result is only used as truthy/falsy).
|
|
3
|
+
* e.g. if conditions, while loops, ternary tests, logical operators
|
|
4
|
+
*/
|
|
5
|
+
export function isInBooleanContext(node) {
|
|
6
|
+
const parent = node.parent;
|
|
7
|
+
if (!parent) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
// if/while/for/do-while test
|
|
11
|
+
if ((parent.type === 'IfStatement' && parent.test === node) ||
|
|
12
|
+
(parent.type === 'WhileStatement' && parent.test === node) ||
|
|
13
|
+
(parent.type === 'ForStatement' && parent.test === node) ||
|
|
14
|
+
(parent.type === 'DoWhileStatement' && parent.test === node)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
// ternaries
|
|
18
|
+
if (parent.type === 'ConditionalExpression' && parent.test === node) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
// check the parent recursively for unary ! and logical operators
|
|
22
|
+
if ((parent.type === 'UnaryExpression' && parent.operator === '!') ||
|
|
23
|
+
(parent.type === 'LogicalExpression' &&
|
|
24
|
+
(parent.operator === '&&' || parent.operator === '||'))) {
|
|
25
|
+
return isInBooleanContext(parent);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a node is undefined, null, or void 0.
|
|
31
|
+
* Returns the type of nullish value or false if not nullish.
|
|
32
|
+
*/
|
|
33
|
+
export function isNullish(node) {
|
|
34
|
+
if (node.type === 'Identifier' && node.name === 'undefined') {
|
|
35
|
+
return 'undefined';
|
|
36
|
+
}
|
|
37
|
+
if (node.type === 'Literal' && node.value === null) {
|
|
38
|
+
return 'null';
|
|
39
|
+
}
|
|
40
|
+
// void 0
|
|
41
|
+
if (node.type === 'UnaryExpression' &&
|
|
42
|
+
node.operator === 'void' &&
|
|
43
|
+
node.argument.type === 'Literal' &&
|
|
44
|
+
node.argument.value === 0) {
|
|
45
|
+
return 'undefined';
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
1
49
|
/**
|
|
2
50
|
* Checks if a CallExpression is a copy operation that creates a shallow copy of an array.
|
|
3
51
|
* e.g. concat(), slice(), slice(0)
|
|
@@ -11,4 +11,15 @@ export interface ParserServices {
|
|
|
11
11
|
getTypeAtLocation: (node: TSESTree.Node) => ts.Type;
|
|
12
12
|
program: ts.Program;
|
|
13
13
|
}
|
|
14
|
+
export declare function tryGetTypedParserServices(context: Readonly<TSESLint.RuleContext<string, unknown[]>>): ParserServicesWithTypeInformation | null;
|
|
14
15
|
export declare function getTypedParserServices(context: Readonly<TSESLint.RuleContext<string, unknown[]>>): ParserServicesWithTypeInformation;
|
|
16
|
+
/**
|
|
17
|
+
* Checks if a node's type is an Array type (Array, tuple, or typed array)
|
|
18
|
+
* Returns true if types are unavailable (to avoid false negatives)
|
|
19
|
+
*/
|
|
20
|
+
export declare function isArrayType(node: TSESTree.Node, context: Readonly<TSESLint.RuleContext<string, unknown[]>>): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a node's type is a string
|
|
23
|
+
* Returns false if types are unavailable
|
|
24
|
+
*/
|
|
25
|
+
export declare function isStringType(node: TSESTree.Node, context: Readonly<TSESLint.RuleContext<string, unknown[]>>): boolean;
|
package/lib/utils/typescript.js
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function tryGetTypedParserServices(context) {
|
|
2
2
|
if (context.sourceCode.parserServices?.program == null) {
|
|
3
|
-
|
|
3
|
+
return null;
|
|
4
4
|
}
|
|
5
5
|
return context.sourceCode.parserServices;
|
|
6
6
|
}
|
|
7
|
+
export function getTypedParserServices(context) {
|
|
8
|
+
const services = tryGetTypedParserServices(context);
|
|
9
|
+
if (services === null) {
|
|
10
|
+
throw new Error(`You have used a rule which requires type information. Please ensure you have typescript-eslint setup alongside this plugin and configured to enable type-aware linting. See https://typescript-eslint.io/getting-started/typed-linting for more information.`);
|
|
11
|
+
}
|
|
12
|
+
return services;
|
|
13
|
+
}
|
|
14
|
+
const typedArrayTypes = [
|
|
15
|
+
'Int8Array',
|
|
16
|
+
'Uint8Array',
|
|
17
|
+
'Uint8ClampedArray',
|
|
18
|
+
'Int16Array',
|
|
19
|
+
'Uint16Array',
|
|
20
|
+
'Int32Array',
|
|
21
|
+
'Uint32Array',
|
|
22
|
+
'Float32Array',
|
|
23
|
+
'Float64Array',
|
|
24
|
+
'BigInt64Array',
|
|
25
|
+
'BigUint64Array'
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Checks if a node's type is an Array type (Array, tuple, or typed array)
|
|
29
|
+
* Returns true if types are unavailable (to avoid false negatives)
|
|
30
|
+
*/
|
|
31
|
+
export function isArrayType(node, context) {
|
|
32
|
+
const services = tryGetTypedParserServices(context);
|
|
33
|
+
if (!services) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const type = services.getTypeAtLocation(node);
|
|
37
|
+
if (!type) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const checker = services.program.getTypeChecker();
|
|
41
|
+
if (checker.isArrayType(type)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (checker.isTupleType(type)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const typeString = checker.typeToString(type);
|
|
48
|
+
if (typedArrayTypes.some((t) => typeString.startsWith(t))) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Checks if a node's type is a string
|
|
55
|
+
* Returns false if types are unavailable
|
|
56
|
+
*/
|
|
57
|
+
export function isStringType(node, context) {
|
|
58
|
+
const services = tryGetTypedParserServices(context);
|
|
59
|
+
if (!services) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const type = services.getTypeAtLocation(node);
|
|
63
|
+
if (!type) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const checker = services.program.getTypeChecker();
|
|
67
|
+
const stringType = checker.getStringType();
|
|
68
|
+
return checker.isTypeAssignableTo(type, stringType);
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e18e/eslint-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "The official e18e ESLint plugin for modernizing code and improving performance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -36,16 +36,18 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@eslint/js": "^9.39.2",
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"@typescript-eslint/
|
|
42
|
-
"@
|
|
39
|
+
"@eslint/json": "^0.14.0",
|
|
40
|
+
"@types/node": "^25.0.10",
|
|
41
|
+
"@typescript-eslint/rule-tester": "^8.53.1",
|
|
42
|
+
"@typescript-eslint/typescript-estree": "^8.53.1",
|
|
43
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
43
44
|
"eslint": "^9.39.2",
|
|
44
|
-
"eslint-plugin-eslint-plugin": "^7.
|
|
45
|
-
"
|
|
46
|
-
"
|
|
45
|
+
"eslint-plugin-eslint-plugin": "^7.3.0",
|
|
46
|
+
"jsonc-eslint-parser": "^2.4.2",
|
|
47
|
+
"oxlint": "^1.41.0",
|
|
48
|
+
"prettier": "^3.8.1",
|
|
47
49
|
"typescript": "^5.9.3",
|
|
48
|
-
"typescript-eslint": "^8.
|
|
50
|
+
"typescript-eslint": "^8.53.1",
|
|
49
51
|
"vitest": "^4.0.14"
|
|
50
52
|
},
|
|
51
53
|
"peerDependencies": {
|