@e18e/eslint-plugin 0.1.1 → 0.1.3
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 +15 -0
- package/lib/configs/performance-improvements.js +3 -1
- package/lib/main.js +4 -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 +173 -0
- package/lib/rules/prefer-spread-syntax.js +7 -3
- package/lib/utils/typescript.d.ts +1 -0
- package/lib/utils/typescript.js +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,12 +63,25 @@ If you're using oxlint, you can enable the e18e plugin by adding it to your `.ox
|
|
|
63
63
|
}
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
You can enable the recommended configuration by copying the rules from each of the ESLint configuration files into your `.oxlintrc.json` file.
|
|
67
|
+
|
|
68
|
+
- [modernization configuration](./src/configs/modernization.ts)
|
|
69
|
+
- [module replacements configuration](./src/configs/module-replacements.ts)
|
|
70
|
+
- [performance improvements configuration](./src/configs/performance-improvements.ts)
|
|
71
|
+
|
|
72
|
+
Copying these rules into your `rules` object will achieve the same effect as using the recommended configuration in ESLint.
|
|
73
|
+
|
|
74
|
+
> [!NOTE]
|
|
75
|
+
> Our type-aware rules depend on TypeScript ESLint's parser, which means they
|
|
76
|
+
> will not work with oxlint at this time.
|
|
77
|
+
|
|
66
78
|
## Rules
|
|
67
79
|
|
|
68
80
|
**Legend:**
|
|
69
81
|
- ✅ = Yes / Enabled
|
|
70
82
|
- ✖️ = No / Disabled
|
|
71
83
|
- 💡 = Has suggestions (requires user confirmation for fixes)
|
|
84
|
+
- 🔶 = Optionally uses types (works without TypeScript but more powerful with it)
|
|
72
85
|
|
|
73
86
|
### Modernization
|
|
74
87
|
|
|
@@ -99,6 +112,8 @@ If you're using oxlint, you can enable the e18e plugin by adding it to your `.ox
|
|
|
99
112
|
| [no-indexof-equality](./src/rules/no-indexof-equality.ts) | Prefer `startsWith()` for strings and direct array access over `indexOf()` equality checks | ✖️ | ✅ | ✅ |
|
|
100
113
|
| [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 | ✅ | ✅ | ✖️ |
|
|
101
114
|
| [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` | ✅ | ✅ | ✖️ |
|
|
115
|
+
| [prefer-date-now](./src/rules/prefer-date-now.ts) | Prefer `Date.now()` over `new Date().getTime()` and `+new Date()` | ✅ | ✅ | ✖️ |
|
|
116
|
+
| [prefer-regex-test](./src/rules/prefer-regex-test.ts) | Prefer `RegExp.test()` over `String.match()` and `RegExp.exec()` when only checking for match existence | ✅ | ✅ | 🔶 |
|
|
102
117
|
|
|
103
118
|
## License
|
|
104
119
|
|
|
@@ -4,6 +4,8 @@ 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'
|
|
8
10
|
}
|
|
9
11
|
});
|
package/lib/main.js
CHANGED
|
@@ -16,6 +16,8 @@ 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';
|
|
19
21
|
import { rules as dependRules } from 'eslint-plugin-depend';
|
|
20
22
|
const plugin = {
|
|
21
23
|
meta: {
|
|
@@ -38,6 +40,8 @@ const plugin = {
|
|
|
38
40
|
'prefer-url-canparse': preferUrlCanParse,
|
|
39
41
|
'no-indexof-equality': noIndexOfEquality,
|
|
40
42
|
'prefer-timer-args': preferTimerArgs,
|
|
43
|
+
'prefer-date-now': preferDateNow,
|
|
44
|
+
'prefer-regex-test': preferRegexTest,
|
|
41
45
|
...dependRules
|
|
42
46
|
}
|
|
43
47
|
};
|
|
@@ -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,173 @@
|
|
|
1
|
+
import { tryGetTypedParserServices } from '../utils/typescript.js';
|
|
2
|
+
function isRegExpLiteral(node) {
|
|
3
|
+
return (node.type === 'Literal' &&
|
|
4
|
+
'regex' in node &&
|
|
5
|
+
node.regex !== undefined &&
|
|
6
|
+
node.regex !== null);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Checks if a node is a `new RegExp(...)`
|
|
10
|
+
*/
|
|
11
|
+
function isRegExpConstructor(node) {
|
|
12
|
+
if (node.type !== 'NewExpression') {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const { callee } = node;
|
|
16
|
+
// new RegExp()
|
|
17
|
+
if (callee.type === 'Identifier' && callee.name === 'RegExp') {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
// new window.RegExp() or new globalThis.RegExp()
|
|
21
|
+
if (callee.type === 'MemberExpression' &&
|
|
22
|
+
callee.object.type === 'Identifier' &&
|
|
23
|
+
(callee.object.name === 'window' || callee.object.name === 'globalThis') &&
|
|
24
|
+
callee.property.type === 'Identifier' &&
|
|
25
|
+
callee.property.name === 'RegExp' &&
|
|
26
|
+
!callee.computed) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Checks if a node is a RegExp (literal or constructor)
|
|
33
|
+
*/
|
|
34
|
+
function isRegExp(node) {
|
|
35
|
+
return (node !== null &&
|
|
36
|
+
node !== undefined &&
|
|
37
|
+
(isRegExpLiteral(node) || isRegExpConstructor(node)));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Checks if a node resolves to a RegExp using TypeScript types (when available)
|
|
41
|
+
*/
|
|
42
|
+
function isRegExpByType(node, context) {
|
|
43
|
+
const services = tryGetTypedParserServices(context);
|
|
44
|
+
if (!services) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const type = services.getTypeAtLocation(node);
|
|
48
|
+
if (!type) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const checker = services.program.getTypeChecker();
|
|
52
|
+
const typeString = checker.typeToString(type);
|
|
53
|
+
return typeString === 'RegExp';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Checks if a node resolves to a RegExp (literal, constructor, or by type)
|
|
57
|
+
*/
|
|
58
|
+
function resolvesToRegExp(node, context) {
|
|
59
|
+
if (isRegExpByType(node, context)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (isRegExp(node)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (node.type !== 'Identifier') {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const scope = context.sourceCode.getScope(node);
|
|
69
|
+
const variable = scope.references.find((ref) => ref.identifier === node)?.resolved;
|
|
70
|
+
if (!variable) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
for (const def of variable.defs) {
|
|
74
|
+
if (def.type === 'Variable' && def.node.type === 'VariableDeclarator') {
|
|
75
|
+
const init = def.node.init;
|
|
76
|
+
if (isRegExp(init)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Checks if a node is in a test/condition
|
|
85
|
+
*/
|
|
86
|
+
function isInBooleanContext(node) {
|
|
87
|
+
const parent = node.parent;
|
|
88
|
+
if (!parent) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
// if/while/for/do-while test
|
|
92
|
+
if ((parent.type === 'IfStatement' && parent.test === node) ||
|
|
93
|
+
(parent.type === 'WhileStatement' && parent.test === node) ||
|
|
94
|
+
(parent.type === 'ForStatement' && parent.test === node) ||
|
|
95
|
+
(parent.type === 'DoWhileStatement' && parent.test === node)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
// ternaries
|
|
99
|
+
if (parent.type === 'ConditionalExpression' && parent.test === node) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
// check the parent
|
|
103
|
+
if ((parent.type === 'UnaryExpression' && parent.operator === '!') ||
|
|
104
|
+
(parent.type === 'LogicalExpression' &&
|
|
105
|
+
(parent.operator === '&&' || parent.operator === '||'))) {
|
|
106
|
+
return isInBooleanContext(parent);
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
export const preferRegexTest = {
|
|
111
|
+
meta: {
|
|
112
|
+
type: 'suggestion',
|
|
113
|
+
docs: {
|
|
114
|
+
description: 'prefer `RegExp.test()` over `String.match()` and `RegExp.exec()` when only checking for match existence'
|
|
115
|
+
},
|
|
116
|
+
fixable: 'code',
|
|
117
|
+
messages: {
|
|
118
|
+
preferTest: 'Prefer `{{regex}}.test({{string}})` over `{{original}}` for boolean checks'
|
|
119
|
+
},
|
|
120
|
+
schema: []
|
|
121
|
+
},
|
|
122
|
+
defaultOptions: [],
|
|
123
|
+
create(context) {
|
|
124
|
+
return {
|
|
125
|
+
CallExpression(node) {
|
|
126
|
+
if (!isInBooleanContext(node)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const { callee } = node;
|
|
130
|
+
if (callee.type !== 'MemberExpression') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const property = callee.property;
|
|
134
|
+
if (property.type !== 'Identifier' || node.arguments.length !== 1) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let regexNode;
|
|
138
|
+
let stringNode;
|
|
139
|
+
if (property.name === 'match') {
|
|
140
|
+
// str.match(regex)
|
|
141
|
+
stringNode = callee.object;
|
|
142
|
+
regexNode = node.arguments[0];
|
|
143
|
+
}
|
|
144
|
+
else if (property.name === 'exec') {
|
|
145
|
+
// regex.exec(str)
|
|
146
|
+
regexNode = callee.object;
|
|
147
|
+
stringNode = node.arguments[0];
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!resolvesToRegExp(regexNode, context)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const sourceCode = context.sourceCode;
|
|
156
|
+
const regexText = sourceCode.getText(regexNode);
|
|
157
|
+
const stringText = sourceCode.getText(stringNode);
|
|
158
|
+
context.report({
|
|
159
|
+
node,
|
|
160
|
+
messageId: 'preferTest',
|
|
161
|
+
data: {
|
|
162
|
+
regex: regexText,
|
|
163
|
+
string: stringText,
|
|
164
|
+
original: sourceCode.getText(node)
|
|
165
|
+
},
|
|
166
|
+
fix(fixer) {
|
|
167
|
+
return fixer.replaceText(node, `${regexText}.test(${stringText})`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
};
|
|
@@ -47,9 +47,13 @@ export const preferSpreadSyntax = {
|
|
|
47
47
|
node.callee.property.type === 'Identifier' &&
|
|
48
48
|
node.callee.property.name === 'from' &&
|
|
49
49
|
node.arguments.length === 1) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const firstArg = node.arguments[0];
|
|
51
|
+
if (firstArg.type !== 'SpreadElement' &&
|
|
52
|
+
firstArg.type !== 'ObjectExpression') {
|
|
53
|
+
const iterableText = sourceCode.getText(firstArg);
|
|
54
|
+
replacement = `[...${iterableText}]`;
|
|
55
|
+
messageId = 'preferSpreadArrayFrom';
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
// Object.assign({...}, ...)
|
|
55
59
|
else if (node.callee.object.type === 'Identifier' &&
|
|
@@ -11,4 +11,5 @@ 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;
|
package/lib/utils/typescript.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
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
|
+
}
|