@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
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
export const preferSpreadSyntax = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Prefer spread syntax over Array.concat(), Array.from(), Object.assign({}, ...), and Function.apply()',
|
|
12
|
+
recommended: true
|
|
13
|
+
},
|
|
14
|
+
fixable: 'code',
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
preferSpreadArray: 'Use spread syntax [...arr, ...other] instead of arr.concat(other)',
|
|
18
|
+
preferSpreadArrayFrom: 'Use spread syntax [...iterable] instead of Array.from(iterable) when no mapper function is provided',
|
|
19
|
+
preferSpreadObject: 'Use spread syntax {...a, ...b} instead of Object.assign({}, a, b)',
|
|
20
|
+
preferSpreadFunction: 'Use spread syntax fn(...args) instead of fn.apply(null/undefined, args)'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
const sourceCode = context.sourceCode;
|
|
25
|
+
return {
|
|
26
|
+
CallExpression(node) {
|
|
27
|
+
if (node.callee.type !== 'MemberExpression') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let messageId;
|
|
31
|
+
let replacement;
|
|
32
|
+
// array.concat()
|
|
33
|
+
if (node.callee.property.type === 'Identifier' &&
|
|
34
|
+
node.callee.property.name === 'concat' &&
|
|
35
|
+
node.arguments.length > 0) {
|
|
36
|
+
const arrayText = sourceCode.getText(node.callee.object);
|
|
37
|
+
const argTexts = node.arguments.map((arg) => sourceCode.getText(arg));
|
|
38
|
+
const spreadParts = [arrayText, ...argTexts]
|
|
39
|
+
.map((part) => `...${part}`)
|
|
40
|
+
.join(', ');
|
|
41
|
+
replacement = `[${spreadParts}]`;
|
|
42
|
+
messageId = 'preferSpreadArray';
|
|
43
|
+
}
|
|
44
|
+
// Array.from(iterable) with no mapper
|
|
45
|
+
else if (node.callee.object.type === 'Identifier' &&
|
|
46
|
+
node.callee.object.name === 'Array' &&
|
|
47
|
+
node.callee.property.type === 'Identifier' &&
|
|
48
|
+
node.callee.property.name === 'from' &&
|
|
49
|
+
node.arguments.length === 1) {
|
|
50
|
+
const iterableText = sourceCode.getText(node.arguments[0]);
|
|
51
|
+
replacement = `[...${iterableText}]`;
|
|
52
|
+
messageId = 'preferSpreadArrayFrom';
|
|
53
|
+
}
|
|
54
|
+
// Object.assign({...}, ...)
|
|
55
|
+
else if (node.callee.object.type === 'Identifier' &&
|
|
56
|
+
node.callee.object.name === 'Object' &&
|
|
57
|
+
node.callee.property.type === 'Identifier' &&
|
|
58
|
+
node.callee.property.name === 'assign' &&
|
|
59
|
+
node.arguments.length >= 2) {
|
|
60
|
+
const firstArg = node.arguments[0];
|
|
61
|
+
if (firstArg.type !== 'SpreadElement' &&
|
|
62
|
+
firstArg.type === 'ObjectExpression') {
|
|
63
|
+
const hasUnquotedProto = firstArg.properties.some((prop) => prop.type === 'Property' &&
|
|
64
|
+
!prop.computed &&
|
|
65
|
+
prop.key.type === 'Identifier' &&
|
|
66
|
+
prop.key.name === '__proto__');
|
|
67
|
+
if (!hasUnquotedProto) {
|
|
68
|
+
const spreadArgs = node.arguments
|
|
69
|
+
.slice(1)
|
|
70
|
+
.map((arg) => `...${sourceCode.getText(arg)}`)
|
|
71
|
+
.join(', ');
|
|
72
|
+
if (firstArg.properties.length === 0) {
|
|
73
|
+
replacement = `{${spreadArgs}}`;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const literalText = sourceCode.getText(firstArg);
|
|
77
|
+
const innerContent = literalText.slice(1, -1); // Remove { and }
|
|
78
|
+
replacement = `{${innerContent}, ${spreadArgs}}`;
|
|
79
|
+
}
|
|
80
|
+
messageId = 'preferSpreadObject';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// function.apply(null/undefined, args)
|
|
85
|
+
else if (node.callee.property.type === 'Identifier' &&
|
|
86
|
+
node.callee.property.name === 'apply' &&
|
|
87
|
+
node.arguments.length === 2) {
|
|
88
|
+
const firstArg = node.arguments[0];
|
|
89
|
+
if (firstArg.type !== 'SpreadElement' &&
|
|
90
|
+
isNullOrUndefined(firstArg)) {
|
|
91
|
+
const fnText = sourceCode.getText(node.callee.object);
|
|
92
|
+
const argsText = sourceCode.getText(node.arguments[1]);
|
|
93
|
+
replacement = `${fnText}(...${argsText})`;
|
|
94
|
+
messageId = 'preferSpreadFunction';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (messageId && replacement) {
|
|
98
|
+
context.report({
|
|
99
|
+
node,
|
|
100
|
+
messageId,
|
|
101
|
+
fix(fixer) {
|
|
102
|
+
return fixer.replaceText(node, replacement);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
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' ||
|
|
18
|
+
arg.property.type === 'PrivateIdentifier') {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (!isSafeArgument(arg.object)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (arg.computed) {
|
|
25
|
+
return isSafeArgument(arg.property);
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
case 'ArrayExpression':
|
|
29
|
+
return arg.elements.every((el) => el === null || isSafeArgument(el));
|
|
30
|
+
case 'ObjectExpression':
|
|
31
|
+
return arg.properties.every((prop) => {
|
|
32
|
+
if (prop.type === 'SpreadElement') {
|
|
33
|
+
return isSafeArgument(prop.argument);
|
|
34
|
+
}
|
|
35
|
+
const valueType = prop.value.type;
|
|
36
|
+
if (valueType === 'ObjectPattern' ||
|
|
37
|
+
valueType === 'ArrayPattern' ||
|
|
38
|
+
valueType === 'RestElement' ||
|
|
39
|
+
valueType === 'AssignmentPattern') {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return isSafeArgument(prop.value);
|
|
43
|
+
});
|
|
44
|
+
case 'UnaryExpression':
|
|
45
|
+
case 'UpdateExpression':
|
|
46
|
+
return isSafeArgument(arg.argument);
|
|
47
|
+
case 'BinaryExpression':
|
|
48
|
+
case 'LogicalExpression':
|
|
49
|
+
return (arg.left.type !== 'PrivateIdentifier' &&
|
|
50
|
+
isSafeArgument(arg.left) &&
|
|
51
|
+
isSafeArgument(arg.right));
|
|
52
|
+
case 'ConditionalExpression':
|
|
53
|
+
return (isSafeArgument(arg.test) &&
|
|
54
|
+
isSafeArgument(arg.consequent) &&
|
|
55
|
+
isSafeArgument(arg.alternate));
|
|
56
|
+
// CallExpression, NewExpression, etc.
|
|
57
|
+
default:
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export const preferTimerArgs = {
|
|
62
|
+
meta: {
|
|
63
|
+
type: 'suggestion',
|
|
64
|
+
docs: {
|
|
65
|
+
description: 'Prefer passing function and arguments directly to setTimeout/setInterval instead of wrapping in an arrow function or using bind',
|
|
66
|
+
recommended: true
|
|
67
|
+
},
|
|
68
|
+
fixable: 'code',
|
|
69
|
+
schema: [],
|
|
70
|
+
messages: {
|
|
71
|
+
preferArgs: 'Pass function and arguments directly to timer function to avoid allocating an extra function'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
create(context) {
|
|
75
|
+
const sourceCode = context.sourceCode;
|
|
76
|
+
return {
|
|
77
|
+
CallExpression(node) {
|
|
78
|
+
// Check if this is setTimeout/setInterval (with optional window/globalThis prefix)
|
|
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) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (node.arguments.length < 2) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const firstArg = node.arguments[0];
|
|
96
|
+
if (!firstArg || firstArg.type === 'SpreadElement') {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const delayText = sourceCode.getText(node.arguments[1]);
|
|
100
|
+
const timerCall = sourceCode.getText(node.callee);
|
|
101
|
+
let replacement = null;
|
|
102
|
+
// simple arrow functions, e.g. () => fn(args)
|
|
103
|
+
if (firstArg.type === 'ArrowFunctionExpression') {
|
|
104
|
+
const arrowFn = firstArg;
|
|
105
|
+
// skip if it is a block body
|
|
106
|
+
if (arrowFn.body.type === 'BlockStatement') {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// skip if it has parameters
|
|
110
|
+
if (arrowFn.params.length > 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (arrowFn.body.type !== 'CallExpression') {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const callExpression = arrowFn.body;
|
|
117
|
+
const callee = callExpression.callee;
|
|
118
|
+
const callArgs = callExpression.arguments;
|
|
119
|
+
if (!callArgs.every(isSafeArgument)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const calleeText = sourceCode.getText(callee);
|
|
123
|
+
if (callArgs.length === 0) {
|
|
124
|
+
replacement = `${timerCall}(${calleeText}, ${delayText})`;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const argsTexts = callArgs.map((arg) => sourceCode.getText(arg));
|
|
128
|
+
replacement = `${timerCall}(${calleeText}, ${delayText}, ${argsTexts.join(', ')})`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// fn.bind(null/undefined, args)
|
|
132
|
+
else if (firstArg.type === 'CallExpression') {
|
|
133
|
+
const bindCall = firstArg;
|
|
134
|
+
if (bindCall.callee.type !== 'MemberExpression' ||
|
|
135
|
+
bindCall.callee.property.type !== 'Identifier' ||
|
|
136
|
+
bindCall.callee.property.name !== 'bind' ||
|
|
137
|
+
bindCall.arguments.length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const bindContext = bindCall.arguments[0];
|
|
141
|
+
if (!bindContext || bindContext.type === 'SpreadElement') {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!isNullOrUndefined(bindContext)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const fnText = sourceCode.getText(bindCall.callee.object);
|
|
148
|
+
const bindArgs = bindCall.arguments.slice(1);
|
|
149
|
+
// Check if any bind argument contains a call expression or other unsafe construct
|
|
150
|
+
if (!bindArgs.every(isSafeArgument)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (bindArgs.length === 0) {
|
|
154
|
+
replacement = `${timerCall}(${fnText}, ${delayText})`;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const argsTexts = bindArgs.map((arg) => sourceCode.getText(arg));
|
|
158
|
+
replacement = `${timerCall}(${fnText}, ${delayText}, ${argsTexts.join(', ')})`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (replacement) {
|
|
165
|
+
context.report({
|
|
166
|
+
node,
|
|
167
|
+
messageId: 'preferArgs',
|
|
168
|
+
fix(fixer) {
|
|
169
|
+
return fixer.replaceText(node, replacement);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { formatArguments } from '../utils/ast.js';
|
|
2
|
+
/**
|
|
3
|
+
* Check if a statement is `new URL(...)`
|
|
4
|
+
*/
|
|
5
|
+
function isNewURLStatement(stmt) {
|
|
6
|
+
return (stmt.type === 'ExpressionStatement' &&
|
|
7
|
+
stmt.expression.type === 'NewExpression' &&
|
|
8
|
+
stmt.expression.callee.type === 'Identifier' &&
|
|
9
|
+
stmt.expression.callee.name === 'URL' &&
|
|
10
|
+
stmt.expression.arguments.length >= 1);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Check if a statement is `return (true|false)`
|
|
14
|
+
*/
|
|
15
|
+
function isReturnBoolean(stmt, value) {
|
|
16
|
+
return (stmt.type === 'ReturnStatement' &&
|
|
17
|
+
stmt.argument?.type === 'Literal' &&
|
|
18
|
+
stmt.argument.value === value);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if block has only a return statement with a boolean literal
|
|
22
|
+
*/
|
|
23
|
+
function hasOnlyReturnBoolean(block, value) {
|
|
24
|
+
if (block.body.length !== 1) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const firstStmt = block.body[0];
|
|
28
|
+
if (!firstStmt) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return isReturnBoolean(firstStmt, value);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check if block is empty or contains only empty statements
|
|
35
|
+
*/
|
|
36
|
+
function isEmptyBlock(block, sourceCode) {
|
|
37
|
+
return (block.body.length === 0 ||
|
|
38
|
+
block.body.every((stmt) => stmt.type === 'EmptyStatement' || !sourceCode.getText(stmt).trim()));
|
|
39
|
+
}
|
|
40
|
+
export const preferUrlCanParse = {
|
|
41
|
+
meta: {
|
|
42
|
+
type: 'suggestion',
|
|
43
|
+
docs: {
|
|
44
|
+
description: 'Prefer URL.canParse() over try-catch blocks for URL validation',
|
|
45
|
+
recommended: true
|
|
46
|
+
},
|
|
47
|
+
hasSuggestions: true,
|
|
48
|
+
schema: [],
|
|
49
|
+
messages: {
|
|
50
|
+
preferCanParse: 'Use URL.canParse() instead of try-catch for URL validation',
|
|
51
|
+
replaceWithCanParse: 'Replace with URL.canParse()'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
create(context) {
|
|
55
|
+
const sourceCode = context.sourceCode;
|
|
56
|
+
return {
|
|
57
|
+
TryStatement(node) {
|
|
58
|
+
const tryBlock = node.block;
|
|
59
|
+
const catchClause = node.handler;
|
|
60
|
+
if (!catchClause) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const tryStatements = tryBlock.body;
|
|
64
|
+
if (tryStatements.length === 0) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const firstStmt = tryStatements[0];
|
|
68
|
+
if (!firstStmt || !isNewURLStatement(firstStmt)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const urlArgText = formatArguments(firstStmt.expression.arguments, sourceCode);
|
|
72
|
+
// try { new URL(u); return true; } catch { return false; }
|
|
73
|
+
const secondStmt = tryStatements[1];
|
|
74
|
+
if (tryStatements.length === 2 &&
|
|
75
|
+
secondStmt &&
|
|
76
|
+
isReturnBoolean(secondStmt, true) &&
|
|
77
|
+
hasOnlyReturnBoolean(catchClause.body, false)) {
|
|
78
|
+
context.report({
|
|
79
|
+
node,
|
|
80
|
+
messageId: 'preferCanParse',
|
|
81
|
+
suggest: [
|
|
82
|
+
{
|
|
83
|
+
messageId: 'replaceWithCanParse',
|
|
84
|
+
fix(fixer) {
|
|
85
|
+
return fixer.replaceText(node, `return URL.canParse(${urlArgText})`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// try { new URL(u); ...body } catch { ...catchBody }
|
|
93
|
+
// Basically if there's a body after the URL construction
|
|
94
|
+
if (tryStatements.length >= 2) {
|
|
95
|
+
const bodyAfterURL = tryStatements.slice(1);
|
|
96
|
+
const firstBodyStmt = bodyAfterURL[0];
|
|
97
|
+
const lastBodyStmt = bodyAfterURL.at(-1);
|
|
98
|
+
const bodyText = firstBodyStmt &&
|
|
99
|
+
lastBodyStmt &&
|
|
100
|
+
firstBodyStmt.range &&
|
|
101
|
+
lastBodyStmt.range
|
|
102
|
+
? sourceCode.text.slice(firstBodyStmt.range[0], lastBodyStmt.range[1])
|
|
103
|
+
: '';
|
|
104
|
+
const catchBody = catchClause.body;
|
|
105
|
+
const catchBodyEmpty = isEmptyBlock(catchBody, sourceCode);
|
|
106
|
+
let replacement;
|
|
107
|
+
if (catchBodyEmpty) {
|
|
108
|
+
// No catch body, just if without else
|
|
109
|
+
replacement = `if (URL.canParse(${urlArgText})) {\n${bodyText}\n}`;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const catchStatements = catchBody.body;
|
|
113
|
+
const firstCatchStmt = catchStatements[0];
|
|
114
|
+
const lastCatchStmt = catchStatements.at(-1);
|
|
115
|
+
const catchBodyText = firstCatchStmt &&
|
|
116
|
+
lastCatchStmt &&
|
|
117
|
+
firstCatchStmt.range &&
|
|
118
|
+
lastCatchStmt.range
|
|
119
|
+
? sourceCode.text.slice(firstCatchStmt.range[0], lastCatchStmt.range[1])
|
|
120
|
+
: '';
|
|
121
|
+
replacement = `if (URL.canParse(${urlArgText})) {\n${bodyText}\n} else {\n${catchBodyText}\n}`;
|
|
122
|
+
}
|
|
123
|
+
context.report({
|
|
124
|
+
node,
|
|
125
|
+
messageId: 'preferCanParse',
|
|
126
|
+
suggest: [
|
|
127
|
+
{
|
|
128
|
+
messageId: 'replaceWithCanParse',
|
|
129
|
+
fix(fixer) {
|
|
130
|
+
return fixer.replaceText(node, replacement);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import { describe, it, afterAll } from 'vitest';
|
|
3
|
+
import { RuleTester as TSRuleTester } from '@typescript-eslint/rule-tester';
|
|
4
|
+
RuleTester.describe = describe;
|
|
5
|
+
RuleTester.it = it;
|
|
6
|
+
RuleTester.itOnly = it.only;
|
|
7
|
+
TSRuleTester.afterAll = afterAll;
|
|
8
|
+
TSRuleTester.describe = describe;
|
|
9
|
+
TSRuleTester.it = it;
|
|
10
|
+
TSRuleTester.itOnly = it.only;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CallExpression, Node } from 'estree';
|
|
2
|
+
import type { SourceCode } from 'eslint';
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a CallExpression is a copy operation that creates a shallow copy of an array.
|
|
5
|
+
* e.g. concat(), slice(), slice(0)
|
|
6
|
+
*/
|
|
7
|
+
export declare function isCopyCall(node: CallExpression): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Extracts the array node from array copy patterns.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getArrayFromCopyPattern(node: Node): Node | null;
|
|
12
|
+
/**
|
|
13
|
+
* Formats arguments from a CallExpression as a comma-separated string.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatArguments(args: CallExpression['arguments'], sourceCode: SourceCode): string;
|