@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.
@@ -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-chained-type-assertions.ts
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 = defineRule({
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 defineRule2 } from "@oxlint/plugins";
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 = defineRule2({
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 defineRule3 } from "@oxlint/plugins";
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 substitutions2 = aliasSubstitution(alias, unwrapped, new Map);
507
- const resolved2 = substitutions2 === null ? null : classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions2, new Set([name]));
508
- return resolved2?.kind === "open dictionary" ? { kind: "generic container" } : null;
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 = defineRule3({
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 defineRule4 } from "@oxlint/plugins";
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 = defineRule4({
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 defineRule5 } from "@oxlint/plugins";
985
- var noObjectParametersRule = defineRule5({
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 defineRule6 } from "@oxlint/plugins";
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 = defineRule6({
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 defineRule7 } from "@oxlint/plugins";
1094
- var noReflectGetRule = defineRule7({
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 defineRule8 } from "@oxlint/plugins";
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 = defineRule8({
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 defineRule9 } from "@oxlint/plugins";
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 = defineRule9({
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 defineRule10 } from "@oxlint/plugins";
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 = defineRule10({
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 defineRule11 } from "@oxlint/plugins";
1265
- var noUnknownReturnsRule = defineRule11({
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 defineRule12 } from "@oxlint/plugins";
1319
- var noUnknownTypeAliasesRule = defineRule12({
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 defineRule13 } from "@oxlint/plugins";
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 = defineRule13({
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 defineRule14 } from "@oxlint/plugins";
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 = defineRule14({
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 defineRule15 } from "@oxlint/plugins";
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 = defineRule15({
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.0",
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.81.0",
49
- "oxlint": "1.81.0"
48
+ "@oxlint/plugins": "1.82.0",
49
+ "oxlint": "1.82.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@oxlint/plugins": "1.81.0",
52
+ "@oxlint/plugins": "1.82.0",
53
53
  "@types/bun": "^1.3.14",
54
- "oxlint": "1.81.0",
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
+ });