oxlint-plugin-effect 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +26 -14
  2. package/THIRD_PARTY_NOTICES.md +4 -2
  3. package/dist/presets/recommended.d.ts +10 -0
  4. package/dist/presets/recommended.js +11 -1
  5. package/dist/rules/_anti-slop-array-method.js +78 -0
  6. package/dist/rules/_anti-slop-dictionary-types.js +198 -150
  7. package/dist/rules/_anti-slop-function-parameters.js +30 -0
  8. package/dist/rules/_anti-slop-lexical-type-parameters.js +25 -0
  9. package/dist/rules/_anti-slop-reflect-method.js +25 -0
  10. package/dist/rules/_anti-slop-scope.js +15 -0
  11. package/dist/rules/_anti-slop-type-alias-resolution.js +139 -0
  12. package/dist/rules/_ast-ancestors.js +14 -0
  13. package/dist/rules/_ast-children.js +18 -0
  14. package/dist/rules/_call-arguments.js +11 -0
  15. package/dist/rules/_effect-context.js +25 -20
  16. package/dist/rules/_function-metrics.js +94 -75
  17. package/dist/rules/_tagged-control-flow.js +38 -60
  18. package/dist/rules/_test-apis.js +47 -0
  19. package/dist/rules/_test-files.js +8 -0
  20. package/dist/rules/index.d.ts +11 -1
  21. package/dist/rules/index.js +21 -1
  22. package/dist/rules/no-array-filter-map.d.ts +7 -0
  23. package/dist/rules/no-array-filter-map.js +49 -0
  24. package/dist/rules/no-chained-type-assertions.js +7 -4
  25. package/dist/rules/no-conditional-empty-object-spread.d.ts +1 -1
  26. package/dist/rules/no-conditional-empty-object-spread.js +7 -47
  27. package/dist/rules/no-dynamic-imports.js +33 -25
  28. package/dist/rules/no-eager-acquire.d.ts +6 -0
  29. package/dist/rules/no-eager-acquire.js +98 -0
  30. package/dist/rules/no-effect-run-in-tests.d.ts +6 -0
  31. package/dist/rules/no-effect-run-in-tests.js +60 -0
  32. package/dist/rules/no-globals.js +42 -31
  33. package/dist/rules/no-known-value-widening.js +67 -50
  34. package/dist/rules/no-module-level-mutable-state.d.ts +6 -0
  35. package/dist/rules/no-module-level-mutable-state.js +47 -0
  36. package/dist/rules/no-module-mocks.js +37 -48
  37. package/dist/rules/no-new-promise.js +6 -2
  38. package/dist/rules/no-node-builtin-import.js +56 -33
  39. package/dist/rules/no-nullish.js +5 -1
  40. package/dist/rules/no-object-parameters.js +16 -30
  41. package/dist/rules/no-per-call-cache-construction.js +2 -2
  42. package/dist/rules/no-reduce-accumulator-copy.d.ts +7 -0
  43. package/dist/rules/no-reduce-accumulator-copy.js +106 -0
  44. package/dist/rules/no-reflect-apply.d.ts +7 -0
  45. package/dist/rules/no-reflect-apply.js +28 -0
  46. package/dist/rules/no-reflect-get.d.ts +7 -0
  47. package/dist/rules/no-reflect-get.js +28 -0
  48. package/dist/rules/no-run-collect-on-unbounded-stream.js +23 -25
  49. package/dist/rules/no-runtime-typeof.js +21 -4
  50. package/dist/rules/no-sequential-effect-all.js +18 -11
  51. package/dist/rules/no-shape-in-symbol-names.js +15 -5
  52. package/dist/rules/no-silent-catch-all.js +20 -14
  53. package/dist/rules/no-test-globals.d.ts +6 -0
  54. package/dist/rules/no-test-globals.js +147 -0
  55. package/dist/rules/no-test-lifecycle-hooks.js +4 -2
  56. package/dist/rules/no-unbounded-concurrency.js +22 -17
  57. package/dist/rules/no-unbounded-retry.js +47 -62
  58. package/dist/rules/no-unknown-parameters.d.ts +1 -1
  59. package/dist/rules/no-unknown-parameters.js +26 -17
  60. package/dist/rules/no-unknown-returns.d.ts +7 -0
  61. package/dist/rules/no-unknown-returns.js +74 -0
  62. package/dist/rules/no-unknown-type-aliases.js +21 -27
  63. package/dist/rules/no-unsafe-dictionary-type.js +71 -25
  64. package/dist/rules/no-widen-then-assert.js +96 -73
  65. package/dist/rules/prefer-catch-tag.js +29 -37
  66. package/dist/rules/prefer-match-tags-exhaustive.js +35 -47
  67. package/dist/rules/prefer-predicate-is-tagged.js +4 -6
  68. package/dist/rules/prefer-service-of.js +39 -26
  69. package/dist/rules/require-named-effect-fn.js +4 -2
  70. package/dist/rules/require-suppression-reason.d.ts +6 -0
  71. package/dist/rules/require-suppression-reason.js +92 -0
  72. package/dist/vendor/effect-oxlint/Diagnostic.js +1 -1
  73. package/dist/vendor/effect-oxlint/Scope.js +1 -1
  74. package/package.json +3 -2
@@ -2,13 +2,10 @@ 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 { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
6
+ import * as Option from "effect/Option";
5
7
  import * as Effect from "effect/Effect";
6
8
  //#region src/rules/no-unknown-type-aliases.ts
7
- function referencedAliasName(type) {
8
- if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation);
9
- if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
10
- return type.typeArguments === null || type.typeArguments === void 0 || type.typeArguments.params.length === 0 ? type.typeName.name : null;
11
- }
12
9
  /** Ban named aliases that merely conceal TypeScript's unknown top type. */
13
10
  const noUnknownTypeAliases = define({
14
11
  name: "no-unknown-type-aliases",
@@ -19,29 +16,26 @@ const noUnknownTypeAliases = define({
19
16
  }),
20
17
  create: function* () {
21
18
  const context = yield* RuleContext;
22
- const aliases = /* @__PURE__ */ new Map();
23
- const resolvesToUnknown = (type, visited = /* @__PURE__ */ new Set()) => {
24
- if (type.type === "TSUnknownKeyword") return true;
25
- if (type.type === "TSParenthesizedType") return resolvesToUnknown(type.typeAnnotation, visited);
26
- const name = referencedAliasName(type);
27
- if (name === null || visited.has(name)) return false;
28
- const alias = aliases.get(name);
29
- if (alias === void 0 || alias.typeParameters !== null && alias.typeParameters !== void 0) return false;
30
- const nextVisited = new Set(visited);
31
- nextVisited.add(name);
32
- return resolvesToUnknown(alias.typeAnnotation, nextVisited);
33
- };
34
- return { Program: (node) => {
35
- for (const statement of node.body) {
36
- const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
37
- if (declaration?.type === "TSTypeAliasDeclaration") aliases.set(declaration.id.name, declaration);
19
+ let environment = Option.none();
20
+ const resolvesToUnknown = (type) => Option.exists(environment, (present) => resolvedTypeMatches(type, present, (resolved, matches) => {
21
+ if (resolved.type === "TSUnknownKeyword") return true;
22
+ if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
23
+ return resolved.type === "TSUnionType" && resolved.types.some(matches);
24
+ }));
25
+ return {
26
+ Program: (node) => {
27
+ environment = Option.some(createTypeAliasEnvironment(node, context.sourceCode.visitorKeys));
28
+ return Effect.void;
29
+ },
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
+ }));
38
37
  }
39
- return Effect.forEach(aliases.values(), (alias) => resolvesToUnknown(alias.typeAnnotation, new Set([alias.id.name])) ? context.report(fromId({
40
- node: alias.id,
41
- messageId: "unknownAlias",
42
- data: { alias: alias.id.name }
43
- })) : Effect.void, { discard: true });
44
- } };
38
+ };
45
39
  }
46
40
  });
47
41
  //#endregion
@@ -2,37 +2,81 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { ancestors } from "./_ast-ancestors.js";
6
+ import { visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
5
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";
6
11
  import * as Effect from "effect/Effect";
7
12
  //#region src/rules/no-unsafe-dictionary-type.ts
13
+ const typeNodeKinds = new Set([
14
+ "JSDocNonNullableType",
15
+ "JSDocNullableType",
16
+ "JSDocUnknownType",
17
+ "TSAnyKeyword",
18
+ "TSArrayType",
19
+ "TSBigIntKeyword",
20
+ "TSBooleanKeyword",
21
+ "TSConditionalType",
22
+ "TSConstructorType",
23
+ "TSFunctionType",
24
+ "TSImportType",
25
+ "TSIndexedAccessType",
26
+ "TSInferType",
27
+ "TSIntersectionType",
28
+ "TSIntrinsicKeyword",
29
+ "TSLiteralType",
30
+ "TSMappedType",
31
+ "TSNamedTupleMember",
32
+ "TSNeverKeyword",
33
+ "TSNullKeyword",
34
+ "TSNumberKeyword",
35
+ "TSObjectKeyword",
36
+ "TSParenthesizedType",
37
+ "TSStringKeyword",
38
+ "TSSymbolKeyword",
39
+ "TSTemplateLiteralType",
40
+ "TSThisType",
41
+ "TSTupleType",
42
+ "TSTypeLiteral",
43
+ "TSTypeOperator",
44
+ "TSTypePredicate",
45
+ "TSTypeQuery",
46
+ "TSTypeReference",
47
+ "TSUndefinedKeyword",
48
+ "TSUnionType",
49
+ "TSUnknownKeyword",
50
+ "TSVoidKeyword"
51
+ ]);
8
52
  function isTypeNode(node) {
9
- return node.type.startsWith("TS") && node.type !== "TSTypeAnnotation";
53
+ return typeNodeKinds.has(node.type);
10
54
  }
11
55
  function typeReferenceName(type) {
12
- 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);
13
58
  }
14
59
  function isInsideTypeAliasDeclaration(node) {
15
- let current = node.parent;
16
- while (current !== null && current.type !== "Program") {
17
- if (current.type === "TSTypeAliasDeclaration") return true;
18
- current = current.parent;
19
- }
20
- return false;
60
+ return Option.isSome(Arr.findFirst(ancestors(node), (ancestor) => ancestor.type === "TSTypeAliasDeclaration"));
21
61
  }
22
62
  function isPlainAliasConsumerUse(node, environment) {
23
63
  if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
24
- const name = typeReferenceName(node);
25
- return name !== null && environment.aliases.has(name) && !isInsideTypeAliasDeclaration(node);
64
+ return Option.exists(typeReferenceName(node), (name) => Option.isSome(visibleTypeAlias(name, node, environment.typeAliases))) && !isInsideTypeAliasDeclaration(node);
65
+ }
66
+ function isInsideTypeParameterConstraint(node) {
67
+ let child = node;
68
+ for (const parent of ancestors(node)) {
69
+ if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
70
+ child = parent;
71
+ }
72
+ return false;
26
73
  }
27
74
  function shouldReportType(node, environment) {
75
+ if (isInsideTypeParameterConstraint(node)) return false;
28
76
  if (isPlainAliasConsumerUse(node, environment)) return false;
29
- if (classifyUnsafeDictionary(node, environment) === null) return false;
30
- let current = node.parent;
31
- while (current !== null && current.type !== "Program") {
32
- if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
33
- current = current.parent;
34
- }
35
- 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);
36
80
  }
37
81
  /** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
38
82
  const noUnsafeDictionaryType = define({
@@ -44,29 +88,31 @@ const noUnsafeDictionaryType = define({
44
88
  }),
45
89
  create: function* () {
46
90
  const context = yield* RuleContext;
47
- let environment = null;
91
+ let environment = Option.none();
48
92
  const report = (node, value) => context.report(fromId({
49
93
  node,
50
94
  messageId: "unsafeDictionary",
51
95
  data: { value }
52
96
  }));
97
+ const reportUnsafe = (node, unsafe) => Option.match(unsafe, {
98
+ onNone: () => Effect.void,
99
+ onSome: (found) => report(node, found.unsafeValue)
100
+ });
53
101
  const reportIfUnsafe = (node) => {
54
- if (environment === null || !shouldReportType(node, environment)) return Effect.void;
55
- const unsafe = classifyUnsafeDictionary(node, environment);
56
- 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));
57
104
  };
58
105
  return {
59
106
  Program: (node) => {
60
- environment = createTypeEnvironment(node);
107
+ environment = Option.some(createTypeEnvironment(node, context.sourceCode.visitorKeys));
61
108
  return Effect.void;
62
109
  },
63
110
  TSTypeReference: reportIfUnsafe,
64
111
  TSTypeLiteral: reportIfUnsafe,
65
112
  TSMappedType: reportIfUnsafe,
66
113
  TSIndexSignature: (node) => {
67
- if (environment === null || node.typeAnnotation === null || node.parent?.type === "TSTypeLiteral") return Effect.void;
68
- const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
69
- 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));
70
116
  }
71
117
  };
72
118
  }
@@ -2,11 +2,14 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { ancestors } from "./_ast-ancestors.js";
6
+ import * as Arr from "effect/Array";
7
+ import * as Option from "effect/Option";
5
8
  import * as Effect from "effect/Effect";
6
9
  //#region src/rules/no-widen-then-assert.ts
7
10
  /**
8
11
  * Ported from dmmulroy/anti-slop at
9
- * b5d2288db1f00469a1d5f2e3b0e265e5a5676fd0.
12
+ * c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b.
10
13
  */
11
14
  const functionBoundaryTypes = new Set([
12
15
  "ArrowFunctionExpression",
@@ -15,6 +18,7 @@ const functionBoundaryTypes = new Set([
15
18
  "TSDeclareFunction",
16
19
  "TSEmptyBodyFunctionExpression"
17
20
  ]);
21
+ const sameBoundary = Option.makeEquivalence((left, right) => left === right);
18
22
  function unwrapExpressionParentheses(expression) {
19
23
  let current = expression;
20
24
  while (current.type === "ParenthesizedExpression") current = current.expression;
@@ -26,7 +30,20 @@ function unwrapTypeParentheses(type) {
26
30
  return current;
27
31
  }
28
32
  function typeReferenceName(type) {
29
- return type.typeName.type === "Identifier" ? type.typeName.name : null;
33
+ if (type.typeName.type !== "Identifier") return Option.none();
34
+ return Option.some(type.typeName.name);
35
+ }
36
+ function isReferenceNamed(type, name) {
37
+ return Option.contains(typeReferenceName(type), name);
38
+ }
39
+ /** The two type arguments of a reference such as `Record<K, V>`, when it has exactly two. */
40
+ function typeArgumentPair(reference) {
41
+ const parameters = reference.typeArguments?.params ?? [];
42
+ if (parameters.length !== 2) return Option.none();
43
+ return Option.all([Arr.get(parameters, 0), Arr.get(parameters, 1)]);
44
+ }
45
+ function firstTypeArgument(reference) {
46
+ return Arr.head(reference.typeArguments?.params ?? []);
30
47
  }
31
48
  function isUnknownOrAnyType(type) {
32
49
  const unwrapped = unwrapTypeParentheses(type);
@@ -36,42 +53,45 @@ function isBroadRecordKeyType(type) {
36
53
  const unwrapped = unwrapTypeParentheses(type);
37
54
  if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
38
55
  if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
39
- return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
56
+ return unwrapped.type === "TSTypeReference" && isReferenceNamed(unwrapped, "PropertyKey");
57
+ }
58
+ function isBroadRecordReference(reference) {
59
+ if (isReferenceNamed(reference, "Readonly")) return Option.exists(firstTypeArgument(reference), isBroadRecordType);
60
+ if (!isReferenceNamed(reference, "Record")) return false;
61
+ return Option.exists(typeArgumentPair(reference), ([key, value]) => isBroadRecordKeyType(key) && isUnknownOrAnyType(value));
62
+ }
63
+ function isBroadIndexSignature(member) {
64
+ return member.parameters.length === 1 && Option.exists(Arr.head(member.parameters), (parameter) => isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation)) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
40
65
  }
41
66
  function isBroadRecordType(type) {
42
67
  const unwrapped = unwrapTypeParentheses(type);
43
- if (unwrapped.type === "TSTypeReference") {
44
- if (typeReferenceName(unwrapped) === "Readonly") {
45
- const [inner] = unwrapped.typeArguments?.params ?? [];
46
- return inner !== void 0 && isBroadRecordType(inner);
47
- }
48
- if (typeReferenceName(unwrapped) !== "Record") return false;
49
- const parameters = unwrapped.typeArguments?.params ?? [];
50
- return parameters.length === 2 && parameters[0] !== void 0 && parameters[1] !== void 0 && isBroadRecordKeyType(parameters[0]) && isUnknownOrAnyType(parameters[1]);
51
- }
68
+ if (unwrapped.type === "TSTypeReference") return isBroadRecordReference(unwrapped);
52
69
  if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
53
- const [member] = unwrapped.members;
54
- const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
55
- return member?.type === "TSIndexSignature" && member.parameters.length === 1 && parameter !== void 0 && isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
70
+ return Option.exists(Arr.head(unwrapped.members), (member) => member.type === "TSIndexSignature" && isBroadIndexSignature(member));
56
71
  }
57
72
  function broadTypeKind(type) {
58
73
  const unwrapped = unwrapTypeParentheses(type);
59
- if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
60
- if (unwrapped.type === "TSObjectKeyword") return "object";
61
- return isBroadRecordType(unwrapped) ? "record" : null;
74
+ if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return Option.some("top");
75
+ if (unwrapped.type === "TSObjectKeyword") return Option.some("object");
76
+ if (isBroadRecordType(unwrapped)) return Option.some("record");
77
+ return Option.none();
78
+ }
79
+ function isBroadType(type) {
80
+ return Option.isSome(broadTypeKind(type));
62
81
  }
63
82
  function assertedExpression(node) {
64
83
  return unwrapExpressionParentheses(node.expression);
65
84
  }
66
85
  function assertionFromExpression(expression) {
67
86
  const unwrapped = unwrapExpressionParentheses(expression);
68
- return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" ? unwrapped : null;
87
+ if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") return Option.some(unwrapped);
88
+ return Option.none();
69
89
  }
70
90
  function normalizedTypeText(sourceText, type) {
71
91
  return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
72
92
  }
73
93
  function typesHaveSameSyntax(sourceText, left, right) {
74
- return left !== null && normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right));
94
+ return Option.exists(left, (type) => normalizedTypeText(sourceText, unwrapTypeParentheses(type)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right)));
75
95
  }
76
96
  function isDefinitelyObjectType(type) {
77
97
  const unwrapped = unwrapTypeParentheses(type);
@@ -92,74 +112,79 @@ function isDefinitelyNarrowerRecordType(type) {
92
112
  const unwrapped = unwrapTypeParentheses(type);
93
113
  if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
94
114
  if (unwrapped.type !== "TSTypeReference") return false;
95
- if (typeReferenceName(unwrapped) === "Readonly") {
96
- const [inner] = unwrapped.typeArguments?.params ?? [];
97
- return inner !== void 0 && isDefinitelyNarrowerRecordType(inner);
98
- }
99
- if (typeReferenceName(unwrapped) !== "Record") return false;
100
- const parameters = unwrapped.typeArguments?.params ?? [];
101
- return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
115
+ if (isReferenceNamed(unwrapped, "Readonly")) return Option.exists(firstTypeArgument(unwrapped), isDefinitelyNarrowerRecordType);
116
+ if (!isReferenceNamed(unwrapped, "Record")) return false;
117
+ return Option.exists(typeArgumentPair(unwrapped), ([, value]) => !isUnknownOrAnyType(value));
102
118
  }
103
119
  function functionBoundary(node) {
104
- let current = node.parent;
105
- while (current !== null && current.type !== "Program") {
106
- if (functionBoundaryTypes.has(current.type)) return current;
107
- current = current.parent;
108
- }
109
- return null;
120
+ return Arr.findFirst(ancestors(node), (ancestor) => functionBoundaryTypes.has(ancestor.type));
110
121
  }
111
122
  function resolvedVariableForIdentifier(scopes, identifier) {
112
- for (const scope of scopes) {
113
- const reference = scope.references.find((candidate) => candidate.identifier.start === identifier.start && candidate.identifier.end === identifier.end);
114
- if (reference !== void 0) return reference.resolved;
115
- }
116
- return null;
123
+ const reference = Arr.findFirst(scopes, (scope) => Arr.findFirst(scope.references, (candidate) => candidate.identifier.start === identifier.start && candidate.identifier.end === identifier.end));
124
+ return Option.flatMap(reference, (found) => Option.fromNullishOr(found.resolved));
117
125
  }
118
126
  function variableDeclarator(variable) {
119
- for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return definition.node;
120
- return null;
127
+ for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return Option.some(definition.node);
128
+ return Option.none();
129
+ }
130
+ function isConstDeclarator(declarator) {
131
+ return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const";
132
+ }
133
+ function isReassigned(variable) {
134
+ return variable.references.some((reference) => reference.isWrite() && !reference.init);
121
135
  }
122
136
  function knownValueEvidence(expression, scopes, boundary, visitedVariables) {
123
137
  const unwrapped = unwrapExpressionParentheses(expression);
124
138
  if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
125
- if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
126
- return { type: unwrapped.typeAnnotation };
139
+ if (isBroadType(unwrapped.typeAnnotation)) return Option.none();
140
+ return Option.some({ type: Option.some(unwrapped.typeAnnotation) });
127
141
  }
128
- if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return { type: null };
129
- if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return { type: null };
130
- if (unwrapped.type !== "Identifier") return null;
131
- const variable = resolvedVariableForIdentifier(scopes, unwrapped);
132
- if (variable === null || visitedVariables.has(variable)) return null;
133
- const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== void 0);
134
- const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
135
- if (annotation !== void 0 && annotatedIdentifier !== void 0) {
136
- if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) return null;
137
- return { type: annotation };
138
- }
139
- const declarator = variableDeclarator(variable);
140
- if (declarator === null || declarator.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init) || functionBoundary(declarator) !== boundary) return null;
141
- return knownValueEvidence(declarator.init, scopes, boundary, new Set([...visitedVariables, variable]));
142
+ if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return Option.some({ type: Option.none() });
143
+ if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return Option.some({ type: Option.none() });
144
+ if (unwrapped.type === "Identifier") return identifierValueEvidence(unwrapped, scopes, boundary, visitedVariables);
145
+ return Option.none();
146
+ }
147
+ function identifierValueEvidence(expression, scopes, boundary, visitedVariables) {
148
+ return resolvedVariableForIdentifier(scopes, expression).pipe(Option.filter((variable) => !visitedVariables.has(variable)), Option.flatMap((variable) => Option.match(annotatedIdentifier(variable), {
149
+ onSome: ([identifier, annotation]) => annotatedValueEvidence(identifier, annotation, boundary),
150
+ onNone: () => declaredValueEvidence(variable, scopes, boundary, visitedVariables)
151
+ })));
152
+ }
153
+ /** The first declaration identifier of a variable that carries a type annotation. */
154
+ function annotatedIdentifier(variable) {
155
+ return Arr.findFirst(variable.identifiers, (identifier) => Option.map(Option.fromNullishOr(identifier.typeAnnotation), (annotation) => [identifier, annotation.typeAnnotation]));
156
+ }
157
+ function annotatedValueEvidence(identifier, annotation, boundary) {
158
+ if (!sameBoundary(functionBoundary(identifier), boundary) || isBroadType(annotation)) return Option.none();
159
+ return Option.some({ type: Option.some(annotation) });
160
+ }
161
+ function declaredValueEvidence(variable, scopes, boundary, visitedVariables) {
162
+ return variableDeclarator(variable).pipe(Option.filter((declarator) => isConstDeclarator(declarator) && !isReassigned(variable) && sameBoundary(functionBoundary(declarator), boundary)), Option.flatMap((declarator) => Option.fromNullishOr(declarator.init)), Option.flatMap((init) => knownValueEvidence(init, scopes, boundary, new Set([...visitedVariables, variable]))));
142
163
  }
143
164
  function widenedBinding(variable, scopes) {
144
- const declarator = variableDeclarator(variable);
145
- if (declarator === null || declarator.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.id.type !== "Identifier" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
146
- const identifier = variable.identifiers.find((candidate) => candidate.start === declarator.id.start && candidate.end === declarator.id.end);
165
+ return variableDeclarator(variable).pipe(Option.filter((declarator) => isConstDeclarator(declarator) && declarator.id.type === "Identifier" && !isReassigned(variable)), Option.flatMap((declarator) => Option.flatMap(Option.fromNullishOr(declarator.init), (init) => widenedDeclarator(variable, declarator, init, scopes))));
166
+ }
167
+ function widenedDeclarator(variable, declarator, init, scopes) {
168
+ const identifier = Arr.findFirst(variable.identifiers, (candidate) => candidate.start === declarator.id.start && candidate.end === declarator.id.end);
147
169
  const boundary = functionBoundary(declarator);
148
- const declaredType = identifier?.typeAnnotation?.typeAnnotation;
149
- const initializerAssertion = assertionFromExpression(declarator.init);
150
- const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
151
- const broadKind = (declaredType === void 0 ? null : broadTypeKind(declaredType)) ?? initializerBroadKind;
152
- if (broadKind === null) return null;
153
- const evidence = knownValueEvidence(initializerAssertion !== null && initializerBroadKind !== null ? assertedExpression(initializerAssertion) : declarator.init, scopes, boundary, new Set([variable]));
154
- return evidence === null ? null : {
155
- broadKind,
170
+ const declaredType = identifier.pipe(Option.flatMap((found) => Option.fromNullishOr(found.typeAnnotation)), Option.map((annotation) => annotation.typeAnnotation));
171
+ const initializerAssertion = assertionFromExpression(init);
172
+ const initializerBroadKind = Option.flatMap(initializerAssertion, (assertion) => broadTypeKind(assertion.typeAnnotation));
173
+ const declaredBroadKind = Option.flatMap(declaredType, broadTypeKind);
174
+ const broadKind = Option.orElse(declaredBroadKind, () => initializerBroadKind);
175
+ const originalExpression = Option.match(Option.zipLeft(initializerAssertion, initializerBroadKind), {
176
+ onNone: () => init,
177
+ onSome: assertedExpression
178
+ });
179
+ return Option.flatMap(broadKind, (kind) => Option.map(knownValueEvidence(originalExpression, scopes, boundary, new Set([variable])), (evidence) => ({
180
+ broadKind: kind,
156
181
  evidence,
157
182
  declaredAt: declarator.end,
158
183
  boundary
159
- };
184
+ })));
160
185
  }
161
186
  function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
162
- if (broadTypeKind(assertedType) !== null) return false;
187
+ if (isBroadType(assertedType)) return false;
163
188
  if (broadKind === "top") return true;
164
189
  if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
165
190
  if (broadKind === "object") return isDefinitelyObjectType(assertedType);
@@ -179,10 +204,8 @@ const noWidenThenAssert = define({
179
204
  const checkAssertion = (node) => {
180
205
  const expression = assertedExpression(node);
181
206
  if (expression.type !== "Identifier") return Effect.void;
182
- const variable = resolvedVariableForIdentifier(scopes, expression);
183
- if (variable === null) return Effect.void;
184
- const widened = widenedBinding(variable, scopes);
185
- if (widened === null || node.start <= widened.declaredAt || functionBoundary(node) !== widened.boundary || !assertionIsNarrower(context.sourceCode.text, widened.broadKind, widened.evidence, node.typeAnnotation)) return Effect.void;
207
+ const widened = Option.flatMap(resolvedVariableForIdentifier(scopes, expression), (variable) => widenedBinding(variable, scopes));
208
+ if (!Option.exists(widened, (binding) => node.start > binding.declaredAt && sameBoundary(functionBoundary(node), binding.boundary) && assertionIsNarrower(context.sourceCode.text, binding.broadKind, binding.evidence, node.typeAnnotation))) return Effect.void;
186
209
  return context.report(fromId({
187
210
  node,
188
211
  messageId: "widenThenAssert",
@@ -3,51 +3,43 @@ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
5
  import { isEffectCall, tagComparisonsInOr, taggedSwitchSubject } from "./_tagged-control-flow.js";
6
+ import * as Arr from "effect/Array";
7
+ import * as Option from "effect/Option";
8
+ import * as Predicate from "effect/Predicate";
6
9
  import * as Effect from "effect/Effect";
7
10
  //#region src/rules/prefer-catch-tag.ts
8
- const returnedExpression = (fn) => {
9
- if (fn.body === null) return null;
10
- if (fn.body.type !== "BlockStatement") return fn.body;
11
- if (fn.body.body.length !== 1) return null;
12
- const statement = fn.body.body[0];
13
- if (statement?.type !== "ReturnStatement" || statement.argument === null) return null;
14
- return statement.argument;
15
- };
16
- const hasManualTagPredicate = (argument) => {
17
- if (argument?.type !== "ArrowFunctionExpression" && argument?.type !== "FunctionExpression") return false;
18
- const parameter = argument.params[0];
19
- if (parameter?.type !== "Identifier") return false;
20
- const expression = returnedExpression(argument);
21
- if (expression === null) return false;
22
- const comparisons = tagComparisonsInOr(expression);
23
- if (comparisons === null || comparisons.length === 0) return false;
24
- return comparisons.every((comparison) => comparison.subject === parameter.name);
25
- };
26
- const isFunction = (argument) => argument?.type === "ArrowFunctionExpression" || argument?.type === "FunctionExpression";
27
- const hasTagCondition = (node, parameter) => {
28
- const comparisons = tagComparisonsInOr(node.test);
29
- if (comparisons === null || comparisons.length === 0) return false;
30
- return comparisons.every((comparison) => comparison.subject === parameter);
31
- };
11
+ const isFunction = (argument) => argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression";
12
+ const isIdentifier = (parameter) => parameter.type === "Identifier";
13
+ /** The name of a callback's first parameter when it is a plain identifier. */
14
+ const identifierParameter = (fn) => Arr.head(fn.params).pipe(Option.filter(isIdentifier), Option.map((parameter) => parameter.name));
15
+ /** The expression a callback returns when its body is that expression or a lone `return`. */
16
+ const returnedExpression = (fn) => Option.flatMap(Option.fromNullishOr(fn.body), (body) => {
17
+ if (body.type !== "BlockStatement") return Option.some(body);
18
+ const [statement] = body.body;
19
+ if (body.body.length !== 1 || statement?.type !== "ReturnStatement") return Option.none();
20
+ return Option.fromNullishOr(statement.argument);
21
+ });
22
+ /** Whether `expression` is one or more `parameter._tag === "Tag"` checks joined by `||`. */
23
+ const isTagCheckOn = (expression, parameter) => Option.exists(tagComparisonsInOr(expression), (comparisons) => comparisons.every((comparison) => comparison.subject === parameter));
24
+ const hasManualTagPredicate = (argument) => isFunction(argument) && Option.exists(Option.all([identifierParameter(argument), returnedExpression(argument)]), ([parameter, expression]) => isTagCheckOn(expression, parameter));
32
25
  const hasTagSwitch = (node, parameter) => {
33
- if (taggedSwitchSubject(node) !== parameter) return false;
34
- return node.cases.some((switchCase) => switchCase.test?.type === "Literal" && typeof switchCase.test.value === "string");
26
+ if (!Option.contains(taggedSwitchSubject(node), parameter)) return false;
27
+ return node.cases.some((switchCase) => switchCase.test?.type === "Literal" && Predicate.isString(switchCase.test.value));
35
28
  };
36
29
  const hasManualTagDispatch = (argument) => {
37
30
  if (!isFunction(argument) || argument.body?.type !== "BlockStatement") return false;
38
- const parameter = argument.params[0];
39
- if (parameter?.type !== "Identifier") return false;
40
- return argument.body.body.some((statement) => {
41
- if (statement.type === "IfStatement") return hasTagCondition(statement, parameter.name);
42
- if (statement.type === "SwitchStatement") return hasTagSwitch(statement, parameter.name);
31
+ const statements = argument.body.body;
32
+ return Option.exists(identifierParameter(argument), (parameter) => statements.some((statement) => {
33
+ if (statement.type === "IfStatement") return isTagCheckOn(statement.test, parameter);
34
+ if (statement.type === "SwitchStatement") return hasTagSwitch(statement, parameter);
43
35
  return false;
44
- });
36
+ }));
45
37
  };
46
38
  const catchAllHandler = (node) => {
47
- if (!isEffectCall(node, "catchAll")) return void 0;
39
+ if (!isEffectCall(node, "catchAll")) return Option.none();
48
40
  let handlerIndex = 1;
49
41
  if (node.arguments.length === 1) handlerIndex = 0;
50
- return node.arguments[handlerIndex];
42
+ return Arr.get(node.arguments, handlerIndex);
51
43
  };
52
44
  const preferCatchTag = define({
53
45
  name: "prefer-catch-tag",
@@ -61,9 +53,9 @@ const preferCatchTag = define({
61
53
  if (isEffectCall(node, "catchIf")) {
62
54
  let predicateIndex = 1;
63
55
  if (node.arguments.length === 2) predicateIndex = 0;
64
- const predicate = node.arguments[predicateIndex];
65
- if (!hasManualTagPredicate(predicate)) return Effect.void;
66
- } else if (!hasManualTagDispatch(catchAllHandler(node))) return Effect.void;
56
+ const predicate = Arr.get(node.arguments, predicateIndex);
57
+ if (!Option.exists(predicate, hasManualTagPredicate)) return Effect.void;
58
+ } else if (!Option.exists(catchAllHandler(node), hasManualTagDispatch)) return Effect.void;
67
59
  return context.report(make({
68
60
  node,
69
61
  message: "Use Effect.catchTag for one tagged failure or Effect.catchTags for multiple tagged failures."