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.
Files changed (60) hide show
  1. package/README.md +2 -1
  2. package/dist/presets/recommended.d.ts +1 -0
  3. package/dist/presets/recommended.js +1 -0
  4. package/dist/rules/_anti-slop-array-method.js +41 -33
  5. package/dist/rules/_anti-slop-dictionary-types.js +172 -129
  6. package/dist/rules/_anti-slop-function-parameters.js +9 -6
  7. package/dist/rules/_anti-slop-lexical-type-parameters.js +6 -14
  8. package/dist/rules/_anti-slop-reflect-method.js +7 -3
  9. package/dist/rules/_anti-slop-scope.js +8 -6
  10. package/dist/rules/_anti-slop-type-alias-resolution.js +58 -65
  11. package/dist/rules/_ast-ancestors.js +14 -0
  12. package/dist/rules/_ast-children.js +18 -0
  13. package/dist/rules/_call-arguments.js +11 -0
  14. package/dist/rules/_effect-context.js +25 -20
  15. package/dist/rules/_function-metrics.js +63 -44
  16. package/dist/rules/_tagged-control-flow.js +38 -60
  17. package/dist/rules/_test-apis.js +47 -0
  18. package/dist/rules/index.d.ts +2 -1
  19. package/dist/rules/index.js +2 -0
  20. package/dist/rules/no-array-filter-map.js +24 -14
  21. package/dist/rules/no-chained-type-assertions.js +7 -4
  22. package/dist/rules/no-conditional-empty-object-spread.js +7 -4
  23. package/dist/rules/no-dynamic-imports.js +33 -25
  24. package/dist/rules/no-eager-acquire.js +40 -34
  25. package/dist/rules/no-effect-run-in-tests.js +11 -8
  26. package/dist/rules/no-globals.js +42 -31
  27. package/dist/rules/no-known-value-widening.js +64 -39
  28. package/dist/rules/no-module-level-mutable-state.js +4 -8
  29. package/dist/rules/no-module-mocks.js +37 -48
  30. package/dist/rules/no-new-promise.js +6 -2
  31. package/dist/rules/no-node-builtin-import.js +56 -33
  32. package/dist/rules/no-nullish.js +5 -1
  33. package/dist/rules/no-object-parameters.js +9 -7
  34. package/dist/rules/no-per-call-cache-construction.js +2 -2
  35. package/dist/rules/no-reduce-accumulator-copy.js +65 -53
  36. package/dist/rules/no-reflect-apply.js +7 -4
  37. package/dist/rules/no-reflect-get.js +7 -4
  38. package/dist/rules/no-run-collect-on-unbounded-stream.js +23 -25
  39. package/dist/rules/no-runtime-typeof.js +9 -5
  40. package/dist/rules/no-sequential-effect-all.js +18 -11
  41. package/dist/rules/no-shape-in-symbol-names.js +10 -6
  42. package/dist/rules/no-silent-catch-all.js +20 -14
  43. package/dist/rules/no-test-globals.d.ts +6 -0
  44. package/dist/rules/no-test-globals.js +147 -0
  45. package/dist/rules/no-test-lifecycle-hooks.js +4 -2
  46. package/dist/rules/no-unbounded-concurrency.js +22 -17
  47. package/dist/rules/no-unbounded-retry.js +47 -62
  48. package/dist/rules/no-unknown-parameters.js +5 -3
  49. package/dist/rules/no-unknown-returns.js +11 -10
  50. package/dist/rules/no-unknown-type-aliases.js +13 -9
  51. package/dist/rules/no-unsafe-dictionary-type.js +22 -27
  52. package/dist/rules/no-widen-then-assert.js +95 -72
  53. package/dist/rules/prefer-catch-tag.js +29 -37
  54. package/dist/rules/prefer-match-tags-exhaustive.js +35 -47
  55. package/dist/rules/prefer-predicate-is-tagged.js +4 -6
  56. package/dist/rules/prefer-service-of.js +39 -26
  57. package/dist/rules/require-named-effect-fn.js +4 -2
  58. package/dist/rules/require-suppression-reason.js +8 -3
  59. package/dist/vendor/effect-oxlint/Scope.js +1 -1
  60. package/package.json +1 -1
@@ -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
- return {
180
- MemberExpression: (node) => {
181
- const memberExpression = Option.getOrUndefined(narrow(node, "MemberExpression"));
182
- if (memberExpression === void 0) return Effect.void;
183
- if (isCryptoDigest(memberExpression) && isUnshadowedGlobal(ctx, memberExpression, "crypto")) return report(memberExpression, "crypto.subtle.digest", "Crypto.digest");
184
- const names = staticMember(memberExpression);
185
- if (names === void 0) return Effect.void;
186
- const [object, property] = names;
187
- if (!isUnshadowedGlobal(ctx, memberExpression, object)) return Effect.void;
188
- for (const [bannedObject, properties, alternative] of memberBans) if (object === bannedObject && properties.has(property)) {
189
- if (object === "process" && processStreams.has(property) && isTtyRead(memberExpression)) return Effect.void;
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
- return Effect.void;
193
- },
194
- CallExpression: (node) => {
195
- const call = Option.getOrUndefined(narrow(node, "CallExpression"));
196
- if (call === void 0) return Effect.void;
197
- const name = Option.getOrUndefined(calleeName(call));
198
- if (name === void 0 || !isUnshadowedGlobal(ctx, call, name)) return Effect.void;
199
- const alternative = callBans.get(name);
200
- return alternative === void 0 ? Effect.void : report(call, `${name}()`, alternative);
201
- },
202
- NewExpression: (node) => {
203
- const expression = Option.getOrUndefined(narrow(node, "NewExpression"));
204
- if (expression === void 0 || expression.callee.type !== "Identifier") return Effect.void;
205
- const name = expression.callee.name;
206
- if (!isUnshadowedGlobal(ctx, expression, name)) return Effect.void;
207
- const alternative = constructorBans.get(name);
208
- return alternative === void 0 ? Effect.void : report(expression, `new ${name}()`, alternative);
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 null;
20
- const [definition] = variable.defs;
21
- return definition?.type === "Variable" && definition.node.type === "VariableDeclarator" ? definition.node : null;
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 === null || visitedVariables.has(variable)) return false;
32
- const declarator = variableDeclarator(variable);
33
- if (declarator === null || declarator.init === null || !isStableConstVariable(variable, declarator)) return false;
34
- visitedVariables.add(variable);
35
- return hasKnownEvidence(sourceCode, declarator.init, visitedVariables);
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 annotation === null || annotation === void 0 ? null : classifyWideningTarget(annotation.typeAnnotation, environment);
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
- let current = node.parent;
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
- if (owner === null) return "anonymous function";
55
- if (owner.id !== null) return owner.id.name;
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 = null;
100
+ let environment = Option.none();
82
101
  const reportFlow = (expression, destination, subject) => {
83
- if (destination === null) return Effect.void;
84
- if (isDictionaryAccumulatorTarget(destination) && isEmptyObjectExpression(expression)) return Effect.void;
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: destination.kind
111
+ target: target.kind
92
112
  }
93
113
  }));
94
114
  };
95
- const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
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 === null || node.id.type !== "Identifier") return Effect.void;
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 === null) return Effect.void;
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 === null) return Effect.void;
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 variable = resolveVariable(context.sourceCode, node.left);
116
- if (variable === null) return Effect.void;
117
- const declarator = variableDeclarator(variable);
118
- if (declarator === null || declarator.id.type !== "Identifier") return Effect.void;
119
- return reportFlow(node.right, targetFromAnnotation(declarator.id.typeAnnotation), `binding \`${declarator.id.name}\``);
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 === null) return Effect.void;
147
+ if (Predicate.isNull(node.argument)) return Effect.void;
123
148
  const owner = enclosingFunction(node);
124
- return reportFlow(node.argument, targetFromAnnotation(owner?.returnType), `return value of \`${functionName(context.sourceCode, 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: (node) => environment === null || hasParentAssertion(node) ? Effect.void : reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion"),
131
- TSTypeAssertion: (node) => environment === null || hasParentAssertion(node) ? Effect.void : reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion")
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 { importSource, memberNames, narrow } from "../vendor/effect-oxlint/AST.js";
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 bannedMethods = new Set(["mock", "spyOn"]);
11
- const identifierName = (node) => {
12
- if (node?.type === "Identifier") return node.name;
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 globalTestApi = (name) => {
15
- if (name === "jest" || name === "vi") return name;
16
- };
17
- const importedTestApi = (source, imported) => {
18
- if (source === "vitest" && imported === "vi") return "vi";
19
- if (source === "@jest/globals" && imported === "jest") return "jest";
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 importedBindings = /* @__PURE__ */ new Map();
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) => Option.match(narrow(node, "ImportDeclaration"), {
35
- onNone: () => Effect.void,
36
- onSome: (declaration) => {
37
- const source = importSource(declaration);
38
- for (const specifier of declaration.specifiers) {
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: (call) => {
51
- if (call.callee.type !== "MemberExpression") return Effect.void;
52
- const names = Option.getOrUndefined(memberNames(call.callee));
53
- if (names === void 0) return Effect.void;
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) => isPromiseIdentifier(expression.callee) ? report(expression) : Effect.void
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
- return Option.getOrUndefined(memberNames(call.callee))?.[0] === "Promise" ? report(call) : Effect.void;
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
- const withoutPrefix = source.startsWith("node:") ? source.slice(5) : source;
43
- return withoutPrefix.split("/")[0] ?? withoutPrefix;
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 void 0;
48
- if (node.object.type === "Identifier") return [node.object.name, node.property.name];
49
- if (node.object.type !== "MemberExpression") return void 0;
50
- const parentPath = memberPath(node.object);
51
- return parentPath === void 0 ? void 0 : [...parentPath, node.property.name];
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 partialReplacement = (module, path) => {
54
- const operation = path.at(-1);
55
- if (operation === void 0) return void 0;
56
- if (module === "process") return processOperations.has(operation) && path.length === 2 ? "Config, Stdio, Clock, or Effect scheduling" : void 0;
57
- if (module === "subtle") return operation === "digest" && path.length === 2 ? "Crypto.digest" : void 0;
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
- return operation === "digest" && path.at(-2) === "subtle" ? "Crypto.digest" : void 0;
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
- if (cryptoOperations.has(operation) && path.length === 2) return "Crypto";
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 declaration = Option.getOrUndefined(narrow(node, "ImportDeclaration"));
82
- if (declaration === void 0) return Effect.void;
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 !== void 0) return report(declaration, `importing '${source}'`, 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
- const member = Option.getOrUndefined(narrow(node, "MemberExpression"));
104
- if (member === void 0) return Effect.void;
105
- const path = memberPath(member);
106
- if (path === void 0) return Effect.void;
107
- const alias = path[0];
108
- if (alias === void 0) return Effect.void;
109
- const module = partialModuleByAlias.get(alias);
110
- if (module === void 0) return Effect.void;
111
- const alternative = partialReplacement(module, path);
112
- return alternative === void 0 ? Effect.void : report(member, `${path.join(".")}`, alternative);
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
  });
@@ -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) => literal.value === null && !("regex" in literal) && !isNullPrototypeArgument(literal) ? report(literal) : Effect.void
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 = null;
20
- const resolvesToObject = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
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 === null || annotation === void 0) return Effect.void;
28
- if (!resolvesToObject(annotation.typeAnnotation)) return Effect.void;
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: annotation.typeAnnotation,
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
- const program = enclosingEffectProgram(node, effects);
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({