@timmo001/oxlint-rules 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +2 -1
  2. package/THIRD_PARTY_NOTICES.md +9 -0
  3. package/dist/cli.js +978 -83
  4. package/dist/configs/recommended-effect.js +1207 -312
  5. package/dist/configs/recommended.js +772 -70
  6. package/dist/upstream/anti-slop.js +765 -63
  7. package/dist/upstream/effect.js +196 -3
  8. package/package.json +5 -5
  9. package/vendor/anti-slop/src/effect/index.ts +8 -0
  10. package/vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts +52 -0
  11. package/vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts +45 -0
  12. package/vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts +37 -0
  13. package/vendor/anti-slop/src/effect/rules/prefer-effect-match.ts +54 -0
  14. package/vendor/anti-slop/src/effect/shared/tagged-values.ts +97 -0
  15. package/vendor/anti-slop/src/index.ts +6 -0
  16. package/vendor/anti-slop/src/rules/no-array-filter-map.ts +28 -0
  17. package/vendor/anti-slop/src/rules/no-known-value-widening.ts +2 -14
  18. package/vendor/anti-slop/src/rules/no-module-mocking.ts +3 -14
  19. package/vendor/anti-slop/src/rules/no-reduce-accumulator-copy.ts +109 -0
  20. package/vendor/anti-slop/src/rules/require-readable-spacing.ts +47 -0
  21. package/vendor/anti-slop/src/shared/array-method.ts +94 -0
  22. package/vendor/anti-slop/src/shared/reflect-method.ts +2 -13
  23. package/vendor/anti-slop/src/shared/scope.ts +15 -0
  24. package/vendor/anti-slop/src/vendor/eslint-stylistic/LICENSE +22 -0
  25. package/vendor/anti-slop/src/vendor/eslint-stylistic/UPSTREAM.md +28 -0
  26. package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-ast.ts +51 -0
  27. package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-between-statements.ts +906 -0
  28. package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-options.d.ts +87 -0
@@ -1,8 +1,149 @@
1
1
  // vendor/anti-slop/src/effect/index.ts
2
2
  import { eslintCompatPlugin } from "@oxlint/plugins";
3
3
 
4
- // vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
4
+ // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
5
5
  import { defineRule } from "@oxlint/plugins";
6
+
7
+ // vendor/anti-slop/src/effect/shared/tagged-values.ts
8
+ var equalityOperators = new Set(["==", "===", "!=", "!=="]);
9
+ var broadEffectCatchMethods = new Set(["catch", "catchAll", "catchIf"]);
10
+ var isStringLiteral = (node) => node?.type === "Literal" && typeof node.value === "string";
11
+ var isTagMember = (node) => node?.type === "MemberExpression" && (!node.computed && node.property.type === "Identifier" && node.property.name === "_tag" || node.computed && isStringLiteral(node.property) && node.property.value === "_tag");
12
+ var tagMemberFromComparison = (node) => {
13
+ if (!equalityOperators.has(node.operator))
14
+ return;
15
+ if (isTagMember(node.left) && isStringLiteral(node.right))
16
+ return node.left;
17
+ if (isTagMember(node.right) && isStringLiteral(node.left))
18
+ return node.right;
19
+ return;
20
+ };
21
+ var isBroadEffectCatchCall = (node) => node?.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Effect" && !node.callee.computed && node.callee.property.type === "Identifier" && broadEffectCatchMethods.has(node.callee.property.name);
22
+ var isInsideBroadEffectHandler = (node) => {
23
+ let current = node.parent;
24
+ while (current !== null && current !== undefined) {
25
+ if (current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") {
26
+ return isBroadEffectCatchCall(current.parent) && current.parent.arguments.includes(current);
27
+ }
28
+ current = current.parent;
29
+ }
30
+ return false;
31
+ };
32
+ var isReasonTagMember = (node) => node.object.type === "MemberExpression" && (!node.object.computed && node.object.property.type === "Identifier" && node.object.property.name === "reason" || node.object.computed && isStringLiteral(node.object.property) && node.object.property.value === "reason");
33
+ var propertyName = (property) => {
34
+ if (!property.computed && property.key.type === "Identifier") {
35
+ return property.key.name;
36
+ }
37
+ if (property.key.type === "Literal" && typeof property.key.value === "string") {
38
+ return property.key.value;
39
+ }
40
+ return;
41
+ };
42
+ var isMatchPatternObject = (node) => {
43
+ const call = node.parent;
44
+ if (call?.type !== "CallExpression" || !call.arguments.includes(node)) {
45
+ return false;
46
+ }
47
+ const callee = call.callee;
48
+ return callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "Match" && !callee.computed && callee.property.type === "Identifier" && (callee.property.name === "when" || callee.property.name === "not");
49
+ };
50
+
51
+ // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
52
+ var noManualEffectErrorTagRule = defineRule({
53
+ meta: {
54
+ type: "problem",
55
+ docs: {
56
+ description: "Use Effect tagged error handlers instead of manually branching on `_tag` in a catch handler."
57
+ },
58
+ messages: {
59
+ tag: "Use Effect.catchTag or Effect.catchTags instead of manually discriminating a tagged error in a broad Effect catch handler.",
60
+ reason: "Use Effect.catchReason or Effect.catchReasons instead of manually discriminating a tagged `reason` in a broad Effect catch handler."
61
+ }
62
+ },
63
+ createOnce(context) {
64
+ return {
65
+ BinaryExpression(node) {
66
+ const tagMember = tagMemberFromComparison(node);
67
+ if (tagMember === undefined || !isInsideBroadEffectHandler(node)) {
68
+ return;
69
+ }
70
+ context.report({
71
+ node,
72
+ messageId: isReasonTagMember(tagMember) ? "reason" : "tag"
73
+ });
74
+ },
75
+ SwitchStatement(node) {
76
+ if (!isTagMember(node.discriminant) || !isInsideBroadEffectHandler(node)) {
77
+ return;
78
+ }
79
+ context.report({
80
+ node,
81
+ messageId: isReasonTagMember(node.discriminant) ? "reason" : "tag"
82
+ });
83
+ }
84
+ };
85
+ }
86
+ });
87
+
88
+ // vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts
89
+ import { defineRule as defineRule2 } from "@oxlint/plugins";
90
+ var noManualTagComparisonRule = defineRule2({
91
+ meta: {
92
+ type: "problem",
93
+ docs: {
94
+ description: "Use Effect Match or Predicate helpers instead of manually branching on `_tag`."
95
+ },
96
+ messages: {
97
+ manualComparison: "Use Match.tag/Match.tags for tagged-value branching, or Predicate.isTagged for a simple reusable predicate.",
98
+ manualSwitch: "Use Match.value(value).pipe(Match.tag/Match.tags/Match.tagsExhaustive) or the tagged enum `$match` helper instead of switching on `_tag`."
99
+ }
100
+ },
101
+ createOnce(context) {
102
+ return {
103
+ BinaryExpression(node) {
104
+ if (tagMemberFromComparison(node) === undefined || isInsideBroadEffectHandler(node)) {
105
+ return;
106
+ }
107
+ context.report({ node, messageId: "manualComparison" });
108
+ },
109
+ SwitchStatement(node) {
110
+ if (!isTagMember(node.discriminant) || isInsideBroadEffectHandler(node)) {
111
+ return;
112
+ }
113
+ context.report({ node, messageId: "manualSwitch" });
114
+ }
115
+ };
116
+ }
117
+ });
118
+
119
+ // vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts
120
+ import { defineRule as defineRule3 } from "@oxlint/plugins";
121
+ var noManualTaggedConstructionRule = defineRule3({
122
+ meta: {
123
+ type: "problem",
124
+ docs: {
125
+ description: "Construct tagged values with their existing Effect constructor instead of writing `_tag` manually."
126
+ },
127
+ messages: {
128
+ manualConstruction: "Use the existing Schema tagged `.make`, tagged class/error constructor, or Data.taggedEnum variant constructor instead of writing a literal `_tag` object."
129
+ }
130
+ },
131
+ createOnce(context) {
132
+ return {
133
+ ObjectExpression(node) {
134
+ if (isMatchPatternObject(node))
135
+ return;
136
+ const tag = node.properties.find((property) => property.type === "Property" && propertyName(property) === "_tag" && isStringLiteral(property.value));
137
+ if (tag !== undefined) {
138
+ context.report({ node: tag, messageId: "manualConstruction" });
139
+ }
140
+ }
141
+ };
142
+ }
143
+ });
144
+
145
+ // vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
146
+ import { defineRule as defineRule4 } from "@oxlint/plugins";
6
147
  var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
7
148
  var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
8
149
  function isProjectLocalImport(source) {
@@ -13,7 +154,7 @@ function getImportedName(specifier) {
13
154
  return specifier.imported.name;
14
155
  return specifier.imported.value;
15
156
  }
16
- var noServiceConstructorImportsRule = defineRule({
157
+ var noServiceConstructorImportsRule = defineRule4({
17
158
  meta: {
18
159
  type: "problem",
19
160
  docs: {
@@ -46,11 +187,63 @@ var noServiceConstructorImportsRule = defineRule({
46
187
  }
47
188
  });
48
189
 
190
+ // vendor/anti-slop/src/effect/rules/prefer-effect-match.ts
191
+ import { defineRule as defineRule5 } from "@oxlint/plugins";
192
+ var equalityOperators2 = new Set(["==", "===", "!=", "!=="]);
193
+ var preferEffectMatchRule = defineRule5({
194
+ meta: {
195
+ type: "problem",
196
+ docs: {
197
+ description: "Use Match from Effect for chained literal ternaries over the same value."
198
+ },
199
+ messages: {
200
+ preferMatch: "Use Match from Effect instead of a chained literal ternary."
201
+ }
202
+ },
203
+ createOnce(context) {
204
+ const isLiteral = (node) => node.type === "Literal" || node.type === "TemplateLiteral" && node.expressions.length === 0;
205
+ const comparedValue = (node) => {
206
+ if (node.type !== "BinaryExpression" || !equalityOperators2.has(node.operator)) {
207
+ return;
208
+ }
209
+ if (isLiteral(node.left))
210
+ return context.sourceCode.getText(node.right);
211
+ if (isLiteral(node.right))
212
+ return context.sourceCode.getText(node.left);
213
+ return;
214
+ };
215
+ return {
216
+ ConditionalExpression(node) {
217
+ if (node.parent?.type === "ConditionalExpression")
218
+ return;
219
+ const value = comparedValue(node.test);
220
+ if (value === undefined)
221
+ return;
222
+ let alternate = node.alternate;
223
+ let literalChecks = 1;
224
+ while (alternate.type === "ConditionalExpression") {
225
+ if (comparedValue(alternate.test) !== value)
226
+ return;
227
+ literalChecks += 1;
228
+ alternate = alternate.alternate;
229
+ }
230
+ if (literalChecks > 1) {
231
+ context.report({ node, messageId: "preferMatch" });
232
+ }
233
+ }
234
+ };
235
+ }
236
+ });
237
+
49
238
  // vendor/anti-slop/src/effect/index.ts
50
239
  var antiSlopEffectPlugin = eslintCompatPlugin({
51
240
  meta: { name: "anti-slop-effect" },
52
241
  rules: {
53
- "no-service-constructor-imports": noServiceConstructorImportsRule
242
+ "no-manual-effect-error-tag": noManualEffectErrorTagRule,
243
+ "no-manual-tag-comparison": noManualTagComparisonRule,
244
+ "no-manual-tagged-construction": noManualTaggedConstructionRule,
245
+ "no-service-constructor-imports": noServiceConstructorImportsRule,
246
+ "prefer-effect-match": preferEffectMatchRule
54
247
  }
55
248
  });
56
249
  var effect_default = antiSlopEffectPlugin;
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.2",
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,12 +1,20 @@
1
1
  import { eslintCompatPlugin } from "@oxlint/plugins";
2
2
 
3
+ import { noManualEffectErrorTagRule } from "./rules/no-manual-effect-error-tag.ts";
4
+ import { noManualTagComparisonRule } from "./rules/no-manual-tag-comparison.ts";
5
+ import { noManualTaggedConstructionRule } from "./rules/no-manual-tagged-construction.ts";
3
6
  import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.ts";
7
+ import { preferEffectMatchRule } from "./rules/prefer-effect-match.ts";
4
8
 
5
9
  /** Opt-in Oxlint rules for Effect service and Layer architecture. */
6
10
  const antiSlopEffectPlugin = eslintCompatPlugin({
7
11
  meta: { name: "anti-slop-effect" },
8
12
  rules: {
13
+ "no-manual-effect-error-tag": noManualEffectErrorTagRule,
14
+ "no-manual-tag-comparison": noManualTagComparisonRule,
15
+ "no-manual-tagged-construction": noManualTaggedConstructionRule,
9
16
  "no-service-constructor-imports": noServiceConstructorImportsRule,
17
+ "prefer-effect-match": preferEffectMatchRule,
10
18
  },
11
19
  });
12
20
 
@@ -0,0 +1,52 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+
3
+ import {
4
+ isInsideBroadEffectHandler,
5
+ isReasonTagMember,
6
+ isTagMember,
7
+ tagMemberFromComparison,
8
+ } from "../shared/tagged-values.ts";
9
+
10
+ export const noManualEffectErrorTagRule = defineRule({
11
+ meta: {
12
+ type: "problem",
13
+ docs: {
14
+ description:
15
+ "Use Effect tagged error handlers instead of manually branching on `_tag` in a catch handler.",
16
+ },
17
+ messages: {
18
+ tag: "Use Effect.catchTag or Effect.catchTags instead of manually discriminating a tagged error in a broad Effect catch handler.",
19
+ reason:
20
+ "Use Effect.catchReason or Effect.catchReasons instead of manually discriminating a tagged `reason` in a broad Effect catch handler.",
21
+ },
22
+ },
23
+ createOnce(context) {
24
+ return {
25
+ BinaryExpression(node) {
26
+ const tagMember = tagMemberFromComparison(node);
27
+ if (
28
+ tagMember === undefined ||
29
+ !isInsideBroadEffectHandler(node)
30
+ ) {
31
+ return;
32
+ }
33
+ context.report({
34
+ node,
35
+ messageId: isReasonTagMember(tagMember) ? "reason" : "tag",
36
+ });
37
+ },
38
+ SwitchStatement(node) {
39
+ if (
40
+ !isTagMember(node.discriminant) ||
41
+ !isInsideBroadEffectHandler(node)
42
+ ) {
43
+ return;
44
+ }
45
+ context.report({
46
+ node,
47
+ messageId: isReasonTagMember(node.discriminant) ? "reason" : "tag",
48
+ });
49
+ },
50
+ };
51
+ },
52
+ });
@@ -0,0 +1,45 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+
3
+ import {
4
+ isInsideBroadEffectHandler,
5
+ isTagMember,
6
+ tagMemberFromComparison,
7
+ } from "../shared/tagged-values.ts";
8
+
9
+ export const noManualTagComparisonRule = defineRule({
10
+ meta: {
11
+ type: "problem",
12
+ docs: {
13
+ description:
14
+ "Use Effect Match or Predicate helpers instead of manually branching on `_tag`.",
15
+ },
16
+ messages: {
17
+ manualComparison:
18
+ "Use Match.tag/Match.tags for tagged-value branching, or Predicate.isTagged for a simple reusable predicate.",
19
+ manualSwitch:
20
+ "Use Match.value(value).pipe(Match.tag/Match.tags/Match.tagsExhaustive) or the tagged enum `$match` helper instead of switching on `_tag`.",
21
+ },
22
+ },
23
+ createOnce(context) {
24
+ return {
25
+ BinaryExpression(node) {
26
+ if (
27
+ tagMemberFromComparison(node) === undefined ||
28
+ isInsideBroadEffectHandler(node)
29
+ ) {
30
+ return;
31
+ }
32
+ context.report({ node, messageId: "manualComparison" });
33
+ },
34
+ SwitchStatement(node) {
35
+ if (
36
+ !isTagMember(node.discriminant) ||
37
+ isInsideBroadEffectHandler(node)
38
+ ) {
39
+ return;
40
+ }
41
+ context.report({ node, messageId: "manualSwitch" });
42
+ },
43
+ };
44
+ },
45
+ });
@@ -0,0 +1,37 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+
3
+ import {
4
+ isMatchPatternObject,
5
+ isStringLiteral,
6
+ propertyName,
7
+ } from "../shared/tagged-values.ts";
8
+
9
+ export const noManualTaggedConstructionRule = defineRule({
10
+ meta: {
11
+ type: "problem",
12
+ docs: {
13
+ description:
14
+ "Construct tagged values with their existing Effect constructor instead of writing `_tag` manually.",
15
+ },
16
+ messages: {
17
+ manualConstruction:
18
+ "Use the existing Schema tagged `.make`, tagged class/error constructor, or Data.taggedEnum variant constructor instead of writing a literal `_tag` object.",
19
+ },
20
+ },
21
+ createOnce(context) {
22
+ return {
23
+ ObjectExpression(node) {
24
+ if (isMatchPatternObject(node)) return;
25
+ const tag = node.properties.find(
26
+ (property) =>
27
+ property.type === "Property" &&
28
+ propertyName(property) === "_tag" &&
29
+ isStringLiteral(property.value),
30
+ );
31
+ if (tag !== undefined) {
32
+ context.report({ node: tag, messageId: "manualConstruction" });
33
+ }
34
+ },
35
+ };
36
+ },
37
+ });
@@ -0,0 +1,54 @@
1
+ import { defineRule, type ESTree } from "@oxlint/plugins";
2
+
3
+ const equalityOperators = new Set(["==", "===", "!=", "!=="]);
4
+
5
+ export const preferEffectMatchRule = defineRule({
6
+ meta: {
7
+ type: "problem",
8
+ docs: {
9
+ description:
10
+ "Use Match from Effect for chained literal ternaries over the same value.",
11
+ },
12
+ messages: {
13
+ preferMatch:
14
+ "Use Match from Effect instead of a chained literal ternary.",
15
+ },
16
+ },
17
+ createOnce(context) {
18
+ const isLiteral = (node: ESTree.Node): boolean =>
19
+ node.type === "Literal" ||
20
+ (node.type === "TemplateLiteral" && node.expressions.length === 0);
21
+
22
+ const comparedValue = (node: ESTree.Expression): string | undefined => {
23
+ if (
24
+ node.type !== "BinaryExpression" ||
25
+ !equalityOperators.has(node.operator)
26
+ ) {
27
+ return undefined;
28
+ }
29
+ if (isLiteral(node.left)) return context.sourceCode.getText(node.right);
30
+ if (isLiteral(node.right)) return context.sourceCode.getText(node.left);
31
+ return undefined;
32
+ };
33
+
34
+ return {
35
+ ConditionalExpression(node) {
36
+ if (node.parent?.type === "ConditionalExpression") return;
37
+ const value = comparedValue(node.test);
38
+ if (value === undefined) return;
39
+
40
+ let alternate = node.alternate;
41
+ let literalChecks = 1;
42
+ while (alternate.type === "ConditionalExpression") {
43
+ if (comparedValue(alternate.test) !== value) return;
44
+ literalChecks += 1;
45
+ alternate = alternate.alternate;
46
+ }
47
+
48
+ if (literalChecks > 1) {
49
+ context.report({ node, messageId: "preferMatch" });
50
+ }
51
+ },
52
+ };
53
+ },
54
+ });
@@ -0,0 +1,97 @@
1
+ import type { ESTree } from "@oxlint/plugins";
2
+
3
+ const equalityOperators = new Set(["==", "===", "!=", "!=="]);
4
+ const broadEffectCatchMethods = new Set(["catch", "catchAll", "catchIf"]);
5
+
6
+ export const isStringLiteral = (
7
+ node: ESTree.Node | null | undefined,
8
+ ): node is ESTree.StringLiteral =>
9
+ node?.type === "Literal" && typeof node.value === "string";
10
+
11
+ export const isTagMember = (
12
+ node: ESTree.Node | null | undefined,
13
+ ): node is ESTree.MemberExpression =>
14
+ node?.type === "MemberExpression" &&
15
+ ((!node.computed &&
16
+ node.property.type === "Identifier" &&
17
+ node.property.name === "_tag") ||
18
+ (node.computed &&
19
+ isStringLiteral(node.property) &&
20
+ node.property.value === "_tag"));
21
+
22
+ export const tagMemberFromComparison = (
23
+ node: ESTree.BinaryExpression,
24
+ ): ESTree.MemberExpression | undefined => {
25
+ if (!equalityOperators.has(node.operator)) return undefined;
26
+ if (isTagMember(node.left) && isStringLiteral(node.right)) return node.left;
27
+ if (isTagMember(node.right) && isStringLiteral(node.left)) return node.right;
28
+ return undefined;
29
+ };
30
+
31
+ const isBroadEffectCatchCall = (
32
+ node: ESTree.Node | null | undefined,
33
+ ): node is ESTree.CallExpression =>
34
+ node?.type === "CallExpression" &&
35
+ node.callee.type === "MemberExpression" &&
36
+ node.callee.object.type === "Identifier" &&
37
+ node.callee.object.name === "Effect" &&
38
+ !node.callee.computed &&
39
+ node.callee.property.type === "Identifier" &&
40
+ broadEffectCatchMethods.has(node.callee.property.name);
41
+
42
+ export const isInsideBroadEffectHandler = (node: ESTree.Node): boolean => {
43
+ let current: ESTree.Node | null | undefined = node.parent;
44
+ while (current !== null && current !== undefined) {
45
+ if (
46
+ current.type === "ArrowFunctionExpression" ||
47
+ current.type === "FunctionExpression"
48
+ ) {
49
+ return (
50
+ isBroadEffectCatchCall(current.parent) &&
51
+ current.parent.arguments.includes(current)
52
+ );
53
+ }
54
+ current = current.parent;
55
+ }
56
+ return false;
57
+ };
58
+
59
+ export const isReasonTagMember = (node: ESTree.MemberExpression): boolean =>
60
+ node.object.type === "MemberExpression" &&
61
+ ((!node.object.computed &&
62
+ node.object.property.type === "Identifier" &&
63
+ node.object.property.name === "reason") ||
64
+ (node.object.computed &&
65
+ isStringLiteral(node.object.property) &&
66
+ node.object.property.value === "reason"));
67
+
68
+ export const propertyName = (
69
+ property: ESTree.ObjectProperty,
70
+ ): string | undefined => {
71
+ if (!property.computed && property.key.type === "Identifier") {
72
+ return property.key.name;
73
+ }
74
+ if (
75
+ property.key.type === "Literal" &&
76
+ typeof property.key.value === "string"
77
+ ) {
78
+ return property.key.value;
79
+ }
80
+ return undefined;
81
+ };
82
+
83
+ export const isMatchPatternObject = (node: ESTree.ObjectExpression): boolean => {
84
+ const call = node.parent;
85
+ if (call?.type !== "CallExpression" || !call.arguments.includes(node)) {
86
+ return false;
87
+ }
88
+ const callee = call.callee;
89
+ return (
90
+ callee.type === "MemberExpression" &&
91
+ callee.object.type === "Identifier" &&
92
+ callee.object.name === "Match" &&
93
+ !callee.computed &&
94
+ callee.property.type === "Identifier" &&
95
+ (callee.property.name === "when" || callee.property.name === "not")
96
+ );
97
+ };
@@ -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";
@@ -14,12 +16,15 @@ import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts";
14
16
  import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts";
15
17
  import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts";
16
18
  import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts";
19
+ import { requireReadableSpacingRule } from "./rules/require-readable-spacing.ts";
17
20
  import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts";
18
21
 
19
22
  /** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
20
23
  const antiSlopPlugin = eslintCompatPlugin({
21
24
  meta: { name: "anti-slop" },
22
25
  rules: {
26
+ "no-array-filter-map": noArrayFilterMapRule,
27
+ "no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
23
28
  "no-chained-type-assertions": noChainedTypeAssertionsRule,
24
29
  "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
25
30
  "no-known-value-widening": noKnownValueWideningRule,
@@ -34,6 +39,7 @@ const antiSlopPlugin = eslintCompatPlugin({
34
39
  "no-unknown-returns": noUnknownReturnsRule,
35
40
  "no-unknown-type-aliases": noUnknownTypeAliasesRule,
36
41
  "no-widen-then-assert": noWidenThenAssertRule,
42
+ "require-readable-spacing": requireReadableSpacingRule,
37
43
  "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
38
44
  },
39
45
  });
@@ -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
+ });
@@ -13,8 +13,9 @@ import {
13
13
  functionParameterBindingName,
14
14
  functionParameterTypeAnnotation,
15
15
  } from "../shared/function-parameters.ts";
16
+ import { resolveVariable } from "../shared/scope.ts";
16
17
 
17
- import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
18
+ import type { ESTree, SourceCode, Variable } from "@oxlint/plugins";
18
19
 
19
20
  type FunctionExpression = ESTree.ArrowFunctionExpression | ESTree.Function;
20
21
 
@@ -32,19 +33,6 @@ function unwrapExpression(expression: ESTree.Expression): ESTree.Expression {
32
33
  return current;
33
34
  }
34
35
 
35
- function resolveVariable(
36
- sourceCode: SourceCode,
37
- identifier: ESTree.IdentifierReference,
38
- ): Variable | null {
39
- let scope: Scope | null = sourceCode.getScope(identifier);
40
- while (scope !== null) {
41
- const variable = scope.set.get(identifier.name);
42
- if (variable !== undefined) return variable;
43
- scope = scope.upper;
44
- }
45
- return null;
46
- }
47
-
48
36
  function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null {
49
37
  if (variable.defs.length !== 1) return null;
50
38
  const [definition] = variable.defs;
@@ -1,21 +1,10 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
2
 
3
- import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
3
+ import { resolveVariable } from "../shared/scope.ts";
4
4
 
5
- const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
5
+ import type { ESTree, SourceCode } from "@oxlint/plugins";
6
6
 
7
- function resolveVariable(
8
- sourceCode: SourceCode,
9
- identifier: ESTree.IdentifierReference,
10
- ): Variable | null {
11
- let scope: Scope | null = sourceCode.getScope(identifier);
12
- while (scope !== null) {
13
- const variable = scope.set.get(identifier.name);
14
- if (variable !== undefined) return variable;
15
- scope = scope.upper;
16
- }
17
- return null;
18
- }
7
+ const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
19
8
 
20
9
  function importedName(node: ESTree.Node): string | null {
21
10
  if (node.type !== "ImportSpecifier") return null;