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
@@ -2,18 +2,24 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { expressionArgument, staticProperties } from "./_call-arguments.js";
5
6
  import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
7
+ import * as Arr from "effect/Array";
8
+ import * as Option from "effect/Option";
6
9
  import * as Effect from "effect/Effect";
7
10
  //#region src/rules/no-unbounded-concurrency.ts
11
+ /** The first `concurrency: "unbounded"` value in an options object literal. */
8
12
  const unboundedConcurrency = (node) => {
9
- if (node?.type !== "ObjectExpression") return void 0;
10
- for (const property of node.properties) {
11
- if (property.type !== "Property" || property.computed) continue;
12
- if (!(property.key.type === "Identifier" && property.key.name === "concurrency" || property.key.type === "Literal" && property.key.value === "concurrency")) continue;
13
- if (property.value.type === "Literal" && property.value.value === "unbounded") return property.value;
14
- }
13
+ if (node.type !== "ObjectExpression") return Option.none();
14
+ return Option.map(Arr.findFirst(staticProperties(node, "concurrency"), (property) => property.value.type === "Literal" && property.value.value === "unbounded"), (property) => property.value);
15
+ };
16
+ const fixedCollection = (node) => node.type === "ArrayExpression" || node.type === "ObjectExpression";
17
+ /** Where `Effect.all` and `Effect.forEach` take their options. */
18
+ const optionsIndex = (callee, effectNamespaces) => {
19
+ if (isStaticMember(callee, effectNamespaces, "forEach")) return Option.some(2);
20
+ if (isStaticMember(callee, effectNamespaces, "all")) return Option.some(1);
21
+ return Option.none();
15
22
  };
16
- const fixedCollection = (node) => node?.type === "ArrayExpression" || node?.type === "ObjectExpression";
17
23
  const noUnboundedConcurrency = define({
18
24
  name: "no-unbounded-concurrency",
19
25
  meta: meta({
@@ -32,16 +38,15 @@ const noUnboundedConcurrency = define({
32
38
  CallExpression: (node) => {
33
39
  if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
34
40
  const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
35
- let optionsIndex;
36
- if (isStaticMember(node.callee, namespaces, "all")) optionsIndex = 1;
37
- if (isStaticMember(node.callee, namespaces, "forEach")) optionsIndex = 2;
38
- if (optionsIndex === void 0 || fixedCollection(node.arguments[0])) return Effect.void;
39
- const unbounded = unboundedConcurrency(node.arguments[optionsIndex]);
40
- if (unbounded === void 0) return Effect.void;
41
- return ctx.report(make({
42
- node: unbounded,
43
- message: "Bound concurrency for a collection that can grow. Use a finite concurrency value."
44
- }));
41
+ if (Option.exists(Arr.head(node.arguments), fixedCollection)) return Effect.void;
42
+ const unbounded = Option.flatMap(optionsIndex(node.callee, namespaces), (index) => Option.flatMap(expressionArgument(node, index), unboundedConcurrency));
43
+ return Option.match(unbounded, {
44
+ onNone: () => Effect.void,
45
+ onSome: (value) => ctx.report(make({
46
+ node: value,
47
+ message: "Bound concurrency for a collection that can grow. Use a finite concurrency value."
48
+ }))
49
+ });
45
50
  }
46
51
  };
47
52
  }
@@ -2,7 +2,10 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { expressionArgument, staticProperties } from "./_call-arguments.js";
5
6
  import { importedNamespaces, isStaticCall, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
7
+ import * as Arr from "effect/Array";
8
+ import * as Option from "effect/Option";
6
9
  import * as Effect from "effect/Effect";
7
10
  //#region src/rules/no-unbounded-retry.ts
8
11
  const unboundedConstructors = new Set([
@@ -12,68 +15,49 @@ const unboundedConstructors = new Set([
12
15
  ]);
13
16
  const boundedCombinators = new Set(["take"]);
14
17
  const staticPropertyName = (node) => {
15
- if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return;
16
- return node.property.name;
17
- };
18
- const hasOwnProperty = (node, name) => {
19
- for (const property of node.properties) {
20
- if (property.type !== "Property" || property.computed) continue;
21
- if (property.key.type === "Identifier" && property.key.name === name) return true;
22
- if (property.key.type === "Literal" && property.key.value === name) return true;
23
- }
24
- return false;
25
- };
26
- const propertyValue = (node, name) => {
27
- for (const property of node.properties) {
28
- if (property.type !== "Property" || property.computed) continue;
29
- if (property.key.type === "Identifier" && property.key.name === name || property.key.type === "Literal" && property.key.value === name) return property.value;
30
- }
18
+ if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return Option.none();
19
+ return Option.some(node.property.name);
31
20
  };
32
21
  const isBoundedOperation = (node, scheduleNamespaces) => {
33
22
  if (node.type !== "CallExpression" || node.callee.type === "Super") return false;
34
- const property = staticPropertyName(node.callee);
35
- if (property === void 0) return false;
36
- if (boundedCombinators.has(property) && isStaticMember(node.callee, scheduleNamespaces, property)) return true;
37
- if (!isStaticMember(node.callee, scheduleNamespaces, "both")) return false;
38
- const bound = node.arguments[0];
39
- return bound !== void 0 && bound.type !== "SpreadElement" && isStaticCall(bound, scheduleNamespaces, "recurs");
23
+ const callee = node.callee;
24
+ if (Option.exists(staticPropertyName(callee), (property) => boundedCombinators.has(property) && isStaticMember(callee, scheduleNamespaces, property))) return true;
25
+ if (!isStaticMember(callee, scheduleNamespaces, "both")) return false;
26
+ return Option.exists(expressionArgument(node, 0), (bound) => isStaticCall(bound, scheduleNamespaces, "recurs"));
40
27
  };
41
28
  const unboundedSchedule = (node, scheduleNamespaces) => {
42
- if (node.type === "MemberExpression" && isStaticMember(node, scheduleNamespaces, "forever")) return node;
43
- if (node.type !== "CallExpression" || node.callee.type === "Super") return void 0;
44
- const property = staticPropertyName(node.callee);
45
- if (property !== void 0 && unboundedConstructors.has(property) && isStaticMember(node.callee, scheduleNamespaces, property)) return node;
46
- if (property !== "pipe" || node.callee.type !== "MemberExpression") return void 0;
47
- const root = unboundedSchedule(node.callee.object, scheduleNamespaces);
48
- if (root === void 0) return void 0;
49
- for (const operation of node.arguments) if (isBoundedOperation(operation, scheduleNamespaces)) return void 0;
50
- return root;
29
+ if (node.type === "MemberExpression" && isStaticMember(node, scheduleNamespaces, "forever")) return Option.some(node);
30
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Option.none();
31
+ const callee = node.callee;
32
+ const property = staticPropertyName(callee);
33
+ if (Option.exists(property, (name) => unboundedConstructors.has(name) && isStaticMember(callee, scheduleNamespaces, name))) return Option.some(node);
34
+ if (!Option.contains(property, "pipe") || callee.type !== "MemberExpression") return Option.none();
35
+ return Option.filter(unboundedSchedule(callee.object, scheduleNamespaces), () => !node.arguments.some((operation) => isBoundedOperation(operation, scheduleNamespaces)));
36
+ };
37
+ /** Arguments a data-last retry call takes; one more means the data-first form. */
38
+ const retryArity = (callee, effectNamespaces, streamNamespaces, httpClientNamespaces) => {
39
+ if (isStaticMember(callee, effectNamespaces, "retryOrElse")) return Option.some(2);
40
+ if (isStaticMember(callee, effectNamespaces, "retry") || isStaticMember(callee, streamNamespaces, "retry") || isStaticMember(callee, httpClientNamespaces, "retryTransient")) return Option.some(1);
41
+ return Option.none();
42
+ };
43
+ /** The policy argument: first in the data-last form, second in the data-first form. */
44
+ const policyArgument = (node, arity) => {
45
+ if (node.arguments.length < arity) return Option.none();
46
+ let policyIndex = 1;
47
+ if (node.arguments.length === arity) policyIndex = 0;
48
+ return Arr.get(node.arguments, policyIndex);
49
+ };
50
+ /** The schedule a policy argument retries on, unless it sets `times`. */
51
+ const policySchedule = (policy) => {
52
+ if (policy.type === "SpreadElement") return Option.none();
53
+ if (policy.type !== "ObjectExpression") return Option.some(policy);
54
+ if (Arr.isReadonlyArrayNonEmpty(staticProperties(policy, "times"))) return Option.none();
55
+ return Option.map(Arr.head(staticProperties(policy, "schedule")), (property) => property.value);
51
56
  };
52
57
  const retryPolicy = (node, effectNamespaces, streamNamespaces, httpClientNamespaces) => {
53
- if (node.callee.type === "Super") return void 0;
54
- let policyIndex;
55
- if (isStaticMember(node.callee, effectNamespaces, "retry")) {
56
- if (node.arguments.length === 1) policyIndex = 0;
57
- if (node.arguments.length >= 2) policyIndex = 1;
58
- }
59
- if (isStaticMember(node.callee, effectNamespaces, "retryOrElse")) {
60
- if (node.arguments.length === 2) policyIndex = 0;
61
- if (node.arguments.length >= 3) policyIndex = 1;
62
- }
63
- if (isStaticMember(node.callee, streamNamespaces, "retry")) {
64
- if (node.arguments.length === 1) policyIndex = 0;
65
- if (node.arguments.length >= 2) policyIndex = 1;
66
- }
67
- if (isStaticMember(node.callee, httpClientNamespaces, "retryTransient")) {
68
- if (node.arguments.length === 1) policyIndex = 0;
69
- if (node.arguments.length >= 2) policyIndex = 1;
70
- }
71
- if (policyIndex === void 0) return void 0;
72
- const policy = node.arguments[policyIndex];
73
- if (policy?.type === "SpreadElement") return void 0;
74
- if (policy?.type !== "ObjectExpression") return policy;
75
- if (hasOwnProperty(policy, "times")) return void 0;
76
- return propertyValue(policy, "schedule");
58
+ if (node.callee.type === "Super") return Option.none();
59
+ const arity = retryArity(node.callee, effectNamespaces, streamNamespaces, httpClientNamespaces);
60
+ return Option.flatMap(Option.flatMap(arity, (count) => policyArgument(node, count)), policySchedule);
77
61
  };
78
62
  const noUnboundedRetry = define({
79
63
  name: "no-unbounded-retry",
@@ -99,13 +83,14 @@ const noUnboundedRetry = define({
99
83
  CallExpression: (node) => {
100
84
  if (node.type !== "CallExpression") return Effect.void;
101
85
  const policy = retryPolicy(node, visibleNamespaces(ctx, node, effectNamespaces), visibleNamespaces(ctx, node, streamNamespaces), visibleNamespaces(ctx, node, httpClientNamespaces));
102
- if (policy === void 0) return Effect.void;
103
- const unbounded = unboundedSchedule(policy, visibleNamespaces(ctx, node, scheduleNamespaces));
104
- if (unbounded === void 0) return Effect.void;
105
- return ctx.report(make({
106
- node: unbounded,
107
- message: "Bound retry attempts or elapsed time. Add times or compose the schedule with Schedule.recurs or Schedule.take."
108
- }));
86
+ const unbounded = Option.flatMap(policy, (schedule) => unboundedSchedule(schedule, visibleNamespaces(ctx, node, scheduleNamespaces)));
87
+ return Option.match(unbounded, {
88
+ onNone: () => Effect.void,
89
+ onSome: (schedule) => ctx.report(make({
90
+ node: schedule,
91
+ message: "Bound retry attempts or elapsed time. Add times or compose the schedule with Schedule.recurs or Schedule.take."
92
+ }))
93
+ });
109
94
  }
110
95
  };
111
96
  }
@@ -3,6 +3,7 @@ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
5
  import { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation } from "./_anti-slop-function-parameters.js";
6
+ import * as Option from "effect/Option";
6
7
  import * as Effect from "effect/Effect";
7
8
  //#region src/rules/no-unknown-parameters.ts
8
9
  function isTypePredicateSubject(owner, parameterName) {
@@ -34,12 +35,13 @@ const noUnknownParameters = define({
34
35
  const context = yield* RuleContext;
35
36
  const checkParameters = (node) => Effect.forEach(node.params, (parameter) => {
36
37
  const annotation = functionParameterTypeAnnotation(parameter);
37
- if (annotation === null || annotation === void 0) return Effect.void;
38
- if (!containsUnknownType(annotation.typeAnnotation)) return Effect.void;
38
+ if (Option.isNone(annotation)) return Effect.void;
39
+ const type = annotation.value.typeAnnotation;
40
+ if (!containsUnknownType(type)) return Effect.void;
39
41
  const name = functionParameterBindingName(parameter, context.sourceCode);
40
42
  if (name === "cause" || isTypePredicateSubject(node, name) || parameter === node.params[0] && isRejectionHandler(node)) return Effect.void;
41
43
  return context.report(fromId({
42
- node: annotation.typeAnnotation,
44
+ node: type,
43
45
  messageId: "unknownParameter",
44
46
  data: { parameter: name }
45
47
  }));
@@ -2,7 +2,11 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { ancestors } from "./_ast-ancestors.js";
5
6
  import { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
7
+ import * as Arr from "effect/Array";
8
+ import * as Option from "effect/Option";
9
+ import * as Predicate from "effect/Predicate";
6
10
  import * as Effect from "effect/Effect";
7
11
  //#region src/rules/no-unknown-returns.ts
8
12
  /**
@@ -13,12 +17,10 @@ import * as Effect from "effect/Effect";
13
17
  */
14
18
  function isInsideTypePattern(node) {
15
19
  let child = node;
16
- let parent = node.parent;
17
- while (parent !== null && parent.type !== "Program") {
20
+ for (const parent of ancestors(node)) {
18
21
  if (parent.type === "TSConditionalType" && parent.extendsType === child) return true;
19
22
  if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
20
23
  child = parent;
21
- parent = parent.parent;
22
24
  }
23
25
  return false;
24
26
  }
@@ -32,18 +34,17 @@ const noUnknownReturns = define({
32
34
  }),
33
35
  create: function* () {
34
36
  const context = yield* RuleContext;
35
- let environment = null;
36
- const resolvesToUnknown = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
37
+ let environment = Option.none();
38
+ const resolvesToUnknown = (type) => Option.exists(environment, (present) => resolvedTypeMatches(type, present, (resolved, matches) => {
37
39
  if (resolved.type === "TSUnknownKeyword") return true;
38
40
  if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
39
41
  if (resolved.type === "TSUnionType") return resolved.types.some(matches);
40
42
  if (resolved.type !== "TSTypeReference" || resolved.typeName.type !== "Identifier" || resolved.typeName.name !== "Promise" && resolved.typeName.name !== "PromiseLike") return false;
41
- const value = resolved.typeArguments?.params[0];
42
- return value !== void 0 && matches(value);
43
- });
43
+ return Option.exists(Arr.head(resolved.typeArguments?.params ?? []), matches);
44
+ }));
44
45
  const checkReturnType = (node) => {
45
46
  const annotation = node.returnType;
46
- if (annotation === null || annotation === void 0) return Effect.void;
47
+ if (Predicate.isNullish(annotation)) return Effect.void;
47
48
  if (!resolvesToUnknown(annotation.typeAnnotation)) return Effect.void;
48
49
  if (isInsideTypePattern(node)) return Effect.void;
49
50
  return context.report(fromId({
@@ -53,7 +54,7 @@ const noUnknownReturns = define({
53
54
  };
54
55
  return {
55
56
  Program: (node) => {
56
- environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
57
+ environment = Option.some(createTypeAliasEnvironment(node, context.sourceCode.visitorKeys));
57
58
  return Effect.void;
58
59
  },
59
60
  ArrowFunctionExpression: checkReturnType,
@@ -3,6 +3,7 @@ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
5
  import { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
6
+ import * as Option from "effect/Option";
6
7
  import * as Effect from "effect/Effect";
7
8
  //#region src/rules/no-unknown-type-aliases.ts
8
9
  /** Ban named aliases that merely conceal TypeScript's unknown top type. */
@@ -15,22 +16,25 @@ const noUnknownTypeAliases = define({
15
16
  }),
16
17
  create: function* () {
17
18
  const context = yield* RuleContext;
18
- let environment = null;
19
- const resolvesToUnknown = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
19
+ let environment = Option.none();
20
+ const resolvesToUnknown = (type) => Option.exists(environment, (present) => resolvedTypeMatches(type, present, (resolved, matches) => {
20
21
  if (resolved.type === "TSUnknownKeyword") return true;
21
22
  if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
22
23
  return resolved.type === "TSUnionType" && resolved.types.some(matches);
23
- });
24
+ }));
24
25
  return {
25
26
  Program: (node) => {
26
- environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
27
+ environment = Option.some(createTypeAliasEnvironment(node, context.sourceCode.visitorKeys));
27
28
  return Effect.void;
28
29
  },
29
- TSTypeAliasDeclaration: (node) => resolvesToUnknown(node.typeAnnotation) ? context.report(fromId({
30
- node: node.id,
31
- messageId: "unknownAlias",
32
- data: { alias: node.id.name }
33
- })) : Effect.void
30
+ TSTypeAliasDeclaration: (node) => {
31
+ if (!resolvesToUnknown(node.typeAnnotation)) return Effect.void;
32
+ return context.report(fromId({
33
+ node: node.id,
34
+ messageId: "unknownAlias",
35
+ data: { alias: node.id.name }
36
+ }));
37
+ }
34
38
  };
35
39
  }
36
40
  });
@@ -2,8 +2,12 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { ancestors } from "./_ast-ancestors.js";
5
6
  import { visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
6
7
  import { classifyUnsafeDictionary, classifyUnsafeDictionaryValue, createTypeEnvironment } from "./_anti-slop-dictionary-types.js";
8
+ import * as Arr from "effect/Array";
9
+ import * as Option from "effect/Option";
10
+ import * as Predicate from "effect/Predicate";
7
11
  import * as Effect from "effect/Effect";
8
12
  //#region src/rules/no-unsafe-dictionary-type.ts
9
13
  const typeNodeKinds = new Set([
@@ -49,41 +53,30 @@ function isTypeNode(node) {
49
53
  return typeNodeKinds.has(node.type);
50
54
  }
51
55
  function typeReferenceName(type) {
52
- return type.typeName.type === "Identifier" ? type.typeName.name : null;
56
+ if (type.typeName.type !== "Identifier") return Option.none();
57
+ return Option.some(type.typeName.name);
53
58
  }
54
59
  function isInsideTypeAliasDeclaration(node) {
55
- let current = node.parent;
56
- while (current !== null && current.type !== "Program") {
57
- if (current.type === "TSTypeAliasDeclaration") return true;
58
- current = current.parent;
59
- }
60
- return false;
60
+ return Option.isSome(Arr.findFirst(ancestors(node), (ancestor) => ancestor.type === "TSTypeAliasDeclaration"));
61
61
  }
62
62
  function isPlainAliasConsumerUse(node, environment) {
63
63
  if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
64
- const name = typeReferenceName(node);
65
- return name !== null && visibleTypeAlias(name, node, environment.typeAliases) !== null && !isInsideTypeAliasDeclaration(node);
64
+ return Option.exists(typeReferenceName(node), (name) => Option.isSome(visibleTypeAlias(name, node, environment.typeAliases))) && !isInsideTypeAliasDeclaration(node);
66
65
  }
67
66
  function isInsideTypeParameterConstraint(node) {
68
67
  let child = node;
69
- let parent = child.parent;
70
- while (parent !== null && parent.type !== "Program") {
68
+ for (const parent of ancestors(node)) {
71
69
  if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
72
70
  child = parent;
73
- parent = child.parent;
74
71
  }
75
72
  return false;
76
73
  }
77
74
  function shouldReportType(node, environment) {
78
75
  if (isInsideTypeParameterConstraint(node)) return false;
79
76
  if (isPlainAliasConsumerUse(node, environment)) return false;
80
- if (classifyUnsafeDictionary(node, environment) === null) return false;
81
- let current = node.parent;
82
- while (current !== null && current.type !== "Program") {
83
- if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
84
- current = current.parent;
85
- }
86
- return true;
77
+ if (Option.isNone(classifyUnsafeDictionary(node, environment))) return false;
78
+ const unsafeAncestor = Arr.findFirst(ancestors(node), (ancestor) => isTypeNode(ancestor) && Option.isSome(classifyUnsafeDictionary(ancestor, environment)));
79
+ return Option.isNone(unsafeAncestor);
87
80
  }
88
81
  /** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
89
82
  const noUnsafeDictionaryType = define({
@@ -95,29 +88,31 @@ const noUnsafeDictionaryType = define({
95
88
  }),
96
89
  create: function* () {
97
90
  const context = yield* RuleContext;
98
- let environment = null;
91
+ let environment = Option.none();
99
92
  const report = (node, value) => context.report(fromId({
100
93
  node,
101
94
  messageId: "unsafeDictionary",
102
95
  data: { value }
103
96
  }));
97
+ const reportUnsafe = (node, unsafe) => Option.match(unsafe, {
98
+ onNone: () => Effect.void,
99
+ onSome: (found) => report(node, found.unsafeValue)
100
+ });
104
101
  const reportIfUnsafe = (node) => {
105
- if (environment === null || !shouldReportType(node, environment)) return Effect.void;
106
- const unsafe = classifyUnsafeDictionary(node, environment);
107
- return unsafe === null ? Effect.void : report(node, unsafe.unsafeValue);
102
+ if (Option.isNone(environment) || !shouldReportType(node, environment.value)) return Effect.void;
103
+ return reportUnsafe(node, classifyUnsafeDictionary(node, environment.value));
108
104
  };
109
105
  return {
110
106
  Program: (node) => {
111
- environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
107
+ environment = Option.some(createTypeEnvironment(node, context.sourceCode.visitorKeys));
112
108
  return Effect.void;
113
109
  },
114
110
  TSTypeReference: reportIfUnsafe,
115
111
  TSTypeLiteral: reportIfUnsafe,
116
112
  TSMappedType: reportIfUnsafe,
117
113
  TSIndexSignature: (node) => {
118
- if (environment === null || node.typeAnnotation === null || node.parent?.type === "TSTypeLiteral") return Effect.void;
119
- const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
120
- return unsafe === null ? Effect.void : report(node, unsafe.unsafeValue);
114
+ if (Option.isNone(environment) || Predicate.isNull(node.typeAnnotation) || node.parent?.type === "TSTypeLiteral") return Effect.void;
115
+ return reportUnsafe(node, classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment.value));
121
116
  }
122
117
  };
123
118
  }