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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/oxclippy.js +2428 -0
  4. package/package.json +53 -0
  5. package/src/index.ts +1 -0
  6. package/src/plugin.ts +134 -0
  7. package/src/rules/almost-swapped.ts +52 -0
  8. package/src/rules/bool-comparison.ts +38 -0
  9. package/src/rules/bool-to-int-with-if.ts +30 -0
  10. package/src/rules/cognitive-complexity.ts +137 -0
  11. package/src/rules/collapsible-if.ts +38 -0
  12. package/src/rules/enum-variant-names.ts +45 -0
  13. package/src/rules/excessive-nesting.ts +84 -0
  14. package/src/rules/explicit-counter-loop.ts +66 -0
  15. package/src/rules/filter-then-first.ts +26 -0
  16. package/src/rules/float-comparison.ts +52 -0
  17. package/src/rules/float-equality-without-abs.ts +43 -0
  18. package/src/rules/fn-params-excessive-bools.ts +49 -0
  19. package/src/rules/identity-op.ts +46 -0
  20. package/src/rules/if-same-then-else.ts +34 -0
  21. package/src/rules/int-plus-one.ts +74 -0
  22. package/src/rules/let-and-return.ts +43 -0
  23. package/src/rules/manual-clamp.ts +63 -0
  24. package/src/rules/manual-every.ts +47 -0
  25. package/src/rules/manual-find.ts +59 -0
  26. package/src/rules/manual-includes.ts +65 -0
  27. package/src/rules/manual-is-finite.ts +57 -0
  28. package/src/rules/manual-some.ts +46 -0
  29. package/src/rules/manual-strip.ts +96 -0
  30. package/src/rules/manual-swap.ts +73 -0
  31. package/src/rules/map-identity.ts +67 -0
  32. package/src/rules/map-void-return.ts +23 -0
  33. package/src/rules/match-same-arms.ts +46 -0
  34. package/src/rules/needless-bool.ts +67 -0
  35. package/src/rules/needless-continue.ts +47 -0
  36. package/src/rules/needless-late-init.ts +44 -0
  37. package/src/rules/needless-range-loop.ts +127 -0
  38. package/src/rules/neg-multiply.ts +36 -0
  39. package/src/rules/never-loop.ts +49 -0
  40. package/src/rules/object-keys-values.ts +67 -0
  41. package/src/rules/prefer-structured-clone.ts +30 -0
  42. package/src/rules/promise-new-resolve.ts +59 -0
  43. package/src/rules/redundant-closure-call.ts +61 -0
  44. package/src/rules/redundant-closure.ts +81 -0
  45. package/src/rules/search-is-some.ts +45 -0
  46. package/src/rules/similar-names.ts +155 -0
  47. package/src/rules/single-case-switch.ts +27 -0
  48. package/src/rules/single-element-loop.ts +21 -0
  49. package/src/rules/struct-field-names.ts +108 -0
  50. package/src/rules/too-many-arguments.ts +37 -0
  51. package/src/rules/too-many-lines.ts +45 -0
  52. package/src/rules/unnecessary-fold.ts +65 -0
  53. package/src/rules/unnecessary-reduce-collect.ts +109 -0
  54. package/src/rules/unreadable-literal.ts +59 -0
  55. package/src/rules/used-underscore-binding.ts +41 -0
  56. package/src/rules/useless-conversion.ts +115 -0
  57. package/src/rules/xor-used-as-pow.ts +34 -0
  58. package/src/rules/zero-divided-by-zero.ts +24 -0
  59. 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
+ };