@e18e/eslint-plugin 0.1.2 → 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 +3 -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/utils/typescript.d.ts +1 -0
- package/lib/utils/typescript.js +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,7 @@ Copying these rules into your `rules` object will achieve the same effect as usi
|
|
|
81
81
|
- ✅ = Yes / Enabled
|
|
82
82
|
- ✖️ = No / Disabled
|
|
83
83
|
- 💡 = Has suggestions (requires user confirmation for fixes)
|
|
84
|
+
- 🔶 = Optionally uses types (works without TypeScript but more powerful with it)
|
|
84
85
|
|
|
85
86
|
### Modernization
|
|
86
87
|
|
|
@@ -111,6 +112,8 @@ Copying these rules into your `rules` object will achieve the same effect as usi
|
|
|
111
112
|
| [no-indexof-equality](./src/rules/no-indexof-equality.ts) | Prefer `startsWith()` for strings and direct array access over `indexOf()` equality checks | ✖️ | ✅ | ✅ |
|
|
112
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 | ✅ | ✅ | ✖️ |
|
|
113
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 | ✅ | ✅ | 🔶 |
|
|
114
117
|
|
|
115
118
|
## License
|
|
116
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
|
+
};
|
|
@@ -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
|
+
}
|