@timmo001/oxlint-rules 0.3.0 → 0.3.1
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/dist/cli.js +247 -50
- package/dist/configs/recommended-effect.js +417 -220
- package/dist/configs/recommended.js +237 -40
- package/dist/upstream/anti-slop.js +230 -33
- package/package.json +5 -5
- package/vendor/anti-slop/src/index.ts +4 -0
- package/vendor/anti-slop/src/rules/no-array-filter-map.ts +28 -0
- package/vendor/anti-slop/src/rules/no-reduce-accumulator-copy.ts +109 -0
- package/vendor/anti-slop/src/shared/array-method.ts +94 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
|
|
4
|
+
export function unwrapArrayExpression(node: ESTree.Node): ESTree.Node {
|
|
5
|
+
while (
|
|
6
|
+
node.type === "ParenthesizedExpression" ||
|
|
7
|
+
node.type === "ChainExpression" ||
|
|
8
|
+
node.type === "TSAsExpression" ||
|
|
9
|
+
node.type === "TSTypeAssertion" ||
|
|
10
|
+
node.type === "TSNonNullExpression" ||
|
|
11
|
+
node.type === "TSSatisfiesExpression"
|
|
12
|
+
) {
|
|
13
|
+
node = node.expression;
|
|
14
|
+
}
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Resolve a local binding by scope, not by identifier spelling. */
|
|
19
|
+
export function resolveArrayBinding(sourceCode: SourceCode, node: ESTree.Node): Variable | null {
|
|
20
|
+
node = unwrapArrayExpression(node);
|
|
21
|
+
if (node.type !== "Identifier") return null;
|
|
22
|
+
let scope: Scope | null = sourceCode.getScope(node);
|
|
23
|
+
while (scope !== null) {
|
|
24
|
+
const variable = scope.set.get(node.name);
|
|
25
|
+
if (variable !== undefined) return variable;
|
|
26
|
+
scope = scope.upper;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Read static method names, including computed string literals, without evaluating expressions. */
|
|
32
|
+
export function arrayMethodTarget(
|
|
33
|
+
node: ESTree.Node,
|
|
34
|
+
): { readonly name: string; readonly object: ESTree.Node } | null {
|
|
35
|
+
node = unwrapArrayExpression(node);
|
|
36
|
+
if (node.type !== "MemberExpression") return null;
|
|
37
|
+
const property = node.property;
|
|
38
|
+
if (!node.computed && property.type === "Identifier") {
|
|
39
|
+
return { name: property.name, object: node.object };
|
|
40
|
+
}
|
|
41
|
+
if (node.computed && property.type === "Literal" && typeof property.value === "string") {
|
|
42
|
+
return { name: property.value, object: node.object };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isArrayAnnotation(type: ESTree.TSType): boolean {
|
|
48
|
+
if (type.type === "TSArrayType" || type.type === "TSTupleType") return true;
|
|
49
|
+
if (type.type === "TSParenthesizedType") return isArrayAnnotation(type.typeAnnotation);
|
|
50
|
+
if (type.type === "TSTypeOperator" && type.operator === "readonly") {
|
|
51
|
+
return isArrayAnnotation(type.typeAnnotation);
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
type.type === "TSTypeReference" && type.typeName.type === "Identifier" &&
|
|
55
|
+
(type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray")
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */
|
|
60
|
+
export function isKnownArrayExpression(
|
|
61
|
+
sourceCode: SourceCode,
|
|
62
|
+
node: ESTree.Node,
|
|
63
|
+
visited = new Set<Variable>(),
|
|
64
|
+
): boolean {
|
|
65
|
+
node = unwrapArrayExpression(node);
|
|
66
|
+
if (node.type === "ArrayExpression") return true;
|
|
67
|
+
if (node.type === "CallExpression") {
|
|
68
|
+
const method = arrayMethodTarget(node.callee);
|
|
69
|
+
return (
|
|
70
|
+
method !== null &&
|
|
71
|
+
["map", "filter", "flatMap", "slice", "concat", "toSorted", "toReversed", "toSpliced"].includes(method.name) &&
|
|
72
|
+
isKnownArrayExpression(sourceCode, method.object, visited)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (node.type !== "Identifier") return false;
|
|
76
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
77
|
+
if (variable === null || visited.has(variable)) return false;
|
|
78
|
+
visited.add(variable);
|
|
79
|
+
if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false;
|
|
80
|
+
for (const identifier of variable.identifiers) {
|
|
81
|
+
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
82
|
+
if (annotation !== undefined) return isArrayAnnotation(annotation);
|
|
83
|
+
}
|
|
84
|
+
for (const definition of variable.defs) {
|
|
85
|
+
if (
|
|
86
|
+
definition.type === "Variable" && definition.node.type === "VariableDeclarator" &&
|
|
87
|
+
definition.node.id.type === "Identifier" && definition.node.init !== null &&
|
|
88
|
+
definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const"
|
|
89
|
+
) {
|
|
90
|
+
return isKnownArrayExpression(sourceCode, definition.node.init, visited);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|