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
@@ -0,0 +1,47 @@
1
+ import { importSource, narrow } from "../vendor/effect-oxlint/AST.js";
2
+ import { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
3
+ import "../vendor/effect-oxlint/index.js";
4
+ import * as Option from "effect/Option";
5
+ //#region src/rules/_test-apis.ts
6
+ const bindingsBySource = new Map([
7
+ ["vitest", new Map([["vi", "vi"]])],
8
+ ["@jest/globals", new Map([["jest", "jest"]])],
9
+ ["bun:test", new Map([
10
+ ["jest", "jest"],
11
+ ["mock", "mock"],
12
+ ["spyOn", "spyOn"],
13
+ ["vi", "vi"]
14
+ ])]
15
+ ]);
16
+ /** Runner objects that exist as ambient globals when no import provides them. */
17
+ const globalBinding = (name) => {
18
+ if (name === "jest" || name === "vi") return Option.some(name);
19
+ return Option.none();
20
+ };
21
+ const identifierName = (node) => {
22
+ if (node.type === "Identifier") return Option.some(node.name);
23
+ return Option.none();
24
+ };
25
+ /** Record each named import of a runner binding under its local name. */
26
+ const collectTestBindings = (node, bindings) => {
27
+ const declaration = narrow(node, "ImportDeclaration");
28
+ if (Option.isNone(declaration)) return;
29
+ const bySource = Option.fromUndefinedOr(bindingsBySource.get(importSource(declaration.value)));
30
+ if (Option.isNone(bySource)) return;
31
+ for (const specifier of declaration.value.specifiers) {
32
+ if (specifier.type !== "ImportSpecifier") continue;
33
+ const binding = Option.flatMap(identifierName(specifier.imported), (imported) => Option.fromUndefinedOr(bySource.value.get(imported)));
34
+ if (Option.isSome(binding)) bindings.set(specifier.local.name, binding.value);
35
+ }
36
+ };
37
+ const bindingFromVariable = (variable, bindings) => {
38
+ if (variable.defs.some((definition) => definition.type === "ImportBinding")) return Option.fromUndefinedOr(bindings.get(variable.name));
39
+ return Option.none();
40
+ };
41
+ /** The runner binding `name` refers to at `scope`: an imported binding, or the unshadowed global. */
42
+ const resolveTestBinding = (scope, name, bindings) => Option.match(findVariableUp(scope, name), {
43
+ onNone: () => globalBinding(name),
44
+ onSome: (variable) => Option.orElse(bindingFromVariable(variable, bindings), () => Option.filter(globalBinding(name), () => variable.defs.length === 0))
45
+ });
46
+ //#endregion
47
+ export { collectTestBindings, resolveTestBinding };
@@ -0,0 +1,8 @@
1
+ //#region src/rules/_test-files.ts
2
+ /** Test and spec modules: `*.test.*` and `*.spec.*` with a JS or TS extension. */
3
+ const testFilePattern = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
4
+ const isTestFile = (filename) => testFilePattern.test(filename);
5
+ /** Visitor for a file a rule does not apply to. */
6
+ const skipFile = {};
7
+ //#endregion
8
+ export { isTestFile, skipFile };
@@ -2,15 +2,19 @@ import { noEffectBind } from "./no-effect-bind.js";
2
2
  import { noEffectDo } from "./no-effect-do.js";
3
3
  import { maxCognitiveComplexity } from "./max-cognitive-complexity.js";
4
4
  import { maxHalsteadDifficulty } from "./max-halstead-difficulty.js";
5
+ import { noArrayFilterMap } from "./no-array-filter-map.js";
5
6
  import { noAs } from "./no-as.js";
6
7
  import { noAsyncFunction } from "./no-async-function.js";
7
8
  import { noChainedTypeAssertions } from "./no-chained-type-assertions.js";
8
9
  import { noConditionalEmptyObjectSpread } from "./no-conditional-empty-object-spread.js";
9
10
  import { noDynamicImports } from "./no-dynamic-imports.js";
11
+ import { noEagerAcquire } from "./no-eager-acquire.js";
12
+ import { noEffectRunInTests } from "./no-effect-run-in-tests.js";
10
13
  import { noGlobals } from "./no-globals.js";
11
14
  import { noInlineProvide } from "./no-inline-provide.js";
12
15
  import { noKnownValueWidening } from "./no-known-value-widening.js";
13
16
  import { noManagedRuntimeInEffect } from "./no-managed-runtime-in-effect.js";
17
+ import { noModuleLevelMutableState } from "./no-module-level-mutable-state.js";
14
18
  import { noModuleMocks } from "./no-module-mocks.js";
15
19
  import { noNestedEffectGen } from "./no-nested-effect-gen.js";
16
20
  import { noNewError } from "./no-new-error.js";
@@ -19,18 +23,23 @@ import { noNodeBuiltinImport } from "./no-node-builtin-import.js";
19
23
  import { noNullish } from "./no-nullish.js";
20
24
  import { noObjectParameters } from "./no-object-parameters.js";
21
25
  import { noPerCallCacheConstruction } from "./no-per-call-cache-construction.js";
26
+ import { noReduceAccumulatorCopy } from "./no-reduce-accumulator-copy.js";
27
+ import { noReflectApply } from "./no-reflect-apply.js";
28
+ import { noReflectGet } from "./no-reflect-get.js";
22
29
  import { noRunCollectOnUnboundedStream } from "./no-run-collect-on-unbounded-stream.js";
23
30
  import { noRuntimeTypeof } from "./no-runtime-typeof.js";
24
31
  import { noSequentialEffectAll } from "./no-sequential-effect-all.js";
25
32
  import { noShapeInSymbolNames } from "./no-shape-in-symbol-names.js";
26
33
  import { noSilentCatchAll } from "./no-silent-catch-all.js";
27
34
  import { noTernary } from "./no-ternary.js";
35
+ import { noTestGlobals } from "./no-test-globals.js";
28
36
  import { noTestLifecycleHooks } from "./no-test-lifecycle-hooks.js";
29
37
  import { noThrowStatement } from "./no-throw-statement.js";
30
38
  import { noTryCatch } from "./no-try-catch.js";
31
39
  import { noUnboundedConcurrency } from "./no-unbounded-concurrency.js";
32
40
  import { noUnboundedRetry } from "./no-unbounded-retry.js";
33
41
  import { noUnknownParameters } from "./no-unknown-parameters.js";
42
+ import { noUnknownReturns } from "./no-unknown-returns.js";
34
43
  import { noUnknownTypeAliases } from "./no-unknown-type-aliases.js";
35
44
  import { noUnsafeDictionaryType } from "./no-unsafe-dictionary-type.js";
36
45
  import { noWidenThenAssert } from "./no-widen-then-assert.js";
@@ -40,10 +49,11 @@ import { preferMatchTagsExhaustive } from "./prefer-match-tags-exhaustive.js";
40
49
  import { preferPredicateIsTagged } from "./prefer-predicate-is-tagged.js";
41
50
  import { preferServiceOf } from "./prefer-service-of.js";
42
51
  import { requireNamedEffectFn } from "./require-named-effect-fn.js";
52
+ import { requireSuppressionReason } from "./require-suppression-reason.js";
43
53
 
44
54
  //#region src/rules/index.d.ts
45
55
  declare namespace index_d_exports {
46
- export { maxCognitiveComplexity, maxHalsteadDifficulty, noAs, noAsyncFunction, noChainedTypeAssertions, noConditionalEmptyObjectSpread, noDynamicImports, noEffectBind, noEffectDo, noGlobals, noInlineProvide, noKnownValueWidening, noManagedRuntimeInEffect, noModuleMocks, noNestedEffectGen, noNewError, noNewPromise, noNodeBuiltinImport, noNullish, noObjectParameters, noPerCallCacheConstruction, noRunCollectOnUnboundedStream, noRuntimeTypeof, noSequentialEffectAll, noShapeInSymbolNames, noSilentCatchAll, noTernary, noTestLifecycleHooks, noThrowStatement, noTryCatch, noUnboundedConcurrency, noUnboundedRetry, noUnknownParameters, noUnknownTypeAliases, noUnsafeDictionaryType, noWidenThenAssert, preferCatchTag, preferEffectFn, preferMatchTagsExhaustive, preferPredicateIsTagged, preferServiceOf, requireNamedEffectFn };
56
+ export { maxCognitiveComplexity, maxHalsteadDifficulty, noArrayFilterMap, noAs, noAsyncFunction, noChainedTypeAssertions, noConditionalEmptyObjectSpread, noDynamicImports, noEagerAcquire, noEffectBind, noEffectDo, noEffectRunInTests, noGlobals, noInlineProvide, noKnownValueWidening, noManagedRuntimeInEffect, noModuleLevelMutableState, noModuleMocks, noNestedEffectGen, noNewError, noNewPromise, noNodeBuiltinImport, noNullish, noObjectParameters, noPerCallCacheConstruction, noReduceAccumulatorCopy, noReflectApply, noReflectGet, noRunCollectOnUnboundedStream, noRuntimeTypeof, noSequentialEffectAll, noShapeInSymbolNames, noSilentCatchAll, noTernary, noTestGlobals, noTestLifecycleHooks, noThrowStatement, noTryCatch, noUnboundedConcurrency, noUnboundedRetry, noUnknownParameters, noUnknownReturns, noUnknownTypeAliases, noUnsafeDictionaryType, noWidenThenAssert, preferCatchTag, preferEffectFn, preferMatchTagsExhaustive, preferPredicateIsTagged, preferServiceOf, requireNamedEffectFn, requireSuppressionReason };
47
57
  }
48
58
  //#endregion
49
59
  export { index_d_exports };
@@ -3,15 +3,19 @@ import { noEffectBind } from "./no-effect-bind.js";
3
3
  import { noEffectDo } from "./no-effect-do.js";
4
4
  import { maxCognitiveComplexity } from "./max-cognitive-complexity.js";
5
5
  import { maxHalsteadDifficulty } from "./max-halstead-difficulty.js";
6
+ import { noArrayFilterMap } from "./no-array-filter-map.js";
6
7
  import { noAs } from "./no-as.js";
7
8
  import { noAsyncFunction } from "./no-async-function.js";
8
9
  import { noChainedTypeAssertions } from "./no-chained-type-assertions.js";
9
10
  import { noConditionalEmptyObjectSpread } from "./no-conditional-empty-object-spread.js";
10
11
  import { noDynamicImports } from "./no-dynamic-imports.js";
12
+ import { noEagerAcquire } from "./no-eager-acquire.js";
13
+ import { noEffectRunInTests } from "./no-effect-run-in-tests.js";
11
14
  import { noGlobals } from "./no-globals.js";
12
15
  import { noInlineProvide } from "./no-inline-provide.js";
13
16
  import { noKnownValueWidening } from "./no-known-value-widening.js";
14
17
  import { noManagedRuntimeInEffect } from "./no-managed-runtime-in-effect.js";
18
+ import { noModuleLevelMutableState } from "./no-module-level-mutable-state.js";
15
19
  import { noModuleMocks } from "./no-module-mocks.js";
16
20
  import { noNestedEffectGen } from "./no-nested-effect-gen.js";
17
21
  import { noNewError } from "./no-new-error.js";
@@ -20,18 +24,23 @@ import { noNodeBuiltinImport } from "./no-node-builtin-import.js";
20
24
  import { noNullish } from "./no-nullish.js";
21
25
  import { noObjectParameters } from "./no-object-parameters.js";
22
26
  import { noPerCallCacheConstruction } from "./no-per-call-cache-construction.js";
27
+ import { noReduceAccumulatorCopy } from "./no-reduce-accumulator-copy.js";
28
+ import { noReflectApply } from "./no-reflect-apply.js";
29
+ import { noReflectGet } from "./no-reflect-get.js";
23
30
  import { noRunCollectOnUnboundedStream } from "./no-run-collect-on-unbounded-stream.js";
24
31
  import { noRuntimeTypeof } from "./no-runtime-typeof.js";
25
32
  import { noSequentialEffectAll } from "./no-sequential-effect-all.js";
26
33
  import { noShapeInSymbolNames } from "./no-shape-in-symbol-names.js";
27
34
  import { noSilentCatchAll } from "./no-silent-catch-all.js";
28
35
  import { noTernary } from "./no-ternary.js";
36
+ import { noTestGlobals } from "./no-test-globals.js";
29
37
  import { noTestLifecycleHooks } from "./no-test-lifecycle-hooks.js";
30
38
  import { noThrowStatement } from "./no-throw-statement.js";
31
39
  import { noTryCatch } from "./no-try-catch.js";
32
40
  import { noUnboundedConcurrency } from "./no-unbounded-concurrency.js";
33
41
  import { noUnboundedRetry } from "./no-unbounded-retry.js";
34
42
  import { noUnknownParameters } from "./no-unknown-parameters.js";
43
+ import { noUnknownReturns } from "./no-unknown-returns.js";
35
44
  import { noUnknownTypeAliases } from "./no-unknown-type-aliases.js";
36
45
  import { noUnsafeDictionaryType } from "./no-unsafe-dictionary-type.js";
37
46
  import { noWidenThenAssert } from "./no-widen-then-assert.js";
@@ -41,21 +50,26 @@ import { preferMatchTagsExhaustive } from "./prefer-match-tags-exhaustive.js";
41
50
  import { preferPredicateIsTagged } from "./prefer-predicate-is-tagged.js";
42
51
  import { preferServiceOf } from "./prefer-service-of.js";
43
52
  import { requireNamedEffectFn } from "./require-named-effect-fn.js";
53
+ import { requireSuppressionReason } from "./require-suppression-reason.js";
44
54
  //#region src/rules/index.ts
45
55
  var rules_exports = /* @__PURE__ */ __exportAll({
46
56
  maxCognitiveComplexity: () => maxCognitiveComplexity,
47
57
  maxHalsteadDifficulty: () => maxHalsteadDifficulty,
58
+ noArrayFilterMap: () => noArrayFilterMap,
48
59
  noAs: () => noAs,
49
60
  noAsyncFunction: () => noAsyncFunction,
50
61
  noChainedTypeAssertions: () => noChainedTypeAssertions,
51
62
  noConditionalEmptyObjectSpread: () => noConditionalEmptyObjectSpread,
52
63
  noDynamicImports: () => noDynamicImports,
64
+ noEagerAcquire: () => noEagerAcquire,
53
65
  noEffectBind: () => noEffectBind,
54
66
  noEffectDo: () => noEffectDo,
67
+ noEffectRunInTests: () => noEffectRunInTests,
55
68
  noGlobals: () => noGlobals,
56
69
  noInlineProvide: () => noInlineProvide,
57
70
  noKnownValueWidening: () => noKnownValueWidening,
58
71
  noManagedRuntimeInEffect: () => noManagedRuntimeInEffect,
72
+ noModuleLevelMutableState: () => noModuleLevelMutableState,
59
73
  noModuleMocks: () => noModuleMocks,
60
74
  noNestedEffectGen: () => noNestedEffectGen,
61
75
  noNewError: () => noNewError,
@@ -64,18 +78,23 @@ var rules_exports = /* @__PURE__ */ __exportAll({
64
78
  noNullish: () => noNullish,
65
79
  noObjectParameters: () => noObjectParameters,
66
80
  noPerCallCacheConstruction: () => noPerCallCacheConstruction,
81
+ noReduceAccumulatorCopy: () => noReduceAccumulatorCopy,
82
+ noReflectApply: () => noReflectApply,
83
+ noReflectGet: () => noReflectGet,
67
84
  noRunCollectOnUnboundedStream: () => noRunCollectOnUnboundedStream,
68
85
  noRuntimeTypeof: () => noRuntimeTypeof,
69
86
  noSequentialEffectAll: () => noSequentialEffectAll,
70
87
  noShapeInSymbolNames: () => noShapeInSymbolNames,
71
88
  noSilentCatchAll: () => noSilentCatchAll,
72
89
  noTernary: () => noTernary,
90
+ noTestGlobals: () => noTestGlobals,
73
91
  noTestLifecycleHooks: () => noTestLifecycleHooks,
74
92
  noThrowStatement: () => noThrowStatement,
75
93
  noTryCatch: () => noTryCatch,
76
94
  noUnboundedConcurrency: () => noUnboundedConcurrency,
77
95
  noUnboundedRetry: () => noUnboundedRetry,
78
96
  noUnknownParameters: () => noUnknownParameters,
97
+ noUnknownReturns: () => noUnknownReturns,
79
98
  noUnknownTypeAliases: () => noUnknownTypeAliases,
80
99
  noUnsafeDictionaryType: () => noUnsafeDictionaryType,
81
100
  noWidenThenAssert: () => noWidenThenAssert,
@@ -84,7 +103,8 @@ var rules_exports = /* @__PURE__ */ __exportAll({
84
103
  preferMatchTagsExhaustive: () => preferMatchTagsExhaustive,
85
104
  preferPredicateIsTagged: () => preferPredicateIsTagged,
86
105
  preferServiceOf: () => preferServiceOf,
87
- requireNamedEffectFn: () => requireNamedEffectFn
106
+ requireNamedEffectFn: () => requireNamedEffectFn,
107
+ requireSuppressionReason: () => requireSuppressionReason
88
108
  });
89
109
  //#endregion
90
110
  export { rules_exports };
@@ -0,0 +1,7 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-array-filter-map.d.ts
4
+ /** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */
5
+ declare const noArrayFilterMap: _$_oxlint_plugins0.CreateRule;
6
+ //#endregion
7
+ export { noArrayFilterMap };
@@ -0,0 +1,49 @@
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, isKnownArrayExpression, unwrapArrayExpression } from "./_anti-slop-array-method.js";
6
+ import * as Option from "effect/Option";
7
+ import * as Effect from "effect/Effect";
8
+ //#region src/rules/no-array-filter-map.ts
9
+ const pairedMethod = (name) => {
10
+ if (name === "map") return Option.some("filter");
11
+ if (name === "filter") return Option.some("map");
12
+ return Option.none();
13
+ };
14
+ function filterMapPair(sourceCode, node) {
15
+ return Option.flatMap(arrayMethodTarget(node.callee), (outer) => Option.flatMap(pairedMethod(outer.name), (pairedName) => {
16
+ const innerCall = unwrapArrayExpression(outer.object);
17
+ if (innerCall.type !== "CallExpression") return Option.none();
18
+ const inner = Option.filter(arrayMethodTarget(innerCall.callee), (method) => method.name === pairedName && isKnownArrayExpression(sourceCode, method.object));
19
+ return Option.map(inner, (method) => ({
20
+ first: method.name,
21
+ second: outer.name
22
+ }));
23
+ }));
24
+ }
25
+ /** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */
26
+ const noArrayFilterMap = define({
27
+ name: "no-array-filter-map",
28
+ meta: meta({
29
+ type: "suggestion",
30
+ description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation.",
31
+ messages: { arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics." }
32
+ }),
33
+ create: function* () {
34
+ const context = yield* RuleContext;
35
+ return { CallExpression: (node) => Option.match(filterMapPair(context.sourceCode, node), {
36
+ onNone: () => Effect.void,
37
+ onSome: ({ first, second }) => context.report(fromId({
38
+ node,
39
+ messageId: "arrayFilterMap",
40
+ data: {
41
+ first,
42
+ second
43
+ }
44
+ }))
45
+ }) };
46
+ }
47
+ });
48
+ //#endregion
49
+ export { noArrayFilterMap };
@@ -46,10 +46,13 @@ const noChainedTypeAssertions = define({
46
46
  }),
47
47
  create: function* () {
48
48
  const context = yield* RuleContext;
49
- const checkTypeAssertion = (node) => isOutermostAssertionInChain(node) && isForbiddenAssertionChain(node) ? context.report(fromId({
50
- node,
51
- messageId: "chained"
52
- })) : Effect.void;
49
+ const checkTypeAssertion = (node) => {
50
+ if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return Effect.void;
51
+ return context.report(fromId({
52
+ node,
53
+ messageId: "chained"
54
+ }));
55
+ };
53
56
  return {
54
57
  TSAsExpression: checkTypeAssertion,
55
58
  TSTypeAssertion: checkTypeAssertion
@@ -1,7 +1,7 @@
1
1
  import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
2
 
3
3
  //#region src/rules/no-conditional-empty-object-spread.d.ts
4
- /** Ban conditional empty-object spreads and autofix equivalent direct property declarations. */
4
+ /** Ban conditional empty-object spreads without changing their omission semantics. */
5
5
  declare const noConditionalEmptyObjectSpread: _$_oxlint_plugins0.CreateRule;
6
6
  //#endregion
7
7
  export { noConditionalEmptyObjectSpread };
@@ -1,4 +1,4 @@
1
- import { fromId, replaceText, withFix } from "../vendor/effect-oxlint/Diagnostic.js";
1
+ 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";
@@ -12,66 +12,26 @@ function unwrapParentheses(node) {
12
12
  function isEmptyObjectExpression(node) {
13
13
  return node.type === "ObjectExpression" && node.properties.length === 0;
14
14
  }
15
- function singleObjectProperty(node) {
16
- if (node.type !== "ObjectExpression" || node.properties.length !== 1) return null;
17
- const [property] = node.properties;
18
- if (property?.type !== "Property" || property.kind !== "init" || property.method || property.computed) return null;
19
- return property;
20
- }
21
- function conditionalEmptyObjectSpread(node) {
15
+ function isConditionalEmptyObjectSpread(node) {
22
16
  const conditional = unwrapParentheses(node);
23
- if (conditional.type !== "ConditionalExpression") return null;
24
- if (isEmptyObjectExpression(conditional.consequent)) return {
25
- conditional,
26
- property: singleObjectProperty(conditional.alternate)
27
- };
28
- if (isEmptyObjectExpression(conditional.alternate)) return {
29
- conditional,
30
- property: singleObjectProperty(conditional.consequent)
31
- };
32
- return null;
33
- }
34
- function undefinedCheckedExpression(test) {
35
- const binary = unwrapParentheses(test);
36
- if (binary.type !== "BinaryExpression") return null;
37
- if (binary.operator !== "===" && binary.operator !== "!==") return null;
38
- const left = unwrapParentheses(binary.left);
39
- const right = unwrapParentheses(binary.right);
40
- const leftIsUndefined = left.type === "Identifier" && left.name === "undefined";
41
- if (leftIsUndefined === (right.type === "Identifier" && right.name === "undefined")) return null;
42
- return {
43
- expression: leftIsUndefined ? right : left,
44
- isDefinedWhenTrue: binary.operator === "!=="
45
- };
46
- }
47
- function canAutofixConditionalEmptyObjectSpread(sourceCode, conditional, property) {
48
- const checked = undefinedCheckedExpression(conditional.test);
49
- if (checked === null) return false;
50
- if (conditional.consequent === property.parent !== checked.isDefinedWhenTrue) return false;
51
- return sourceCode.getText(unwrapParentheses(checked.expression)) === sourceCode.getText(property.value);
17
+ return conditional.type === "ConditionalExpression" && (isEmptyObjectExpression(conditional.consequent) || isEmptyObjectExpression(conditional.alternate));
52
18
  }
53
- /** Ban conditional empty-object spreads and autofix equivalent direct property declarations. */
19
+ /** Ban conditional empty-object spreads without changing their omission semantics. */
54
20
  const noConditionalEmptyObjectSpread = define({
55
21
  name: "no-conditional-empty-object-spread",
56
22
  meta: meta({
57
23
  type: "suggestion",
58
- fixable: "code",
59
24
  description: "Disallow object spreads that conditionally spread an empty object to omit fields.",
60
25
  messages: { avoid: "Do not use conditional empty-object spreads. Prefer a direct property or build the object in separate statements." }
61
26
  }),
62
27
  create: function* () {
63
28
  const context = yield* RuleContext;
64
29
  return { SpreadElement: (node) => {
65
- if (node.parent?.type !== "ObjectExpression") return Effect.void;
66
- const match = conditionalEmptyObjectSpread(node.argument);
67
- if (match === null) return Effect.void;
68
- const { conditional, property } = match;
69
- const diagnostic = fromId({
30
+ if (node.parent?.type !== "ObjectExpression" || !isConditionalEmptyObjectSpread(node.argument)) return Effect.void;
31
+ return context.report(fromId({
70
32
  node,
71
33
  messageId: "avoid"
72
- });
73
- if (property === null || !canAutofixConditionalEmptyObjectSpread(context.sourceCode, conditional, property)) return context.report(diagnostic);
74
- return context.report(withFix(diagnostic, replaceText(node, context.sourceCode.getText(property))));
34
+ }));
75
35
  } };
76
36
  }
77
37
  });
@@ -3,14 +3,20 @@ 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 { ancestors, parentOf } from "./_ast-ancestors.js";
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-dynamic-imports.ts
9
- const identifierName = (node) => node?.type === "Identifier" ? node.name : void 0;
11
+ const identifierName = (node) => {
12
+ if (node.type === "Identifier") return Option.some(node.name);
13
+ return Option.none();
14
+ };
15
+ const isIdentifierNamed = (node, name) => Option.contains(identifierName(node), name);
10
16
  const hasNamedBinding = (pattern) => {
11
17
  if (pattern.type === "Identifier") return true;
12
18
  if (pattern.type === "ObjectPattern") return pattern.properties.length > 0;
13
- if (pattern.type === "ArrayPattern") return pattern.elements.some((element) => element !== null);
19
+ if (pattern.type === "ArrayPattern") return pattern.elements.some(Predicate.isNotNull);
14
20
  return false;
15
21
  };
16
22
  const isNamedVariable = (node, value) => node.type === "VariableDeclarator" && node.init === value && hasNamedBinding(node.id);
@@ -22,42 +28,45 @@ const transparentWrappers = new Set([
22
28
  "TSTypeAssertion",
23
29
  "YieldExpression"
24
30
  ]);
31
+ /** The outermost node reached from `start` through transparent wrapper ancestors. */
25
32
  const climbTransparent = (start) => {
26
33
  let current = start;
27
- while (current.parent != null && transparentWrappers.has(current.parent.type)) current = current.parent;
34
+ for (const parent of ancestors(start)) {
35
+ if (!transparentWrappers.has(parent.type)) break;
36
+ current = parent;
37
+ }
28
38
  return current;
29
39
  };
30
40
  const isNamedDirectBinding = (node) => {
31
41
  const value = climbTransparent(node);
32
- return value.parent != null && isNamedVariable(value.parent, value);
42
+ return Option.exists(parentOf(value), (parent) => isNamedVariable(parent, value));
33
43
  };
44
+ const isNamedFunctionDeclarationBody = (block) => block.type === "BlockStatement" && Option.exists(parentOf(block), (fn) => fn.type === "FunctionDeclaration" && Predicate.isNotNull(fn.id));
34
45
  const isNamedFunctionBoundary = (node) => {
35
46
  const parent = node.parent;
36
- if (parent?.type === "ArrowFunctionExpression" && parent.body === node) return parent.parent != null && isNamedVariable(parent.parent, parent);
47
+ if (parent?.type === "ArrowFunctionExpression" && parent.body === node) return Option.exists(parentOf(parent), (declarator) => isNamedVariable(declarator, parent));
37
48
  if (parent?.type !== "ReturnStatement" || parent.argument !== node) return false;
38
- const block = parent.parent;
39
- const fn = block?.parent;
40
- return block?.type === "BlockStatement" && fn?.type === "FunctionDeclaration" && fn.id !== null;
49
+ return Option.exists(parentOf(parent), isNamedFunctionDeclarationBody);
41
50
  };
42
51
  const isEffectPromiseCall = (node) => {
43
52
  if (node.type !== "CallExpression") return false;
44
53
  const callee = node.callee;
45
54
  if (callee.type !== "MemberExpression" || callee.computed) return false;
46
- return identifierName(callee.object) === "Effect" && ["promise", "tryPromise"].includes(identifierName(callee.property) ?? "");
55
+ return isIdentifierNamed(callee.object, "Effect") && Option.exists(identifierName(callee.property), (name) => ["promise", "tryPromise"].includes(name));
47
56
  };
48
57
  const isEffectPromiseBoundary = (node) => {
49
58
  const callback = node.parent;
50
59
  if (callback?.type !== "ArrowFunctionExpression" || callback.body !== node) return false;
51
- const call = callback.parent;
52
- return call != null && isEffectPromiseCall(call);
60
+ return Option.exists(parentOf(callback), isEffectPromiseCall);
53
61
  };
54
62
  const isNamedLazyBoundary = (node) => {
55
63
  return isNamedDirectBinding(node) || isNamedFunctionBoundary(node) || isEffectPromiseBoundary(node);
56
64
  };
57
65
  const dynamicRequireMessage = (callee) => {
58
- if (identifierName(callee) === "require") return "Avoid require(). Use a static import.";
59
- if (callee?.type !== "MemberExpression") return void 0;
60
- return identifierName(callee.object) === "module" && identifierName(callee.property) === "require" ? "Avoid module.require(). Use a static import." : void 0;
66
+ if (isIdentifierNamed(callee, "require")) return Option.some("Avoid require(). Use a static import.");
67
+ if (callee.type !== "MemberExpression") return Option.none();
68
+ if (isIdentifierNamed(callee.object, "module") && isIdentifierNamed(callee.property, "require")) return Option.some("Avoid module.require(). Use a static import.");
69
+ return Option.none();
61
70
  };
62
71
  const noDynamicImports = define({
63
72
  name: "no-dynamic-imports",
@@ -78,7 +87,7 @@ const noDynamicImports = define({
78
87
  onNone: () => Effect.void,
79
88
  onSome: (declaration) => {
80
89
  if (!["module", "node:module"].includes(importSource(declaration))) return Effect.void;
81
- for (const specifier of declaration.specifiers) if (specifier.type === "ImportSpecifier" && identifierName(specifier.imported) === "createRequire") createRequireNames.add(specifier.local.name);
90
+ for (const specifier of declaration.specifiers) if (specifier.type === "ImportSpecifier" && isIdentifierNamed(specifier.imported, "createRequire")) createRequireNames.add(specifier.local.name);
82
91
  return Effect.void;
83
92
  }
84
93
  }),
@@ -94,21 +103,20 @@ const noDynamicImports = define({
94
103
  }),
95
104
  ImportExpression: (node) => Option.match(narrow(node, "ImportExpression"), {
96
105
  onNone: () => Effect.void,
97
- onSome: (importExpression) => isNamedLazyBoundary(importExpression) ? Effect.void : ctx.report(make({
98
- node,
99
- message: "Avoid inline dynamic imports. Bind the imported module or a lazy loader to a descriptive name before using it."
100
- }))
106
+ onSome: (importExpression) => {
107
+ if (isNamedLazyBoundary(importExpression)) return Effect.void;
108
+ return report(node, "Avoid inline dynamic imports. Bind the imported module or a lazy loader to a descriptive name before using it.");
109
+ }
101
110
  }),
102
111
  CallExpression: (node) => {
103
112
  return Option.match(narrow(node, "CallExpression"), {
104
113
  onNone: () => Effect.void,
105
114
  onSome: (call) => {
106
- const calleeName = identifierName(call.callee);
107
- const message = calleeName !== void 0 && requireAliases.has(calleeName) ? "Avoid createRequire aliases. Keep module loading static." : dynamicRequireMessage(call.callee);
108
- return message === void 0 ? Effect.void : ctx.report(make({
109
- node: call,
110
- message
111
- }));
115
+ const aliasMessage = Option.map(Option.filter(identifierName(call.callee), (name) => requireAliases.has(name)), () => "Avoid createRequire aliases. Keep module loading static.");
116
+ return Option.match(Option.orElse(aliasMessage, () => dynamicRequireMessage(call.callee)), {
117
+ onNone: () => Effect.void,
118
+ onSome: (message) => report(call, message)
119
+ });
112
120
  }
113
121
  });
114
122
  }
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-eager-acquire.d.ts
4
+ declare const noEagerAcquire: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noEagerAcquire };
@@ -0,0 +1,98 @@
1
+ import { make } 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 { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
5
+ import "../vendor/effect-oxlint/index.js";
6
+ import { ancestors } from "./_ast-ancestors.js";
7
+ import { expressionArgument } from "./_call-arguments.js";
8
+ import { importedNamespaces, isStaticCall, visibleNamespaces } from "./_effect-namespaces.js";
9
+ import * as Arr from "effect/Array";
10
+ import * as Option from "effect/Option";
11
+ import * as Predicate from "effect/Predicate";
12
+ import * as Effect from "effect/Effect";
13
+ //#region src/rules/no-eager-acquire.ts
14
+ const unwrapExpression = (node) => {
15
+ let current = node;
16
+ while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion") current = current.expression;
17
+ return current;
18
+ };
19
+ const isWithin = (node, ancestor) => {
20
+ if (node === ancestor) return true;
21
+ for (const current of ancestors(node)) if (current === ancestor) return true;
22
+ return false;
23
+ };
24
+ const isThunk = (node) => node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
25
+ /** The value a thunk returns, when its body is an expression or holds exactly one return. */
26
+ const singleReturnedValue = (thunk) => {
27
+ const body = thunk.body;
28
+ if (Predicate.isNull(body)) return Option.none();
29
+ if (body.type !== "BlockStatement") return Option.some(body);
30
+ const returns = body.body.filter((statement) => statement.type === "ReturnStatement");
31
+ if (returns.length !== 1) return Option.none();
32
+ return Option.flatMap(Arr.head(returns), (returned) => Option.fromNullOr(returned.argument));
33
+ };
34
+ const noEagerAcquire = define({
35
+ name: "no-eager-acquire",
36
+ meta: meta({
37
+ type: "problem",
38
+ description: "Construct a resource inside the Effect.acquireRelease acquire Effect, not before it."
39
+ }),
40
+ create: function* () {
41
+ const ctx = yield* RuleContext;
42
+ const effectNamespaces = new Set(["Effect"]);
43
+ /** Whether the value was bound outside the thunk, so it exists before acquire runs. */
44
+ const isCaptured = (expression, thunk, seen) => {
45
+ const value = unwrapExpression(expression);
46
+ if (value.type === "ThisExpression") return true;
47
+ if (value.type === "MemberExpression") return value.object.type !== "Super" && isCaptured(value.object, thunk, seen);
48
+ if (value.type !== "Identifier") return false;
49
+ return Option.match(findVariableUp(ctx.sourceCode.getScope(value), value.name), {
50
+ onNone: () => true,
51
+ onSome: (variable) => Option.match(Arr.head(variable.defs), {
52
+ onNone: () => true,
53
+ onSome: (definition) => isCapturedDefinition(definition.node, value.name, thunk, seen)
54
+ })
55
+ });
56
+ };
57
+ /** Whether a binding's definition holds a value that exists before the thunk runs. */
58
+ const isCapturedDefinition = (definition, name, thunk, seen) => {
59
+ if (seen.has(definition)) return false;
60
+ seen.add(definition);
61
+ if (!isWithin(definition, thunk)) return true;
62
+ if (definition.type === "VariableDeclarator") {
63
+ const init = definition.init;
64
+ return Predicate.isNotNull(init) && isCaptured(init, thunk, seen);
65
+ }
66
+ return Option.exists(Arr.findFirst(thunk.params, (parameter) => parameter.type === "AssignmentPattern" && parameter.left.type === "Identifier" && parameter.left.name === name), (defaulted) => isCaptured(defaulted.right, thunk, seen));
67
+ };
68
+ const returnsCapturedHandle = (expression, effects) => {
69
+ if (!isStaticCall(expression, effects, "sync")) return false;
70
+ const thunk = Option.filter(Option.map(expressionArgument(expression, 0), unwrapExpression), isThunk);
71
+ return Option.exists(thunk, (lazy) => Option.exists(singleReturnedValue(lazy), (returned) => isCaptured(returned, lazy, /* @__PURE__ */ new Set())));
72
+ };
73
+ return {
74
+ ImportDeclaration: (node) => {
75
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
76
+ return Effect.void;
77
+ },
78
+ CallExpression: (node) => {
79
+ if (node.callee.type === "Super") return Effect.void;
80
+ const effects = visibleNamespaces(ctx, node, effectNamespaces);
81
+ if (!isStaticCall(node, effects, "acquireRelease")) return Effect.void;
82
+ const eagerAcquire = Option.filter(expressionArgument(node, 0), (acquire) => {
83
+ const expression = unwrapExpression(acquire);
84
+ return isStaticCall(expression, effects, "succeed") || returnsCapturedHandle(expression, effects);
85
+ });
86
+ return Option.match(eagerAcquire, {
87
+ onNone: () => Effect.void,
88
+ onSome: (acquire) => ctx.report(make({
89
+ node: acquire,
90
+ message: "Build the resource inside acquire (Effect.sync(() => new Resource()), Effect.tryPromise, or another lazy Effect). Effect.succeed(handle) or Effect.sync(() => handle) creates it before acquire runs, so an interruption in between leaks it."
91
+ }))
92
+ });
93
+ }
94
+ };
95
+ }
96
+ });
97
+ //#endregion
98
+ export { noEagerAcquire };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-effect-run-in-tests.d.ts
4
+ declare const noEffectRunInTests: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noEffectRunInTests };