oxlint-plugin-effect 0.14.0 → 0.16.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 (61) hide show
  1. package/README.md +16 -3
  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 +3 -1
  61. package/presets/recommended.json +58 -0
@@ -2,6 +2,9 @@ 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";
6
+ import * as Arr from "effect/Array";
7
+ import * as Option from "effect/Option";
5
8
  import * as Effect from "effect/Effect";
6
9
  //#region src/rules/no-widen-then-assert.ts
7
10
  /**
@@ -15,6 +18,7 @@ const functionBoundaryTypes = new Set([
15
18
  "TSDeclareFunction",
16
19
  "TSEmptyBodyFunctionExpression"
17
20
  ]);
21
+ const sameBoundary = Option.makeEquivalence((left, right) => left === right);
18
22
  function unwrapExpressionParentheses(expression) {
19
23
  let current = expression;
20
24
  while (current.type === "ParenthesizedExpression") current = current.expression;
@@ -26,7 +30,20 @@ function unwrapTypeParentheses(type) {
26
30
  return current;
27
31
  }
28
32
  function typeReferenceName(type) {
29
- return type.typeName.type === "Identifier" ? type.typeName.name : null;
33
+ if (type.typeName.type !== "Identifier") return Option.none();
34
+ return Option.some(type.typeName.name);
35
+ }
36
+ function isReferenceNamed(type, name) {
37
+ return Option.contains(typeReferenceName(type), name);
38
+ }
39
+ /** The two type arguments of a reference such as `Record<K, V>`, when it has exactly two. */
40
+ function typeArgumentPair(reference) {
41
+ const parameters = reference.typeArguments?.params ?? [];
42
+ if (parameters.length !== 2) return Option.none();
43
+ return Option.all([Arr.get(parameters, 0), Arr.get(parameters, 1)]);
44
+ }
45
+ function firstTypeArgument(reference) {
46
+ return Arr.head(reference.typeArguments?.params ?? []);
30
47
  }
31
48
  function isUnknownOrAnyType(type) {
32
49
  const unwrapped = unwrapTypeParentheses(type);
@@ -36,42 +53,45 @@ function isBroadRecordKeyType(type) {
36
53
  const unwrapped = unwrapTypeParentheses(type);
37
54
  if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
38
55
  if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
39
- return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
56
+ return unwrapped.type === "TSTypeReference" && isReferenceNamed(unwrapped, "PropertyKey");
57
+ }
58
+ function isBroadRecordReference(reference) {
59
+ if (isReferenceNamed(reference, "Readonly")) return Option.exists(firstTypeArgument(reference), isBroadRecordType);
60
+ if (!isReferenceNamed(reference, "Record")) return false;
61
+ return Option.exists(typeArgumentPair(reference), ([key, value]) => isBroadRecordKeyType(key) && isUnknownOrAnyType(value));
62
+ }
63
+ function isBroadIndexSignature(member) {
64
+ return member.parameters.length === 1 && Option.exists(Arr.head(member.parameters), (parameter) => isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation)) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
40
65
  }
41
66
  function isBroadRecordType(type) {
42
67
  const unwrapped = unwrapTypeParentheses(type);
43
- if (unwrapped.type === "TSTypeReference") {
44
- if (typeReferenceName(unwrapped) === "Readonly") {
45
- const [inner] = unwrapped.typeArguments?.params ?? [];
46
- return inner !== void 0 && isBroadRecordType(inner);
47
- }
48
- if (typeReferenceName(unwrapped) !== "Record") return false;
49
- const parameters = unwrapped.typeArguments?.params ?? [];
50
- return parameters.length === 2 && parameters[0] !== void 0 && parameters[1] !== void 0 && isBroadRecordKeyType(parameters[0]) && isUnknownOrAnyType(parameters[1]);
51
- }
68
+ if (unwrapped.type === "TSTypeReference") return isBroadRecordReference(unwrapped);
52
69
  if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
53
- const [member] = unwrapped.members;
54
- const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
55
- return member?.type === "TSIndexSignature" && member.parameters.length === 1 && parameter !== void 0 && isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
70
+ return Option.exists(Arr.head(unwrapped.members), (member) => member.type === "TSIndexSignature" && isBroadIndexSignature(member));
56
71
  }
57
72
  function broadTypeKind(type) {
58
73
  const unwrapped = unwrapTypeParentheses(type);
59
- if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
60
- if (unwrapped.type === "TSObjectKeyword") return "object";
61
- return isBroadRecordType(unwrapped) ? "record" : null;
74
+ if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return Option.some("top");
75
+ if (unwrapped.type === "TSObjectKeyword") return Option.some("object");
76
+ if (isBroadRecordType(unwrapped)) return Option.some("record");
77
+ return Option.none();
78
+ }
79
+ function isBroadType(type) {
80
+ return Option.isSome(broadTypeKind(type));
62
81
  }
63
82
  function assertedExpression(node) {
64
83
  return unwrapExpressionParentheses(node.expression);
65
84
  }
66
85
  function assertionFromExpression(expression) {
67
86
  const unwrapped = unwrapExpressionParentheses(expression);
68
- return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" ? unwrapped : null;
87
+ if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") return Option.some(unwrapped);
88
+ return Option.none();
69
89
  }
70
90
  function normalizedTypeText(sourceText, type) {
71
91
  return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
72
92
  }
73
93
  function typesHaveSameSyntax(sourceText, left, right) {
74
- return left !== null && normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right));
94
+ return Option.exists(left, (type) => normalizedTypeText(sourceText, unwrapTypeParentheses(type)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right)));
75
95
  }
76
96
  function isDefinitelyObjectType(type) {
77
97
  const unwrapped = unwrapTypeParentheses(type);
@@ -92,74 +112,79 @@ function isDefinitelyNarrowerRecordType(type) {
92
112
  const unwrapped = unwrapTypeParentheses(type);
93
113
  if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
94
114
  if (unwrapped.type !== "TSTypeReference") return false;
95
- if (typeReferenceName(unwrapped) === "Readonly") {
96
- const [inner] = unwrapped.typeArguments?.params ?? [];
97
- return inner !== void 0 && isDefinitelyNarrowerRecordType(inner);
98
- }
99
- if (typeReferenceName(unwrapped) !== "Record") return false;
100
- const parameters = unwrapped.typeArguments?.params ?? [];
101
- return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
115
+ if (isReferenceNamed(unwrapped, "Readonly")) return Option.exists(firstTypeArgument(unwrapped), isDefinitelyNarrowerRecordType);
116
+ if (!isReferenceNamed(unwrapped, "Record")) return false;
117
+ return Option.exists(typeArgumentPair(unwrapped), ([, value]) => !isUnknownOrAnyType(value));
102
118
  }
103
119
  function functionBoundary(node) {
104
- let current = node.parent;
105
- while (current !== null && current.type !== "Program") {
106
- if (functionBoundaryTypes.has(current.type)) return current;
107
- current = current.parent;
108
- }
109
- return null;
120
+ return Arr.findFirst(ancestors(node), (ancestor) => functionBoundaryTypes.has(ancestor.type));
110
121
  }
111
122
  function resolvedVariableForIdentifier(scopes, identifier) {
112
- for (const scope of scopes) {
113
- const reference = scope.references.find((candidate) => candidate.identifier.start === identifier.start && candidate.identifier.end === identifier.end);
114
- if (reference !== void 0) return reference.resolved;
115
- }
116
- return null;
123
+ const reference = Arr.findFirst(scopes, (scope) => Arr.findFirst(scope.references, (candidate) => candidate.identifier.start === identifier.start && candidate.identifier.end === identifier.end));
124
+ return Option.flatMap(reference, (found) => Option.fromNullishOr(found.resolved));
117
125
  }
118
126
  function variableDeclarator(variable) {
119
- for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return definition.node;
120
- return null;
127
+ for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return Option.some(definition.node);
128
+ return Option.none();
129
+ }
130
+ function isConstDeclarator(declarator) {
131
+ return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const";
132
+ }
133
+ function isReassigned(variable) {
134
+ return variable.references.some((reference) => reference.isWrite() && !reference.init);
121
135
  }
122
136
  function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
123
137
  const unwrapped = unwrapExpressionParentheses(expression);
124
138
  if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
125
- if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
126
- return { type: unwrapped.typeAnnotation };
139
+ if (isBroadType(unwrapped.typeAnnotation)) return Option.none();
140
+ return Option.some({ type: Option.some(unwrapped.typeAnnotation) });
127
141
  }
128
- if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return { type: null };
129
- if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return { type: null };
130
- if (unwrapped.type !== "Identifier") return null;
131
- const variable = resolvedVariableForIdentifier(scopes, unwrapped);
132
- if (variable === null || visitedVariables.has(variable)) return null;
133
- const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== void 0);
134
- const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
135
- if (annotation !== void 0 && annotatedIdentifier !== void 0) {
136
- if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) return null;
137
- return { type: annotation };
138
- }
139
- const declarator = variableDeclarator(variable);
140
- if (declarator === null || declarator.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init) || functionBoundary(declarator) !== boundary) return null;
141
- return knownValueEvidence(declarator.init, scopes, boundary, new Set([...visitedVariables, variable]));
142
+ if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return Option.some({ type: Option.none() });
143
+ if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return Option.some({ type: Option.none() });
144
+ if (unwrapped.type === "Identifier") return identifierValueEvidence(unwrapped, scopes, boundary, visitedVariables);
145
+ return Option.none();
146
+ }
147
+ function identifierValueEvidence(expression, scopes, boundary, visitedVariables) {
148
+ return resolvedVariableForIdentifier(scopes, expression).pipe(Option.filter((variable) => !visitedVariables.has(variable)), Option.flatMap((variable) => Option.match(annotatedIdentifier(variable), {
149
+ onSome: ([identifier, annotation]) => annotatedValueEvidence(identifier, annotation, boundary),
150
+ onNone: () => declaredValueEvidence(variable, scopes, boundary, visitedVariables)
151
+ })));
152
+ }
153
+ /** The first declaration identifier of a variable that carries a type annotation. */
154
+ function annotatedIdentifier(variable) {
155
+ return Arr.findFirst(variable.identifiers, (identifier) => Option.map(Option.fromNullishOr(identifier.typeAnnotation), (annotation) => [identifier, annotation.typeAnnotation]));
156
+ }
157
+ function annotatedValueEvidence(identifier, annotation, boundary) {
158
+ if (!sameBoundary(functionBoundary(identifier), boundary) || isBroadType(annotation)) return Option.none();
159
+ return Option.some({ type: Option.some(annotation) });
160
+ }
161
+ function declaredValueEvidence(variable, scopes, boundary, visitedVariables) {
162
+ return variableDeclarator(variable).pipe(Option.filter((declarator) => isConstDeclarator(declarator) && !isReassigned(variable) && sameBoundary(functionBoundary(declarator), boundary)), Option.flatMap((declarator) => Option.fromNullishOr(declarator.init)), Option.flatMap((init) => knownValueEvidence(init, scopes, boundary, new Set([...visitedVariables, variable]))));
142
163
  }
143
164
  function widenedBinding(variable, scopes) {
144
- const declarator = variableDeclarator(variable);
145
- if (declarator === null || declarator.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.id.type !== "Identifier" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
146
- const identifier = variable.identifiers.find((candidate) => candidate.start === declarator.id.start && candidate.end === declarator.id.end);
165
+ return variableDeclarator(variable).pipe(Option.filter((declarator) => isConstDeclarator(declarator) && declarator.id.type === "Identifier" && !isReassigned(variable)), Option.flatMap((declarator) => Option.flatMap(Option.fromNullishOr(declarator.init), (init) => widenedDeclarator(variable, declarator, init, scopes))));
166
+ }
167
+ function widenedDeclarator(variable, declarator, init, scopes) {
168
+ const identifier = Arr.findFirst(variable.identifiers, (candidate) => candidate.start === declarator.id.start && candidate.end === declarator.id.end);
147
169
  const boundary = functionBoundary(declarator);
148
- const declaredType = identifier?.typeAnnotation?.typeAnnotation;
149
- const initializerAssertion = assertionFromExpression(declarator.init);
150
- const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
151
- const broadKind = (declaredType === void 0 ? null : broadTypeKind(declaredType)) ?? initializerBroadKind;
152
- if (broadKind === null) return null;
153
- const evidence = knownValueEvidence(initializerAssertion !== null && initializerBroadKind !== null ? assertedExpression(initializerAssertion) : declarator.init, scopes, boundary, new Set([variable]));
154
- return evidence === null ? null : {
155
- broadKind,
170
+ const declaredType = identifier.pipe(Option.flatMap((found) => Option.fromNullishOr(found.typeAnnotation)), Option.map((annotation) => annotation.typeAnnotation));
171
+ const initializerAssertion = assertionFromExpression(init);
172
+ const initializerBroadKind = Option.flatMap(initializerAssertion, (assertion) => broadTypeKind(assertion.typeAnnotation));
173
+ const declaredBroadKind = Option.flatMap(declaredType, broadTypeKind);
174
+ const broadKind = Option.orElse(declaredBroadKind, () => initializerBroadKind);
175
+ const originalExpression = Option.match(Option.zipLeft(initializerAssertion, initializerBroadKind), {
176
+ onNone: () => init,
177
+ onSome: assertedExpression
178
+ });
179
+ return Option.flatMap(broadKind, (kind) => Option.map(knownValueEvidence(originalExpression, scopes, boundary, new Set([variable])), (evidence) => ({
180
+ broadKind: kind,
156
181
  evidence,
157
182
  declaredAt: declarator.end,
158
183
  boundary
159
- };
184
+ })));
160
185
  }
161
186
  function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
162
- if (broadTypeKind(assertedType) !== null) return false;
187
+ if (isBroadType(assertedType)) return false;
163
188
  if (broadKind === "top") return true;
164
189
  if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
165
190
  if (broadKind === "object") return isDefinitelyObjectType(assertedType);
@@ -179,10 +204,8 @@ const noWidenThenAssert = define({
179
204
  const checkAssertion = (node) => {
180
205
  const expression = assertedExpression(node);
181
206
  if (expression.type !== "Identifier") return Effect.void;
182
- const variable = resolvedVariableForIdentifier(scopes, expression);
183
- if (variable === null) return Effect.void;
184
- const widened = widenedBinding(variable, scopes);
185
- if (widened === null || node.start <= widened.declaredAt || functionBoundary(node) !== widened.boundary || !assertionIsNarrower(context.sourceCode.text, widened.broadKind, widened.evidence, node.typeAnnotation)) return Effect.void;
207
+ const widened = Option.flatMap(resolvedVariableForIdentifier(scopes, expression), (variable) => widenedBinding(variable, scopes));
208
+ if (!Option.exists(widened, (binding) => node.start > binding.declaredAt && sameBoundary(functionBoundary(node), binding.boundary) && assertionIsNarrower(context.sourceCode.text, binding.broadKind, binding.evidence, node.typeAnnotation))) return Effect.void;
186
209
  return context.report(fromId({
187
210
  node,
188
211
  messageId: "widenThenAssert",
@@ -3,51 +3,43 @@ 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
5
  import { isEffectCall, tagComparisonsInOr, taggedSwitchSubject } from "./_tagged-control-flow.js";
6
+ import * as Arr from "effect/Array";
7
+ import * as Option from "effect/Option";
8
+ import * as Predicate from "effect/Predicate";
6
9
  import * as Effect from "effect/Effect";
7
10
  //#region src/rules/prefer-catch-tag.ts
8
- const returnedExpression = (fn) => {
9
- if (fn.body === null) return null;
10
- if (fn.body.type !== "BlockStatement") return fn.body;
11
- if (fn.body.body.length !== 1) return null;
12
- const statement = fn.body.body[0];
13
- if (statement?.type !== "ReturnStatement" || statement.argument === null) return null;
14
- return statement.argument;
15
- };
16
- const hasManualTagPredicate = (argument) => {
17
- if (argument?.type !== "ArrowFunctionExpression" && argument?.type !== "FunctionExpression") return false;
18
- const parameter = argument.params[0];
19
- if (parameter?.type !== "Identifier") return false;
20
- const expression = returnedExpression(argument);
21
- if (expression === null) return false;
22
- const comparisons = tagComparisonsInOr(expression);
23
- if (comparisons === null || comparisons.length === 0) return false;
24
- return comparisons.every((comparison) => comparison.subject === parameter.name);
25
- };
26
- const isFunction = (argument) => argument?.type === "ArrowFunctionExpression" || argument?.type === "FunctionExpression";
27
- const hasTagCondition = (node, parameter) => {
28
- const comparisons = tagComparisonsInOr(node.test);
29
- if (comparisons === null || comparisons.length === 0) return false;
30
- return comparisons.every((comparison) => comparison.subject === parameter);
31
- };
11
+ const isFunction = (argument) => argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression";
12
+ const isIdentifier = (parameter) => parameter.type === "Identifier";
13
+ /** The name of a callback's first parameter when it is a plain identifier. */
14
+ const identifierParameter = (fn) => Arr.head(fn.params).pipe(Option.filter(isIdentifier), Option.map((parameter) => parameter.name));
15
+ /** The expression a callback returns when its body is that expression or a lone `return`. */
16
+ const returnedExpression = (fn) => Option.flatMap(Option.fromNullishOr(fn.body), (body) => {
17
+ if (body.type !== "BlockStatement") return Option.some(body);
18
+ const [statement] = body.body;
19
+ if (body.body.length !== 1 || statement?.type !== "ReturnStatement") return Option.none();
20
+ return Option.fromNullishOr(statement.argument);
21
+ });
22
+ /** Whether `expression` is one or more `parameter._tag === "Tag"` checks joined by `||`. */
23
+ const isTagCheckOn = (expression, parameter) => Option.exists(tagComparisonsInOr(expression), (comparisons) => comparisons.every((comparison) => comparison.subject === parameter));
24
+ const hasManualTagPredicate = (argument) => isFunction(argument) && Option.exists(Option.all([identifierParameter(argument), returnedExpression(argument)]), ([parameter, expression]) => isTagCheckOn(expression, parameter));
32
25
  const hasTagSwitch = (node, parameter) => {
33
- if (taggedSwitchSubject(node) !== parameter) return false;
34
- return node.cases.some((switchCase) => switchCase.test?.type === "Literal" && typeof switchCase.test.value === "string");
26
+ if (!Option.contains(taggedSwitchSubject(node), parameter)) return false;
27
+ return node.cases.some((switchCase) => switchCase.test?.type === "Literal" && Predicate.isString(switchCase.test.value));
35
28
  };
36
29
  const hasManualTagDispatch = (argument) => {
37
30
  if (!isFunction(argument) || argument.body?.type !== "BlockStatement") return false;
38
- const parameter = argument.params[0];
39
- if (parameter?.type !== "Identifier") return false;
40
- return argument.body.body.some((statement) => {
41
- if (statement.type === "IfStatement") return hasTagCondition(statement, parameter.name);
42
- if (statement.type === "SwitchStatement") return hasTagSwitch(statement, parameter.name);
31
+ const statements = argument.body.body;
32
+ return Option.exists(identifierParameter(argument), (parameter) => statements.some((statement) => {
33
+ if (statement.type === "IfStatement") return isTagCheckOn(statement.test, parameter);
34
+ if (statement.type === "SwitchStatement") return hasTagSwitch(statement, parameter);
43
35
  return false;
44
- });
36
+ }));
45
37
  };
46
38
  const catchAllHandler = (node) => {
47
- if (!isEffectCall(node, "catchAll")) return void 0;
39
+ if (!isEffectCall(node, "catchAll")) return Option.none();
48
40
  let handlerIndex = 1;
49
41
  if (node.arguments.length === 1) handlerIndex = 0;
50
- return node.arguments[handlerIndex];
42
+ return Arr.get(node.arguments, handlerIndex);
51
43
  };
52
44
  const preferCatchTag = define({
53
45
  name: "prefer-catch-tag",
@@ -61,9 +53,9 @@ const preferCatchTag = define({
61
53
  if (isEffectCall(node, "catchIf")) {
62
54
  let predicateIndex = 1;
63
55
  if (node.arguments.length === 2) predicateIndex = 0;
64
- const predicate = node.arguments[predicateIndex];
65
- if (!hasManualTagPredicate(predicate)) return Effect.void;
66
- } else if (!hasManualTagDispatch(catchAllHandler(node))) return Effect.void;
56
+ const predicate = Arr.get(node.arguments, predicateIndex);
57
+ if (!Option.exists(predicate, hasManualTagPredicate)) return Effect.void;
58
+ } else if (!Option.exists(catchAllHandler(node), hasManualTagDispatch)) return Effect.void;
67
59
  return context.report(make({
68
60
  node,
69
61
  message: "Use Effect.catchTag for one tagged failure or Effect.catchTags for multiple tagged failures."
@@ -2,26 +2,23 @@ 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 { isInsideCatchAllHandler, tagComparison, taggedSwitchSubject } from "./_tagged-control-flow.js";
5
+ import { hasOneTaggedSubject, isInsideCatchAllHandler, tagComparison, taggedSwitchSubject } from "./_tagged-control-flow.js";
6
+ import * as Arr from "effect/Array";
7
+ import * as Option from "effect/Option";
8
+ import * as Predicate from "effect/Predicate";
6
9
  import * as Effect from "effect/Effect";
7
10
  //#region src/rules/prefer-match-tags-exhaustive.ts
8
11
  const statementReturns = (statement) => {
9
12
  if (statement.type === "ReturnStatement") return true;
10
13
  if (statement.type !== "BlockStatement") return false;
11
- const last = statement.body.at(-1);
12
- if (last === void 0) return false;
13
- return statementReturns(last);
14
+ return Option.exists(Arr.last(statement.body), statementReturns);
14
15
  };
15
- const caseReturns = (switchCase) => {
16
- const last = switchCase.consequent.at(-1);
17
- if (last === void 0) return false;
18
- return statementReturns(last);
19
- };
20
- const isCompleteTransformationShape = (node) => {
21
- if (node.cases.length < 2 || taggedSwitchSubject(node) === null) return false;
16
+ const caseReturns = (switchCase) => Option.exists(Arr.last(switchCase.consequent), statementReturns);
17
+ const isCompleteSwitchTransformation = (node) => {
18
+ if (node.cases.length < 2 || Option.isNone(taggedSwitchSubject(node))) return false;
22
19
  for (const switchCase of node.cases) {
23
- if (switchCase.test === null || switchCase.test.type !== "Literal") return false;
24
- if (typeof switchCase.test.value !== "string" || !caseReturns(switchCase)) return false;
20
+ if (switchCase.test?.type !== "Literal") return false;
21
+ if (!Predicate.isString(switchCase.test.value) || !caseReturns(switchCase)) return false;
25
22
  }
26
23
  return true;
27
24
  };
@@ -30,45 +27,36 @@ const isTerminalStatement = (node) => {
30
27
  if (node.parent?.type !== "BlockStatement") return true;
31
28
  return node.parent.body.at(-1) === node;
32
29
  };
33
- const isCompleteIfTransformationShape = (node) => {
34
- if (isElseIf(node) || !isTerminalStatement(node)) return false;
30
+ const isPlainIf = (statement) => statement.type === "IfStatement" && Predicate.isNull(statement.alternate);
31
+ /** The tag a branch compares, when the branch always returns. */
32
+ const returningTagComparison = (branch) => Option.filter(tagComparison(branch.test), () => statementReturns(branch.consequent));
33
+ /** Whether at least two returning branches each compare one subject against a distinct tag. */
34
+ const isDistinctTagDispatch = (branches) => Option.exists(Option.all(branches.map(returningTagComparison)), (comparisons) => hasOneTaggedSubject(comparisons) && new Set(comparisons.map((comparison) => comparison.tag)).size === comparisons.length);
35
+ /** The if statement and each `else if` after it; none when the chain ends in a plain else. */
36
+ const elseIfChain = (node) => {
37
+ const chain = [node];
35
38
  let current = node;
36
- let subject;
37
- const tags = /* @__PURE__ */ new Set();
38
- let branchCount = 0;
39
- while (true) {
40
- const comparison = tagComparison(current.test);
41
- if (comparison === null || !statementReturns(current.consequent)) return false;
42
- if (subject !== void 0 && comparison.subject !== subject) return false;
43
- if (tags.has(comparison.tag)) return false;
44
- subject = comparison.subject;
45
- tags.add(comparison.tag);
46
- branchCount += 1;
47
- if (current.alternate === null) break;
48
- if (current.alternate.type !== "IfStatement") return false;
39
+ while (current.alternate?.type === "IfStatement") {
49
40
  current = current.alternate;
41
+ chain.push(current);
50
42
  }
51
- return branchCount >= 2;
43
+ if (Predicate.isNotNull(current.alternate)) return Option.none();
44
+ return Option.some(chain);
45
+ };
46
+ const isCompleteIfTransformation = (node) => {
47
+ if (isElseIf(node) || !isTerminalStatement(node)) return false;
48
+ return Option.exists(elseIfChain(node), isDistinctTagDispatch);
52
49
  };
53
- const isCompleteSequentialIfTransformationShape = (node) => {
54
- if (node.alternate !== null || node.parent?.type !== "BlockStatement") return false;
50
+ /** Whether `previous` is a returning tag check on the same subject, so `node` does not start a run. */
51
+ const continuesRun = (previous, node) => Option.exists(previous, (statement) => isPlainIf(statement) && statementReturns(statement.consequent) && Option.exists(Option.all([tagComparison(statement.test), tagComparison(node.test)]), ([before, current]) => before.subject === current.subject));
52
+ const isCompleteSequentialIfTransformation = (node) => {
53
+ if (!isPlainIf(node) || node.parent?.type !== "BlockStatement") return false;
55
54
  const statements = node.parent.body;
56
55
  const index = statements.indexOf(node);
57
56
  if (index < 0) return false;
58
- const comparison = tagComparison(node.test);
59
- if (comparison === null || !statementReturns(node.consequent)) return false;
60
- const previous = statements[index - 1];
61
- if (previous?.type === "IfStatement" && previous.alternate === null) {
62
- if (tagComparison(previous.test)?.subject === comparison.subject && statementReturns(previous.consequent)) return false;
63
- }
64
- const tags = new Set([comparison.tag]);
65
- for (const statement of statements.slice(index + 1)) {
66
- if (statement.type !== "IfStatement" || statement.alternate !== null) return false;
67
- const next = tagComparison(statement.test);
68
- if (next === null || next.subject !== comparison.subject || tags.has(next.tag) || !statementReturns(statement.consequent)) return false;
69
- tags.add(next.tag);
70
- }
71
- return tags.size >= 2;
57
+ const run = statements.slice(index);
58
+ if (!run.every(isPlainIf) || continuesRun(Arr.get(statements, index - 1), node)) return false;
59
+ return isDistinctTagDispatch(run);
72
60
  };
73
61
  const preferMatchTagsExhaustive = define({
74
62
  name: "prefer-match-tags-exhaustive",
@@ -80,14 +68,14 @@ const preferMatchTagsExhaustive = define({
80
68
  const context = yield* RuleContext;
81
69
  return {
82
70
  SwitchStatement: (node) => {
83
- if (!isCompleteTransformationShape(node) || isInsideCatchAllHandler(node)) return Effect.void;
71
+ if (!isCompleteSwitchTransformation(node) || isInsideCatchAllHandler(node)) return Effect.void;
84
72
  return context.report(make({
85
73
  node,
86
74
  message: "Use Match.type with Match.tagsExhaustive so a new tagged variant requires a new case."
87
75
  }));
88
76
  },
89
77
  IfStatement: (node) => {
90
- if (!isCompleteIfTransformationShape(node) && !isCompleteSequentialIfTransformationShape(node) || isInsideCatchAllHandler(node)) return Effect.void;
78
+ if (!isCompleteIfTransformation(node) && !isCompleteSequentialIfTransformation(node) || isInsideCatchAllHandler(node)) return Effect.void;
91
79
  return context.report(make({
92
80
  node,
93
81
  message: "Use Match.type with Match.tagsExhaustive so a new tagged variant requires a new branch."
@@ -3,15 +3,14 @@ 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
5
  import { effectCallbackLocation, hasOneTaggedSubject, tagComparisonsInOr } from "./_tagged-control-flow.js";
6
+ import * as Option from "effect/Option";
6
7
  import * as Effect from "effect/Effect";
7
8
  //#region src/rules/prefer-predicate-is-tagged.ts
8
- const isCatchIfPredicate = (node) => {
9
- const location = effectCallbackLocation(node, "catchIf");
10
- if (location === null) return false;
9
+ const isCatchIfPredicate = (node) => Option.exists(effectCallbackLocation(node, "catchIf"), (location) => {
11
10
  let predicateIndex = 1;
12
11
  if (location.argumentCount === 2) predicateIndex = 0;
13
12
  return location.index === predicateIndex;
14
- };
13
+ });
15
14
  const preferPredicateIsTagged = define({
16
15
  name: "prefer-predicate-is-tagged",
17
16
  meta: meta({
@@ -24,8 +23,7 @@ const preferPredicateIsTagged = define({
24
23
  if (node.operator !== "||") return Effect.void;
25
24
  if (node.parent?.type === "LogicalExpression" && node.parent.operator === "||") return Effect.void;
26
25
  if (isCatchIfPredicate(node)) return Effect.void;
27
- const comparisons = tagComparisonsInOr(node);
28
- if (comparisons === null || !hasOneTaggedSubject(comparisons)) return Effect.void;
26
+ if (!Option.exists(tagComparisonsInOr(node), hasOneTaggedSubject)) return Effect.void;
29
27
  return context.report(make({
30
28
  node,
31
29
  message: "Use a named Predicate refinement composed from Predicate.isTagged for multiple tags."
@@ -2,33 +2,44 @@ 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 { expressionArgument } from "./_call-arguments.js";
5
6
  import { importedNamespaces, isStaticCall, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
7
+ import * as Arr from "effect/Array";
8
+ import * as Option from "effect/Option";
9
+ import * as Predicate from "effect/Predicate";
6
10
  import * as Effect from "effect/Effect";
7
11
  //#region src/rules/prefer-service-of.ts
12
+ const isObjectExpression = (node) => node.type === "ObjectExpression";
13
+ const returnedObjectStatement = (statement) => {
14
+ if (statement.type !== "ReturnStatement" || statement.argument?.type !== "ObjectExpression") return Option.none();
15
+ return Option.some(statement.argument);
16
+ };
17
+ /** The object a function argument returns: its expression body or its first returned object. */
8
18
  const returnedObject = (node) => {
9
- if (node?.type !== "ArrowFunctionExpression" && node?.type !== "FunctionExpression") return;
10
- if (node.body === null) return void 0;
11
- if (node.body.type === "ObjectExpression") return node.body;
12
- if (node.body.type !== "BlockStatement") return void 0;
13
- for (const statement of node.body.body) if (statement.type === "ReturnStatement" && statement.argument?.type === "ObjectExpression") return statement.argument;
19
+ if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return Option.none();
20
+ const body = node.body;
21
+ if (Predicate.isNull(body)) return Option.none();
22
+ if (body.type === "ObjectExpression") return Option.some(body);
23
+ if (body.type !== "BlockStatement") return Option.none();
24
+ return Arr.findFirst(body.body, returnedObjectStatement);
25
+ };
26
+ /** The object an `Effect.succeed(...)` or `Effect.gen(...)` implementation produces. */
27
+ const effectImplementationObject = (implementation, effectNamespaces) => {
28
+ if (implementation.type !== "CallExpression" || implementation.callee.type === "Super") return Option.none();
29
+ if (isStaticMember(implementation.callee, effectNamespaces, "succeed")) return Option.filter(Arr.head(implementation.arguments), isObjectExpression);
30
+ if (isStaticCall(implementation, effectNamespaces, "gen")) return Option.flatMap(Arr.head(implementation.arguments), returnedObject);
31
+ return Option.none();
32
+ };
33
+ const layerImplementationObject = (callee, implementation, layerNamespaces, effectNamespaces) => {
34
+ if (isStaticMember(callee, layerNamespaces, "succeed")) return Option.liftPredicate(implementation, isObjectExpression);
35
+ if (isStaticMember(callee, layerNamespaces, "sync")) return returnedObject(implementation);
36
+ if (!isStaticMember(callee, layerNamespaces, "effect")) return Option.none();
37
+ return effectImplementationObject(implementation, effectNamespaces);
14
38
  };
15
39
  const implementationObject = (node, layerNamespaces, effectNamespaces) => {
16
- if (node.callee.type === "Super" || node.arguments.length < 2) return void 0;
17
- const implementation = node.arguments[1];
18
- if (implementation === void 0 || implementation.type === "SpreadElement") return void 0;
19
- if (isStaticMember(node.callee, layerNamespaces, "succeed")) {
20
- if (implementation.type === "ObjectExpression") return implementation;
21
- return;
22
- }
23
- if (isStaticMember(node.callee, layerNamespaces, "sync")) return returnedObject(implementation);
24
- if (!isStaticMember(node.callee, layerNamespaces, "effect")) return void 0;
25
- if (implementation.type !== "CallExpression" || implementation.callee.type === "Super") return;
26
- if (isStaticMember(implementation.callee, effectNamespaces, "succeed")) {
27
- const value = implementation.arguments[0];
28
- if (value?.type === "ObjectExpression") return value;
29
- return;
30
- }
31
- if (isStaticCall(implementation, effectNamespaces, "gen")) return returnedObject(implementation.arguments[0]);
40
+ const callee = node.callee;
41
+ if (callee.type === "Super") return Option.none();
42
+ return Option.flatMap(expressionArgument(node, 1), (implementation) => layerImplementationObject(callee, implementation, layerNamespaces, effectNamespaces));
32
43
  };
33
44
  const preferServiceOf = define({
34
45
  name: "prefer-service-of",
@@ -52,11 +63,13 @@ const preferServiceOf = define({
52
63
  const service = node.arguments[0];
53
64
  if (service?.type !== "Identifier") return Effect.void;
54
65
  const implementation = implementationObject(node, visibleNamespaces(ctx, node, layerNamespaces), visibleNamespaces(ctx, node, effectNamespaces));
55
- if (implementation === void 0) return Effect.void;
56
- return ctx.report(make({
57
- node: implementation,
58
- message: `Wrap this implementation with ${service.name}.of(...) so it stays checked against the service interface.`
59
- }));
66
+ return Option.match(implementation, {
67
+ onNone: () => Effect.void,
68
+ onSome: (object) => ctx.report(make({
69
+ node: object,
70
+ message: `Wrap this implementation with ${service.name}.of(...) so it stays checked against the service interface.`
71
+ }))
72
+ });
60
73
  }
61
74
  };
62
75
  }
@@ -3,9 +3,11 @@ 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
5
  import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import * as Arr from "effect/Array";
7
+ import * as Option from "effect/Option";
6
8
  import * as Effect from "effect/Effect";
7
9
  //#region src/rules/require-named-effect-fn.ts
8
- const isFunction = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
10
+ const isFunction = (node) => node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
9
11
  const requireNamedEffectFn = define({
10
12
  name: "require-named-effect-fn",
11
13
  meta: meta({
@@ -24,7 +26,7 @@ const requireNamedEffectFn = define({
24
26
  CallExpression: (node) => {
25
27
  if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
26
28
  const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
27
- if (!isStaticMember(node.callee, namespaces, "fn") || !isFunction(node.arguments[0])) return Effect.void;
29
+ if (!isStaticMember(node.callee, namespaces, "fn") || !Option.exists(Arr.head(node.arguments), isFunction)) return Effect.void;
28
30
  return ctx.report(make({
29
31
  node,
30
32
  message: "Name this operation with Effect.fn(\"Domain.operation\") for stable tracing and diagnostics."