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,108 @@
|
|
|
1
|
+
// clippy::struct_field_names — interface/type with redundantly prefixed or suffixed fields
|
|
2
|
+
// Detects: interface User { userName, userEmail, userAge } — "user" prefix is redundant
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
function getFieldNames(node: Node): string[] {
|
|
7
|
+
const body = node.body?.body ?? node.body?.members ?? node.members;
|
|
8
|
+
if (!Array.isArray(body)) return [];
|
|
9
|
+
return body
|
|
10
|
+
.map((m: Node) => {
|
|
11
|
+
if (m.type === "TSPropertySignature" || m.type === "PropertyDefinition") {
|
|
12
|
+
return m.key?.type === "Identifier" ? m.key.name : null;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
})
|
|
16
|
+
.filter((n: string | null): n is string => n !== null);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function commonPrefix(names: string[]): string {
|
|
20
|
+
if (names.length < 2) return "";
|
|
21
|
+
let prefix = names[0]!;
|
|
22
|
+
for (let i = 1; i < names.length; i++) {
|
|
23
|
+
const name = names[i]!;
|
|
24
|
+
let j = 0;
|
|
25
|
+
while (
|
|
26
|
+
j < prefix.length &&
|
|
27
|
+
j < name.length &&
|
|
28
|
+
prefix[j]!.toLowerCase() === name[j]!.toLowerCase()
|
|
29
|
+
)
|
|
30
|
+
j++;
|
|
31
|
+
prefix = prefix.slice(0, j);
|
|
32
|
+
}
|
|
33
|
+
if (!prefix) return "";
|
|
34
|
+
// Check that prefix ends at a word boundary in the original names
|
|
35
|
+
// For camelCase: the next char after prefix in any name should be uppercase or absent
|
|
36
|
+
// For snake_case: prefix should end with _
|
|
37
|
+
if (prefix.endsWith("_")) return prefix;
|
|
38
|
+
// Check camelCase boundary: char at prefix.length in first name that's longer should be uppercase
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
if (name.length > prefix.length) {
|
|
41
|
+
const nextChar = name[prefix.length]!;
|
|
42
|
+
if (nextChar >= "A" && nextChar <= "Z") return prefix;
|
|
43
|
+
// Not a word boundary
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return prefix;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function commonSuffix(names: string[]): string {
|
|
51
|
+
if (names.length < 2) return "";
|
|
52
|
+
// Find raw common suffix
|
|
53
|
+
let suffix = names[0]!;
|
|
54
|
+
for (let i = 1; i < names.length; i++) {
|
|
55
|
+
const name = names[i]!;
|
|
56
|
+
let j = 0;
|
|
57
|
+
while (
|
|
58
|
+
j < suffix.length &&
|
|
59
|
+
j < name.length &&
|
|
60
|
+
suffix[suffix.length - 1 - j] === name[name.length - 1 - j]
|
|
61
|
+
)
|
|
62
|
+
j++;
|
|
63
|
+
suffix = suffix.slice(suffix.length - j);
|
|
64
|
+
}
|
|
65
|
+
if (!suffix) return "";
|
|
66
|
+
// Check word boundary: suffix should start with uppercase (camelCase) or _
|
|
67
|
+
if (suffix[0]! >= "A" && suffix[0]! <= "Z") return suffix;
|
|
68
|
+
if (suffix.startsWith("_")) return suffix;
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default {
|
|
73
|
+
create(context: Context) {
|
|
74
|
+
function check(node: Node, typeName: string | undefined) {
|
|
75
|
+
if (!typeName) return;
|
|
76
|
+
const names = getFieldNames(node);
|
|
77
|
+
if (names.length < 2) return;
|
|
78
|
+
|
|
79
|
+
const prefix = commonPrefix(names);
|
|
80
|
+
if (prefix.length >= 3) {
|
|
81
|
+
context.report({
|
|
82
|
+
message: `Struct field names: all fields of \`${typeName}\` share the prefix \`${prefix}\`. Remove it — the type name provides context. (clippy::struct_field_names)`,
|
|
83
|
+
node,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const suffix = commonSuffix(names);
|
|
89
|
+
if (suffix.length >= 3) {
|
|
90
|
+
context.report({
|
|
91
|
+
message: `Struct field names: all fields of \`${typeName}\` share the suffix \`${suffix}\`. Consider removing it. (clippy::struct_field_names)`,
|
|
92
|
+
node,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
TSInterfaceDeclaration(node: Node) {
|
|
99
|
+
check(node, node.id?.name);
|
|
100
|
+
},
|
|
101
|
+
TSTypeAliasDeclaration(node: Node) {
|
|
102
|
+
if (node.typeAnnotation?.type === "TSTypeLiteral") {
|
|
103
|
+
check(node.typeAnnotation, node.id?.name);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// clippy::too_many_arguments — functions with too many parameters
|
|
2
|
+
// Default threshold: 5 (Clippy uses 7, but JS convention favors options objects earlier)
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
const THRESHOLD = 5;
|
|
7
|
+
|
|
8
|
+
function checkParams(node: Node, context: Context) {
|
|
9
|
+
const params: Node[] = node.params;
|
|
10
|
+
if (!params || params.length <= THRESHOLD) return;
|
|
11
|
+
|
|
12
|
+
const name =
|
|
13
|
+
node.id?.name ??
|
|
14
|
+
(node.parent?.type === "VariableDeclarator" ? node.parent.id?.name : null) ??
|
|
15
|
+
"<anonymous>";
|
|
16
|
+
|
|
17
|
+
context.report({
|
|
18
|
+
message: `Too many arguments: \`${name}\` has ${params.length} parameters (max ${THRESHOLD}). Consider using an options object. (clippy::too_many_arguments)`,
|
|
19
|
+
node,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
create(context: Context) {
|
|
25
|
+
return {
|
|
26
|
+
FunctionDeclaration(node: Node) {
|
|
27
|
+
checkParams(node, context);
|
|
28
|
+
},
|
|
29
|
+
FunctionExpression(node: Node) {
|
|
30
|
+
checkParams(node, context);
|
|
31
|
+
},
|
|
32
|
+
ArrowFunctionExpression(node: Node) {
|
|
33
|
+
checkParams(node, context);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// clippy::too_many_lines — functions exceeding a line count threshold
|
|
2
|
+
// Default threshold: 100 lines (matches Clippy default)
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
const THRESHOLD = 100;
|
|
7
|
+
|
|
8
|
+
function countLines(node: Node, sourceText: string): number {
|
|
9
|
+
if (node.loc) return node.loc.end.line - node.loc.start.line + 1;
|
|
10
|
+
if (node.start != null && node.end != null) {
|
|
11
|
+
return sourceText.substring(node.start, node.end).split("\n").length;
|
|
12
|
+
}
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function checkLines(node: Node, context: Context) {
|
|
17
|
+
const lines = countLines(node, context.sourceCode.text);
|
|
18
|
+
if (lines <= THRESHOLD) return;
|
|
19
|
+
|
|
20
|
+
const name =
|
|
21
|
+
node.id?.name ??
|
|
22
|
+
(node.parent?.type === "VariableDeclarator" ? node.parent.id?.name : null) ??
|
|
23
|
+
"<anonymous>";
|
|
24
|
+
|
|
25
|
+
context.report({
|
|
26
|
+
message: `Too many lines: \`${name}\` is ${lines} lines long (max ${THRESHOLD}). Consider breaking it into smaller functions. (clippy::too_many_lines)`,
|
|
27
|
+
node,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
create(context: Context) {
|
|
33
|
+
return {
|
|
34
|
+
FunctionDeclaration(node: Node) {
|
|
35
|
+
checkLines(node, context);
|
|
36
|
+
},
|
|
37
|
+
FunctionExpression(node: Node) {
|
|
38
|
+
checkLines(node, context);
|
|
39
|
+
},
|
|
40
|
+
ArrowFunctionExpression(node: Node) {
|
|
41
|
+
checkLines(node, context);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// clippy::unnecessary_fold — .reduce() that could be .some() or .every()
|
|
2
|
+
// Detects: arr.reduce((acc, x) => acc || cond(x), false) → arr.some(cond)
|
|
3
|
+
// Detects: arr.reduce((acc, x) => acc && cond(x), true) → arr.every(cond)
|
|
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
|
+
CallExpression(node: Node) {
|
|
12
|
+
if (!isMethodCall(node, "reduce")) return;
|
|
13
|
+
const args = node.arguments;
|
|
14
|
+
if (!args || args.length !== 2) return;
|
|
15
|
+
|
|
16
|
+
const callback = args[0]!;
|
|
17
|
+
const initial = args[1]!;
|
|
18
|
+
|
|
19
|
+
// Callback must be (acc, x) => acc OP expr
|
|
20
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")
|
|
21
|
+
return;
|
|
22
|
+
if (callback.params.length !== 2) return;
|
|
23
|
+
const accParam = callback.params[0]!;
|
|
24
|
+
if (accParam.type !== "Identifier") return;
|
|
25
|
+
const accName = accParam.name;
|
|
26
|
+
|
|
27
|
+
// Get the body expression
|
|
28
|
+
let bodyExpr: Node | null = null;
|
|
29
|
+
if (callback.body.type === "BlockStatement") {
|
|
30
|
+
if (
|
|
31
|
+
callback.body.body.length === 1 &&
|
|
32
|
+
callback.body.body[0]!.type === "ReturnStatement"
|
|
33
|
+
) {
|
|
34
|
+
bodyExpr = callback.body.body[0]!.argument;
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
bodyExpr = callback.body;
|
|
38
|
+
}
|
|
39
|
+
if (!bodyExpr || bodyExpr.type !== "LogicalExpression") return;
|
|
40
|
+
|
|
41
|
+
// acc || expr with initial false → .some()
|
|
42
|
+
if (bodyExpr.operator === "||" && isLiteral(initial, false)) {
|
|
43
|
+
if (bodyExpr.left.type === "Identifier" && bodyExpr.left.name === accName) {
|
|
44
|
+
context.report({
|
|
45
|
+
message:
|
|
46
|
+
"Unnecessary fold: this `.reduce()` with `||` and initial `false` can be replaced with `.some()`. (clippy::unnecessary_fold)",
|
|
47
|
+
node,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// acc && expr with initial true → .every()
|
|
53
|
+
if (bodyExpr.operator === "&&" && isLiteral(initial, true)) {
|
|
54
|
+
if (bodyExpr.left.type === "Identifier" && bodyExpr.left.name === accName) {
|
|
55
|
+
context.report({
|
|
56
|
+
message:
|
|
57
|
+
"Unnecessary fold: this `.reduce()` with `&&` and initial `true` can be replaced with `.every()`. (clippy::unnecessary_fold)",
|
|
58
|
+
node,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// clippy principle: "prefer combinators" — .reduce() that builds an array is really .map() or .filter()
|
|
2
|
+
//
|
|
3
|
+
// Detects: arr.reduce((acc, x) => { acc.push(f(x)); return acc; }, []) → arr.map(f)
|
|
4
|
+
// Detects: arr.reduce((acc, x) => { if (c(x)) acc.push(x); return acc; }, []) → arr.filter(c)
|
|
5
|
+
// Detects: arr.reduce((acc, x) => { acc.push(...f(x)); return acc; }, []) → arr.flatMap(f)
|
|
6
|
+
|
|
7
|
+
import type { Context, Node } from "../types";
|
|
8
|
+
import { isMethodCall } from "../types";
|
|
9
|
+
|
|
10
|
+
function isEmptyArray(node: Node): boolean {
|
|
11
|
+
return node.type === "ArrayExpression" && (!node.elements || node.elements.length === 0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
create(context: Context) {
|
|
16
|
+
return {
|
|
17
|
+
CallExpression(node: Node) {
|
|
18
|
+
if (!isMethodCall(node, "reduce")) return;
|
|
19
|
+
const args = node.arguments;
|
|
20
|
+
if (!args || args.length !== 2) return;
|
|
21
|
+
|
|
22
|
+
const callback = args[0]!;
|
|
23
|
+
const init = args[1]!;
|
|
24
|
+
|
|
25
|
+
if (!isEmptyArray(init)) return;
|
|
26
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")
|
|
27
|
+
return;
|
|
28
|
+
if (callback.params.length !== 2) return;
|
|
29
|
+
|
|
30
|
+
const accParam = callback.params[0]!;
|
|
31
|
+
if (accParam.type !== "Identifier") return;
|
|
32
|
+
const accName = accParam.name;
|
|
33
|
+
|
|
34
|
+
// Get the body statements
|
|
35
|
+
let bodyStmts: Node[] | null = null;
|
|
36
|
+
if (callback.body.type === "BlockStatement") {
|
|
37
|
+
bodyStmts = callback.body.body;
|
|
38
|
+
}
|
|
39
|
+
if (!bodyStmts) return;
|
|
40
|
+
|
|
41
|
+
// Pattern: { acc.push(...); return acc; }
|
|
42
|
+
if (bodyStmts.length === 2) {
|
|
43
|
+
const pushStmt = bodyStmts[0]!;
|
|
44
|
+
const returnStmt = bodyStmts[1]!;
|
|
45
|
+
|
|
46
|
+
if (returnStmt.type !== "ReturnStatement") return;
|
|
47
|
+
if (returnStmt.argument?.type !== "Identifier" || returnStmt.argument.name !== accName)
|
|
48
|
+
return;
|
|
49
|
+
|
|
50
|
+
// Check for acc.push(expr)
|
|
51
|
+
if (
|
|
52
|
+
pushStmt.type === "ExpressionStatement" &&
|
|
53
|
+
pushStmt.expression?.type === "CallExpression" &&
|
|
54
|
+
pushStmt.expression.callee?.type === "MemberExpression" &&
|
|
55
|
+
pushStmt.expression.callee.object?.type === "Identifier" &&
|
|
56
|
+
pushStmt.expression.callee.object.name === accName &&
|
|
57
|
+
pushStmt.expression.callee.property?.type === "Identifier" &&
|
|
58
|
+
pushStmt.expression.callee.property.name === "push" &&
|
|
59
|
+
pushStmt.expression.arguments?.length === 1
|
|
60
|
+
) {
|
|
61
|
+
context.report({
|
|
62
|
+
message:
|
|
63
|
+
"Unnecessary reduce: this `.reduce()` builds an array with `.push()`. Use `.map()` instead. (clippy: prefer combinators)",
|
|
64
|
+
node,
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Pattern: { if (cond) acc.push(x); return acc; }
|
|
71
|
+
if (bodyStmts.length === 2) {
|
|
72
|
+
const ifStmt = bodyStmts[0]!;
|
|
73
|
+
const returnStmt = bodyStmts[1]!;
|
|
74
|
+
|
|
75
|
+
if (returnStmt.type !== "ReturnStatement") return;
|
|
76
|
+
if (returnStmt.argument?.type !== "Identifier" || returnStmt.argument.name !== accName)
|
|
77
|
+
return;
|
|
78
|
+
|
|
79
|
+
if (ifStmt.type === "IfStatement" && !ifStmt.alternate) {
|
|
80
|
+
const consequent = ifStmt.consequent;
|
|
81
|
+
let pushExpr: Node | null = null;
|
|
82
|
+
|
|
83
|
+
if (consequent.type === "BlockStatement" && consequent.body.length === 1) {
|
|
84
|
+
pushExpr = consequent.body[0]!;
|
|
85
|
+
} else if (consequent.type === "ExpressionStatement") {
|
|
86
|
+
pushExpr = consequent;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
pushExpr?.type === "ExpressionStatement" &&
|
|
91
|
+
pushExpr.expression?.type === "CallExpression" &&
|
|
92
|
+
pushExpr.expression.callee?.type === "MemberExpression" &&
|
|
93
|
+
pushExpr.expression.callee.object?.type === "Identifier" &&
|
|
94
|
+
pushExpr.expression.callee.object.name === accName &&
|
|
95
|
+
pushExpr.expression.callee.property?.type === "Identifier" &&
|
|
96
|
+
pushExpr.expression.callee.property.name === "push"
|
|
97
|
+
) {
|
|
98
|
+
context.report({
|
|
99
|
+
message:
|
|
100
|
+
"Unnecessary reduce: this `.reduce()` conditionally pushes items. Use `.filter()` instead. (clippy: prefer combinators)",
|
|
101
|
+
node,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// clippy::unreadable_literal — large numeric literals without digit separators
|
|
2
|
+
// Detects: 1000000 → 1_000_000, 0xFF00FF → 0xFF_00_FF
|
|
3
|
+
// Threshold: integers >= 10_000, floats >= 100_000
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
|
|
7
|
+
const INT_THRESHOLD = 10_000;
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
create(context: Context) {
|
|
11
|
+
return {
|
|
12
|
+
Literal(node: Node) {
|
|
13
|
+
if (typeof node.value !== "number") return;
|
|
14
|
+
if (!Number.isFinite(node.value)) return;
|
|
15
|
+
|
|
16
|
+
const raw: string | undefined = node.raw;
|
|
17
|
+
if (!raw) return;
|
|
18
|
+
|
|
19
|
+
// Skip if already has underscores
|
|
20
|
+
if (raw.includes("_")) return;
|
|
21
|
+
|
|
22
|
+
// Skip hex/octal/binary (different grouping rules)
|
|
23
|
+
if (
|
|
24
|
+
raw.startsWith("0x") ||
|
|
25
|
+
raw.startsWith("0o") ||
|
|
26
|
+
raw.startsWith("0b") ||
|
|
27
|
+
raw.startsWith("0X") ||
|
|
28
|
+
raw.startsWith("0O") ||
|
|
29
|
+
raw.startsWith("0B")
|
|
30
|
+
)
|
|
31
|
+
return;
|
|
32
|
+
|
|
33
|
+
// Skip floats with decimal points (e.g. 3.14)
|
|
34
|
+
if (raw.includes(".")) return;
|
|
35
|
+
|
|
36
|
+
// Skip scientific notation
|
|
37
|
+
if (raw.includes("e") || raw.includes("E")) return;
|
|
38
|
+
|
|
39
|
+
const absVal = Math.abs(node.value);
|
|
40
|
+
if (absVal >= INT_THRESHOLD && Number.isInteger(absVal)) {
|
|
41
|
+
context.report({
|
|
42
|
+
message: `Unreadable literal: \`${raw}\` is hard to read. Use numeric separators: \`${formatWithSeparators(raw)}\`. (clippy::unreadable_literal)`,
|
|
43
|
+
node,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function formatWithSeparators(raw: string): string {
|
|
52
|
+
const negative = raw.startsWith("-");
|
|
53
|
+
const digits = negative ? raw.slice(1) : raw;
|
|
54
|
+
const parts: string[] = [];
|
|
55
|
+
for (let i = digits.length; i > 0; i -= 3) {
|
|
56
|
+
parts.unshift(digits.slice(Math.max(0, i - 3), i));
|
|
57
|
+
}
|
|
58
|
+
return (negative ? "-" : "") + parts.join("_");
|
|
59
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// clippy::used_underscore_binding — variables prefixed with _ that are actually used
|
|
2
|
+
// Convention: _ prefix means "intentionally unused." Using such variables is confusing.
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
create(context: Context) {
|
|
8
|
+
const declared = new Map<string, Node>();
|
|
9
|
+
const used = new Set<string>();
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
VariableDeclarator(node: Node) {
|
|
13
|
+
if (
|
|
14
|
+
node.id?.type === "Identifier" &&
|
|
15
|
+
node.id.name.startsWith("_") &&
|
|
16
|
+
node.id.name !== "_"
|
|
17
|
+
) {
|
|
18
|
+
declared.set(node.id.name, node.id);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
Identifier(node: Node) {
|
|
22
|
+
if (node.name?.startsWith("_") && node.name !== "_") {
|
|
23
|
+
// Only count as "used" if it's not the declaration site
|
|
24
|
+
if (node.parent?.type !== "VariableDeclarator" || node.parent.id !== node) {
|
|
25
|
+
used.add(node.name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"Program:exit"() {
|
|
30
|
+
for (const [name, declNode] of declared) {
|
|
31
|
+
if (used.has(name)) {
|
|
32
|
+
context.report({
|
|
33
|
+
message: `Used underscore binding: \`${name}\` starts with \`_\` (conventionally unused) but is actually used. Remove the underscore prefix. (clippy::used_underscore_binding)`,
|
|
34
|
+
node: declNode,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// clippy::useless_conversion — redundant type conversions
|
|
2
|
+
// Detects: String("hello"), Number(42), Boolean(true), Array.from([...])
|
|
3
|
+
// Also: "str".toString(), (42).valueOf()
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
import { isIdentifier, isCallOf } from "../types";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
create(context: Context) {
|
|
10
|
+
return {
|
|
11
|
+
CallExpression(node: Node) {
|
|
12
|
+
const { callee } = node;
|
|
13
|
+
const args = node.arguments;
|
|
14
|
+
if (!args || args.length !== 1) return;
|
|
15
|
+
const arg = args[0];
|
|
16
|
+
|
|
17
|
+
// String("literal string")
|
|
18
|
+
if (
|
|
19
|
+
isIdentifier(callee, "String") &&
|
|
20
|
+
arg.type === "Literal" &&
|
|
21
|
+
typeof arg.value === "string"
|
|
22
|
+
) {
|
|
23
|
+
context.report({
|
|
24
|
+
message:
|
|
25
|
+
"Useless conversion: `String()` called on a string literal. (clippy::useless_conversion)",
|
|
26
|
+
node,
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Number(42)
|
|
32
|
+
if (
|
|
33
|
+
isIdentifier(callee, "Number") &&
|
|
34
|
+
arg.type === "Literal" &&
|
|
35
|
+
typeof arg.value === "number"
|
|
36
|
+
) {
|
|
37
|
+
context.report({
|
|
38
|
+
message:
|
|
39
|
+
"Useless conversion: `Number()` called on a number literal. (clippy::useless_conversion)",
|
|
40
|
+
node,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Boolean(true/false)
|
|
46
|
+
if (
|
|
47
|
+
isIdentifier(callee, "Boolean") &&
|
|
48
|
+
arg.type === "Literal" &&
|
|
49
|
+
typeof arg.value === "boolean"
|
|
50
|
+
) {
|
|
51
|
+
context.report({
|
|
52
|
+
message:
|
|
53
|
+
"Useless conversion: `Boolean()` called on a boolean literal. (clippy::useless_conversion)",
|
|
54
|
+
node,
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Array.from([...])
|
|
60
|
+
if (isCallOf(node, "Array", "from") && arg.type === "ArrayExpression") {
|
|
61
|
+
context.report({
|
|
62
|
+
message:
|
|
63
|
+
"Useless conversion: `Array.from()` called on an array literal. Use the array directly. (clippy::useless_conversion)",
|
|
64
|
+
node,
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// "str".toString()
|
|
70
|
+
if (
|
|
71
|
+
callee.type === "MemberExpression" &&
|
|
72
|
+
isIdentifier(callee.property, "toString") &&
|
|
73
|
+
callee.object.type === "Literal" &&
|
|
74
|
+
typeof callee.object.value === "string" &&
|
|
75
|
+
args.length === 0
|
|
76
|
+
) {
|
|
77
|
+
// This branch handles 0-arg toString, but our check above requires 1 arg.
|
|
78
|
+
// We'll handle it in a separate visitor.
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Handle 0-arg method calls like "str".toString(), (42).valueOf()
|
|
83
|
+
"CallExpression:exit"(node: Node) {
|
|
84
|
+
const { callee } = node;
|
|
85
|
+
const args = node.arguments;
|
|
86
|
+
if (!args || args.length !== 0) return;
|
|
87
|
+
if (callee.type !== "MemberExpression") return;
|
|
88
|
+
|
|
89
|
+
const obj = callee.object;
|
|
90
|
+
const method = callee.property;
|
|
91
|
+
if (method.type !== "Identifier") return;
|
|
92
|
+
|
|
93
|
+
// "str".toString()
|
|
94
|
+
if (obj.type === "Literal" && typeof obj.value === "string" && method.name === "toString") {
|
|
95
|
+
context.report({
|
|
96
|
+
message:
|
|
97
|
+
"Useless conversion: `.toString()` called on a string literal. (clippy::useless_conversion)",
|
|
98
|
+
node,
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// (42).toString() is NOT useless — it converts number to string
|
|
104
|
+
// But (42).valueOf() IS useless
|
|
105
|
+
if (obj.type === "Literal" && typeof obj.value === "number" && method.name === "valueOf") {
|
|
106
|
+
context.report({
|
|
107
|
+
message:
|
|
108
|
+
"Useless conversion: `.valueOf()` called on a number literal. (clippy::useless_conversion)",
|
|
109
|
+
node,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// clippy::suspicious_xor_used_as_pow — XOR operator mistaken for exponentiation
|
|
2
|
+
// Detects: 2 ^ 8 (likely meant 2 ** 8 = 256, but ^ is XOR = 10)
|
|
3
|
+
// Only flags when both sides are integer literals and the result looks like
|
|
4
|
+
// the user intended exponentiation.
|
|
5
|
+
|
|
6
|
+
import type { Context, Node } from "../types";
|
|
7
|
+
|
|
8
|
+
function isIntLiteral(node: Node): boolean {
|
|
9
|
+
return node.type === "Literal" && typeof node.value === "number" && Number.isInteger(node.value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
create(context: Context) {
|
|
14
|
+
return {
|
|
15
|
+
BinaryExpression(node: Node) {
|
|
16
|
+
if (node.operator !== "^") return;
|
|
17
|
+
|
|
18
|
+
// Only flag when both sides are integer literals (strong signal of mistake)
|
|
19
|
+
if (!isIntLiteral(node.left) || !isIntLiteral(node.right)) return;
|
|
20
|
+
|
|
21
|
+
const base = node.left.value as number;
|
|
22
|
+
const exp = node.right.value as number;
|
|
23
|
+
|
|
24
|
+
// Common exponentiation patterns: 2^N, 10^N, small^small
|
|
25
|
+
if (base >= 2 && exp >= 2) {
|
|
26
|
+
context.report({
|
|
27
|
+
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)`,
|
|
28
|
+
node,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// clippy::zero_divided_by_zero — 0 / 0 produces NaN; use NaN directly
|
|
2
|
+
// Also catches 0.0 / 0.0
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
function isZero(node: Node): boolean {
|
|
7
|
+
return node.type === "Literal" && (node.value === 0 || node.value === 0.0);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
create(context: Context) {
|
|
12
|
+
return {
|
|
13
|
+
BinaryExpression(node: Node) {
|
|
14
|
+
if (node.operator === "/" && isZero(node.left) && isZero(node.right)) {
|
|
15
|
+
context.report({
|
|
16
|
+
message:
|
|
17
|
+
"Zero divided by zero: `0 / 0` is `NaN`. Use `NaN` directly if intentional. (clippy::zero_divided_by_zero)",
|
|
18
|
+
node,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|