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
package/README.md CHANGED
@@ -35,7 +35,8 @@ If the project uses JSON configuration, copy the exported rule map into `rules`;
35
35
  | `effect/noNewPromise` | Bans new Promise, Promise calls, and Promise static APIs |
36
36
  | `effect/noNewError` | Allows native Error values only as direct arguments to Effect.die, Cause.die, or Exit.die |
37
37
  | `effect/noNullish` | Bans null and undefined; permits `Object.create(null)` for prototype-free dictionaries |
38
- | `effect/noModuleMocks` | Bans Vitest and Jest module mocks and method spies; use Effect service test layers |
38
+ | `effect/noModuleMocks` | Bans Vitest, Jest, and bun:test module mocks, mock functions, and spies; use test layers |
39
+ | `effect/noTestGlobals` | Bans global writes, global stubs, and implicit runner globals in test files |
39
40
  | `effect/noTernary` | Bans conditional expressions while allowing ordinary if statements |
40
41
  | `effect/noManagedRuntimeInEffect` | Keeps ManagedRuntime construction at non-Effect host boundaries |
41
42
  | `effect/noModuleLevelMutableState` | Bans module-level `let` and `var` outside tests; hold shared state in a Layer-owned `Ref` |
@@ -46,6 +46,7 @@ declare const recommended: {
46
46
  readonly "effect/noShapeInSymbolNames": "error";
47
47
  readonly "effect/noSilentCatchAll": "error";
48
48
  readonly "effect/noTernary": "error";
49
+ readonly "effect/noTestGlobals": "error";
49
50
  readonly "effect/noTestLifecycleHooks": "error";
50
51
  readonly "effect/noThrowStatement": "error";
51
52
  readonly "effect/noTryCatch": "error";
@@ -40,6 +40,7 @@ const recommended = {
40
40
  "effect/noShapeInSymbolNames": "error",
41
41
  "effect/noSilentCatchAll": "error",
42
42
  "effect/noTernary": "error",
43
+ "effect/noTestGlobals": "error",
43
44
  "effect/noTestLifecycleHooks": "error",
44
45
  "effect/noThrowStatement": "error",
45
46
  "effect/noTryCatch": "error",
@@ -1,35 +1,40 @@
1
+ import * as Arr from "effect/Array";
2
+ import * as Option from "effect/Option";
3
+ import * as Predicate from "effect/Predicate";
1
4
  //#region src/rules/_anti-slop-array-method.ts
2
5
  /** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
3
6
  function unwrapArrayExpression(node) {
4
7
  while (node.type === "ParenthesizedExpression" || node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression") node = node.expression;
5
8
  return node;
6
9
  }
10
+ function* scopeChain(scope) {
11
+ let current = Option.some(scope);
12
+ while (Option.isSome(current)) {
13
+ yield current.value;
14
+ current = Option.fromNullOr(current.value.upper);
15
+ }
16
+ }
7
17
  /** Resolve a local binding by scope, not by identifier spelling. */
8
18
  function resolveArrayBinding(sourceCode, node) {
9
19
  node = unwrapArrayExpression(node);
10
- if (node.type !== "Identifier") return null;
11
- let scope = sourceCode.getScope(node);
12
- while (scope !== null) {
13
- const variable = scope.set.get(node.name);
14
- if (variable !== void 0) return variable;
15
- scope = scope.upper;
16
- }
17
- return null;
20
+ if (node.type !== "Identifier") return Option.none();
21
+ const name = node.name;
22
+ return Arr.findFirst(scopeChain(sourceCode.getScope(node)), (scope) => Option.fromUndefinedOr(scope.set.get(name)));
18
23
  }
19
24
  /** Read static method names, including computed string literals, without evaluating expressions. */
20
25
  function arrayMethodTarget(node) {
21
26
  node = unwrapArrayExpression(node);
22
- if (node.type !== "MemberExpression") return null;
27
+ if (node.type !== "MemberExpression") return Option.none();
23
28
  const property = node.property;
24
- if (!node.computed && property.type === "Identifier") return {
29
+ if (!node.computed && property.type === "Identifier") return Option.some({
25
30
  name: property.name,
26
31
  object: node.object
27
- };
28
- if (node.computed && property.type === "Literal" && typeof property.value === "string") return {
32
+ });
33
+ if (node.computed && property.type === "Literal" && Predicate.isString(property.value)) return Option.some({
29
34
  name: property.value,
30
35
  object: node.object
31
- };
32
- return null;
36
+ });
37
+ return Option.none();
33
38
  }
34
39
  function isArrayAnnotation(type) {
35
40
  if (type.type === "TSArrayType" || type.type === "TSTupleType") return true;
@@ -41,30 +46,33 @@ function isArrayAnnotation(type) {
41
46
  function isKnownArrayExpression(sourceCode, node, visited = /* @__PURE__ */ new Set()) {
42
47
  node = unwrapArrayExpression(node);
43
48
  if (node.type === "ArrayExpression") return true;
44
- if (node.type === "CallExpression") {
45
- const method = arrayMethodTarget(node.callee);
46
- return method !== null && [
47
- "map",
48
- "filter",
49
- "flatMap",
50
- "slice",
51
- "concat",
52
- "toSorted",
53
- "toReversed",
54
- "toSpliced"
55
- ].includes(method.name) && isKnownArrayExpression(sourceCode, method.object, visited);
56
- }
49
+ if (node.type === "CallExpression") return Option.exists(arrayMethodTarget(node.callee), (method) => [
50
+ "map",
51
+ "filter",
52
+ "flatMap",
53
+ "slice",
54
+ "concat",
55
+ "toSorted",
56
+ "toReversed",
57
+ "toSpliced"
58
+ ].includes(method.name) && isKnownArrayExpression(sourceCode, method.object, visited));
57
59
  if (node.type !== "Identifier") return false;
58
- const variable = resolveArrayBinding(sourceCode, node);
59
- if (variable === null || visited.has(variable)) return false;
60
+ return Option.exists(resolveArrayBinding(sourceCode, node), (variable) => isKnownArrayVariable(sourceCode, variable, visited));
61
+ }
62
+ function isKnownArrayVariable(sourceCode, variable, visited) {
63
+ if (visited.has(variable)) return false;
60
64
  visited.add(variable);
61
65
  if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
62
66
  for (const identifier of variable.identifiers) {
63
67
  const annotation = identifier.typeAnnotation?.typeAnnotation;
64
- if (annotation !== void 0) return isArrayAnnotation(annotation);
68
+ if (Predicate.isNotUndefined(annotation)) return isArrayAnnotation(annotation);
65
69
  }
66
- for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return isKnownArrayExpression(sourceCode, definition.node.init, visited);
67
- return false;
70
+ return Option.exists(constInitializer(variable), (init) => isKnownArrayExpression(sourceCode, init, visited));
71
+ }
72
+ /** The initializer of the first `const name = init` definition of a variable. */
73
+ function constInitializer(variable) {
74
+ for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && Predicate.isNotNull(definition.node.init) && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return Option.some(definition.node.init);
75
+ return Option.none();
68
76
  }
69
77
  //#endregion
70
- export { arrayMethodTarget, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression };
78
+ export { arrayMethodTarget, constInitializer, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression };
@@ -1,4 +1,7 @@
1
1
  import { createTypeAliasEnvironment, hasVisibleTypeBinding, visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
2
+ import * as Arr from "effect/Array";
3
+ import * as Option from "effect/Option";
4
+ import * as Predicate from "effect/Predicate";
2
5
  //#region src/rules/_anti-slop-dictionary-types.ts
3
6
  const BUILT_INS = new Set([
4
7
  "Record",
@@ -17,13 +20,16 @@ const TRANSPARENT_WRAPPERS = new Set([
17
20
  "NonNullable"
18
21
  ]);
19
22
  function declaredStatement(statement) {
20
- return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration ?? null : statement;
23
+ if (statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration") return Option.fromNullishOr(statement.declaration);
24
+ return Option.some(statement);
25
+ }
26
+ function isInterfaceDeclaration(node) {
27
+ return node.type === "TSInterfaceDeclaration";
21
28
  }
22
29
  function createTypeEnvironment(program, visitorKeys) {
23
30
  const interfaces = /* @__PURE__ */ new Map();
24
- for (const statement of program.body) {
25
- const declaration = declaredStatement(statement);
26
- if (declaration?.type !== "TSInterfaceDeclaration") continue;
31
+ const declared = Arr.getSomes(program.body.map(declaredStatement));
32
+ for (const declaration of declared.filter(isInterfaceDeclaration)) {
27
33
  const declarations = interfaces.get(declaration.id.name) ?? [];
28
34
  declarations.push(declaration);
29
35
  interfaces.set(declaration.id.name, declarations);
@@ -34,14 +40,18 @@ function createTypeEnvironment(program, visitorKeys) {
34
40
  };
35
41
  }
36
42
  function typeReferenceName(type) {
37
- return type.typeName.type === "Identifier" ? type.typeName.name : null;
43
+ if (type.typeName.type !== "Identifier") return Option.none();
44
+ return Option.some(type.typeName.name);
45
+ }
46
+ function typeArgument(type, index) {
47
+ return Arr.get(type.typeArguments?.params ?? [], index);
38
48
  }
39
49
  function isBuiltIn(name, use, environment) {
40
50
  return BUILT_INS.has(name) && !hasVisibleTypeBinding(name, use, environment.typeAliases);
41
51
  }
42
52
  function isUnappliedReferenceTo(type, name) {
43
53
  const unwrapped = unwrapTransparentType(type);
44
- return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === name && (unwrapped.typeArguments === null || unwrapped.typeArguments === void 0 || unwrapped.typeArguments.params.length === 0);
54
+ return unwrapped.type === "TSTypeReference" && Option.contains(typeReferenceName(unwrapped), name) && (unwrapped.typeArguments?.params.length ?? 0) === 0;
45
55
  }
46
56
  function unwrapTransparentType(type) {
47
57
  let current = type;
@@ -52,191 +62,224 @@ function isNeverType(type) {
52
62
  return unwrapTransparentType(type).type === "TSNeverKeyword";
53
63
  }
54
64
  function isEffectivelyEmptyMember(member) {
55
- return member.type === "TSPropertySignature" && member.optional === true && member.typeAnnotation !== null && member.typeAnnotation !== void 0 && isNeverType(member.typeAnnotation.typeAnnotation);
65
+ return member.type === "TSPropertySignature" && member.optional === true && Predicate.isNotNullish(member.typeAnnotation) && isNeverType(member.typeAnnotation.typeAnnotation);
56
66
  }
57
67
  function isEffectivelyEmptyTypeLiteral(type) {
58
68
  return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
59
69
  }
60
70
  function isEffectivelyEmptyInterface(declarations) {
61
71
  if (declarations.length !== 1) return false;
62
- const [type] = declarations;
63
- return type !== void 0 && type.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember));
72
+ return Option.exists(Arr.head(declarations), (type) => type.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember)));
64
73
  }
65
74
  function resolvedSubstitutionArgument(type, base, resolving = /* @__PURE__ */ new Set()) {
66
75
  const unwrapped = unwrapTransparentType(type);
67
76
  if (unwrapped.type !== "TSTypeReference") return type;
68
- const name = typeReferenceName(unwrapped);
69
- if (name === null || resolving.has(name)) return type;
77
+ const referenceName = typeReferenceName(unwrapped);
78
+ if (Option.isNone(referenceName) || resolving.has(referenceName.value)) return type;
79
+ const name = referenceName.value;
70
80
  const substitution = base.get(name);
71
- if (substitution === void 0) return type;
81
+ if (Predicate.isUndefined(substitution)) return type;
72
82
  const nextResolving = new Set(resolving);
73
83
  nextResolving.add(name);
74
84
  return resolvedSubstitutionArgument(substitution, base, nextResolving);
75
85
  }
76
86
  function aliasSubstitution(alias, type, base) {
77
87
  const parameters = alias.typeParameters?.params ?? [];
78
- const typeArguments = type.typeArguments?.params ?? [];
79
88
  const next = new Map(base);
80
89
  for (const [index, parameter] of parameters.entries()) {
81
- const argument = typeArguments[index] ?? parameter.default;
82
- if (argument === null || argument === void 0) return null;
83
- next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next));
90
+ const argument = Option.orElse(typeArgument(type, index), () => Option.fromNullishOr(parameter.default));
91
+ if (Option.isNone(argument)) return Option.none();
92
+ next.set(parameter.name.name, resolvedSubstitutionArgument(argument.value, next));
84
93
  }
85
- return next;
94
+ return Option.some(next);
95
+ }
96
+ /**
97
+ * The visible alias named by a reference, expanded with its substitutions, unless it is
98
+ * already resolving. (Local change: upstream repeats this step in each reference resolver.)
99
+ */
100
+ function aliasExpansion(name, reference, environment, substitutions, resolvingAliases) {
101
+ const alias = visibleTypeAlias(name, reference, environment.typeAliases);
102
+ if (Option.isNone(alias) || resolvingAliases.has(name)) return Option.none();
103
+ return Option.map(aliasSubstitution(alias.value, reference, substitutions), (next) => {
104
+ const nextResolving = new Set(resolvingAliases);
105
+ nextResolving.add(name);
106
+ return {
107
+ type: alias.value.typeAnnotation,
108
+ substitutions: next,
109
+ resolvingAliases: nextResolving
110
+ };
111
+ });
86
112
  }
87
113
  function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
88
114
  const unwrapped = unwrapTransparentType(type);
89
- if (unwrapped.type === "TSUnknownKeyword") return "unknown";
90
- if (unwrapped.type === "TSAnyKeyword") return "any";
91
- if (unwrapped.type === "TSObjectKeyword") return "object";
92
- if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return "empty-object";
93
- if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases) !== null) ? "union" : null;
115
+ if (unwrapped.type === "TSUnknownKeyword") return Option.some("unknown");
116
+ if (unwrapped.type === "TSAnyKeyword") return Option.some("any");
117
+ if (unwrapped.type === "TSObjectKeyword") return Option.some("object");
118
+ if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return Option.some("empty-object");
119
+ if (unwrapped.type === "TSUnionType") {
120
+ if (unwrapped.types.some((member) => Option.isSome(unsafeDirectValue(member, environment, substitutions, resolvingAliases)))) return Option.some("union");
121
+ return Option.none();
122
+ }
94
123
  if (unwrapped.type === "TSIntersectionType") {
95
124
  const unsafeMembers = unwrapped.types.map((member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases));
96
- if (unsafeMembers.includes("any")) return "any";
97
- return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null) ? unsafeMembers[0] ?? null : null;
98
- }
99
- if (unwrapped.type !== "TSTypeReference") return null;
100
- const name = typeReferenceName(unwrapped);
101
- if (name === null) return null;
102
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
103
- const wrapped = unwrapped.typeArguments?.params[0];
104
- return wrapped === void 0 ? null : unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases);
125
+ if (Arr.getSomes(unsafeMembers).includes("any")) return Option.some("any");
126
+ return Option.flatMap(Option.all(unsafeMembers), Arr.head);
105
127
  }
128
+ if (unwrapped.type !== "TSTypeReference") return Option.none();
129
+ return unsafeReferenceValue(unwrapped, environment, substitutions, resolvingAliases);
130
+ }
131
+ function unsafeReferenceValue(unwrapped, environment, substitutions, resolvingAliases) {
132
+ const referenceName = typeReferenceName(unwrapped);
133
+ if (Option.isNone(referenceName)) return Option.none();
134
+ const name = referenceName.value;
135
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) return Option.flatMap(typeArgument(unwrapped, 0), (wrapped) => unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases));
106
136
  const substitution = substitutions.get(name);
107
- if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
137
+ if (Predicate.isNotUndefined(substitution)) {
138
+ if (isUnappliedReferenceTo(substitution, name)) return Option.none();
139
+ return unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
140
+ }
108
141
  const interfaceDeclarations = environment.interfaces.get(name);
109
- if (interfaceDeclarations !== void 0) return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
110
- const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
111
- if (alias === null || resolvingAliases.has(name)) return null;
112
- const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
113
- if (nextSubstitutions === null) return null;
114
- const nextResolving = new Set(resolvingAliases);
115
- nextResolving.add(name);
116
- return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
142
+ if (Predicate.isNotUndefined(interfaceDeclarations)) {
143
+ if (isEffectivelyEmptyInterface(interfaceDeclarations)) return Option.some("empty-object");
144
+ return Option.none();
145
+ }
146
+ return Option.flatMap(aliasExpansion(name, unwrapped, environment, substitutions, resolvingAliases), (expansion) => unsafeDirectValue(expansion.type, environment, expansion.substitutions, expansion.resolvingAliases));
117
147
  }
118
148
  function dictionaryValueTypes(type, environment, substitutions, resolvingAliases) {
119
149
  const unwrapped = unwrapTransparentType(type);
120
- if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) => member.type === "TSIndexSignature" && member.typeAnnotation !== null ? [{
121
- type: member.typeAnnotation.typeAnnotation,
122
- substitutions
123
- }] : []);
124
- if (unwrapped.type === "TSMappedType") return unwrapped.typeAnnotation === null ? [] : [{
125
- type: unwrapped.typeAnnotation,
126
- substitutions
127
- }];
128
- if (unwrapped.type !== "TSTypeReference") return [];
129
- const name = typeReferenceName(unwrapped);
130
- if (name === null) return [];
131
- const substitution = substitutions.get(name);
132
- if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
133
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
134
- const wrapped = unwrapped.typeArguments?.params[0];
135
- return wrapped === void 0 ? [] : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
136
- }
137
- if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
138
- const value = unwrapped.typeArguments?.params[1] ?? null;
139
- return value === null ? [] : [{
140
- type: value,
150
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) => {
151
+ if (member.type !== "TSIndexSignature" || Predicate.isNull(member.typeAnnotation)) return [];
152
+ return [{
153
+ type: member.typeAnnotation.typeAnnotation,
154
+ substitutions
155
+ }];
156
+ });
157
+ if (unwrapped.type === "TSMappedType") {
158
+ if (Predicate.isNull(unwrapped.typeAnnotation)) return [];
159
+ return [{
160
+ type: unwrapped.typeAnnotation,
141
161
  substitutions
142
162
  }];
143
163
  }
144
- if ((name === "Pick" || name === "Omit") && isBuiltIn(name, unwrapped, environment)) {
145
- const source = unwrapped.typeArguments?.params[0];
146
- return source === void 0 ? [] : dictionaryValueTypes(source, environment, substitutions, resolvingAliases);
164
+ if (unwrapped.type !== "TSTypeReference") return [];
165
+ return referenceValueTypes(unwrapped, environment, substitutions, resolvingAliases);
166
+ }
167
+ function referenceValueTypes(unwrapped, environment, substitutions, resolvingAliases) {
168
+ const referenceName = typeReferenceName(unwrapped);
169
+ if (Option.isNone(referenceName)) return [];
170
+ const name = referenceName.value;
171
+ const substitution = substitutions.get(name);
172
+ if (Predicate.isNotUndefined(substitution)) {
173
+ if (isUnappliedReferenceTo(substitution, name)) return [];
174
+ return dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
147
175
  }
148
- const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
149
- if (alias === null || resolvingAliases.has(name)) return [];
150
- const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
151
- if (nextSubstitutions === null) return [];
152
- const nextResolving = new Set(resolvingAliases);
153
- nextResolving.add(name);
154
- return dictionaryValueTypes(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
176
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) return Option.match(typeArgument(unwrapped, 0), {
177
+ onNone: () => [],
178
+ onSome: (wrapped) => dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases)
179
+ });
180
+ if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return Option.toArray(Option.map(typeArgument(unwrapped, 1), (value) => ({
181
+ type: value,
182
+ substitutions
183
+ })));
184
+ if ((name === "Pick" || name === "Omit") && isBuiltIn(name, unwrapped, environment)) return Option.match(typeArgument(unwrapped, 0), {
185
+ onNone: () => [],
186
+ onSome: (source) => dictionaryValueTypes(source, environment, substitutions, resolvingAliases)
187
+ });
188
+ return Option.match(aliasExpansion(name, unwrapped, environment, substitutions, resolvingAliases), {
189
+ onNone: () => [],
190
+ onSome: (expansion) => dictionaryValueTypes(expansion.type, environment, expansion.substitutions, expansion.resolvingAliases)
191
+ });
155
192
  }
193
+ const unsafeDictionary = (unsafeValue) => ({
194
+ kind: "unsafe-dictionary",
195
+ unsafeValue
196
+ });
156
197
  function classifyUnsafeDictionaryValue(valueType, environment) {
157
- const unsafeValue = unsafeDirectValue(valueType, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
158
- return unsafeValue === null ? null : {
159
- kind: "unsafe-dictionary",
160
- unsafeValue
161
- };
198
+ return Option.map(unsafeDirectValue(valueType, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set()), unsafeDictionary);
162
199
  }
163
200
  function classifyUnsafeDictionary(type, environment) {
164
- for (const valueType of dictionaryValueTypes(type, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set())) {
165
- const unsafeValue = unsafeDirectValue(valueType.type, environment, valueType.substitutions, /* @__PURE__ */ new Set());
166
- if (unsafeValue !== null) return {
167
- kind: "unsafe-dictionary",
168
- unsafeValue
169
- };
170
- }
171
- return null;
201
+ return Arr.findFirst(dictionaryValueTypes(type, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set()), (valueType) => Option.map(unsafeDirectValue(valueType.type, environment, valueType.substitutions, /* @__PURE__ */ new Set()), unsafeDictionary));
172
202
  }
173
203
  function classifyWideningTarget(type, environment) {
174
204
  const unwrapped = unwrapTransparentType(type);
175
- if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
176
- if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
177
- if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : unwrapped.members.length > 0 ? { kind: "anonymous object" } : null;
178
- if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
179
- if (unwrapped.type !== "TSTypeReference") return null;
180
- const name = typeReferenceName(unwrapped);
181
- if (name === null) return null;
182
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
183
- const wrapped = unwrapped.typeArguments?.params[0];
184
- return wrapped === void 0 ? null : classifyWideningTarget(wrapped, environment);
205
+ if (unwrapped.type === "TSUnknownKeyword") return Option.some({ kind: "unknown" });
206
+ if (unwrapped.type === "TSObjectKeyword") return Option.some({ kind: "object" });
207
+ if (unwrapped.type === "TSTypeLiteral") {
208
+ if (unwrapped.members.some((member) => member.type === "TSIndexSignature")) return Option.some({ kind: "open dictionary" });
209
+ if (unwrapped.members.length > 0) return Option.some({ kind: "anonymous object" });
210
+ return Option.none();
185
211
  }
186
- if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return hasBroadRecordKey(unwrapped, environment, /* @__PURE__ */ new Map()) ? { kind: "open dictionary" } : null;
187
- const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
188
- if (alias === null) return null;
189
- if ((alias.typeParameters?.params.length ?? 0) > 0) {
190
- const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
191
- return (substitutions === null ? null : classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name])))?.kind === "open dictionary" ? { kind: "generic container" } : null;
212
+ if (unwrapped.type === "TSMappedType") return Option.some({ kind: "open dictionary" });
213
+ if (unwrapped.type !== "TSTypeReference") return Option.none();
214
+ const referenceName = typeReferenceName(unwrapped);
215
+ if (Option.isNone(referenceName)) return Option.none();
216
+ const name = referenceName.value;
217
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) return Option.flatMap(typeArgument(unwrapped, 0), (wrapped) => classifyWideningTarget(wrapped, environment));
218
+ if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
219
+ if (hasBroadRecordKey(unwrapped, environment, /* @__PURE__ */ new Map())) return Option.some({ kind: "open dictionary" });
220
+ return Option.none();
192
221
  }
193
- const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
194
- if (substitutions === null) return null;
195
- return classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name]));
222
+ return Option.flatMap(visibleTypeAlias(name, unwrapped, environment.typeAliases), (alias) => classifyWideningAlias(alias, unwrapped, name, environment));
223
+ }
224
+ function classifyWideningAlias(alias, unwrapped, name, environment) {
225
+ const resolved = Option.flatMap(aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map()), (substitutions) => classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name])));
226
+ if ((alias.typeParameters?.params.length ?? 0) === 0) return resolved;
227
+ if (Option.exists(resolved, (target) => target.kind === "open dictionary")) return Option.some({ kind: "generic container" });
228
+ return Option.none();
196
229
  }
197
230
  function hasBroadRecordKey(type, environment, substitutions) {
198
- const key = type.typeArguments?.params[0];
199
- return key === void 0 || isBroadMappedKey(key, environment, substitutions);
231
+ return Option.match(typeArgument(type, 0), {
232
+ onNone: () => true,
233
+ onSome: (key) => isBroadMappedKey(key, environment, substitutions)
234
+ });
200
235
  }
201
236
  function isBroadMappedKey(type, environment, substitutions, visitedAliases = /* @__PURE__ */ new Set()) {
202
237
  const unwrapped = unwrapTransparentType(type);
203
238
  if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
204
239
  if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => isBroadMappedKey(member, environment, substitutions, visitedAliases));
205
240
  if (unwrapped.type !== "TSTypeReference") return false;
206
- const name = typeReferenceName(unwrapped);
207
- if (name === null) return false;
241
+ const referenceName = typeReferenceName(unwrapped);
242
+ if (Option.isNone(referenceName)) return false;
243
+ const name = referenceName.value;
208
244
  const substitution = substitutions.get(name);
209
- if (substitution !== void 0 && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
245
+ if (Predicate.isNotUndefined(substitution) && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
210
246
  if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true;
211
247
  const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
212
- if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0 || visitedAliases.has(name)) return false;
248
+ if (Option.isNone(alias) || (alias.value.typeParameters?.params.length ?? 0) > 0 || visitedAliases.has(name)) return false;
213
249
  const nextVisited = new Set(visitedAliases);
214
250
  nextVisited.add(name);
215
- return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
251
+ return isBroadMappedKey(alias.value.typeAnnotation, environment, substitutions, nextVisited);
216
252
  }
217
253
  function classifyAliasBroadTarget(type, environment, substitutions, resolvingAliases) {
218
254
  const unwrapped = unwrapTransparentType(type);
219
- if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
220
- if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
221
- if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : null;
222
- if (unwrapped.type === "TSMappedType") return isBroadMappedKey(unwrapped.constraint, environment, substitutions) ? { kind: "open dictionary" } : null;
223
- if (unwrapped.type !== "TSTypeReference") return null;
224
- const name = typeReferenceName(unwrapped);
225
- if (name === null) return null;
255
+ if (unwrapped.type === "TSUnknownKeyword") return Option.some({ kind: "unknown" });
256
+ if (unwrapped.type === "TSObjectKeyword") return Option.some({ kind: "object" });
257
+ if (unwrapped.type === "TSTypeLiteral") {
258
+ if (unwrapped.members.some((member) => member.type === "TSIndexSignature")) return Option.some({ kind: "open dictionary" });
259
+ return Option.none();
260
+ }
261
+ if (unwrapped.type === "TSMappedType") {
262
+ if (isBroadMappedKey(unwrapped.constraint, environment, substitutions)) return Option.some({ kind: "open dictionary" });
263
+ return Option.none();
264
+ }
265
+ if (unwrapped.type !== "TSTypeReference") return Option.none();
266
+ return classifyReferenceBroadTarget(unwrapped, environment, substitutions, resolvingAliases);
267
+ }
268
+ function classifyReferenceBroadTarget(unwrapped, environment, substitutions, resolvingAliases) {
269
+ const referenceName = typeReferenceName(unwrapped);
270
+ if (Option.isNone(referenceName)) return Option.none();
271
+ const name = referenceName.value;
226
272
  const substitution = substitutions.get(name);
227
- if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
228
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
229
- const wrapped = unwrapped.typeArguments?.params[0];
230
- return wrapped === void 0 ? null : classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases);
273
+ if (Predicate.isNotUndefined(substitution)) {
274
+ if (isUnappliedReferenceTo(substitution, name)) return Option.none();
275
+ return classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
231
276
  }
232
- if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return hasBroadRecordKey(unwrapped, environment, substitutions) ? { kind: "open dictionary" } : null;
233
- const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
234
- if (alias === null || resolvingAliases.has(name)) return null;
235
- const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
236
- if (nextSubstitutions === null) return null;
237
- const nextResolving = new Set(resolvingAliases);
238
- nextResolving.add(name);
239
- return classifyAliasBroadTarget(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
277
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) return Option.flatMap(typeArgument(unwrapped, 0), (wrapped) => classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases));
278
+ if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
279
+ if (hasBroadRecordKey(unwrapped, environment, substitutions)) return Option.some({ kind: "open dictionary" });
280
+ return Option.none();
281
+ }
282
+ return Option.flatMap(aliasExpansion(name, unwrapped, environment, substitutions, resolvingAliases), (expansion) => classifyAliasBroadTarget(expansion.type, environment, expansion.substitutions, expansion.resolvingAliases));
240
283
  }
241
284
  function isKnownEvidenceExpression(expression) {
242
285
  let current = expression;
@@ -1,3 +1,4 @@
1
+ import * as Option from "effect/Option";
1
2
  //#region src/rules/_anti-slop-function-parameters.ts
2
3
  /** Return whether a type is or contains TypeScript's absorbing unknown top type. */
3
4
  function containsUnknownType(type) {
@@ -8,20 +9,22 @@ function containsUnknownType(type) {
8
9
  /** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */
9
10
  function functionParameterTypeAnnotation(parameter) {
10
11
  if (parameter.type === "TSParameterProperty") return functionParameterTypeAnnotation(parameter.parameter);
11
- if (parameter.type === "RestElement") return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.argument);
12
- if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.left);
13
- return parameter.typeAnnotation;
12
+ if (parameter.type === "RestElement") return Option.orElse(Option.fromNullishOr(parameter.typeAnnotation), () => functionParameterTypeAnnotation(parameter.argument));
13
+ if (parameter.type === "AssignmentPattern") return Option.orElse(Option.fromNullishOr(parameter.typeAnnotation), () => functionParameterTypeAnnotation(parameter.left));
14
+ return Option.fromNullishOr(parameter.typeAnnotation);
14
15
  }
15
16
  /** Return only a function parameter's local binding, excluding its annotation and default value. */
16
17
  function functionParameterBindingName(parameter, sourceCode) {
17
18
  if (parameter.type === "TSParameterProperty") return functionParameterBindingName(parameter.parameter, sourceCode);
18
19
  if (parameter.type === "AssignmentPattern") return functionParameterBindingName(parameter.left, sourceCode);
19
20
  if (parameter.type === "RestElement") return functionParameterBindingName(parameter.argument, sourceCode);
20
- const annotation = functionParameterTypeAnnotation(parameter);
21
- const annotationOffset = annotation === null || annotation === void 0 ? void 0 : annotation.start - parameter.start;
21
+ const annotationOffset = Option.map(functionParameterTypeAnnotation(parameter), (annotation) => annotation.start - parameter.start);
22
22
  if (parameter.type === "Identifier") return parameter.name;
23
23
  const sourceText = sourceCode.getText(parameter);
24
- return annotationOffset === void 0 ? sourceText : sourceText.slice(0, annotationOffset).trimEnd();
24
+ return Option.match(annotationOffset, {
25
+ onNone: () => sourceText,
26
+ onSome: (offset) => sourceText.slice(0, offset).trimEnd()
27
+ });
25
28
  }
26
29
  //#endregion
27
30
  export { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation };
@@ -1,31 +1,23 @@
1
+ import { childNodesAt, isAstNode } from "./_ast-children.js";
2
+ import { ancestors } from "./_ast-ancestors.js";
1
3
  //#region src/rules/_anti-slop-lexical-type-parameters.ts
2
4
  function isNode(value) {
3
- return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
5
+ return isAstNode(value);
4
6
  }
5
7
  function collectInferTypeParameterNames(node, visitorKeys, names) {
6
8
  if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
7
- const record = node;
8
- for (const key of visitorKeys[node.type] ?? []) {
9
- const value = record[key];
10
- if (isNode(value)) {
11
- collectInferTypeParameterNames(value, visitorKeys, names);
12
- continue;
13
- }
14
- if (!Array.isArray(value)) continue;
15
- for (const child of value) if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names);
16
- }
9
+ for (const key of visitorKeys[node.type] ?? []) for (const child of childNodesAt(node, key, isNode)) collectInferTypeParameterNames(child, visitorKeys, names);
17
10
  }
18
11
  /** Collect type binders that are in scope at a node and can shadow module aliases. */
19
12
  function lexicalTypeParameterNames(node, visitorKeys) {
20
13
  const names = /* @__PURE__ */ new Set();
21
14
  let descendant = node;
22
- let current = node;
23
- while (current !== null && current.type !== "Program") {
15
+ for (const current of [node, ...ancestors(node)]) {
16
+ if (current.type === "Program") break;
24
17
  if ("typeParameters" in current) for (const parameter of current.typeParameters?.params ?? []) names.add(parameter.name.name);
25
18
  if (current.type === "TSMappedType" && (descendant === current.nameType || descendant === current.typeAnnotation)) names.add(current.key.name);
26
19
  if (current.type === "TSConditionalType" && descendant === current.trueType) collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
27
20
  descendant = current;
28
- current = current.parent;
29
21
  }
30
22
  return names;
31
23
  }