oxlint-plugin-effect 0.13.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 (74) hide show
  1. package/README.md +26 -14
  2. package/THIRD_PARTY_NOTICES.md +4 -2
  3. package/dist/presets/recommended.d.ts +10 -0
  4. package/dist/presets/recommended.js +11 -1
  5. package/dist/rules/_anti-slop-array-method.js +78 -0
  6. package/dist/rules/_anti-slop-dictionary-types.js +198 -150
  7. package/dist/rules/_anti-slop-function-parameters.js +30 -0
  8. package/dist/rules/_anti-slop-lexical-type-parameters.js +25 -0
  9. package/dist/rules/_anti-slop-reflect-method.js +25 -0
  10. package/dist/rules/_anti-slop-scope.js +15 -0
  11. package/dist/rules/_anti-slop-type-alias-resolution.js +139 -0
  12. package/dist/rules/_ast-ancestors.js +14 -0
  13. package/dist/rules/_ast-children.js +18 -0
  14. package/dist/rules/_call-arguments.js +11 -0
  15. package/dist/rules/_effect-context.js +25 -20
  16. package/dist/rules/_function-metrics.js +94 -75
  17. package/dist/rules/_tagged-control-flow.js +38 -60
  18. package/dist/rules/_test-apis.js +47 -0
  19. package/dist/rules/_test-files.js +8 -0
  20. package/dist/rules/index.d.ts +11 -1
  21. package/dist/rules/index.js +21 -1
  22. package/dist/rules/no-array-filter-map.d.ts +7 -0
  23. package/dist/rules/no-array-filter-map.js +49 -0
  24. package/dist/rules/no-chained-type-assertions.js +7 -4
  25. package/dist/rules/no-conditional-empty-object-spread.d.ts +1 -1
  26. package/dist/rules/no-conditional-empty-object-spread.js +7 -47
  27. package/dist/rules/no-dynamic-imports.js +33 -25
  28. package/dist/rules/no-eager-acquire.d.ts +6 -0
  29. package/dist/rules/no-eager-acquire.js +98 -0
  30. package/dist/rules/no-effect-run-in-tests.d.ts +6 -0
  31. package/dist/rules/no-effect-run-in-tests.js +60 -0
  32. package/dist/rules/no-globals.js +42 -31
  33. package/dist/rules/no-known-value-widening.js +67 -50
  34. package/dist/rules/no-module-level-mutable-state.d.ts +6 -0
  35. package/dist/rules/no-module-level-mutable-state.js +47 -0
  36. package/dist/rules/no-module-mocks.js +37 -48
  37. package/dist/rules/no-new-promise.js +6 -2
  38. package/dist/rules/no-node-builtin-import.js +56 -33
  39. package/dist/rules/no-nullish.js +5 -1
  40. package/dist/rules/no-object-parameters.js +16 -30
  41. package/dist/rules/no-per-call-cache-construction.js +2 -2
  42. package/dist/rules/no-reduce-accumulator-copy.d.ts +7 -0
  43. package/dist/rules/no-reduce-accumulator-copy.js +106 -0
  44. package/dist/rules/no-reflect-apply.d.ts +7 -0
  45. package/dist/rules/no-reflect-apply.js +28 -0
  46. package/dist/rules/no-reflect-get.d.ts +7 -0
  47. package/dist/rules/no-reflect-get.js +28 -0
  48. package/dist/rules/no-run-collect-on-unbounded-stream.js +23 -25
  49. package/dist/rules/no-runtime-typeof.js +21 -4
  50. package/dist/rules/no-sequential-effect-all.js +18 -11
  51. package/dist/rules/no-shape-in-symbol-names.js +15 -5
  52. package/dist/rules/no-silent-catch-all.js +20 -14
  53. package/dist/rules/no-test-globals.d.ts +6 -0
  54. package/dist/rules/no-test-globals.js +147 -0
  55. package/dist/rules/no-test-lifecycle-hooks.js +4 -2
  56. package/dist/rules/no-unbounded-concurrency.js +22 -17
  57. package/dist/rules/no-unbounded-retry.js +47 -62
  58. package/dist/rules/no-unknown-parameters.d.ts +1 -1
  59. package/dist/rules/no-unknown-parameters.js +26 -17
  60. package/dist/rules/no-unknown-returns.d.ts +7 -0
  61. package/dist/rules/no-unknown-returns.js +74 -0
  62. package/dist/rules/no-unknown-type-aliases.js +21 -27
  63. package/dist/rules/no-unsafe-dictionary-type.js +71 -25
  64. package/dist/rules/no-widen-then-assert.js +96 -73
  65. package/dist/rules/prefer-catch-tag.js +29 -37
  66. package/dist/rules/prefer-match-tags-exhaustive.js +35 -47
  67. package/dist/rules/prefer-predicate-is-tagged.js +4 -6
  68. package/dist/rules/prefer-service-of.js +39 -26
  69. package/dist/rules/require-named-effect-fn.js +4 -2
  70. package/dist/rules/require-suppression-reason.d.ts +6 -0
  71. package/dist/rules/require-suppression-reason.js +92 -0
  72. package/dist/vendor/effect-oxlint/Diagnostic.js +1 -1
  73. package/dist/vendor/effect-oxlint/Scope.js +1 -1
  74. package/package.json +3 -2
@@ -0,0 +1,139 @@
1
+ import { childNodesAt, isAstNode } from "./_ast-children.js";
2
+ import { ancestors } from "./_ast-ancestors.js";
3
+ import { lexicalTypeParameterNames } from "./_anti-slop-lexical-type-parameters.js";
4
+ import * as Arr from "effect/Array";
5
+ import * as Option from "effect/Option";
6
+ import * as Predicate from "effect/Predicate";
7
+ //#region src/rules/_anti-slop-type-alias-resolution.ts
8
+ const environmentsByProgram = /* @__PURE__ */ new WeakMap();
9
+ function isNode(value) {
10
+ return isAstNode(value);
11
+ }
12
+ function isTypeScope(node) {
13
+ return node.type === "Program" || node.type === "BlockStatement" || node.type === "TSModuleBlock" || node.type === "StaticBlock" || node.type === "SwitchStatement";
14
+ }
15
+ function enclosingTypeScope(node) {
16
+ return Option.getOrElse(Arr.findFirst(ancestors(node), isTypeScope), () => node);
17
+ }
18
+ function declaredTypeBinding(node) {
19
+ if (node.type === "TSTypeAliasDeclaration") return Option.some({
20
+ alias: Option.some(node),
21
+ name: node.id.name
22
+ });
23
+ if (node.type === "TSInterfaceDeclaration" || node.type === "TSEnumDeclaration" || node.type === "ClassDeclaration" || node.type === "ClassExpression") return Option.map(Option.fromNullishOr(node.id), (id) => ({
24
+ alias: Option.none(),
25
+ name: id.name
26
+ }));
27
+ if (node.type === "ImportSpecifier" || node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier") return Option.some({
28
+ alias: Option.none(),
29
+ name: node.local.name
30
+ });
31
+ return Option.none();
32
+ }
33
+ function collectTypeBindings(node, visitorKeys, bindingsByName, aliases) {
34
+ const declared = declaredTypeBinding(node);
35
+ if (Option.isSome(declared)) {
36
+ const { alias, name } = declared.value;
37
+ const bindings = bindingsByName.get(name) ?? [];
38
+ bindings.push({
39
+ alias,
40
+ name,
41
+ scope: enclosingTypeScope(node)
42
+ });
43
+ bindingsByName.set(name, bindings);
44
+ if (Option.isSome(alias)) aliases.push(alias.value);
45
+ }
46
+ for (const key of visitorKeys[node.type] ?? []) for (const child of childNodesAt(node, key, isNode)) collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
47
+ }
48
+ /** Collect every lexical type alias and competing type binding in a program. */
49
+ function createTypeAliasEnvironment(program, visitorKeys) {
50
+ const cached = environmentsByProgram.get(program);
51
+ if (Predicate.isNotUndefined(cached)) return cached;
52
+ const bindingsByName = /* @__PURE__ */ new Map();
53
+ const aliases = [];
54
+ collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
55
+ const environment = {
56
+ aliases,
57
+ bindingsByName,
58
+ visitorKeys
59
+ };
60
+ environmentsByProgram.set(program, environment);
61
+ return environment;
62
+ }
63
+ function ancestorDistance(ancestor, node) {
64
+ return Arr.findFirstIndex([node, ...ancestors(node)], (current) => current === ancestor);
65
+ }
66
+ function nearestTypeBindings(name, use, environment) {
67
+ const candidates = environment.bindingsByName.get(name) ?? [];
68
+ let nearestDistance = Number.POSITIVE_INFINITY;
69
+ let nearest = [];
70
+ for (const candidate of candidates) {
71
+ const distance = ancestorDistance(candidate.scope, use);
72
+ if (Option.isNone(distance) || distance.value > nearestDistance) continue;
73
+ if (distance.value === nearestDistance) {
74
+ nearest.push(candidate);
75
+ continue;
76
+ }
77
+ nearestDistance = distance.value;
78
+ nearest = [candidate];
79
+ }
80
+ return nearest;
81
+ }
82
+ /** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
83
+ function visibleTypeAlias(name, use, environment) {
84
+ if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return Option.none();
85
+ const bindings = nearestTypeBindings(name, use, environment);
86
+ if (bindings.length !== 1) return Option.none();
87
+ return Option.flatMap(Arr.head(bindings), (binding) => binding.alias);
88
+ }
89
+ /** Return whether a local declaration shadows a built-in type at this use. */
90
+ function hasVisibleTypeBinding(name, use, environment) {
91
+ return lexicalTypeParameterNames(use, environment.visitorKeys).has(name) || nearestTypeBindings(name, use, environment).length > 0;
92
+ }
93
+ function typeReferenceName(type) {
94
+ if (type.typeName.type !== "Identifier") return Option.none();
95
+ return Option.some(type.typeName.name);
96
+ }
97
+ function aliasSubstitutions(alias, reference, base) {
98
+ const parameters = alias.typeParameters?.params ?? [];
99
+ const typeArguments = reference.typeArguments?.params ?? [];
100
+ const next = new Map(base);
101
+ for (const [index, parameter] of parameters.entries()) {
102
+ const explicitArgument = Arr.get(typeArguments, index);
103
+ const argument = Option.orElse(explicitArgument, () => Option.fromNullishOr(parameter.default));
104
+ if (Option.isNone(argument)) return Option.none();
105
+ let argumentSubstitutions = base;
106
+ if (Option.isNone(explicitArgument)) argumentSubstitutions = next;
107
+ next.set(parameter.name.name, {
108
+ type: argument.value,
109
+ substitutions: new Map(argumentSubstitutions)
110
+ });
111
+ }
112
+ return Option.some(next);
113
+ }
114
+ /** Match a type after resolving visible aliases and substituting their type parameters. */
115
+ function resolvedTypeMatches(type, environment, matcher) {
116
+ const evaluate = (current, substitutions, resolvingAliases) => {
117
+ if (current.type === "TSTypeReference") {
118
+ const name = typeReferenceName(current);
119
+ if (Option.isSome(name)) {
120
+ const substitution = substitutions.get(name.value);
121
+ if (Predicate.isNotUndefined(substitution) && !current.typeArguments?.params.length) return evaluate(substitution.type, substitution.substitutions, resolvingAliases);
122
+ const expansion = Option.flatMap(Option.filter(visibleTypeAlias(name.value, current, environment), (alias) => !resolvingAliases.has(alias)), (alias) => Option.map(aliasSubstitutions(alias, current, substitutions), (next) => ({
123
+ alias,
124
+ next
125
+ })));
126
+ if (Option.isSome(expansion)) {
127
+ const { alias, next } = expansion.value;
128
+ const nextResolving = new Set(resolvingAliases);
129
+ nextResolving.add(alias);
130
+ return evaluate(alias.typeAnnotation, next, nextResolving);
131
+ }
132
+ }
133
+ }
134
+ return matcher(current, (child) => evaluate(child, substitutions, resolvingAliases));
135
+ };
136
+ return evaluate(type, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
137
+ }
138
+ //#endregion
139
+ export { createTypeAliasEnvironment, hasVisibleTypeBinding, resolvedTypeMatches, visibleTypeAlias };
@@ -0,0 +1,14 @@
1
+ import * as Option from "effect/Option";
2
+ //#region src/rules/_ast-ancestors.ts
3
+ /** The node's parent, or none at the Program root. */
4
+ const parentOf = (node) => Option.fromNullishOr(node.parent);
5
+ /** The node's ancestors, nearest first, ending at the Program root. */
6
+ function* ancestors(node) {
7
+ let current = parentOf(node);
8
+ while (Option.isSome(current)) {
9
+ yield current.value;
10
+ current = parentOf(current.value);
11
+ }
12
+ }
13
+ //#endregion
14
+ export { ancestors, parentOf };
@@ -0,0 +1,18 @@
1
+ import * as Predicate from "effect/Predicate";
2
+ //#region src/rules/_ast-children.ts
3
+ /**
4
+ * Child-node access by property name, for AST walkers driven by visitor keys
5
+ * or object keys. Reads through the property descriptor, so no walker has to
6
+ * view a node as an open dictionary.
7
+ */
8
+ /** Whether a value is an AST node: an object whose `type` is a string. */
9
+ const isAstNode = (value) => Predicate.hasProperty(value, "type") && Predicate.isString(value.type);
10
+ /** The nodes held at `node[key]`: the node itself, the nodes of an array, or none. */
11
+ const childNodesAt = (node, key, isNode) => {
12
+ const value = Object.getOwnPropertyDescriptor(node, key)?.value;
13
+ if (isNode(value)) return [value];
14
+ if (Array.isArray(value)) return value.filter(isNode);
15
+ return [];
16
+ };
17
+ //#endregion
18
+ export { childNodesAt, isAstNode };
@@ -0,0 +1,11 @@
1
+ import * as Arr from "effect/Array";
2
+ import * as Option from "effect/Option";
3
+ //#region src/rules/_call-arguments.ts
4
+ const isExpression = (argument) => argument.type !== "SpreadElement";
5
+ /** The call's argument at `index`, or none when it is missing or spread. */
6
+ const expressionArgument = (node, index) => Option.filter(Arr.get(node.arguments, index), isExpression);
7
+ const isStaticProperty = (property, name) => property.type === "Property" && !property.computed && (property.key.type === "Identifier" && property.key.name === name || property.key.type === "Literal" && property.key.value === name);
8
+ /** Every non-computed property of an object literal named `name`, in source order. */
9
+ const staticProperties = (node, name) => node.properties.filter((property) => isStaticProperty(property, name));
10
+ //#endregion
11
+ export { expressionArgument, staticProperties };
@@ -1,27 +1,32 @@
1
+ import { ancestors } from "./_ast-ancestors.js";
1
2
  import { isStaticCall, isStaticMember } from "./_effect-namespaces.js";
3
+ import * as Arr from "effect/Array";
4
+ import * as Option from "effect/Option";
2
5
  //#region src/rules/_effect-context.ts
3
- const isFunction = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
6
+ const isFunction = (node) => node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
7
+ /** `Effect.gen(...)`, `Effect.fn(...)`, or `Effect.fnUntraced(...)` called with the program directly. */
8
+ const directProgramKind = (callee, effectNamespaces) => {
9
+ if (isStaticMember(callee, effectNamespaces, "gen")) return Option.some("gen");
10
+ if (isStaticMember(callee, effectNamespaces, "fn")) return Option.some("fn");
11
+ if (isStaticMember(callee, effectNamespaces, "fnUntraced")) return Option.some("fnUntraced");
12
+ return Option.none();
13
+ };
14
+ /** `Effect.fn("name")(...)` or `Effect.fnUntraced(options)(...)` called with the program. */
15
+ const curriedProgramKind = (callee, effectNamespaces) => {
16
+ if (isStaticCall(callee, effectNamespaces, "fn")) return Option.some("fn");
17
+ if (isStaticCall(callee, effectNamespaces, "fnUntraced")) return Option.some("fnUntraced");
18
+ return Option.none();
19
+ };
20
+ /** The kind of Effect program a function is the body of, when it is an argument of one. */
4
21
  const effectProgramKind = (node, effectNamespaces) => {
5
22
  const parent = node.parent;
6
- if (parent?.type !== "CallExpression" || !parent.arguments.includes(node)) return void 0;
7
- if (parent.callee.type !== "Super" && isStaticMember(parent.callee, effectNamespaces, "gen")) return "gen";
8
- if (parent.callee.type !== "Super" && isStaticMember(parent.callee, effectNamespaces, "fn")) return "fn";
9
- if (parent.callee.type !== "Super" && isStaticMember(parent.callee, effectNamespaces, "fnUntraced")) return "fnUntraced";
10
- if (parent.callee.type !== "CallExpression") return void 0;
11
- if (isStaticCall(parent.callee, effectNamespaces, "fn")) return "fn";
12
- if (isStaticCall(parent.callee, effectNamespaces, "fnUntraced")) return "fnUntraced";
13
- };
14
- const enclosingEffectProgram = (node, effectNamespaces) => {
15
- let current = node.parent;
16
- while (current !== void 0 && current !== null) {
17
- if (isFunction(current)) {
18
- const kind = effectProgramKind(current, effectNamespaces);
19
- if (kind === void 0) return void 0;
20
- return [current, kind];
21
- }
22
- current = current.parent;
23
- }
23
+ if (parent?.type !== "CallExpression" || !parent.arguments.includes(node)) return Option.none();
24
+ const callee = parent.callee;
25
+ if (callee.type === "Super") return Option.none();
26
+ return Option.orElse(directProgramKind(callee, effectNamespaces), () => curriedProgramKind(callee, effectNamespaces));
24
27
  };
25
- const isInsideEffectProgram = (node, effectNamespaces) => enclosingEffectProgram(node, effectNamespaces) !== void 0;
28
+ /** The nearest enclosing function, when it is the body of an Effect program. */
29
+ const enclosingEffectProgram = (node, effectNamespaces) => Option.flatMap(Arr.findFirst(ancestors(node), isFunction), (program) => Option.map(effectProgramKind(program, effectNamespaces), (kind) => [program, kind]));
30
+ const isInsideEffectProgram = (node, effectNamespaces) => Option.isSome(enclosingEffectProgram(node, effectNamespaces));
26
31
  //#endregion
27
32
  export { enclosingEffectProgram, isInsideEffectProgram };
@@ -1,5 +1,9 @@
1
+ import { childNodesAt, isAstNode } from "./_ast-children.js";
2
+ import * as Arr from "effect/Array";
3
+ import * as Option from "effect/Option";
4
+ import * as Predicate from "effect/Predicate";
1
5
  //#region src/rules/_function-metrics.ts
2
- const isNode = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
6
+ const isNode = (value) => isAstNode(value);
3
7
  const isFunctionType = (type) => type === "ArrowFunctionExpression" || type === "FunctionDeclaration" || type === "FunctionExpression";
4
8
  const isFunctionNode = (node) => isFunctionType(node.type);
5
9
  /** Slots that hold the parent link, source positions, or type-level syntax. */
@@ -26,12 +30,7 @@ const transparentTypeWrappers = new Set([
26
30
  "TSInstantiationExpression"
27
31
  ]);
28
32
  const isTypeOnly = (node) => node.type.startsWith("TS") && !transparentTypeWrappers.has(node.type);
29
- const slot = (node, key) => {
30
- const value = node[key];
31
- if (isNode(value)) return [value];
32
- if (Array.isArray(value)) return value.filter(isNode);
33
- return [];
34
- };
33
+ const slot = (node, key) => childNodesAt(node, key, isNode);
35
34
  /** Runtime child nodes of `node`, in source order of their slots. */
36
35
  const children = (node) => {
37
36
  if (transparentTypeWrappers.has(node.type)) return slot(node, "expression");
@@ -44,8 +43,7 @@ const children = (node) => {
44
43
  };
45
44
  /** The nodes that form the body of a function unit: its parameters and its body. */
46
45
  const unitRoots = (fn) => {
47
- const node = fn;
48
- return [...slot(node, "params"), ...slot(node, "body")];
46
+ return [...slot(fn, "params"), ...slot(fn, "body")];
49
47
  };
50
48
  /**
51
49
  * Flatten a logical expression tree into its operator sequence, left to right.
@@ -81,7 +79,8 @@ const cognitiveComplexity = (fn) => {
81
79
  for (const child of slot(node, key)) visit(child, nesting);
82
80
  };
83
81
  const visitIf = (node, nesting, isElseIf) => {
84
- total += isElseIf ? 1 : 1 + nesting;
82
+ total += 1;
83
+ if (!isElseIf) total += nesting;
85
84
  visitSlot(node, "test", nesting);
86
85
  visitSlot(node, "consequent", nesting + 1);
87
86
  for (const alternate of slot(node, "alternate")) if (alternate.type === "IfStatement") visitIf(alternate, nesting, true);
@@ -140,50 +139,61 @@ const halsteadDifficulty = (counts) => {
140
139
  };
141
140
  const functionOperator = (node) => {
142
141
  if (node.type === "ArrowFunctionExpression") return "=>";
143
- return node.generator === true ? "function*" : "function";
142
+ if (node.generator === true) return "function*";
143
+ return "function";
144
144
  };
145
145
  const literalOperand = (node) => {
146
- if (typeof node.raw === "string") return node.raw;
147
- if (typeof node.bigint === "string") return `${node.bigint}n`;
146
+ if (Predicate.isString(node.raw)) return node.raw;
147
+ if (Predicate.isString(node.bigint)) return `${node.bigint}n`;
148
148
  return String(node.value);
149
149
  };
150
+ const isNonEmpty = (text) => text.length > 0;
150
151
  const templateOperand = (node) => {
151
152
  const value = node.value;
152
- if (typeof value !== "object" || value === null) return null;
153
- const raw = value.raw;
154
- return typeof raw === "string" && raw.length > 0 ? raw : null;
153
+ if (!Predicate.hasProperty(value, "raw") || !Predicate.isString(value.raw)) return Option.none();
154
+ return Option.liftPredicate(value.raw, isNonEmpty);
155
155
  };
156
- /** Node types that always stand for the same operator tokens. */
157
- const fixedOperators = {
158
- ForStatement: ["for"],
159
- ForInStatement: ["for-in"],
160
- WhileStatement: ["while"],
161
- DoWhileStatement: ["do-while"],
162
- SwitchStatement: ["switch"],
163
- ReturnStatement: ["return"],
164
- ThrowStatement: ["throw"],
165
- CatchClause: ["catch"],
166
- BreakStatement: ["break"],
167
- ContinueStatement: ["continue"],
168
- LabeledStatement: [":"],
169
- DebuggerStatement: ["debugger"],
170
- WithStatement: ["with"],
171
- AssignmentPattern: ["="],
172
- NewExpression: ["new"],
173
- ArrayExpression: ["[]"],
174
- ArrayPattern: ["[]"],
175
- ObjectExpression: ["{}"],
176
- ObjectPattern: ["{}"],
177
- SpreadElement: ["..."],
178
- RestElement: ["..."],
179
- ConditionalExpression: ["?:"],
180
- SequenceExpression: [","],
181
- AwaitExpression: ["await"],
182
- ImportExpression: ["import()"],
183
- TemplateLiteral: ["`"],
184
- JSXElement: ["<>"],
185
- JSXFragment: ["<>"]
156
+ /** `tokens` when `present`, otherwise none: the operator tokens of an optional clause. */
157
+ const tokensIf = (present, ...tokens) => {
158
+ if (!present) return [];
159
+ return tokens;
186
160
  };
161
+ /** The `name` of the node held in a slot, or empty when the slot holds none. */
162
+ const slotName = (node, key) => Option.match(Arr.head(slot(node, key)), {
163
+ onNone: () => "",
164
+ onSome: (child) => String(child.name)
165
+ });
166
+ /** Node types that always stand for the same operator tokens. */
167
+ const fixedOperators = new Map([
168
+ ["ForStatement", ["for"]],
169
+ ["ForInStatement", ["for-in"]],
170
+ ["WhileStatement", ["while"]],
171
+ ["DoWhileStatement", ["do-while"]],
172
+ ["SwitchStatement", ["switch"]],
173
+ ["ReturnStatement", ["return"]],
174
+ ["ThrowStatement", ["throw"]],
175
+ ["CatchClause", ["catch"]],
176
+ ["BreakStatement", ["break"]],
177
+ ["ContinueStatement", ["continue"]],
178
+ ["LabeledStatement", [":"]],
179
+ ["DebuggerStatement", ["debugger"]],
180
+ ["WithStatement", ["with"]],
181
+ ["AssignmentPattern", ["="]],
182
+ ["NewExpression", ["new"]],
183
+ ["ArrayExpression", ["[]"]],
184
+ ["ArrayPattern", ["[]"]],
185
+ ["ObjectExpression", ["{}"]],
186
+ ["ObjectPattern", ["{}"]],
187
+ ["SpreadElement", ["..."]],
188
+ ["RestElement", ["..."]],
189
+ ["ConditionalExpression", ["?:"]],
190
+ ["SequenceExpression", [","]],
191
+ ["AwaitExpression", ["await"]],
192
+ ["ImportExpression", ["import()"]],
193
+ ["TemplateLiteral", ["`"]],
194
+ ["JSXElement", ["<>"]],
195
+ ["JSXFragment", ["<>"]]
196
+ ]);
187
197
  /** Node types whose operator token carries the operator itself. */
188
198
  const operatorCarriers = new Set([
189
199
  "BinaryExpression",
@@ -195,41 +205,53 @@ const operatorCarriers = new Set([
195
205
  /** Statement-level nodes whose operator tokens depend on the node's shape. */
196
206
  const statementOperators = (node) => {
197
207
  switch (node.type) {
198
- case "IfStatement": return isNode(node.alternate) ? ["if", "else"] : ["if"];
199
- case "ForOfStatement": return node.await === true ? ["for-await-of"] : ["for-of"];
200
- case "SwitchCase": return isNode(node.test) ? ["case"] : ["default"];
201
- case "TryStatement": return isNode(node.finalizer) ? ["try", "finally"] : ["try"];
208
+ case "IfStatement": return ["if", ...tokensIf(isNode(node.alternate), "else")];
209
+ case "ForOfStatement":
210
+ if (node.await === true) return ["for-await-of"];
211
+ return ["for-of"];
212
+ case "SwitchCase":
213
+ if (isNode(node.test)) return ["case"];
214
+ return ["default"];
215
+ case "TryStatement": return ["try", ...tokensIf(isNode(node.finalizer), "finally")];
202
216
  case "VariableDeclaration": return [String(node.kind)];
203
- case "VariableDeclarator": return isNode(node.init) ? ["="] : [];
217
+ case "VariableDeclarator": return tokensIf(isNode(node.init), "=");
204
218
  case "ClassDeclaration":
205
- case "ClassExpression": return isNode(node.superClass) ? ["class", "extends"] : ["class"];
206
- case "PropertyDefinition": return isNode(node.value) ? ["="] : [];
219
+ case "ClassExpression": return ["class", ...tokensIf(isNode(node.superClass), "extends")];
220
+ case "PropertyDefinition": return tokensIf(isNode(node.value), "=");
207
221
  default: return [];
208
222
  }
209
223
  };
224
+ /** `?.` calls and accesses carry a leading `?`. */
225
+ const optionalMark = (node) => {
226
+ if (node.optional === true) return "?";
227
+ return "";
228
+ };
229
+ const memberAccess = (node) => {
230
+ if (node.computed === true) return "[]";
231
+ return ".";
232
+ };
210
233
  /** Expression-level nodes whose operator tokens depend on the node's shape. */
211
234
  const expressionOperators = (node) => {
212
235
  if (operatorCarriers.has(node.type)) return [String(node.operator)];
213
236
  switch (node.type) {
214
- case "CallExpression": return node.optional === true ? ["?.()"] : ["()"];
215
- case "MemberExpression": {
216
- const access = node.computed === true ? "[]" : ".";
217
- return node.optional === true ? [`?${access}`] : [access];
218
- }
237
+ case "CallExpression": return [`${optionalMark(node)}()`];
238
+ case "MemberExpression": return [`${optionalMark(node)}${memberAccess(node)}`];
219
239
  case "Property":
220
240
  if (node.kind === "get" || node.kind === "set") return [String(node.kind)];
221
- return node.shorthand === true || node.method === true ? [] : [":"];
222
- case "YieldExpression": return node.delegate === true ? ["yield*"] : ["yield"];
223
- case "JSXAttribute": return isNode(node.value) ? ["="] : [];
241
+ return tokensIf(node.shorthand !== true && node.method !== true, ":");
242
+ case "YieldExpression":
243
+ if (node.delegate === true) return ["yield*"];
244
+ return ["yield"];
245
+ case "JSXAttribute": return tokensIf(isNode(node.value), "=");
224
246
  case "ArrowFunctionExpression":
225
247
  case "FunctionDeclaration":
226
- case "FunctionExpression": return node.async === true ? ["async", functionOperator(node)] : [functionOperator(node)];
248
+ case "FunctionExpression": return [...tokensIf(node.async === true, "async"), functionOperator(node)];
227
249
  default: return [];
228
250
  }
229
251
  };
230
252
  /** Every operator token a node stands for. */
231
253
  const operatorsOf = (node) => [
232
- ...fixedOperators[node.type] ?? [],
254
+ ...fixedOperators.get(node.type) ?? [],
233
255
  ...statementOperators(node),
234
256
  ...expressionOperators(node)
235
257
  ];
@@ -237,18 +259,15 @@ const operatorsOf = (node) => [
237
259
  const operandOf = (node) => {
238
260
  switch (node.type) {
239
261
  case "Identifier":
240
- case "JSXIdentifier": return String(node.name);
241
- case "PrivateIdentifier": return `#${String(node.name)}`;
242
- case "Literal": return literalOperand(node);
262
+ case "JSXIdentifier": return Option.some(String(node.name));
263
+ case "PrivateIdentifier": return Option.some(`#${String(node.name)}`);
264
+ case "Literal": return Option.some(literalOperand(node));
243
265
  case "TemplateElement": return templateOperand(node);
244
- case "ThisExpression": return "this";
245
- case "Super": return "super";
246
- case "MetaProperty": return `${isNode(node.meta) ? String(node.meta.name) : ""}.${isNode(node.property) ? String(node.property.name) : ""}`;
247
- case "JSXText": {
248
- const text = String(node.value).trim();
249
- return text.length > 0 ? text : null;
250
- }
251
- default: return null;
266
+ case "ThisExpression": return Option.some("this");
267
+ case "Super": return Option.some("super");
268
+ case "MetaProperty": return Option.some(`${slotName(node, "meta")}.${slotName(node, "property")}`);
269
+ case "JSXText": return Option.liftPredicate(String(node.value).trim(), isNonEmpty);
270
+ default: return Option.none();
252
271
  }
253
272
  };
254
273
  /**
@@ -266,7 +285,7 @@ const halstead = (fn) => {
266
285
  if (isTypeOnly(node)) return;
267
286
  for (const operator of operatorsOf(node)) tally(operators, operator);
268
287
  const operand = operandOf(node);
269
- if (operand !== null) tally(operands, operand);
288
+ if (Option.isSome(operand)) tally(operands, operand.value);
270
289
  if (isFunctionType(node.type) || node.type === "MetaProperty") return;
271
290
  for (const child of children(node)) visit(child);
272
291
  };
@@ -1,79 +1,57 @@
1
+ import { ancestors } from "./_ast-ancestors.js";
2
+ import * as Arr from "effect/Array";
3
+ import * as Option from "effect/Option";
4
+ import * as Predicate from "effect/Predicate";
1
5
  //#region src/rules/_tagged-control-flow.ts
2
6
  const expressionKey = (node) => {
3
- if (node.type === "Identifier") return node.name;
4
- if (node.type === "ThisExpression") return "this";
5
- if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return null;
6
- const object = expressionKey(node.object);
7
- if (object === null) return null;
8
- return `${object}.${node.property.name}`;
7
+ if (node.type === "Identifier") return Option.some(node.name);
8
+ if (node.type === "ThisExpression") return Option.some("this");
9
+ if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return Option.none();
10
+ const property = node.property.name;
11
+ return Option.map(expressionKey(node.object), (object) => `${object}.${property}`);
9
12
  };
10
13
  const stringLiteral = (node) => {
11
- if (node.type !== "Literal") return null;
12
- if (typeof node.value !== "string") return null;
13
- return node.value;
14
+ if (node.type !== "Literal" || !Predicate.isString(node.value)) return Option.none();
15
+ return Option.some(node.value);
14
16
  };
15
17
  const taggedSubject = (node) => {
16
- if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier" || node.property.name !== "_tag") return null;
18
+ if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier" || node.property.name !== "_tag") return Option.none();
17
19
  return expressionKey(node.object);
18
20
  };
21
+ /** `subjectSide._tag === "Tag"` read from one orientation of the comparison. */
22
+ const orientedComparison = (subjectSide, tagSide) => Option.all({
23
+ subject: taggedSubject(subjectSide),
24
+ tag: stringLiteral(tagSide)
25
+ });
19
26
  const tagComparison = (node) => {
20
- if (node.type !== "BinaryExpression" || node.operator !== "===") return null;
21
- const leftSubject = taggedSubject(node.left);
22
- const rightTag = stringLiteral(node.right);
23
- if (leftSubject !== null && rightTag !== null) return {
24
- subject: leftSubject,
25
- tag: rightTag
26
- };
27
- const rightSubject = taggedSubject(node.right);
28
- const leftTag = stringLiteral(node.left);
29
- if (rightSubject === null || leftTag === null) return null;
30
- return {
31
- subject: rightSubject,
32
- tag: leftTag
33
- };
27
+ if (node.type !== "BinaryExpression" || node.operator !== "===") return Option.none();
28
+ const { left, right } = node;
29
+ return Option.orElse(orientedComparison(left, right), () => orientedComparison(right, left));
34
30
  };
35
31
  const tagComparisonsInOr = (node) => {
36
- if (node.type === "LogicalExpression" && node.operator === "||") {
37
- const left = tagComparisonsInOr(node.left);
38
- const right = tagComparisonsInOr(node.right);
39
- if (left === null || right === null) return null;
40
- return [...left, ...right];
41
- }
42
- const comparison = tagComparison(node);
43
- if (comparison === null) return null;
44
- return [comparison];
45
- };
46
- const hasOneTaggedSubject = (comparisons) => {
47
- const first = comparisons[0];
48
- if (first === void 0 || comparisons.length < 2) return false;
49
- return comparisons.every((comparison) => comparison.subject === first.subject);
32
+ if (node.type === "LogicalExpression" && node.operator === "||") return Option.zipWith(tagComparisonsInOr(node.left), tagComparisonsInOr(node.right), (left, right) => [...left, ...right]);
33
+ return Option.map(tagComparison(node), (comparison) => [comparison]);
50
34
  };
35
+ const hasOneTaggedSubject = (comparisons) => comparisons.length >= 2 && Option.exists(Arr.head(comparisons), (first) => comparisons.every((comparison) => comparison.subject === first.subject));
51
36
  const isEffectCall = (node, operation) => node.callee.type !== "Super" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.object.type === "Identifier" && node.callee.object.name === "Effect" && node.callee.property.type === "Identifier" && node.callee.property.name === operation;
52
- const isFunction = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
53
- const effectCallbackLocation = (node, operation) => {
54
- let current = node.parent;
55
- while (current !== void 0 && current !== null) {
56
- if (isFunction(current)) {
57
- const call = current.parent;
58
- if (call?.type !== "CallExpression" || !isEffectCall(call, operation)) return null;
59
- const index = call.arguments.indexOf(current);
60
- if (index < 0) return null;
61
- return {
62
- argumentCount: call.arguments.length,
63
- index
64
- };
65
- }
66
- current = current.parent;
67
- }
68
- return null;
69
- };
70
- const isInsideCatchAllHandler = (node) => {
71
- const location = effectCallbackLocation(node, "catchAll");
72
- if (location === null) return false;
37
+ const isFunction = (node) => node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
38
+ const callbackLocation = (callback, operation) => {
39
+ const call = callback.parent;
40
+ if (call?.type !== "CallExpression" || !isEffectCall(call, operation)) return Option.none();
41
+ const index = call.arguments.indexOf(callback);
42
+ if (index < 0) return Option.none();
43
+ return Option.some({
44
+ argumentCount: call.arguments.length,
45
+ index
46
+ });
47
+ };
48
+ /** Where the nearest enclosing function sits among the arguments of an `Effect.<operation>` call. */
49
+ const effectCallbackLocation = (node, operation) => Option.flatMap(Arr.findFirst(ancestors(node), isFunction), (callback) => callbackLocation(callback, operation));
50
+ const isInsideCatchAllHandler = (node) => Option.exists(effectCallbackLocation(node, "catchAll"), (location) => {
73
51
  let handlerIndex = 1;
74
52
  if (location.argumentCount === 1) handlerIndex = 0;
75
53
  return location.index === handlerIndex;
76
- };
54
+ });
77
55
  const taggedSwitchSubject = (node) => taggedSubject(node.discriminant);
78
56
  //#endregion
79
57
  export { effectCallbackLocation, hasOneTaggedSubject, isEffectCall, isInsideCatchAllHandler, tagComparison, tagComparisonsInOr, taggedSwitchSubject };