@e18e/eslint-plugin 0.0.1 → 0.1.0
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 +13 -3
- package/lib/main.js +1 -1
- package/lib/rules/no-indexof-equality.d.ts +4 -2
- package/lib/rules/no-indexof-equality.js +2 -2
- package/lib/utils/typescript.d.ts +3 -3
- package/lib/utils/typescript.js +1 -1
- package/package.json +2 -1
- package/lib/rules/prefer-optimized-indexof.d.ts +0 -2
- package/lib/rules/prefer-optimized-indexof.js +0 -90
- package/lib/rules/prefer-settimeout-args.d.ts +0 -2
- package/lib/rules/prefer-settimeout-args.js +0 -175
package/README.md
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
> The official e18e ESLint plugin for modernizing JavaScript/TypeScript code and improving performance.
|
|
4
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
5
|
This plugin focuses on applying the e18e community's best practices and advise to JavaScript/TypeScript codebases.
|
|
9
6
|
|
|
10
7
|
## Overview
|
|
@@ -53,6 +50,19 @@ export default [
|
|
|
53
50
|
];
|
|
54
51
|
```
|
|
55
52
|
|
|
53
|
+
## Usage with oxlint
|
|
54
|
+
|
|
55
|
+
If you're using oxlint, you can enable the e18e plugin by adding it to your `.oxlintrc.json` file:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"jsPlugins": ["@e18e/eslint-plugin"],
|
|
60
|
+
"rules": {
|
|
61
|
+
"e18e/prefer-includes": "error"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
56
66
|
## Rules
|
|
57
67
|
|
|
58
68
|
**Legend:**
|
package/lib/main.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type MessageIds = 'preferDirectAccess' | 'preferStartsWith';
|
|
3
|
+
export declare const noIndexOfEquality: TSESLint.RuleModule<MessageIds, []>;
|
|
4
|
+
export {};
|
|
@@ -3,8 +3,7 @@ export const noIndexOfEquality = {
|
|
|
3
3
|
meta: {
|
|
4
4
|
type: 'suggestion',
|
|
5
5
|
docs: {
|
|
6
|
-
description: 'Prefer optimized alternatives to `indexOf()` equality checks'
|
|
7
|
-
recommended: false
|
|
6
|
+
description: 'Prefer optimized alternatives to `indexOf()` equality checks'
|
|
8
7
|
},
|
|
9
8
|
fixable: 'code',
|
|
10
9
|
schema: [],
|
|
@@ -13,6 +12,7 @@ export const noIndexOfEquality = {
|
|
|
13
12
|
preferStartsWith: 'Use `.startsWith()` instead of `indexOf() === 0` for strings'
|
|
14
13
|
}
|
|
15
14
|
},
|
|
15
|
+
defaultOptions: [],
|
|
16
16
|
create(context) {
|
|
17
17
|
const sourceCode = context.sourceCode;
|
|
18
18
|
const services = getTypedParserServices(context);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { TSNode, TSToken, TSESTree, ParserServicesWithTypeInformation } from '@typescript-eslint/typescript-estree';
|
|
2
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
3
3
|
import type ts from 'typescript';
|
|
4
4
|
export interface ParserServices {
|
|
5
5
|
emitDecoratorMetadata: boolean | undefined;
|
|
@@ -11,4 +11,4 @@ export interface ParserServices {
|
|
|
11
11
|
getTypeAtLocation: (node: TSESTree.Node) => ts.Type;
|
|
12
12
|
program: ts.Program;
|
|
13
13
|
}
|
|
14
|
-
export declare function getTypedParserServices(context: Readonly<
|
|
14
|
+
export declare function getTypedParserServices(context: Readonly<TSESLint.RuleContext<string, unknown[]>>): ParserServicesWithTypeInformation;
|
package/lib/utils/typescript.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function getTypedParserServices(context) {
|
|
2
|
-
if (context.sourceCode.parserServices
|
|
2
|
+
if (context.sourceCode.parserServices?.program == null) {
|
|
3
3
|
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.`);
|
|
4
4
|
}
|
|
5
5
|
return context.sourceCode.parserServices;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e18e/eslint-plugin",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "The official e18e ESLint plugin for modernizing code and improving performance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@vitest/coverage-v8": "^4.0.16",
|
|
43
43
|
"eslint": "^9.39.2",
|
|
44
44
|
"eslint-plugin-eslint-plugin": "^7.2.0",
|
|
45
|
+
"oxlint": "^1.34.0",
|
|
45
46
|
"prettier": "^3.7.4",
|
|
46
47
|
"typescript": "^5.9.3",
|
|
47
48
|
"typescript-eslint": "^8.50.0",
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { getTypedParserServices } from '../utils/typescript.js';
|
|
2
|
-
export const preferOptimizedIndexof = {
|
|
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
|
-
};
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
function isNullOrUndefined(node) {
|
|
2
|
-
if (node.type === 'Literal' && node.value === null) {
|
|
3
|
-
return true;
|
|
4
|
-
}
|
|
5
|
-
return node.type === 'Identifier' && node.name === 'undefined';
|
|
6
|
-
}
|
|
7
|
-
function isSafeArgument(arg) {
|
|
8
|
-
if (arg.type === 'SpreadElement') {
|
|
9
|
-
return arg.argument.type === 'Identifier';
|
|
10
|
-
}
|
|
11
|
-
switch (arg.type) {
|
|
12
|
-
case 'Identifier':
|
|
13
|
-
case 'Literal':
|
|
14
|
-
case 'TemplateLiteral':
|
|
15
|
-
return true;
|
|
16
|
-
case 'MemberExpression':
|
|
17
|
-
if (arg.object.type === 'Super' || arg.property.type === 'PrivateIdentifier') {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
if (!isSafeArgument(arg.object)) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
if (arg.computed) {
|
|
24
|
-
return isSafeArgument(arg.property);
|
|
25
|
-
}
|
|
26
|
-
return true;
|
|
27
|
-
case 'ArrayExpression':
|
|
28
|
-
return arg.elements.every((el) => el === null || isSafeArgument(el));
|
|
29
|
-
case 'ObjectExpression':
|
|
30
|
-
return arg.properties.every((prop) => {
|
|
31
|
-
if (prop.type === 'SpreadElement') {
|
|
32
|
-
return isSafeArgument(prop.argument);
|
|
33
|
-
}
|
|
34
|
-
const valueType = prop.value.type;
|
|
35
|
-
if (valueType === 'ObjectPattern' ||
|
|
36
|
-
valueType === 'ArrayPattern' ||
|
|
37
|
-
valueType === 'RestElement' ||
|
|
38
|
-
valueType === 'AssignmentPattern') {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
return isSafeArgument(prop.value);
|
|
42
|
-
});
|
|
43
|
-
case 'UnaryExpression':
|
|
44
|
-
case 'UpdateExpression':
|
|
45
|
-
return isSafeArgument(arg.argument);
|
|
46
|
-
case 'BinaryExpression':
|
|
47
|
-
case 'LogicalExpression':
|
|
48
|
-
return arg.left.type !== 'PrivateIdentifier' && isSafeArgument(arg.left) && isSafeArgument(arg.right);
|
|
49
|
-
case 'ConditionalExpression':
|
|
50
|
-
return (isSafeArgument(arg.test) &&
|
|
51
|
-
isSafeArgument(arg.consequent) &&
|
|
52
|
-
isSafeArgument(arg.alternate));
|
|
53
|
-
// CallExpression, NewExpression, etc. are NOT safe
|
|
54
|
-
default:
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export const preferSetTimeoutArgs = {
|
|
59
|
-
meta: {
|
|
60
|
-
type: 'suggestion',
|
|
61
|
-
docs: {
|
|
62
|
-
description: 'Prefer passing function and arguments directly to setTimeout instead of wrapping in an arrow function or using bind',
|
|
63
|
-
recommended: true
|
|
64
|
-
},
|
|
65
|
-
fixable: 'code',
|
|
66
|
-
schema: [],
|
|
67
|
-
messages: {
|
|
68
|
-
preferArgs: 'Pass function and arguments directly to setTimeout to avoid allocating an extra function'
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
create(context) {
|
|
72
|
-
const sourceCode = context.sourceCode;
|
|
73
|
-
return {
|
|
74
|
-
CallExpression(node) {
|
|
75
|
-
// Check if this is setTimeout, window.setTimeout, or globalThis.setTimeout
|
|
76
|
-
const isSetTimeout = (node.callee.type === 'Identifier' &&
|
|
77
|
-
node.callee.name === 'setTimeout') ||
|
|
78
|
-
(node.callee.type === 'MemberExpression' &&
|
|
79
|
-
node.callee.object.type === 'Identifier' &&
|
|
80
|
-
(node.callee.object.name === 'window' ||
|
|
81
|
-
node.callee.object.name === 'globalThis') &&
|
|
82
|
-
node.callee.property.type === 'Identifier' &&
|
|
83
|
-
node.callee.property.name === 'setTimeout');
|
|
84
|
-
if (!isSetTimeout) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (node.arguments.length < 2) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const firstArg = node.arguments[0];
|
|
91
|
-
if (!firstArg || firstArg.type === 'SpreadElement') {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const delayText = sourceCode.getText(node.arguments[1]);
|
|
95
|
-
// Preserve the original setTimeout call style
|
|
96
|
-
const setTimeoutCall = sourceCode.getText(node.callee);
|
|
97
|
-
let replacement = null;
|
|
98
|
-
// simple arrow functions, e.g. () => fn(args)
|
|
99
|
-
if (firstArg.type === 'ArrowFunctionExpression') {
|
|
100
|
-
const arrowFn = firstArg;
|
|
101
|
-
// skip if it is a block body
|
|
102
|
-
if (arrowFn.body.type === 'BlockStatement') {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// skip if it has parameters
|
|
106
|
-
if (arrowFn.params.length > 0) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (arrowFn.body.type !== 'CallExpression') {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const callExpression = arrowFn.body;
|
|
113
|
-
const callee = callExpression.callee;
|
|
114
|
-
const callArgs = callExpression.arguments;
|
|
115
|
-
// Check if any argument contains a call expression or other unsafe construct
|
|
116
|
-
// If so, transforming would change when those expressions are evaluated
|
|
117
|
-
if (!callArgs.every(isSafeArgument)) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
// Build the replacement
|
|
121
|
-
const calleeText = sourceCode.getText(callee);
|
|
122
|
-
if (callArgs.length === 0) {
|
|
123
|
-
replacement = `${setTimeoutCall}(${calleeText}, ${delayText})`;
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
const argsTexts = callArgs.map((arg) => sourceCode.getText(arg));
|
|
127
|
-
replacement = `${setTimeoutCall}(${calleeText}, ${delayText}, ${argsTexts.join(', ')})`;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// fn.bind(null/undefined, args)
|
|
131
|
-
else if (firstArg.type === 'CallExpression') {
|
|
132
|
-
const bindCall = firstArg;
|
|
133
|
-
if (bindCall.callee.type !== 'MemberExpression' ||
|
|
134
|
-
bindCall.callee.property.type !== 'Identifier' ||
|
|
135
|
-
bindCall.callee.property.name !== 'bind' ||
|
|
136
|
-
bindCall.arguments.length === 0) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const bindContext = bindCall.arguments[0];
|
|
140
|
-
if (!bindContext || bindContext.type === 'SpreadElement') {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (!isNullOrUndefined(bindContext)) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const fnText = sourceCode.getText(bindCall.callee.object);
|
|
147
|
-
const bindArgs = bindCall.arguments.slice(1);
|
|
148
|
-
// Check if any bind argument contains a call expression or other unsafe construct
|
|
149
|
-
if (!bindArgs.every(isSafeArgument)) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (bindArgs.length === 0) {
|
|
153
|
-
replacement = `${setTimeoutCall}(${fnText}, ${delayText})`;
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
const argsTexts = bindArgs.map((arg) => sourceCode.getText(arg));
|
|
157
|
-
replacement = `${setTimeoutCall}(${fnText}, ${delayText}, ${argsTexts.join(', ')})`;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (replacement) {
|
|
164
|
-
context.report({
|
|
165
|
-
node,
|
|
166
|
-
messageId: 'preferArgs',
|
|
167
|
-
fix(fixer) {
|
|
168
|
-
return fixer.replaceText(node, replacement);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
};
|