@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
|
@@ -1,8 +1,203 @@
|
|
|
1
1
|
// vendor/anti-slop/src/index.ts
|
|
2
2
|
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
3
|
|
|
4
|
-
// vendor/anti-slop/src/rules/no-
|
|
4
|
+
// vendor/anti-slop/src/rules/no-array-filter-map.ts
|
|
5
5
|
import { defineRule } from "@oxlint/plugins";
|
|
6
|
+
|
|
7
|
+
// vendor/anti-slop/src/shared/array-method.ts
|
|
8
|
+
function unwrapArrayExpression(node) {
|
|
9
|
+
while (node.type === "ParenthesizedExpression" || node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression") {
|
|
10
|
+
node = node.expression;
|
|
11
|
+
}
|
|
12
|
+
return node;
|
|
13
|
+
}
|
|
14
|
+
function resolveArrayBinding(sourceCode, node) {
|
|
15
|
+
node = unwrapArrayExpression(node);
|
|
16
|
+
if (node.type !== "Identifier")
|
|
17
|
+
return null;
|
|
18
|
+
let scope = sourceCode.getScope(node);
|
|
19
|
+
while (scope !== null) {
|
|
20
|
+
const variable = scope.set.get(node.name);
|
|
21
|
+
if (variable !== undefined)
|
|
22
|
+
return variable;
|
|
23
|
+
scope = scope.upper;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function arrayMethodTarget(node) {
|
|
28
|
+
node = unwrapArrayExpression(node);
|
|
29
|
+
if (node.type !== "MemberExpression")
|
|
30
|
+
return null;
|
|
31
|
+
const property = node.property;
|
|
32
|
+
if (!node.computed && property.type === "Identifier") {
|
|
33
|
+
return { name: property.name, object: node.object };
|
|
34
|
+
}
|
|
35
|
+
if (node.computed && property.type === "Literal" && typeof property.value === "string") {
|
|
36
|
+
return { name: property.value, object: node.object };
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function isArrayAnnotation(type) {
|
|
41
|
+
if (type.type === "TSArrayType" || type.type === "TSTupleType")
|
|
42
|
+
return true;
|
|
43
|
+
if (type.type === "TSParenthesizedType")
|
|
44
|
+
return isArrayAnnotation(type.typeAnnotation);
|
|
45
|
+
if (type.type === "TSTypeOperator" && type.operator === "readonly") {
|
|
46
|
+
return isArrayAnnotation(type.typeAnnotation);
|
|
47
|
+
}
|
|
48
|
+
return type.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray");
|
|
49
|
+
}
|
|
50
|
+
function isKnownArrayExpression(sourceCode, node, visited = new Set) {
|
|
51
|
+
node = unwrapArrayExpression(node);
|
|
52
|
+
if (node.type === "ArrayExpression")
|
|
53
|
+
return true;
|
|
54
|
+
if (node.type === "CallExpression") {
|
|
55
|
+
const method = arrayMethodTarget(node.callee);
|
|
56
|
+
return method !== null && ["map", "filter", "flatMap", "slice", "concat", "toSorted", "toReversed", "toSpliced"].includes(method.name) && isKnownArrayExpression(sourceCode, method.object, visited);
|
|
57
|
+
}
|
|
58
|
+
if (node.type !== "Identifier")
|
|
59
|
+
return false;
|
|
60
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
61
|
+
if (variable === null || visited.has(variable))
|
|
62
|
+
return false;
|
|
63
|
+
visited.add(variable);
|
|
64
|
+
if (variable.references.some((reference) => reference.isWrite() && !reference.init))
|
|
65
|
+
return false;
|
|
66
|
+
for (const identifier of variable.identifiers) {
|
|
67
|
+
const annotation = identifier.typeAnnotation?.typeAnnotation;
|
|
68
|
+
if (annotation !== undefined)
|
|
69
|
+
return isArrayAnnotation(annotation);
|
|
70
|
+
}
|
|
71
|
+
for (const definition of variable.defs) {
|
|
72
|
+
if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") {
|
|
73
|
+
return isKnownArrayExpression(sourceCode, definition.node.init, visited);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// vendor/anti-slop/src/rules/no-array-filter-map.ts
|
|
80
|
+
var noArrayFilterMapRule = defineRule({
|
|
81
|
+
meta: {
|
|
82
|
+
type: "suggestion",
|
|
83
|
+
docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
|
|
84
|
+
messages: {
|
|
85
|
+
arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics."
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
createOnce(context) {
|
|
89
|
+
return {
|
|
90
|
+
CallExpression(node) {
|
|
91
|
+
const outer = arrayMethodTarget(node.callee);
|
|
92
|
+
if (outer === null || outer.name !== "map" && outer.name !== "filter")
|
|
93
|
+
return;
|
|
94
|
+
const innerCall = unwrapArrayExpression(outer.object);
|
|
95
|
+
if (innerCall.type !== "CallExpression")
|
|
96
|
+
return;
|
|
97
|
+
const inner = arrayMethodTarget(innerCall.callee);
|
|
98
|
+
if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map"))
|
|
99
|
+
return;
|
|
100
|
+
if (!isKnownArrayExpression(context.sourceCode, inner.object))
|
|
101
|
+
return;
|
|
102
|
+
context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// vendor/anti-slop/src/rules/no-reduce-accumulator-copy.ts
|
|
109
|
+
import { defineRule as defineRule2 } from "@oxlint/plugins";
|
|
110
|
+
function enclosingReducer(node) {
|
|
111
|
+
let parent = node.parent;
|
|
112
|
+
while (parent !== null) {
|
|
113
|
+
if (parent.type === "FunctionDeclaration")
|
|
114
|
+
return null;
|
|
115
|
+
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
|
|
116
|
+
const callback = parent;
|
|
117
|
+
let owner = callback.parent;
|
|
118
|
+
while (owner !== null && unwrapArrayExpression(owner) === callback)
|
|
119
|
+
owner = owner.parent;
|
|
120
|
+
if (owner?.type !== "CallExpression")
|
|
121
|
+
return null;
|
|
122
|
+
const method = arrayMethodTarget(owner.callee);
|
|
123
|
+
const firstArgument = owner.arguments[0];
|
|
124
|
+
if (method === null || method.name !== "reduce" && method.name !== "reduceRight" || owner.arguments.length > 2 || firstArgument === undefined || unwrapArrayExpression(firstArgument) !== callback)
|
|
125
|
+
return null;
|
|
126
|
+
const firstParameter = callback.params[0];
|
|
127
|
+
const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
|
|
128
|
+
if (accumulator?.type !== "Identifier")
|
|
129
|
+
return null;
|
|
130
|
+
return { callback, accumulator, initialValue: owner.arguments[1] };
|
|
131
|
+
}
|
|
132
|
+
parent = parent.parent;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function referencesAccumulator(sourceCode, node, accumulator, visited = new Set) {
|
|
137
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
138
|
+
if (variable === null || visited.has(variable))
|
|
139
|
+
return false;
|
|
140
|
+
if (variable === accumulator)
|
|
141
|
+
return true;
|
|
142
|
+
visited.add(variable);
|
|
143
|
+
if (variable.references.some((reference) => reference.isWrite() && !reference.init))
|
|
144
|
+
return false;
|
|
145
|
+
for (const definition of variable.defs) {
|
|
146
|
+
if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") {
|
|
147
|
+
return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
function isGlobalCopyOwner(sourceCode, node, name) {
|
|
153
|
+
node = unwrapArrayExpression(node);
|
|
154
|
+
if (node.type !== "Identifier" || node.name !== name)
|
|
155
|
+
return false;
|
|
156
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
157
|
+
return variable === null || variable.defs.length === 0;
|
|
158
|
+
}
|
|
159
|
+
var noReduceAccumulatorCopyRule = defineRule2({
|
|
160
|
+
meta: {
|
|
161
|
+
type: "problem",
|
|
162
|
+
docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
|
|
163
|
+
messages: {
|
|
164
|
+
accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap."
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
createOnce(context) {
|
|
168
|
+
return {
|
|
169
|
+
CallExpression(node) {
|
|
170
|
+
const method = arrayMethodTarget(node.callee);
|
|
171
|
+
if (method === null)
|
|
172
|
+
return;
|
|
173
|
+
const reducer = enclosingReducer(node);
|
|
174
|
+
if (reducer === null)
|
|
175
|
+
return;
|
|
176
|
+
const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find((variable) => variable.identifiers.some((identifier) => identifier.start === reducer.accumulator.start));
|
|
177
|
+
if (accumulator === undefined)
|
|
178
|
+
return;
|
|
179
|
+
const isAccumulator = (expression) => referencesAccumulator(context.sourceCode, expression, accumulator);
|
|
180
|
+
let copiesAccumulator = false;
|
|
181
|
+
if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
|
|
182
|
+
const target = node.arguments[0];
|
|
183
|
+
copiesAccumulator = target !== undefined && unwrapArrayExpression(target).type === "ObjectExpression" && node.arguments.slice(1).some(isAccumulator);
|
|
184
|
+
} else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
|
|
185
|
+
const source = node.arguments[0];
|
|
186
|
+
copiesAccumulator = source !== undefined && isAccumulator(source);
|
|
187
|
+
} else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) {
|
|
188
|
+
const initialValue = reducer.initialValue;
|
|
189
|
+
const arrayAccumulator = initialValue !== undefined && isKnownArrayExpression(context.sourceCode, initialValue);
|
|
190
|
+
copiesAccumulator = arrayAccumulator && isAccumulator(method.object);
|
|
191
|
+
}
|
|
192
|
+
if (copiesAccumulator)
|
|
193
|
+
context.report({ node, messageId: "accumulatorCopy" });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// vendor/anti-slop/src/rules/no-chained-type-assertions.ts
|
|
200
|
+
import { defineRule as defineRule3 } from "@oxlint/plugins";
|
|
6
201
|
function isTypeAssertionExpression(node) {
|
|
7
202
|
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
|
|
8
203
|
}
|
|
@@ -37,7 +232,7 @@ function isForbiddenAssertionChain(node) {
|
|
|
37
232
|
}
|
|
38
233
|
return assertionCount > 1 && hasNonConstAssertion;
|
|
39
234
|
}
|
|
40
|
-
var noChainedTypeAssertionsRule =
|
|
235
|
+
var noChainedTypeAssertionsRule = defineRule3({
|
|
41
236
|
meta: {
|
|
42
237
|
type: "problem",
|
|
43
238
|
docs: {
|
|
@@ -61,7 +256,7 @@ var noChainedTypeAssertionsRule = defineRule({
|
|
|
61
256
|
});
|
|
62
257
|
|
|
63
258
|
// vendor/anti-slop/src/rules/no-conditional-empty-object-spread.ts
|
|
64
|
-
import { defineRule as
|
|
259
|
+
import { defineRule as defineRule4 } from "@oxlint/plugins";
|
|
65
260
|
function unwrapParentheses(node) {
|
|
66
261
|
let current = node;
|
|
67
262
|
while (current.type === "ParenthesizedExpression") {
|
|
@@ -76,7 +271,7 @@ function isConditionalEmptyObjectSpread(node) {
|
|
|
76
271
|
const conditional = unwrapParentheses(node);
|
|
77
272
|
return conditional.type === "ConditionalExpression" && (isEmptyObjectExpression(conditional.consequent) || isEmptyObjectExpression(conditional.alternate));
|
|
78
273
|
}
|
|
79
|
-
var noConditionalEmptyObjectSpreadRule =
|
|
274
|
+
var noConditionalEmptyObjectSpreadRule = defineRule4({
|
|
80
275
|
meta: {
|
|
81
276
|
type: "suggestion",
|
|
82
277
|
docs: {
|
|
@@ -100,7 +295,7 @@ var noConditionalEmptyObjectSpreadRule = defineRule2({
|
|
|
100
295
|
});
|
|
101
296
|
|
|
102
297
|
// vendor/anti-slop/src/rules/no-known-value-widening.ts
|
|
103
|
-
import { defineRule as
|
|
298
|
+
import { defineRule as defineRule5 } from "@oxlint/plugins";
|
|
104
299
|
|
|
105
300
|
// vendor/anti-slop/src/shared/lexical-type-parameters.ts
|
|
106
301
|
function isNode(value) {
|
|
@@ -503,9 +698,9 @@ function classifyWideningTarget(type, environment) {
|
|
|
503
698
|
if (alias === null)
|
|
504
699
|
return null;
|
|
505
700
|
if ((alias.typeParameters?.params.length ?? 0) > 0) {
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
return
|
|
701
|
+
const substitutions = aliasSubstitution(alias, unwrapped, new Map);
|
|
702
|
+
const resolved = substitutions === null ? null : classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name]));
|
|
703
|
+
return resolved?.kind === "open dictionary" ? { kind: "generic container" } : null;
|
|
509
704
|
}
|
|
510
705
|
const substitutions = aliasSubstitution(alias, unwrapped, new Map);
|
|
511
706
|
if (substitutions === null)
|
|
@@ -799,7 +994,7 @@ function isDictionaryAccumulatorTarget(destination) {
|
|
|
799
994
|
function hasParentAssertion(node) {
|
|
800
995
|
return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
|
|
801
996
|
}
|
|
802
|
-
var noKnownValueWideningRule =
|
|
997
|
+
var noKnownValueWideningRule = defineRule5({
|
|
803
998
|
meta: {
|
|
804
999
|
type: "problem",
|
|
805
1000
|
docs: {
|
|
@@ -912,7 +1107,7 @@ var noKnownValueWideningRule = defineRule3({
|
|
|
912
1107
|
});
|
|
913
1108
|
|
|
914
1109
|
// vendor/anti-slop/src/rules/no-module-mocking.ts
|
|
915
|
-
import { defineRule as
|
|
1110
|
+
import { defineRule as defineRule6 } from "@oxlint/plugins";
|
|
916
1111
|
var moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
|
|
917
1112
|
function resolveVariable2(sourceCode, identifier) {
|
|
918
1113
|
let scope = sourceCode.getScope(identifier);
|
|
@@ -957,7 +1152,7 @@ function moduleMockCall(sourceCode, callee) {
|
|
|
957
1152
|
const method = callee.computed ? property.type === "Literal" && (property.value === "doMock" || property.value === "mock" || property.value === "unstable_mockModule") ? property.value : null : property.type === "Identifier" ? property.name : null;
|
|
958
1153
|
return method !== null && moduleMockMethods.has(method);
|
|
959
1154
|
}
|
|
960
|
-
var noModuleMockingRule =
|
|
1155
|
+
var noModuleMockingRule = defineRule6({
|
|
961
1156
|
meta: {
|
|
962
1157
|
type: "problem",
|
|
963
1158
|
docs: {
|
|
@@ -981,8 +1176,8 @@ var noModuleMockingRule = defineRule4({
|
|
|
981
1176
|
});
|
|
982
1177
|
|
|
983
1178
|
// vendor/anti-slop/src/rules/no-object-parameters.ts
|
|
984
|
-
import { defineRule as
|
|
985
|
-
var noObjectParametersRule =
|
|
1179
|
+
import { defineRule as defineRule7 } from "@oxlint/plugins";
|
|
1180
|
+
var noObjectParametersRule = defineRule7({
|
|
986
1181
|
meta: {
|
|
987
1182
|
type: "problem",
|
|
988
1183
|
docs: {
|
|
@@ -1035,7 +1230,7 @@ var noObjectParametersRule = defineRule5({
|
|
|
1035
1230
|
});
|
|
1036
1231
|
|
|
1037
1232
|
// vendor/anti-slop/src/rules/no-reflect-apply.ts
|
|
1038
|
-
import { defineRule as
|
|
1233
|
+
import { defineRule as defineRule8 } from "@oxlint/plugins";
|
|
1039
1234
|
|
|
1040
1235
|
// vendor/anti-slop/src/shared/reflect-method.ts
|
|
1041
1236
|
function resolveVariable3(sourceCode, identifier) {
|
|
@@ -1066,7 +1261,7 @@ function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
|
|
|
1066
1261
|
}
|
|
1067
1262
|
|
|
1068
1263
|
// vendor/anti-slop/src/rules/no-reflect-apply.ts
|
|
1069
|
-
var noReflectApplyRule =
|
|
1264
|
+
var noReflectApplyRule = defineRule8({
|
|
1070
1265
|
meta: {
|
|
1071
1266
|
type: "problem",
|
|
1072
1267
|
docs: {
|
|
@@ -1090,8 +1285,8 @@ var noReflectApplyRule = defineRule6({
|
|
|
1090
1285
|
});
|
|
1091
1286
|
|
|
1092
1287
|
// vendor/anti-slop/src/rules/no-reflect-get.ts
|
|
1093
|
-
import { defineRule as
|
|
1094
|
-
var noReflectGetRule =
|
|
1288
|
+
import { defineRule as defineRule9 } from "@oxlint/plugins";
|
|
1289
|
+
var noReflectGetRule = defineRule9({
|
|
1095
1290
|
meta: {
|
|
1096
1291
|
type: "problem",
|
|
1097
1292
|
docs: {
|
|
@@ -1115,7 +1310,7 @@ var noReflectGetRule = defineRule7({
|
|
|
1115
1310
|
});
|
|
1116
1311
|
|
|
1117
1312
|
// vendor/anti-slop/src/rules/no-runtime-typeof.ts
|
|
1118
|
-
import { defineRule as
|
|
1313
|
+
import { defineRule as defineRule10 } from "@oxlint/plugins";
|
|
1119
1314
|
function isRuntimeFunction(node) {
|
|
1120
1315
|
return node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
|
|
1121
1316
|
}
|
|
@@ -1138,7 +1333,7 @@ function isExistenceProbe(node) {
|
|
|
1138
1333
|
const other = parent.left === node ? parent.right : parent.left;
|
|
1139
1334
|
return other.type === "Literal" && other.value === "undefined";
|
|
1140
1335
|
}
|
|
1141
|
-
var noRuntimeTypeofRule =
|
|
1336
|
+
var noRuntimeTypeofRule = defineRule10({
|
|
1142
1337
|
meta: {
|
|
1143
1338
|
type: "problem",
|
|
1144
1339
|
docs: {
|
|
@@ -1172,7 +1367,7 @@ var noRuntimeTypeofRule = defineRule8({
|
|
|
1172
1367
|
});
|
|
1173
1368
|
|
|
1174
1369
|
// vendor/anti-slop/src/rules/no-shape-in-symbol-names.ts
|
|
1175
|
-
import { defineRule as
|
|
1370
|
+
import { defineRule as defineRule11 } from "@oxlint/plugins";
|
|
1176
1371
|
var FORBIDDEN_SYMBOL_NAME = "shape";
|
|
1177
1372
|
function containsForbiddenSymbolName(name) {
|
|
1178
1373
|
return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
|
|
@@ -1183,7 +1378,7 @@ function isBorrowedMemberName(node) {
|
|
|
1183
1378
|
return false;
|
|
1184
1379
|
return parent.property === node && parent.computed === false;
|
|
1185
1380
|
}
|
|
1186
|
-
var noForbiddenTermInSymbolNamesRule =
|
|
1381
|
+
var noForbiddenTermInSymbolNamesRule = defineRule11({
|
|
1187
1382
|
meta: {
|
|
1188
1383
|
type: "problem",
|
|
1189
1384
|
docs: {
|
|
@@ -1212,12 +1407,12 @@ var noForbiddenTermInSymbolNamesRule = defineRule9({
|
|
|
1212
1407
|
});
|
|
1213
1408
|
|
|
1214
1409
|
// vendor/anti-slop/src/rules/no-unknown-parameters.ts
|
|
1215
|
-
import { defineRule as
|
|
1410
|
+
import { defineRule as defineRule12 } from "@oxlint/plugins";
|
|
1216
1411
|
function isTypePredicateSubject(owner, parameterName) {
|
|
1217
1412
|
const predicate = owner.returnType?.typeAnnotation;
|
|
1218
1413
|
return predicate?.type === "TSTypePredicate" && predicate.parameterName.type === "Identifier" && predicate.parameterName.name === parameterName;
|
|
1219
1414
|
}
|
|
1220
|
-
var noUnknownParametersRule =
|
|
1415
|
+
var noUnknownParametersRule = defineRule12({
|
|
1221
1416
|
meta: {
|
|
1222
1417
|
type: "problem",
|
|
1223
1418
|
docs: {
|
|
@@ -1261,8 +1456,8 @@ var noUnknownParametersRule = defineRule10({
|
|
|
1261
1456
|
});
|
|
1262
1457
|
|
|
1263
1458
|
// vendor/anti-slop/src/rules/no-unknown-returns.ts
|
|
1264
|
-
import { defineRule as
|
|
1265
|
-
var noUnknownReturnsRule =
|
|
1459
|
+
import { defineRule as defineRule13 } from "@oxlint/plugins";
|
|
1460
|
+
var noUnknownReturnsRule = defineRule13({
|
|
1266
1461
|
meta: {
|
|
1267
1462
|
type: "problem",
|
|
1268
1463
|
docs: {
|
|
@@ -1315,8 +1510,8 @@ var noUnknownReturnsRule = defineRule11({
|
|
|
1315
1510
|
});
|
|
1316
1511
|
|
|
1317
1512
|
// vendor/anti-slop/src/rules/no-unknown-type-aliases.ts
|
|
1318
|
-
import { defineRule as
|
|
1319
|
-
var noUnknownTypeAliasesRule =
|
|
1513
|
+
import { defineRule as defineRule14 } from "@oxlint/plugins";
|
|
1514
|
+
var noUnknownTypeAliasesRule = defineRule14({
|
|
1320
1515
|
meta: {
|
|
1321
1516
|
type: "problem",
|
|
1322
1517
|
docs: {
|
|
@@ -1354,7 +1549,7 @@ var noUnknownTypeAliasesRule = defineRule12({
|
|
|
1354
1549
|
});
|
|
1355
1550
|
|
|
1356
1551
|
// vendor/anti-slop/src/rules/no-unsafe-dictionary-type.ts
|
|
1357
|
-
import { defineRule as
|
|
1552
|
+
import { defineRule as defineRule15 } from "@oxlint/plugins";
|
|
1358
1553
|
var typeNodeKinds = new Set([
|
|
1359
1554
|
"JSDocNonNullableType",
|
|
1360
1555
|
"JSDocNullableType",
|
|
@@ -1441,7 +1636,7 @@ function shouldReportType(node, environment) {
|
|
|
1441
1636
|
}
|
|
1442
1637
|
return true;
|
|
1443
1638
|
}
|
|
1444
|
-
var noUnsafeDictionaryTypeRule =
|
|
1639
|
+
var noUnsafeDictionaryTypeRule = defineRule15({
|
|
1445
1640
|
meta: {
|
|
1446
1641
|
type: "problem",
|
|
1447
1642
|
docs: {
|
|
@@ -1483,7 +1678,7 @@ var noUnsafeDictionaryTypeRule = defineRule13({
|
|
|
1483
1678
|
});
|
|
1484
1679
|
|
|
1485
1680
|
// vendor/anti-slop/src/rules/no-widen-then-assert.ts
|
|
1486
|
-
import { defineRule as
|
|
1681
|
+
import { defineRule as defineRule16 } from "@oxlint/plugins";
|
|
1487
1682
|
var functionBoundaryTypes = new Set([
|
|
1488
1683
|
"ArrowFunctionExpression",
|
|
1489
1684
|
"FunctionDeclaration",
|
|
@@ -1679,7 +1874,7 @@ function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
|
|
|
1679
1874
|
return isDefinitelyObjectType(assertedType);
|
|
1680
1875
|
return isDefinitelyNarrowerRecordType(assertedType);
|
|
1681
1876
|
}
|
|
1682
|
-
var noWidenThenAssertRule =
|
|
1877
|
+
var noWidenThenAssertRule = defineRule16({
|
|
1683
1878
|
meta: {
|
|
1684
1879
|
type: "problem",
|
|
1685
1880
|
docs: {
|
|
@@ -1719,7 +1914,7 @@ var noWidenThenAssertRule = defineRule14({
|
|
|
1719
1914
|
});
|
|
1720
1915
|
|
|
1721
1916
|
// vendor/anti-slop/src/rules/require-safety-comment-for-type-assertion.ts
|
|
1722
|
-
import { defineRule as
|
|
1917
|
+
import { defineRule as defineRule17 } from "@oxlint/plugins";
|
|
1723
1918
|
var DEFAULT_SAFETY_MARKERS = ["SAFETY"];
|
|
1724
1919
|
var commentOwnerKinds = new Set([
|
|
1725
1920
|
"ExpressionStatement",
|
|
@@ -1762,7 +1957,7 @@ function hasSafetyComment(sourceCode, node, pattern) {
|
|
|
1762
1957
|
current = current.parent;
|
|
1763
1958
|
}
|
|
1764
1959
|
}
|
|
1765
|
-
var requireSafetyCommentForTypeAssertionRule =
|
|
1960
|
+
var requireSafetyCommentForTypeAssertionRule = defineRule17({
|
|
1766
1961
|
meta: {
|
|
1767
1962
|
type: "problem",
|
|
1768
1963
|
docs: {
|
|
@@ -1815,6 +2010,8 @@ var requireSafetyCommentForTypeAssertionRule = defineRule15({
|
|
|
1815
2010
|
var antiSlopPlugin = eslintCompatPlugin({
|
|
1816
2011
|
meta: { name: "anti-slop" },
|
|
1817
2012
|
rules: {
|
|
2013
|
+
"no-array-filter-map": noArrayFilterMapRule,
|
|
2014
|
+
"no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
|
|
1818
2015
|
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
1819
2016
|
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
|
|
1820
2017
|
"no-known-value-widening": noKnownValueWideningRule,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmo001/oxlint-rules",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Shared Oxlint plugins and configs with optional Effect rules.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"typecheck": "tsc --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@oxlint/plugins": "1.
|
|
49
|
-
"oxlint": "1.
|
|
48
|
+
"@oxlint/plugins": "1.82.0",
|
|
49
|
+
"oxlint": "1.82.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@oxlint/plugins": "1.
|
|
52
|
+
"@oxlint/plugins": "1.82.0",
|
|
53
53
|
"@types/bun": "^1.3.14",
|
|
54
|
-
"oxlint": "1.
|
|
54
|
+
"oxlint": "1.82.0",
|
|
55
55
|
"prettier": "^3.9.5",
|
|
56
56
|
"skills-ref": "0.1.5",
|
|
57
57
|
"typescript": "^7.0.0"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
2
|
|
|
3
|
+
import { noArrayFilterMapRule } from "./rules/no-array-filter-map.ts";
|
|
4
|
+
import { noReduceAccumulatorCopyRule } from "./rules/no-reduce-accumulator-copy.ts";
|
|
3
5
|
import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts";
|
|
4
6
|
import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts";
|
|
5
7
|
import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts";
|
|
@@ -20,6 +22,8 @@ import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety
|
|
|
20
22
|
const antiSlopPlugin = eslintCompatPlugin({
|
|
21
23
|
meta: { name: "anti-slop" },
|
|
22
24
|
rules: {
|
|
25
|
+
"no-array-filter-map": noArrayFilterMapRule,
|
|
26
|
+
"no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
|
|
23
27
|
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
24
28
|
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
|
|
25
29
|
"no-known-value-widening": noKnownValueWideningRule,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { arrayMethodTarget, isKnownArrayExpression, unwrapArrayExpression } from "../shared/array-method.ts";
|
|
4
|
+
|
|
5
|
+
/** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */
|
|
6
|
+
export const noArrayFilterMapRule = defineRule({
|
|
7
|
+
meta: {
|
|
8
|
+
type: "suggestion",
|
|
9
|
+
docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
|
|
10
|
+
messages: {
|
|
11
|
+
arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
createOnce(context) {
|
|
15
|
+
return {
|
|
16
|
+
CallExpression(node) {
|
|
17
|
+
const outer = arrayMethodTarget(node.callee);
|
|
18
|
+
if (outer === null || (outer.name !== "map" && outer.name !== "filter")) return;
|
|
19
|
+
const innerCall = unwrapArrayExpression(outer.object);
|
|
20
|
+
if (innerCall.type !== "CallExpression") return;
|
|
21
|
+
const inner = arrayMethodTarget(innerCall.callee);
|
|
22
|
+
if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map")) return;
|
|
23
|
+
if (!isKnownArrayExpression(context.sourceCode, inner.object)) return;
|
|
24
|
+
context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } });
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import type { ESTree, SourceCode, Variable } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
arrayMethodTarget,
|
|
6
|
+
isKnownArrayExpression,
|
|
7
|
+
resolveArrayBinding,
|
|
8
|
+
unwrapArrayExpression,
|
|
9
|
+
} from "../shared/array-method.ts";
|
|
10
|
+
|
|
11
|
+
function enclosingReducer(node: ESTree.Node) {
|
|
12
|
+
let parent = node.parent;
|
|
13
|
+
while (parent !== null) {
|
|
14
|
+
if (parent.type === "FunctionDeclaration") return null;
|
|
15
|
+
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") {
|
|
16
|
+
const callback = parent;
|
|
17
|
+
let owner: ESTree.Node | null = callback.parent;
|
|
18
|
+
while (owner !== null && unwrapArrayExpression(owner) === callback) owner = owner.parent;
|
|
19
|
+
if (owner?.type !== "CallExpression") return null;
|
|
20
|
+
const method = arrayMethodTarget(owner.callee);
|
|
21
|
+
const firstArgument = owner.arguments[0];
|
|
22
|
+
if (
|
|
23
|
+
method === null || (method.name !== "reduce" && method.name !== "reduceRight") ||
|
|
24
|
+
owner.arguments.length > 2 || firstArgument === undefined ||
|
|
25
|
+
unwrapArrayExpression(firstArgument) !== callback
|
|
26
|
+
) return null;
|
|
27
|
+
const firstParameter = callback.params[0];
|
|
28
|
+
const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
|
|
29
|
+
if (accumulator?.type !== "Identifier") return null;
|
|
30
|
+
return { callback, accumulator, initialValue: owner.arguments[1] };
|
|
31
|
+
}
|
|
32
|
+
parent = parent.parent;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function referencesAccumulator(
|
|
38
|
+
sourceCode: SourceCode,
|
|
39
|
+
node: ESTree.Node,
|
|
40
|
+
accumulator: Variable,
|
|
41
|
+
visited = new Set<Variable>(),
|
|
42
|
+
): boolean {
|
|
43
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
44
|
+
if (variable === null || visited.has(variable)) return false;
|
|
45
|
+
if (variable === accumulator) return true;
|
|
46
|
+
visited.add(variable);
|
|
47
|
+
if (variable.references.some(reference => reference.isWrite() && !reference.init)) return false;
|
|
48
|
+
for (const definition of variable.defs) {
|
|
49
|
+
if (
|
|
50
|
+
definition.type === "Variable" && definition.node.type === "VariableDeclarator" &&
|
|
51
|
+
definition.node.id.type === "Identifier" && definition.node.init !== null &&
|
|
52
|
+
definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const"
|
|
53
|
+
) {
|
|
54
|
+
return referencesAccumulator(sourceCode, definition.node.init, accumulator, visited);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isGlobalCopyOwner(sourceCode: SourceCode, node: ESTree.Node, name: string): boolean {
|
|
61
|
+
node = unwrapArrayExpression(node);
|
|
62
|
+
if (node.type !== "Identifier" || node.name !== name) return false;
|
|
63
|
+
const variable = resolveArrayBinding(sourceCode, node);
|
|
64
|
+
return variable === null || variable.defs.length === 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
|
|
68
|
+
export const noReduceAccumulatorCopyRule = defineRule({
|
|
69
|
+
meta: {
|
|
70
|
+
type: "problem",
|
|
71
|
+
docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
|
|
72
|
+
messages: {
|
|
73
|
+
accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
createOnce(context) {
|
|
77
|
+
return {
|
|
78
|
+
CallExpression(node) {
|
|
79
|
+
const method = arrayMethodTarget(node.callee);
|
|
80
|
+
if (method === null) return;
|
|
81
|
+
const reducer = enclosingReducer(node);
|
|
82
|
+
if (reducer === null) return;
|
|
83
|
+
const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find(variable =>
|
|
84
|
+
variable.identifiers.some(identifier => identifier.start === reducer.accumulator.start),
|
|
85
|
+
);
|
|
86
|
+
if (accumulator === undefined) return;
|
|
87
|
+
const isAccumulator = (expression: ESTree.Node) =>
|
|
88
|
+
referencesAccumulator(context.sourceCode, expression, accumulator);
|
|
89
|
+
let copiesAccumulator = false;
|
|
90
|
+
if (method.name === "assign" && isGlobalCopyOwner(context.sourceCode, method.object, "Object")) {
|
|
91
|
+
const target = node.arguments[0];
|
|
92
|
+
copiesAccumulator = (
|
|
93
|
+
target !== undefined && unwrapArrayExpression(target).type === "ObjectExpression" &&
|
|
94
|
+
node.arguments.slice(1).some(isAccumulator)
|
|
95
|
+
);
|
|
96
|
+
} else if (method.name === "from" && isGlobalCopyOwner(context.sourceCode, method.object, "Array")) {
|
|
97
|
+
const source = node.arguments[0];
|
|
98
|
+
copiesAccumulator = source !== undefined && isAccumulator(source);
|
|
99
|
+
} else if (["concat", "slice", "toSpliced", "toSorted", "toReversed", "with"].includes(method.name)) {
|
|
100
|
+
const initialValue = reducer.initialValue;
|
|
101
|
+
const arrayAccumulator = initialValue !== undefined &&
|
|
102
|
+
isKnownArrayExpression(context.sourceCode, initialValue);
|
|
103
|
+
copiesAccumulator = arrayAccumulator && isAccumulator(method.object);
|
|
104
|
+
}
|
|
105
|
+
if (copiesAccumulator) context.report({ node, messageId: "accumulatorCopy" });
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
});
|