oxlint-plugin-effect 0.12.1 → 0.14.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 (47) hide show
  1. package/README.md +25 -14
  2. package/THIRD_PARTY_NOTICES.md +4 -2
  3. package/dist/presets/recommended.d.ts +9 -0
  4. package/dist/presets/recommended.js +10 -1
  5. package/dist/rules/_anti-slop-array-method.js +70 -0
  6. package/dist/rules/_anti-slop-dictionary-types.js +58 -53
  7. package/dist/rules/_anti-slop-function-parameters.js +27 -0
  8. package/dist/rules/_anti-slop-lexical-type-parameters.js +33 -0
  9. package/dist/rules/_anti-slop-reflect-method.js +21 -0
  10. package/dist/rules/_anti-slop-scope.js +13 -0
  11. package/dist/rules/_anti-slop-type-alias-resolution.js +146 -0
  12. package/dist/rules/_function-metrics.js +31 -31
  13. package/dist/rules/_test-files.js +8 -0
  14. package/dist/rules/index.d.ts +10 -1
  15. package/dist/rules/index.js +19 -1
  16. package/dist/rules/no-array-filter-map.d.ts +7 -0
  17. package/dist/rules/no-array-filter-map.js +39 -0
  18. package/dist/rules/no-as.js +12 -6
  19. package/dist/rules/no-conditional-empty-object-spread.d.ts +1 -1
  20. package/dist/rules/no-conditional-empty-object-spread.js +8 -51
  21. package/dist/rules/no-eager-acquire.d.ts +6 -0
  22. package/dist/rules/no-eager-acquire.js +92 -0
  23. package/dist/rules/no-effect-run-in-tests.d.ts +6 -0
  24. package/dist/rules/no-effect-run-in-tests.js +57 -0
  25. package/dist/rules/no-known-value-widening.js +6 -14
  26. package/dist/rules/no-module-level-mutable-state.d.ts +6 -0
  27. package/dist/rules/no-module-level-mutable-state.js +51 -0
  28. package/dist/rules/no-object-parameters.js +11 -27
  29. package/dist/rules/no-reduce-accumulator-copy.d.ts +7 -0
  30. package/dist/rules/no-reduce-accumulator-copy.js +94 -0
  31. package/dist/rules/no-reflect-apply.d.ts +7 -0
  32. package/dist/rules/no-reflect-apply.js +25 -0
  33. package/dist/rules/no-reflect-get.d.ts +7 -0
  34. package/dist/rules/no-reflect-get.js +25 -0
  35. package/dist/rules/no-runtime-typeof.js +14 -1
  36. package/dist/rules/no-shape-in-symbol-names.js +7 -1
  37. package/dist/rules/no-unknown-parameters.d.ts +1 -1
  38. package/dist/rules/no-unknown-parameters.js +23 -16
  39. package/dist/rules/no-unknown-returns.d.ts +7 -0
  40. package/dist/rules/no-unknown-returns.js +73 -0
  41. package/dist/rules/no-unknown-type-aliases.js +17 -27
  42. package/dist/rules/no-unsafe-dictionary-type.js +54 -3
  43. package/dist/rules/no-widen-then-assert.js +1 -1
  44. package/dist/rules/require-suppression-reason.d.ts +6 -0
  45. package/dist/rules/require-suppression-reason.js +87 -0
  46. package/dist/vendor/effect-oxlint/Diagnostic.js +1 -1
  47. package/package.json +5 -4
package/README.md CHANGED
@@ -27,17 +27,20 @@ If the project uses JSON configuration, copy the exported rule map into `rules`;
27
27
 
28
28
  | Rule | Contract |
29
29
  | -------------------------------------- | ----------------------------------------------------------------------------------------- |
30
- | `effect/noAs` | Bans TypeScript `as` assertions; use `satisfies` |
30
+ | `effect/noAs` | Bans TypeScript `as` assertions; use `satisfies` (`as const` is allowed) |
31
31
  | `effect/noAsyncFunction` | Bans async functions and await expressions |
32
32
  | `effect/noTryCatch` | Bans every try/catch/finally statement |
33
33
  | `effect/noTestLifecycleHooks` | Bans `beforeEach`, `afterEach`, `beforeAll`, and `afterAll`; use Effect scopes instead |
34
34
  | `effect/noThrowStatement` | Bans every throw statement |
35
35
  | `effect/noNewPromise` | Bans new Promise, Promise calls, and Promise static APIs |
36
36
  | `effect/noNewError` | Allows native Error values only as direct arguments to Effect.die, Cause.die, or Exit.die |
37
- | `effect/noNullish` | Bans null and undefined; permits `Object.create(null)` for prototype-free dictionaries |
37
+ | `effect/noNullish` | Bans null and undefined; permits `Object.create(null)` for prototype-free dictionaries |
38
38
  | `effect/noModuleMocks` | Bans Vitest and Jest module mocks and method spies; use Effect service test layers |
39
39
  | `effect/noTernary` | Bans conditional expressions while allowing ordinary if statements |
40
40
  | `effect/noManagedRuntimeInEffect` | Keeps ManagedRuntime construction at non-Effect host boundaries |
41
+ | `effect/noModuleLevelMutableState` | Bans module-level `let` and `var` outside tests; hold shared state in a Layer-owned `Ref` |
42
+ | `effect/noEagerAcquire` | Bans `acquireRelease` acquires that build or capture the resource before acquire runs |
43
+ | `effect/noEffectRunInTests` | Bans `Effect.run*` and `ManagedRuntime.make` in tests; use `it.effect` or `it.layer` |
41
44
  | `effect/noInlineProvide` | Keeps dependency provisioning at explicit composition boundaries |
42
45
  | `effect/noNestedEffectGen` | Flattens directly yielded nested generators |
43
46
  | `effect/noPerCallCacheConstruction` | Constructs shared caches once in their owning layer |
@@ -55,26 +58,34 @@ If the project uses JSON configuration, copy the exported rule map into `rules`;
55
58
  | `effect/preferPredicateIsTagged` | Replaces combined `_tag` comparisons with a named `Predicate` refinement |
56
59
  | `effect/preferServiceOf` | Checks inline Layer implementations through `Service.of` |
57
60
  | `effect/requireNamedEffectFn` | Requires stable names for `Effect.fn` operations |
61
+ | `effect/requireSuppressionReason` | Requires lint, Effect, and TS suppressions to name their target and give a `--` reason |
58
62
  | `effect/noGlobals` | Bans ambient capabilities with direct Effect replacements; allows `process.std*.isTTY` |
59
63
  | `effect/noNodeBuiltinImport` | Bans fully replaced Node modules and replaced operations from partial modules |
60
64
 
65
+ `effect/requireSuppressionReason` also rejects `@effect-diagnostics effect/name:off`: @effect/tsgo ignores the `effect/` prefix, so write the bare rule name. A blanket directive that covers its own line, such as a bare `// oxlint-disable-line` or a file-leading `/* eslint-disable */`, suppresses this rule's report too; oxlint applies the directive before the rule can report it.
66
+
61
67
  ## Anti-Slop Rules
62
68
 
63
69
  The recommended preset also includes these rules from
64
70
  [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop):
65
71
 
66
- | Rule | Contract |
67
- | --------------------------------------- | ------------------------------------------------------------------ |
68
- | `effect/noChainedTypeAssertions` | Bans nested type assertions that invent type evidence |
69
- | `effect/noConditionalEmptyObjectSpread` | Bans conditional spreads that use an empty object to omit fields |
70
- | `effect/noKnownValueWidening` | Bans broad target types that discard known value evidence |
71
- | `effect/noObjectParameters` | Bans the broad `object` type on function inputs |
72
- | `effect/noRuntimeTypeof` | Requires boundary parsing instead of runtime `typeof` narrowing |
73
- | `effect/noShapeInSymbolNames` | Bans `shape` in symbol names |
74
- | `effect/noUnknownParameters` | Bans `unknown` inputs except an input named `cause` |
75
- | `effect/noUnknownTypeAliases` | Bans aliases that only hide `unknown` |
76
- | `effect/noUnsafeDictionaryType` | Bans dictionaries with unsafe broad value types |
77
- | `effect/noWidenThenAssert` | Bans local flows that widen known values and then assert them back |
72
+ | Rule | Contract |
73
+ | --------------------------------------- | -------------------------------------------------------------------------------- |
74
+ | `effect/noArrayFilterMap` | Bans adjacent array `filter`/`map` passes over a known array |
75
+ | `effect/noChainedTypeAssertions` | Bans nested type assertions that invent type evidence |
76
+ | `effect/noConditionalEmptyObjectSpread` | Bans conditional spreads that use an empty object to omit fields |
77
+ | `effect/noKnownValueWidening` | Bans broad target types that discard known value evidence |
78
+ | `effect/noObjectParameters` | Bans the broad `object` type on function inputs |
79
+ | `effect/noReduceAccumulatorCopy` | Bans copying a reducer accumulator on every iteration |
80
+ | `effect/noReflectApply` | Bans `Reflect.apply`; call typed functions directly |
81
+ | `effect/noReflectGet` | Bans `Reflect.get`; use typed property access |
82
+ | `effect/noRuntimeTypeof` | Requires boundary parsing instead of `typeof` narrowing; allows existence probes |
83
+ | `effect/noShapeInSymbolNames` | Bans `shape` in symbol names |
84
+ | `effect/noUnknownParameters` | Bans `unknown` inputs except `cause`, type guards, and rejection reasons |
85
+ | `effect/noUnknownReturns` | Bans return contracts of `unknown` or `Promise<unknown>` |
86
+ | `effect/noUnknownTypeAliases` | Bans aliases that only hide `unknown` |
87
+ | `effect/noUnsafeDictionaryType` | Bans dictionaries with unsafe broad value types |
88
+ | `effect/noWidenThenAssert` | Bans local flows that widen known values and then assert them back |
78
89
 
79
90
  See `THIRD_PARTY_NOTICES.md` for the source revision and license.
80
91
 
@@ -1,8 +1,10 @@
1
1
  # Third-Party Notices
2
2
 
3
3
  The anti-slop rules include code derived from
4
- [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop) at commit
5
- `b5d2288db1f00469a1d5f2e3b0e265e5a5676fd0`.
4
+ [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop), synced to commit
5
+ `c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b`. The rejection-handler exemption in
6
+ `noUnknownParameters` comes from the vendored copy in
7
+ [`joelhooks/rat-stack`](https://github.com/joelhooks/rat-stack).
6
8
 
7
9
  MIT License
8
10
 
@@ -14,17 +14,21 @@ declare const recommended: {
14
14
  readonly "effect/maxHalsteadDifficulty": readonly ["error", {
15
15
  readonly max: 79;
16
16
  }];
17
+ readonly "effect/noArrayFilterMap": "error";
17
18
  readonly "effect/noAs": "error";
18
19
  readonly "effect/noAsyncFunction": "error";
19
20
  readonly "effect/noChainedTypeAssertions": "error";
20
21
  readonly "effect/noConditionalEmptyObjectSpread": "error";
21
22
  readonly "effect/noDynamicImports": "error";
23
+ readonly "effect/noEagerAcquire": "error";
22
24
  readonly "effect/noEffectBind": "error";
23
25
  readonly "effect/noEffectDo": "error";
26
+ readonly "effect/noEffectRunInTests": "error";
24
27
  readonly "effect/noGlobals": "error";
25
28
  readonly "effect/noInlineProvide": "error";
26
29
  readonly "effect/noKnownValueWidening": "error";
27
30
  readonly "effect/noManagedRuntimeInEffect": "error";
31
+ readonly "effect/noModuleLevelMutableState": "error";
28
32
  readonly "effect/noModuleMocks": "error";
29
33
  readonly "effect/noNestedEffectGen": "error";
30
34
  readonly "effect/noNewError": "error";
@@ -33,6 +37,9 @@ declare const recommended: {
33
37
  readonly "effect/noNullish": "error";
34
38
  readonly "effect/noObjectParameters": "error";
35
39
  readonly "effect/noPerCallCacheConstruction": "error";
40
+ readonly "effect/noReduceAccumulatorCopy": "error";
41
+ readonly "effect/noReflectApply": "error";
42
+ readonly "effect/noReflectGet": "error";
36
43
  readonly "effect/noRunCollectOnUnboundedStream": "error";
37
44
  readonly "effect/noRuntimeTypeof": "error";
38
45
  readonly "effect/noSequentialEffectAll": "error";
@@ -45,6 +52,7 @@ declare const recommended: {
45
52
  readonly "effect/noUnboundedConcurrency": "error";
46
53
  readonly "effect/noUnboundedRetry": "error";
47
54
  readonly "effect/noUnknownParameters": "error";
55
+ readonly "effect/noUnknownReturns": "error";
48
56
  readonly "effect/noUnknownTypeAliases": "error";
49
57
  readonly "effect/noUnsafeDictionaryType": "error";
50
58
  readonly "effect/noWidenThenAssert": "error";
@@ -54,6 +62,7 @@ declare const recommended: {
54
62
  readonly "effect/preferPredicateIsTagged": "error";
55
63
  readonly "effect/preferServiceOf": "error";
56
64
  readonly "effect/requireNamedEffectFn": "error";
65
+ readonly "effect/requireSuppressionReason": "error";
57
66
  };
58
67
  //#endregion
59
68
  export { recommended };
@@ -8,17 +8,21 @@ const recommended = {
8
8
  complexity: ["error", { max: 21 }],
9
9
  "effect/maxCognitiveComplexity": ["error", { max: 21 }],
10
10
  "effect/maxHalsteadDifficulty": ["error", { max: 79 }],
11
+ "effect/noArrayFilterMap": "error",
11
12
  "effect/noAs": "error",
12
13
  "effect/noAsyncFunction": "error",
13
14
  "effect/noChainedTypeAssertions": "error",
14
15
  "effect/noConditionalEmptyObjectSpread": "error",
15
16
  "effect/noDynamicImports": "error",
17
+ "effect/noEagerAcquire": "error",
16
18
  "effect/noEffectBind": "error",
17
19
  "effect/noEffectDo": "error",
20
+ "effect/noEffectRunInTests": "error",
18
21
  "effect/noGlobals": "error",
19
22
  "effect/noInlineProvide": "error",
20
23
  "effect/noKnownValueWidening": "error",
21
24
  "effect/noManagedRuntimeInEffect": "error",
25
+ "effect/noModuleLevelMutableState": "error",
22
26
  "effect/noModuleMocks": "error",
23
27
  "effect/noNestedEffectGen": "error",
24
28
  "effect/noNewError": "error",
@@ -27,6 +31,9 @@ const recommended = {
27
31
  "effect/noNullish": "error",
28
32
  "effect/noObjectParameters": "error",
29
33
  "effect/noPerCallCacheConstruction": "error",
34
+ "effect/noReduceAccumulatorCopy": "error",
35
+ "effect/noReflectApply": "error",
36
+ "effect/noReflectGet": "error",
30
37
  "effect/noRunCollectOnUnboundedStream": "error",
31
38
  "effect/noRuntimeTypeof": "error",
32
39
  "effect/noSequentialEffectAll": "error",
@@ -39,6 +46,7 @@ const recommended = {
39
46
  "effect/noUnboundedConcurrency": "error",
40
47
  "effect/noUnboundedRetry": "error",
41
48
  "effect/noUnknownParameters": "error",
49
+ "effect/noUnknownReturns": "error",
42
50
  "effect/noUnknownTypeAliases": "error",
43
51
  "effect/noUnsafeDictionaryType": "error",
44
52
  "effect/noWidenThenAssert": "error",
@@ -47,7 +55,8 @@ const recommended = {
47
55
  "effect/preferMatchTagsExhaustive": "error",
48
56
  "effect/preferPredicateIsTagged": "error",
49
57
  "effect/preferServiceOf": "error",
50
- "effect/requireNamedEffectFn": "error"
58
+ "effect/requireNamedEffectFn": "error",
59
+ "effect/requireSuppressionReason": "error"
51
60
  };
52
61
  //#endregion
53
62
  export { recommended };
@@ -0,0 +1,70 @@
1
+ //#region src/rules/_anti-slop-array-method.ts
2
+ /** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
3
+ function unwrapArrayExpression(node) {
4
+ while (node.type === "ParenthesizedExpression" || node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression") node = node.expression;
5
+ return node;
6
+ }
7
+ /** Resolve a local binding by scope, not by identifier spelling. */
8
+ function resolveArrayBinding(sourceCode, node) {
9
+ node = unwrapArrayExpression(node);
10
+ if (node.type !== "Identifier") return null;
11
+ let scope = sourceCode.getScope(node);
12
+ while (scope !== null) {
13
+ const variable = scope.set.get(node.name);
14
+ if (variable !== void 0) return variable;
15
+ scope = scope.upper;
16
+ }
17
+ return null;
18
+ }
19
+ /** Read static method names, including computed string literals, without evaluating expressions. */
20
+ function arrayMethodTarget(node) {
21
+ node = unwrapArrayExpression(node);
22
+ if (node.type !== "MemberExpression") return null;
23
+ const property = node.property;
24
+ if (!node.computed && property.type === "Identifier") return {
25
+ name: property.name,
26
+ object: node.object
27
+ };
28
+ if (node.computed && property.type === "Literal" && typeof property.value === "string") return {
29
+ name: property.value,
30
+ object: node.object
31
+ };
32
+ return null;
33
+ }
34
+ function isArrayAnnotation(type) {
35
+ if (type.type === "TSArrayType" || type.type === "TSTupleType") return true;
36
+ if (type.type === "TSParenthesizedType") return isArrayAnnotation(type.typeAnnotation);
37
+ if (type.type === "TSTypeOperator" && type.operator === "readonly") return isArrayAnnotation(type.typeAnnotation);
38
+ return type.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Array" || type.typeName.name === "ReadonlyArray");
39
+ }
40
+ /** Recognize local array evidence; unknown receivers and iterator pipelines are deliberately excluded. */
41
+ function isKnownArrayExpression(sourceCode, node, visited = /* @__PURE__ */ new Set()) {
42
+ node = unwrapArrayExpression(node);
43
+ if (node.type === "ArrayExpression") return true;
44
+ if (node.type === "CallExpression") {
45
+ const method = arrayMethodTarget(node.callee);
46
+ return method !== null && [
47
+ "map",
48
+ "filter",
49
+ "flatMap",
50
+ "slice",
51
+ "concat",
52
+ "toSorted",
53
+ "toReversed",
54
+ "toSpliced"
55
+ ].includes(method.name) && isKnownArrayExpression(sourceCode, method.object, visited);
56
+ }
57
+ if (node.type !== "Identifier") return false;
58
+ const variable = resolveArrayBinding(sourceCode, node);
59
+ if (variable === null || visited.has(variable)) return false;
60
+ visited.add(variable);
61
+ if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
62
+ for (const identifier of variable.identifiers) {
63
+ const annotation = identifier.typeAnnotation?.typeAnnotation;
64
+ if (annotation !== void 0) return isArrayAnnotation(annotation);
65
+ }
66
+ for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.id.type === "Identifier" && definition.node.init !== null && definition.node.parent.type === "VariableDeclaration" && definition.node.parent.kind === "const") return isKnownArrayExpression(sourceCode, definition.node.init, visited);
67
+ return false;
68
+ }
69
+ //#endregion
70
+ export { arrayMethodTarget, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression };
@@ -1,3 +1,4 @@
1
+ import { createTypeAliasEnvironment, hasVisibleTypeBinding, visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
1
2
  //#region src/rules/_anti-slop-dictionary-types.ts
2
3
  const BUILT_INS = new Set([
3
4
  "Record",
@@ -18,48 +19,25 @@ const TRANSPARENT_WRAPPERS = new Set([
18
19
  function declaredStatement(statement) {
19
20
  return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration ?? null : statement;
20
21
  }
21
- function createTypeEnvironment(program) {
22
- const aliases = /* @__PURE__ */ new Map();
22
+ function createTypeEnvironment(program, visitorKeys) {
23
23
  const interfaces = /* @__PURE__ */ new Map();
24
- const shadowedBuiltIns = /* @__PURE__ */ new Set();
25
24
  for (const statement of program.body) {
26
25
  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
- }
26
+ if (declaration?.type !== "TSInterfaceDeclaration") continue;
27
+ const declarations = interfaces.get(declaration.id.name) ?? [];
28
+ declarations.push(declaration);
29
+ interfaces.set(declaration.id.name, declarations);
51
30
  }
52
31
  return {
53
- aliases,
54
32
  interfaces,
55
- shadowedBuiltIns
33
+ typeAliases: createTypeAliasEnvironment(program, visitorKeys)
56
34
  };
57
35
  }
58
36
  function typeReferenceName(type) {
59
37
  return type.typeName.type === "Identifier" ? type.typeName.name : null;
60
38
  }
61
- function isBuiltIn(name, environment) {
62
- return BUILT_INS.has(name) && !environment.shadowedBuiltIns.has(name);
39
+ function isBuiltIn(name, use, environment) {
40
+ return BUILT_INS.has(name) && !hasVisibleTypeBinding(name, use, environment.typeAliases);
63
41
  }
64
42
  function isUnappliedReferenceTo(type, name) {
65
43
  const unwrapped = unwrapTransparentType(type);
@@ -84,13 +62,16 @@ function isEffectivelyEmptyInterface(declarations) {
84
62
  const [type] = declarations;
85
63
  return type !== void 0 && type.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember));
86
64
  }
87
- function resolvedSubstitutionArgument(type, base) {
65
+ function resolvedSubstitutionArgument(type, base, resolving = /* @__PURE__ */ new Set()) {
88
66
  const unwrapped = unwrapTransparentType(type);
89
67
  if (unwrapped.type !== "TSTypeReference") return type;
90
68
  const name = typeReferenceName(unwrapped);
91
- if (name === null) return type;
69
+ if (name === null || resolving.has(name)) return type;
92
70
  const substitution = base.get(name);
93
- return substitution === void 0 ? type : resolvedSubstitutionArgument(substitution, base);
71
+ if (substitution === void 0) return type;
72
+ const nextResolving = new Set(resolving);
73
+ nextResolving.add(name);
74
+ return resolvedSubstitutionArgument(substitution, base, nextResolving);
94
75
  }
95
76
  function aliasSubstitution(alias, type, base) {
96
77
  const parameters = alias.typeParameters?.params ?? [];
@@ -118,7 +99,7 @@ function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
118
99
  if (unwrapped.type !== "TSTypeReference") return null;
119
100
  const name = typeReferenceName(unwrapped);
120
101
  if (name === null) return null;
121
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
102
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
122
103
  const wrapped = unwrapped.typeArguments?.params[0];
123
104
  return wrapped === void 0 ? null : unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases);
124
105
  }
@@ -126,8 +107,8 @@ function unsafeDirectValue(type, environment, substitutions, resolvingAliases) {
126
107
  if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
127
108
  const interfaceDeclarations = environment.interfaces.get(name);
128
109
  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;
110
+ const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
111
+ if (alias === null || resolvingAliases.has(name)) return null;
131
112
  const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
132
113
  if (nextSubstitutions === null) return null;
133
114
  const nextResolving = new Set(resolvingAliases);
@@ -149,23 +130,23 @@ function dictionaryValueTypes(type, environment, substitutions, resolvingAliases
149
130
  if (name === null) return [];
150
131
  const substitution = substitutions.get(name);
151
132
  if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
152
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
133
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
153
134
  const wrapped = unwrapped.typeArguments?.params[0];
154
135
  return wrapped === void 0 ? [] : dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
155
136
  }
156
- if (name === "Record" && isBuiltIn(name, environment)) {
137
+ if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
157
138
  const value = unwrapped.typeArguments?.params[1] ?? null;
158
139
  return value === null ? [] : [{
159
140
  type: value,
160
141
  substitutions
161
142
  }];
162
143
  }
163
- if ((name === "Pick" || name === "Omit") && isBuiltIn(name, environment)) {
144
+ if ((name === "Pick" || name === "Omit") && isBuiltIn(name, unwrapped, environment)) {
164
145
  const source = unwrapped.typeArguments?.params[0];
165
146
  return source === void 0 ? [] : dictionaryValueTypes(source, environment, substitutions, resolvingAliases);
166
147
  }
167
- const alias = environment.aliases.get(name);
168
- if (alias === void 0 || resolvingAliases.has(name)) return [];
148
+ const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
149
+ if (alias === null || resolvingAliases.has(name)) return [];
169
150
  const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
170
151
  if (nextSubstitutions === null) return [];
171
152
  const nextResolving = new Set(resolvingAliases);
@@ -189,9 +170,6 @@ function classifyUnsafeDictionary(type, environment) {
189
170
  }
190
171
  return null;
191
172
  }
192
- function resolvesToDictionary(type, environment, substitutions, resolvingAliases) {
193
- return dictionaryValueTypes(type, environment, substitutions, resolvingAliases).length > 0;
194
- }
195
173
  function classifyWideningTarget(type, environment) {
196
174
  const unwrapped = unwrapTransparentType(type);
197
175
  if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
@@ -201,32 +179,59 @@ function classifyWideningTarget(type, environment) {
201
179
  if (unwrapped.type !== "TSTypeReference") return null;
202
180
  const name = typeReferenceName(unwrapped);
203
181
  if (name === null) return null;
204
- if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
182
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
205
183
  const wrapped = unwrapped.typeArguments?.params[0];
206
184
  return wrapped === void 0 ? null : classifyWideningTarget(wrapped, environment);
207
185
  }
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;
186
+ if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return hasBroadRecordKey(unwrapped, environment, /* @__PURE__ */ new Map()) ? { kind: "open dictionary" } : null;
187
+ const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
188
+ if (alias === null) return null;
211
189
  if ((alias.typeParameters?.params.length ?? 0) > 0) {
212
190
  const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
213
- return substitutions !== null && resolvesToDictionary(alias.typeAnnotation, environment, substitutions, new Set([name])) ? { kind: "generic container" } : null;
191
+ return (substitutions === null ? null : classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name])))?.kind === "open dictionary" ? { kind: "generic container" } : null;
214
192
  }
215
193
  const substitutions = aliasSubstitution(alias, unwrapped, /* @__PURE__ */ new Map());
216
194
  if (substitutions === null) return null;
217
195
  return classifyAliasBroadTarget(alias.typeAnnotation, environment, substitutions, new Set([name]));
218
196
  }
197
+ function hasBroadRecordKey(type, environment, substitutions) {
198
+ const key = type.typeArguments?.params[0];
199
+ return key === void 0 || isBroadMappedKey(key, environment, substitutions);
200
+ }
201
+ function isBroadMappedKey(type, environment, substitutions, visitedAliases = /* @__PURE__ */ new Set()) {
202
+ const unwrapped = unwrapTransparentType(type);
203
+ if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
204
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => isBroadMappedKey(member, environment, substitutions, visitedAliases));
205
+ if (unwrapped.type !== "TSTypeReference") return false;
206
+ const name = typeReferenceName(unwrapped);
207
+ if (name === null) return false;
208
+ const substitution = substitutions.get(name);
209
+ if (substitution !== void 0 && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
210
+ if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true;
211
+ const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
212
+ if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0 || visitedAliases.has(name)) return false;
213
+ const nextVisited = new Set(visitedAliases);
214
+ nextVisited.add(name);
215
+ return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
216
+ }
219
217
  function classifyAliasBroadTarget(type, environment, substitutions, resolvingAliases) {
220
218
  const unwrapped = unwrapTransparentType(type);
221
219
  if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
222
220
  if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
221
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : null;
222
+ if (unwrapped.type === "TSMappedType") return isBroadMappedKey(unwrapped.constraint, environment, substitutions) ? { kind: "open dictionary" } : null;
223
223
  if (unwrapped.type !== "TSTypeReference") return null;
224
224
  const name = typeReferenceName(unwrapped);
225
225
  if (name === null) return null;
226
226
  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;
227
+ if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : classifyAliasBroadTarget(substitution, environment, substitutions, resolvingAliases);
228
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
229
+ const wrapped = unwrapped.typeArguments?.params[0];
230
+ return wrapped === void 0 ? null : classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases);
231
+ }
232
+ if (name === "Record" && isBuiltIn(name, unwrapped, environment)) return hasBroadRecordKey(unwrapped, environment, substitutions) ? { kind: "open dictionary" } : null;
233
+ const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
234
+ if (alias === null || resolvingAliases.has(name)) return null;
230
235
  const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
231
236
  if (nextSubstitutions === null) return null;
232
237
  const nextResolving = new Set(resolvingAliases);
@@ -0,0 +1,27 @@
1
+ //#region src/rules/_anti-slop-function-parameters.ts
2
+ /** Return whether a type is or contains TypeScript's absorbing unknown top type. */
3
+ function containsUnknownType(type) {
4
+ if (type.type === "TSUnknownKeyword") return true;
5
+ if (type.type === "TSParenthesizedType") return containsUnknownType(type.typeAnnotation);
6
+ return type.type === "TSUnionType" && type.types.some(containsUnknownType);
7
+ }
8
+ /** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */
9
+ function functionParameterTypeAnnotation(parameter) {
10
+ if (parameter.type === "TSParameterProperty") return functionParameterTypeAnnotation(parameter.parameter);
11
+ if (parameter.type === "RestElement") return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.argument);
12
+ if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.left);
13
+ return parameter.typeAnnotation;
14
+ }
15
+ /** Return only a function parameter's local binding, excluding its annotation and default value. */
16
+ function functionParameterBindingName(parameter, sourceCode) {
17
+ if (parameter.type === "TSParameterProperty") return functionParameterBindingName(parameter.parameter, sourceCode);
18
+ if (parameter.type === "AssignmentPattern") return functionParameterBindingName(parameter.left, sourceCode);
19
+ if (parameter.type === "RestElement") return functionParameterBindingName(parameter.argument, sourceCode);
20
+ const annotation = functionParameterTypeAnnotation(parameter);
21
+ const annotationOffset = annotation === null || annotation === void 0 ? void 0 : annotation.start - parameter.start;
22
+ if (parameter.type === "Identifier") return parameter.name;
23
+ const sourceText = sourceCode.getText(parameter);
24
+ return annotationOffset === void 0 ? sourceText : sourceText.slice(0, annotationOffset).trimEnd();
25
+ }
26
+ //#endregion
27
+ export { containsUnknownType, functionParameterBindingName, functionParameterTypeAnnotation };
@@ -0,0 +1,33 @@
1
+ //#region src/rules/_anti-slop-lexical-type-parameters.ts
2
+ function isNode(value) {
3
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
4
+ }
5
+ function collectInferTypeParameterNames(node, visitorKeys, names) {
6
+ if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
7
+ const record = node;
8
+ for (const key of visitorKeys[node.type] ?? []) {
9
+ const value = record[key];
10
+ if (isNode(value)) {
11
+ collectInferTypeParameterNames(value, visitorKeys, names);
12
+ continue;
13
+ }
14
+ if (!Array.isArray(value)) continue;
15
+ for (const child of value) if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names);
16
+ }
17
+ }
18
+ /** Collect type binders that are in scope at a node and can shadow module aliases. */
19
+ function lexicalTypeParameterNames(node, visitorKeys) {
20
+ const names = /* @__PURE__ */ new Set();
21
+ let descendant = node;
22
+ let current = node;
23
+ while (current !== null && current.type !== "Program") {
24
+ if ("typeParameters" in current) for (const parameter of current.typeParameters?.params ?? []) names.add(parameter.name.name);
25
+ if (current.type === "TSMappedType" && (descendant === current.nameType || descendant === current.typeAnnotation)) names.add(current.key.name);
26
+ if (current.type === "TSConditionalType" && descendant === current.trueType) collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
27
+ descendant = current;
28
+ current = current.parent;
29
+ }
30
+ return names;
31
+ }
32
+ //#endregion
33
+ export { lexicalTypeParameterNames };
@@ -0,0 +1,21 @@
1
+ import { resolveVariable } from "./_anti-slop-scope.js";
2
+ //#region src/rules/_anti-slop-reflect-method.ts
3
+ /**
4
+ * Ported from dmmulroy/anti-slop at
5
+ * c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b.
6
+ */
7
+ function isGlobalReflect(sourceCode, expression) {
8
+ if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
9
+ if (sourceCode.isGlobalReference(expression)) return true;
10
+ const variable = resolveVariable(sourceCode, expression);
11
+ return variable === null || variable.defs.length === 0;
12
+ }
13
+ /** Reports whether a call target names one method on the global Reflect object. */
14
+ function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
15
+ if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
16
+ if (!isGlobalReflect(sourceCode, callee.object)) return false;
17
+ const property = callee.property;
18
+ return callee.computed ? property.type === "Literal" && property.value === methodName : property.type === "Identifier" && property.name === methodName;
19
+ }
20
+ //#endregion
21
+ export { isGlobalReflectMethodCall };
@@ -0,0 +1,13 @@
1
+ //#region src/rules/_anti-slop-scope.ts
2
+ /** Resolve an identifier to its binding by walking lexical scopes upward. */
3
+ function resolveVariable(sourceCode, identifier) {
4
+ let scope = sourceCode.getScope(identifier);
5
+ while (scope !== null) {
6
+ const variable = scope.set.get(identifier.name);
7
+ if (variable !== void 0) return variable;
8
+ scope = scope.upper;
9
+ }
10
+ return null;
11
+ }
12
+ //#endregion
13
+ export { resolveVariable };