oxclippy 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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/oxclippy.js +2428 -0
- package/package.json +53 -0
- package/src/index.ts +1 -0
- package/src/plugin.ts +134 -0
- package/src/rules/almost-swapped.ts +52 -0
- package/src/rules/bool-comparison.ts +38 -0
- package/src/rules/bool-to-int-with-if.ts +30 -0
- package/src/rules/cognitive-complexity.ts +137 -0
- package/src/rules/collapsible-if.ts +38 -0
- package/src/rules/enum-variant-names.ts +45 -0
- package/src/rules/excessive-nesting.ts +84 -0
- package/src/rules/explicit-counter-loop.ts +66 -0
- package/src/rules/filter-then-first.ts +26 -0
- package/src/rules/float-comparison.ts +52 -0
- package/src/rules/float-equality-without-abs.ts +43 -0
- package/src/rules/fn-params-excessive-bools.ts +49 -0
- package/src/rules/identity-op.ts +46 -0
- package/src/rules/if-same-then-else.ts +34 -0
- package/src/rules/int-plus-one.ts +74 -0
- package/src/rules/let-and-return.ts +43 -0
- package/src/rules/manual-clamp.ts +63 -0
- package/src/rules/manual-every.ts +47 -0
- package/src/rules/manual-find.ts +59 -0
- package/src/rules/manual-includes.ts +65 -0
- package/src/rules/manual-is-finite.ts +57 -0
- package/src/rules/manual-some.ts +46 -0
- package/src/rules/manual-strip.ts +96 -0
- package/src/rules/manual-swap.ts +73 -0
- package/src/rules/map-identity.ts +67 -0
- package/src/rules/map-void-return.ts +23 -0
- package/src/rules/match-same-arms.ts +46 -0
- package/src/rules/needless-bool.ts +67 -0
- package/src/rules/needless-continue.ts +47 -0
- package/src/rules/needless-late-init.ts +44 -0
- package/src/rules/needless-range-loop.ts +127 -0
- package/src/rules/neg-multiply.ts +36 -0
- package/src/rules/never-loop.ts +49 -0
- package/src/rules/object-keys-values.ts +67 -0
- package/src/rules/prefer-structured-clone.ts +30 -0
- package/src/rules/promise-new-resolve.ts +59 -0
- package/src/rules/redundant-closure-call.ts +61 -0
- package/src/rules/redundant-closure.ts +81 -0
- package/src/rules/search-is-some.ts +45 -0
- package/src/rules/similar-names.ts +155 -0
- package/src/rules/single-case-switch.ts +27 -0
- package/src/rules/single-element-loop.ts +21 -0
- package/src/rules/struct-field-names.ts +108 -0
- package/src/rules/too-many-arguments.ts +37 -0
- package/src/rules/too-many-lines.ts +45 -0
- package/src/rules/unnecessary-fold.ts +65 -0
- package/src/rules/unnecessary-reduce-collect.ts +109 -0
- package/src/rules/unreadable-literal.ts +59 -0
- package/src/rules/used-underscore-binding.ts +41 -0
- package/src/rules/useless-conversion.ts +115 -0
- package/src/rules/xor-used-as-pow.ts +34 -0
- package/src/rules/zero-divided-by-zero.ts +24 -0
- package/src/types.ts +75 -0
package/dist/oxclippy.js
ADDED
|
@@ -0,0 +1,2428 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
function isLiteral(node, value) {
|
|
3
|
+
if (!node || node.type !== "Literal")
|
|
4
|
+
return false;
|
|
5
|
+
return value === undefined ? true : node.value === value;
|
|
6
|
+
}
|
|
7
|
+
function isBoolLiteral(node) {
|
|
8
|
+
return isLiteral(node) && typeof node.value === "boolean";
|
|
9
|
+
}
|
|
10
|
+
function isIdentifier(node, name) {
|
|
11
|
+
if (!node || node.type !== "Identifier")
|
|
12
|
+
return false;
|
|
13
|
+
return name === undefined ? true : node.name === name;
|
|
14
|
+
}
|
|
15
|
+
function isCallOf(node, object, method) {
|
|
16
|
+
if (node.type !== "CallExpression")
|
|
17
|
+
return false;
|
|
18
|
+
const callee = node.callee;
|
|
19
|
+
if (callee.type !== "MemberExpression")
|
|
20
|
+
return false;
|
|
21
|
+
return isIdentifier(callee.object, object) && isIdentifier(callee.property, method);
|
|
22
|
+
}
|
|
23
|
+
function isMethodCall(node, method) {
|
|
24
|
+
if (node.type !== "CallExpression")
|
|
25
|
+
return false;
|
|
26
|
+
const callee = node.callee;
|
|
27
|
+
return callee.type === "MemberExpression" && !callee.computed && isIdentifier(callee.property, method);
|
|
28
|
+
}
|
|
29
|
+
function unwrapBlock(node) {
|
|
30
|
+
if (!node)
|
|
31
|
+
return null;
|
|
32
|
+
if (node.type === "BlockStatement") {
|
|
33
|
+
return node.body.length === 1 ? node.body[0] : null;
|
|
34
|
+
}
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
37
|
+
function getFunctionBody(node) {
|
|
38
|
+
const body = node.body;
|
|
39
|
+
if (!body)
|
|
40
|
+
return null;
|
|
41
|
+
if (body.type === "BlockStatement")
|
|
42
|
+
return body.body;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function getForOfVar(node) {
|
|
46
|
+
const left = node.left;
|
|
47
|
+
if (left.type === "VariableDeclaration" && left.declarations.length === 1) {
|
|
48
|
+
const decl = left.declarations[0];
|
|
49
|
+
if (decl.id && decl.id.type === "Identifier")
|
|
50
|
+
return decl.id.name;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/rules/needless-bool.ts
|
|
56
|
+
var needless_bool_default = {
|
|
57
|
+
create(context) {
|
|
58
|
+
return {
|
|
59
|
+
IfStatement(node) {
|
|
60
|
+
if (!node.alternate)
|
|
61
|
+
return;
|
|
62
|
+
const consequent = unwrapBlock(node.consequent);
|
|
63
|
+
const alternate = unwrapBlock(node.alternate);
|
|
64
|
+
if (!consequent || !alternate)
|
|
65
|
+
return;
|
|
66
|
+
if (consequent.type === "ReturnStatement" && alternate.type === "ReturnStatement") {
|
|
67
|
+
const cArg = consequent.argument;
|
|
68
|
+
const aArg = alternate.argument;
|
|
69
|
+
if (isBoolLiteral(cArg) && isBoolLiteral(aArg) && cArg.value !== aArg.value) {
|
|
70
|
+
const suggestion = cArg.value ? "return <condition>" : "return !<condition>";
|
|
71
|
+
context.report({
|
|
72
|
+
message: `Needless bool: this if/else returns opposing booleans. Simplify to \`${suggestion}\`. (clippy::needless_bool)`,
|
|
73
|
+
node
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (consequent.type === "ExpressionStatement" && alternate.type === "ExpressionStatement" && consequent.expression.type === "AssignmentExpression" && alternate.expression.type === "AssignmentExpression" && consequent.expression.operator === "=" && alternate.expression.operator === "=") {
|
|
79
|
+
const cExpr = consequent.expression;
|
|
80
|
+
const aExpr = alternate.expression;
|
|
81
|
+
if (cExpr.left.type === "Identifier" && aExpr.left.type === "Identifier" && cExpr.left.name === aExpr.left.name && isBoolLiteral(cExpr.right) && isBoolLiteral(aExpr.right) && cExpr.right.value !== aExpr.right.value) {
|
|
82
|
+
const varName = cExpr.left.name;
|
|
83
|
+
const suggestion = cExpr.right.value ? `${varName} = <condition>` : `${varName} = !<condition>`;
|
|
84
|
+
context.report({
|
|
85
|
+
message: `Needless bool: this if/else assigns opposing booleans. Simplify to \`${suggestion}\`. (clippy::needless_bool)`,
|
|
86
|
+
node
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/rules/collapsible-if.ts
|
|
96
|
+
var collapsible_if_default = {
|
|
97
|
+
create(context) {
|
|
98
|
+
return {
|
|
99
|
+
IfStatement(node) {
|
|
100
|
+
if (node.alternate)
|
|
101
|
+
return;
|
|
102
|
+
const body = node.consequent;
|
|
103
|
+
if (!body)
|
|
104
|
+
return;
|
|
105
|
+
let inner = null;
|
|
106
|
+
if (body.type === "BlockStatement" && body.body.length === 1) {
|
|
107
|
+
inner = body.body[0];
|
|
108
|
+
} else if (body.type === "IfStatement") {
|
|
109
|
+
inner = body;
|
|
110
|
+
}
|
|
111
|
+
if (!inner || inner.type !== "IfStatement")
|
|
112
|
+
return;
|
|
113
|
+
if (inner.alternate)
|
|
114
|
+
return;
|
|
115
|
+
context.report({
|
|
116
|
+
message: "Collapsible if: these nested ifs can be combined with `&&`. (clippy::collapsible_if)",
|
|
117
|
+
node
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/rules/neg-multiply.ts
|
|
125
|
+
function isNegOne(node) {
|
|
126
|
+
if (node.type === "UnaryExpression" && node.operator === "-" && node.argument.type === "Literal" && node.argument.value === 1) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (node.type === "Literal" && node.value === -1)
|
|
130
|
+
return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
var neg_multiply_default = {
|
|
134
|
+
create(context) {
|
|
135
|
+
return {
|
|
136
|
+
BinaryExpression(node) {
|
|
137
|
+
if (node.operator !== "*")
|
|
138
|
+
return;
|
|
139
|
+
if (isNegOne(node.left) || isNegOne(node.right)) {
|
|
140
|
+
context.report({
|
|
141
|
+
message: "Neg multiply: multiplying by -1 is less readable than negation. Use `-x` instead of `x * -1`. (clippy::neg_multiply)",
|
|
142
|
+
node
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/rules/bool-comparison.ts
|
|
151
|
+
var bool_comparison_default = {
|
|
152
|
+
create(context) {
|
|
153
|
+
return {
|
|
154
|
+
BinaryExpression(node) {
|
|
155
|
+
if (node.operator !== "===" && node.operator !== "!==")
|
|
156
|
+
return;
|
|
157
|
+
const leftBool = isBoolLiteral(node.left);
|
|
158
|
+
const rightBool = isBoolLiteral(node.right);
|
|
159
|
+
if (!leftBool && !rightBool)
|
|
160
|
+
return;
|
|
161
|
+
if (leftBool && rightBool)
|
|
162
|
+
return;
|
|
163
|
+
const boolVal = leftBool ? node.left.value : node.right.value;
|
|
164
|
+
const isEqual = node.operator === "===";
|
|
165
|
+
let suggestion;
|
|
166
|
+
if (boolVal && isEqual || !boolVal && !isEqual) {
|
|
167
|
+
suggestion = "use the expression directly";
|
|
168
|
+
} else {
|
|
169
|
+
suggestion = "negate the expression with `!`";
|
|
170
|
+
}
|
|
171
|
+
context.report({
|
|
172
|
+
message: `Bool comparison: comparison to \`${boolVal}\` is needless. Instead, ${suggestion}. (clippy::bool_comparison)`,
|
|
173
|
+
node
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/rules/identity-op.ts
|
|
181
|
+
var identity_op_default = {
|
|
182
|
+
create(context) {
|
|
183
|
+
return {
|
|
184
|
+
BinaryExpression(node) {
|
|
185
|
+
const { operator, left, right } = node;
|
|
186
|
+
let match = false;
|
|
187
|
+
switch (operator) {
|
|
188
|
+
case "+":
|
|
189
|
+
if (isLiteral(left) && typeof left.value === "string")
|
|
190
|
+
break;
|
|
191
|
+
if (isLiteral(right) && typeof right.value === "string")
|
|
192
|
+
break;
|
|
193
|
+
match = isLiteral(left, 0) || isLiteral(right, 0);
|
|
194
|
+
break;
|
|
195
|
+
case "-":
|
|
196
|
+
match = isLiteral(right, 0);
|
|
197
|
+
break;
|
|
198
|
+
case "*":
|
|
199
|
+
match = isLiteral(left, 1) || isLiteral(right, 1);
|
|
200
|
+
break;
|
|
201
|
+
case "/":
|
|
202
|
+
match = isLiteral(right, 1);
|
|
203
|
+
break;
|
|
204
|
+
case "**":
|
|
205
|
+
match = isLiteral(right, 1);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (match) {
|
|
209
|
+
context.report({
|
|
210
|
+
message: `Identity op: \`${operator} ${operator === "+" || operator === "-" ? "0" : "1"}\` has no effect. (clippy::identity_op)`,
|
|
211
|
+
node
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/rules/single-case-switch.ts
|
|
220
|
+
var single_case_switch_default = {
|
|
221
|
+
create(context) {
|
|
222
|
+
return {
|
|
223
|
+
SwitchStatement(node) {
|
|
224
|
+
const cases = node.cases;
|
|
225
|
+
if (!cases)
|
|
226
|
+
return;
|
|
227
|
+
const nonDefaultCases = cases.filter((c) => c.test !== null);
|
|
228
|
+
const defaultCase = cases.find((c) => c.test === null);
|
|
229
|
+
if (nonDefaultCases.length !== 1)
|
|
230
|
+
return;
|
|
231
|
+
const suggestion = defaultCase ? "if/else" : "if";
|
|
232
|
+
context.report({
|
|
233
|
+
message: `Single-case switch: this switch has only one case. Use an \`${suggestion}\` statement instead. (clippy::single_match)`,
|
|
234
|
+
node
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/rules/too-many-arguments.ts
|
|
242
|
+
var THRESHOLD = 5;
|
|
243
|
+
function checkParams(node, context) {
|
|
244
|
+
const params = node.params;
|
|
245
|
+
if (!params || params.length <= THRESHOLD)
|
|
246
|
+
return;
|
|
247
|
+
const name = node.id?.name ?? (node.parent?.type === "VariableDeclarator" ? node.parent.id?.name : null) ?? "<anonymous>";
|
|
248
|
+
context.report({
|
|
249
|
+
message: `Too many arguments: \`${name}\` has ${params.length} parameters (max ${THRESHOLD}). Consider using an options object. (clippy::too_many_arguments)`,
|
|
250
|
+
node
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
var too_many_arguments_default = {
|
|
254
|
+
create(context) {
|
|
255
|
+
return {
|
|
256
|
+
FunctionDeclaration(node) {
|
|
257
|
+
checkParams(node, context);
|
|
258
|
+
},
|
|
259
|
+
FunctionExpression(node) {
|
|
260
|
+
checkParams(node, context);
|
|
261
|
+
},
|
|
262
|
+
ArrowFunctionExpression(node) {
|
|
263
|
+
checkParams(node, context);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// src/rules/too-many-lines.ts
|
|
270
|
+
var THRESHOLD2 = 100;
|
|
271
|
+
function countLines(node, sourceText) {
|
|
272
|
+
if (node.loc)
|
|
273
|
+
return node.loc.end.line - node.loc.start.line + 1;
|
|
274
|
+
if (node.start != null && node.end != null) {
|
|
275
|
+
return sourceText.substring(node.start, node.end).split(`
|
|
276
|
+
`).length;
|
|
277
|
+
}
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
function checkLines(node, context) {
|
|
281
|
+
const lines = countLines(node, context.sourceCode.text);
|
|
282
|
+
if (lines <= THRESHOLD2)
|
|
283
|
+
return;
|
|
284
|
+
const name = node.id?.name ?? (node.parent?.type === "VariableDeclarator" ? node.parent.id?.name : null) ?? "<anonymous>";
|
|
285
|
+
context.report({
|
|
286
|
+
message: `Too many lines: \`${name}\` is ${lines} lines long (max ${THRESHOLD2}). Consider breaking it into smaller functions. (clippy::too_many_lines)`,
|
|
287
|
+
node
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
var too_many_lines_default = {
|
|
291
|
+
create(context) {
|
|
292
|
+
return {
|
|
293
|
+
FunctionDeclaration(node) {
|
|
294
|
+
checkLines(node, context);
|
|
295
|
+
},
|
|
296
|
+
FunctionExpression(node) {
|
|
297
|
+
checkLines(node, context);
|
|
298
|
+
},
|
|
299
|
+
ArrowFunctionExpression(node) {
|
|
300
|
+
checkLines(node, context);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/rules/filter-then-first.ts
|
|
307
|
+
var filter_then_first_default = {
|
|
308
|
+
create(context) {
|
|
309
|
+
return {
|
|
310
|
+
MemberExpression(node) {
|
|
311
|
+
if (!node.computed || !isLiteral(node.property, 0))
|
|
312
|
+
return;
|
|
313
|
+
const obj = node.object;
|
|
314
|
+
if (!isMethodCall(obj, "filter"))
|
|
315
|
+
return;
|
|
316
|
+
context.report({
|
|
317
|
+
message: "Filter then first: `.filter(fn)[0]` is less efficient and readable than `.find(fn)`. (clippy::filter_next)",
|
|
318
|
+
node
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/rules/map-void-return.ts
|
|
326
|
+
var map_void_return_default = {
|
|
327
|
+
create(context) {
|
|
328
|
+
return {
|
|
329
|
+
ExpressionStatement(node) {
|
|
330
|
+
const expr = node.expression;
|
|
331
|
+
if (!isMethodCall(expr, "map"))
|
|
332
|
+
return;
|
|
333
|
+
context.report({
|
|
334
|
+
message: "Map void return: `.map()` result is unused. Use `.forEach()` for side effects. (clippy::map_unit_fn)",
|
|
335
|
+
node
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/rules/useless-conversion.ts
|
|
343
|
+
var useless_conversion_default = {
|
|
344
|
+
create(context) {
|
|
345
|
+
return {
|
|
346
|
+
CallExpression(node) {
|
|
347
|
+
const { callee } = node;
|
|
348
|
+
const args = node.arguments;
|
|
349
|
+
if (!args || args.length !== 1)
|
|
350
|
+
return;
|
|
351
|
+
const arg = args[0];
|
|
352
|
+
if (isIdentifier(callee, "String") && arg.type === "Literal" && typeof arg.value === "string") {
|
|
353
|
+
context.report({
|
|
354
|
+
message: "Useless conversion: `String()` called on a string literal. (clippy::useless_conversion)",
|
|
355
|
+
node
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (isIdentifier(callee, "Number") && arg.type === "Literal" && typeof arg.value === "number") {
|
|
360
|
+
context.report({
|
|
361
|
+
message: "Useless conversion: `Number()` called on a number literal. (clippy::useless_conversion)",
|
|
362
|
+
node
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (isIdentifier(callee, "Boolean") && arg.type === "Literal" && typeof arg.value === "boolean") {
|
|
367
|
+
context.report({
|
|
368
|
+
message: "Useless conversion: `Boolean()` called on a boolean literal. (clippy::useless_conversion)",
|
|
369
|
+
node
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (isCallOf(node, "Array", "from") && arg.type === "ArrayExpression") {
|
|
374
|
+
context.report({
|
|
375
|
+
message: "Useless conversion: `Array.from()` called on an array literal. Use the array directly. (clippy::useless_conversion)",
|
|
376
|
+
node
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (callee.type === "MemberExpression" && isIdentifier(callee.property, "toString") && callee.object.type === "Literal" && typeof callee.object.value === "string" && args.length === 0) {}
|
|
381
|
+
},
|
|
382
|
+
"CallExpression:exit"(node) {
|
|
383
|
+
const { callee } = node;
|
|
384
|
+
const args = node.arguments;
|
|
385
|
+
if (!args || args.length !== 0)
|
|
386
|
+
return;
|
|
387
|
+
if (callee.type !== "MemberExpression")
|
|
388
|
+
return;
|
|
389
|
+
const obj = callee.object;
|
|
390
|
+
const method = callee.property;
|
|
391
|
+
if (method.type !== "Identifier")
|
|
392
|
+
return;
|
|
393
|
+
if (obj.type === "Literal" && typeof obj.value === "string" && method.name === "toString") {
|
|
394
|
+
context.report({
|
|
395
|
+
message: "Useless conversion: `.toString()` called on a string literal. (clippy::useless_conversion)",
|
|
396
|
+
node
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (obj.type === "Literal" && typeof obj.value === "number" && method.name === "valueOf") {
|
|
401
|
+
context.report({
|
|
402
|
+
message: "Useless conversion: `.valueOf()` called on a number literal. (clippy::useless_conversion)",
|
|
403
|
+
node
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/rules/manual-clamp.ts
|
|
412
|
+
function isMathCall(node, method) {
|
|
413
|
+
if (node.type !== "CallExpression")
|
|
414
|
+
return false;
|
|
415
|
+
const callee = node.callee;
|
|
416
|
+
return callee.type === "MemberExpression" && isIdentifier(callee.object, "Math") && isIdentifier(callee.property, method);
|
|
417
|
+
}
|
|
418
|
+
var manual_clamp_default = {
|
|
419
|
+
create(context) {
|
|
420
|
+
return {
|
|
421
|
+
CallExpression(node) {
|
|
422
|
+
const args = node.arguments;
|
|
423
|
+
if (!args || args.length !== 2)
|
|
424
|
+
return;
|
|
425
|
+
if (isMathCall(node, "max") && isMathCall(args[0], "min")) {
|
|
426
|
+
context.report({
|
|
427
|
+
message: "Manual clamp: this `Math.max(min, Math.min(val, max))` pattern is a clamp. Consider extracting a `clamp(val, min, max)` helper. (clippy::manual_clamp)",
|
|
428
|
+
node
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (isMathCall(node, "max") && isMathCall(args[1], "min")) {
|
|
433
|
+
context.report({
|
|
434
|
+
message: "Manual clamp: this `Math.max(Math.min(val, max), min)` pattern is a clamp. Consider extracting a `clamp(val, min, max)` helper. (clippy::manual_clamp)",
|
|
435
|
+
node
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (isMathCall(node, "min") && isMathCall(args[0], "max")) {
|
|
440
|
+
context.report({
|
|
441
|
+
message: "Manual clamp: this `Math.min(max, Math.max(val, min))` pattern is a clamp. Consider extracting a `clamp(val, min, max)` helper. (clippy::manual_clamp)",
|
|
442
|
+
node
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (isMathCall(node, "min") && isMathCall(args[1], "max")) {
|
|
447
|
+
context.report({
|
|
448
|
+
message: "Manual clamp: this `Math.min(Math.max(val, min), max)` pattern is a clamp. Consider extracting a `clamp(val, min, max)` helper. (clippy::manual_clamp)",
|
|
449
|
+
node
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// src/rules/manual-strip.ts
|
|
459
|
+
function getStringMethodTarget(testExpr, methodName) {
|
|
460
|
+
if (!isMethodCall(testExpr, methodName))
|
|
461
|
+
return null;
|
|
462
|
+
const callee = testExpr.callee;
|
|
463
|
+
if (callee.object?.type !== "Identifier")
|
|
464
|
+
return null;
|
|
465
|
+
if (!testExpr.arguments || testExpr.arguments.length !== 1)
|
|
466
|
+
return null;
|
|
467
|
+
const arg = testExpr.arguments[0];
|
|
468
|
+
if (arg.type !== "Identifier" && arg.type !== "Literal")
|
|
469
|
+
return null;
|
|
470
|
+
return {
|
|
471
|
+
str: callee.object.name,
|
|
472
|
+
arg: arg.type === "Identifier" ? arg.name : JSON.stringify(arg.value)
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function bodyContainsSlice(node, strName) {
|
|
476
|
+
if (!node || typeof node !== "object")
|
|
477
|
+
return false;
|
|
478
|
+
if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === strName && node.callee.property?.type === "Identifier" && node.callee.property.name === "slice") {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
for (const key of Object.keys(node)) {
|
|
482
|
+
if (key === "type" || key === "loc" || key === "range" || key === "parent" || key === "start" || key === "end")
|
|
483
|
+
continue;
|
|
484
|
+
const val = node[key];
|
|
485
|
+
if (Array.isArray(val)) {
|
|
486
|
+
for (const child of val) {
|
|
487
|
+
if (child && typeof child === "object" && child.type && bodyContainsSlice(child, strName))
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
} else if (val && typeof val === "object" && val.type && bodyContainsSlice(val, strName)) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
var manual_strip_default = {
|
|
497
|
+
create(context) {
|
|
498
|
+
return {
|
|
499
|
+
IfStatement(node) {
|
|
500
|
+
const test = node.test;
|
|
501
|
+
if (!test)
|
|
502
|
+
return;
|
|
503
|
+
const startsInfo = getStringMethodTarget(test, "startsWith");
|
|
504
|
+
if (startsInfo && bodyContainsSlice(node.consequent, startsInfo.str)) {
|
|
505
|
+
context.report({
|
|
506
|
+
message: `Manual strip: checking \`startsWith()\` then slicing is a manual prefix strip. Consider extracting a \`stripPrefix\` helper. (clippy::manual_strip)`,
|
|
507
|
+
node
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const endsInfo = getStringMethodTarget(test, "endsWith");
|
|
512
|
+
if (endsInfo && bodyContainsSlice(node.consequent, endsInfo.str)) {
|
|
513
|
+
context.report({
|
|
514
|
+
message: `Manual strip: checking \`endsWith()\` then slicing is a manual suffix strip. Consider extracting a \`stripSuffix\` helper. (clippy::manual_strip)`,
|
|
515
|
+
node
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// src/rules/manual-find.ts
|
|
524
|
+
function isReturnOfVar(stmt, varName) {
|
|
525
|
+
if (stmt.type !== "ReturnStatement")
|
|
526
|
+
return false;
|
|
527
|
+
return stmt.argument?.type === "Identifier" && stmt.argument.name === varName;
|
|
528
|
+
}
|
|
529
|
+
function isReturnNullish(stmt) {
|
|
530
|
+
if (stmt.type !== "ReturnStatement")
|
|
531
|
+
return false;
|
|
532
|
+
if (!stmt.argument)
|
|
533
|
+
return true;
|
|
534
|
+
if (stmt.argument.type === "Identifier" && stmt.argument.name === "undefined")
|
|
535
|
+
return true;
|
|
536
|
+
if (stmt.argument.type === "Literal" && stmt.argument.value === null)
|
|
537
|
+
return true;
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
function checkBody(body, context) {
|
|
541
|
+
for (let i = 0;i < body.length; i++) {
|
|
542
|
+
const stmt = body[i];
|
|
543
|
+
if (stmt.type !== "ForOfStatement")
|
|
544
|
+
continue;
|
|
545
|
+
const loopVar = getForOfVar(stmt);
|
|
546
|
+
if (!loopVar)
|
|
547
|
+
continue;
|
|
548
|
+
const inner = unwrapBlock(stmt.body);
|
|
549
|
+
if (!inner)
|
|
550
|
+
continue;
|
|
551
|
+
if (inner.type === "IfStatement" && !inner.alternate) {
|
|
552
|
+
const consequent = unwrapBlock(inner.consequent);
|
|
553
|
+
if (consequent && isReturnOfVar(consequent, loopVar)) {
|
|
554
|
+
const next = body[i + 1];
|
|
555
|
+
if (next && isReturnNullish(next)) {
|
|
556
|
+
context.report({
|
|
557
|
+
message: "Manual find: this loop can be replaced with `.find()`. (clippy::manual_find)",
|
|
558
|
+
node: stmt
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
var manual_find_default = {
|
|
566
|
+
create(context) {
|
|
567
|
+
function checkFunction(node) {
|
|
568
|
+
const body = getFunctionBody(node);
|
|
569
|
+
if (body)
|
|
570
|
+
checkBody(body, context);
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
FunctionDeclaration: checkFunction,
|
|
574
|
+
FunctionExpression: checkFunction,
|
|
575
|
+
ArrowFunctionExpression: checkFunction
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/rules/manual-some.ts
|
|
581
|
+
function checkBody2(body, context) {
|
|
582
|
+
for (let i = 0;i < body.length; i++) {
|
|
583
|
+
const stmt = body[i];
|
|
584
|
+
if (stmt.type !== "ForOfStatement")
|
|
585
|
+
continue;
|
|
586
|
+
const inner = unwrapBlock(stmt.body);
|
|
587
|
+
if (!inner || inner.type !== "IfStatement" || inner.alternate)
|
|
588
|
+
continue;
|
|
589
|
+
const consequent = unwrapBlock(inner.consequent);
|
|
590
|
+
if (!consequent || consequent.type !== "ReturnStatement" || !isLiteral(consequent.argument, true))
|
|
591
|
+
continue;
|
|
592
|
+
const next = body[i + 1];
|
|
593
|
+
if (next && next.type === "ReturnStatement" && isLiteral(next.argument, false)) {
|
|
594
|
+
context.report({
|
|
595
|
+
message: "Manual some: this loop can be replaced with `.some()`. (clippy::manual_find)",
|
|
596
|
+
node: stmt
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
var manual_some_default = {
|
|
602
|
+
create(context) {
|
|
603
|
+
function checkFunction(node) {
|
|
604
|
+
const body = getFunctionBody(node);
|
|
605
|
+
if (body)
|
|
606
|
+
checkBody2(body, context);
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
FunctionDeclaration: checkFunction,
|
|
610
|
+
FunctionExpression: checkFunction,
|
|
611
|
+
ArrowFunctionExpression: checkFunction
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// src/rules/manual-every.ts
|
|
617
|
+
function checkBody3(body, context) {
|
|
618
|
+
for (let i = 0;i < body.length; i++) {
|
|
619
|
+
const stmt = body[i];
|
|
620
|
+
if (stmt.type !== "ForOfStatement")
|
|
621
|
+
continue;
|
|
622
|
+
const inner = unwrapBlock(stmt.body);
|
|
623
|
+
if (!inner || inner.type !== "IfStatement" || inner.alternate)
|
|
624
|
+
continue;
|
|
625
|
+
const consequent = unwrapBlock(inner.consequent);
|
|
626
|
+
if (!consequent || consequent.type !== "ReturnStatement" || !isLiteral(consequent.argument, false))
|
|
627
|
+
continue;
|
|
628
|
+
const next = body[i + 1];
|
|
629
|
+
if (next && next.type === "ReturnStatement" && isLiteral(next.argument, true)) {
|
|
630
|
+
context.report({
|
|
631
|
+
message: "Manual every: this loop can be replaced with `.every()`. (clippy::manual_find)",
|
|
632
|
+
node: stmt
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
var manual_every_default = {
|
|
638
|
+
create(context) {
|
|
639
|
+
function checkFunction(node) {
|
|
640
|
+
const body = getFunctionBody(node);
|
|
641
|
+
if (body)
|
|
642
|
+
checkBody3(body, context);
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
FunctionDeclaration: checkFunction,
|
|
646
|
+
FunctionExpression: checkFunction,
|
|
647
|
+
ArrowFunctionExpression: checkFunction
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// src/rules/manual-includes.ts
|
|
653
|
+
function isEqualityCheck(test, varName) {
|
|
654
|
+
if (test.type !== "BinaryExpression")
|
|
655
|
+
return false;
|
|
656
|
+
if (test.operator !== "===" && test.operator !== "==")
|
|
657
|
+
return false;
|
|
658
|
+
const left = test.left;
|
|
659
|
+
const right = test.right;
|
|
660
|
+
return left.type === "Identifier" && left.name === varName || right.type === "Identifier" && right.name === varName;
|
|
661
|
+
}
|
|
662
|
+
function checkBody4(body, context) {
|
|
663
|
+
for (let i = 0;i < body.length; i++) {
|
|
664
|
+
const stmt = body[i];
|
|
665
|
+
if (stmt.type !== "ForOfStatement")
|
|
666
|
+
continue;
|
|
667
|
+
const loopVar = getForOfVar(stmt);
|
|
668
|
+
if (!loopVar)
|
|
669
|
+
continue;
|
|
670
|
+
const inner = unwrapBlock(stmt.body);
|
|
671
|
+
if (!inner || inner.type !== "IfStatement" || inner.alternate)
|
|
672
|
+
continue;
|
|
673
|
+
if (!isEqualityCheck(inner.test, loopVar))
|
|
674
|
+
continue;
|
|
675
|
+
const consequent = unwrapBlock(inner.consequent);
|
|
676
|
+
if (!consequent || consequent.type !== "ReturnStatement" || !isLiteral(consequent.argument, true))
|
|
677
|
+
continue;
|
|
678
|
+
const next = body[i + 1];
|
|
679
|
+
if (next && next.type === "ReturnStatement" && isLiteral(next.argument, false)) {
|
|
680
|
+
context.report({
|
|
681
|
+
message: "Manual includes: this loop can be replaced with `.includes()`. (clippy::manual_find)",
|
|
682
|
+
node: stmt
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
var manual_includes_default = {
|
|
688
|
+
create(context) {
|
|
689
|
+
function checkFunction(node) {
|
|
690
|
+
const body = getFunctionBody(node);
|
|
691
|
+
if (body)
|
|
692
|
+
checkBody4(body, context);
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
FunctionDeclaration: checkFunction,
|
|
696
|
+
FunctionExpression: checkFunction,
|
|
697
|
+
ArrowFunctionExpression: checkFunction
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// src/rules/cognitive-complexity.ts
|
|
703
|
+
var THRESHOLD3 = 25;
|
|
704
|
+
function calculateComplexity(node) {
|
|
705
|
+
let complexity = 0;
|
|
706
|
+
function walk(n, nesting) {
|
|
707
|
+
if (!n || typeof n !== "object")
|
|
708
|
+
return;
|
|
709
|
+
switch (n.type) {
|
|
710
|
+
case "IfStatement":
|
|
711
|
+
complexity += 1 + nesting;
|
|
712
|
+
walk(n.test, nesting);
|
|
713
|
+
walk(n.consequent, nesting + 1);
|
|
714
|
+
if (n.alternate) {
|
|
715
|
+
if (n.alternate.type === "IfStatement") {
|
|
716
|
+
complexity += 1;
|
|
717
|
+
walk(n.alternate.test, nesting);
|
|
718
|
+
walk(n.alternate.consequent, nesting + 1);
|
|
719
|
+
if (n.alternate.alternate) {
|
|
720
|
+
walk(n.alternate.alternate, nesting);
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
complexity += 1;
|
|
724
|
+
walk(n.alternate, nesting + 1);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return;
|
|
728
|
+
case "ForStatement":
|
|
729
|
+
case "ForInStatement":
|
|
730
|
+
case "ForOfStatement":
|
|
731
|
+
case "WhileStatement":
|
|
732
|
+
case "DoWhileStatement":
|
|
733
|
+
complexity += 1 + nesting;
|
|
734
|
+
walkChildren(n, nesting + 1);
|
|
735
|
+
return;
|
|
736
|
+
case "CatchClause":
|
|
737
|
+
complexity += 1 + nesting;
|
|
738
|
+
walkChildren(n, nesting + 1);
|
|
739
|
+
return;
|
|
740
|
+
case "SwitchStatement":
|
|
741
|
+
complexity += 1 + nesting;
|
|
742
|
+
walkChildren(n, nesting + 1);
|
|
743
|
+
return;
|
|
744
|
+
case "ConditionalExpression":
|
|
745
|
+
complexity += 1 + nesting;
|
|
746
|
+
walk(n.test, nesting);
|
|
747
|
+
walk(n.consequent, nesting + 1);
|
|
748
|
+
walk(n.alternate, nesting + 1);
|
|
749
|
+
return;
|
|
750
|
+
case "LogicalExpression":
|
|
751
|
+
complexity += 1;
|
|
752
|
+
walk(n.left, nesting);
|
|
753
|
+
walk(n.right, nesting);
|
|
754
|
+
return;
|
|
755
|
+
case "FunctionDeclaration":
|
|
756
|
+
case "FunctionExpression":
|
|
757
|
+
case "ArrowFunctionExpression":
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
walkChildren(n, nesting);
|
|
761
|
+
}
|
|
762
|
+
function walkChildren(n, nesting) {
|
|
763
|
+
for (const key of Object.keys(n)) {
|
|
764
|
+
if (key === "type" || key === "loc" || key === "range" || key === "parent")
|
|
765
|
+
continue;
|
|
766
|
+
const val = n[key];
|
|
767
|
+
if (Array.isArray(val)) {
|
|
768
|
+
for (const child of val) {
|
|
769
|
+
if (child && typeof child === "object" && child.type) {
|
|
770
|
+
walk(child, nesting);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
} else if (val && typeof val === "object" && val.type) {
|
|
774
|
+
walk(val, nesting);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (node.body) {
|
|
779
|
+
if (node.body.type === "BlockStatement") {
|
|
780
|
+
for (const stmt of node.body.body) {
|
|
781
|
+
walk(stmt, 0);
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
walk(node.body, 0);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return complexity;
|
|
788
|
+
}
|
|
789
|
+
var cognitive_complexity_default = {
|
|
790
|
+
create(context) {
|
|
791
|
+
function checkFunction(node) {
|
|
792
|
+
const complexity = calculateComplexity(node);
|
|
793
|
+
if (complexity <= THRESHOLD3)
|
|
794
|
+
return;
|
|
795
|
+
const name = node.id?.name ?? (node.parent?.type === "VariableDeclarator" ? node.parent.id?.name : null) ?? "<anonymous>";
|
|
796
|
+
context.report({
|
|
797
|
+
message: `Cognitive complexity: \`${name}\` has a complexity of ${complexity} (max ${THRESHOLD3}). Consider refactoring into smaller functions. (clippy::cognitive_complexity)`,
|
|
798
|
+
node
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
FunctionDeclaration: checkFunction,
|
|
803
|
+
FunctionExpression: checkFunction,
|
|
804
|
+
ArrowFunctionExpression: checkFunction
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
// src/rules/float-comparison.ts
|
|
810
|
+
function isFloatLiteral(node) {
|
|
811
|
+
return node.type === "Literal" && typeof node.value === "number" && !Number.isInteger(node.value);
|
|
812
|
+
}
|
|
813
|
+
function isArithmetic(node) {
|
|
814
|
+
return node.type === "BinaryExpression" && (node.operator === "+" || node.operator === "-" || node.operator === "*" || node.operator === "/");
|
|
815
|
+
}
|
|
816
|
+
function involvesFloat(node) {
|
|
817
|
+
if (isFloatLiteral(node))
|
|
818
|
+
return true;
|
|
819
|
+
if (isArithmetic(node)) {
|
|
820
|
+
return involvesFloat(node.left) || involvesFloat(node.right);
|
|
821
|
+
}
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
var float_comparison_default = {
|
|
825
|
+
create(context) {
|
|
826
|
+
return {
|
|
827
|
+
BinaryExpression(node) {
|
|
828
|
+
if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==" && node.operator !== "!=")
|
|
829
|
+
return;
|
|
830
|
+
if (involvesFloat(node.left) || involvesFloat(node.right)) {
|
|
831
|
+
context.report({
|
|
832
|
+
message: "Float comparison: direct equality comparison of floating-point numbers is unreliable. Use `Math.abs(a - b) < Number.EPSILON` instead. (clippy::float_cmp)",
|
|
833
|
+
node
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// src/rules/needless-range-loop.ts
|
|
842
|
+
function isZeroInit(init) {
|
|
843
|
+
if (init.type !== "VariableDeclaration" || init.declarations.length !== 1)
|
|
844
|
+
return null;
|
|
845
|
+
const decl = init.declarations[0];
|
|
846
|
+
if (decl.id?.type !== "Identifier")
|
|
847
|
+
return null;
|
|
848
|
+
if (decl.init?.type !== "Literal" || decl.init.value !== 0)
|
|
849
|
+
return null;
|
|
850
|
+
return decl.id.name;
|
|
851
|
+
}
|
|
852
|
+
function isLengthTest(test, indexVar) {
|
|
853
|
+
if (test.type !== "BinaryExpression" || test.operator !== "<")
|
|
854
|
+
return null;
|
|
855
|
+
if (test.left?.type !== "Identifier" || test.left.name !== indexVar)
|
|
856
|
+
return null;
|
|
857
|
+
if (test.right?.type === "MemberExpression" && test.right.object?.type === "Identifier" && test.right.property?.type === "Identifier" && test.right.property.name === "length") {
|
|
858
|
+
return test.right.object.name;
|
|
859
|
+
}
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
function isIncrement(update, indexVar) {
|
|
863
|
+
if (update.type === "UpdateExpression" && update.operator === "++" && update.argument?.type === "Identifier" && update.argument.name === indexVar)
|
|
864
|
+
return true;
|
|
865
|
+
if (update.type === "AssignmentExpression" && update.operator === "+=" && update.left?.type === "Identifier" && update.left.name === indexVar && update.right?.type === "Literal" && update.right.value === 1)
|
|
866
|
+
return true;
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
function onlyUsedAsIndex(body, indexVar, arrName) {
|
|
870
|
+
let onlyIndexAccess = true;
|
|
871
|
+
function walk(n) {
|
|
872
|
+
if (!n || typeof n !== "object" || !n.type)
|
|
873
|
+
return;
|
|
874
|
+
if (!onlyIndexAccess)
|
|
875
|
+
return;
|
|
876
|
+
if (n.type === "Identifier" && n.name === indexVar) {
|
|
877
|
+
onlyIndexAccess = false;
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (n.type === "MemberExpression" && n.computed && n.object?.type === "Identifier" && n.object.name === arrName && n.property?.type === "Identifier" && n.property.name === indexVar) {
|
|
881
|
+
walk(n.object);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
for (const key of Object.keys(n)) {
|
|
885
|
+
if (key === "type" || key === "loc" || key === "range" || key === "parent" || key === "start" || key === "end")
|
|
886
|
+
continue;
|
|
887
|
+
const val = n[key];
|
|
888
|
+
if (Array.isArray(val)) {
|
|
889
|
+
for (const child of val) {
|
|
890
|
+
if (child && typeof child === "object" && child.type)
|
|
891
|
+
walk(child);
|
|
892
|
+
}
|
|
893
|
+
} else if (val && typeof val === "object" && val.type) {
|
|
894
|
+
walk(val);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
walk(body);
|
|
899
|
+
return onlyIndexAccess;
|
|
900
|
+
}
|
|
901
|
+
var needless_range_loop_default = {
|
|
902
|
+
create(context) {
|
|
903
|
+
return {
|
|
904
|
+
ForStatement(node) {
|
|
905
|
+
if (!node.init || !node.test || !node.update)
|
|
906
|
+
return;
|
|
907
|
+
const indexVar = isZeroInit(node.init);
|
|
908
|
+
if (!indexVar)
|
|
909
|
+
return;
|
|
910
|
+
const arrName = isLengthTest(node.test, indexVar);
|
|
911
|
+
if (!arrName)
|
|
912
|
+
return;
|
|
913
|
+
if (!isIncrement(node.update, indexVar))
|
|
914
|
+
return;
|
|
915
|
+
if (onlyUsedAsIndex(node.body, indexVar, arrName)) {
|
|
916
|
+
context.report({
|
|
917
|
+
message: `Needless range loop: \`${indexVar}\` is only used to index \`${arrName}\`. Use \`for (const item of ${arrName})\` instead. (clippy::needless_range_loop)`,
|
|
918
|
+
node
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
// src/rules/manual-swap.ts
|
|
927
|
+
function exprName(node) {
|
|
928
|
+
if (node.type === "Identifier")
|
|
929
|
+
return node.name;
|
|
930
|
+
if (node.type === "MemberExpression" && !node.computed && node.object?.type === "Identifier" && node.property?.type === "Identifier") {
|
|
931
|
+
return `${node.object.name}.${node.property.name}`;
|
|
932
|
+
}
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
function checkBody5(body, context) {
|
|
936
|
+
for (let i = 0;i < body.length - 2; i++) {
|
|
937
|
+
const s1 = body[i];
|
|
938
|
+
const s2 = body[i + 1];
|
|
939
|
+
const s3 = body[i + 2];
|
|
940
|
+
if (s1.type !== "VariableDeclaration" || s1.declarations.length !== 1)
|
|
941
|
+
continue;
|
|
942
|
+
const decl = s1.declarations[0];
|
|
943
|
+
if (decl.id?.type !== "Identifier" || !decl.init)
|
|
944
|
+
continue;
|
|
945
|
+
const tmpName = decl.id.name;
|
|
946
|
+
const aName = exprName(decl.init);
|
|
947
|
+
if (!aName)
|
|
948
|
+
continue;
|
|
949
|
+
if (s2.type !== "ExpressionStatement" || s2.expression?.type !== "AssignmentExpression" || s2.expression.operator !== "=")
|
|
950
|
+
continue;
|
|
951
|
+
const assignLeft = exprName(s2.expression.left);
|
|
952
|
+
const bName = exprName(s2.expression.right);
|
|
953
|
+
if (assignLeft !== aName || !bName)
|
|
954
|
+
continue;
|
|
955
|
+
if (s3.type !== "ExpressionStatement" || s3.expression?.type !== "AssignmentExpression" || s3.expression.operator !== "=")
|
|
956
|
+
continue;
|
|
957
|
+
const assign2Left = exprName(s3.expression.left);
|
|
958
|
+
const assign2Right = exprName(s3.expression.right);
|
|
959
|
+
if (assign2Left !== bName || assign2Right !== tmpName)
|
|
960
|
+
continue;
|
|
961
|
+
context.report({
|
|
962
|
+
message: `Manual swap: use destructuring \`[${aName}, ${bName}] = [${bName}, ${aName}]\` instead of a temp variable. (clippy::manual_swap)`,
|
|
963
|
+
node: s1
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
var manual_swap_default = {
|
|
968
|
+
create(context) {
|
|
969
|
+
return {
|
|
970
|
+
BlockStatement(node) {
|
|
971
|
+
if (node.body)
|
|
972
|
+
checkBody5(node.body, context);
|
|
973
|
+
},
|
|
974
|
+
Program(node) {
|
|
975
|
+
if (node.body)
|
|
976
|
+
checkBody5(node.body, context);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
// src/rules/search-is-some.ts
|
|
983
|
+
function isNullish(node) {
|
|
984
|
+
if (node.type === "Identifier" && node.name === "undefined")
|
|
985
|
+
return true;
|
|
986
|
+
if (node.type === "Literal" && node.value === null)
|
|
987
|
+
return true;
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
function isFindCall(node) {
|
|
991
|
+
return isMethodCall(node, "find");
|
|
992
|
+
}
|
|
993
|
+
function matchesFindNullish(left, right) {
|
|
994
|
+
return isFindCall(left) && isNullish(right) || isNullish(left) && isFindCall(right);
|
|
995
|
+
}
|
|
996
|
+
var search_is_some_default = {
|
|
997
|
+
create(context) {
|
|
998
|
+
return {
|
|
999
|
+
BinaryExpression(node) {
|
|
1000
|
+
const { operator, left, right } = node;
|
|
1001
|
+
if (!matchesFindNullish(left, right))
|
|
1002
|
+
return;
|
|
1003
|
+
if (operator === "!==" || operator === "!=") {
|
|
1004
|
+
context.report({
|
|
1005
|
+
message: "Search is some: `.find(fn) !== undefined` can be simplified to `.some(fn)` when you don't need the found value. (clippy::search_is_some)",
|
|
1006
|
+
node
|
|
1007
|
+
});
|
|
1008
|
+
} else if (operator === "===" || operator === "==") {
|
|
1009
|
+
context.report({
|
|
1010
|
+
message: "Search is some: `.find(fn) === undefined` can be simplified to `!.some(fn)` when you don't need the found value. (clippy::search_is_some)",
|
|
1011
|
+
node
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// src/rules/let-and-return.ts
|
|
1020
|
+
function checkBody6(body, context) {
|
|
1021
|
+
if (body.length < 2)
|
|
1022
|
+
return;
|
|
1023
|
+
const last = body[body.length - 1];
|
|
1024
|
+
const secondLast = body[body.length - 2];
|
|
1025
|
+
if (last.type !== "ReturnStatement" || !last.argument || last.argument.type !== "Identifier")
|
|
1026
|
+
return;
|
|
1027
|
+
const returnedName = last.argument.name;
|
|
1028
|
+
if (secondLast.type !== "VariableDeclaration" || secondLast.declarations.length !== 1)
|
|
1029
|
+
return;
|
|
1030
|
+
const decl = secondLast.declarations[0];
|
|
1031
|
+
if (decl.id?.type !== "Identifier" || decl.id.name !== returnedName || !decl.init)
|
|
1032
|
+
return;
|
|
1033
|
+
context.report({
|
|
1034
|
+
message: `Let and return: \`${returnedName}\` is immediately returned. Return the expression directly. (clippy::let_and_return)`,
|
|
1035
|
+
node: secondLast
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
var let_and_return_default = {
|
|
1039
|
+
create(context) {
|
|
1040
|
+
function checkFunction(node) {
|
|
1041
|
+
const body = getFunctionBody(node);
|
|
1042
|
+
if (body)
|
|
1043
|
+
checkBody6(body, context);
|
|
1044
|
+
}
|
|
1045
|
+
return {
|
|
1046
|
+
FunctionDeclaration: checkFunction,
|
|
1047
|
+
FunctionExpression: checkFunction,
|
|
1048
|
+
ArrowFunctionExpression: checkFunction
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
// src/rules/xor-used-as-pow.ts
|
|
1054
|
+
function isIntLiteral(node) {
|
|
1055
|
+
return node.type === "Literal" && typeof node.value === "number" && Number.isInteger(node.value);
|
|
1056
|
+
}
|
|
1057
|
+
var xor_used_as_pow_default = {
|
|
1058
|
+
create(context) {
|
|
1059
|
+
return {
|
|
1060
|
+
BinaryExpression(node) {
|
|
1061
|
+
if (node.operator !== "^")
|
|
1062
|
+
return;
|
|
1063
|
+
if (!isIntLiteral(node.left) || !isIntLiteral(node.right))
|
|
1064
|
+
return;
|
|
1065
|
+
const base = node.left.value;
|
|
1066
|
+
const exp = node.right.value;
|
|
1067
|
+
if (base >= 2 && exp >= 2) {
|
|
1068
|
+
context.report({
|
|
1069
|
+
message: `XOR used as pow: \`${base} ^ ${exp}\` is bitwise XOR (= ${base ^ exp}), not exponentiation. Use \`${base} ** ${exp}\` (= ${base ** exp}) or \`Math.pow(${base}, ${exp})\` instead. (clippy::suspicious_xor_used_as_pow)`,
|
|
1070
|
+
node
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// src/rules/map-identity.ts
|
|
1079
|
+
function isIdentityClosure(node) {
|
|
1080
|
+
if (node.type === "ArrowFunctionExpression" && node.params.length === 1) {
|
|
1081
|
+
const param = node.params[0];
|
|
1082
|
+
const body = node.body;
|
|
1083
|
+
if (param.type === "Identifier" && body.type === "Identifier" && body.name === param.name) {
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
if (body.type === "BlockStatement" && body.body.length === 1) {
|
|
1087
|
+
const stmt = body.body[0];
|
|
1088
|
+
if (stmt.type === "ReturnStatement" && stmt.argument?.type === "Identifier" && stmt.argument.name === param.name) {
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (node.type === "FunctionExpression" && node.params.length === 1) {
|
|
1094
|
+
const param = node.params[0];
|
|
1095
|
+
if (param.type === "Identifier" && node.body.type === "BlockStatement" && node.body.body.length === 1) {
|
|
1096
|
+
const stmt = node.body.body[0];
|
|
1097
|
+
if (stmt.type === "ReturnStatement" && stmt.argument?.type === "Identifier" && stmt.argument.name === param.name) {
|
|
1098
|
+
return true;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
var map_identity_default = {
|
|
1105
|
+
create(context) {
|
|
1106
|
+
return {
|
|
1107
|
+
CallExpression(node) {
|
|
1108
|
+
if (!isMethodCall(node, "map"))
|
|
1109
|
+
return;
|
|
1110
|
+
const args = node.arguments;
|
|
1111
|
+
if (!args || args.length !== 1)
|
|
1112
|
+
return;
|
|
1113
|
+
if (isIdentityClosure(args[0])) {
|
|
1114
|
+
context.report({
|
|
1115
|
+
message: "Map identity: `.map(x => x)` is a no-op. Remove it, or use `.slice()` / `[...arr]` to copy. (clippy::map_identity)",
|
|
1116
|
+
node
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// src/rules/redundant-closure-call.ts
|
|
1125
|
+
var redundant_closure_call_default = {
|
|
1126
|
+
create(context) {
|
|
1127
|
+
return {
|
|
1128
|
+
CallExpression(node) {
|
|
1129
|
+
const { callee } = node;
|
|
1130
|
+
if (node.arguments.length !== 0)
|
|
1131
|
+
return;
|
|
1132
|
+
let fn = callee;
|
|
1133
|
+
if (fn.type === "ArrowFunctionExpression" && fn.params.length === 0) {
|
|
1134
|
+
if (fn.body.type !== "BlockStatement") {
|
|
1135
|
+
context.report({
|
|
1136
|
+
message: "Redundant closure call: this IIFE wraps a single expression. Use the expression directly. (clippy::redundant_closure_call)",
|
|
1137
|
+
node
|
|
1138
|
+
});
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (fn.body.body.length === 1) {
|
|
1142
|
+
const stmt = fn.body.body[0];
|
|
1143
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
1144
|
+
context.report({
|
|
1145
|
+
message: "Redundant closure call: this IIFE wraps a single return. Use the expression directly. (clippy::redundant_closure_call)",
|
|
1146
|
+
node
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (fn.type === "FunctionExpression" && fn.params.length === 0 && fn.body.type === "BlockStatement" && fn.body.body.length === 1) {
|
|
1153
|
+
const stmt = fn.body.body[0];
|
|
1154
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
1155
|
+
context.report({
|
|
1156
|
+
message: "Redundant closure call: this IIFE wraps a single return. Use the expression directly. (clippy::redundant_closure_call)",
|
|
1157
|
+
node
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
// src/rules/almost-swapped.ts
|
|
1167
|
+
function assignTarget(node) {
|
|
1168
|
+
if (node.type !== "ExpressionStatement")
|
|
1169
|
+
return null;
|
|
1170
|
+
const expr = node.expression;
|
|
1171
|
+
if (expr.type !== "AssignmentExpression" || expr.operator !== "=")
|
|
1172
|
+
return null;
|
|
1173
|
+
if (expr.left.type === "Identifier")
|
|
1174
|
+
return expr.left.name;
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
function assignSource(node) {
|
|
1178
|
+
if (node.type !== "ExpressionStatement")
|
|
1179
|
+
return null;
|
|
1180
|
+
const expr = node.expression;
|
|
1181
|
+
if (expr.type !== "AssignmentExpression" || expr.operator !== "=")
|
|
1182
|
+
return null;
|
|
1183
|
+
if (expr.right.type === "Identifier")
|
|
1184
|
+
return expr.right.name;
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
function checkBody7(body, context) {
|
|
1188
|
+
for (let i = 0;i < body.length - 1; i++) {
|
|
1189
|
+
const s1 = body[i];
|
|
1190
|
+
const s2 = body[i + 1];
|
|
1191
|
+
const t1 = assignTarget(s1);
|
|
1192
|
+
const src1 = assignSource(s1);
|
|
1193
|
+
const t2 = assignTarget(s2);
|
|
1194
|
+
const src2 = assignSource(s2);
|
|
1195
|
+
if (t1 && src1 && t2 && src2 && t1 === src2 && t2 === src1 && t1 !== t2) {
|
|
1196
|
+
context.report({
|
|
1197
|
+
message: `Almost swapped: \`${t1} = ${src1}; ${t2} = ${src2};\` overwrites \`${t1}\` before saving it. Use \`[${t1}, ${t2}] = [${t2}, ${t1}]\`. (clippy::almost_swapped)`,
|
|
1198
|
+
node: s1
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
var almost_swapped_default = {
|
|
1204
|
+
create(context) {
|
|
1205
|
+
return {
|
|
1206
|
+
BlockStatement(node) {
|
|
1207
|
+
if (node.body)
|
|
1208
|
+
checkBody7(node.body, context);
|
|
1209
|
+
},
|
|
1210
|
+
Program(node) {
|
|
1211
|
+
if (node.body)
|
|
1212
|
+
checkBody7(node.body, context);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
// src/rules/if-same-then-else.ts
|
|
1219
|
+
function sourceRange(node, text) {
|
|
1220
|
+
if (node.start != null && node.end != null) {
|
|
1221
|
+
return text.slice(node.start, node.end);
|
|
1222
|
+
}
|
|
1223
|
+
return "";
|
|
1224
|
+
}
|
|
1225
|
+
var if_same_then_else_default = {
|
|
1226
|
+
create(context) {
|
|
1227
|
+
return {
|
|
1228
|
+
IfStatement(node) {
|
|
1229
|
+
if (!node.alternate)
|
|
1230
|
+
return;
|
|
1231
|
+
if (node.alternate.type === "IfStatement")
|
|
1232
|
+
return;
|
|
1233
|
+
const text = context.sourceCode.text;
|
|
1234
|
+
const consequentSrc = sourceRange(node.consequent, text);
|
|
1235
|
+
const alternateSrc = sourceRange(node.alternate, text);
|
|
1236
|
+
if (consequentSrc && consequentSrc === alternateSrc) {
|
|
1237
|
+
context.report({
|
|
1238
|
+
message: "If same then else: both branches of this if/else are identical. (clippy::if_same_then_else)",
|
|
1239
|
+
node
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
// src/rules/never-loop.ts
|
|
1248
|
+
function alwaysExits(node) {
|
|
1249
|
+
if (!node)
|
|
1250
|
+
return false;
|
|
1251
|
+
switch (node.type) {
|
|
1252
|
+
case "ReturnStatement":
|
|
1253
|
+
case "ThrowStatement":
|
|
1254
|
+
case "BreakStatement":
|
|
1255
|
+
return true;
|
|
1256
|
+
case "BlockStatement":
|
|
1257
|
+
return node.body.length > 0 && alwaysExits(node.body[node.body.length - 1]);
|
|
1258
|
+
case "IfStatement":
|
|
1259
|
+
return !!node.alternate && alwaysExits(node.consequent) && alwaysExits(node.alternate);
|
|
1260
|
+
case "ExpressionStatement":
|
|
1261
|
+
return false;
|
|
1262
|
+
default:
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function checkLoop(node, context) {
|
|
1267
|
+
const body = node.body;
|
|
1268
|
+
if (!body)
|
|
1269
|
+
return;
|
|
1270
|
+
if (alwaysExits(body)) {
|
|
1271
|
+
context.report({
|
|
1272
|
+
message: "Never loop: this loop always exits on the first iteration. Consider replacing with a conditional. (clippy::never_loop)",
|
|
1273
|
+
node
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
var never_loop_default = {
|
|
1278
|
+
create(context) {
|
|
1279
|
+
return {
|
|
1280
|
+
ForStatement: (node) => checkLoop(node, context),
|
|
1281
|
+
ForOfStatement: (node) => checkLoop(node, context),
|
|
1282
|
+
ForInStatement: (node) => checkLoop(node, context),
|
|
1283
|
+
WhileStatement: (node) => checkLoop(node, context),
|
|
1284
|
+
DoWhileStatement: (node) => checkLoop(node, context)
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// src/rules/explicit-counter-loop.ts
|
|
1290
|
+
function checkBody8(body, context) {
|
|
1291
|
+
for (let i = 0;i < body.length - 1; i++) {
|
|
1292
|
+
const decl = body[i];
|
|
1293
|
+
const loop = body[i + 1];
|
|
1294
|
+
if (decl.type !== "VariableDeclaration" || decl.declarations.length !== 1)
|
|
1295
|
+
continue;
|
|
1296
|
+
const d = decl.declarations[0];
|
|
1297
|
+
if (d.id?.type !== "Identifier" || d.init?.type !== "Literal" || d.init.value !== 0)
|
|
1298
|
+
continue;
|
|
1299
|
+
const counterName = d.id.name;
|
|
1300
|
+
if (loop.type !== "ForOfStatement")
|
|
1301
|
+
continue;
|
|
1302
|
+
if (loop.body?.type !== "BlockStatement" || loop.body.body.length === 0)
|
|
1303
|
+
continue;
|
|
1304
|
+
const stmts = loop.body.body;
|
|
1305
|
+
const last = stmts[stmts.length - 1];
|
|
1306
|
+
const isIncrement2 = last.type === "ExpressionStatement" && last.expression?.type === "UpdateExpression" && last.expression.operator === "++" && last.expression.argument?.type === "Identifier" && last.expression.argument.name === counterName || last.type === "ExpressionStatement" && last.expression?.type === "AssignmentExpression" && last.expression.operator === "+=" && last.expression.left?.type === "Identifier" && last.expression.left.name === counterName && last.expression.right?.type === "Literal" && last.expression.right.value === 1;
|
|
1307
|
+
if (isIncrement2) {
|
|
1308
|
+
context.report({
|
|
1309
|
+
message: `Explicit counter loop: manual counter \`${counterName}\` can be replaced with \`for (const [${counterName}, item] of arr.entries())\`. (clippy::explicit_counter_loop)`,
|
|
1310
|
+
node: loop
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
var explicit_counter_loop_default = {
|
|
1316
|
+
create(context) {
|
|
1317
|
+
function checkFunction(node) {
|
|
1318
|
+
const body = getFunctionBody(node);
|
|
1319
|
+
if (body)
|
|
1320
|
+
checkBody8(body, context);
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
FunctionDeclaration: checkFunction,
|
|
1324
|
+
FunctionExpression: checkFunction,
|
|
1325
|
+
ArrowFunctionExpression: checkFunction,
|
|
1326
|
+
Program(node) {
|
|
1327
|
+
if (node.body)
|
|
1328
|
+
checkBody8(node.body, context);
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
// src/rules/excessive-nesting.ts
|
|
1335
|
+
var THRESHOLD4 = 5;
|
|
1336
|
+
var NESTING_NODES = new Set([
|
|
1337
|
+
"IfStatement",
|
|
1338
|
+
"ForStatement",
|
|
1339
|
+
"ForInStatement",
|
|
1340
|
+
"ForOfStatement",
|
|
1341
|
+
"WhileStatement",
|
|
1342
|
+
"DoWhileStatement",
|
|
1343
|
+
"SwitchStatement",
|
|
1344
|
+
"TryStatement"
|
|
1345
|
+
]);
|
|
1346
|
+
var excessive_nesting_default = {
|
|
1347
|
+
create(context) {
|
|
1348
|
+
let reported = false;
|
|
1349
|
+
function walk(node, depth) {
|
|
1350
|
+
if (!node || typeof node !== "object" || !node.type)
|
|
1351
|
+
return;
|
|
1352
|
+
if (reported)
|
|
1353
|
+
return;
|
|
1354
|
+
const isNesting = NESTING_NODES.has(node.type);
|
|
1355
|
+
const newDepth = isNesting ? depth + 1 : depth;
|
|
1356
|
+
if (newDepth > THRESHOLD4 && isNesting) {
|
|
1357
|
+
reported = true;
|
|
1358
|
+
context.report({
|
|
1359
|
+
message: `Excessive nesting: this code is nested ${newDepth} levels deep (max ${THRESHOLD4}). Consider extracting into helper functions. (clippy::excessive_nesting)`,
|
|
1360
|
+
node
|
|
1361
|
+
});
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
1365
|
+
if (depth > 0)
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
for (const key of Object.keys(node)) {
|
|
1369
|
+
if (key === "type" || key === "loc" || key === "range" || key === "parent" || key === "start" || key === "end")
|
|
1370
|
+
continue;
|
|
1371
|
+
const val = node[key];
|
|
1372
|
+
if (Array.isArray(val)) {
|
|
1373
|
+
for (const child of val) {
|
|
1374
|
+
if (child && typeof child === "object" && child.type)
|
|
1375
|
+
walk(child, newDepth);
|
|
1376
|
+
}
|
|
1377
|
+
} else if (val && typeof val === "object" && val.type) {
|
|
1378
|
+
walk(val, newDepth);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return {
|
|
1383
|
+
FunctionDeclaration(node) {
|
|
1384
|
+
reported = false;
|
|
1385
|
+
walk(node.body, 0);
|
|
1386
|
+
},
|
|
1387
|
+
FunctionExpression(node) {
|
|
1388
|
+
reported = false;
|
|
1389
|
+
walk(node.body, 0);
|
|
1390
|
+
},
|
|
1391
|
+
ArrowFunctionExpression(node) {
|
|
1392
|
+
reported = false;
|
|
1393
|
+
walk(node.body, 0);
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
// src/rules/fn-params-excessive-bools.ts
|
|
1400
|
+
var THRESHOLD5 = 3;
|
|
1401
|
+
function countBoolParams(node) {
|
|
1402
|
+
const params = node.params;
|
|
1403
|
+
if (!params)
|
|
1404
|
+
return 0;
|
|
1405
|
+
let count = 0;
|
|
1406
|
+
for (const p of params) {
|
|
1407
|
+
if (p.type === "Identifier" && p.typeAnnotation) {
|
|
1408
|
+
const ann = p.typeAnnotation.typeAnnotation ?? p.typeAnnotation;
|
|
1409
|
+
if (ann.type === "TSBooleanKeyword")
|
|
1410
|
+
count++;
|
|
1411
|
+
}
|
|
1412
|
+
if (p.type === "AssignmentPattern" && p.right?.type === "Literal" && typeof p.right.value === "boolean") {
|
|
1413
|
+
count++;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return count;
|
|
1417
|
+
}
|
|
1418
|
+
function checkFunction(node, context) {
|
|
1419
|
+
const boolCount = countBoolParams(node);
|
|
1420
|
+
if (boolCount <= THRESHOLD5)
|
|
1421
|
+
return;
|
|
1422
|
+
const name = node.id?.name ?? "<anonymous>";
|
|
1423
|
+
context.report({
|
|
1424
|
+
message: `Excessive bools: \`${name}\` has ${boolCount} boolean parameters (max ${THRESHOLD5}). Consider using an options object. (clippy::fn_params_excessive_bools)`,
|
|
1425
|
+
node
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
var fn_params_excessive_bools_default = {
|
|
1429
|
+
create(context) {
|
|
1430
|
+
return {
|
|
1431
|
+
FunctionDeclaration: (node) => checkFunction(node, context),
|
|
1432
|
+
FunctionExpression: (node) => checkFunction(node, context),
|
|
1433
|
+
ArrowFunctionExpression: (node) => checkFunction(node, context)
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
// src/rules/float-equality-without-abs.ts
|
|
1439
|
+
function isEpsilonLike(node) {
|
|
1440
|
+
if (node.type === "MemberExpression" && node.object?.type === "Identifier" && node.object.name === "Number" && node.property?.type === "Identifier" && node.property.name === "EPSILON")
|
|
1441
|
+
return true;
|
|
1442
|
+
if (node.type === "Literal" && typeof node.value === "number" && node.value > 0 && node.value < 1)
|
|
1443
|
+
return true;
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
function isSubtraction(node) {
|
|
1447
|
+
return node.type === "BinaryExpression" && node.operator === "-";
|
|
1448
|
+
}
|
|
1449
|
+
var float_equality_without_abs_default = {
|
|
1450
|
+
create(context) {
|
|
1451
|
+
return {
|
|
1452
|
+
BinaryExpression(node) {
|
|
1453
|
+
if (node.operator !== "<" && node.operator !== "<=")
|
|
1454
|
+
return;
|
|
1455
|
+
if (isSubtraction(node.left) && isEpsilonLike(node.right)) {
|
|
1456
|
+
context.report({
|
|
1457
|
+
message: "Float equality without abs: `(a - b) < epsilon` fails when a < b. Use `Math.abs(a - b) < epsilon`. (clippy::float_equality_without_abs)",
|
|
1458
|
+
node
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
// src/rules/manual-is-finite.ts
|
|
1467
|
+
function isInfinityCheck(node, operator) {
|
|
1468
|
+
if (node.type !== "BinaryExpression" || node.operator !== operator)
|
|
1469
|
+
return false;
|
|
1470
|
+
return isInfinityLiteral(node.right) || isInfinityLiteral(node.left);
|
|
1471
|
+
}
|
|
1472
|
+
function isInfinityLiteral(node) {
|
|
1473
|
+
if (node.type === "Identifier" && node.name === "Infinity")
|
|
1474
|
+
return true;
|
|
1475
|
+
if (node.type === "UnaryExpression" && node.operator === "-" && node.argument?.type === "Identifier" && node.argument.name === "Infinity")
|
|
1476
|
+
return true;
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
var manual_is_finite_default = {
|
|
1480
|
+
create(context) {
|
|
1481
|
+
return {
|
|
1482
|
+
LogicalExpression(node) {
|
|
1483
|
+
if (node.operator === "&&" && isInfinityCheck(node.left, "!==") && isInfinityCheck(node.right, "!==")) {
|
|
1484
|
+
context.report({
|
|
1485
|
+
message: "Manual isFinite: this pattern checks for finiteness manually. Use `Number.isFinite(x)` instead. (clippy::manual_is_finite)",
|
|
1486
|
+
node
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
if (node.operator === "||" && isInfinityCheck(node.left, "===") && isInfinityCheck(node.right, "===")) {
|
|
1490
|
+
context.report({
|
|
1491
|
+
message: "Manual isInfinite: this pattern checks for infinity manually. Use `!Number.isFinite(x)` instead. (clippy::manual_is_infinite)",
|
|
1492
|
+
node
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
// src/rules/unnecessary-fold.ts
|
|
1501
|
+
var unnecessary_fold_default = {
|
|
1502
|
+
create(context) {
|
|
1503
|
+
return {
|
|
1504
|
+
CallExpression(node) {
|
|
1505
|
+
if (!isMethodCall(node, "reduce"))
|
|
1506
|
+
return;
|
|
1507
|
+
const args = node.arguments;
|
|
1508
|
+
if (!args || args.length !== 2)
|
|
1509
|
+
return;
|
|
1510
|
+
const callback = args[0];
|
|
1511
|
+
const initial = args[1];
|
|
1512
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")
|
|
1513
|
+
return;
|
|
1514
|
+
if (callback.params.length !== 2)
|
|
1515
|
+
return;
|
|
1516
|
+
const accParam = callback.params[0];
|
|
1517
|
+
if (accParam.type !== "Identifier")
|
|
1518
|
+
return;
|
|
1519
|
+
const accName = accParam.name;
|
|
1520
|
+
let bodyExpr = null;
|
|
1521
|
+
if (callback.body.type === "BlockStatement") {
|
|
1522
|
+
if (callback.body.body.length === 1 && callback.body.body[0].type === "ReturnStatement") {
|
|
1523
|
+
bodyExpr = callback.body.body[0].argument;
|
|
1524
|
+
}
|
|
1525
|
+
} else {
|
|
1526
|
+
bodyExpr = callback.body;
|
|
1527
|
+
}
|
|
1528
|
+
if (!bodyExpr || bodyExpr.type !== "LogicalExpression")
|
|
1529
|
+
return;
|
|
1530
|
+
if (bodyExpr.operator === "||" && isLiteral(initial, false)) {
|
|
1531
|
+
if (bodyExpr.left.type === "Identifier" && bodyExpr.left.name === accName) {
|
|
1532
|
+
context.report({
|
|
1533
|
+
message: "Unnecessary fold: this `.reduce()` with `||` and initial `false` can be replaced with `.some()`. (clippy::unnecessary_fold)",
|
|
1534
|
+
node
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
if (bodyExpr.operator === "&&" && isLiteral(initial, true)) {
|
|
1539
|
+
if (bodyExpr.left.type === "Identifier" && bodyExpr.left.name === accName) {
|
|
1540
|
+
context.report({
|
|
1541
|
+
message: "Unnecessary fold: this `.reduce()` with `&&` and initial `true` can be replaced with `.every()`. (clippy::unnecessary_fold)",
|
|
1542
|
+
node
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
// src/rules/needless-late-init.ts
|
|
1552
|
+
function checkBody9(body, context) {
|
|
1553
|
+
for (let i = 0;i < body.length - 1; i++) {
|
|
1554
|
+
const decl = body[i];
|
|
1555
|
+
const next = body[i + 1];
|
|
1556
|
+
if (decl.type !== "VariableDeclaration" || decl.declarations.length !== 1)
|
|
1557
|
+
continue;
|
|
1558
|
+
const d = decl.declarations[0];
|
|
1559
|
+
if (d.id?.type !== "Identifier" || d.init !== null)
|
|
1560
|
+
continue;
|
|
1561
|
+
const varName = d.id.name;
|
|
1562
|
+
if (next.type === "ExpressionStatement" && next.expression?.type === "AssignmentExpression" && next.expression.operator === "=" && next.expression.left?.type === "Identifier" && next.expression.left.name === varName) {
|
|
1563
|
+
context.report({
|
|
1564
|
+
message: `Needless late init: \`${varName}\` is declared then immediately assigned. Initialize on declaration with \`const\`. (clippy::needless_late_init)`,
|
|
1565
|
+
node: decl
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
var needless_late_init_default = {
|
|
1571
|
+
create(context) {
|
|
1572
|
+
return {
|
|
1573
|
+
BlockStatement(node) {
|
|
1574
|
+
if (node.body)
|
|
1575
|
+
checkBody9(node.body, context);
|
|
1576
|
+
},
|
|
1577
|
+
Program(node) {
|
|
1578
|
+
if (node.body)
|
|
1579
|
+
checkBody9(node.body, context);
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
// src/rules/single-element-loop.ts
|
|
1586
|
+
var single_element_loop_default = {
|
|
1587
|
+
create(context) {
|
|
1588
|
+
return {
|
|
1589
|
+
ForOfStatement(node) {
|
|
1590
|
+
const right = node.right;
|
|
1591
|
+
if (right?.type === "ArrayExpression" && right.elements?.length === 1) {
|
|
1592
|
+
context.report({
|
|
1593
|
+
message: "Single element loop: this loop iterates over a single-element array. Use the value directly. (clippy::single_element_loop)",
|
|
1594
|
+
node
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
// src/rules/int-plus-one.ts
|
|
1603
|
+
var int_plus_one_default = {
|
|
1604
|
+
create(context) {
|
|
1605
|
+
return {
|
|
1606
|
+
BinaryExpression(node) {
|
|
1607
|
+
const { operator, left, right } = node;
|
|
1608
|
+
if (operator === ">=" && right.type === "BinaryExpression" && right.operator === "+" && isLiteral(right.right, 1)) {
|
|
1609
|
+
context.report({
|
|
1610
|
+
message: "Int plus one: `x >= y + 1` can be simplified to `x > y`. (clippy::int_plus_one)",
|
|
1611
|
+
node
|
|
1612
|
+
});
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
if (operator === "<=" && left.type === "BinaryExpression" && left.operator === "+" && isLiteral(left.right, 1)) {
|
|
1616
|
+
context.report({
|
|
1617
|
+
message: "Int plus one: `x + 1 <= y` can be simplified to `x < y`. (clippy::int_plus_one)",
|
|
1618
|
+
node
|
|
1619
|
+
});
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (operator === ">=" && left.type === "BinaryExpression" && left.operator === "-" && isLiteral(left.right, 1)) {
|
|
1623
|
+
context.report({
|
|
1624
|
+
message: "Int plus one: `x - 1 >= y` can be simplified to `x > y`. (clippy::int_plus_one)",
|
|
1625
|
+
node
|
|
1626
|
+
});
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
if (operator === "<=" && right.type === "BinaryExpression" && right.operator === "-" && isLiteral(right.right, 1)) {
|
|
1630
|
+
context.report({
|
|
1631
|
+
message: "Int plus one: `y <= x - 1` can be simplified to `y < x`. (clippy::int_plus_one)",
|
|
1632
|
+
node
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
// src/rules/zero-divided-by-zero.ts
|
|
1641
|
+
function isZero(node) {
|
|
1642
|
+
return node.type === "Literal" && (node.value === 0 || node.value === 0);
|
|
1643
|
+
}
|
|
1644
|
+
var zero_divided_by_zero_default = {
|
|
1645
|
+
create(context) {
|
|
1646
|
+
return {
|
|
1647
|
+
BinaryExpression(node) {
|
|
1648
|
+
if (node.operator === "/" && isZero(node.left) && isZero(node.right)) {
|
|
1649
|
+
context.report({
|
|
1650
|
+
message: "Zero divided by zero: `0 / 0` is `NaN`. Use `NaN` directly if intentional. (clippy::zero_divided_by_zero)",
|
|
1651
|
+
node
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
// src/rules/redundant-closure.ts
|
|
1660
|
+
var SAFE_SINGLE_ARG = new Set([
|
|
1661
|
+
"String",
|
|
1662
|
+
"Number",
|
|
1663
|
+
"Boolean",
|
|
1664
|
+
"BigInt",
|
|
1665
|
+
"parseFloat",
|
|
1666
|
+
"isNaN",
|
|
1667
|
+
"isFinite",
|
|
1668
|
+
"encodeURIComponent",
|
|
1669
|
+
"decodeURIComponent",
|
|
1670
|
+
"encodeURI",
|
|
1671
|
+
"decodeURI"
|
|
1672
|
+
]);
|
|
1673
|
+
var redundant_closure_default = {
|
|
1674
|
+
create(context) {
|
|
1675
|
+
return {
|
|
1676
|
+
CallExpression(node) {
|
|
1677
|
+
const callee = node.callee;
|
|
1678
|
+
if (callee.type !== "MemberExpression")
|
|
1679
|
+
return;
|
|
1680
|
+
const args = node.arguments;
|
|
1681
|
+
if (!args || args.length !== 1)
|
|
1682
|
+
return;
|
|
1683
|
+
const callback = args[0];
|
|
1684
|
+
let paramName = null;
|
|
1685
|
+
let bodyExpr = null;
|
|
1686
|
+
if (callback.type === "ArrowFunctionExpression" && callback.params.length === 1) {
|
|
1687
|
+
const param = callback.params[0];
|
|
1688
|
+
if (param.type !== "Identifier")
|
|
1689
|
+
return;
|
|
1690
|
+
paramName = param.name;
|
|
1691
|
+
if (callback.body.type === "BlockStatement" && callback.body.body.length === 1) {
|
|
1692
|
+
const stmt = callback.body.body[0];
|
|
1693
|
+
if (stmt.type === "ReturnStatement")
|
|
1694
|
+
bodyExpr = stmt.argument;
|
|
1695
|
+
} else if (callback.body.type !== "BlockStatement") {
|
|
1696
|
+
bodyExpr = callback.body;
|
|
1697
|
+
}
|
|
1698
|
+
} else if (callback.type === "FunctionExpression" && callback.params.length === 1) {
|
|
1699
|
+
const param = callback.params[0];
|
|
1700
|
+
if (param.type !== "Identifier")
|
|
1701
|
+
return;
|
|
1702
|
+
paramName = param.name;
|
|
1703
|
+
if (callback.body.type === "BlockStatement" && callback.body.body.length === 1) {
|
|
1704
|
+
const stmt = callback.body.body[0];
|
|
1705
|
+
if (stmt.type === "ReturnStatement")
|
|
1706
|
+
bodyExpr = stmt.argument;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if (!paramName || !bodyExpr)
|
|
1710
|
+
return;
|
|
1711
|
+
if (bodyExpr.type !== "CallExpression")
|
|
1712
|
+
return;
|
|
1713
|
+
if (bodyExpr.arguments.length !== 1)
|
|
1714
|
+
return;
|
|
1715
|
+
const innerArg = bodyExpr.arguments[0];
|
|
1716
|
+
if (!isIdentifier(innerArg, paramName))
|
|
1717
|
+
return;
|
|
1718
|
+
const fnCallee = bodyExpr.callee;
|
|
1719
|
+
if (isIdentifier(fnCallee) && SAFE_SINGLE_ARG.has(fnCallee.name)) {
|
|
1720
|
+
context.report({
|
|
1721
|
+
message: `Redundant closure: \`x => ${fnCallee.name}(x)\` can be simplified to \`${fnCallee.name}\`. (clippy::redundant_closure)`,
|
|
1722
|
+
node: callback
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
// src/rules/unnecessary-reduce-collect.ts
|
|
1731
|
+
function isEmptyArray(node) {
|
|
1732
|
+
return node.type === "ArrayExpression" && (!node.elements || node.elements.length === 0);
|
|
1733
|
+
}
|
|
1734
|
+
var unnecessary_reduce_collect_default = {
|
|
1735
|
+
create(context) {
|
|
1736
|
+
return {
|
|
1737
|
+
CallExpression(node) {
|
|
1738
|
+
if (!isMethodCall(node, "reduce"))
|
|
1739
|
+
return;
|
|
1740
|
+
const args = node.arguments;
|
|
1741
|
+
if (!args || args.length !== 2)
|
|
1742
|
+
return;
|
|
1743
|
+
const callback = args[0];
|
|
1744
|
+
const init = args[1];
|
|
1745
|
+
if (!isEmptyArray(init))
|
|
1746
|
+
return;
|
|
1747
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")
|
|
1748
|
+
return;
|
|
1749
|
+
if (callback.params.length !== 2)
|
|
1750
|
+
return;
|
|
1751
|
+
const accParam = callback.params[0];
|
|
1752
|
+
if (accParam.type !== "Identifier")
|
|
1753
|
+
return;
|
|
1754
|
+
const accName = accParam.name;
|
|
1755
|
+
let bodyStmts = null;
|
|
1756
|
+
if (callback.body.type === "BlockStatement") {
|
|
1757
|
+
bodyStmts = callback.body.body;
|
|
1758
|
+
}
|
|
1759
|
+
if (!bodyStmts)
|
|
1760
|
+
return;
|
|
1761
|
+
if (bodyStmts.length === 2) {
|
|
1762
|
+
const pushStmt = bodyStmts[0];
|
|
1763
|
+
const returnStmt = bodyStmts[1];
|
|
1764
|
+
if (returnStmt.type !== "ReturnStatement")
|
|
1765
|
+
return;
|
|
1766
|
+
if (returnStmt.argument?.type !== "Identifier" || returnStmt.argument.name !== accName)
|
|
1767
|
+
return;
|
|
1768
|
+
if (pushStmt.type === "ExpressionStatement" && pushStmt.expression?.type === "CallExpression" && pushStmt.expression.callee?.type === "MemberExpression" && pushStmt.expression.callee.object?.type === "Identifier" && pushStmt.expression.callee.object.name === accName && pushStmt.expression.callee.property?.type === "Identifier" && pushStmt.expression.callee.property.name === "push" && pushStmt.expression.arguments?.length === 1) {
|
|
1769
|
+
context.report({
|
|
1770
|
+
message: "Unnecessary reduce: this `.reduce()` builds an array with `.push()`. Use `.map()` instead. (clippy: prefer combinators)",
|
|
1771
|
+
node
|
|
1772
|
+
});
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
if (bodyStmts.length === 2) {
|
|
1777
|
+
const ifStmt = bodyStmts[0];
|
|
1778
|
+
const returnStmt = bodyStmts[1];
|
|
1779
|
+
if (returnStmt.type !== "ReturnStatement")
|
|
1780
|
+
return;
|
|
1781
|
+
if (returnStmt.argument?.type !== "Identifier" || returnStmt.argument.name !== accName)
|
|
1782
|
+
return;
|
|
1783
|
+
if (ifStmt.type === "IfStatement" && !ifStmt.alternate) {
|
|
1784
|
+
const consequent = ifStmt.consequent;
|
|
1785
|
+
let pushExpr = null;
|
|
1786
|
+
if (consequent.type === "BlockStatement" && consequent.body.length === 1) {
|
|
1787
|
+
pushExpr = consequent.body[0];
|
|
1788
|
+
} else if (consequent.type === "ExpressionStatement") {
|
|
1789
|
+
pushExpr = consequent;
|
|
1790
|
+
}
|
|
1791
|
+
if (pushExpr?.type === "ExpressionStatement" && pushExpr.expression?.type === "CallExpression" && pushExpr.expression.callee?.type === "MemberExpression" && pushExpr.expression.callee.object?.type === "Identifier" && pushExpr.expression.callee.object.name === accName && pushExpr.expression.callee.property?.type === "Identifier" && pushExpr.expression.callee.property.name === "push") {
|
|
1792
|
+
context.report({
|
|
1793
|
+
message: "Unnecessary reduce: this `.reduce()` conditionally pushes items. Use `.filter()` instead. (clippy: prefer combinators)",
|
|
1794
|
+
node
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
// src/rules/prefer-structured-clone.ts
|
|
1805
|
+
var prefer_structured_clone_default = {
|
|
1806
|
+
create(context) {
|
|
1807
|
+
return {
|
|
1808
|
+
CallExpression(node) {
|
|
1809
|
+
if (!isCallOf(node, "JSON", "parse"))
|
|
1810
|
+
return;
|
|
1811
|
+
const args = node.arguments;
|
|
1812
|
+
if (!args || args.length !== 1)
|
|
1813
|
+
return;
|
|
1814
|
+
const inner = args[0];
|
|
1815
|
+
if (isCallOf(inner, "JSON", "stringify") && inner.arguments?.length === 1) {
|
|
1816
|
+
context.report({
|
|
1817
|
+
message: "Prefer structuredClone: `JSON.parse(JSON.stringify(x))` can be replaced with `structuredClone(x)`, which handles more types and is more correct. (clippy: don't reinvent stdlib)",
|
|
1818
|
+
node
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
// src/rules/object-keys-values.ts
|
|
1827
|
+
var object_keys_values_default = {
|
|
1828
|
+
create(context) {
|
|
1829
|
+
return {
|
|
1830
|
+
CallExpression(node) {
|
|
1831
|
+
const callee = node.callee;
|
|
1832
|
+
if (callee.type !== "MemberExpression")
|
|
1833
|
+
return;
|
|
1834
|
+
const methodName = callee.property;
|
|
1835
|
+
if (methodName?.type !== "Identifier")
|
|
1836
|
+
return;
|
|
1837
|
+
if (methodName.name !== "map" && methodName.name !== "forEach")
|
|
1838
|
+
return;
|
|
1839
|
+
const receiver = callee.object;
|
|
1840
|
+
if (!isCallOf(receiver, "Object", "keys"))
|
|
1841
|
+
return;
|
|
1842
|
+
if (!receiver.arguments || receiver.arguments.length !== 1)
|
|
1843
|
+
return;
|
|
1844
|
+
const objArg = receiver.arguments[0];
|
|
1845
|
+
if (objArg.type !== "Identifier")
|
|
1846
|
+
return;
|
|
1847
|
+
const objName = objArg.name;
|
|
1848
|
+
const args = node.arguments;
|
|
1849
|
+
if (!args || args.length !== 1)
|
|
1850
|
+
return;
|
|
1851
|
+
const callback = args[0];
|
|
1852
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")
|
|
1853
|
+
return;
|
|
1854
|
+
if (callback.params.length !== 1)
|
|
1855
|
+
return;
|
|
1856
|
+
const param = callback.params[0];
|
|
1857
|
+
if (param.type !== "Identifier")
|
|
1858
|
+
return;
|
|
1859
|
+
const keyName = param.name;
|
|
1860
|
+
const bodyStr = context.sourceCode.text.slice(callback.body.start, callback.body.end);
|
|
1861
|
+
const indexPattern = `${objName}[${keyName}]`;
|
|
1862
|
+
if (bodyStr.includes(indexPattern)) {
|
|
1863
|
+
const stripped = bodyStr.replace(new RegExp(`${objName}\\[${keyName}\\]`, "g"), "");
|
|
1864
|
+
const keyStillUsed = new RegExp(`\\b${keyName}\\b`).test(stripped);
|
|
1865
|
+
if (keyStillUsed) {
|
|
1866
|
+
context.report({
|
|
1867
|
+
message: `Object keys+values: \`Object.keys(${objName}).${methodName.name}(${keyName} => ... ${objName}[${keyName}] ...)\` can use \`Object.entries(${objName})\` for clearer access to both key and value. (clippy: for_kv_map)`,
|
|
1868
|
+
node
|
|
1869
|
+
});
|
|
1870
|
+
} else {
|
|
1871
|
+
context.report({
|
|
1872
|
+
message: `Object keys→values: \`Object.keys(${objName}).${methodName.name}(${keyName} => ... ${objName}[${keyName}] ...)\` can be simplified to \`Object.values(${objName}).${methodName.name}(...)\`. (clippy: for_kv_map)`,
|
|
1873
|
+
node
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
|
|
1882
|
+
// src/rules/promise-new-resolve.ts
|
|
1883
|
+
var promise_new_resolve_default = {
|
|
1884
|
+
create(context) {
|
|
1885
|
+
return {
|
|
1886
|
+
NewExpression(node) {
|
|
1887
|
+
if (!isIdentifier(node.callee, "Promise"))
|
|
1888
|
+
return;
|
|
1889
|
+
if (!node.arguments || node.arguments.length !== 1)
|
|
1890
|
+
return;
|
|
1891
|
+
const callback = node.arguments[0];
|
|
1892
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")
|
|
1893
|
+
return;
|
|
1894
|
+
if (callback.params.length < 1)
|
|
1895
|
+
return;
|
|
1896
|
+
const resolveParam = callback.params[0];
|
|
1897
|
+
const rejectParam = callback.params[1];
|
|
1898
|
+
let bodyExpr = null;
|
|
1899
|
+
if (callback.body.type === "BlockStatement" && callback.body.body.length === 1) {
|
|
1900
|
+
const stmt = callback.body.body[0];
|
|
1901
|
+
if (stmt.type === "ExpressionStatement")
|
|
1902
|
+
bodyExpr = stmt.expression;
|
|
1903
|
+
} else if (callback.body.type !== "BlockStatement") {
|
|
1904
|
+
bodyExpr = callback.body;
|
|
1905
|
+
}
|
|
1906
|
+
if (!bodyExpr || bodyExpr.type !== "CallExpression")
|
|
1907
|
+
return;
|
|
1908
|
+
if (bodyExpr.arguments.length > 1)
|
|
1909
|
+
return;
|
|
1910
|
+
const calledFn = bodyExpr.callee;
|
|
1911
|
+
if (calledFn.type !== "Identifier")
|
|
1912
|
+
return;
|
|
1913
|
+
if (resolveParam.type === "Identifier" && calledFn.name === resolveParam.name) {
|
|
1914
|
+
context.report({
|
|
1915
|
+
message: "Promise constructor wrapping sync value: `new Promise(resolve => resolve(x))` can be simplified to `Promise.resolve(x)`. (clippy: avoid error-prone constructors)",
|
|
1916
|
+
node
|
|
1917
|
+
});
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
if (rejectParam?.type === "Identifier" && calledFn.name === rejectParam.name) {
|
|
1921
|
+
context.report({
|
|
1922
|
+
message: "Promise constructor wrapping sync rejection: `new Promise((_, reject) => reject(x))` can be simplified to `Promise.reject(x)`. (clippy: avoid error-prone constructors)",
|
|
1923
|
+
node
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
// src/rules/similar-names.ts
|
|
1932
|
+
var ALLOWED_PAIRS = new Set([
|
|
1933
|
+
"i,j",
|
|
1934
|
+
"j,k",
|
|
1935
|
+
"i,k",
|
|
1936
|
+
"x,y",
|
|
1937
|
+
"y,z",
|
|
1938
|
+
"x,z",
|
|
1939
|
+
"a,b",
|
|
1940
|
+
"b,c",
|
|
1941
|
+
"a,c",
|
|
1942
|
+
"n,m",
|
|
1943
|
+
"r,s"
|
|
1944
|
+
]);
|
|
1945
|
+
function areSimilar(a, b) {
|
|
1946
|
+
if (Math.abs(a.length - b.length) > 1)
|
|
1947
|
+
return false;
|
|
1948
|
+
if (a.length === b.length) {
|
|
1949
|
+
let diffs2 = 0;
|
|
1950
|
+
let firstDiff = -1;
|
|
1951
|
+
for (let i = 0;i < a.length; i++) {
|
|
1952
|
+
if (a[i] !== b[i]) {
|
|
1953
|
+
if (diffs2 === 0)
|
|
1954
|
+
firstDiff = i;
|
|
1955
|
+
diffs2++;
|
|
1956
|
+
if (diffs2 > 2)
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
if (diffs2 <= 1)
|
|
1961
|
+
return diffs2 === 1;
|
|
1962
|
+
if (diffs2 === 2 && a[firstDiff] === b[firstDiff + 1] && a[firstDiff + 1] === b[firstDiff])
|
|
1963
|
+
return true;
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
const [shorter, longer] = a.length < b.length ? [a, b] : [b, a];
|
|
1967
|
+
let diffs = 0;
|
|
1968
|
+
let si = 0;
|
|
1969
|
+
for (let li = 0;li < longer.length; li++) {
|
|
1970
|
+
if (shorter[si] === longer[li]) {
|
|
1971
|
+
si++;
|
|
1972
|
+
} else {
|
|
1973
|
+
diffs++;
|
|
1974
|
+
if (diffs > 1)
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|
|
1980
|
+
function collectNames(node, names) {
|
|
1981
|
+
if (!node || typeof node !== "object" || !node.type)
|
|
1982
|
+
return;
|
|
1983
|
+
if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
|
|
1984
|
+
names.set(node.id.name, node.id);
|
|
1985
|
+
}
|
|
1986
|
+
if (node.type === "FunctionDeclaration" && node.id?.type === "Identifier") {
|
|
1987
|
+
names.set(node.id.name, node.id);
|
|
1988
|
+
}
|
|
1989
|
+
if (node.params) {
|
|
1990
|
+
for (const p of node.params) {
|
|
1991
|
+
if (p.type === "Identifier")
|
|
1992
|
+
names.set(p.name, p);
|
|
1993
|
+
if (p.type === "AssignmentPattern" && p.left?.type === "Identifier")
|
|
1994
|
+
names.set(p.left.name, p.left);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")
|
|
1998
|
+
return;
|
|
1999
|
+
for (const key of Object.keys(node)) {
|
|
2000
|
+
if (key === "type" || key === "loc" || key === "range" || key === "parent" || key === "start" || key === "end")
|
|
2001
|
+
continue;
|
|
2002
|
+
const val = node[key];
|
|
2003
|
+
if (Array.isArray(val)) {
|
|
2004
|
+
for (const child of val) {
|
|
2005
|
+
if (child && typeof child === "object" && child.type)
|
|
2006
|
+
collectNames(child, names);
|
|
2007
|
+
}
|
|
2008
|
+
} else if (val && typeof val === "object" && val.type) {
|
|
2009
|
+
collectNames(val, names);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
var similar_names_default = {
|
|
2014
|
+
create(context) {
|
|
2015
|
+
function checkScope(node) {
|
|
2016
|
+
const names = new Map;
|
|
2017
|
+
if (node.params) {
|
|
2018
|
+
for (const p of node.params) {
|
|
2019
|
+
if (p.type === "Identifier")
|
|
2020
|
+
names.set(p.name, p);
|
|
2021
|
+
if (p.type === "AssignmentPattern" && p.left?.type === "Identifier")
|
|
2022
|
+
names.set(p.left.name, p.left);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
if (node.body?.type === "BlockStatement") {
|
|
2026
|
+
for (const stmt of node.body.body) {
|
|
2027
|
+
collectNames(stmt, names);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
const nameList = [...names.entries()];
|
|
2031
|
+
const reported = new Set;
|
|
2032
|
+
for (let i = 0;i < nameList.length; i++) {
|
|
2033
|
+
for (let j = i + 1;j < nameList.length; j++) {
|
|
2034
|
+
const [nameA] = nameList[i];
|
|
2035
|
+
const [nameB, nodeB] = nameList[j];
|
|
2036
|
+
if (nameA.length < 3 || nameB.length < 3)
|
|
2037
|
+
continue;
|
|
2038
|
+
const pair = [nameA, nameB].sort().join(",");
|
|
2039
|
+
if (ALLOWED_PAIRS.has(pair))
|
|
2040
|
+
continue;
|
|
2041
|
+
if (reported.has(pair))
|
|
2042
|
+
continue;
|
|
2043
|
+
if (areSimilar(nameA.toLowerCase(), nameB.toLowerCase())) {
|
|
2044
|
+
reported.add(pair);
|
|
2045
|
+
context.report({
|
|
2046
|
+
message: `Similar names: \`${nameA}\` and \`${nameB}\` differ by only one character, which is easy to confuse. (clippy::similar_names)`,
|
|
2047
|
+
node: nodeB
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return {
|
|
2054
|
+
FunctionDeclaration: checkScope,
|
|
2055
|
+
FunctionExpression: checkScope,
|
|
2056
|
+
ArrowFunctionExpression: checkScope
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
|
|
2061
|
+
// src/rules/match-same-arms.ts
|
|
2062
|
+
function caseBodySource(caseNode, sourceText) {
|
|
2063
|
+
const stmts = caseNode.consequent;
|
|
2064
|
+
if (!stmts || stmts.length === 0)
|
|
2065
|
+
return "";
|
|
2066
|
+
const meaningful = stmts.filter((s) => s.type !== "BreakStatement");
|
|
2067
|
+
if (meaningful.length === 0)
|
|
2068
|
+
return "";
|
|
2069
|
+
const first = meaningful[0];
|
|
2070
|
+
const last = meaningful[meaningful.length - 1];
|
|
2071
|
+
if (first.start == null || last.end == null)
|
|
2072
|
+
return "";
|
|
2073
|
+
return sourceText.slice(first.start, last.end);
|
|
2074
|
+
}
|
|
2075
|
+
var match_same_arms_default = {
|
|
2076
|
+
create(context) {
|
|
2077
|
+
return {
|
|
2078
|
+
SwitchStatement(node) {
|
|
2079
|
+
const cases = node.cases;
|
|
2080
|
+
if (!cases || cases.length < 2)
|
|
2081
|
+
return;
|
|
2082
|
+
const seen = new Map;
|
|
2083
|
+
for (const c of cases) {
|
|
2084
|
+
if (c.test === null)
|
|
2085
|
+
continue;
|
|
2086
|
+
const body = caseBodySource(c, context.sourceCode.text);
|
|
2087
|
+
if (!body)
|
|
2088
|
+
continue;
|
|
2089
|
+
const existing = seen.get(body);
|
|
2090
|
+
if (existing) {
|
|
2091
|
+
context.report({
|
|
2092
|
+
message: "Match same arms: multiple switch cases have identical bodies. Consider combining them. (clippy::match_same_arms)",
|
|
2093
|
+
node: c
|
|
2094
|
+
});
|
|
2095
|
+
} else {
|
|
2096
|
+
seen.set(body, c);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
// src/rules/used-underscore-binding.ts
|
|
2105
|
+
var used_underscore_binding_default = {
|
|
2106
|
+
create(context) {
|
|
2107
|
+
const declared = new Map;
|
|
2108
|
+
const used = new Set;
|
|
2109
|
+
return {
|
|
2110
|
+
VariableDeclarator(node) {
|
|
2111
|
+
if (node.id?.type === "Identifier" && node.id.name.startsWith("_") && node.id.name !== "_") {
|
|
2112
|
+
declared.set(node.id.name, node.id);
|
|
2113
|
+
}
|
|
2114
|
+
},
|
|
2115
|
+
Identifier(node) {
|
|
2116
|
+
if (node.name?.startsWith("_") && node.name !== "_") {
|
|
2117
|
+
if (node.parent?.type !== "VariableDeclarator" || node.parent.id !== node) {
|
|
2118
|
+
used.add(node.name);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
"Program:exit"() {
|
|
2123
|
+
for (const [name, declNode] of declared) {
|
|
2124
|
+
if (used.has(name)) {
|
|
2125
|
+
context.report({
|
|
2126
|
+
message: `Used underscore binding: \`${name}\` starts with \`_\` (conventionally unused) but is actually used. Remove the underscore prefix. (clippy::used_underscore_binding)`,
|
|
2127
|
+
node: declNode
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
// src/rules/needless-continue.ts
|
|
2137
|
+
function checkLoopBody(body, context) {
|
|
2138
|
+
if (body.type !== "BlockStatement" || body.body.length === 0)
|
|
2139
|
+
return;
|
|
2140
|
+
const stmts = body.body;
|
|
2141
|
+
const last = stmts[stmts.length - 1];
|
|
2142
|
+
if (last.type === "ContinueStatement" && !last.label) {
|
|
2143
|
+
context.report({
|
|
2144
|
+
message: "Needless continue: `continue` at the end of a loop body has no effect. (clippy::needless_continue)",
|
|
2145
|
+
node: last
|
|
2146
|
+
});
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
if (last.type === "IfStatement" && last.alternate) {
|
|
2150
|
+
const alt = last.alternate;
|
|
2151
|
+
const altBody = alt.type === "BlockStatement" ? alt.body : [alt];
|
|
2152
|
+
if (altBody.length === 1 && altBody[0].type === "ContinueStatement" && !altBody[0].label) {
|
|
2153
|
+
context.report({
|
|
2154
|
+
message: "Needless continue: `else { continue; }` at the end of a loop is redundant. Remove the else branch. (clippy::needless_continue)",
|
|
2155
|
+
node: altBody[0]
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
var needless_continue_default = {
|
|
2161
|
+
create(context) {
|
|
2162
|
+
return {
|
|
2163
|
+
ForStatement: (node) => checkLoopBody(node.body, context),
|
|
2164
|
+
ForOfStatement: (node) => checkLoopBody(node.body, context),
|
|
2165
|
+
ForInStatement: (node) => checkLoopBody(node.body, context),
|
|
2166
|
+
WhileStatement: (node) => checkLoopBody(node.body, context),
|
|
2167
|
+
DoWhileStatement: (node) => checkLoopBody(node.body, context)
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
|
|
2172
|
+
// src/rules/enum-variant-names.ts
|
|
2173
|
+
var enum_variant_names_default = {
|
|
2174
|
+
create(context) {
|
|
2175
|
+
return {
|
|
2176
|
+
TSEnumDeclaration(node) {
|
|
2177
|
+
const enumName = node.id?.name;
|
|
2178
|
+
if (!enumName)
|
|
2179
|
+
return;
|
|
2180
|
+
const members = node.members ?? node.body?.members;
|
|
2181
|
+
if (!members || members.length < 2)
|
|
2182
|
+
return;
|
|
2183
|
+
const names = members.map((m) => m.id?.type === "Identifier" ? m.id.name : null).filter((n) => n !== null);
|
|
2184
|
+
if (names.length < 2)
|
|
2185
|
+
return;
|
|
2186
|
+
const lowerEnum = enumName.toLowerCase();
|
|
2187
|
+
const allPrefixed = names.every((n) => n.toLowerCase().startsWith(lowerEnum));
|
|
2188
|
+
if (allPrefixed) {
|
|
2189
|
+
context.report({
|
|
2190
|
+
message: `Enum variant names: all members of \`${enumName}\` are prefixed with \`${enumName}\`. Remove the redundant prefix. (clippy::enum_variant_names)`,
|
|
2191
|
+
node
|
|
2192
|
+
});
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
const allSuffixed = names.every((n) => n.toLowerCase().endsWith(lowerEnum));
|
|
2196
|
+
if (allSuffixed) {
|
|
2197
|
+
context.report({
|
|
2198
|
+
message: `Enum variant names: all members of \`${enumName}\` are suffixed with \`${enumName}\`. Remove the redundant suffix. (clippy::enum_variant_names)`,
|
|
2199
|
+
node
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
// src/rules/struct-field-names.ts
|
|
2208
|
+
function getFieldNames(node) {
|
|
2209
|
+
const body = node.body?.body ?? node.body?.members ?? node.members;
|
|
2210
|
+
if (!Array.isArray(body))
|
|
2211
|
+
return [];
|
|
2212
|
+
return body.map((m) => {
|
|
2213
|
+
if (m.type === "TSPropertySignature" || m.type === "PropertyDefinition") {
|
|
2214
|
+
return m.key?.type === "Identifier" ? m.key.name : null;
|
|
2215
|
+
}
|
|
2216
|
+
return null;
|
|
2217
|
+
}).filter((n) => n !== null);
|
|
2218
|
+
}
|
|
2219
|
+
function commonPrefix(names) {
|
|
2220
|
+
if (names.length < 2)
|
|
2221
|
+
return "";
|
|
2222
|
+
let prefix = names[0];
|
|
2223
|
+
for (let i = 1;i < names.length; i++) {
|
|
2224
|
+
const name = names[i];
|
|
2225
|
+
let j = 0;
|
|
2226
|
+
while (j < prefix.length && j < name.length && prefix[j].toLowerCase() === name[j].toLowerCase())
|
|
2227
|
+
j++;
|
|
2228
|
+
prefix = prefix.slice(0, j);
|
|
2229
|
+
}
|
|
2230
|
+
if (!prefix)
|
|
2231
|
+
return "";
|
|
2232
|
+
if (prefix.endsWith("_"))
|
|
2233
|
+
return prefix;
|
|
2234
|
+
for (const name of names) {
|
|
2235
|
+
if (name.length > prefix.length) {
|
|
2236
|
+
const nextChar = name[prefix.length];
|
|
2237
|
+
if (nextChar >= "A" && nextChar <= "Z")
|
|
2238
|
+
return prefix;
|
|
2239
|
+
return "";
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
return prefix;
|
|
2243
|
+
}
|
|
2244
|
+
function commonSuffix(names) {
|
|
2245
|
+
if (names.length < 2)
|
|
2246
|
+
return "";
|
|
2247
|
+
let suffix = names[0];
|
|
2248
|
+
for (let i = 1;i < names.length; i++) {
|
|
2249
|
+
const name = names[i];
|
|
2250
|
+
let j = 0;
|
|
2251
|
+
while (j < suffix.length && j < name.length && suffix[suffix.length - 1 - j] === name[name.length - 1 - j])
|
|
2252
|
+
j++;
|
|
2253
|
+
suffix = suffix.slice(suffix.length - j);
|
|
2254
|
+
}
|
|
2255
|
+
if (!suffix)
|
|
2256
|
+
return "";
|
|
2257
|
+
if (suffix[0] >= "A" && suffix[0] <= "Z")
|
|
2258
|
+
return suffix;
|
|
2259
|
+
if (suffix.startsWith("_"))
|
|
2260
|
+
return suffix;
|
|
2261
|
+
return "";
|
|
2262
|
+
}
|
|
2263
|
+
var struct_field_names_default = {
|
|
2264
|
+
create(context) {
|
|
2265
|
+
function check(node, typeName) {
|
|
2266
|
+
if (!typeName)
|
|
2267
|
+
return;
|
|
2268
|
+
const names = getFieldNames(node);
|
|
2269
|
+
if (names.length < 2)
|
|
2270
|
+
return;
|
|
2271
|
+
const prefix = commonPrefix(names);
|
|
2272
|
+
if (prefix.length >= 3) {
|
|
2273
|
+
context.report({
|
|
2274
|
+
message: `Struct field names: all fields of \`${typeName}\` share the prefix \`${prefix}\`. Remove it — the type name provides context. (clippy::struct_field_names)`,
|
|
2275
|
+
node
|
|
2276
|
+
});
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const suffix = commonSuffix(names);
|
|
2280
|
+
if (suffix.length >= 3) {
|
|
2281
|
+
context.report({
|
|
2282
|
+
message: `Struct field names: all fields of \`${typeName}\` share the suffix \`${suffix}\`. Consider removing it. (clippy::struct_field_names)`,
|
|
2283
|
+
node
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
return {
|
|
2288
|
+
TSInterfaceDeclaration(node) {
|
|
2289
|
+
check(node, node.id?.name);
|
|
2290
|
+
},
|
|
2291
|
+
TSTypeAliasDeclaration(node) {
|
|
2292
|
+
if (node.typeAnnotation?.type === "TSTypeLiteral") {
|
|
2293
|
+
check(node.typeAnnotation, node.id?.name);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
|
|
2300
|
+
// src/rules/unreadable-literal.ts
|
|
2301
|
+
var INT_THRESHOLD = 1e4;
|
|
2302
|
+
var unreadable_literal_default = {
|
|
2303
|
+
create(context) {
|
|
2304
|
+
return {
|
|
2305
|
+
Literal(node) {
|
|
2306
|
+
if (typeof node.value !== "number")
|
|
2307
|
+
return;
|
|
2308
|
+
if (!Number.isFinite(node.value))
|
|
2309
|
+
return;
|
|
2310
|
+
const raw = node.raw;
|
|
2311
|
+
if (!raw)
|
|
2312
|
+
return;
|
|
2313
|
+
if (raw.includes("_"))
|
|
2314
|
+
return;
|
|
2315
|
+
if (raw.startsWith("0x") || raw.startsWith("0o") || raw.startsWith("0b") || raw.startsWith("0X") || raw.startsWith("0O") || raw.startsWith("0B"))
|
|
2316
|
+
return;
|
|
2317
|
+
if (raw.includes("."))
|
|
2318
|
+
return;
|
|
2319
|
+
if (raw.includes("e") || raw.includes("E"))
|
|
2320
|
+
return;
|
|
2321
|
+
const absVal = Math.abs(node.value);
|
|
2322
|
+
if (absVal >= INT_THRESHOLD && Number.isInteger(absVal)) {
|
|
2323
|
+
context.report({
|
|
2324
|
+
message: `Unreadable literal: \`${raw}\` is hard to read. Use numeric separators: \`${formatWithSeparators(raw)}\`. (clippy::unreadable_literal)`,
|
|
2325
|
+
node
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
function formatWithSeparators(raw) {
|
|
2333
|
+
const negative = raw.startsWith("-");
|
|
2334
|
+
const digits = negative ? raw.slice(1) : raw;
|
|
2335
|
+
const parts = [];
|
|
2336
|
+
for (let i = digits.length;i > 0; i -= 3) {
|
|
2337
|
+
parts.unshift(digits.slice(Math.max(0, i - 3), i));
|
|
2338
|
+
}
|
|
2339
|
+
return (negative ? "-" : "") + parts.join("_");
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// src/rules/bool-to-int-with-if.ts
|
|
2343
|
+
var bool_to_int_with_if_default = {
|
|
2344
|
+
create(context) {
|
|
2345
|
+
return {
|
|
2346
|
+
ConditionalExpression(node) {
|
|
2347
|
+
const { consequent, alternate } = node;
|
|
2348
|
+
if (isLiteral(consequent, 1) && isLiteral(alternate, 0)) {
|
|
2349
|
+
context.report({
|
|
2350
|
+
message: "Bool to int with if: `cond ? 1 : 0` can be simplified to `Number(cond)` or `+cond`. (clippy::bool_to_int_with_if)",
|
|
2351
|
+
node
|
|
2352
|
+
});
|
|
2353
|
+
} else if (isLiteral(consequent, 0) && isLiteral(alternate, 1)) {
|
|
2354
|
+
context.report({
|
|
2355
|
+
message: "Bool to int with if: `cond ? 0 : 1` can be simplified to `Number(!cond)` or `+!cond`. (clippy::bool_to_int_with_if)",
|
|
2356
|
+
node
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
|
|
2364
|
+
// src/plugin.ts
|
|
2365
|
+
var plugin = {
|
|
2366
|
+
meta: {
|
|
2367
|
+
name: "oxclippy",
|
|
2368
|
+
version: "0.1.0"
|
|
2369
|
+
},
|
|
2370
|
+
rules: {
|
|
2371
|
+
"needless-bool": needless_bool_default,
|
|
2372
|
+
"collapsible-if": collapsible_if_default,
|
|
2373
|
+
"neg-multiply": neg_multiply_default,
|
|
2374
|
+
"bool-comparison": bool_comparison_default,
|
|
2375
|
+
"single-case-switch": single_case_switch_default,
|
|
2376
|
+
"let-and-return": let_and_return_default,
|
|
2377
|
+
"int-plus-one": int_plus_one_default,
|
|
2378
|
+
"needless-late-init": needless_late_init_default,
|
|
2379
|
+
"identity-op": identity_op_default,
|
|
2380
|
+
"manual-clamp": manual_clamp_default,
|
|
2381
|
+
"manual-strip": manual_strip_default,
|
|
2382
|
+
"useless-conversion": useless_conversion_default,
|
|
2383
|
+
"manual-swap": manual_swap_default,
|
|
2384
|
+
"manual-is-finite": manual_is_finite_default,
|
|
2385
|
+
"float-comparison": float_comparison_default,
|
|
2386
|
+
"xor-used-as-pow": xor_used_as_pow_default,
|
|
2387
|
+
"almost-swapped": almost_swapped_default,
|
|
2388
|
+
"if-same-then-else": if_same_then_else_default,
|
|
2389
|
+
"never-loop": never_loop_default,
|
|
2390
|
+
"float-equality-without-abs": float_equality_without_abs_default,
|
|
2391
|
+
"zero-divided-by-zero": zero_divided_by_zero_default,
|
|
2392
|
+
"filter-then-first": filter_then_first_default,
|
|
2393
|
+
"map-void-return": map_void_return_default,
|
|
2394
|
+
"map-identity": map_identity_default,
|
|
2395
|
+
"manual-find": manual_find_default,
|
|
2396
|
+
"manual-some": manual_some_default,
|
|
2397
|
+
"manual-every": manual_every_default,
|
|
2398
|
+
"manual-includes": manual_includes_default,
|
|
2399
|
+
"search-is-some": search_is_some_default,
|
|
2400
|
+
"needless-range-loop": needless_range_loop_default,
|
|
2401
|
+
"redundant-closure-call": redundant_closure_call_default,
|
|
2402
|
+
"explicit-counter-loop": explicit_counter_loop_default,
|
|
2403
|
+
"unnecessary-fold": unnecessary_fold_default,
|
|
2404
|
+
"single-element-loop": single_element_loop_default,
|
|
2405
|
+
"too-many-arguments": too_many_arguments_default,
|
|
2406
|
+
"too-many-lines": too_many_lines_default,
|
|
2407
|
+
"cognitive-complexity": cognitive_complexity_default,
|
|
2408
|
+
"excessive-nesting": excessive_nesting_default,
|
|
2409
|
+
"fn-params-excessive-bools": fn_params_excessive_bools_default,
|
|
2410
|
+
"redundant-closure": redundant_closure_default,
|
|
2411
|
+
"unnecessary-reduce-collect": unnecessary_reduce_collect_default,
|
|
2412
|
+
"prefer-structured-clone": prefer_structured_clone_default,
|
|
2413
|
+
"object-keys-values": object_keys_values_default,
|
|
2414
|
+
"promise-new-resolve": promise_new_resolve_default,
|
|
2415
|
+
"similar-names": similar_names_default,
|
|
2416
|
+
"match-same-arms": match_same_arms_default,
|
|
2417
|
+
"used-underscore-binding": used_underscore_binding_default,
|
|
2418
|
+
"needless-continue": needless_continue_default,
|
|
2419
|
+
"enum-variant-names": enum_variant_names_default,
|
|
2420
|
+
"struct-field-names": struct_field_names_default,
|
|
2421
|
+
"unreadable-literal": unreadable_literal_default,
|
|
2422
|
+
"bool-to-int-with-if": bool_to_int_with_if_default
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
var plugin_default = plugin;
|
|
2426
|
+
export {
|
|
2427
|
+
plugin_default as default
|
|
2428
|
+
};
|