oxlint-plugin-effect 0.14.0 → 0.15.0
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/dist/presets/recommended.d.ts +1 -0
- package/dist/presets/recommended.js +1 -0
- package/dist/rules/_anti-slop-array-method.js +41 -33
- package/dist/rules/_anti-slop-dictionary-types.js +172 -129
- package/dist/rules/_anti-slop-function-parameters.js +9 -6
- package/dist/rules/_anti-slop-lexical-type-parameters.js +6 -14
- package/dist/rules/_anti-slop-reflect-method.js +7 -3
- package/dist/rules/_anti-slop-scope.js +8 -6
- package/dist/rules/_anti-slop-type-alias-resolution.js +58 -65
- package/dist/rules/_ast-ancestors.js +14 -0
- package/dist/rules/_ast-children.js +18 -0
- package/dist/rules/_call-arguments.js +11 -0
- package/dist/rules/_effect-context.js +25 -20
- package/dist/rules/_function-metrics.js +63 -44
- package/dist/rules/_tagged-control-flow.js +38 -60
- package/dist/rules/_test-apis.js +47 -0
- package/dist/rules/index.d.ts +2 -1
- package/dist/rules/index.js +2 -0
- package/dist/rules/no-array-filter-map.js +24 -14
- package/dist/rules/no-chained-type-assertions.js +7 -4
- package/dist/rules/no-conditional-empty-object-spread.js +7 -4
- package/dist/rules/no-dynamic-imports.js +33 -25
- package/dist/rules/no-eager-acquire.js +40 -34
- package/dist/rules/no-effect-run-in-tests.js +11 -8
- package/dist/rules/no-globals.js +42 -31
- package/dist/rules/no-known-value-widening.js +64 -39
- package/dist/rules/no-module-level-mutable-state.js +4 -8
- package/dist/rules/no-module-mocks.js +37 -48
- package/dist/rules/no-new-promise.js +6 -2
- package/dist/rules/no-node-builtin-import.js +56 -33
- package/dist/rules/no-nullish.js +5 -1
- package/dist/rules/no-object-parameters.js +9 -7
- package/dist/rules/no-per-call-cache-construction.js +2 -2
- package/dist/rules/no-reduce-accumulator-copy.js +65 -53
- package/dist/rules/no-reflect-apply.js +7 -4
- package/dist/rules/no-reflect-get.js +7 -4
- package/dist/rules/no-run-collect-on-unbounded-stream.js +23 -25
- package/dist/rules/no-runtime-typeof.js +9 -5
- package/dist/rules/no-sequential-effect-all.js +18 -11
- package/dist/rules/no-shape-in-symbol-names.js +10 -6
- package/dist/rules/no-silent-catch-all.js +20 -14
- package/dist/rules/no-test-globals.d.ts +6 -0
- package/dist/rules/no-test-globals.js +147 -0
- package/dist/rules/no-test-lifecycle-hooks.js +4 -2
- package/dist/rules/no-unbounded-concurrency.js +22 -17
- package/dist/rules/no-unbounded-retry.js +47 -62
- package/dist/rules/no-unknown-parameters.js +5 -3
- package/dist/rules/no-unknown-returns.js +11 -10
- package/dist/rules/no-unknown-type-aliases.js +13 -9
- package/dist/rules/no-unsafe-dictionary-type.js +22 -27
- package/dist/rules/no-widen-then-assert.js +95 -72
- package/dist/rules/prefer-catch-tag.js +29 -37
- package/dist/rules/prefer-match-tags-exhaustive.js +35 -47
- package/dist/rules/prefer-predicate-is-tagged.js +4 -6
- package/dist/rules/prefer-service-of.js +39 -26
- package/dist/rules/require-named-effect-fn.js +4 -2
- package/dist/rules/require-suppression-reason.js +8 -3
- package/dist/vendor/effect-oxlint/Scope.js +1 -1
- package/package.json +1 -1
package/dist/rules/no-globals.js
CHANGED
|
@@ -147,7 +147,6 @@ const isUnshadowedGlobal = (ctx, node, name) => Option.match(findVariableUp(ctx.
|
|
|
147
147
|
onNone: () => true,
|
|
148
148
|
onSome: (variable) => variable.defs.length === 0
|
|
149
149
|
});
|
|
150
|
-
const staticMember = (node) => Option.getOrUndefined(memberNames(node));
|
|
151
150
|
const processStreams = new Set([
|
|
152
151
|
"stderr",
|
|
153
152
|
"stdin",
|
|
@@ -176,37 +175,49 @@ const noGlobals = define({
|
|
|
176
175
|
node,
|
|
177
176
|
message: `Avoid ${used}. Use ${alternative}; platform adapters may disable this rule explicitly.`
|
|
178
177
|
}));
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return report(memberExpression, `${object}.${property}`, alternative);
|
|
178
|
+
const reportMember = (memberExpression) => {
|
|
179
|
+
if (isCryptoDigest(memberExpression) && isUnshadowedGlobal(ctx, memberExpression, "crypto")) return report(memberExpression, "crypto.subtle.digest", "Crypto.digest");
|
|
180
|
+
return Option.match(memberNames(memberExpression), {
|
|
181
|
+
onNone: () => Effect.void,
|
|
182
|
+
onSome: ([object, property]) => {
|
|
183
|
+
if (!isUnshadowedGlobal(ctx, memberExpression, object)) return Effect.void;
|
|
184
|
+
for (const [bannedObject, properties, alternative] of memberBans) if (object === bannedObject && properties.has(property)) {
|
|
185
|
+
if (object === "process" && processStreams.has(property) && isTtyRead(memberExpression)) return Effect.void;
|
|
186
|
+
return report(memberExpression, `${object}.${property}`, alternative);
|
|
187
|
+
}
|
|
188
|
+
return Effect.void;
|
|
191
189
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
const reportCall = (call) => Option.match(Option.filter(calleeName(call), (name) => isUnshadowedGlobal(ctx, call, name)), {
|
|
193
|
+
onNone: () => Effect.void,
|
|
194
|
+
onSome: (name) => Option.match(Option.fromUndefinedOr(callBans.get(name)), {
|
|
195
|
+
onNone: () => Effect.void,
|
|
196
|
+
onSome: (alternative) => report(call, `${name}()`, alternative)
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
const reportConstructor = (expression) => {
|
|
200
|
+
if (expression.callee.type !== "Identifier") return Effect.void;
|
|
201
|
+
const name = expression.callee.name;
|
|
202
|
+
if (!isUnshadowedGlobal(ctx, expression, name)) return Effect.void;
|
|
203
|
+
return Option.match(Option.fromUndefinedOr(constructorBans.get(name)), {
|
|
204
|
+
onNone: () => Effect.void,
|
|
205
|
+
onSome: (alternative) => report(expression, `new ${name}()`, alternative)
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
MemberExpression: (node) => Option.match(narrow(node, "MemberExpression"), {
|
|
210
|
+
onNone: () => Effect.void,
|
|
211
|
+
onSome: reportMember
|
|
212
|
+
}),
|
|
213
|
+
CallExpression: (node) => Option.match(narrow(node, "CallExpression"), {
|
|
214
|
+
onNone: () => Effect.void,
|
|
215
|
+
onSome: reportCall
|
|
216
|
+
}),
|
|
217
|
+
NewExpression: (node) => Option.match(narrow(node, "NewExpression"), {
|
|
218
|
+
onNone: () => Effect.void,
|
|
219
|
+
onSome: reportConstructor
|
|
220
|
+
})
|
|
210
221
|
};
|
|
211
222
|
}
|
|
212
223
|
});
|
|
@@ -2,8 +2,12 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
|
2
2
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { ancestors } from "./_ast-ancestors.js";
|
|
5
6
|
import { classifyWideningTarget, createTypeEnvironment, isKnownEvidenceExpression } from "./_anti-slop-dictionary-types.js";
|
|
6
7
|
import { resolveVariable } from "./_anti-slop-scope.js";
|
|
8
|
+
import * as Arr from "effect/Array";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
import * as Predicate from "effect/Predicate";
|
|
7
11
|
import * as Effect from "effect/Effect";
|
|
8
12
|
//#region src/rules/no-known-value-widening.ts
|
|
9
13
|
/**
|
|
@@ -16,34 +20,44 @@ function unwrapExpression(expression) {
|
|
|
16
20
|
return current;
|
|
17
21
|
}
|
|
18
22
|
function variableDeclarator(variable) {
|
|
19
|
-
if (variable.defs.length !== 1) return
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
if (variable.defs.length !== 1) return Option.none();
|
|
24
|
+
return Option.flatMap(Arr.head(variable.defs), (definition) => {
|
|
25
|
+
if (definition.type !== "Variable" || definition.node.type !== "VariableDeclarator") return Option.none();
|
|
26
|
+
return Option.some(definition.node);
|
|
27
|
+
});
|
|
22
28
|
}
|
|
23
29
|
function isStableConstVariable(variable, declarator) {
|
|
24
30
|
return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const" && variable.references.every((reference) => reference.init || !reference.isWrite());
|
|
25
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The initializer of a variable's single const declarator that is never reassigned.
|
|
34
|
+
* (Local change: split from hasKnownEvidence to read the Option-returning helpers.)
|
|
35
|
+
*/
|
|
36
|
+
function stableConstInitializer(variable) {
|
|
37
|
+
return Option.flatMap(Option.filter(variableDeclarator(variable), (declarator) => isStableConstVariable(variable, declarator)), (declarator) => Option.fromNullishOr(declarator.init));
|
|
38
|
+
}
|
|
26
39
|
function hasKnownEvidence(sourceCode, expression, visitedVariables = /* @__PURE__ */ new Set()) {
|
|
27
40
|
if (isKnownEvidenceExpression(expression)) return true;
|
|
28
41
|
const unwrapped = unwrapExpression(expression);
|
|
29
42
|
if (unwrapped.type !== "Identifier") return false;
|
|
30
43
|
const variable = resolveVariable(sourceCode, unwrapped);
|
|
31
|
-
if (variable
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
visitedVariables.add(variable);
|
|
35
|
-
return hasKnownEvidence(sourceCode,
|
|
44
|
+
if (Option.isNone(variable) || visitedVariables.has(variable.value)) return false;
|
|
45
|
+
const initializer = stableConstInitializer(variable.value);
|
|
46
|
+
if (Option.isNone(initializer)) return false;
|
|
47
|
+
visitedVariables.add(variable.value);
|
|
48
|
+
return hasKnownEvidence(sourceCode, initializer.value, visitedVariables);
|
|
36
49
|
}
|
|
37
50
|
function annotationTarget(annotation, environment) {
|
|
38
|
-
return
|
|
51
|
+
return Option.flatMap(Option.all({
|
|
52
|
+
annotation,
|
|
53
|
+
environment
|
|
54
|
+
}), (present) => classifyWideningTarget(present.annotation.typeAnnotation, present.environment));
|
|
55
|
+
}
|
|
56
|
+
function isFunctionExpression(node) {
|
|
57
|
+
return node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
|
|
39
58
|
}
|
|
40
59
|
function enclosingFunction(node) {
|
|
41
|
-
|
|
42
|
-
while (current !== null && current.type !== "Program") {
|
|
43
|
-
if (current.type === "ArrowFunctionExpression" || current.type === "FunctionDeclaration" || current.type === "FunctionExpression") return current;
|
|
44
|
-
current = current.parent;
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
60
|
+
return Arr.findFirst(ancestors(node), isFunctionExpression);
|
|
47
61
|
}
|
|
48
62
|
function sourceKeyName(sourceCode, key) {
|
|
49
63
|
if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
|
|
@@ -51,8 +65,13 @@ function sourceKeyName(sourceCode, key) {
|
|
|
51
65
|
return sourceCode.getText(key);
|
|
52
66
|
}
|
|
53
67
|
function functionName(sourceCode, owner) {
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
return Option.match(owner, {
|
|
69
|
+
onNone: () => "anonymous function",
|
|
70
|
+
onSome: (present) => ownerFunctionName(sourceCode, present)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function ownerFunctionName(sourceCode, owner) {
|
|
74
|
+
if (Predicate.isNotNull(owner.id)) return owner.id.name;
|
|
56
75
|
const parent = owner.parent;
|
|
57
76
|
if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") return parent.id.name;
|
|
58
77
|
if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
|
|
@@ -78,57 +97,63 @@ const noKnownValueWidening = define({
|
|
|
78
97
|
}),
|
|
79
98
|
create: function* () {
|
|
80
99
|
const context = yield* RuleContext;
|
|
81
|
-
let environment =
|
|
100
|
+
let environment = Option.none();
|
|
82
101
|
const reportFlow = (expression, destination, subject) => {
|
|
83
|
-
if (destination
|
|
84
|
-
|
|
102
|
+
if (Option.isNone(destination)) return Effect.void;
|
|
103
|
+
const target = destination.value;
|
|
104
|
+
if (isDictionaryAccumulatorTarget(target) && isEmptyObjectExpression(expression)) return Effect.void;
|
|
85
105
|
if (!hasKnownEvidence(context.sourceCode, expression)) return Effect.void;
|
|
86
106
|
return context.report(fromId({
|
|
87
107
|
node: expression,
|
|
88
108
|
messageId: "widening",
|
|
89
109
|
data: {
|
|
90
110
|
subject,
|
|
91
|
-
target:
|
|
111
|
+
target: target.kind
|
|
92
112
|
}
|
|
93
113
|
}));
|
|
94
114
|
};
|
|
95
|
-
const targetFromAnnotation = (annotation) =>
|
|
115
|
+
const targetFromAnnotation = (annotation) => annotationTarget(annotation, environment);
|
|
116
|
+
const reportAssertion = (node) => {
|
|
117
|
+
if (hasParentAssertion(node)) return Effect.void;
|
|
118
|
+
const target = Option.flatMap(environment, (present) => classifyWideningTarget(node.typeAnnotation, present));
|
|
119
|
+
return reportFlow(node.expression, target, "assertion");
|
|
120
|
+
};
|
|
96
121
|
return {
|
|
97
122
|
Program: (node) => {
|
|
98
|
-
environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
|
|
123
|
+
environment = Option.some(createTypeEnvironment(node, context.sourceCode.visitorKeys));
|
|
99
124
|
return Effect.void;
|
|
100
125
|
},
|
|
101
126
|
VariableDeclarator: (node) => {
|
|
102
|
-
if (node.init
|
|
103
|
-
return reportFlow(node.init, targetFromAnnotation(node.id.typeAnnotation), `binding \`${node.id.name}\``);
|
|
127
|
+
if (Predicate.isNull(node.init) || node.id.type !== "Identifier") return Effect.void;
|
|
128
|
+
return reportFlow(node.init, targetFromAnnotation(Option.fromNullishOr(node.id.typeAnnotation)), `binding \`${node.id.name}\``);
|
|
104
129
|
},
|
|
105
130
|
PropertyDefinition: (node) => {
|
|
106
|
-
if (node.value
|
|
107
|
-
return reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
131
|
+
if (Predicate.isNull(node.value)) return Effect.void;
|
|
132
|
+
return reportFlow(node.value, targetFromAnnotation(Option.fromNullishOr(node.typeAnnotation)), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
108
133
|
},
|
|
109
134
|
AccessorProperty: (node) => {
|
|
110
|
-
if (node.value
|
|
111
|
-
return reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
135
|
+
if (Predicate.isNull(node.value)) return Effect.void;
|
|
136
|
+
return reportFlow(node.value, targetFromAnnotation(Option.fromNullishOr(node.typeAnnotation)), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
112
137
|
},
|
|
113
138
|
AssignmentExpression: (node) => {
|
|
114
139
|
if (node.operator !== "=" || node.left.type !== "Identifier") return Effect.void;
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
return reportFlow(node.right, targetFromAnnotation(
|
|
140
|
+
const declarator = Option.flatMap(resolveVariable(context.sourceCode, node.left), variableDeclarator);
|
|
141
|
+
if (Option.isNone(declarator)) return Effect.void;
|
|
142
|
+
const { id } = declarator.value;
|
|
143
|
+
if (id.type !== "Identifier") return Effect.void;
|
|
144
|
+
return reportFlow(node.right, targetFromAnnotation(Option.fromNullishOr(id.typeAnnotation)), `binding \`${id.name}\``);
|
|
120
145
|
},
|
|
121
146
|
ReturnStatement: (node) => {
|
|
122
|
-
if (node.argument
|
|
147
|
+
if (Predicate.isNull(node.argument)) return Effect.void;
|
|
123
148
|
const owner = enclosingFunction(node);
|
|
124
|
-
return reportFlow(node.argument, targetFromAnnotation(owner
|
|
149
|
+
return reportFlow(node.argument, targetFromAnnotation(Option.flatMap(owner, (present) => Option.fromNullishOr(present.returnType))), `return value of \`${functionName(context.sourceCode, owner)}\``);
|
|
125
150
|
},
|
|
126
151
|
ArrowFunctionExpression: (node) => {
|
|
127
152
|
if (node.body.type === "BlockStatement") return Effect.void;
|
|
128
|
-
return reportFlow(node.body, targetFromAnnotation(node.returnType), `return value of \`${functionName(context.sourceCode, node)}\``);
|
|
153
|
+
return reportFlow(node.body, targetFromAnnotation(Option.fromNullishOr(node.returnType)), `return value of \`${functionName(context.sourceCode, Option.some(node))}\``);
|
|
129
154
|
},
|
|
130
|
-
TSAsExpression:
|
|
131
|
-
TSTypeAssertion:
|
|
155
|
+
TSAsExpression: reportAssertion,
|
|
156
|
+
TSTypeAssertion: reportAssertion
|
|
132
157
|
};
|
|
133
158
|
}
|
|
134
159
|
});
|
|
@@ -2,7 +2,10 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
|
2
2
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
3
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { ancestors } from "./_ast-ancestors.js";
|
|
5
6
|
import { isTestFile, skipFile } from "./_test-files.js";
|
|
7
|
+
import * as Arr from "effect/Array";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
6
9
|
import * as Effect from "effect/Effect";
|
|
7
10
|
//#region src/rules/no-module-level-mutable-state.ts
|
|
8
11
|
/** Nodes that start a new `var` scope, so a `var` below them is not module state. */
|
|
@@ -14,14 +17,7 @@ const functionScopeBoundaries = new Set([
|
|
|
14
17
|
"TSDeclareFunction",
|
|
15
18
|
"TSModuleBlock"
|
|
16
19
|
]);
|
|
17
|
-
const isModuleScopedVar = (node) =>
|
|
18
|
-
let current = node.parent;
|
|
19
|
-
while (current !== null && current.type !== "Program") {
|
|
20
|
-
if (functionScopeBoundaries.has(current.type)) return false;
|
|
21
|
-
current = current.parent;
|
|
22
|
-
}
|
|
23
|
-
return current !== null;
|
|
24
|
-
};
|
|
20
|
+
const isModuleScopedVar = (node) => Option.exists(Arr.findFirst(ancestors(node), (ancestor) => ancestor.type === "Program" || functionScopeBoundaries.has(ancestor.type)), (boundary) => boundary.type === "Program");
|
|
25
21
|
const noModuleLevelMutableState = define({
|
|
26
22
|
name: "no-module-level-mutable-state",
|
|
27
23
|
meta: meta({
|
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { memberNames, narrow } from "../vendor/effect-oxlint/AST.js";
|
|
2
2
|
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
3
3
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
4
4
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
5
|
-
import { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
|
|
6
5
|
import "../vendor/effect-oxlint/index.js";
|
|
6
|
+
import { collectTestBindings, resolveTestBinding } from "./_test-apis.js";
|
|
7
7
|
import * as Option from "effect/Option";
|
|
8
8
|
import * as Effect from "effect/Effect";
|
|
9
9
|
//#region src/rules/no-module-mocks.ts
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const mockingMethods = new Set([
|
|
11
|
+
"doMock",
|
|
12
|
+
"fn",
|
|
13
|
+
"mock",
|
|
14
|
+
"mocked",
|
|
15
|
+
"spyOn",
|
|
16
|
+
"unmock"
|
|
17
|
+
]);
|
|
18
|
+
/** bun:test `mock(...)` and `spyOn(...)` are mock doubles in their own right. */
|
|
19
|
+
const bannedBareCall = (binding) => {
|
|
20
|
+
if (binding === "mock" || binding === "spyOn") return Option.some(`${binding}()`);
|
|
21
|
+
return Option.none();
|
|
13
22
|
};
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
};
|
|
21
|
-
const testApiFromVariable = (variable, importedBindings) => {
|
|
22
|
-
if (variable.defs.some((definition) => definition.type === "ImportBinding")) return importedBindings.get(variable.name);
|
|
23
|
+
const bannedMemberCall = (binding, method) => {
|
|
24
|
+
if (binding === "mock") {
|
|
25
|
+
if (method === "module") return Option.some("mock.module()");
|
|
26
|
+
return Option.none();
|
|
27
|
+
}
|
|
28
|
+
if (binding === "spyOn" || !mockingMethods.has(method)) return Option.none();
|
|
29
|
+
return Option.some(`${binding}.${method}()`);
|
|
23
30
|
};
|
|
24
31
|
const noModuleMocks = define({
|
|
25
32
|
name: "no-module-mocks",
|
|
@@ -29,42 +36,24 @@ const noModuleMocks = define({
|
|
|
29
36
|
}),
|
|
30
37
|
create: function* () {
|
|
31
38
|
const context = yield* RuleContext;
|
|
32
|
-
const
|
|
39
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
40
|
+
const bannedCall = (call) => {
|
|
41
|
+
const scope = context.sourceCode.getScope(call);
|
|
42
|
+
if (call.callee.type === "Identifier") return Option.flatMap(resolveTestBinding(scope, call.callee.name, bindings), bannedBareCall);
|
|
43
|
+
if (call.callee.type !== "MemberExpression") return Option.none();
|
|
44
|
+
return Option.flatMap(memberNames(call.callee), ([object, method]) => Option.flatMap(resolveTestBinding(scope, object, bindings), (binding) => bannedMemberCall(binding, method)));
|
|
45
|
+
};
|
|
33
46
|
return {
|
|
34
|
-
ImportDeclaration: (node) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (specifier.type !== "ImportSpecifier") continue;
|
|
40
|
-
const imported = identifierName(specifier.imported);
|
|
41
|
-
if (imported === void 0) continue;
|
|
42
|
-
const api = importedTestApi(source, imported);
|
|
43
|
-
if (api !== void 0) importedBindings.set(specifier.local.name, api);
|
|
44
|
-
}
|
|
45
|
-
return Effect.void;
|
|
46
|
-
}
|
|
47
|
-
}),
|
|
48
|
-
CallExpression: (node) => Option.match(narrow(node, "CallExpression"), {
|
|
47
|
+
ImportDeclaration: (node) => {
|
|
48
|
+
collectTestBindings(node, bindings);
|
|
49
|
+
return Effect.void;
|
|
50
|
+
},
|
|
51
|
+
CallExpression: (node) => Option.match(Option.flatMap(narrow(node, "CallExpression"), bannedCall), {
|
|
49
52
|
onNone: () => Effect.void,
|
|
50
|
-
onSome: (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const [object, method] = names;
|
|
55
|
-
if (!bannedMethods.has(method)) return Effect.void;
|
|
56
|
-
const variable = Option.getOrUndefined(findVariableUp(context.sourceCode.getScope(call), object));
|
|
57
|
-
let api = globalTestApi(object);
|
|
58
|
-
if (variable !== void 0) {
|
|
59
|
-
api = testApiFromVariable(variable, importedBindings);
|
|
60
|
-
if (api === void 0 && variable.defs.length === 0) api = globalTestApi(object);
|
|
61
|
-
}
|
|
62
|
-
if (api === void 0) return Effect.void;
|
|
63
|
-
return context.report(make({
|
|
64
|
-
node: call,
|
|
65
|
-
message: `Avoid ${api}.${method}(). Replace the external boundary with an Effect service test Layer.`
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
53
|
+
onSome: (used) => context.report(make({
|
|
54
|
+
node,
|
|
55
|
+
message: `Avoid ${used}. Replace the external boundary with an Effect service test Layer.`
|
|
56
|
+
}))
|
|
68
57
|
})
|
|
69
58
|
};
|
|
70
59
|
}
|
|
@@ -22,14 +22,18 @@ const noNewPromise = define({
|
|
|
22
22
|
return {
|
|
23
23
|
NewExpression: (node) => Option.match(narrow(node, "NewExpression"), {
|
|
24
24
|
onNone: () => Effect.void,
|
|
25
|
-
onSome: (expression) =>
|
|
25
|
+
onSome: (expression) => {
|
|
26
|
+
if (!isPromiseIdentifier(expression.callee)) return Effect.void;
|
|
27
|
+
return report(expression);
|
|
28
|
+
}
|
|
26
29
|
}),
|
|
27
30
|
CallExpression: (node) => Option.match(narrow(node, "CallExpression"), {
|
|
28
31
|
onNone: () => Effect.void,
|
|
29
32
|
onSome: (call) => {
|
|
30
33
|
if (isPromiseIdentifier(call.callee)) return report(call);
|
|
31
34
|
if (call.callee.type !== "MemberExpression") return Effect.void;
|
|
32
|
-
|
|
35
|
+
if (!Option.exists(memberNames(call.callee), ([object]) => object === "Promise")) return Effect.void;
|
|
36
|
+
return report(call);
|
|
33
37
|
}
|
|
34
38
|
})
|
|
35
39
|
};
|
|
@@ -3,7 +3,9 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
|
3
3
|
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
4
4
|
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
5
5
|
import "../vendor/effect-oxlint/index.js";
|
|
6
|
+
import * as Arr from "effect/Array";
|
|
6
7
|
import * as Option from "effect/Option";
|
|
8
|
+
import * as Predicate from "effect/Predicate";
|
|
7
9
|
import * as Effect from "effect/Effect";
|
|
8
10
|
//#region src/rules/no-node-builtin-import.ts
|
|
9
11
|
const replacedModules = new Map([
|
|
@@ -39,30 +41,47 @@ const processOperations = new Set([
|
|
|
39
41
|
"stdout"
|
|
40
42
|
]);
|
|
41
43
|
const moduleBase = (source) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
let withoutPrefix = source;
|
|
45
|
+
if (source.startsWith("node:")) withoutPrefix = source.slice(5);
|
|
46
|
+
return Option.getOrElse(Arr.head(withoutPrefix.split("/")), () => withoutPrefix);
|
|
47
|
+
};
|
|
48
|
+
const importedName = (specifier) => {
|
|
49
|
+
if (specifier.imported.type === "Identifier") return specifier.imported.name;
|
|
50
|
+
return specifier.imported.value;
|
|
44
51
|
};
|
|
45
|
-
const importedName = (specifier) => specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
46
52
|
const memberPath = (node) => {
|
|
47
|
-
if (node.computed || node.property.type !== "Identifier") return
|
|
48
|
-
|
|
49
|
-
if (node.object.type
|
|
50
|
-
|
|
51
|
-
return parentPath
|
|
53
|
+
if (node.computed || node.property.type !== "Identifier") return Option.none();
|
|
54
|
+
const property = node.property.name;
|
|
55
|
+
if (node.object.type === "Identifier") return Option.some([node.object.name, property]);
|
|
56
|
+
if (node.object.type !== "MemberExpression") return Option.none();
|
|
57
|
+
return Option.map(memberPath(node.object), (parentPath) => [...parentPath, property]);
|
|
58
|
+
};
|
|
59
|
+
/** Whether the segment before the operation in `path` is `name`. */
|
|
60
|
+
const followsSegment = (path, name) => Option.contains(Arr.get(path, path.length - 2), name);
|
|
61
|
+
const isDigestUnder = (operation, path, name) => operation === "digest" && followsSegment(path, name);
|
|
62
|
+
const cryptoReplacement = (operation, path) => {
|
|
63
|
+
if (cryptoOperations.has(operation) && path.length === 2) return Option.some("Crypto");
|
|
64
|
+
if (followsSegment(path, "webcrypto") && cryptoOperations.has(operation)) return Option.some("Crypto");
|
|
65
|
+
if (isDigestUnder(operation, path, "subtle")) return Option.some("Crypto.digest");
|
|
66
|
+
return Option.none();
|
|
52
67
|
};
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
const replacementFor = (module, operation, path) => {
|
|
69
|
+
if (module === "process") {
|
|
70
|
+
if (processOperations.has(operation) && path.length === 2) return Option.some("Config, Stdio, Clock, or Effect scheduling");
|
|
71
|
+
return Option.none();
|
|
72
|
+
}
|
|
73
|
+
if (module === "subtle") {
|
|
74
|
+
if (operation === "digest" && path.length === 2) return Option.some("Crypto.digest");
|
|
75
|
+
return Option.none();
|
|
76
|
+
}
|
|
58
77
|
if (module === "webcrypto") {
|
|
59
|
-
if (cryptoOperations.has(operation) && path.length === 2) return "Crypto";
|
|
60
|
-
|
|
78
|
+
if (cryptoOperations.has(operation) && path.length === 2) return Option.some("Crypto");
|
|
79
|
+
if (isDigestUnder(operation, path, "subtle")) return Option.some("Crypto.digest");
|
|
80
|
+
return Option.none();
|
|
61
81
|
}
|
|
62
|
-
|
|
63
|
-
if (path.at(-2) === "webcrypto" && cryptoOperations.has(operation)) return "Crypto";
|
|
64
|
-
return operation === "digest" && path.at(-2) === "subtle" ? "Crypto.digest" : void 0;
|
|
82
|
+
return cryptoReplacement(operation, path);
|
|
65
83
|
};
|
|
84
|
+
const partialReplacement = (module, path) => Option.flatMap(Arr.last(path), (operation) => replacementFor(module, operation, path));
|
|
66
85
|
const noNodeBuiltinImport = define({
|
|
67
86
|
name: "no-node-builtin-import",
|
|
68
87
|
meta: meta({
|
|
@@ -76,14 +95,17 @@ const noNodeBuiltinImport = define({
|
|
|
76
95
|
node,
|
|
77
96
|
message: `Avoid ${used}. Use ${alternative}; platform adapters may disable this rule explicitly.`
|
|
78
97
|
}));
|
|
98
|
+
/** The partially replaced module a member path starts from, if its root is an alias. */
|
|
99
|
+
const aliasedModule = (path) => Option.flatMap(Arr.head(path), (alias) => Option.fromUndefinedOr(partialModuleByAlias.get(alias)));
|
|
79
100
|
return {
|
|
80
101
|
ImportDeclaration: (node) => {
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
102
|
+
const narrowed = narrow(node, "ImportDeclaration");
|
|
103
|
+
if (Option.isNone(narrowed)) return Effect.void;
|
|
104
|
+
const declaration = narrowed.value;
|
|
83
105
|
const source = importSource(declaration);
|
|
84
106
|
const module = moduleBase(source);
|
|
85
107
|
const alternative = replacedModules.get(module);
|
|
86
|
-
if (alternative
|
|
108
|
+
if (Predicate.isNotUndefined(alternative)) return report(declaration, `importing '${source}'`, alternative);
|
|
87
109
|
if (module !== "crypto" && module !== "process") return Effect.void;
|
|
88
110
|
const diagnostics = [];
|
|
89
111
|
for (const specifier of declaration.specifiers) {
|
|
@@ -99,18 +121,19 @@ const noNodeBuiltinImport = define({
|
|
|
99
121
|
}
|
|
100
122
|
return Effect.all(diagnostics, { discard: true });
|
|
101
123
|
},
|
|
102
|
-
MemberExpression: (node) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
MemberExpression: (node) => Option.match(narrow(node, "MemberExpression"), {
|
|
125
|
+
onNone: () => Effect.void,
|
|
126
|
+
onSome: (member) => {
|
|
127
|
+
const replacement = Option.flatMap(memberPath(member), (path) => Option.map(Option.flatMap(aliasedModule(path), (module) => partialReplacement(module, path)), (alternative) => ({
|
|
128
|
+
path,
|
|
129
|
+
alternative
|
|
130
|
+
})));
|
|
131
|
+
return Option.match(replacement, {
|
|
132
|
+
onNone: () => Effect.void,
|
|
133
|
+
onSome: ({ path, alternative }) => report(member, `${path.join(".")}`, alternative)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
})
|
|
114
137
|
};
|
|
115
138
|
}
|
|
116
139
|
});
|
package/dist/rules/no-nullish.js
CHANGED
|
@@ -5,6 +5,7 @@ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
|
5
5
|
import { isGlobalReference } from "../vendor/effect-oxlint/SourceCode.js";
|
|
6
6
|
import "../vendor/effect-oxlint/index.js";
|
|
7
7
|
import * as Option from "effect/Option";
|
|
8
|
+
import * as Predicate from "effect/Predicate";
|
|
8
9
|
import * as Effect from "effect/Effect";
|
|
9
10
|
//#region src/rules/no-nullish.ts
|
|
10
11
|
const message = "Avoid null and undefined. Use Option for presence or absence. Use a domain enum when the state has more than two cases.";
|
|
@@ -37,7 +38,10 @@ const noNullish = define({
|
|
|
37
38
|
}),
|
|
38
39
|
Literal: (node) => Option.match(narrow(node, "Literal"), {
|
|
39
40
|
onNone: () => Effect.void,
|
|
40
|
-
onSome: (literal) =>
|
|
41
|
+
onSome: (literal) => {
|
|
42
|
+
if (!Predicate.isNull(literal.value) || "regex" in literal || isNullPrototypeArgument(literal)) return Effect.void;
|
|
43
|
+
return report(literal);
|
|
44
|
+
}
|
|
41
45
|
}),
|
|
42
46
|
TSNullKeyword: (node) => Option.match(narrow(node, "TSNullKeyword"), {
|
|
43
47
|
onNone: () => Effect.void,
|
|
@@ -4,6 +4,7 @@ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
5
|
import { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
|
|
6
6
|
import { functionParameterBindingName, functionParameterTypeAnnotation } from "./_anti-slop-function-parameters.js";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
7
8
|
import * as Effect from "effect/Effect";
|
|
8
9
|
//#region src/rules/no-object-parameters.ts
|
|
9
10
|
/** Ban the broad object type on function inputs, including local aliases to object. */
|
|
@@ -16,25 +17,26 @@ const noObjectParameters = define({
|
|
|
16
17
|
}),
|
|
17
18
|
create: function* () {
|
|
18
19
|
const context = yield* RuleContext;
|
|
19
|
-
let environment =
|
|
20
|
-
const resolvesToObject = (type) => environment
|
|
20
|
+
let environment = Option.none();
|
|
21
|
+
const resolvesToObject = (type) => Option.exists(environment, (present) => resolvedTypeMatches(type, present, (resolved, matches) => {
|
|
21
22
|
if (resolved.type === "TSObjectKeyword") return true;
|
|
22
23
|
if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
|
|
23
24
|
return resolved.type === "TSUnionType" && resolved.types.some(matches);
|
|
24
|
-
});
|
|
25
|
+
}));
|
|
25
26
|
const checkParameters = (node) => Effect.forEach(node.params, (parameter) => {
|
|
26
27
|
const annotation = functionParameterTypeAnnotation(parameter);
|
|
27
|
-
if (annotation
|
|
28
|
-
|
|
28
|
+
if (Option.isNone(annotation)) return Effect.void;
|
|
29
|
+
const type = annotation.value.typeAnnotation;
|
|
30
|
+
if (!resolvesToObject(type)) return Effect.void;
|
|
29
31
|
return context.report(fromId({
|
|
30
|
-
node:
|
|
32
|
+
node: type,
|
|
31
33
|
messageId: "objectParameter",
|
|
32
34
|
data: { parameter: functionParameterBindingName(parameter, context.sourceCode) }
|
|
33
35
|
}));
|
|
34
36
|
}, { discard: true });
|
|
35
37
|
return {
|
|
36
38
|
Program: (node) => {
|
|
37
|
-
environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
|
|
39
|
+
environment = Option.some(createTypeAliasEnvironment(node, context.sourceCode.visitorKeys));
|
|
38
40
|
return Effect.void;
|
|
39
41
|
},
|
|
40
42
|
ArrowFunctionExpression: checkParameters,
|
|
@@ -4,6 +4,7 @@ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
|
4
4
|
import "../vendor/effect-oxlint/index.js";
|
|
5
5
|
import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
|
|
6
6
|
import { enclosingEffectProgram } from "./_effect-context.js";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
7
8
|
import * as Effect from "effect/Effect";
|
|
8
9
|
//#region src/rules/no-per-call-cache-construction.ts
|
|
9
10
|
const cachedEffectOperations = new Set([
|
|
@@ -36,8 +37,7 @@ const noPerCallCacheConstruction = define({
|
|
|
36
37
|
CallExpression: (node) => {
|
|
37
38
|
if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
|
|
38
39
|
const effects = visibleNamespaces(ctx, node, effectNamespaces);
|
|
39
|
-
|
|
40
|
-
if (program === void 0 || program[1] === "gen") return Effect.void;
|
|
40
|
+
if (!Option.exists(enclosingEffectProgram(node, effects), ([, kind]) => kind !== "gen")) return Effect.void;
|
|
41
41
|
const caches = visibleNamespaces(ctx, node, cacheNamespaces);
|
|
42
42
|
if (!(isOperation(node.callee, effects, cachedEffectOperations) || isOperation(node.callee, caches, cacheConstructors))) return Effect.void;
|
|
43
43
|
return ctx.report(make({
|