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
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "oxclippy",
3
+ "version": "0.1.0",
4
+ "description": "An oxlint JS plugin that mirrors Rust Clippy rules for TypeScript/JavaScript",
5
+ "keywords": [
6
+ "clippy",
7
+ "javascript",
8
+ "lint",
9
+ "linter",
10
+ "oxc",
11
+ "oxlint",
12
+ "plugin",
13
+ "typescript"
14
+ ],
15
+ "homepage": "https://github.com/rayhanadev/oxclippy#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/rayhanadev/oxclippy/issues"
18
+ },
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/rayhanadev/oxclippy"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "type": "module",
29
+ "main": "dist/oxclippy.js",
30
+ "exports": {
31
+ ".": "./dist/oxclippy.js"
32
+ },
33
+ "scripts": {
34
+ "build": "bun build src/plugin.ts --outfile dist/oxclippy.js --format esm",
35
+ "lint": "oxlint",
36
+ "format": "oxfmt",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "bun test",
39
+ "prepublishOnly": "bun run build"
40
+ },
41
+ "dependencies": {
42
+ "oxc-parser": "^0.123.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/bun": "latest",
46
+ "oxfmt": "^0.43.0",
47
+ "oxlint": "^1.58.0",
48
+ "oxlint-tsgolint": "^0.19.0"
49
+ },
50
+ "peerDependencies": {
51
+ "typescript": "^6"
52
+ }
53
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./plugin";
package/src/plugin.ts ADDED
@@ -0,0 +1,134 @@
1
+ // oxclippy: An oxlint JS plugin that mirrors Rust Clippy rules for TypeScript/JavaScript
2
+ //
3
+ // Each rule is named after its Clippy counterpart. Rules that already exist in
4
+ // oxlint's built-in plugins (eslint, typescript, unicorn, oxc) are intentionally
5
+ // omitted to avoid duplication.
6
+
7
+ import needlessBool from "./rules/needless-bool";
8
+ import collapsibleIf from "./rules/collapsible-if";
9
+ import negMultiply from "./rules/neg-multiply";
10
+ import boolComparison from "./rules/bool-comparison";
11
+ import identityOp from "./rules/identity-op";
12
+ import singleCaseSwitch from "./rules/single-case-switch";
13
+ import tooManyArguments from "./rules/too-many-arguments";
14
+ import tooManyLines from "./rules/too-many-lines";
15
+ import filterThenFirst from "./rules/filter-then-first";
16
+ import mapVoidReturn from "./rules/map-void-return";
17
+ import uselessConversion from "./rules/useless-conversion";
18
+ import manualClamp from "./rules/manual-clamp";
19
+ import manualStrip from "./rules/manual-strip";
20
+ import manualFind from "./rules/manual-find";
21
+ import manualSome from "./rules/manual-some";
22
+ import manualEvery from "./rules/manual-every";
23
+ import manualIncludes from "./rules/manual-includes";
24
+ import cognitiveComplexity from "./rules/cognitive-complexity";
25
+ import floatComparison from "./rules/float-comparison";
26
+ import needlessRangeLoop from "./rules/needless-range-loop";
27
+ import manualSwap from "./rules/manual-swap";
28
+ import searchIsSome from "./rules/search-is-some";
29
+ import letAndReturn from "./rules/let-and-return";
30
+ import xorUsedAsPow from "./rules/xor-used-as-pow";
31
+ import mapIdentity from "./rules/map-identity";
32
+ import redundantClosureCall from "./rules/redundant-closure-call";
33
+ import almostSwapped from "./rules/almost-swapped";
34
+ import ifSameThenElse from "./rules/if-same-then-else";
35
+ import neverLoop from "./rules/never-loop";
36
+ import explicitCounterLoop from "./rules/explicit-counter-loop";
37
+ import excessiveNesting from "./rules/excessive-nesting";
38
+ import fnParamsExcessiveBools from "./rules/fn-params-excessive-bools";
39
+ import floatEqualityWithoutAbs from "./rules/float-equality-without-abs";
40
+ import manualIsFinite from "./rules/manual-is-finite";
41
+ import unnecessaryFold from "./rules/unnecessary-fold";
42
+ import needlessLateInit from "./rules/needless-late-init";
43
+ import singleElementLoop from "./rules/single-element-loop";
44
+ import intPlusOne from "./rules/int-plus-one";
45
+ import zeroDividedByZero from "./rules/zero-divided-by-zero";
46
+ import redundantClosure from "./rules/redundant-closure";
47
+ import unnecessaryReduceCollect from "./rules/unnecessary-reduce-collect";
48
+ import preferStructuredClone from "./rules/prefer-structured-clone";
49
+ import objectKeysValues from "./rules/object-keys-values";
50
+ import promiseNewResolve from "./rules/promise-new-resolve";
51
+ import similarNames from "./rules/similar-names";
52
+ import matchSameArms from "./rules/match-same-arms";
53
+ import usedUnderscoreBinding from "./rules/used-underscore-binding";
54
+ import needlessContinue from "./rules/needless-continue";
55
+ import enumVariantNames from "./rules/enum-variant-names";
56
+ import structFieldNames from "./rules/struct-field-names";
57
+ import unreadableLiteral from "./rules/unreadable-literal";
58
+ import boolToIntWithIf from "./rules/bool-to-int-with-if";
59
+
60
+ const plugin = {
61
+ meta: {
62
+ name: "oxclippy",
63
+ version: "0.1.0",
64
+ },
65
+ rules: {
66
+ // Style — code clarity and readability
67
+ "needless-bool": needlessBool,
68
+ "collapsible-if": collapsibleIf,
69
+ "neg-multiply": negMultiply,
70
+ "bool-comparison": boolComparison,
71
+ "single-case-switch": singleCaseSwitch,
72
+ "let-and-return": letAndReturn,
73
+ "int-plus-one": intPlusOne,
74
+ "needless-late-init": needlessLateInit,
75
+
76
+ // Complexity — simplifiable patterns
77
+ "identity-op": identityOp,
78
+ "manual-clamp": manualClamp,
79
+ "manual-strip": manualStrip,
80
+ "useless-conversion": uselessConversion,
81
+ "manual-swap": manualSwap,
82
+ "manual-is-finite": manualIsFinite,
83
+
84
+ // Correctness — likely bugs
85
+ "float-comparison": floatComparison,
86
+ "xor-used-as-pow": xorUsedAsPow,
87
+ "almost-swapped": almostSwapped,
88
+ "if-same-then-else": ifSameThenElse,
89
+ "never-loop": neverLoop,
90
+ "float-equality-without-abs": floatEqualityWithoutAbs,
91
+ "zero-divided-by-zero": zeroDividedByZero,
92
+
93
+ // Iterator — loops replaceable with array methods
94
+ "filter-then-first": filterThenFirst,
95
+ "map-void-return": mapVoidReturn,
96
+ "map-identity": mapIdentity,
97
+ "manual-find": manualFind,
98
+ "manual-some": manualSome,
99
+ "manual-every": manualEvery,
100
+ "manual-includes": manualIncludes,
101
+ "search-is-some": searchIsSome,
102
+ "needless-range-loop": needlessRangeLoop,
103
+ "redundant-closure-call": redundantClosureCall,
104
+ "explicit-counter-loop": explicitCounterLoop,
105
+ "unnecessary-fold": unnecessaryFold,
106
+ "single-element-loop": singleElementLoop,
107
+
108
+ // Functions — function-level quality
109
+ "too-many-arguments": tooManyArguments,
110
+ "too-many-lines": tooManyLines,
111
+ "cognitive-complexity": cognitiveComplexity,
112
+ "excessive-nesting": excessiveNesting,
113
+ "fn-params-excessive-bools": fnParamsExcessiveBools,
114
+
115
+ // Principles — idiomatic JS/TS from Clippy philosophy
116
+ "redundant-closure": redundantClosure,
117
+ "unnecessary-reduce-collect": unnecessaryReduceCollect,
118
+ "prefer-structured-clone": preferStructuredClone,
119
+ "object-keys-values": objectKeysValues,
120
+ "promise-new-resolve": promiseNewResolve,
121
+
122
+ // Pedantic — naming, style, readability
123
+ "similar-names": similarNames,
124
+ "match-same-arms": matchSameArms,
125
+ "used-underscore-binding": usedUnderscoreBinding,
126
+ "needless-continue": needlessContinue,
127
+ "enum-variant-names": enumVariantNames,
128
+ "struct-field-names": structFieldNames,
129
+ "unreadable-literal": unreadableLiteral,
130
+ "bool-to-int-with-if": boolToIntWithIf,
131
+ },
132
+ };
133
+
134
+ export default plugin;
@@ -0,0 +1,52 @@
1
+ // clippy::almost_swapped — broken swap: a = b; b = a; (original a is lost)
2
+ // Detects: consecutive assignments that look like a swap but forgot the temp variable.
3
+
4
+ import type { Context, Node } from "../types";
5
+
6
+ function assignTarget(node: Node): string | null {
7
+ if (node.type !== "ExpressionStatement") return null;
8
+ const expr = node.expression;
9
+ if (expr.type !== "AssignmentExpression" || expr.operator !== "=") return null;
10
+ if (expr.left.type === "Identifier") return expr.left.name;
11
+ return null;
12
+ }
13
+
14
+ function assignSource(node: Node): string | null {
15
+ if (node.type !== "ExpressionStatement") return null;
16
+ const expr = node.expression;
17
+ if (expr.type !== "AssignmentExpression" || expr.operator !== "=") return null;
18
+ if (expr.right.type === "Identifier") return expr.right.name;
19
+ return null;
20
+ }
21
+
22
+ function checkBody(body: Node[], context: Context) {
23
+ for (let i = 0; i < body.length - 1; i++) {
24
+ const s1 = body[i]!;
25
+ const s2 = body[i + 1]!;
26
+
27
+ const t1 = assignTarget(s1);
28
+ const src1 = assignSource(s1);
29
+ const t2 = assignTarget(s2);
30
+ const src2 = assignSource(s2);
31
+
32
+ if (t1 && src1 && t2 && src2 && t1 === src2 && t2 === src1 && t1 !== t2) {
33
+ context.report({
34
+ message: `Almost swapped: \`${t1} = ${src1}; ${t2} = ${src2};\` overwrites \`${t1}\` before saving it. Use \`[${t1}, ${t2}] = [${t2}, ${t1}]\`. (clippy::almost_swapped)`,
35
+ node: s1,
36
+ });
37
+ }
38
+ }
39
+ }
40
+
41
+ export default {
42
+ create(context: Context) {
43
+ return {
44
+ BlockStatement(node: Node) {
45
+ if (node.body) checkBody(node.body, context);
46
+ },
47
+ Program(node: Node) {
48
+ if (node.body) checkBody(node.body, context);
49
+ },
50
+ };
51
+ },
52
+ };
@@ -0,0 +1,38 @@
1
+ // clippy::bool_comparison — explicit comparison to boolean literals
2
+ // Detects: x === true → x, x === false → !x, x !== true → !x, x !== false → x
3
+ // NOT covered by oxlint's built-in rules (no-unnecessary-boolean-literal-compare is not in oxlint)
4
+
5
+ import type { Context, Node } from "../types";
6
+ import { isBoolLiteral } from "../types";
7
+
8
+ export default {
9
+ create(context: Context) {
10
+ return {
11
+ BinaryExpression(node: Node) {
12
+ if (node.operator !== "===" && node.operator !== "!==") return;
13
+
14
+ const leftBool = isBoolLiteral(node.left);
15
+ const rightBool = isBoolLiteral(node.right);
16
+
17
+ if (!leftBool && !rightBool) return;
18
+ // Skip if both sides are bool literals (that's a different issue)
19
+ if (leftBool && rightBool) return;
20
+
21
+ const boolVal = leftBool ? node.left.value : node.right.value;
22
+ const isEqual = node.operator === "===";
23
+
24
+ let suggestion: string;
25
+ if ((boolVal && isEqual) || (!boolVal && !isEqual)) {
26
+ suggestion = "use the expression directly";
27
+ } else {
28
+ suggestion = "negate the expression with `!`";
29
+ }
30
+
31
+ context.report({
32
+ message: `Bool comparison: comparison to \`${boolVal}\` is needless. Instead, ${suggestion}. (clippy::bool_comparison)`,
33
+ node,
34
+ });
35
+ },
36
+ };
37
+ },
38
+ };
@@ -0,0 +1,30 @@
1
+ // clippy::bool_to_int_with_if — converting a boolean to 0/1 with a ternary or if/else
2
+ // Detects: cond ? 1 : 0 → +cond or Number(cond)
3
+ // Detects: cond ? 0 : 1 → +!cond or Number(!cond)
4
+
5
+ import type { Context, Node } from "../types";
6
+ import { isLiteral } from "../types";
7
+
8
+ export default {
9
+ create(context: Context) {
10
+ return {
11
+ ConditionalExpression(node: Node) {
12
+ const { consequent, alternate } = node;
13
+
14
+ if (isLiteral(consequent, 1) && isLiteral(alternate, 0)) {
15
+ context.report({
16
+ message:
17
+ "Bool to int with if: `cond ? 1 : 0` can be simplified to `Number(cond)` or `+cond`. (clippy::bool_to_int_with_if)",
18
+ node,
19
+ });
20
+ } else if (isLiteral(consequent, 0) && isLiteral(alternate, 1)) {
21
+ context.report({
22
+ message:
23
+ "Bool to int with if: `cond ? 0 : 1` can be simplified to `Number(!cond)` or `+!cond`. (clippy::bool_to_int_with_if)",
24
+ node,
25
+ });
26
+ }
27
+ },
28
+ };
29
+ },
30
+ };
@@ -0,0 +1,137 @@
1
+ // clippy::cognitive_complexity — function exceeds complexity threshold
2
+ // Uses a simplified cognitive complexity model:
3
+ // - +1 for each: if, else if, else, for, for-in, for-of, while, do-while, catch, ternary (?:)
4
+ // - +1 for each: &&, || (logical operators)
5
+ // - +1 nesting bonus per level of nesting for control flow
6
+ // Default threshold: 25 (matches Clippy)
7
+
8
+ import type { Context, Node } from "../types";
9
+
10
+ const THRESHOLD = 25;
11
+
12
+ function calculateComplexity(node: Node): number {
13
+ let complexity = 0;
14
+
15
+ function walk(n: Node, nesting: number) {
16
+ if (!n || typeof n !== "object") return;
17
+
18
+ switch (n.type) {
19
+ case "IfStatement":
20
+ complexity += 1 + nesting;
21
+ walk(n.test, nesting);
22
+ walk(n.consequent, nesting + 1);
23
+ if (n.alternate) {
24
+ if (n.alternate.type === "IfStatement") {
25
+ // else if: +1 but no nesting increase
26
+ complexity += 1;
27
+ walk(n.alternate.test, nesting);
28
+ walk(n.alternate.consequent, nesting + 1);
29
+ if (n.alternate.alternate) {
30
+ walk(n.alternate.alternate, nesting);
31
+ }
32
+ } else {
33
+ // else: +1
34
+ complexity += 1;
35
+ walk(n.alternate, nesting + 1);
36
+ }
37
+ }
38
+ return; // handled children manually
39
+
40
+ case "ForStatement":
41
+ case "ForInStatement":
42
+ case "ForOfStatement":
43
+ case "WhileStatement":
44
+ case "DoWhileStatement":
45
+ complexity += 1 + nesting;
46
+ walkChildren(n, nesting + 1);
47
+ return;
48
+
49
+ case "CatchClause":
50
+ complexity += 1 + nesting;
51
+ walkChildren(n, nesting + 1);
52
+ return;
53
+
54
+ case "SwitchStatement":
55
+ complexity += 1 + nesting;
56
+ walkChildren(n, nesting + 1);
57
+ return;
58
+
59
+ case "ConditionalExpression":
60
+ complexity += 1 + nesting;
61
+ walk(n.test, nesting);
62
+ walk(n.consequent, nesting + 1);
63
+ walk(n.alternate, nesting + 1);
64
+ return;
65
+
66
+ case "LogicalExpression":
67
+ // Each && or || adds 1 (no nesting bonus for these)
68
+ complexity += 1;
69
+ walk(n.left, nesting);
70
+ walk(n.right, nesting);
71
+ return;
72
+
73
+ // Don't recurse into nested function declarations/expressions
74
+ case "FunctionDeclaration":
75
+ case "FunctionExpression":
76
+ case "ArrowFunctionExpression":
77
+ return;
78
+ }
79
+
80
+ walkChildren(n, nesting);
81
+ }
82
+
83
+ function walkChildren(n: Node, nesting: number) {
84
+ for (const key of Object.keys(n)) {
85
+ if (key === "type" || key === "loc" || key === "range" || key === "parent") continue;
86
+ const val = n[key];
87
+ if (Array.isArray(val)) {
88
+ for (const child of val) {
89
+ if (child && typeof child === "object" && child.type) {
90
+ walk(child, nesting);
91
+ }
92
+ }
93
+ } else if (val && typeof val === "object" && val.type) {
94
+ walk(val, nesting);
95
+ }
96
+ }
97
+ }
98
+
99
+ // Start walking from the function body
100
+ if (node.body) {
101
+ if (node.body.type === "BlockStatement") {
102
+ for (const stmt of node.body.body) {
103
+ walk(stmt, 0);
104
+ }
105
+ } else {
106
+ // Arrow function with expression body
107
+ walk(node.body, 0);
108
+ }
109
+ }
110
+
111
+ return complexity;
112
+ }
113
+
114
+ export default {
115
+ create(context: Context) {
116
+ function checkFunction(node: Node) {
117
+ const complexity = calculateComplexity(node);
118
+ if (complexity <= THRESHOLD) return;
119
+
120
+ const name =
121
+ node.id?.name ??
122
+ (node.parent?.type === "VariableDeclarator" ? node.parent.id?.name : null) ??
123
+ "<anonymous>";
124
+
125
+ context.report({
126
+ message: `Cognitive complexity: \`${name}\` has a complexity of ${complexity} (max ${THRESHOLD}). Consider refactoring into smaller functions. (clippy::cognitive_complexity)`,
127
+ node,
128
+ });
129
+ }
130
+
131
+ return {
132
+ FunctionDeclaration: checkFunction,
133
+ FunctionExpression: checkFunction,
134
+ ArrowFunctionExpression: checkFunction,
135
+ };
136
+ },
137
+ };
@@ -0,0 +1,38 @@
1
+ // clippy::collapsible_if — nested ifs that can be merged with &&
2
+ // Detects: if (a) { if (b) { ... } } → if (a && b) { ... }
3
+ // NOT covered by eslint/no-lonely-if (which only catches else { if })
4
+
5
+ import type { Context, Node } from "../types";
6
+
7
+ export default {
8
+ create(context: Context) {
9
+ return {
10
+ IfStatement(node: Node) {
11
+ // Only applies when outer if has no else
12
+ if (node.alternate) return;
13
+
14
+ const body = node.consequent;
15
+ if (!body) return;
16
+
17
+ // Get the single statement inside the consequent block
18
+ let inner: Node | null = null;
19
+ if (body.type === "BlockStatement" && body.body.length === 1) {
20
+ inner = body.body[0];
21
+ } else if (body.type === "IfStatement") {
22
+ inner = body;
23
+ }
24
+
25
+ if (!inner || inner.type !== "IfStatement") return;
26
+
27
+ // Inner if must also have no else
28
+ if (inner.alternate) return;
29
+
30
+ context.report({
31
+ message:
32
+ "Collapsible if: these nested ifs can be combined with `&&`. (clippy::collapsible_if)",
33
+ node,
34
+ });
35
+ },
36
+ };
37
+ },
38
+ };
@@ -0,0 +1,45 @@
1
+ // clippy::enum_variant_names — TS enum members with redundant prefix or suffix
2
+ // Detects: enum Color { ColorRed, ColorGreen, ColorBlue } — "Color" prefix is redundant
3
+ // Also: enum Status { StatusActive, StatusInactive } — all share the enum name as prefix
4
+
5
+ import type { Context, Node } from "../types";
6
+
7
+ export default {
8
+ create(context: Context) {
9
+ return {
10
+ TSEnumDeclaration(node: Node) {
11
+ const enumName = node.id?.name;
12
+ if (!enumName) return;
13
+ const members: Node[] = node.members ?? node.body?.members;
14
+ if (!members || members.length < 2) return;
15
+
16
+ const names: string[] = members
17
+ .map((m: Node) => (m.id?.type === "Identifier" ? m.id.name : null))
18
+ .filter((n: string | null): n is string => n !== null);
19
+
20
+ if (names.length < 2) return;
21
+
22
+ const lowerEnum = enumName.toLowerCase();
23
+
24
+ // Check prefix: all members start with the enum name
25
+ const allPrefixed = names.every((n) => n.toLowerCase().startsWith(lowerEnum));
26
+ if (allPrefixed) {
27
+ context.report({
28
+ message: `Enum variant names: all members of \`${enumName}\` are prefixed with \`${enumName}\`. Remove the redundant prefix. (clippy::enum_variant_names)`,
29
+ node,
30
+ });
31
+ return;
32
+ }
33
+
34
+ // Check suffix: all members end with the enum name
35
+ const allSuffixed = names.every((n) => n.toLowerCase().endsWith(lowerEnum));
36
+ if (allSuffixed) {
37
+ context.report({
38
+ message: `Enum variant names: all members of \`${enumName}\` are suffixed with \`${enumName}\`. Remove the redundant suffix. (clippy::enum_variant_names)`,
39
+ node,
40
+ });
41
+ }
42
+ },
43
+ };
44
+ },
45
+ };
@@ -0,0 +1,84 @@
1
+ // clippy::excessive_nesting — code nested beyond a threshold
2
+ // Default threshold: 5 levels
3
+
4
+ import type { Context, Node } from "../types";
5
+
6
+ const THRESHOLD = 5;
7
+
8
+ const NESTING_NODES = new Set([
9
+ "IfStatement",
10
+ "ForStatement",
11
+ "ForInStatement",
12
+ "ForOfStatement",
13
+ "WhileStatement",
14
+ "DoWhileStatement",
15
+ "SwitchStatement",
16
+ "TryStatement",
17
+ ]);
18
+
19
+ export default {
20
+ create(context: Context) {
21
+ let reported = false;
22
+
23
+ function walk(node: Node, depth: number) {
24
+ if (!node || typeof node !== "object" || !node.type) return;
25
+ if (reported) return;
26
+
27
+ const isNesting = NESTING_NODES.has(node.type);
28
+ const newDepth = isNesting ? depth + 1 : depth;
29
+
30
+ if (newDepth > THRESHOLD && isNesting) {
31
+ reported = true;
32
+ context.report({
33
+ message: `Excessive nesting: this code is nested ${newDepth} levels deep (max ${THRESHOLD}). Consider extracting into helper functions. (clippy::excessive_nesting)`,
34
+ node,
35
+ });
36
+ return;
37
+ }
38
+
39
+ // Don't recurse into nested functions
40
+ if (
41
+ node.type === "FunctionDeclaration" ||
42
+ node.type === "FunctionExpression" ||
43
+ node.type === "ArrowFunctionExpression"
44
+ ) {
45
+ if (depth > 0) return; // only skip nested functions, not the top-level one
46
+ }
47
+
48
+ for (const key of Object.keys(node)) {
49
+ if (
50
+ key === "type" ||
51
+ key === "loc" ||
52
+ key === "range" ||
53
+ key === "parent" ||
54
+ key === "start" ||
55
+ key === "end"
56
+ )
57
+ continue;
58
+ const val = node[key];
59
+ if (Array.isArray(val)) {
60
+ for (const child of val) {
61
+ if (child && typeof child === "object" && child.type) walk(child, newDepth);
62
+ }
63
+ } else if (val && typeof val === "object" && val.type) {
64
+ walk(val, newDepth);
65
+ }
66
+ }
67
+ }
68
+
69
+ return {
70
+ FunctionDeclaration(node: Node) {
71
+ reported = false;
72
+ walk(node.body, 0);
73
+ },
74
+ FunctionExpression(node: Node) {
75
+ reported = false;
76
+ walk(node.body, 0);
77
+ },
78
+ ArrowFunctionExpression(node: Node) {
79
+ reported = false;
80
+ walk(node.body, 0);
81
+ },
82
+ };
83
+ },
84
+ };
@@ -0,0 +1,66 @@
1
+ // clippy::explicit_counter_loop — manual counter variable with for-of
2
+ // Detects: let i = 0; for (const x of arr) { ...i...; i++; }
3
+ // Suggests: for (const [i, x] of arr.entries())
4
+
5
+ import type { Context, Node } from "../types";
6
+ import { getFunctionBody, getForOfVar } from "../types";
7
+
8
+ function checkBody(body: Node[], context: Context) {
9
+ for (let i = 0; i < body.length - 1; i++) {
10
+ const decl = body[i]!;
11
+ const loop = body[i + 1]!;
12
+
13
+ // decl: let counter = 0;
14
+ if (decl.type !== "VariableDeclaration" || decl.declarations.length !== 1) continue;
15
+ const d = decl.declarations[0]!;
16
+ if (d.id?.type !== "Identifier" || d.init?.type !== "Literal" || d.init.value !== 0) continue;
17
+ const counterName = d.id.name;
18
+
19
+ // loop: for (const x of arr) { ... counter++; }
20
+ if (loop.type !== "ForOfStatement") continue;
21
+ if (loop.body?.type !== "BlockStatement" || loop.body.body.length === 0) continue;
22
+
23
+ const stmts: Node[] = loop.body.body;
24
+ const last = stmts[stmts.length - 1]!;
25
+
26
+ // Check last statement is counter++ or counter += 1
27
+ const isIncrement =
28
+ (last.type === "ExpressionStatement" &&
29
+ last.expression?.type === "UpdateExpression" &&
30
+ last.expression.operator === "++" &&
31
+ last.expression.argument?.type === "Identifier" &&
32
+ last.expression.argument.name === counterName) ||
33
+ (last.type === "ExpressionStatement" &&
34
+ last.expression?.type === "AssignmentExpression" &&
35
+ last.expression.operator === "+=" &&
36
+ last.expression.left?.type === "Identifier" &&
37
+ last.expression.left.name === counterName &&
38
+ last.expression.right?.type === "Literal" &&
39
+ last.expression.right.value === 1);
40
+
41
+ if (isIncrement) {
42
+ context.report({
43
+ message: `Explicit counter loop: manual counter \`${counterName}\` can be replaced with \`for (const [${counterName}, item] of arr.entries())\`. (clippy::explicit_counter_loop)`,
44
+ node: loop,
45
+ });
46
+ }
47
+ }
48
+ }
49
+
50
+ export default {
51
+ create(context: Context) {
52
+ function checkFunction(node: Node) {
53
+ const body = getFunctionBody(node);
54
+ if (body) checkBody(body, context);
55
+ }
56
+
57
+ return {
58
+ FunctionDeclaration: checkFunction,
59
+ FunctionExpression: checkFunction,
60
+ ArrowFunctionExpression: checkFunction,
61
+ Program(node: Node) {
62
+ if (node.body) checkBody(node.body, context);
63
+ },
64
+ };
65
+ },
66
+ };