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
@@ -1,4 +1,5 @@
1
1
  import { resolveVariable } from "./_anti-slop-scope.js";
2
+ import * as Option from "effect/Option";
2
3
  //#region src/rules/_anti-slop-reflect-method.ts
3
4
  /**
4
5
  * Ported from dmmulroy/anti-slop at
@@ -7,15 +8,18 @@ import { resolveVariable } from "./_anti-slop-scope.js";
7
8
  function isGlobalReflect(sourceCode, expression) {
8
9
  if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
9
10
  if (sourceCode.isGlobalReference(expression)) return true;
10
- const variable = resolveVariable(sourceCode, expression);
11
- return variable === null || variable.defs.length === 0;
11
+ return Option.match(resolveVariable(sourceCode, expression), {
12
+ onNone: () => true,
13
+ onSome: (variable) => variable.defs.length === 0
14
+ });
12
15
  }
13
16
  /** Reports whether a call target names one method on the global Reflect object. */
14
17
  function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
15
18
  if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
16
19
  if (!isGlobalReflect(sourceCode, callee.object)) return false;
17
20
  const property = callee.property;
18
- return callee.computed ? property.type === "Literal" && property.value === methodName : property.type === "Identifier" && property.name === methodName;
21
+ if (callee.computed) return property.type === "Literal" && property.value === methodName;
22
+ return property.type === "Identifier" && property.name === methodName;
19
23
  }
20
24
  //#endregion
21
25
  export { isGlobalReflectMethodCall };
@@ -1,13 +1,15 @@
1
+ import * as Option from "effect/Option";
2
+ import * as Predicate from "effect/Predicate";
1
3
  //#region src/rules/_anti-slop-scope.ts
2
4
  /** Resolve an identifier to its binding by walking lexical scopes upward. */
3
5
  function resolveVariable(sourceCode, identifier) {
4
- let scope = sourceCode.getScope(identifier);
5
- while (scope !== null) {
6
- const variable = scope.set.get(identifier.name);
7
- if (variable !== void 0) return variable;
8
- scope = scope.upper;
6
+ let scope = Option.some(sourceCode.getScope(identifier));
7
+ while (Option.isSome(scope)) {
8
+ const variable = scope.value.set.get(identifier.name);
9
+ if (Predicate.isNotUndefined(variable)) return Option.some(variable);
10
+ scope = Option.fromNullishOr(scope.value.upper);
9
11
  }
10
- return null;
12
+ return Option.none();
11
13
  }
12
14
  //#endregion
13
15
  export { resolveVariable };
@@ -1,58 +1,54 @@
1
+ import { childNodesAt, isAstNode } from "./_ast-children.js";
2
+ import { ancestors } from "./_ast-ancestors.js";
1
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";
2
7
  //#region src/rules/_anti-slop-type-alias-resolution.ts
3
8
  const environmentsByProgram = /* @__PURE__ */ new WeakMap();
4
9
  function isNode(value) {
5
- return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
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";
6
14
  }
7
15
  function enclosingTypeScope(node) {
8
- let current = node.parent;
9
- while (current !== null) {
10
- if (current.type === "Program" || current.type === "BlockStatement" || current.type === "TSModuleBlock" || current.type === "StaticBlock" || current.type === "SwitchStatement") return current;
11
- current = current.parent;
12
- }
13
- return node;
16
+ return Option.getOrElse(Arr.findFirst(ancestors(node), isTypeScope), () => node);
14
17
  }
15
18
  function declaredTypeBinding(node) {
16
- if (node.type === "TSTypeAliasDeclaration") return {
17
- alias: node,
19
+ if (node.type === "TSTypeAliasDeclaration") return Option.some({
20
+ alias: Option.some(node),
18
21
  name: node.id.name
19
- };
20
- if (node.type === "TSInterfaceDeclaration" || node.type === "TSEnumDeclaration" || node.type === "ClassDeclaration" || node.type === "ClassExpression") return node.id === null ? null : {
21
- alias: null,
22
- name: node.id.name
23
- };
24
- if (node.type === "ImportSpecifier" || node.type === "ImportDefaultSpecifier" || node.type === "ImportNamespaceSpecifier") return {
25
- alias: null,
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(),
26
29
  name: node.local.name
27
- };
28
- return null;
30
+ });
31
+ return Option.none();
29
32
  }
30
33
  function collectTypeBindings(node, visitorKeys, bindingsByName, aliases) {
31
34
  const declared = declaredTypeBinding(node);
32
- if (declared !== null) {
33
- const bindings = bindingsByName.get(declared.name) ?? [];
35
+ if (Option.isSome(declared)) {
36
+ const { alias, name } = declared.value;
37
+ const bindings = bindingsByName.get(name) ?? [];
34
38
  bindings.push({
35
- ...declared,
39
+ alias,
40
+ name,
36
41
  scope: enclosingTypeScope(node)
37
42
  });
38
- bindingsByName.set(declared.name, bindings);
39
- if (declared.alias !== null) aliases.push(declared.alias);
40
- }
41
- const fields = node;
42
- for (const key of visitorKeys[node.type] ?? []) {
43
- const value = fields[key];
44
- if (isNode(value)) {
45
- collectTypeBindings(value, visitorKeys, bindingsByName, aliases);
46
- continue;
47
- }
48
- if (!Array.isArray(value)) continue;
49
- for (const child of value) if (isNode(child)) collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
43
+ bindingsByName.set(name, bindings);
44
+ if (Option.isSome(alias)) aliases.push(alias.value);
50
45
  }
46
+ for (const key of visitorKeys[node.type] ?? []) for (const child of childNodesAt(node, key, isNode)) collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
51
47
  }
52
48
  /** Collect every lexical type alias and competing type binding in a program. */
53
49
  function createTypeAliasEnvironment(program, visitorKeys) {
54
50
  const cached = environmentsByProgram.get(program);
55
- if (cached !== void 0) return cached;
51
+ if (Predicate.isNotUndefined(cached)) return cached;
56
52
  const bindingsByName = /* @__PURE__ */ new Map();
57
53
  const aliases = [];
58
54
  collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
@@ -65,14 +61,7 @@ function createTypeAliasEnvironment(program, visitorKeys) {
65
61
  return environment;
66
62
  }
67
63
  function ancestorDistance(ancestor, node) {
68
- let current = node;
69
- let distance = 0;
70
- while (current !== null) {
71
- if (current === ancestor) return distance;
72
- current = current.parent;
73
- distance += 1;
74
- }
75
- return null;
64
+ return Arr.findFirstIndex([node, ...ancestors(node)], (current) => current === ancestor);
76
65
  }
77
66
  function nearestTypeBindings(name, use, environment) {
78
67
  const candidates = environment.bindingsByName.get(name) ?? [];
@@ -80,61 +69,65 @@ function nearestTypeBindings(name, use, environment) {
80
69
  let nearest = [];
81
70
  for (const candidate of candidates) {
82
71
  const distance = ancestorDistance(candidate.scope, use);
83
- if (distance === null || distance > nearestDistance) continue;
84
- if (distance === nearestDistance) {
72
+ if (Option.isNone(distance) || distance.value > nearestDistance) continue;
73
+ if (distance.value === nearestDistance) {
85
74
  nearest.push(candidate);
86
75
  continue;
87
76
  }
88
- nearestDistance = distance;
77
+ nearestDistance = distance.value;
89
78
  nearest = [candidate];
90
79
  }
91
80
  return nearest;
92
81
  }
93
82
  /** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
94
83
  function visibleTypeAlias(name, use, environment) {
95
- if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return null;
84
+ if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return Option.none();
96
85
  const bindings = nearestTypeBindings(name, use, environment);
97
- return bindings.length === 1 ? bindings[0]?.alias ?? null : null;
86
+ if (bindings.length !== 1) return Option.none();
87
+ return Option.flatMap(Arr.head(bindings), (binding) => binding.alias);
98
88
  }
99
89
  /** Return whether a local declaration shadows a built-in type at this use. */
100
90
  function hasVisibleTypeBinding(name, use, environment) {
101
91
  return lexicalTypeParameterNames(use, environment.visitorKeys).has(name) || nearestTypeBindings(name, use, environment).length > 0;
102
92
  }
103
93
  function typeReferenceName(type) {
104
- return type.typeName.type === "Identifier" ? type.typeName.name : null;
94
+ if (type.typeName.type !== "Identifier") return Option.none();
95
+ return Option.some(type.typeName.name);
105
96
  }
106
97
  function aliasSubstitutions(alias, reference, base) {
107
98
  const parameters = alias.typeParameters?.params ?? [];
108
99
  const typeArguments = reference.typeArguments?.params ?? [];
109
100
  const next = new Map(base);
110
101
  for (const [index, parameter] of parameters.entries()) {
111
- const explicitArgument = typeArguments[index];
112
- const argument = explicitArgument ?? parameter.default;
113
- if (argument === null || argument === void 0) return null;
114
- const argumentSubstitutions = explicitArgument === void 0 ? next : base;
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;
115
107
  next.set(parameter.name.name, {
116
- type: argument,
108
+ type: argument.value,
117
109
  substitutions: new Map(argumentSubstitutions)
118
110
  });
119
111
  }
120
- return next;
112
+ return Option.some(next);
121
113
  }
122
114
  /** Match a type after resolving visible aliases and substituting their type parameters. */
123
115
  function resolvedTypeMatches(type, environment, matcher) {
124
116
  const evaluate = (current, substitutions, resolvingAliases) => {
125
117
  if (current.type === "TSTypeReference") {
126
118
  const name = typeReferenceName(current);
127
- if (name !== null) {
128
- const substitution = substitutions.get(name);
129
- if (substitution !== void 0 && !current.typeArguments?.params.length) return evaluate(substitution.type, substitution.substitutions, resolvingAliases);
130
- const alias = visibleTypeAlias(name, current, environment);
131
- if (alias !== null && !resolvingAliases.has(alias)) {
132
- const nextSubstitutions = aliasSubstitutions(alias, current, substitutions);
133
- if (nextSubstitutions !== null) {
134
- const nextResolving = new Set(resolvingAliases);
135
- nextResolving.add(alias);
136
- return evaluate(alias.typeAnnotation, nextSubstitutions, nextResolving);
137
- }
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);
138
131
  }
139
132
  }
140
133
  }
@@ -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,19 +139,30 @@ 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
+ };
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;
155
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
+ });
156
166
  /** Node types that always stand for the same operator tokens. */
157
167
  const fixedOperators = new Map([
158
168
  ["ForStatement", ["for"]],
@@ -195,35 +205,47 @@ 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
  };
@@ -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
  };