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,49 @@
|
|
|
1
|
+
// clippy::never_loop — loop that always exits on the first iteration
|
|
2
|
+
// Detects: for/while where every path through the body ends with break/return/throw.
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
import { unwrapBlock } from "../types";
|
|
6
|
+
|
|
7
|
+
function alwaysExits(node: Node): boolean {
|
|
8
|
+
if (!node) return false;
|
|
9
|
+
|
|
10
|
+
switch (node.type) {
|
|
11
|
+
case "ReturnStatement":
|
|
12
|
+
case "ThrowStatement":
|
|
13
|
+
case "BreakStatement":
|
|
14
|
+
return true;
|
|
15
|
+
case "BlockStatement":
|
|
16
|
+
return node.body.length > 0 && alwaysExits(node.body[node.body.length - 1]!);
|
|
17
|
+
case "IfStatement":
|
|
18
|
+
return !!node.alternate && alwaysExits(node.consequent) && alwaysExits(node.alternate);
|
|
19
|
+
case "ExpressionStatement":
|
|
20
|
+
return false;
|
|
21
|
+
default:
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function checkLoop(node: Node, context: Context) {
|
|
27
|
+
const body = node.body;
|
|
28
|
+
if (!body) return;
|
|
29
|
+
|
|
30
|
+
if (alwaysExits(body)) {
|
|
31
|
+
context.report({
|
|
32
|
+
message:
|
|
33
|
+
"Never loop: this loop always exits on the first iteration. Consider replacing with a conditional. (clippy::never_loop)",
|
|
34
|
+
node,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
create(context: Context) {
|
|
41
|
+
return {
|
|
42
|
+
ForStatement: (node: Node) => checkLoop(node, context),
|
|
43
|
+
ForOfStatement: (node: Node) => checkLoop(node, context),
|
|
44
|
+
ForInStatement: (node: Node) => checkLoop(node, context),
|
|
45
|
+
WhileStatement: (node: Node) => checkLoop(node, context),
|
|
46
|
+
DoWhileStatement: (node: Node) => checkLoop(node, context),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// clippy principle: "don't reinvent the standard library"
|
|
2
|
+
// Detects: Object.keys(obj).map(k => obj[k]) → Object.values(obj)
|
|
3
|
+
// Detects: Object.keys(obj).forEach(k => use(obj[k])) when only value is used → Object.values(obj)
|
|
4
|
+
// Analogous to Clippy's for_kv_map (iterating kv pairs when only one is needed).
|
|
5
|
+
|
|
6
|
+
import type { Context, Node } from "../types";
|
|
7
|
+
import { isCallOf, isIdentifier } from "../types";
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
create(context: Context) {
|
|
11
|
+
return {
|
|
12
|
+
CallExpression(node: Node) {
|
|
13
|
+
const callee = node.callee;
|
|
14
|
+
if (callee.type !== "MemberExpression") return;
|
|
15
|
+
|
|
16
|
+
const methodName = callee.property;
|
|
17
|
+
if (methodName?.type !== "Identifier") return;
|
|
18
|
+
if (methodName.name !== "map" && methodName.name !== "forEach") return;
|
|
19
|
+
|
|
20
|
+
// Check: Object.keys(obj).map/forEach(...)
|
|
21
|
+
const receiver = callee.object;
|
|
22
|
+
if (!isCallOf(receiver, "Object", "keys")) return;
|
|
23
|
+
if (!receiver.arguments || receiver.arguments.length !== 1) return;
|
|
24
|
+
|
|
25
|
+
const objArg = receiver.arguments[0]!;
|
|
26
|
+
if (objArg.type !== "Identifier") return;
|
|
27
|
+
const objName = objArg.name;
|
|
28
|
+
|
|
29
|
+
// Check callback: k => expr using obj[k]
|
|
30
|
+
const args = node.arguments;
|
|
31
|
+
if (!args || args.length !== 1) return;
|
|
32
|
+
const callback = args[0]!;
|
|
33
|
+
|
|
34
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")
|
|
35
|
+
return;
|
|
36
|
+
if (callback.params.length !== 1) return;
|
|
37
|
+
const param = callback.params[0]!;
|
|
38
|
+
if (param.type !== "Identifier") return;
|
|
39
|
+
const keyName = param.name;
|
|
40
|
+
|
|
41
|
+
// Check if callback body contains obj[key] — if so, suggest Object.values or Object.entries
|
|
42
|
+
const bodyStr = context.sourceCode.text.slice(callback.body.start, callback.body.end);
|
|
43
|
+
|
|
44
|
+
// Simple heuristic: if the callback references obj[key], suggest Object.values/entries
|
|
45
|
+
const indexPattern = `${objName}[${keyName}]`;
|
|
46
|
+
if (bodyStr.includes(indexPattern)) {
|
|
47
|
+
// Check if key is ALSO used independently (not just as obj[key])
|
|
48
|
+
// Simple heuristic: count occurrences of keyName outside obj[keyName]
|
|
49
|
+
const stripped = bodyStr.replace(new RegExp(`${objName}\\[${keyName}\\]`, "g"), "");
|
|
50
|
+
const keyStillUsed = new RegExp(`\\b${keyName}\\b`).test(stripped);
|
|
51
|
+
|
|
52
|
+
if (keyStillUsed) {
|
|
53
|
+
context.report({
|
|
54
|
+
message: `Object keys+values: \`Object.keys(${objName}).${methodName.name}(${keyName} => ... ${objName}[${keyName}] ...)\` can use \`Object.entries(${objName})\` for clearer access to both key and value. (clippy: for_kv_map)`,
|
|
55
|
+
node,
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
context.report({
|
|
59
|
+
message: `Object keys→values: \`Object.keys(${objName}).${methodName.name}(${keyName} => ... ${objName}[${keyName}] ...)\` can be simplified to \`Object.values(${objName}).${methodName.name}(...)\`. (clippy: for_kv_map)`,
|
|
60
|
+
node,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// clippy principle: "don't reinvent the standard library"
|
|
2
|
+
// Detects: JSON.parse(JSON.stringify(x)) → structuredClone(x)
|
|
3
|
+
// structuredClone handles more types (Date, RegExp, Map, Set, ArrayBuffer, etc.)
|
|
4
|
+
// and avoids the gotchas of JSON round-tripping (undefined → missing, functions → missing).
|
|
5
|
+
|
|
6
|
+
import type { Context, Node } from "../types";
|
|
7
|
+
import { isIdentifier, isCallOf } from "../types";
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
create(context: Context) {
|
|
11
|
+
return {
|
|
12
|
+
CallExpression(node: Node) {
|
|
13
|
+
// JSON.parse(JSON.stringify(x))
|
|
14
|
+
if (!isCallOf(node, "JSON", "parse")) return;
|
|
15
|
+
|
|
16
|
+
const args = node.arguments;
|
|
17
|
+
if (!args || args.length !== 1) return;
|
|
18
|
+
|
|
19
|
+
const inner = args[0]!;
|
|
20
|
+
if (isCallOf(inner, "JSON", "stringify") && inner.arguments?.length === 1) {
|
|
21
|
+
context.report({
|
|
22
|
+
message:
|
|
23
|
+
"Prefer structuredClone: `JSON.parse(JSON.stringify(x))` can be replaced with `structuredClone(x)`, which handles more types and is more correct. (clippy: don't reinvent stdlib)",
|
|
24
|
+
node,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// clippy principle: "avoid error-prone constructors"
|
|
2
|
+
// Detects: new Promise((resolve) => resolve(x)) → Promise.resolve(x)
|
|
3
|
+
// Detects: new Promise((_, reject) => reject(x)) → Promise.reject(x)
|
|
4
|
+
// The Promise constructor is needed for async operations, not for wrapping sync values.
|
|
5
|
+
|
|
6
|
+
import type { Context, Node } from "../types";
|
|
7
|
+
import { isIdentifier } from "../types";
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
create(context: Context) {
|
|
11
|
+
return {
|
|
12
|
+
NewExpression(node: Node) {
|
|
13
|
+
if (!isIdentifier(node.callee, "Promise")) return;
|
|
14
|
+
if (!node.arguments || node.arguments.length !== 1) return;
|
|
15
|
+
|
|
16
|
+
const callback = node.arguments[0]!;
|
|
17
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")
|
|
18
|
+
return;
|
|
19
|
+
if (callback.params.length < 1) return;
|
|
20
|
+
|
|
21
|
+
const resolveParam = callback.params[0]!;
|
|
22
|
+
const rejectParam = callback.params[1];
|
|
23
|
+
|
|
24
|
+
let bodyExpr: Node | null = null;
|
|
25
|
+
if (callback.body.type === "BlockStatement" && callback.body.body.length === 1) {
|
|
26
|
+
const stmt = callback.body.body[0]!;
|
|
27
|
+
if (stmt.type === "ExpressionStatement") bodyExpr = stmt.expression;
|
|
28
|
+
} else if (callback.body.type !== "BlockStatement") {
|
|
29
|
+
bodyExpr = callback.body;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!bodyExpr || bodyExpr.type !== "CallExpression") return;
|
|
33
|
+
if (bodyExpr.arguments.length > 1) return;
|
|
34
|
+
|
|
35
|
+
const calledFn = bodyExpr.callee;
|
|
36
|
+
if (calledFn.type !== "Identifier") return;
|
|
37
|
+
|
|
38
|
+
// new Promise((resolve) => resolve(x)) → Promise.resolve(x)
|
|
39
|
+
if (resolveParam.type === "Identifier" && calledFn.name === resolveParam.name) {
|
|
40
|
+
context.report({
|
|
41
|
+
message:
|
|
42
|
+
"Promise constructor wrapping sync value: `new Promise(resolve => resolve(x))` can be simplified to `Promise.resolve(x)`. (clippy: avoid error-prone constructors)",
|
|
43
|
+
node,
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// new Promise((_, reject) => reject(x)) → Promise.reject(x)
|
|
49
|
+
if (rejectParam?.type === "Identifier" && calledFn.name === rejectParam.name) {
|
|
50
|
+
context.report({
|
|
51
|
+
message:
|
|
52
|
+
"Promise constructor wrapping sync rejection: `new Promise((_, reject) => reject(x))` can be simplified to `Promise.reject(x)`. (clippy: avoid error-prone constructors)",
|
|
53
|
+
node,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// clippy::redundant_closure_call — immediately invoked function expression wrapping a simple expression
|
|
2
|
+
// Detects: (() => expr)() → expr
|
|
3
|
+
// Detects: (function() { return expr; })() → expr
|
|
4
|
+
// Only flags single-expression/return IIFEs, not those with side effects or multiple statements.
|
|
5
|
+
|
|
6
|
+
import type { Context, Node } from "../types";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
create(context: Context) {
|
|
10
|
+
return {
|
|
11
|
+
CallExpression(node: Node) {
|
|
12
|
+
const { callee } = node;
|
|
13
|
+
if (node.arguments.length !== 0) return;
|
|
14
|
+
|
|
15
|
+
// Unwrap parenthesized expression — the callee may be in parens
|
|
16
|
+
let fn = callee;
|
|
17
|
+
// In ESTree, parens don't produce a wrapper node, so callee is the function directly
|
|
18
|
+
|
|
19
|
+
if (fn.type === "ArrowFunctionExpression" && fn.params.length === 0) {
|
|
20
|
+
// (() => expr)()
|
|
21
|
+
if (fn.body.type !== "BlockStatement") {
|
|
22
|
+
context.report({
|
|
23
|
+
message:
|
|
24
|
+
"Redundant closure call: this IIFE wraps a single expression. Use the expression directly. (clippy::redundant_closure_call)",
|
|
25
|
+
node,
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// (() => { return expr; })()
|
|
30
|
+
if (fn.body.body.length === 1) {
|
|
31
|
+
const stmt = fn.body.body[0]!;
|
|
32
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
33
|
+
context.report({
|
|
34
|
+
message:
|
|
35
|
+
"Redundant closure call: this IIFE wraps a single return. Use the expression directly. (clippy::redundant_closure_call)",
|
|
36
|
+
node,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
fn.type === "FunctionExpression" &&
|
|
45
|
+
fn.params.length === 0 &&
|
|
46
|
+
fn.body.type === "BlockStatement" &&
|
|
47
|
+
fn.body.body.length === 1
|
|
48
|
+
) {
|
|
49
|
+
const stmt = fn.body.body[0]!;
|
|
50
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
51
|
+
context.report({
|
|
52
|
+
message:
|
|
53
|
+
"Redundant closure call: this IIFE wraps a single return. Use the expression directly. (clippy::redundant_closure_call)",
|
|
54
|
+
node,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// clippy::redundant_closure — wrapper closures around known single-arg functions
|
|
2
|
+
// Detects: .map(x => String(x)) → .map(String)
|
|
3
|
+
// Detects: .filter(x => Boolean(x)) → .filter(Boolean)
|
|
4
|
+
//
|
|
5
|
+
// SAFETY: Only flags known-safe builtins that ignore extra arguments from .map(val, idx, arr).
|
|
6
|
+
// Does NOT flag arbitrary functions (e.g. parseInt takes a radix arg, so .map(parseInt) breaks).
|
|
7
|
+
|
|
8
|
+
import type { Context, Node } from "../types";
|
|
9
|
+
import { isIdentifier } from "../types";
|
|
10
|
+
|
|
11
|
+
// Builtins that only use their first argument, safe to pass directly to .map/.filter/etc.
|
|
12
|
+
const SAFE_SINGLE_ARG = new Set([
|
|
13
|
+
"String",
|
|
14
|
+
"Number",
|
|
15
|
+
"Boolean",
|
|
16
|
+
"BigInt",
|
|
17
|
+
"parseFloat",
|
|
18
|
+
"isNaN",
|
|
19
|
+
"isFinite",
|
|
20
|
+
"encodeURIComponent",
|
|
21
|
+
"decodeURIComponent",
|
|
22
|
+
"encodeURI",
|
|
23
|
+
"decodeURI",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
create(context: Context) {
|
|
28
|
+
return {
|
|
29
|
+
CallExpression(node: Node) {
|
|
30
|
+
// Look for .method(x => fn(x)) or .method(function(x) { return fn(x); })
|
|
31
|
+
const callee = node.callee;
|
|
32
|
+
if (callee.type !== "MemberExpression") return;
|
|
33
|
+
|
|
34
|
+
const args = node.arguments;
|
|
35
|
+
if (!args || args.length !== 1) return;
|
|
36
|
+
|
|
37
|
+
const callback = args[0]!;
|
|
38
|
+
let paramName: string | null = null;
|
|
39
|
+
let bodyExpr: Node | null = null;
|
|
40
|
+
|
|
41
|
+
if (callback.type === "ArrowFunctionExpression" && callback.params.length === 1) {
|
|
42
|
+
const param = callback.params[0]!;
|
|
43
|
+
if (param.type !== "Identifier") return;
|
|
44
|
+
paramName = param.name;
|
|
45
|
+
|
|
46
|
+
if (callback.body.type === "BlockStatement" && callback.body.body.length === 1) {
|
|
47
|
+
const stmt = callback.body.body[0]!;
|
|
48
|
+
if (stmt.type === "ReturnStatement") bodyExpr = stmt.argument;
|
|
49
|
+
} else if (callback.body.type !== "BlockStatement") {
|
|
50
|
+
bodyExpr = callback.body;
|
|
51
|
+
}
|
|
52
|
+
} else if (callback.type === "FunctionExpression" && callback.params.length === 1) {
|
|
53
|
+
const param = callback.params[0]!;
|
|
54
|
+
if (param.type !== "Identifier") return;
|
|
55
|
+
paramName = param.name;
|
|
56
|
+
|
|
57
|
+
if (callback.body.type === "BlockStatement" && callback.body.body.length === 1) {
|
|
58
|
+
const stmt = callback.body.body[0]!;
|
|
59
|
+
if (stmt.type === "ReturnStatement") bodyExpr = stmt.argument;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!paramName || !bodyExpr) return;
|
|
64
|
+
if (bodyExpr.type !== "CallExpression") return;
|
|
65
|
+
if (bodyExpr.arguments.length !== 1) return;
|
|
66
|
+
|
|
67
|
+
const innerArg = bodyExpr.arguments[0]!;
|
|
68
|
+
if (!isIdentifier(innerArg, paramName)) return;
|
|
69
|
+
|
|
70
|
+
// Check if the called function is a safe single-arg builtin
|
|
71
|
+
const fnCallee = bodyExpr.callee;
|
|
72
|
+
if (isIdentifier(fnCallee) && SAFE_SINGLE_ARG.has(fnCallee.name)) {
|
|
73
|
+
context.report({
|
|
74
|
+
message: `Redundant closure: \`x => ${fnCallee.name}(x)\` can be simplified to \`${fnCallee.name}\`. (clippy::redundant_closure)`,
|
|
75
|
+
node: callback,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// clippy::search_is_some — .find() compared to undefined/null when .some() suffices
|
|
2
|
+
// Detects: arr.find(fn) !== undefined → arr.some(fn)
|
|
3
|
+
// Detects: arr.find(fn) != null → arr.some(fn)
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
import { isMethodCall } from "../types";
|
|
7
|
+
|
|
8
|
+
function isNullish(node: Node): boolean {
|
|
9
|
+
if (node.type === "Identifier" && node.name === "undefined") return true;
|
|
10
|
+
if (node.type === "Literal" && node.value === null) return true;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isFindCall(node: Node): boolean {
|
|
15
|
+
return isMethodCall(node, "find");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function matchesFindNullish(left: Node, right: Node): boolean {
|
|
19
|
+
return (isFindCall(left) && isNullish(right)) || (isNullish(left) && isFindCall(right));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
create(context: Context) {
|
|
24
|
+
return {
|
|
25
|
+
BinaryExpression(node: Node) {
|
|
26
|
+
const { operator, left, right } = node;
|
|
27
|
+
if (!matchesFindNullish(left, right)) return;
|
|
28
|
+
|
|
29
|
+
if (operator === "!==" || operator === "!=") {
|
|
30
|
+
context.report({
|
|
31
|
+
message:
|
|
32
|
+
"Search is some: `.find(fn) !== undefined` can be simplified to `.some(fn)` when you don't need the found value. (clippy::search_is_some)",
|
|
33
|
+
node,
|
|
34
|
+
});
|
|
35
|
+
} else if (operator === "===" || operator === "==") {
|
|
36
|
+
context.report({
|
|
37
|
+
message:
|
|
38
|
+
"Search is some: `.find(fn) === undefined` can be simplified to `!.some(fn)` when you don't need the found value. (clippy::search_is_some)",
|
|
39
|
+
node,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// clippy::similar_names — variables with confusingly similar names
|
|
2
|
+
// Detects: names differing by one character (item/items, data/date, form/from)
|
|
3
|
+
// Minimum length: 3 characters. Excludes common pairs like i/j/k, x/y/z.
|
|
4
|
+
|
|
5
|
+
import type { Context, Node } from "../types";
|
|
6
|
+
|
|
7
|
+
const ALLOWED_PAIRS = new Set([
|
|
8
|
+
"i,j",
|
|
9
|
+
"j,k",
|
|
10
|
+
"i,k",
|
|
11
|
+
"x,y",
|
|
12
|
+
"y,z",
|
|
13
|
+
"x,z",
|
|
14
|
+
"a,b",
|
|
15
|
+
"b,c",
|
|
16
|
+
"a,c",
|
|
17
|
+
"n,m",
|
|
18
|
+
"r,s",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/** Returns true if names are confusingly similar (edit distance 1 or single transposition) */
|
|
22
|
+
function areSimilar(a: string, b: string): boolean {
|
|
23
|
+
if (Math.abs(a.length - b.length) > 1) return false;
|
|
24
|
+
if (a.length === b.length) {
|
|
25
|
+
let diffs = 0;
|
|
26
|
+
let firstDiff = -1;
|
|
27
|
+
for (let i = 0; i < a.length; i++) {
|
|
28
|
+
if (a[i] !== b[i]) {
|
|
29
|
+
if (diffs === 0) firstDiff = i;
|
|
30
|
+
diffs++;
|
|
31
|
+
if (diffs > 2) return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (diffs <= 1) return diffs === 1;
|
|
35
|
+
// Check for transposition: ab→ba
|
|
36
|
+
if (diffs === 2 && a[firstDiff] === b[firstDiff + 1] && a[firstDiff + 1] === b[firstDiff])
|
|
37
|
+
return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// Length differs by 1 — check for single insertion/deletion
|
|
41
|
+
const [shorter, longer] = a.length < b.length ? [a, b] : [b, a];
|
|
42
|
+
let diffs = 0;
|
|
43
|
+
let si = 0;
|
|
44
|
+
for (let li = 0; li < longer.length; li++) {
|
|
45
|
+
if (shorter[si] === longer[li]) {
|
|
46
|
+
si++;
|
|
47
|
+
} else {
|
|
48
|
+
diffs++;
|
|
49
|
+
if (diffs > 1) return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function collectNames(node: Node, names: Map<string, Node>) {
|
|
56
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
57
|
+
|
|
58
|
+
if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
|
|
59
|
+
names.set(node.id.name, node.id);
|
|
60
|
+
}
|
|
61
|
+
if (node.type === "FunctionDeclaration" && node.id?.type === "Identifier") {
|
|
62
|
+
names.set(node.id.name, node.id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Collect parameters
|
|
66
|
+
if (node.params) {
|
|
67
|
+
for (const p of node.params) {
|
|
68
|
+
if (p.type === "Identifier") names.set(p.name, p);
|
|
69
|
+
if (p.type === "AssignmentPattern" && p.left?.type === "Identifier")
|
|
70
|
+
names.set(p.left.name, p.left);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Don't recurse into nested functions (separate scope)
|
|
75
|
+
if (
|
|
76
|
+
node.type === "FunctionDeclaration" ||
|
|
77
|
+
node.type === "FunctionExpression" ||
|
|
78
|
+
node.type === "ArrowFunctionExpression"
|
|
79
|
+
)
|
|
80
|
+
return;
|
|
81
|
+
|
|
82
|
+
for (const key of Object.keys(node)) {
|
|
83
|
+
if (
|
|
84
|
+
key === "type" ||
|
|
85
|
+
key === "loc" ||
|
|
86
|
+
key === "range" ||
|
|
87
|
+
key === "parent" ||
|
|
88
|
+
key === "start" ||
|
|
89
|
+
key === "end"
|
|
90
|
+
)
|
|
91
|
+
continue;
|
|
92
|
+
const val = node[key];
|
|
93
|
+
if (Array.isArray(val)) {
|
|
94
|
+
for (const child of val) {
|
|
95
|
+
if (child && typeof child === "object" && child.type) collectNames(child, names);
|
|
96
|
+
}
|
|
97
|
+
} else if (val && typeof val === "object" && val.type) {
|
|
98
|
+
collectNames(val, names);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default {
|
|
104
|
+
create(context: Context) {
|
|
105
|
+
function checkScope(node: Node) {
|
|
106
|
+
const names = new Map<string, Node>();
|
|
107
|
+
|
|
108
|
+
// Collect params
|
|
109
|
+
if (node.params) {
|
|
110
|
+
for (const p of node.params) {
|
|
111
|
+
if (p.type === "Identifier") names.set(p.name, p);
|
|
112
|
+
if (p.type === "AssignmentPattern" && p.left?.type === "Identifier")
|
|
113
|
+
names.set(p.left.name, p.left);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Collect from body
|
|
118
|
+
if (node.body?.type === "BlockStatement") {
|
|
119
|
+
for (const stmt of node.body.body) {
|
|
120
|
+
collectNames(stmt, names);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const nameList = [...names.entries()];
|
|
125
|
+
const reported = new Set<string>();
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < nameList.length; i++) {
|
|
128
|
+
for (let j = i + 1; j < nameList.length; j++) {
|
|
129
|
+
const [nameA] = nameList[i]!;
|
|
130
|
+
const [nameB, nodeB] = nameList[j]!;
|
|
131
|
+
|
|
132
|
+
if (nameA.length < 3 || nameB.length < 3) continue;
|
|
133
|
+
|
|
134
|
+
const pair = [nameA, nameB].sort().join(",");
|
|
135
|
+
if (ALLOWED_PAIRS.has(pair)) continue;
|
|
136
|
+
if (reported.has(pair)) continue;
|
|
137
|
+
|
|
138
|
+
if (areSimilar(nameA.toLowerCase(), nameB.toLowerCase())) {
|
|
139
|
+
reported.add(pair);
|
|
140
|
+
context.report({
|
|
141
|
+
message: `Similar names: \`${nameA}\` and \`${nameB}\` differ by only one character, which is easy to confuse. (clippy::similar_names)`,
|
|
142
|
+
node: nodeB,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
FunctionDeclaration: checkScope,
|
|
151
|
+
FunctionExpression: checkScope,
|
|
152
|
+
ArrowFunctionExpression: checkScope,
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// clippy::single_match — switch statement with only one non-default case
|
|
2
|
+
// Detects: switch(x) { case 'a': ...; break; } → if (x === 'a') { ... }
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
create(context: Context) {
|
|
8
|
+
return {
|
|
9
|
+
SwitchStatement(node: Node) {
|
|
10
|
+
const cases: Node[] = node.cases;
|
|
11
|
+
if (!cases) return;
|
|
12
|
+
|
|
13
|
+
const nonDefaultCases = cases.filter((c: Node) => c.test !== null);
|
|
14
|
+
const defaultCase = cases.find((c: Node) => c.test === null);
|
|
15
|
+
|
|
16
|
+
if (nonDefaultCases.length !== 1) return;
|
|
17
|
+
|
|
18
|
+
// If single case + optional default, suggest if/else
|
|
19
|
+
const suggestion = defaultCase ? "if/else" : "if";
|
|
20
|
+
context.report({
|
|
21
|
+
message: `Single-case switch: this switch has only one case. Use an \`${suggestion}\` statement instead. (clippy::single_match)`,
|
|
22
|
+
node,
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// clippy::single_element_loop — looping over a single-element array literal
|
|
2
|
+
// Detects: for (const x of [value]) { ... } → just use value directly
|
|
3
|
+
|
|
4
|
+
import type { Context, Node } from "../types";
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
create(context: Context) {
|
|
8
|
+
return {
|
|
9
|
+
ForOfStatement(node: Node) {
|
|
10
|
+
const right = node.right;
|
|
11
|
+
if (right?.type === "ArrayExpression" && right.elements?.length === 1) {
|
|
12
|
+
context.report({
|
|
13
|
+
message:
|
|
14
|
+
"Single element loop: this loop iterates over a single-element array. Use the value directly. (clippy::single_element_loop)",
|
|
15
|
+
node,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
};
|