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,46 @@
|
|
|
1
|
+
// clippy::manual_find (boolean variant) — for loop implementing .some()
|
|
2
|
+
// Detects: for (const x of arr) { if (cond(x)) return true; } return false;
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
import { getFunctionBody, isLiteral, unwrapBlock } from "../types";
|
|
6
|
+
|
|
7
|
+
function checkBody(body: Node[], context: Context) {
|
|
8
|
+
for (let i = 0; i < body.length; i++) {
|
|
9
|
+
const stmt = body[i]!;
|
|
10
|
+
if (stmt.type !== "ForOfStatement") continue;
|
|
11
|
+
|
|
12
|
+
const inner = unwrapBlock(stmt.body);
|
|
13
|
+
if (!inner || inner.type !== "IfStatement" || inner.alternate) continue;
|
|
14
|
+
|
|
15
|
+
const consequent = unwrapBlock(inner.consequent);
|
|
16
|
+
if (
|
|
17
|
+
!consequent ||
|
|
18
|
+
consequent.type !== "ReturnStatement" ||
|
|
19
|
+
!isLiteral(consequent.argument, true)
|
|
20
|
+
)
|
|
21
|
+
continue;
|
|
22
|
+
|
|
23
|
+
const next: Node | undefined = body[i + 1];
|
|
24
|
+
if (next && next.type === "ReturnStatement" && isLiteral(next.argument, false)) {
|
|
25
|
+
context.report({
|
|
26
|
+
message: "Manual some: this loop can be replaced with `.some()`. (clippy::manual_find)",
|
|
27
|
+
node: stmt,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
create(context: Context) {
|
|
35
|
+
function checkFunction(node: Node) {
|
|
36
|
+
const body = getFunctionBody(node);
|
|
37
|
+
if (body) checkBody(body, context);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
FunctionDeclaration: checkFunction,
|
|
42
|
+
FunctionExpression: checkFunction,
|
|
43
|
+
ArrowFunctionExpression: checkFunction,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// clippy::manual_strip — manual prefix/suffix stripping
|
|
2
|
+
// Detects: if (s.startsWith(prefix)) { s.slice(prefix.length, ...) }
|
|
3
|
+
// Detects: if (s.endsWith(suffix)) { s.slice(0, -suffix.length) }
|
|
4
|
+
// Suggests: extract the strip logic or use a helper
|
|
5
|
+
|
|
6
|
+
import type { Context, Node } from "../types";
|
|
7
|
+
import { isMethodCall } from "../types";
|
|
8
|
+
|
|
9
|
+
function getStringMethodTarget(
|
|
10
|
+
testExpr: Node,
|
|
11
|
+
methodName: string,
|
|
12
|
+
): { str: string; arg: string } | null {
|
|
13
|
+
if (!isMethodCall(testExpr, methodName)) return null;
|
|
14
|
+
|
|
15
|
+
const callee = testExpr.callee;
|
|
16
|
+
if (callee.object?.type !== "Identifier") return null;
|
|
17
|
+
if (!testExpr.arguments || testExpr.arguments.length !== 1) return null;
|
|
18
|
+
|
|
19
|
+
const arg = testExpr.arguments[0];
|
|
20
|
+
if (arg.type !== "Identifier" && arg.type !== "Literal") return null;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
str: callee.object.name,
|
|
24
|
+
arg: arg.type === "Identifier" ? arg.name : JSON.stringify(arg.value),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function bodyContainsSlice(node: Node, strName: string): boolean {
|
|
29
|
+
// Walk the AST to find .slice() calls on the same variable
|
|
30
|
+
if (!node || typeof node !== "object") return false;
|
|
31
|
+
|
|
32
|
+
// Check if this node is a call to strName.slice(...)
|
|
33
|
+
if (
|
|
34
|
+
node.type === "CallExpression" &&
|
|
35
|
+
node.callee?.type === "MemberExpression" &&
|
|
36
|
+
node.callee.object?.type === "Identifier" &&
|
|
37
|
+
node.callee.object.name === strName &&
|
|
38
|
+
node.callee.property?.type === "Identifier" &&
|
|
39
|
+
node.callee.property.name === "slice"
|
|
40
|
+
) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Recurse into children
|
|
45
|
+
for (const key of Object.keys(node)) {
|
|
46
|
+
if (
|
|
47
|
+
key === "type" ||
|
|
48
|
+
key === "loc" ||
|
|
49
|
+
key === "range" ||
|
|
50
|
+
key === "parent" ||
|
|
51
|
+
key === "start" ||
|
|
52
|
+
key === "end"
|
|
53
|
+
)
|
|
54
|
+
continue;
|
|
55
|
+
const val = node[key];
|
|
56
|
+
if (Array.isArray(val)) {
|
|
57
|
+
for (const child of val) {
|
|
58
|
+
if (child && typeof child === "object" && child.type && bodyContainsSlice(child, strName))
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
} else if (val && typeof val === "object" && val.type && bodyContainsSlice(val, strName)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default {
|
|
69
|
+
create(context: Context) {
|
|
70
|
+
return {
|
|
71
|
+
IfStatement(node: Node) {
|
|
72
|
+
const test = node.test;
|
|
73
|
+
if (!test) return;
|
|
74
|
+
|
|
75
|
+
// Check for str.startsWith(prefix)
|
|
76
|
+
const startsInfo = getStringMethodTarget(test, "startsWith");
|
|
77
|
+
if (startsInfo && bodyContainsSlice(node.consequent, startsInfo.str)) {
|
|
78
|
+
context.report({
|
|
79
|
+
message: `Manual strip: checking \`startsWith()\` then slicing is a manual prefix strip. Consider extracting a \`stripPrefix\` helper. (clippy::manual_strip)`,
|
|
80
|
+
node,
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for str.endsWith(suffix)
|
|
86
|
+
const endsInfo = getStringMethodTarget(test, "endsWith");
|
|
87
|
+
if (endsInfo && bodyContainsSlice(node.consequent, endsInfo.str)) {
|
|
88
|
+
context.report({
|
|
89
|
+
message: `Manual strip: checking \`endsWith()\` then slicing is a manual suffix strip. Consider extracting a \`stripSuffix\` helper. (clippy::manual_strip)`,
|
|
90
|
+
node,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// clippy::manual_swap — three-statement swap pattern
|
|
2
|
+
// Detects: const tmp = a; a = b; b = tmp; → [a, b] = [b, a]
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
function exprName(node: Node): string | null {
|
|
7
|
+
if (node.type === "Identifier") return node.name;
|
|
8
|
+
if (
|
|
9
|
+
node.type === "MemberExpression" &&
|
|
10
|
+
!node.computed &&
|
|
11
|
+
node.object?.type === "Identifier" &&
|
|
12
|
+
node.property?.type === "Identifier"
|
|
13
|
+
) {
|
|
14
|
+
return `${node.object.name}.${node.property.name}`;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function checkBody(body: Node[], context: Context) {
|
|
20
|
+
for (let i = 0; i < body.length - 2; i++) {
|
|
21
|
+
const s1 = body[i]!;
|
|
22
|
+
const s2 = body[i + 1]!;
|
|
23
|
+
const s3 = body[i + 2]!;
|
|
24
|
+
|
|
25
|
+
// s1: const tmp = a;
|
|
26
|
+
if (s1.type !== "VariableDeclaration" || s1.declarations.length !== 1) continue;
|
|
27
|
+
const decl = s1.declarations[0]!;
|
|
28
|
+
if (decl.id?.type !== "Identifier" || !decl.init) continue;
|
|
29
|
+
const tmpName = decl.id.name;
|
|
30
|
+
const aName = exprName(decl.init);
|
|
31
|
+
if (!aName) continue;
|
|
32
|
+
|
|
33
|
+
// s2: a = b;
|
|
34
|
+
if (
|
|
35
|
+
s2.type !== "ExpressionStatement" ||
|
|
36
|
+
s2.expression?.type !== "AssignmentExpression" ||
|
|
37
|
+
s2.expression.operator !== "="
|
|
38
|
+
)
|
|
39
|
+
continue;
|
|
40
|
+
const assignLeft = exprName(s2.expression.left);
|
|
41
|
+
const bName = exprName(s2.expression.right);
|
|
42
|
+
if (assignLeft !== aName || !bName) continue;
|
|
43
|
+
|
|
44
|
+
// s3: b = tmp;
|
|
45
|
+
if (
|
|
46
|
+
s3.type !== "ExpressionStatement" ||
|
|
47
|
+
s3.expression?.type !== "AssignmentExpression" ||
|
|
48
|
+
s3.expression.operator !== "="
|
|
49
|
+
)
|
|
50
|
+
continue;
|
|
51
|
+
const assign2Left = exprName(s3.expression.left);
|
|
52
|
+
const assign2Right = exprName(s3.expression.right);
|
|
53
|
+
if (assign2Left !== bName || assign2Right !== tmpName) continue;
|
|
54
|
+
|
|
55
|
+
context.report({
|
|
56
|
+
message: `Manual swap: use destructuring \`[${aName}, ${bName}] = [${bName}, ${aName}]\` instead of a temp variable. (clippy::manual_swap)`,
|
|
57
|
+
node: s1,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default {
|
|
63
|
+
create(context: Context) {
|
|
64
|
+
return {
|
|
65
|
+
BlockStatement(node: Node) {
|
|
66
|
+
if (node.body) checkBody(node.body, context);
|
|
67
|
+
},
|
|
68
|
+
Program(node: Node) {
|
|
69
|
+
if (node.body) checkBody(node.body, context);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// clippy::map_identity — .map() with an identity function
|
|
2
|
+
// Detects: arr.map(x => x) — a no-op; remove or use .slice() to copy
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
import { isMethodCall } from "../types";
|
|
6
|
+
|
|
7
|
+
function isIdentityClosure(node: Node): boolean {
|
|
8
|
+
// (x) => x
|
|
9
|
+
if (node.type === "ArrowFunctionExpression" && node.params.length === 1) {
|
|
10
|
+
const param = node.params[0]!;
|
|
11
|
+
const body = node.body;
|
|
12
|
+
if (param.type === "Identifier" && body.type === "Identifier" && body.name === param.name) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
// (x) => { return x; }
|
|
16
|
+
if (body.type === "BlockStatement" && body.body.length === 1) {
|
|
17
|
+
const stmt = body.body[0]!;
|
|
18
|
+
if (
|
|
19
|
+
stmt.type === "ReturnStatement" &&
|
|
20
|
+
stmt.argument?.type === "Identifier" &&
|
|
21
|
+
stmt.argument.name === param.name
|
|
22
|
+
) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// function(x) { return x; }
|
|
28
|
+
if (node.type === "FunctionExpression" && node.params.length === 1) {
|
|
29
|
+
const param = node.params[0]!;
|
|
30
|
+
if (
|
|
31
|
+
param.type === "Identifier" &&
|
|
32
|
+
node.body.type === "BlockStatement" &&
|
|
33
|
+
node.body.body.length === 1
|
|
34
|
+
) {
|
|
35
|
+
const stmt = node.body.body[0]!;
|
|
36
|
+
if (
|
|
37
|
+
stmt.type === "ReturnStatement" &&
|
|
38
|
+
stmt.argument?.type === "Identifier" &&
|
|
39
|
+
stmt.argument.name === param.name
|
|
40
|
+
) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default {
|
|
49
|
+
create(context: Context) {
|
|
50
|
+
return {
|
|
51
|
+
CallExpression(node: Node) {
|
|
52
|
+
if (!isMethodCall(node, "map")) return;
|
|
53
|
+
|
|
54
|
+
const args = node.arguments;
|
|
55
|
+
if (!args || args.length !== 1) return;
|
|
56
|
+
|
|
57
|
+
if (isIdentityClosure(args[0]!)) {
|
|
58
|
+
context.report({
|
|
59
|
+
message:
|
|
60
|
+
"Map identity: `.map(x => x)` is a no-op. Remove it, or use `.slice()` / `[...arr]` to copy. (clippy::map_identity)",
|
|
61
|
+
node,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// clippy::map_unit_fn — using .map() for side effects instead of .forEach()
|
|
2
|
+
// Detects: arr.map(fn) where the result is unused (ExpressionStatement)
|
|
3
|
+
// In Rust, this is .map(f) where f returns ()
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
import { isMethodCall } from "../types";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
create(context: Context) {
|
|
10
|
+
return {
|
|
11
|
+
ExpressionStatement(node: Node) {
|
|
12
|
+
const expr = node.expression;
|
|
13
|
+
if (!isMethodCall(expr, "map")) return;
|
|
14
|
+
|
|
15
|
+
context.report({
|
|
16
|
+
message:
|
|
17
|
+
"Map void return: `.map()` result is unused. Use `.forEach()` for side effects. (clippy::map_unit_fn)",
|
|
18
|
+
node,
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// clippy::match_same_arms — switch cases with identical bodies
|
|
2
|
+
// Detects: switch where two or more cases have the same code.
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
function caseBodySource(caseNode: Node, sourceText: string): string {
|
|
7
|
+
const stmts: Node[] = caseNode.consequent;
|
|
8
|
+
if (!stmts || stmts.length === 0) return "";
|
|
9
|
+
// Exclude break statements from comparison
|
|
10
|
+
const meaningful = stmts.filter((s: Node) => s.type !== "BreakStatement");
|
|
11
|
+
if (meaningful.length === 0) return "";
|
|
12
|
+
const first = meaningful[0]!;
|
|
13
|
+
const last = meaningful[meaningful.length - 1]!;
|
|
14
|
+
if (first.start == null || last.end == null) return "";
|
|
15
|
+
return sourceText.slice(first.start, last.end);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
create(context: Context) {
|
|
20
|
+
return {
|
|
21
|
+
SwitchStatement(node: Node) {
|
|
22
|
+
const cases: Node[] = node.cases;
|
|
23
|
+
if (!cases || cases.length < 2) return;
|
|
24
|
+
|
|
25
|
+
const seen = new Map<string, Node>();
|
|
26
|
+
|
|
27
|
+
for (const c of cases) {
|
|
28
|
+
if (c.test === null) continue; // skip default
|
|
29
|
+
const body = caseBodySource(c, context.sourceCode.text);
|
|
30
|
+
if (!body) continue;
|
|
31
|
+
|
|
32
|
+
const existing = seen.get(body);
|
|
33
|
+
if (existing) {
|
|
34
|
+
context.report({
|
|
35
|
+
message:
|
|
36
|
+
"Match same arms: multiple switch cases have identical bodies. Consider combining them. (clippy::match_same_arms)",
|
|
37
|
+
node: c,
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
seen.set(body, c);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// clippy::needless_bool — if/else returning boolean literals
|
|
2
|
+
// Detects: if (c) { return true } else { return false } → return c
|
|
3
|
+
// Detects: if (c) { x = true } else { x = false } → x = c
|
|
4
|
+
// NOT covered by eslint/no-unneeded-ternary (which only catches ternaries)
|
|
5
|
+
|
|
6
|
+
import type { Context, Node } from "../types";
|
|
7
|
+
import { isBoolLiteral, unwrapBlock } from "../types";
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
create(context: Context) {
|
|
11
|
+
return {
|
|
12
|
+
IfStatement(node: Node) {
|
|
13
|
+
if (!node.alternate) return;
|
|
14
|
+
|
|
15
|
+
const consequent = unwrapBlock(node.consequent);
|
|
16
|
+
const alternate = unwrapBlock(node.alternate);
|
|
17
|
+
if (!consequent || !alternate) return;
|
|
18
|
+
|
|
19
|
+
// Case 1: if (c) return true; else return false;
|
|
20
|
+
if (consequent.type === "ReturnStatement" && alternate.type === "ReturnStatement") {
|
|
21
|
+
const cArg = consequent.argument;
|
|
22
|
+
const aArg = alternate.argument;
|
|
23
|
+
if (isBoolLiteral(cArg) && isBoolLiteral(aArg) && cArg.value !== aArg.value) {
|
|
24
|
+
const suggestion = cArg.value ? "return <condition>" : "return !<condition>";
|
|
25
|
+
context.report({
|
|
26
|
+
message: `Needless bool: this if/else returns opposing booleans. Simplify to \`${suggestion}\`. (clippy::needless_bool)`,
|
|
27
|
+
node,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Case 2: if (c) { x = true } else { x = false }
|
|
34
|
+
if (
|
|
35
|
+
consequent.type === "ExpressionStatement" &&
|
|
36
|
+
alternate.type === "ExpressionStatement" &&
|
|
37
|
+
consequent.expression.type === "AssignmentExpression" &&
|
|
38
|
+
alternate.expression.type === "AssignmentExpression" &&
|
|
39
|
+
consequent.expression.operator === "=" &&
|
|
40
|
+
alternate.expression.operator === "="
|
|
41
|
+
) {
|
|
42
|
+
const cExpr = consequent.expression;
|
|
43
|
+
const aExpr = alternate.expression;
|
|
44
|
+
|
|
45
|
+
// Check both assign to the same variable
|
|
46
|
+
if (
|
|
47
|
+
cExpr.left.type === "Identifier" &&
|
|
48
|
+
aExpr.left.type === "Identifier" &&
|
|
49
|
+
cExpr.left.name === aExpr.left.name &&
|
|
50
|
+
isBoolLiteral(cExpr.right) &&
|
|
51
|
+
isBoolLiteral(aExpr.right) &&
|
|
52
|
+
cExpr.right.value !== aExpr.right.value
|
|
53
|
+
) {
|
|
54
|
+
const varName = cExpr.left.name;
|
|
55
|
+
const suggestion = cExpr.right.value
|
|
56
|
+
? `${varName} = <condition>`
|
|
57
|
+
: `${varName} = !<condition>`;
|
|
58
|
+
context.report({
|
|
59
|
+
message: `Needless bool: this if/else assigns opposing booleans. Simplify to \`${suggestion}\`. (clippy::needless_bool)`,
|
|
60
|
+
node,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// clippy::needless_continue — unnecessary continue at the end of a loop body
|
|
2
|
+
// Detects: for (...) { ...; continue; } — the continue is a no-op
|
|
3
|
+
// Detects: for (...) { if (cond) { stuff } else { continue; } } — else continue is redundant
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
|
|
7
|
+
function checkLoopBody(body: Node, context: Context) {
|
|
8
|
+
if (body.type !== "BlockStatement" || body.body.length === 0) return;
|
|
9
|
+
|
|
10
|
+
const stmts: Node[] = body.body;
|
|
11
|
+
const last = stmts[stmts.length - 1]!;
|
|
12
|
+
|
|
13
|
+
// Direct: last statement is bare continue
|
|
14
|
+
if (last.type === "ContinueStatement" && !last.label) {
|
|
15
|
+
context.report({
|
|
16
|
+
message:
|
|
17
|
+
"Needless continue: `continue` at the end of a loop body has no effect. (clippy::needless_continue)",
|
|
18
|
+
node: last,
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Else-continue: if (...) { ... } else { continue; }
|
|
24
|
+
if (last.type === "IfStatement" && last.alternate) {
|
|
25
|
+
const alt = last.alternate;
|
|
26
|
+
const altBody = alt.type === "BlockStatement" ? alt.body : [alt];
|
|
27
|
+
if (altBody.length === 1 && altBody[0]!.type === "ContinueStatement" && !altBody[0]!.label) {
|
|
28
|
+
context.report({
|
|
29
|
+
message:
|
|
30
|
+
"Needless continue: `else { continue; }` at the end of a loop is redundant. Remove the else branch. (clippy::needless_continue)",
|
|
31
|
+
node: altBody[0]!,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
create(context: Context) {
|
|
39
|
+
return {
|
|
40
|
+
ForStatement: (node: Node) => checkLoopBody(node.body, context),
|
|
41
|
+
ForOfStatement: (node: Node) => checkLoopBody(node.body, context),
|
|
42
|
+
ForInStatement: (node: Node) => checkLoopBody(node.body, context),
|
|
43
|
+
WhileStatement: (node: Node) => checkLoopBody(node.body, context),
|
|
44
|
+
DoWhileStatement: (node: Node) => checkLoopBody(node.body, context),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// clippy::needless_late_init — variable declared without init then immediately assigned
|
|
2
|
+
// Detects: let x; x = expr; → const x = expr;
|
|
3
|
+
// Also: let x; if (c) { x = a; } else { x = b; } → const x = c ? a : b;
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
|
|
7
|
+
function checkBody(body: Node[], context: Context) {
|
|
8
|
+
for (let i = 0; i < body.length - 1; i++) {
|
|
9
|
+
const decl = body[i]!;
|
|
10
|
+
const next = body[i + 1]!;
|
|
11
|
+
|
|
12
|
+
if (decl.type !== "VariableDeclaration" || decl.declarations.length !== 1) continue;
|
|
13
|
+
const d = decl.declarations[0]!;
|
|
14
|
+
if (d.id?.type !== "Identifier" || d.init !== null) continue;
|
|
15
|
+
const varName = d.id.name;
|
|
16
|
+
|
|
17
|
+
// Case 1: let x; x = expr;
|
|
18
|
+
if (
|
|
19
|
+
next.type === "ExpressionStatement" &&
|
|
20
|
+
next.expression?.type === "AssignmentExpression" &&
|
|
21
|
+
next.expression.operator === "=" &&
|
|
22
|
+
next.expression.left?.type === "Identifier" &&
|
|
23
|
+
next.expression.left.name === varName
|
|
24
|
+
) {
|
|
25
|
+
context.report({
|
|
26
|
+
message: `Needless late init: \`${varName}\` is declared then immediately assigned. Initialize on declaration with \`const\`. (clippy::needless_late_init)`,
|
|
27
|
+
node: decl,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
create(context: Context) {
|
|
35
|
+
return {
|
|
36
|
+
BlockStatement(node: Node) {
|
|
37
|
+
if (node.body) checkBody(node.body, context);
|
|
38
|
+
},
|
|
39
|
+
Program(node: Node) {
|
|
40
|
+
if (node.body) checkBody(node.body, context);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// clippy::needless_range_loop — index-based loop where for-of suffices
|
|
2
|
+
// Detects: for (let i = 0; i < arr.length; i++) { ...arr[i]... }
|
|
3
|
+
// when `i` is only used for indexing into `arr`, not for other purposes.
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
|
|
7
|
+
function isZeroInit(init: Node): string | null {
|
|
8
|
+
if (init.type !== "VariableDeclaration" || init.declarations.length !== 1) return null;
|
|
9
|
+
const decl = init.declarations[0]!;
|
|
10
|
+
if (decl.id?.type !== "Identifier") return null;
|
|
11
|
+
if (decl.init?.type !== "Literal" || decl.init.value !== 0) return null;
|
|
12
|
+
return decl.id.name;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isLengthTest(test: Node, indexVar: string): string | null {
|
|
16
|
+
if (test.type !== "BinaryExpression" || test.operator !== "<") return null;
|
|
17
|
+
if (test.left?.type !== "Identifier" || test.left.name !== indexVar) return null;
|
|
18
|
+
// arr.length
|
|
19
|
+
if (
|
|
20
|
+
test.right?.type === "MemberExpression" &&
|
|
21
|
+
test.right.object?.type === "Identifier" &&
|
|
22
|
+
test.right.property?.type === "Identifier" &&
|
|
23
|
+
test.right.property.name === "length"
|
|
24
|
+
) {
|
|
25
|
+
return test.right.object.name;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isIncrement(update: Node, indexVar: string): boolean {
|
|
31
|
+
// i++ or i += 1
|
|
32
|
+
if (
|
|
33
|
+
update.type === "UpdateExpression" &&
|
|
34
|
+
update.operator === "++" &&
|
|
35
|
+
update.argument?.type === "Identifier" &&
|
|
36
|
+
update.argument.name === indexVar
|
|
37
|
+
)
|
|
38
|
+
return true;
|
|
39
|
+
if (
|
|
40
|
+
update.type === "AssignmentExpression" &&
|
|
41
|
+
update.operator === "+=" &&
|
|
42
|
+
update.left?.type === "Identifier" &&
|
|
43
|
+
update.left.name === indexVar &&
|
|
44
|
+
update.right?.type === "Literal" &&
|
|
45
|
+
update.right.value === 1
|
|
46
|
+
)
|
|
47
|
+
return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Check if `indexVar` is used in the body ONLY as arr[indexVar] and nowhere else */
|
|
52
|
+
function onlyUsedAsIndex(body: Node, indexVar: string, arrName: string): boolean {
|
|
53
|
+
let onlyIndexAccess = true;
|
|
54
|
+
|
|
55
|
+
function walk(n: Node) {
|
|
56
|
+
if (!n || typeof n !== "object" || !n.type) return;
|
|
57
|
+
if (!onlyIndexAccess) return;
|
|
58
|
+
|
|
59
|
+
if (n.type === "Identifier" && n.name === indexVar) {
|
|
60
|
+
// This identifier reference to indexVar — check if parent is arr[i]
|
|
61
|
+
// We can't check parent easily, so we take a different approach
|
|
62
|
+
onlyIndexAccess = false;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
n.type === "MemberExpression" &&
|
|
68
|
+
n.computed &&
|
|
69
|
+
n.object?.type === "Identifier" &&
|
|
70
|
+
n.object.name === arrName &&
|
|
71
|
+
n.property?.type === "Identifier" &&
|
|
72
|
+
n.property.name === indexVar
|
|
73
|
+
) {
|
|
74
|
+
// This is arr[i] — skip recursing into property to avoid the false hit
|
|
75
|
+
walk(n.object);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const key of Object.keys(n)) {
|
|
80
|
+
if (
|
|
81
|
+
key === "type" ||
|
|
82
|
+
key === "loc" ||
|
|
83
|
+
key === "range" ||
|
|
84
|
+
key === "parent" ||
|
|
85
|
+
key === "start" ||
|
|
86
|
+
key === "end"
|
|
87
|
+
)
|
|
88
|
+
continue;
|
|
89
|
+
const val = n[key];
|
|
90
|
+
if (Array.isArray(val)) {
|
|
91
|
+
for (const child of val) {
|
|
92
|
+
if (child && typeof child === "object" && child.type) walk(child);
|
|
93
|
+
}
|
|
94
|
+
} else if (val && typeof val === "object" && val.type) {
|
|
95
|
+
walk(val);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
walk(body);
|
|
101
|
+
return onlyIndexAccess;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default {
|
|
105
|
+
create(context: Context) {
|
|
106
|
+
return {
|
|
107
|
+
ForStatement(node: Node) {
|
|
108
|
+
if (!node.init || !node.test || !node.update) return;
|
|
109
|
+
|
|
110
|
+
const indexVar = isZeroInit(node.init);
|
|
111
|
+
if (!indexVar) return;
|
|
112
|
+
|
|
113
|
+
const arrName = isLengthTest(node.test, indexVar);
|
|
114
|
+
if (!arrName) return;
|
|
115
|
+
|
|
116
|
+
if (!isIncrement(node.update, indexVar)) return;
|
|
117
|
+
|
|
118
|
+
if (onlyUsedAsIndex(node.body, indexVar, arrName)) {
|
|
119
|
+
context.report({
|
|
120
|
+
message: `Needless range loop: \`${indexVar}\` is only used to index \`${arrName}\`. Use \`for (const item of ${arrName})\` instead. (clippy::needless_range_loop)`,
|
|
121
|
+
node,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// clippy::neg_multiply — multiplication by -1 instead of negation
|
|
2
|
+
// Detects: x * -1 → -x, -1 * x → -x
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
function isNegOne(node: Node): boolean {
|
|
7
|
+
if (
|
|
8
|
+
node.type === "UnaryExpression" &&
|
|
9
|
+
node.operator === "-" &&
|
|
10
|
+
node.argument.type === "Literal" &&
|
|
11
|
+
node.argument.value === 1
|
|
12
|
+
) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
// Also handle literal -1 (some parsers represent it as Literal with value -1)
|
|
16
|
+
if (node.type === "Literal" && node.value === -1) return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
create(context: Context) {
|
|
22
|
+
return {
|
|
23
|
+
BinaryExpression(node: Node) {
|
|
24
|
+
if (node.operator !== "*") return;
|
|
25
|
+
|
|
26
|
+
if (isNegOne(node.left) || isNegOne(node.right)) {
|
|
27
|
+
context.report({
|
|
28
|
+
message:
|
|
29
|
+
"Neg multiply: multiplying by -1 is less readable than negation. Use `-x` instead of `x * -1`. (clippy::neg_multiply)",
|
|
30
|
+
node,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|