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
@@ -3,7 +3,9 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
3
3
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
4
4
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
5
5
  import "../vendor/effect-oxlint/index.js";
6
+ import * as Arr from "effect/Array";
6
7
  import * as Option from "effect/Option";
8
+ import * as Predicate from "effect/Predicate";
7
9
  import * as Effect from "effect/Effect";
8
10
  //#region src/rules/no-node-builtin-import.ts
9
11
  const replacedModules = new Map([
@@ -39,30 +41,47 @@ const processOperations = new Set([
39
41
  "stdout"
40
42
  ]);
41
43
  const moduleBase = (source) => {
42
- const withoutPrefix = source.startsWith("node:") ? source.slice(5) : source;
43
- return withoutPrefix.split("/")[0] ?? withoutPrefix;
44
+ let withoutPrefix = source;
45
+ if (source.startsWith("node:")) withoutPrefix = source.slice(5);
46
+ return Option.getOrElse(Arr.head(withoutPrefix.split("/")), () => withoutPrefix);
47
+ };
48
+ const importedName = (specifier) => {
49
+ if (specifier.imported.type === "Identifier") return specifier.imported.name;
50
+ return specifier.imported.value;
44
51
  };
45
- const importedName = (specifier) => specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
46
52
  const memberPath = (node) => {
47
- if (node.computed || node.property.type !== "Identifier") return void 0;
48
- if (node.object.type === "Identifier") return [node.object.name, node.property.name];
49
- if (node.object.type !== "MemberExpression") return void 0;
50
- const parentPath = memberPath(node.object);
51
- return parentPath === void 0 ? void 0 : [...parentPath, node.property.name];
53
+ if (node.computed || node.property.type !== "Identifier") return Option.none();
54
+ const property = node.property.name;
55
+ if (node.object.type === "Identifier") return Option.some([node.object.name, property]);
56
+ if (node.object.type !== "MemberExpression") return Option.none();
57
+ return Option.map(memberPath(node.object), (parentPath) => [...parentPath, property]);
58
+ };
59
+ /** Whether the segment before the operation in `path` is `name`. */
60
+ const followsSegment = (path, name) => Option.contains(Arr.get(path, path.length - 2), name);
61
+ const isDigestUnder = (operation, path, name) => operation === "digest" && followsSegment(path, name);
62
+ const cryptoReplacement = (operation, path) => {
63
+ if (cryptoOperations.has(operation) && path.length === 2) return Option.some("Crypto");
64
+ if (followsSegment(path, "webcrypto") && cryptoOperations.has(operation)) return Option.some("Crypto");
65
+ if (isDigestUnder(operation, path, "subtle")) return Option.some("Crypto.digest");
66
+ return Option.none();
52
67
  };
53
- const partialReplacement = (module, path) => {
54
- const operation = path.at(-1);
55
- if (operation === void 0) return void 0;
56
- if (module === "process") return processOperations.has(operation) && path.length === 2 ? "Config, Stdio, Clock, or Effect scheduling" : void 0;
57
- if (module === "subtle") return operation === "digest" && path.length === 2 ? "Crypto.digest" : void 0;
68
+ const replacementFor = (module, operation, path) => {
69
+ if (module === "process") {
70
+ if (processOperations.has(operation) && path.length === 2) return Option.some("Config, Stdio, Clock, or Effect scheduling");
71
+ return Option.none();
72
+ }
73
+ if (module === "subtle") {
74
+ if (operation === "digest" && path.length === 2) return Option.some("Crypto.digest");
75
+ return Option.none();
76
+ }
58
77
  if (module === "webcrypto") {
59
- if (cryptoOperations.has(operation) && path.length === 2) return "Crypto";
60
- return operation === "digest" && path.at(-2) === "subtle" ? "Crypto.digest" : void 0;
78
+ if (cryptoOperations.has(operation) && path.length === 2) return Option.some("Crypto");
79
+ if (isDigestUnder(operation, path, "subtle")) return Option.some("Crypto.digest");
80
+ return Option.none();
61
81
  }
62
- if (cryptoOperations.has(operation) && path.length === 2) return "Crypto";
63
- if (path.at(-2) === "webcrypto" && cryptoOperations.has(operation)) return "Crypto";
64
- return operation === "digest" && path.at(-2) === "subtle" ? "Crypto.digest" : void 0;
82
+ return cryptoReplacement(operation, path);
65
83
  };
84
+ const partialReplacement = (module, path) => Option.flatMap(Arr.last(path), (operation) => replacementFor(module, operation, path));
66
85
  const noNodeBuiltinImport = define({
67
86
  name: "no-node-builtin-import",
68
87
  meta: meta({
@@ -76,14 +95,17 @@ const noNodeBuiltinImport = define({
76
95
  node,
77
96
  message: `Avoid ${used}. Use ${alternative}; platform adapters may disable this rule explicitly.`
78
97
  }));
98
+ /** The partially replaced module a member path starts from, if its root is an alias. */
99
+ const aliasedModule = (path) => Option.flatMap(Arr.head(path), (alias) => Option.fromUndefinedOr(partialModuleByAlias.get(alias)));
79
100
  return {
80
101
  ImportDeclaration: (node) => {
81
- const declaration = Option.getOrUndefined(narrow(node, "ImportDeclaration"));
82
- if (declaration === void 0) return Effect.void;
102
+ const narrowed = narrow(node, "ImportDeclaration");
103
+ if (Option.isNone(narrowed)) return Effect.void;
104
+ const declaration = narrowed.value;
83
105
  const source = importSource(declaration);
84
106
  const module = moduleBase(source);
85
107
  const alternative = replacedModules.get(module);
86
- if (alternative !== void 0) return report(declaration, `importing '${source}'`, alternative);
108
+ if (Predicate.isNotUndefined(alternative)) return report(declaration, `importing '${source}'`, alternative);
87
109
  if (module !== "crypto" && module !== "process") return Effect.void;
88
110
  const diagnostics = [];
89
111
  for (const specifier of declaration.specifiers) {
@@ -99,18 +121,19 @@ const noNodeBuiltinImport = define({
99
121
  }
100
122
  return Effect.all(diagnostics, { discard: true });
101
123
  },
102
- MemberExpression: (node) => {
103
- const member = Option.getOrUndefined(narrow(node, "MemberExpression"));
104
- if (member === void 0) return Effect.void;
105
- const path = memberPath(member);
106
- if (path === void 0) return Effect.void;
107
- const alias = path[0];
108
- if (alias === void 0) return Effect.void;
109
- const module = partialModuleByAlias.get(alias);
110
- if (module === void 0) return Effect.void;
111
- const alternative = partialReplacement(module, path);
112
- return alternative === void 0 ? Effect.void : report(member, `${path.join(".")}`, alternative);
113
- }
124
+ MemberExpression: (node) => Option.match(narrow(node, "MemberExpression"), {
125
+ onNone: () => Effect.void,
126
+ onSome: (member) => {
127
+ const replacement = Option.flatMap(memberPath(member), (path) => Option.map(Option.flatMap(aliasedModule(path), (module) => partialReplacement(module, path)), (alternative) => ({
128
+ path,
129
+ alternative
130
+ })));
131
+ return Option.match(replacement, {
132
+ onNone: () => Effect.void,
133
+ onSome: ({ path, alternative }) => report(member, `${path.join(".")}`, alternative)
134
+ });
135
+ }
136
+ })
114
137
  };
115
138
  }
116
139
  });
@@ -5,6 +5,7 @@ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
5
5
  import { isGlobalReference } from "../vendor/effect-oxlint/SourceCode.js";
6
6
  import "../vendor/effect-oxlint/index.js";
7
7
  import * as Option from "effect/Option";
8
+ import * as Predicate from "effect/Predicate";
8
9
  import * as Effect from "effect/Effect";
9
10
  //#region src/rules/no-nullish.ts
10
11
  const message = "Avoid null and undefined. Use Option for presence or absence. Use a domain enum when the state has more than two cases.";
@@ -37,7 +38,10 @@ const noNullish = define({
37
38
  }),
38
39
  Literal: (node) => Option.match(narrow(node, "Literal"), {
39
40
  onNone: () => Effect.void,
40
- onSome: (literal) => literal.value === null && !("regex" in literal) && !isNullPrototypeArgument(literal) ? report(literal) : Effect.void
41
+ onSome: (literal) => {
42
+ if (!Predicate.isNull(literal.value) || "regex" in literal || isNullPrototypeArgument(literal)) return Effect.void;
43
+ return report(literal);
44
+ }
41
45
  }),
42
46
  TSNullKeyword: (node) => Option.match(narrow(node, "TSNullKeyword"), {
43
47
  onNone: () => Effect.void,
@@ -2,17 +2,11 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
6
+ import { functionParameterBindingName, functionParameterTypeAnnotation } from "./_anti-slop-function-parameters.js";
7
+ import * as Option from "effect/Option";
5
8
  import * as Effect from "effect/Effect";
6
9
  //#region src/rules/no-object-parameters.ts
7
- function parameterAnnotation(parameter) {
8
- if (parameter.type === "TSParameterProperty") return parameterAnnotation(parameter.parameter);
9
- if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
10
- if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
11
- return parameter.typeAnnotation;
12
- }
13
- function parameterName(parameter, sourceCode) {
14
- return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, "");
15
- }
16
10
  /** Ban the broad object type on function inputs, including local aliases to object. */
17
11
  const noObjectParameters = define({
18
12
  name: "no-object-parameters",
@@ -23,34 +17,26 @@ const noObjectParameters = define({
23
17
  }),
24
18
  create: function* () {
25
19
  const context = yield* RuleContext;
26
- const aliases = /* @__PURE__ */ new Map();
27
- const resolvesToObject = (type, visited = /* @__PURE__ */ new Set()) => {
28
- if (type.type === "TSObjectKeyword") return true;
29
- if (type.type === "TSParenthesizedType") return resolvesToObject(type.typeAnnotation, visited);
30
- if (type.type === "TSUnionType") return type.types.some((member) => resolvesToObject(member, visited));
31
- if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier" || type.typeArguments !== null && type.typeArguments !== void 0 && type.typeArguments.params.length > 0 || visited.has(type.typeName.name)) return false;
32
- const alias = aliases.get(type.typeName.name);
33
- if (alias === void 0) return false;
34
- const nextVisited = new Set(visited);
35
- nextVisited.add(type.typeName.name);
36
- return resolvesToObject(alias, nextVisited);
37
- };
20
+ let environment = Option.none();
21
+ const resolvesToObject = (type) => Option.exists(environment, (present) => resolvedTypeMatches(type, present, (resolved, matches) => {
22
+ if (resolved.type === "TSObjectKeyword") return true;
23
+ if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
24
+ return resolved.type === "TSUnionType" && resolved.types.some(matches);
25
+ }));
38
26
  const checkParameters = (node) => Effect.forEach(node.params, (parameter) => {
39
- const annotation = parameterAnnotation(parameter);
40
- if (annotation === null || annotation === void 0) return Effect.void;
41
- if (!resolvesToObject(annotation.typeAnnotation)) return Effect.void;
27
+ const annotation = functionParameterTypeAnnotation(parameter);
28
+ if (Option.isNone(annotation)) return Effect.void;
29
+ const type = annotation.value.typeAnnotation;
30
+ if (!resolvesToObject(type)) return Effect.void;
42
31
  return context.report(fromId({
43
- node: annotation.typeAnnotation,
32
+ node: type,
44
33
  messageId: "objectParameter",
45
- data: { parameter: parameterName(parameter, context.sourceCode) }
34
+ data: { parameter: functionParameterBindingName(parameter, context.sourceCode) }
46
35
  }));
47
36
  }, { discard: true });
48
37
  return {
49
38
  Program: (node) => {
50
- for (const statement of node.body) {
51
- const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
52
- if (declaration?.type === "TSTypeAliasDeclaration" && (declaration.typeParameters === null || declaration.typeParameters === void 0)) aliases.set(declaration.id.name, declaration.typeAnnotation);
53
- }
39
+ environment = Option.some(createTypeAliasEnvironment(node, context.sourceCode.visitorKeys));
54
40
  return Effect.void;
55
41
  },
56
42
  ArrowFunctionExpression: checkParameters,
@@ -4,6 +4,7 @@ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
5
  import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
6
  import { enclosingEffectProgram } from "./_effect-context.js";
7
+ import * as Option from "effect/Option";
7
8
  import * as Effect from "effect/Effect";
8
9
  //#region src/rules/no-per-call-cache-construction.ts
9
10
  const cachedEffectOperations = new Set([
@@ -36,8 +37,7 @@ const noPerCallCacheConstruction = define({
36
37
  CallExpression: (node) => {
37
38
  if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
38
39
  const effects = visibleNamespaces(ctx, node, effectNamespaces);
39
- const program = enclosingEffectProgram(node, effects);
40
- if (program === void 0 || program[1] === "gen") return Effect.void;
40
+ if (!Option.exists(enclosingEffectProgram(node, effects), ([, kind]) => kind !== "gen")) return Effect.void;
41
41
  const caches = visibleNamespaces(ctx, node, cacheNamespaces);
42
42
  if (!(isOperation(node.callee, effects, cachedEffectOperations) || isOperation(node.callee, caches, cacheConstructors))) return Effect.void;
43
43
  return ctx.report(make({
@@ -0,0 +1,7 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-reduce-accumulator-copy.d.ts
4
+ /** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
5
+ declare const noReduceAccumulatorCopy: _$_oxlint_plugins0.CreateRule;
6
+ //#endregion
7
+ export { noReduceAccumulatorCopy };
@@ -0,0 +1,106 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { arrayMethodTarget, constInitializer, isKnownArrayExpression, resolveArrayBinding, unwrapArrayExpression } from "./_anti-slop-array-method.js";
6
+ import { ancestors } from "./_ast-ancestors.js";
7
+ import * as Arr from "effect/Array";
8
+ import * as Option from "effect/Option";
9
+ import * as Effect from "effect/Effect";
10
+ //#region src/rules/no-reduce-accumulator-copy.ts
11
+ const arrayCopyMethods = new Set([
12
+ "concat",
13
+ "slice",
14
+ "toSpliced",
15
+ "toSorted",
16
+ "toReversed",
17
+ "with"
18
+ ]);
19
+ const isCallback = (node) => node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
20
+ const parameterBinding = (parameter) => {
21
+ if (parameter.type === "AssignmentPattern") return parameter.left;
22
+ return parameter;
23
+ };
24
+ const isReduceCall = (owner, callback) => Option.exists(arrayMethodTarget(owner.callee), (method) => method.name === "reduce" || method.name === "reduceRight") && owner.arguments.length <= 2 && Option.exists(Arr.head(owner.arguments), (firstArgument) => unwrapArrayExpression(firstArgument) === callback);
25
+ function reducerForCallback(callback) {
26
+ const owner = Arr.findFirst(ancestors(callback), (node) => unwrapArrayExpression(node) !== callback);
27
+ return Option.flatMap(owner, (call) => {
28
+ if (call.type !== "CallExpression" || !isReduceCall(call, callback)) return Option.none();
29
+ return Option.flatMap(Arr.head(callback.params), (firstParameter) => {
30
+ const accumulator = parameterBinding(firstParameter);
31
+ if (accumulator.type !== "Identifier") return Option.none();
32
+ return Option.some({
33
+ callback,
34
+ accumulator,
35
+ initialValue: Arr.get(call.arguments, 1)
36
+ });
37
+ });
38
+ });
39
+ }
40
+ function enclosingReducer(node) {
41
+ const boundary = Arr.findFirst(ancestors(node), (parent) => parent.type === "FunctionDeclaration" || isCallback(parent));
42
+ return Option.flatMap(boundary, (parent) => {
43
+ if (!isCallback(parent)) return Option.none();
44
+ return reducerForCallback(parent);
45
+ });
46
+ }
47
+ function referencesAccumulator(sourceCode, node, accumulator, visited = /* @__PURE__ */ new Set()) {
48
+ return Option.exists(resolveArrayBinding(sourceCode, node), (variable) => {
49
+ if (visited.has(variable)) return false;
50
+ if (variable === accumulator) return true;
51
+ visited.add(variable);
52
+ if (variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
53
+ return Option.exists(constInitializer(variable), (init) => referencesAccumulator(sourceCode, init, accumulator, visited));
54
+ });
55
+ }
56
+ function isGlobalCopyOwner(sourceCode, node, name) {
57
+ const owner = unwrapArrayExpression(node);
58
+ if (owner.type !== "Identifier" || owner.name !== name) return false;
59
+ return Option.match(resolveArrayBinding(sourceCode, owner), {
60
+ onNone: () => true,
61
+ onSome: (variable) => variable.defs.length === 0
62
+ });
63
+ }
64
+ function accumulatorCopyCandidate(sourceCode, node) {
65
+ const lookups = Option.all({
66
+ method: arrayMethodTarget(node.callee),
67
+ reducer: enclosingReducer(node)
68
+ });
69
+ return Option.flatMap(lookups, ({ method, reducer }) => {
70
+ const accumulator = Arr.findFirst(sourceCode.getDeclaredVariables(reducer.callback), (variable) => variable.identifiers.some((identifier) => identifier.start === reducer.accumulator.start));
71
+ return Option.map(accumulator, (variable) => ({
72
+ method,
73
+ reducer,
74
+ accumulator: variable
75
+ }));
76
+ });
77
+ }
78
+ /** Reject non-spread copies of reducer accumulators; pair with oxc/no-accumulating-spread. */
79
+ const noReduceAccumulatorCopy = define({
80
+ name: "no-reduce-accumulator-copy",
81
+ meta: meta({
82
+ type: "problem",
83
+ description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods.",
84
+ messages: { accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap." }
85
+ }),
86
+ create: function* () {
87
+ const context = yield* RuleContext;
88
+ const { sourceCode } = context;
89
+ const copiesAccumulator = (node, { method, reducer, accumulator }) => {
90
+ const isAccumulator = (expression) => referencesAccumulator(sourceCode, expression, accumulator);
91
+ if (method.name === "assign" && isGlobalCopyOwner(sourceCode, method.object, "Object")) return Option.exists(Arr.head(node.arguments), (target) => unwrapArrayExpression(target).type === "ObjectExpression") && node.arguments.slice(1).some(isAccumulator);
92
+ if (method.name === "from" && isGlobalCopyOwner(sourceCode, method.object, "Array")) return Option.exists(Arr.head(node.arguments), isAccumulator);
93
+ if (!arrayCopyMethods.has(method.name)) return false;
94
+ return Option.exists(reducer.initialValue, (initialValue) => isKnownArrayExpression(sourceCode, initialValue)) && isAccumulator(method.object);
95
+ };
96
+ return { CallExpression: (node) => {
97
+ if (!Option.exists(accumulatorCopyCandidate(sourceCode, node), (candidate) => copiesAccumulator(node, candidate))) return Effect.void;
98
+ return context.report(fromId({
99
+ node,
100
+ messageId: "accumulatorCopy"
101
+ }));
102
+ } };
103
+ }
104
+ });
105
+ //#endregion
106
+ export { noReduceAccumulatorCopy };
@@ -0,0 +1,7 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-reflect-apply.d.ts
4
+ /** Ban Reflect.apply, which bypasses ordinary typed function calls. */
5
+ declare const noReflectApply: _$_oxlint_plugins0.CreateRule;
6
+ //#endregion
7
+ export { noReflectApply };
@@ -0,0 +1,28 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { isGlobalReflectMethodCall } from "./_anti-slop-reflect-method.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-reflect-apply.ts
8
+ /** Ban Reflect.apply, which bypasses ordinary typed function calls. */
9
+ const noReflectApply = define({
10
+ name: "no-reflect-apply",
11
+ meta: meta({
12
+ type: "problem",
13
+ description: "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface.",
14
+ messages: { reflectApply: "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface." }
15
+ }),
16
+ create: function* () {
17
+ const context = yield* RuleContext;
18
+ return { CallExpression: (node) => {
19
+ if (node.callee.type === "Super" || !isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply")) return Effect.void;
20
+ return context.report(fromId({
21
+ node,
22
+ messageId: "reflectApply"
23
+ }));
24
+ } };
25
+ }
26
+ });
27
+ //#endregion
28
+ export { noReflectApply };
@@ -0,0 +1,7 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-reflect-get.d.ts
4
+ /** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
5
+ declare const noReflectGet: _$_oxlint_plugins0.CreateRule;
6
+ //#endregion
7
+ export { noReflectGet };
@@ -0,0 +1,28 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { isGlobalReflectMethodCall } from "./_anti-slop-reflect-method.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-reflect-get.ts
8
+ /** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
9
+ const noReflectGet = define({
10
+ name: "no-reflect-get",
11
+ meta: meta({
12
+ type: "problem",
13
+ description: "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type.",
14
+ messages: { reflectGet: "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it." }
15
+ }),
16
+ create: function* () {
17
+ const context = yield* RuleContext;
18
+ return { CallExpression: (node) => {
19
+ if (node.callee.type === "Super" || !isGlobalReflectMethodCall(context.sourceCode, node.callee, "get")) return Effect.void;
20
+ return context.report(fromId({
21
+ node,
22
+ messageId: "reflectGet"
23
+ }));
24
+ } };
25
+ }
26
+ });
27
+ //#endregion
28
+ export { noReflectGet };
@@ -2,7 +2,9 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { expressionArgument } from "./_call-arguments.js";
5
6
  import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
7
+ import * as Option from "effect/Option";
6
8
  import * as Effect from "effect/Effect";
7
9
  //#region src/rules/no-run-collect-on-unbounded-stream.ts
8
10
  const unboundedSources = new Set([
@@ -23,29 +25,25 @@ const terminatingOperations = new Set([
23
25
  ]);
24
26
  const staticOperation = (node, streamNamespaces, operations) => {
25
27
  if (node.type === "SpreadElement") return false;
26
- const candidate = node.type === "CallExpression" && node.callee.type !== "Super" ? node.callee : node;
28
+ let candidate = node;
29
+ if (node.type === "CallExpression" && node.callee.type !== "Super") candidate = node.callee;
27
30
  for (const operation of operations) if (isStaticMember(candidate, streamNamespaces, operation)) return true;
28
31
  return false;
29
32
  };
33
+ const isPipeMember = (node) => node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.property.name === "pipe";
34
+ const pipesTerminatingOperation = (node, streamNamespaces) => node.arguments.some((operation) => staticOperation(operation, streamNamespaces, terminatingOperations));
30
35
  const unboundedRoot = (node, streamNamespaces) => {
31
- if (node.type !== "CallExpression" || node.callee.type === "Super") return void 0;
32
- for (const source of unboundedSources) if (isStaticMember(node.callee, streamNamespaces, source)) return node;
33
- if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.callee.property.name !== "pipe") return;
34
- const root = unboundedRoot(node.callee.object, streamNamespaces);
35
- if (root === void 0) return void 0;
36
- for (const operation of node.arguments) if (staticOperation(operation, streamNamespaces, terminatingOperations)) return void 0;
37
- return root;
36
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Option.none();
37
+ for (const source of unboundedSources) if (isStaticMember(node.callee, streamNamespaces, source)) return Option.some(node);
38
+ if (!isPipeMember(node.callee)) return Option.none();
39
+ return Option.filter(unboundedRoot(node.callee.object, streamNamespaces), () => !pipesTerminatingOperation(node, streamNamespaces));
38
40
  };
39
41
  const collectedSource = (node, streamNamespaces) => {
40
- if (node.callee.type === "Super") return void 0;
41
- if (isStaticMember(node.callee, streamNamespaces, "runCollect")) {
42
- const source = node.arguments[0];
43
- if (source !== void 0 && source.type !== "SpreadElement") return source;
44
- return;
45
- }
46
- if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.callee.property.name !== "pipe") return;
47
- for (const operation of node.arguments) if (staticOperation(operation, streamNamespaces, terminatingOperations)) return void 0;
48
- for (const operation of node.arguments) if (staticOperation(operation, streamNamespaces, collectOperations)) return node.callee.object;
42
+ if (node.callee.type === "Super") return Option.none();
43
+ if (isStaticMember(node.callee, streamNamespaces, "runCollect")) return expressionArgument(node, 0);
44
+ if (!isPipeMember(node.callee) || pipesTerminatingOperation(node, streamNamespaces)) return Option.none();
45
+ if (!node.arguments.some((operation) => staticOperation(operation, streamNamespaces, collectOperations))) return Option.none();
46
+ return Option.some(node.callee.object);
49
47
  };
50
48
  const noRunCollectOnUnboundedStream = define({
51
49
  name: "no-run-collect-on-unbounded-stream",
@@ -65,14 +63,14 @@ const noRunCollectOnUnboundedStream = define({
65
63
  CallExpression: (node) => {
66
64
  if (node.type !== "CallExpression") return Effect.void;
67
65
  const namespaces = visibleNamespaces(ctx, node, streamNamespaces);
68
- const source = collectedSource(node, namespaces);
69
- if (source === void 0) return Effect.void;
70
- const root = unboundedRoot(source, namespaces);
71
- if (root === void 0) return Effect.void;
72
- return ctx.report(make({
73
- node: root,
74
- message: "Do not collect a clearly unbounded Stream. Add a terminating operation or consume it with runForEach or runDrain."
75
- }));
66
+ const root = Option.flatMap(collectedSource(node, namespaces), (source) => unboundedRoot(source, namespaces));
67
+ return Option.match(root, {
68
+ onNone: () => Effect.void,
69
+ onSome: (stream) => ctx.report(make({
70
+ node: stream,
71
+ message: "Do not collect a clearly unbounded Stream. Add a terminating operation or consume it with runForEach or runDrain."
72
+ }))
73
+ });
76
74
  }
77
75
  };
78
76
  }
@@ -4,6 +4,20 @@ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
5
  import * as Effect from "effect/Effect";
6
6
  //#region src/rules/no-runtime-typeof.ts
7
+ /** Whether `typeof` probes for a possibly absent binding, which only `typeof` can do safely. */
8
+ function isExistenceProbe(node) {
9
+ const parent = node.parent;
10
+ if (parent?.type !== "BinaryExpression") return false;
11
+ if (![
12
+ "===",
13
+ "!==",
14
+ "==",
15
+ "!="
16
+ ].includes(parent.operator)) return false;
17
+ let other = parent.left;
18
+ if (parent.left === node) other = parent.right;
19
+ return other.type === "Literal" && other.value === "undefined";
20
+ }
7
21
  /** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
8
22
  const noRuntimeTypeof = define({
9
23
  name: "no-runtime-typeof",
@@ -14,10 +28,13 @@ const noRuntimeTypeof = define({
14
28
  }),
15
29
  create: function* () {
16
30
  const context = yield* RuleContext;
17
- return { UnaryExpression: (node) => node.operator === "typeof" ? context.report(fromId({
18
- node,
19
- messageId: "runtimeTypeof"
20
- })) : Effect.void };
31
+ return { UnaryExpression: (node) => {
32
+ if (node.operator !== "typeof" || isExistenceProbe(node)) return Effect.void;
33
+ return context.report(fromId({
34
+ node,
35
+ messageId: "runtimeTypeof"
36
+ }));
37
+ } };
21
38
  }
22
39
  });
23
40
  //#endregion
@@ -2,19 +2,26 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { expressionArgument, staticProperties } from "./_call-arguments.js";
5
6
  import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
7
+ import * as Arr from "effect/Array";
8
+ import * as Option from "effect/Option";
9
+ import * as Predicate from "effect/Predicate";
6
10
  import * as Effect from "effect/Effect";
7
11
  //#region src/rules/no-sequential-effect-all.ts
12
+ const primitiveLiteral = (node) => {
13
+ if (node.type !== "Literal") return Option.none();
14
+ const value = node.value;
15
+ if (Predicate.isNull(value) || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return Option.some(node);
16
+ return Option.none();
17
+ };
18
+ /** The first primitive literal given for `name` in an options object literal. */
8
19
  const propertyLiteral = (node, name) => {
9
- if (node?.type !== "ObjectExpression") return void 0;
10
- for (const property of node.properties) {
11
- if (property.type !== "Property" || property.computed) continue;
12
- if ((property.key.type === "Identifier" && property.key.name === name || property.key.type === "Literal" && property.key.value === name) && property.value.type === "Literal") {
13
- const value = property.value.value;
14
- if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
15
- }
16
- }
20
+ if (node.type !== "ObjectExpression") return Option.none();
21
+ return Arr.findFirst(staticProperties(node, name), (property) => primitiveLiteral(property.value));
17
22
  };
23
+ /** Whether the options set `name` to the literal `expected`. */
24
+ const hasLiteralOption = (options, name, expected) => Option.exists(Option.flatMap(options, (node) => propertyLiteral(node, name)), (literal) => literal.value === expected);
18
25
  const pipedToAsVoid = (node, effectNamespaces) => {
19
26
  const member = node.parent;
20
27
  if (member?.type !== "MemberExpression" || member.object !== node || member.computed || member.property.type !== "Identifier" || member.property.name !== "pipe") return false;
@@ -46,9 +53,9 @@ const noSequentialEffectAll = define({
46
53
  const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
47
54
  if (!isStaticMember(node.callee, namespaces, "all")) return Effect.void;
48
55
  if (node.arguments[0]?.type !== "ArrayExpression") return Effect.void;
49
- const options = node.arguments[1];
50
- if (propertyLiteral(options, "concurrency") !== 1) return Effect.void;
51
- if (!(propertyLiteral(options, "discard") === true || pipedToAsVoid(node, namespaces))) return Effect.void;
56
+ const options = expressionArgument(node, 1);
57
+ if (!hasLiteralOption(options, "concurrency", 1)) return Effect.void;
58
+ if (!(hasLiteralOption(options, "discard", true) || pipedToAsVoid(node, namespaces))) return Effect.void;
52
59
  return ctx.report(make({
53
60
  node,
54
61
  message: "Use Effect.andThen or one flat generator for sequential steps. Reserve Effect.all for value aggregation or real concurrency."
@@ -2,12 +2,19 @@ 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 * as Predicate from "effect/Predicate";
5
6
  import * as Effect from "effect/Effect";
6
7
  //#region src/rules/no-shape-in-symbol-names.ts
7
8
  const forbiddenSymbolName = "shape";
8
9
  function containsForbiddenSymbolName(name) {
9
10
  return name.toLowerCase().includes(forbiddenSymbolName);
10
11
  }
12
+ /** Return whether an identifier names a statically accessed member owned by another value. */
13
+ function isBorrowedMemberName(node) {
14
+ const parent = node.parent;
15
+ if (Predicate.isNull(parent) || parent.type !== "MemberExpression") return false;
16
+ return parent.property === node && !parent.computed;
17
+ }
11
18
  /** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
12
19
  const noShapeInSymbolNames = define({
13
20
  name: "no-shape-in-symbol-names",
@@ -18,11 +25,14 @@ const noShapeInSymbolNames = define({
18
25
  }),
19
26
  create: function* () {
20
27
  const context = yield* RuleContext;
21
- const reportForbiddenSymbolName = (node) => containsForbiddenSymbolName(node.name) ? context.report(fromId({
22
- node,
23
- messageId: "forbiddenSymbolName",
24
- data: { name: node.name }
25
- })) : Effect.void;
28
+ const reportForbiddenSymbolName = (node) => {
29
+ if (!containsForbiddenSymbolName(node.name) || isBorrowedMemberName(node)) return Effect.void;
30
+ return context.report(fromId({
31
+ node,
32
+ messageId: "forbiddenSymbolName",
33
+ data: { name: node.name }
34
+ }));
35
+ };
26
36
  return {
27
37
  Identifier: reportForbiddenSymbolName,
28
38
  PrivateIdentifier: reportForbiddenSymbolName,