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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// clippy::filter_next — .filter(fn)[0] instead of .find(fn)
|
|
2
|
+
// Detects: arr.filter(fn)[0] → arr.find(fn)
|
|
3
|
+
// The Rust equivalent is .filter().next() → .find()
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
import { isLiteral, isMethodCall } from "../types";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
create(context: Context) {
|
|
10
|
+
return {
|
|
11
|
+
MemberExpression(node: Node) {
|
|
12
|
+
// arr.filter(fn)[0]
|
|
13
|
+
if (!node.computed || !isLiteral(node.property, 0)) return;
|
|
14
|
+
|
|
15
|
+
const obj = node.object;
|
|
16
|
+
if (!isMethodCall(obj, "filter")) return;
|
|
17
|
+
|
|
18
|
+
context.report({
|
|
19
|
+
message:
|
|
20
|
+
"Filter then first: `.filter(fn)[0]` is less efficient and readable than `.find(fn)`. (clippy::filter_next)",
|
|
21
|
+
node,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// clippy::float_cmp — direct equality comparison of floating-point numbers
|
|
2
|
+
// Detects: x === 0.1 + 0.2, a === b where both are floats
|
|
3
|
+
// In JS, 0.1 + 0.2 !== 0.3 due to IEEE 754 precision.
|
|
4
|
+
// Only flags comparisons where at least one side is a float literal or float arithmetic.
|
|
5
|
+
|
|
6
|
+
import type { Context, Node } from "../types";
|
|
7
|
+
|
|
8
|
+
function isFloatLiteral(node: Node): boolean {
|
|
9
|
+
return node.type === "Literal" && typeof node.value === "number" && !Number.isInteger(node.value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isArithmetic(node: Node): boolean {
|
|
13
|
+
return (
|
|
14
|
+
node.type === "BinaryExpression" &&
|
|
15
|
+
(node.operator === "+" ||
|
|
16
|
+
node.operator === "-" ||
|
|
17
|
+
node.operator === "*" ||
|
|
18
|
+
node.operator === "/")
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function involvesFloat(node: Node): boolean {
|
|
23
|
+
if (isFloatLiteral(node)) return true;
|
|
24
|
+
if (isArithmetic(node)) {
|
|
25
|
+
return involvesFloat(node.left) || involvesFloat(node.right);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
create(context: Context) {
|
|
32
|
+
return {
|
|
33
|
+
BinaryExpression(node: Node) {
|
|
34
|
+
if (
|
|
35
|
+
node.operator !== "===" &&
|
|
36
|
+
node.operator !== "==" &&
|
|
37
|
+
node.operator !== "!==" &&
|
|
38
|
+
node.operator !== "!="
|
|
39
|
+
)
|
|
40
|
+
return;
|
|
41
|
+
|
|
42
|
+
if (involvesFloat(node.left) || involvesFloat(node.right)) {
|
|
43
|
+
context.report({
|
|
44
|
+
message:
|
|
45
|
+
"Float comparison: direct equality comparison of floating-point numbers is unreliable. Use `Math.abs(a - b) < Number.EPSILON` instead. (clippy::float_cmp)",
|
|
46
|
+
node,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// clippy::float_equality_without_abs — float comparison without Math.abs()
|
|
2
|
+
// Detects: (a - b) < EPSILON (missing abs — fails when a < b)
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
function isEpsilonLike(node: Node): boolean {
|
|
7
|
+
// Number.EPSILON
|
|
8
|
+
if (
|
|
9
|
+
node.type === "MemberExpression" &&
|
|
10
|
+
node.object?.type === "Identifier" &&
|
|
11
|
+
node.object.name === "Number" &&
|
|
12
|
+
node.property?.type === "Identifier" &&
|
|
13
|
+
node.property.name === "EPSILON"
|
|
14
|
+
)
|
|
15
|
+
return true;
|
|
16
|
+
// Small float literal
|
|
17
|
+
if (node.type === "Literal" && typeof node.value === "number" && node.value > 0 && node.value < 1)
|
|
18
|
+
return true;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isSubtraction(node: Node): boolean {
|
|
23
|
+
return node.type === "BinaryExpression" && node.operator === "-";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
create(context: Context) {
|
|
28
|
+
return {
|
|
29
|
+
BinaryExpression(node: Node) {
|
|
30
|
+
if (node.operator !== "<" && node.operator !== "<=") return;
|
|
31
|
+
|
|
32
|
+
// (a - b) < epsilon
|
|
33
|
+
if (isSubtraction(node.left) && isEpsilonLike(node.right)) {
|
|
34
|
+
context.report({
|
|
35
|
+
message:
|
|
36
|
+
"Float equality without abs: `(a - b) < epsilon` fails when a < b. Use `Math.abs(a - b) < epsilon`. (clippy::float_equality_without_abs)",
|
|
37
|
+
node,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// clippy::fn_params_excessive_bools — too many boolean parameters (easy to mix up at call sites)
|
|
2
|
+
// Default threshold: 3 boolean params
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
const THRESHOLD = 3;
|
|
7
|
+
|
|
8
|
+
function countBoolParams(node: Node): number {
|
|
9
|
+
const params: Node[] = node.params;
|
|
10
|
+
if (!params) return 0;
|
|
11
|
+
let count = 0;
|
|
12
|
+
for (const p of params) {
|
|
13
|
+
// Check for TypeScript boolean type annotation: param: boolean
|
|
14
|
+
if (p.type === "Identifier" && p.typeAnnotation) {
|
|
15
|
+
const ann = p.typeAnnotation.typeAnnotation ?? p.typeAnnotation;
|
|
16
|
+
if (ann.type === "TSBooleanKeyword") count++;
|
|
17
|
+
}
|
|
18
|
+
// Also check assignment patterns with boolean defaults: param = true/false
|
|
19
|
+
if (
|
|
20
|
+
p.type === "AssignmentPattern" &&
|
|
21
|
+
p.right?.type === "Literal" &&
|
|
22
|
+
typeof p.right.value === "boolean"
|
|
23
|
+
) {
|
|
24
|
+
count++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return count;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function checkFunction(node: Node, context: Context) {
|
|
31
|
+
const boolCount = countBoolParams(node);
|
|
32
|
+
if (boolCount <= THRESHOLD) return;
|
|
33
|
+
|
|
34
|
+
const name = node.id?.name ?? "<anonymous>";
|
|
35
|
+
context.report({
|
|
36
|
+
message: `Excessive bools: \`${name}\` has ${boolCount} boolean parameters (max ${THRESHOLD}). Consider using an options object. (clippy::fn_params_excessive_bools)`,
|
|
37
|
+
node,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
create(context: Context) {
|
|
43
|
+
return {
|
|
44
|
+
FunctionDeclaration: (node: Node) => checkFunction(node, context),
|
|
45
|
+
FunctionExpression: (node: Node) => checkFunction(node, context),
|
|
46
|
+
ArrowFunctionExpression: (node: Node) => checkFunction(node, context),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// clippy::identity_op — operations that have no effect
|
|
2
|
+
// Detects: x + 0, 0 + x, x - 0, x * 1, 1 * x, x / 1, x ** 1
|
|
3
|
+
// NOT covered by oxc/erasing-op (which covers x * 0, x & 0 — destructive ops)
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
import { isLiteral } from "../types";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
create(context: Context) {
|
|
10
|
+
return {
|
|
11
|
+
BinaryExpression(node: Node) {
|
|
12
|
+
const { operator, left, right } = node;
|
|
13
|
+
|
|
14
|
+
let match = false;
|
|
15
|
+
|
|
16
|
+
switch (operator) {
|
|
17
|
+
case "+":
|
|
18
|
+
// Skip string concatenation: "str" + 0 is not identity
|
|
19
|
+
if (isLiteral(left) && typeof left.value === "string") break;
|
|
20
|
+
if (isLiteral(right) && typeof right.value === "string") break;
|
|
21
|
+
match = isLiteral(left, 0) || isLiteral(right, 0);
|
|
22
|
+
break;
|
|
23
|
+
case "-":
|
|
24
|
+
match = isLiteral(right, 0);
|
|
25
|
+
break;
|
|
26
|
+
case "*":
|
|
27
|
+
match = isLiteral(left, 1) || isLiteral(right, 1);
|
|
28
|
+
break;
|
|
29
|
+
case "/":
|
|
30
|
+
match = isLiteral(right, 1);
|
|
31
|
+
break;
|
|
32
|
+
case "**":
|
|
33
|
+
match = isLiteral(right, 1);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (match) {
|
|
38
|
+
context.report({
|
|
39
|
+
message: `Identity op: \`${operator} ${operator === "+" || operator === "-" ? "0" : "1"}\` has no effect. (clippy::identity_op)`,
|
|
40
|
+
node,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// clippy::if_same_then_else — if/else with identical bodies
|
|
2
|
+
// Almost certainly a bug or dead code.
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
function sourceRange(node: Node, text: string): string {
|
|
7
|
+
if (node.start != null && node.end != null) {
|
|
8
|
+
return text.slice(node.start, node.end);
|
|
9
|
+
}
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
create(context: Context) {
|
|
15
|
+
return {
|
|
16
|
+
IfStatement(node: Node) {
|
|
17
|
+
if (!node.alternate) return;
|
|
18
|
+
if (node.alternate.type === "IfStatement") return; // skip else-if chains
|
|
19
|
+
|
|
20
|
+
const text = context.sourceCode.text;
|
|
21
|
+
const consequentSrc = sourceRange(node.consequent, text);
|
|
22
|
+
const alternateSrc = sourceRange(node.alternate, text);
|
|
23
|
+
|
|
24
|
+
if (consequentSrc && consequentSrc === alternateSrc) {
|
|
25
|
+
context.report({
|
|
26
|
+
message:
|
|
27
|
+
"If same then else: both branches of this if/else are identical. (clippy::if_same_then_else)",
|
|
28
|
+
node,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// clippy::int_plus_one — x >= y + 1 can be simplified to x > y
|
|
2
|
+
// Also: x + 1 <= y → x < y, x - 1 >= y → x > y, etc.
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
import { isLiteral } from "../types";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
create(context: Context) {
|
|
9
|
+
return {
|
|
10
|
+
BinaryExpression(node: Node) {
|
|
11
|
+
const { operator, left, right } = node;
|
|
12
|
+
|
|
13
|
+
// x >= y + 1 → x > y
|
|
14
|
+
if (
|
|
15
|
+
operator === ">=" &&
|
|
16
|
+
right.type === "BinaryExpression" &&
|
|
17
|
+
right.operator === "+" &&
|
|
18
|
+
isLiteral(right.right, 1)
|
|
19
|
+
) {
|
|
20
|
+
context.report({
|
|
21
|
+
message:
|
|
22
|
+
"Int plus one: `x >= y + 1` can be simplified to `x > y`. (clippy::int_plus_one)",
|
|
23
|
+
node,
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// x + 1 <= y → x < y
|
|
29
|
+
if (
|
|
30
|
+
operator === "<=" &&
|
|
31
|
+
left.type === "BinaryExpression" &&
|
|
32
|
+
left.operator === "+" &&
|
|
33
|
+
isLiteral(left.right, 1)
|
|
34
|
+
) {
|
|
35
|
+
context.report({
|
|
36
|
+
message:
|
|
37
|
+
"Int plus one: `x + 1 <= y` can be simplified to `x < y`. (clippy::int_plus_one)",
|
|
38
|
+
node,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// x - 1 >= y → x > y
|
|
44
|
+
if (
|
|
45
|
+
operator === ">=" &&
|
|
46
|
+
left.type === "BinaryExpression" &&
|
|
47
|
+
left.operator === "-" &&
|
|
48
|
+
isLiteral(left.right, 1)
|
|
49
|
+
) {
|
|
50
|
+
context.report({
|
|
51
|
+
message:
|
|
52
|
+
"Int plus one: `x - 1 >= y` can be simplified to `x > y`. (clippy::int_plus_one)",
|
|
53
|
+
node,
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// y <= x - 1 → y < x
|
|
59
|
+
if (
|
|
60
|
+
operator === "<=" &&
|
|
61
|
+
right.type === "BinaryExpression" &&
|
|
62
|
+
right.operator === "-" &&
|
|
63
|
+
isLiteral(right.right, 1)
|
|
64
|
+
) {
|
|
65
|
+
context.report({
|
|
66
|
+
message:
|
|
67
|
+
"Int plus one: `y <= x - 1` can be simplified to `y < x`. (clippy::int_plus_one)",
|
|
68
|
+
node,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// clippy::let_and_return — unnecessary variable before return
|
|
2
|
+
// Detects: const x = expr; return x; → return expr;
|
|
3
|
+
// Only flags when the variable is declared and immediately returned with no other uses.
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
import { getFunctionBody } from "../types";
|
|
7
|
+
|
|
8
|
+
function checkBody(body: Node[], context: Context) {
|
|
9
|
+
if (body.length < 2) return;
|
|
10
|
+
|
|
11
|
+
const last = body[body.length - 1]!;
|
|
12
|
+
const secondLast = body[body.length - 2]!;
|
|
13
|
+
|
|
14
|
+
// Last must be: return x;
|
|
15
|
+
if (last.type !== "ReturnStatement" || !last.argument || last.argument.type !== "Identifier")
|
|
16
|
+
return;
|
|
17
|
+
const returnedName = last.argument.name;
|
|
18
|
+
|
|
19
|
+
// Second-to-last must be: const/let x = expr;
|
|
20
|
+
if (secondLast.type !== "VariableDeclaration" || secondLast.declarations.length !== 1) return;
|
|
21
|
+
const decl = secondLast.declarations[0]!;
|
|
22
|
+
if (decl.id?.type !== "Identifier" || decl.id.name !== returnedName || !decl.init) return;
|
|
23
|
+
|
|
24
|
+
context.report({
|
|
25
|
+
message: `Let and return: \`${returnedName}\` is immediately returned. Return the expression directly. (clippy::let_and_return)`,
|
|
26
|
+
node: secondLast,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
create(context: Context) {
|
|
32
|
+
function checkFunction(node: Node) {
|
|
33
|
+
const body = getFunctionBody(node);
|
|
34
|
+
if (body) checkBody(body, context);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
FunctionDeclaration: checkFunction,
|
|
39
|
+
FunctionExpression: checkFunction,
|
|
40
|
+
ArrowFunctionExpression: checkFunction,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// clippy::manual_clamp — manual min/max clamping patterns
|
|
2
|
+
// Detects: Math.max(min, Math.min(val, max)) or Math.min(max, Math.max(val, min))
|
|
3
|
+
// Suggests: use a clamp function or Math.min(Math.max(val, min), max) with a comment
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
import { isIdentifier } from "../types";
|
|
7
|
+
|
|
8
|
+
function isMathCall(node: Node, method: string): boolean {
|
|
9
|
+
if (node.type !== "CallExpression") return false;
|
|
10
|
+
const callee = node.callee;
|
|
11
|
+
return (
|
|
12
|
+
callee.type === "MemberExpression" &&
|
|
13
|
+
isIdentifier(callee.object, "Math") &&
|
|
14
|
+
isIdentifier(callee.property, method)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
create(context: Context) {
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node: Node) {
|
|
22
|
+
const args = node.arguments;
|
|
23
|
+
if (!args || args.length !== 2) return;
|
|
24
|
+
|
|
25
|
+
// Math.max(min, Math.min(val, max))
|
|
26
|
+
if (isMathCall(node, "max") && isMathCall(args[0], "min")) {
|
|
27
|
+
context.report({
|
|
28
|
+
message:
|
|
29
|
+
"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)",
|
|
30
|
+
node,
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (isMathCall(node, "max") && isMathCall(args[1], "min")) {
|
|
35
|
+
context.report({
|
|
36
|
+
message:
|
|
37
|
+
"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)",
|
|
38
|
+
node,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Math.min(max, Math.max(val, min))
|
|
44
|
+
if (isMathCall(node, "min") && isMathCall(args[0], "max")) {
|
|
45
|
+
context.report({
|
|
46
|
+
message:
|
|
47
|
+
"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)",
|
|
48
|
+
node,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (isMathCall(node, "min") && isMathCall(args[1], "max")) {
|
|
53
|
+
context.report({
|
|
54
|
+
message:
|
|
55
|
+
"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)",
|
|
56
|
+
node,
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// clippy::manual_find (every variant) — for loop implementing .every()
|
|
2
|
+
// Detects: for (const x of arr) { if (!cond(x)) return false; } return true;
|
|
3
|
+
// Also: for (const x of arr) { if (cond(x)) return false; } return true;
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
import { getFunctionBody, isLiteral, unwrapBlock } from "../types";
|
|
7
|
+
|
|
8
|
+
function checkBody(body: Node[], context: Context) {
|
|
9
|
+
for (let i = 0; i < body.length; i++) {
|
|
10
|
+
const stmt = body[i]!;
|
|
11
|
+
if (stmt.type !== "ForOfStatement") continue;
|
|
12
|
+
|
|
13
|
+
const inner = unwrapBlock(stmt.body);
|
|
14
|
+
if (!inner || inner.type !== "IfStatement" || inner.alternate) continue;
|
|
15
|
+
|
|
16
|
+
const consequent = unwrapBlock(inner.consequent);
|
|
17
|
+
if (
|
|
18
|
+
!consequent ||
|
|
19
|
+
consequent.type !== "ReturnStatement" ||
|
|
20
|
+
!isLiteral(consequent.argument, false)
|
|
21
|
+
)
|
|
22
|
+
continue;
|
|
23
|
+
|
|
24
|
+
const next: Node | undefined = body[i + 1];
|
|
25
|
+
if (next && next.type === "ReturnStatement" && isLiteral(next.argument, true)) {
|
|
26
|
+
context.report({
|
|
27
|
+
message: "Manual every: this loop can be replaced with `.every()`. (clippy::manual_find)",
|
|
28
|
+
node: stmt,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default {
|
|
35
|
+
create(context: Context) {
|
|
36
|
+
function checkFunction(node: Node) {
|
|
37
|
+
const body = getFunctionBody(node);
|
|
38
|
+
if (body) checkBody(body, context);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
FunctionDeclaration: checkFunction,
|
|
43
|
+
FunctionExpression: checkFunction,
|
|
44
|
+
ArrowFunctionExpression: checkFunction,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// clippy::manual_find — for loop that can be replaced with .find()
|
|
2
|
+
// Detects: for (const x of arr) { if (cond(x)) return x; } return undefined;
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
import { getForOfVar, getFunctionBody, unwrapBlock } from "../types";
|
|
6
|
+
|
|
7
|
+
function isReturnOfVar(stmt: Node, varName: string): boolean {
|
|
8
|
+
if (stmt.type !== "ReturnStatement") return false;
|
|
9
|
+
return stmt.argument?.type === "Identifier" && stmt.argument.name === varName;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isReturnNullish(stmt: Node): boolean {
|
|
13
|
+
if (stmt.type !== "ReturnStatement") return false;
|
|
14
|
+
if (!stmt.argument) return true; // bare return
|
|
15
|
+
if (stmt.argument.type === "Identifier" && stmt.argument.name === "undefined") return true;
|
|
16
|
+
if (stmt.argument.type === "Literal" && stmt.argument.value === null) return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function checkBody(body: Node[], context: Context) {
|
|
21
|
+
for (let i = 0; i < body.length; i++) {
|
|
22
|
+
const stmt = body[i]!;
|
|
23
|
+
if (stmt.type !== "ForOfStatement") continue;
|
|
24
|
+
|
|
25
|
+
const loopVar = getForOfVar(stmt);
|
|
26
|
+
if (!loopVar) continue;
|
|
27
|
+
|
|
28
|
+
const inner = unwrapBlock(stmt.body);
|
|
29
|
+
if (!inner) continue;
|
|
30
|
+
|
|
31
|
+
if (inner.type === "IfStatement" && !inner.alternate) {
|
|
32
|
+
const consequent = unwrapBlock(inner.consequent);
|
|
33
|
+
if (consequent && isReturnOfVar(consequent, loopVar)) {
|
|
34
|
+
const next: Node | undefined = body[i + 1];
|
|
35
|
+
if (next && isReturnNullish(next)) {
|
|
36
|
+
context.report({
|
|
37
|
+
message: "Manual find: this loop can be replaced with `.find()`. (clippy::manual_find)",
|
|
38
|
+
node: stmt,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default {
|
|
47
|
+
create(context: Context) {
|
|
48
|
+
function checkFunction(node: Node) {
|
|
49
|
+
const body = getFunctionBody(node);
|
|
50
|
+
if (body) checkBody(body, context);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
FunctionDeclaration: checkFunction,
|
|
55
|
+
FunctionExpression: checkFunction,
|
|
56
|
+
ArrowFunctionExpression: checkFunction,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// clippy::manual_find (includes variant) — for loop implementing .includes()
|
|
2
|
+
// Detects: for (const x of arr) { if (x === val) return true; } return false;
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
import { getForOfVar, getFunctionBody, isLiteral, unwrapBlock } from "../types";
|
|
6
|
+
|
|
7
|
+
function isEqualityCheck(test: Node, varName: string): boolean {
|
|
8
|
+
if (test.type !== "BinaryExpression") return false;
|
|
9
|
+
if (test.operator !== "===" && test.operator !== "==") return false;
|
|
10
|
+
|
|
11
|
+
const left = test.left;
|
|
12
|
+
const right = test.right;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
(left.type === "Identifier" && left.name === varName) ||
|
|
16
|
+
(right.type === "Identifier" && right.name === varName)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function checkBody(body: Node[], context: Context) {
|
|
21
|
+
for (let i = 0; i < body.length; i++) {
|
|
22
|
+
const stmt = body[i]!;
|
|
23
|
+
if (stmt.type !== "ForOfStatement") continue;
|
|
24
|
+
|
|
25
|
+
const loopVar = getForOfVar(stmt);
|
|
26
|
+
if (!loopVar) continue;
|
|
27
|
+
|
|
28
|
+
const inner = unwrapBlock(stmt.body);
|
|
29
|
+
if (!inner || inner.type !== "IfStatement" || inner.alternate) continue;
|
|
30
|
+
|
|
31
|
+
if (!isEqualityCheck(inner.test, loopVar)) continue;
|
|
32
|
+
|
|
33
|
+
const consequent = unwrapBlock(inner.consequent);
|
|
34
|
+
if (
|
|
35
|
+
!consequent ||
|
|
36
|
+
consequent.type !== "ReturnStatement" ||
|
|
37
|
+
!isLiteral(consequent.argument, true)
|
|
38
|
+
)
|
|
39
|
+
continue;
|
|
40
|
+
|
|
41
|
+
const next: Node | undefined = body[i + 1];
|
|
42
|
+
if (next && next.type === "ReturnStatement" && isLiteral(next.argument, false)) {
|
|
43
|
+
context.report({
|
|
44
|
+
message:
|
|
45
|
+
"Manual includes: this loop can be replaced with `.includes()`. (clippy::manual_find)",
|
|
46
|
+
node: stmt,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default {
|
|
53
|
+
create(context: Context) {
|
|
54
|
+
function checkFunction(node: Node) {
|
|
55
|
+
const body = getFunctionBody(node);
|
|
56
|
+
if (body) checkBody(body, context);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
FunctionDeclaration: checkFunction,
|
|
61
|
+
FunctionExpression: checkFunction,
|
|
62
|
+
ArrowFunctionExpression: checkFunction,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// clippy::manual_is_finite / manual_is_infinite — manual infinity checks
|
|
2
|
+
// Detects: x !== Infinity && x !== -Infinity → Number.isFinite(x)
|
|
3
|
+
// Detects: x === Infinity || x === -Infinity → !Number.isFinite(x)
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
|
|
7
|
+
function isInfinityCheck(node: Node, operator: string): boolean {
|
|
8
|
+
if (node.type !== "BinaryExpression" || node.operator !== operator) return false;
|
|
9
|
+
return isInfinityLiteral(node.right) || isInfinityLiteral(node.left);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isInfinityLiteral(node: Node): boolean {
|
|
13
|
+
// Infinity
|
|
14
|
+
if (node.type === "Identifier" && node.name === "Infinity") return true;
|
|
15
|
+
// -Infinity
|
|
16
|
+
if (
|
|
17
|
+
node.type === "UnaryExpression" &&
|
|
18
|
+
node.operator === "-" &&
|
|
19
|
+
node.argument?.type === "Identifier" &&
|
|
20
|
+
node.argument.name === "Infinity"
|
|
21
|
+
)
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
create(context: Context) {
|
|
28
|
+
return {
|
|
29
|
+
LogicalExpression(node: Node) {
|
|
30
|
+
// x !== Infinity && x !== -Infinity → Number.isFinite(x)
|
|
31
|
+
if (
|
|
32
|
+
node.operator === "&&" &&
|
|
33
|
+
isInfinityCheck(node.left, "!==") &&
|
|
34
|
+
isInfinityCheck(node.right, "!==")
|
|
35
|
+
) {
|
|
36
|
+
context.report({
|
|
37
|
+
message:
|
|
38
|
+
"Manual isFinite: this pattern checks for finiteness manually. Use `Number.isFinite(x)` instead. (clippy::manual_is_finite)",
|
|
39
|
+
node,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// x === Infinity || x === -Infinity → !Number.isFinite(x)
|
|
43
|
+
if (
|
|
44
|
+
node.operator === "||" &&
|
|
45
|
+
isInfinityCheck(node.left, "===") &&
|
|
46
|
+
isInfinityCheck(node.right, "===")
|
|
47
|
+
) {
|
|
48
|
+
context.report({
|
|
49
|
+
message:
|
|
50
|
+
"Manual isInfinite: this pattern checks for infinity manually. Use `!Number.isFinite(x)` instead. (clippy::manual_is_infinite)",
|
|
51
|
+
node,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|