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
@@ -1,3 +1,7 @@
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";
1
5
  //#region src/rules/_anti-slop-dictionary-types.ts
2
6
  const BUILT_INS = new Set([
3
7
  "Record",
@@ -16,54 +20,38 @@ const TRANSPARENT_WRAPPERS = new Set([
16
20
  "NonNullable"
17
21
  ]);
18
22
  function declaredStatement(statement) {
19
- 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);
20
25
  }
21
- function createTypeEnvironment(program) {
22
- const aliases = /* @__PURE__ */ new Map();
26
+ function isInterfaceDeclaration(node) {
27
+ return node.type === "TSInterfaceDeclaration";
28
+ }
29
+ function createTypeEnvironment(program, visitorKeys) {
23
30
  const interfaces = /* @__PURE__ */ new Map();
24
- const shadowedBuiltIns = /* @__PURE__ */ new Set();
25
- for (const statement of program.body) {
26
- const declaration = declaredStatement(statement);
27
- if (declaration?.type === "ImportDeclaration") {
28
- for (const specifier of declaration.specifiers) if (BUILT_INS.has(specifier.local.name)) shadowedBuiltIns.add(specifier.local.name);
29
- continue;
30
- }
31
- if (declaration?.type === "TSTypeAliasDeclaration") {
32
- if (aliases.get(declaration.id.name) === void 0) aliases.set(declaration.id.name, declaration);
33
- else shadowedBuiltIns.add(declaration.id.name);
34
- if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
35
- continue;
36
- }
37
- if (declaration?.type === "TSInterfaceDeclaration") {
38
- const declarations = interfaces.get(declaration.id.name) ?? [];
39
- declarations.push(declaration);
40
- interfaces.set(declaration.id.name, declarations);
41
- if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
42
- continue;
43
- }
44
- if (declaration?.type === "TSEnumDeclaration") {
45
- if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
46
- continue;
47
- }
48
- if ((declaration?.type === "ClassDeclaration" || declaration?.type === "FunctionDeclaration") && declaration.id !== null) {
49
- if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
50
- }
31
+ const declared = Arr.getSomes(program.body.map(declaredStatement));
32
+ for (const declaration of declared.filter(isInterfaceDeclaration)) {
33
+ const declarations = interfaces.get(declaration.id.name) ?? [];
34
+ declarations.push(declaration);
35
+ interfaces.set(declaration.id.name, declarations);
51
36
  }
52
37
  return {
53
- aliases,
54
38
  interfaces,
55
- shadowedBuiltIns
39
+ typeAliases: createTypeAliasEnvironment(program, visitorKeys)
56
40
  };
57
41
  }
58
42
  function typeReferenceName(type) {
59
- 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);
60
48
  }
61
- function isBuiltIn(name, environment) {
62
- return BUILT_INS.has(name) && !environment.shadowedBuiltIns.has(name);
49
+ function isBuiltIn(name, use, environment) {
50
+ return BUILT_INS.has(name) && !hasVisibleTypeBinding(name, use, environment.typeAliases);
63
51
  }
64
52
  function isUnappliedReferenceTo(type, name) {
65
53
  const unwrapped = unwrapTransparentType(type);
66
- 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;
67
55
  }
68
56
  function unwrapTransparentType(type) {
69
57
  let current = type;
@@ -74,164 +62,224 @@ function isNeverType(type) {
74
62
  return unwrapTransparentType(type).type === "TSNeverKeyword";
75
63
  }
76
64
  function isEffectivelyEmptyMember(member) {
77
- 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);
78
66
  }
79
67
  function isEffectivelyEmptyTypeLiteral(type) {
80
68
  return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
81
69
  }
82
70
  function isEffectivelyEmptyInterface(declarations) {
83
71
  if (declarations.length !== 1) return false;
84
- const [type] = declarations;
85
- 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)));
86
73
  }
87
- function resolvedSubstitutionArgument(type, base) {
74
+ function resolvedSubstitutionArgument(type, base, resolving = /* @__PURE__ */ new Set()) {
88
75
  const unwrapped = unwrapTransparentType(type);
89
76
  if (unwrapped.type !== "TSTypeReference") return type;
90
- const name = typeReferenceName(unwrapped);
91
- if (name === null) return type;
77
+ const referenceName = typeReferenceName(unwrapped);
78
+ if (Option.isNone(referenceName) || resolving.has(referenceName.value)) return type;
79
+ const name = referenceName.value;
92
80
  const substitution = base.get(name);
93
- return substitution === void 0 ? type : resolvedSubstitutionArgument(substitution, base);
81
+ if (Predicate.isUndefined(substitution)) return type;
82
+ const nextResolving = new Set(resolving);
83
+ nextResolving.add(name);
84
+ return resolvedSubstitutionArgument(substitution, base, nextResolving);
94
85
  }
95
86
  function aliasSubstitution(alias, type, base) {
96
87
  const parameters = alias.typeParameters?.params ?? [];
97
- const typeArguments = type.typeArguments?.params ?? [];
98
88
  const next = new Map(base);
99
89
  for (const [index, parameter] of parameters.entries()) {
100
- const argument = typeArguments[index] ?? parameter.default;
101
- if (argument === null || argument === void 0) return null;
102
- 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));
103
93
  }
104
- 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
+ });
105
112
  }
106
113
  function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
107
114
  const unwrapped = unwrapTransparentType(type);
108
- if (unwrapped.type === "TSUnknownKeyword") return "unknown";
109
- if (unwrapped.type === "TSAnyKeyword") return "any";
110
- if (unwrapped.type === "TSObjectKeyword") return "object";
111
- if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return "empty-object";
112
- 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
+ }
113
123
  if (unwrapped.type === "TSIntersectionType") {
114
124
  const unsafeMembers = unwrapped.types.map((member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases));
115
- if (unsafeMembers.includes("any")) return "any";
116
- return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null) ? unsafeMembers[0] ?? null : null;
117
- }
118
- if (unwrapped.type !== "TSTypeReference") return null;
119
- const name = typeReferenceName(unwrapped);
120
- if (name === null) return null;
121
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
122
- const wrapped = unwrapped.typeArguments?.params[0];
123
- 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);
124
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));
125
136
  const substitution = substitutions.get(name);
126
- 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
+ }
127
141
  const interfaceDeclarations = environment.interfaces.get(name);
128
- if (interfaceDeclarations !== void 0) return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
129
- const alias = environment.aliases.get(name);
130
- if (alias === void 0 || resolvingAliases.has(name)) return null;
131
- const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
132
- if (nextSubstitutions === null) return null;
133
- const nextResolving = new Set(resolvingAliases);
134
- nextResolving.add(name);
135
- 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));
136
147
  }
137
148
  function dictionaryValueTypes(type, environment, substitutions, resolvingAliases) {
138
149
  const unwrapped = unwrapTransparentType(type);
139
- if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) => member.type === "TSIndexSignature" && member.typeAnnotation !== null ? [{
140
- type: member.typeAnnotation.typeAnnotation,
141
- substitutions
142
- }] : []);
143
- if (unwrapped.type === "TSMappedType") return unwrapped.typeAnnotation === null ? [] : [{
144
- type: unwrapped.typeAnnotation,
145
- substitutions
146
- }];
147
- if (unwrapped.type !== "TSTypeReference") return [];
148
- const name = typeReferenceName(unwrapped);
149
- if (name === null) return [];
150
- const substitution = substitutions.get(name);
151
- if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
152
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
153
- const wrapped = unwrapped.typeArguments?.params[0];
154
- return wrapped === void 0 ? [] : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
155
- }
156
- if (name === "Record" && isBuiltIn(name, environment)) {
157
- const value = unwrapped.typeArguments?.params[1] ?? null;
158
- return value === null ? [] : [{
159
- 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,
160
161
  substitutions
161
162
  }];
162
163
  }
163
- if ((name === "Pick" || name === "Omit") && isBuiltIn(name, environment)) {
164
- const source = unwrapped.typeArguments?.params[0];
165
- 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);
166
175
  }
167
- const alias = environment.aliases.get(name);
168
- if (alias === void 0 || resolvingAliases.has(name)) return [];
169
- const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
170
- if (nextSubstitutions === null) return [];
171
- const nextResolving = new Set(resolvingAliases);
172
- nextResolving.add(name);
173
- 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
+ });
174
192
  }
193
+ const unsafeDictionary = (unsafeValue) => ({
194
+ kind: "unsafe-dictionary",
195
+ unsafeValue
196
+ });
175
197
  function classifyUnsafeDictionaryValue(valueType, environment) {
176
- const unsafeValue = unsafeDirectValue(valueType, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
177
- return unsafeValue === null ? null : {
178
- kind: "unsafe-dictionary",
179
- unsafeValue
180
- };
198
+ return Option.map(unsafeDirectValue(valueType, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set()), unsafeDictionary);
181
199
  }
182
200
  function classifyUnsafeDictionary(type, environment) {
183
- for (const valueType of dictionaryValueTypes(type, environment, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set())) {
184
- const unsafeValue = unsafeDirectValue(valueType.type, environment, valueType.substitutions, /* @__PURE__ */ new Set());
185
- if (unsafeValue !== null) return {
186
- kind: "unsafe-dictionary",
187
- unsafeValue
188
- };
189
- }
190
- return null;
191
- }
192
- function resolvesToDictionary(type, environment, substitutions, resolvingAliases) {
193
- return dictionaryValueTypes(type, environment, substitutions, resolvingAliases).length > 0;
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));
194
202
  }
195
203
  function classifyWideningTarget(type, environment) {
196
204
  const unwrapped = unwrapTransparentType(type);
197
- if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
198
- if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
199
- if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : unwrapped.members.length > 0 ? { kind: "anonymous object" } : null;
200
- if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
201
- if (unwrapped.type !== "TSTypeReference") return null;
202
- const name = typeReferenceName(unwrapped);
203
- if (name === null) return null;
204
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
205
- const wrapped = unwrapped.typeArguments?.params[0];
206
- 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();
207
211
  }
208
- if (name === "Record" && isBuiltIn(name, environment)) return { kind: "open dictionary" };
209
- const alias = environment.aliases.get(name);
210
- if (alias === void 0) return null;
211
- if ((alias.typeParameters?.params.length ?? 0) > 0) {
212
- const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
213
- return substitutions !== null && resolvesToDictionary(alias.typeAnnotation, environment, substitutions, new Set([name])) ? { 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();
214
221
  }
215
- const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
216
- if (substitutions === null) return null;
217
- 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();
229
+ }
230
+ function hasBroadRecordKey(type, environment, substitutions) {
231
+ return Option.match(typeArgument(type, 0), {
232
+ onNone: () => true,
233
+ onSome: (key) => isBroadMappedKey(key, environment, substitutions)
234
+ });
235
+ }
236
+ function isBroadMappedKey(type, environment, substitutions, visitedAliases = /* @__PURE__ */ new Set()) {
237
+ const unwrapped = unwrapTransparentType(type);
238
+ if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
239
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => isBroadMappedKey(member, environment, substitutions, visitedAliases));
240
+ if (unwrapped.type !== "TSTypeReference") return false;
241
+ const referenceName = typeReferenceName(unwrapped);
242
+ if (Option.isNone(referenceName)) return false;
243
+ const name = referenceName.value;
244
+ const substitution = substitutions.get(name);
245
+ if (Predicate.isNotUndefined(substitution) && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
246
+ if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true;
247
+ const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
248
+ if (Option.isNone(alias) || (alias.value.typeParameters?.params.length ?? 0) > 0 || visitedAliases.has(name)) return false;
249
+ const nextVisited = new Set(visitedAliases);
250
+ nextVisited.add(name);
251
+ return isBroadMappedKey(alias.value.typeAnnotation, environment, substitutions, nextVisited);
218
252
  }
219
253
  function classifyAliasBroadTarget(type, environment, substitutions, resolvingAliases) {
220
254
  const unwrapped = unwrapTransparentType(type);
221
- if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
222
- if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
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 classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
228
- const alias = environment.aliases.get(name);
229
- if (alias === void 0 || resolvingAliases.has(name)) return null;
230
- const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
231
- if (nextSubstitutions === null) return null;
232
- const nextResolving = new Set(resolvingAliases);
233
- nextResolving.add(name);
234
- return classifyAliasBroadTarget(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
273
+ if (Predicate.isNotUndefined(substitution)) {
274
+ if (isUnappliedReferenceTo(substitution, name)) return Option.none();
275
+ return classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
276
+ }
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));
235
283
  }
236
284
  function isKnownEvidenceExpression(expression) {
237
285
  let current = expression;
@@ -0,0 +1,30 @@
1
+ import * as Option from "effect/Option";
2
+ //#region src/rules/_anti-slop-function-parameters.ts
3
+ /** Return whether a type is or contains TypeScript's absorbing unknown top type. */
4
+ function containsUnknownType(type) {
5
+ if (type.type === "TSUnknownKeyword") return true;
6
+ if (type.type === "TSParenthesizedType") return containsUnknownType(type.typeAnnotation);
7
+ return type.type === "TSUnionType" && type.types.some(containsUnknownType);
8
+ }
9
+ /** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */
10
+ function functionParameterTypeAnnotation(parameter) {
11
+ if (parameter.type === "TSParameterProperty") return functionParameterTypeAnnotation(parameter.parameter);
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);
15
+ }
16
+ /** Return only a function parameter's local binding, excluding its annotation and default value. */
17
+ function functionParameterBindingName(parameter, sourceCode) {
18
+ if (parameter.type === "TSParameterProperty") return functionParameterBindingName(parameter.parameter, sourceCode);
19
+ if (parameter.type === "AssignmentPattern") return functionParameterBindingName(parameter.left, sourceCode);
20
+ if (parameter.type === "RestElement") return functionParameterBindingName(parameter.argument, sourceCode);
21
+ const annotationOffset = Option.map(functionParameterTypeAnnotation(parameter), (annotation) => annotation.start - parameter.start);
22
+ if (parameter.type === "Identifier") return parameter.name;
23
+ const sourceText = sourceCode.getText(parameter);
24
+ return Option.match(annotationOffset, {
25
+ onNone: () => sourceText,
26
+ onSome: (offset) => sourceText.slice(0, offset).trimEnd()
27
+ });
28
+ }
29
+ //#endregion
30
+ export { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation };
@@ -0,0 +1,25 @@
1
+ import { childNodesAt, isAstNode } from "./_ast-children.js";
2
+ import { ancestors } from "./_ast-ancestors.js";
3
+ //#region src/rules/_anti-slop-lexical-type-parameters.ts
4
+ function isNode(value) {
5
+ return isAstNode(value);
6
+ }
7
+ function collectInferTypeParameterNames(node, visitorKeys, names) {
8
+ if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
9
+ for (const key of visitorKeys[node.type] ?? []) for (const child of childNodesAt(node, key, isNode)) collectInferTypeParameterNames(child, visitorKeys, names);
10
+ }
11
+ /** Collect type binders that are in scope at a node and can shadow module aliases. */
12
+ function lexicalTypeParameterNames(node, visitorKeys) {
13
+ const names = /* @__PURE__ */ new Set();
14
+ let descendant = node;
15
+ for (const current of [node, ...ancestors(node)]) {
16
+ if (current.type === "Program") break;
17
+ if ("typeParameters" in current) for (const parameter of current.typeParameters?.params ?? []) names.add(parameter.name.name);
18
+ if (current.type === "TSMappedType" && (descendant === current.nameType || descendant === current.typeAnnotation)) names.add(current.key.name);
19
+ if (current.type === "TSConditionalType" && descendant === current.trueType) collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
20
+ descendant = current;
21
+ }
22
+ return names;
23
+ }
24
+ //#endregion
25
+ export { lexicalTypeParameterNames };
@@ -0,0 +1,25 @@
1
+ import { resolveVariable } from "./_anti-slop-scope.js";
2
+ import * as Option from "effect/Option";
3
+ //#region src/rules/_anti-slop-reflect-method.ts
4
+ /**
5
+ * Ported from dmmulroy/anti-slop at
6
+ * c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b.
7
+ */
8
+ function isGlobalReflect(sourceCode, expression) {
9
+ if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
10
+ if (sourceCode.isGlobalReference(expression)) return true;
11
+ return Option.match(resolveVariable(sourceCode, expression), {
12
+ onNone: () => true,
13
+ onSome: (variable) => variable.defs.length === 0
14
+ });
15
+ }
16
+ /** Reports whether a call target names one method on the global Reflect object. */
17
+ function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
18
+ if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
19
+ if (!isGlobalReflect(sourceCode, callee.object)) return false;
20
+ const property = callee.property;
21
+ if (callee.computed) return property.type === "Literal" && property.value === methodName;
22
+ return property.type === "Identifier" && property.name === methodName;
23
+ }
24
+ //#endregion
25
+ export { isGlobalReflectMethodCall };
@@ -0,0 +1,15 @@
1
+ import * as Option from "effect/Option";
2
+ import * as Predicate from "effect/Predicate";
3
+ //#region src/rules/_anti-slop-scope.ts
4
+ /** Resolve an identifier to its binding by walking lexical scopes upward. */
5
+ function resolveVariable(sourceCode, identifier) {
6
+ let scope = Option.some(sourceCode.getScope(identifier));
7
+ while (Option.isSome(scope)) {
8
+ const variable = scope.value.set.get(identifier.name);
9
+ if (Predicate.isNotUndefined(variable)) return Option.some(variable);
10
+ scope = Option.fromNullishOr(scope.value.upper);
11
+ }
12
+ return Option.none();
13
+ }
14
+ //#endregion
15
+ export { resolveVariable };