@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.
- package/README.md +2 -1
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/cli.js +978 -83
- package/dist/configs/recommended-effect.js +1207 -312
- package/dist/configs/recommended.js +772 -70
- package/dist/upstream/anti-slop.js +765 -63
- package/dist/upstream/effect.js +196 -3
- package/package.json +5 -5
- package/vendor/anti-slop/src/effect/index.ts +8 -0
- package/vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts +52 -0
- package/vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts +45 -0
- package/vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts +37 -0
- package/vendor/anti-slop/src/effect/rules/prefer-effect-match.ts +54 -0
- package/vendor/anti-slop/src/effect/shared/tagged-values.ts +97 -0
- package/vendor/anti-slop/src/index.ts +6 -0
- package/vendor/anti-slop/src/rules/no-array-filter-map.ts +28 -0
- package/vendor/anti-slop/src/rules/no-known-value-widening.ts +2 -14
- package/vendor/anti-slop/src/rules/no-module-mocking.ts +3 -14
- package/vendor/anti-slop/src/rules/no-reduce-accumulator-copy.ts +109 -0
- package/vendor/anti-slop/src/rules/require-readable-spacing.ts +47 -0
- package/vendor/anti-slop/src/shared/array-method.ts +94 -0
- package/vendor/anti-slop/src/shared/reflect-method.ts +2 -13
- package/vendor/anti-slop/src/shared/scope.ts +15 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/LICENSE +22 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/UPSTREAM.md +28 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-ast.ts +51 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-between-statements.ts +906 -0
- package/vendor/anti-slop/src/vendor/eslint-stylistic/padding-line-options.d.ts +87 -0
package/dist/upstream/effect.js
CHANGED
|
@@ -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-
|
|
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 =
|
|
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-
|
|
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.
|
|
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.
|
|
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,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,
|
|
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
|
|
3
|
+
import { resolveVariable } from "../shared/scope.ts";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import type { ESTree, SourceCode } from "@oxlint/plugins";
|
|
6
6
|
|
|
7
|
-
|
|
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;
|