oxlint-plugin-effect 0.3.0 → 0.4.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 (146) hide show
  1. package/README.md +79 -38
  2. package/dist/presets/index.d.ts +3 -9
  3. package/dist/presets/index.js +3 -17
  4. package/dist/presets/recommended.d.ts +17 -0
  5. package/dist/presets/recommended.js +17 -0
  6. package/dist/rules/index.d.ts +4 -61
  7. package/dist/rules/index.js +4 -118
  8. package/dist/rules/no-async-function.js +8 -13
  9. package/dist/rules/no-dynamic-imports.js +89 -60
  10. package/dist/rules/no-globals.js +164 -127
  11. package/dist/rules/no-new-error.js +26 -13
  12. package/dist/rules/no-new-promise.js +22 -21
  13. package/dist/rules/no-node-builtin-import.js +98 -41
  14. package/dist/rules/no-throw-statement.js +4 -16
  15. package/dist/rules/no-try-catch.js +4 -16
  16. package/package.json +2 -2
  17. package/dist/presets/core.d.ts +0 -50
  18. package/dist/presets/core.js +0 -50
  19. package/dist/presets/effect-native.d.ts +0 -29
  20. package/dist/presets/effect-native.js +0 -29
  21. package/dist/presets/full.d.ts +0 -68
  22. package/dist/presets/full.js +0 -68
  23. package/dist/presets/functional.d.ts +0 -22
  24. package/dist/presets/functional.js +0 -22
  25. package/dist/presets/strict.d.ts +0 -73
  26. package/dist/presets/strict.js +0 -73
  27. package/dist/presets/v3-strict.d.ts +0 -16
  28. package/dist/presets/v3-strict.js +0 -16
  29. package/dist/presets/v3.d.ts +0 -18
  30. package/dist/presets/v3.js +0 -18
  31. package/dist/rules/_effect-context.d.ts +0 -19
  32. package/dist/rules/_effect-context.js +0 -22
  33. package/dist/rules/no-arrow-ladder.d.ts +0 -6
  34. package/dist/rules/no-arrow-ladder.js +0 -61
  35. package/dist/rules/no-catch-all-to-map-error.d.ts +0 -6
  36. package/dist/rules/no-catch-all-to-map-error.js +0 -67
  37. package/dist/rules/no-effect-all-step-sequencing.d.ts +0 -6
  38. package/dist/rules/no-effect-all-step-sequencing.js +0 -63
  39. package/dist/rules/no-effect-as.d.ts +0 -6
  40. package/dist/rules/no-effect-as.js +0 -16
  41. package/dist/rules/no-effect-async.d.ts +0 -6
  42. package/dist/rules/no-effect-async.js +0 -16
  43. package/dist/rules/no-effect-fn-generator.d.ts +0 -6
  44. package/dist/rules/no-effect-fn-generator.js +0 -30
  45. package/dist/rules/no-effect-gen-adapter.d.ts +0 -6
  46. package/dist/rules/no-effect-gen-adapter.js +0 -30
  47. package/dist/rules/no-effect-map-flatten.d.ts +0 -6
  48. package/dist/rules/no-effect-map-flatten.js +0 -53
  49. package/dist/rules/no-effect-map-void.d.ts +0 -6
  50. package/dist/rules/no-effect-map-void.js +0 -40
  51. package/dist/rules/no-effect-never.d.ts +0 -6
  52. package/dist/rules/no-effect-never.js +0 -16
  53. package/dist/rules/no-effect-orElse-ladder.d.ts +0 -6
  54. package/dist/rules/no-effect-orElse-ladder.js +0 -53
  55. package/dist/rules/no-effect-succeed-string.d.ts +0 -6
  56. package/dist/rules/no-effect-succeed-string.js +0 -30
  57. package/dist/rules/no-effect-succeed-variable.d.ts +0 -6
  58. package/dist/rules/no-effect-succeed-variable.js +0 -30
  59. package/dist/rules/no-effect-succeed-void.d.ts +0 -6
  60. package/dist/rules/no-effect-succeed-void.js +0 -33
  61. package/dist/rules/no-effect-sync-console.d.ts +0 -6
  62. package/dist/rules/no-effect-sync-console.js +0 -63
  63. package/dist/rules/no-effect-sync-wrapper.d.ts +0 -6
  64. package/dist/rules/no-effect-sync-wrapper.js +0 -37
  65. package/dist/rules/no-effect-type-alias.d.ts +0 -6
  66. package/dist/rules/no-effect-type-alias.js +0 -52
  67. package/dist/rules/no-effect-wrapper-alias.d.ts +0 -6
  68. package/dist/rules/no-effect-wrapper-alias.js +0 -69
  69. package/dist/rules/no-extends-native-error.d.ts +0 -6
  70. package/dist/rules/no-extends-native-error.js +0 -41
  71. package/dist/rules/no-flatmap-ladder.d.ts +0 -6
  72. package/dist/rules/no-flatmap-ladder.js +0 -48
  73. package/dist/rules/no-fromnullable-coalesce.d.ts +0 -6
  74. package/dist/rules/no-fromnullable-coalesce.js +0 -36
  75. package/dist/rules/no-global-error-in-catch.d.ts +0 -6
  76. package/dist/rules/no-global-error-in-catch.js +0 -75
  77. package/dist/rules/no-global-error-in-failure.d.ts +0 -6
  78. package/dist/rules/no-global-error-in-failure.js +0 -54
  79. package/dist/rules/no-hand-rolled-tagged-union.d.ts +0 -6
  80. package/dist/rules/no-hand-rolled-tagged-union.js +0 -70
  81. package/dist/rules/no-if-statement.d.ts +0 -6
  82. package/dist/rules/no-if-statement.js +0 -18
  83. package/dist/rules/no-iife-wrapper.d.ts +0 -6
  84. package/dist/rules/no-iife-wrapper.js +0 -37
  85. package/dist/rules/no-inline-provide.d.ts +0 -6
  86. package/dist/rules/no-inline-provide.js +0 -31
  87. package/dist/rules/no-instanceof-schema.d.ts +0 -6
  88. package/dist/rules/no-instanceof-schema.js +0 -35
  89. package/dist/rules/no-make-unsafe.d.ts +0 -6
  90. package/dist/rules/no-make-unsafe.js +0 -30
  91. package/dist/rules/no-manual-data-guard.d.ts +0 -6
  92. package/dist/rules/no-manual-data-guard.js +0 -56
  93. package/dist/rules/no-manual-effect-channels.d.ts +0 -6
  94. package/dist/rules/no-manual-effect-channels.js +0 -40
  95. package/dist/rules/no-match-effect-branch.d.ts +0 -6
  96. package/dist/rules/no-match-effect-branch.js +0 -64
  97. package/dist/rules/no-match-void-branch.d.ts +0 -6
  98. package/dist/rules/no-match-void-branch.js +0 -50
  99. package/dist/rules/no-multiple-effect-provide.d.ts +0 -6
  100. package/dist/rules/no-multiple-effect-provide.js +0 -45
  101. package/dist/rules/no-nested-effect-call.d.ts +0 -6
  102. package/dist/rules/no-nested-effect-call.js +0 -97
  103. package/dist/rules/no-nested-effect-gen.d.ts +0 -6
  104. package/dist/rules/no-nested-effect-gen.js +0 -46
  105. package/dist/rules/no-nested-pipe.d.ts +0 -6
  106. package/dist/rules/no-nested-pipe.js +0 -38
  107. package/dist/rules/no-option-as.d.ts +0 -6
  108. package/dist/rules/no-option-as.js +0 -16
  109. package/dist/rules/no-option-boolean-normalization.d.ts +0 -6
  110. package/dist/rules/no-option-boolean-normalization.js +0 -50
  111. package/dist/rules/no-positional-log-error.d.ts +0 -6
  112. package/dist/rules/no-positional-log-error.js +0 -52
  113. package/dist/rules/no-promise-control-flow-in-tests.d.ts +0 -6
  114. package/dist/rules/no-promise-control-flow-in-tests.js +0 -94
  115. package/dist/rules/no-redundant-schema-tag-identifier.d.ts +0 -6
  116. package/dist/rules/no-redundant-schema-tag-identifier.js +0 -46
  117. package/dist/rules/no-return-in-arrow.d.ts +0 -6
  118. package/dist/rules/no-return-in-arrow.js +0 -41
  119. package/dist/rules/no-return-nullish.d.ts +0 -6
  120. package/dist/rules/no-return-nullish.js +0 -47
  121. package/dist/rules/no-run-in-effect-gen.d.ts +0 -6
  122. package/dist/rules/no-run-in-effect-gen.js +0 -48
  123. package/dist/rules/no-runtime-run-fork.d.ts +0 -6
  124. package/dist/rules/no-runtime-run-fork.js +0 -19
  125. package/dist/rules/no-schema-struct-with-tag.d.ts +0 -6
  126. package/dist/rules/no-schema-struct-with-tag.js +0 -48
  127. package/dist/rules/no-schema-struct.d.ts +0 -6
  128. package/dist/rules/no-schema-struct.js +0 -28
  129. package/dist/rules/no-schema-union-of-literals.d.ts +0 -6
  130. package/dist/rules/no-schema-union-of-literals.js +0 -35
  131. package/dist/rules/no-sleep-in-tests.d.ts +0 -6
  132. package/dist/rules/no-sleep-in-tests.js +0 -55
  133. package/dist/rules/no-spread.d.ts +0 -6
  134. package/dist/rules/no-spread.js +0 -26
  135. package/dist/rules/no-string-sentinel-const.d.ts +0 -6
  136. package/dist/rules/no-string-sentinel-const.js +0 -38
  137. package/dist/rules/no-switch-statement.d.ts +0 -6
  138. package/dist/rules/no-switch-statement.js +0 -16
  139. package/dist/rules/no-unnecessary-arrow-block.d.ts +0 -6
  140. package/dist/rules/no-unnecessary-arrow-block.js +0 -34
  141. package/dist/rules/no-unnecessary-effect-gen.d.ts +0 -6
  142. package/dist/rules/no-unnecessary-effect-gen.js +0 -35
  143. package/dist/rules/no-unnecessary-pipe-chain.d.ts +0 -6
  144. package/dist/rules/no-unnecessary-pipe-chain.js +0 -36
  145. package/dist/rules/no-unnecessary-pipe.d.ts +0 -6
  146. package/dist/rules/no-unnecessary-pipe.js +0 -29
@@ -1,87 +1,116 @@
1
+ import { importSource, narrow } from "../vendor/effect-oxlint/AST.js";
1
2
  import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
3
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
4
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
5
  import "../vendor/effect-oxlint/index.js";
6
+ import * as Option from "effect/Option";
5
7
  import * as Effect from "effect/Effect";
6
8
  //#region src/rules/no-dynamic-imports.ts
7
- const allowPattern = /\beffect\/no-dynamic-imports:\s*allow\s+\S/;
8
- const isAstNode = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
9
- const getNodeField = (node, field) => {
10
- const value = node[field];
11
- return isAstNode(value) ? value : void 0;
9
+ const identifierName = (node) => node?.type === "Identifier" ? node.name : void 0;
10
+ const hasNamedBinding = (pattern) => {
11
+ if (pattern.type === "Identifier") return true;
12
+ if (pattern.type === "ObjectPattern") return pattern.properties.length > 0;
13
+ if (pattern.type === "ArrayPattern") return pattern.elements.some((element) => element !== null);
14
+ return false;
12
15
  };
13
- const getStringField = (node, field) => {
14
- const value = node[field];
15
- return typeof value === "string" ? value : void 0;
16
+ const isNamedVariable = (node, value) => node.type === "VariableDeclarator" && node.init === value && hasNamedBinding(node.id);
17
+ const transparentWrappers = new Set([
18
+ "AwaitExpression",
19
+ "ChainExpression",
20
+ "TSAsExpression",
21
+ "TSNonNullExpression",
22
+ "TSTypeAssertion",
23
+ "YieldExpression"
24
+ ]);
25
+ const climbTransparent = (start) => {
26
+ let current = start;
27
+ while (current.parent != null && transparentWrappers.has(current.parent.type)) current = current.parent;
28
+ return current;
16
29
  };
17
- const getLine = (node, edge) => {
18
- const loc = node["loc"];
19
- if (typeof loc !== "object" || loc === null) return void 0;
20
- const point = loc[edge];
21
- if (typeof point !== "object" || point === null) return void 0;
22
- const line = point["line"];
23
- return typeof line === "number" ? line : void 0;
30
+ const isNamedDirectBinding = (node) => {
31
+ const value = climbTransparent(node);
32
+ return value.parent != null && isNamedVariable(value.parent, value);
24
33
  };
25
- const hasAllowComment = (node, comments) => {
26
- const startLine = getLine(node, "start");
27
- if (startLine === void 0) return false;
28
- return comments.some((comment) => {
29
- const endLine = getLine(comment, "end");
30
- if (endLine === void 0) return false;
31
- if (endLine !== startLine - 1 && endLine !== startLine) return false;
32
- return allowPattern.test(comment.value);
33
- });
34
+ const isNamedFunctionBoundary = (node) => {
35
+ const parent = node.parent;
36
+ if (parent?.type === "ArrowFunctionExpression" && parent.body === node) return parent.parent != null && isNamedVariable(parent.parent, parent);
37
+ 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;
34
41
  };
35
- const createRequireAliasName = (node) => {
36
- if (node.type !== "VariableDeclarator") return void 0;
37
- const id = getNodeField(node, "id");
38
- const init = getNodeField(node, "init");
39
- if (id?.type !== "Identifier" || init?.type !== "CallExpression") return void 0;
40
- const callee = getNodeField(init, "callee");
41
- if (callee?.type !== "Identifier" || getStringField(callee, "name") !== "createRequire") return void 0;
42
- return getStringField(id, "name");
42
+ const isEffectPromiseCall = (node) => {
43
+ if (node.type !== "CallExpression") return false;
44
+ const callee = node.callee;
45
+ if (callee.type !== "MemberExpression" || callee.computed) return false;
46
+ return identifierName(callee.object) === "Effect" && ["promise", "tryPromise"].includes(identifierName(callee.property) ?? "");
43
47
  };
44
- const classifyDynamicLoadCall = (callee, createRequireAliases) => {
45
- if (callee === void 0) return void 0;
46
- if (callee.type === "Identifier") {
47
- const name = getStringField(callee, "name");
48
- if (name === "require") return "`require(...)` is forbidden. Use a top-level static import.";
49
- if (name !== void 0 && createRequireAliases.has(name)) return "`createRequire(...)` aliases are forbidden. Use a top-level static import.";
50
- }
51
- if (callee.type === "MemberExpression") {
52
- const object = getNodeField(callee, "object");
53
- const property = getNodeField(callee, "property");
54
- if (object?.type === "Identifier" && getStringField(object, "name") === "module" && property?.type === "Identifier" && getStringField(property, "name") === "require") return "`module.require(...)` is forbidden. Use a top-level static import.";
55
- }
56
- if (callee.type === "CallExpression") {
57
- const inner = getNodeField(callee, "callee");
58
- if (inner?.type === "Identifier" && getStringField(inner, "name") === "createRequire") return "`createRequire(...)(...)` is forbidden. Use a top-level static import.";
59
- }
48
+ const isEffectPromiseBoundary = (node) => {
49
+ const callback = node.parent;
50
+ if (callback?.type !== "ArrowFunctionExpression" || callback.body !== node) return false;
51
+ const call = callback.parent;
52
+ return call != null && isEffectPromiseCall(call);
53
+ };
54
+ const isNamedLazyBoundary = (node) => {
55
+ return isNamedDirectBinding(node) || isNamedFunctionBoundary(node) || isEffectPromiseBoundary(node);
56
+ };
57
+ 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;
60
61
  };
61
62
  const noDynamicImports = define({
62
63
  name: "no-dynamic-imports",
63
64
  meta: meta({
64
65
  type: "problem",
65
- description: "Avoid dynamic import/require. Use top-level static imports."
66
+ description: "Keep dynamic imports behind named lazy-loading boundaries."
66
67
  }),
67
68
  create: function* () {
68
69
  const ctx = yield* RuleContext;
69
- const createRequireAliases = /* @__PURE__ */ new Set();
70
- const reportUnlessAllowed = (node, message) => hasAllowComment(node, ctx.sourceCode.getAllComments()) ? Effect.void : ctx.report(make({
70
+ const createRequireNames = /* @__PURE__ */ new Set();
71
+ const requireAliases = /* @__PURE__ */ new Set();
72
+ const report = (node, message) => ctx.report(make({
71
73
  node,
72
74
  message
73
75
  }));
74
76
  return {
75
- VariableDeclarator: (node) => {
76
- const alias = createRequireAliasName(node);
77
- if (alias === void 0) return Effect.void;
78
- createRequireAliases.add(alias);
79
- return reportUnlessAllowed(node, "`createRequire(...)` is forbidden. Use a top-level static import.");
80
- },
81
- ImportExpression: (node) => reportUnlessAllowed(node, "Dynamic `import(...)` is forbidden. Use a top-level static import."),
77
+ ImportDeclaration: (node) => Option.match(narrow(node, "ImportDeclaration"), {
78
+ onNone: () => Effect.void,
79
+ onSome: (declaration) => {
80
+ 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);
82
+ return Effect.void;
83
+ }
84
+ }),
85
+ VariableDeclarator: (node) => Option.match(narrow(node, "VariableDeclarator"), {
86
+ onNone: () => Effect.void,
87
+ onSome: (declaration) => {
88
+ if (declaration.id.type === "Identifier" && declaration.init?.type === "CallExpression" && declaration.init.callee.type === "Identifier" && createRequireNames.has(declaration.init.callee.name)) {
89
+ requireAliases.add(declaration.id.name);
90
+ return report(declaration, "Avoid createRequire(). Keep module loading static or use a named import() boundary.");
91
+ }
92
+ return Effect.void;
93
+ }
94
+ }),
95
+ ImportExpression: (node) => Option.match(narrow(node, "ImportExpression"), {
96
+ 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
+ }))
101
+ }),
82
102
  CallExpression: (node) => {
83
- const message = classifyDynamicLoadCall(getNodeField(node, "callee"), createRequireAliases);
84
- return message === void 0 ? Effect.void : reportUnlessAllowed(node, message);
103
+ return Option.match(narrow(node, "CallExpression"), {
104
+ onNone: () => Effect.void,
105
+ 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
+ }));
112
+ }
113
+ });
85
114
  }
86
115
  };
87
116
  }
@@ -1,163 +1,200 @@
1
- import { calleeName, matchMember, memberNames } from "../vendor/effect-oxlint/AST.js";
1
+ import { calleeName, memberNames, narrow } from "../vendor/effect-oxlint/AST.js";
2
2
  import { make } from "../vendor/effect-oxlint/Diagnostic.js";
3
3
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
4
- import { merge } from "../vendor/effect-oxlint/Visitor.js";
5
4
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
5
+ import { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
6
6
  import "../vendor/effect-oxlint/index.js";
7
- import { makeEffectContextTracker } from "./_effect-context.js";
8
7
  import * as Option from "effect/Option";
9
8
  import * as Effect from "effect/Effect";
10
- import * as Ref from "effect/Ref";
11
9
  //#region src/rules/no-globals.ts
12
10
  const memberBans = [
13
11
  [
14
12
  "console",
15
- [
16
- "log",
17
- "warn",
13
+ new Set([
14
+ "debug",
18
15
  "error",
19
16
  "info",
20
- "debug",
21
- "trace"
22
- ],
23
- "Avoid console.* inside Effect context. Use Effect.log / Effect.logWarning / Effect.logError."
17
+ "log",
18
+ "trace",
19
+ "warn"
20
+ ]),
21
+ "Effect logging or Console"
24
22
  ],
25
23
  [
26
24
  "Date",
27
- "now",
28
- "Avoid Date.now() inside Effect context. Use Clock service from 'effect'."
25
+ new Set(["now"]),
26
+ "Clock or DateTime"
29
27
  ],
30
28
  [
31
- "JSON",
32
- ["parse", "stringify"],
33
- "Avoid JSON.parse/stringify inside Effect context. Use Schema.fromJsonString."
29
+ "Math",
30
+ new Set(["random"]),
31
+ "Random"
34
32
  ],
35
33
  [
36
- "Math",
37
- "random",
38
- "Avoid Math.random() inside Effect context. Use Random service from 'effect'."
34
+ "performance",
35
+ new Set(["now"]),
36
+ "Clock.currentTimeNanos"
39
37
  ],
40
38
  [
41
39
  "crypto",
42
- ["randomUUID", "getRandomValues"],
43
- "Avoid crypto.randomUUID() inside Effect context. Use Random service from 'effect'."
40
+ new Set(["getRandomValues", "randomUUID"]),
41
+ "Crypto"
42
+ ],
43
+ [
44
+ "JSON",
45
+ new Set(["parse", "stringify"]),
46
+ "Schema JSON codecs"
47
+ ],
48
+ [
49
+ "process",
50
+ new Set([
51
+ "argv",
52
+ "chdir",
53
+ "env",
54
+ "exit",
55
+ "hrtime",
56
+ "nextTick",
57
+ "stderr",
58
+ "stdin",
59
+ "stdout"
60
+ ]),
61
+ "Config, Stdio, Clock, or Effect scheduling"
62
+ ],
63
+ [
64
+ "Bun",
65
+ new Set([
66
+ "$",
67
+ "Glob",
68
+ "connect",
69
+ "env",
70
+ "file",
71
+ "listen",
72
+ "nanoseconds",
73
+ "randomUUIDv7",
74
+ "redis",
75
+ "serve",
76
+ "sleep",
77
+ "spawn",
78
+ "spawnSync",
79
+ "stderr",
80
+ "stdin",
81
+ "stdout",
82
+ "write"
83
+ ]),
84
+ "the corresponding Effect platform service"
85
+ ],
86
+ [
87
+ "Deno",
88
+ new Set([
89
+ "args",
90
+ "env",
91
+ "exit",
92
+ "mkdir",
93
+ "open",
94
+ "readDir",
95
+ "readFile",
96
+ "readTextFile",
97
+ "remove",
98
+ "serve",
99
+ "stat",
100
+ "stderr",
101
+ "stdin",
102
+ "stdout",
103
+ "writeFile",
104
+ "writeTextFile"
105
+ ]),
106
+ "the corresponding Effect platform service"
107
+ ],
108
+ [
109
+ "localStorage",
110
+ new Set([
111
+ "clear",
112
+ "getItem",
113
+ "key",
114
+ "removeItem",
115
+ "setItem"
116
+ ]),
117
+ "KeyValueStore"
118
+ ],
119
+ [
120
+ "sessionStorage",
121
+ new Set([
122
+ "clear",
123
+ "getItem",
124
+ "key",
125
+ "removeItem",
126
+ "setItem"
127
+ ]),
128
+ "KeyValueStore"
44
129
  ]
45
130
  ];
46
- const platformObjects = new Set([
47
- "Bun",
48
- "process",
49
- "Deno"
131
+ const callBans = new Map([
132
+ ["atob", "Encoding.decodeBase64"],
133
+ ["btoa", "Encoding.encodeBase64"],
134
+ ["fetch", "HttpClient"],
135
+ ["queueMicrotask", "Effect scheduling"],
136
+ ["setImmediate", "Effect scheduling"],
137
+ ["setInterval", "Effect.sleep with Schedule"],
138
+ ["setTimeout", "Effect.sleep or Schedule"]
50
139
  ]);
51
- const alternatives = {
52
- Bun: {
53
- file: "Use FileSystem from 'effect'",
54
- write: "Use FileSystem from 'effect'",
55
- read: "Use FileSystem from 'effect'",
56
- serve: "Use HttpServer from 'effect/unstable/http'",
57
- env: "Use Config from 'effect'",
58
- spawn: "Use Command from 'effect'",
59
- sleep: "Use Effect.sleep",
60
- stdin: "Use Terminal from 'effect'",
61
- stdout: "Use Terminal from 'effect'",
62
- stderr: "Use Terminal from 'effect'",
63
- cwd: "Use Path from 'effect'",
64
- which: "Use Command from 'effect'",
65
- resolveSync: "Use Path from 'effect'",
66
- pathToFileURL: "Use Path from 'effect'",
67
- randomUUID: "Use Random service from 'effect'",
68
- randomUUIDv7: "Use Random service from 'effect'"
69
- },
70
- process: {
71
- env: "Use Config from 'effect'",
72
- exit: "Use Effect.fail or Effect.die",
73
- cwd: "Use Path from 'effect'",
74
- argv: "Use Args/Command from 'effect/unstable/cli'",
75
- stdin: "Use Terminal from 'effect'",
76
- stdout: "Use Terminal from 'effect'",
77
- stderr: "Use Terminal from 'effect'",
78
- chdir: "Use Path from 'effect'",
79
- kill: "Use Command from 'effect'",
80
- on: "Use Effect.async or Stream.async",
81
- platform: "Use PlatformInfo from 'effect'",
82
- arch: "Use PlatformInfo from 'effect'",
83
- execPath: "Use Command from 'effect'",
84
- execArgv: "Use Args/Command from 'effect/unstable/cli'"
85
- },
86
- Deno: {
87
- readFile: "Use FileSystem from 'effect'",
88
- writeFile: "Use FileSystem from 'effect'",
89
- readTextFile: "Use FileSystem from 'effect'",
90
- writeTextFile: "Use FileSystem from 'effect'",
91
- open: "Use FileSystem from 'effect'",
92
- remove: "Use FileSystem from 'effect'",
93
- mkdir: "Use FileSystem from 'effect'",
94
- readDir: "Use FileSystem from 'effect'",
95
- stat: "Use FileSystem from 'effect'",
96
- cwd: "Use Path from 'effect'",
97
- env: "Use Config from 'effect'",
98
- exit: "Use Effect.fail or Effect.die",
99
- serve: "Use HttpServer from 'effect/unstable/http'",
100
- run: "Use Command from 'effect'",
101
- args: "Use Args/Command from 'effect/unstable/cli'"
102
- }
103
- };
104
- function getHint(obj, prop) {
105
- const hint = alternatives[obj]?.[prop];
106
- if (hint) return `. ${hint}`;
107
- return ". Wrap in a service for testability";
108
- }
109
- const callBans = {
110
- fetch: "Avoid fetch() inside Effect context. Use HttpClient from 'effect/unstable/http'.",
111
- setTimeout: "Avoid setTimeout inside Effect context. Use Effect.sleep or Schedule.",
112
- setInterval: "Avoid setInterval inside Effect context. Use Effect.sleep or Schedule."
140
+ const constructorBans = new Map([
141
+ ["Date", "Clock or DateTime"],
142
+ ["SharedWorker", "Effect Worker"],
143
+ ["WebSocket", "Socket"],
144
+ ["Worker", "Effect Worker"]
145
+ ]);
146
+ const isUnshadowedGlobal = (ctx, node, name) => Option.match(findVariableUp(ctx.sourceCode.getScope(node), name), {
147
+ onNone: () => true,
148
+ onSome: (variable) => variable.defs.length === 0
149
+ });
150
+ const staticMember = (node) => Option.getOrUndefined(memberNames(node));
151
+ const isCryptoDigest = (node) => {
152
+ if (node.computed || node.property.type !== "Identifier" || node.property.name !== "digest") return false;
153
+ const object = node.object;
154
+ if (object.type !== "MemberExpression" || object.computed) return false;
155
+ return object.object.type === "Identifier" && object.object.name === "crypto" && object.property.type === "Identifier" && object.property.name === "subtle";
113
156
  };
114
157
  const noGlobals = define({
115
158
  name: "no-globals",
116
159
  meta: meta({
117
- type: "suggestion",
118
- description: "Avoid global/platform APIs inside Effect.gen/fn. Use Effect platform services instead."
160
+ type: "problem",
161
+ description: "Avoid ambient runtime capabilities that Effect provides as services."
119
162
  }),
120
163
  create: function* () {
121
164
  const ctx = yield* RuleContext;
122
- const [depth, tracker] = yield* makeEffectContextTracker;
123
- return merge(tracker, {
124
- MemberExpression: (node) => Effect.flatMap(Ref.get(depth), (d) => {
125
- if (d <= 0) return Effect.void;
126
- const memberNode = node;
127
- for (const [obj, props, message] of memberBans) if (Option.isSome(matchMember(memberNode, obj, props))) return ctx.report(make({
128
- node,
129
- message
130
- }));
131
- const names = Option.getOrUndefined(memberNames(memberNode));
132
- if (names !== void 0) {
133
- const [obj, prop] = names;
134
- if (platformObjects.has(obj)) return ctx.report(make({
135
- node,
136
- message: `Avoid ${obj}.${prop} inside Effect context${getHint(obj, prop)}.`
137
- }));
138
- }
139
- return Effect.void;
140
- }),
141
- CallExpression: (node) => Effect.flatMap(Ref.get(depth), (d) => {
142
- if (d <= 0) return Effect.void;
143
- const name = Option.getOrUndefined(calleeName(node));
144
- const message = name !== void 0 ? callBans[name] : void 0;
145
- if (message !== void 0) return ctx.report(make({
146
- node,
147
- message
148
- }));
149
- return Effect.void;
150
- }),
151
- NewExpression: (node) => Effect.flatMap(Ref.get(depth), (d) => {
152
- if (d <= 0) return Effect.void;
153
- const callee = node["callee"];
154
- if (callee.type === "Identifier" && "name" in callee && callee.name === "Date") return ctx.report(make({
155
- node,
156
- message: "Avoid new Date() inside Effect context. Use Clock service from 'effect'."
157
- }));
165
+ const report = (node, used, alternative) => ctx.report(make({
166
+ node,
167
+ message: `Avoid ${used}. Use ${alternative}; platform adapters may disable this rule explicitly.`
168
+ }));
169
+ return {
170
+ MemberExpression: (node) => {
171
+ const memberExpression = Option.getOrUndefined(narrow(node, "MemberExpression"));
172
+ if (memberExpression === void 0) return Effect.void;
173
+ if (isCryptoDigest(memberExpression) && isUnshadowedGlobal(ctx, memberExpression, "crypto")) return report(memberExpression, "crypto.subtle.digest", "Crypto.digest");
174
+ const names = staticMember(memberExpression);
175
+ if (names === void 0) return Effect.void;
176
+ const [object, property] = names;
177
+ if (!isUnshadowedGlobal(ctx, memberExpression, object)) return Effect.void;
178
+ for (const [bannedObject, properties, alternative] of memberBans) if (object === bannedObject && properties.has(property)) return report(memberExpression, `${object}.${property}`, alternative);
158
179
  return Effect.void;
159
- })
160
- });
180
+ },
181
+ CallExpression: (node) => {
182
+ const call = Option.getOrUndefined(narrow(node, "CallExpression"));
183
+ if (call === void 0) return Effect.void;
184
+ const name = Option.getOrUndefined(calleeName(call));
185
+ if (name === void 0 || !isUnshadowedGlobal(ctx, call, name)) return Effect.void;
186
+ const alternative = callBans.get(name);
187
+ return alternative === void 0 ? Effect.void : report(call, `${name}()`, alternative);
188
+ },
189
+ NewExpression: (node) => {
190
+ const expression = Option.getOrUndefined(narrow(node, "NewExpression"));
191
+ if (expression === void 0 || expression.callee.type !== "Identifier") return Effect.void;
192
+ const name = expression.callee.name;
193
+ if (!isUnshadowedGlobal(ctx, expression, name)) return Effect.void;
194
+ const alternative = constructorBans.get(name);
195
+ return alternative === void 0 ? Effect.void : report(expression, `new ${name}()`, alternative);
196
+ }
197
+ };
161
198
  }
162
199
  });
163
200
  //#endregion
@@ -1,11 +1,10 @@
1
+ import { narrow } from "../vendor/effect-oxlint/AST.js";
1
2
  import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
3
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
- import { merge } from "../vendor/effect-oxlint/Visitor.js";
4
4
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
5
5
  import "../vendor/effect-oxlint/index.js";
6
- import { makeEffectContextTracker } from "./_effect-context.js";
6
+ import * as Option from "effect/Option";
7
7
  import * as Effect from "effect/Effect";
8
- import * as Ref from "effect/Ref";
9
8
  //#region src/rules/no-new-error.ts
10
9
  const nativeErrors = new Set([
11
10
  "Error",
@@ -16,23 +15,37 @@ const nativeErrors = new Set([
16
15
  "URIError",
17
16
  "EvalError"
18
17
  ]);
18
+ const defectNamespaces = new Set([
19
+ "Effect",
20
+ "Cause",
21
+ "Exit"
22
+ ]);
23
+ const isDirectDefectArgument = (node) => {
24
+ const parent = node.parent;
25
+ if (parent?.type !== "CallExpression" || !parent.arguments.includes(node)) return false;
26
+ const callee = parent.callee;
27
+ if (callee.type !== "MemberExpression" || callee.computed) return false;
28
+ if (callee.object.type !== "Identifier" || callee.property.type !== "Identifier") return false;
29
+ return defectNamespaces.has(callee.object.name) && callee.property.name === "die";
30
+ };
19
31
  const noNewError = define({
20
32
  name: "no-new-error",
21
33
  meta: meta({
22
34
  type: "suggestion",
23
- description: "Avoid native Error constructors. Use Schema.TaggedErrorClass."
35
+ description: "Reserve native Error values for explicit Effect defects."
24
36
  }),
25
37
  create: function* () {
26
38
  const ctx = yield* RuleContext;
27
- const [depth, tracker] = yield* makeEffectContextTracker;
28
- return merge(tracker, { NewExpression: (node) => Effect.flatMap(Ref.get(depth), (d) => {
29
- const callee = node["callee"];
30
- if (callee.type === "Identifier" && "name" in callee && nativeErrors.has(callee.name)) return ctx.report(make({
31
- node,
32
- message: d > 0 ? "Avoid native Error inside Effect.gen/fn. Define: class MyError extends Schema.TaggedErrorClass<MyError>()(\"MyError\", { ... }) {} — then yield* new MyError()." : "Avoid native Error constructors. Define: class MyError extends Schema.TaggedErrorClass<MyError>()(\"MyError\", { message: Schema.String }) {}"
33
- }));
34
- return Effect.void;
35
- }) });
39
+ return { NewExpression: (node) => Option.match(narrow(node, "NewExpression"), {
40
+ onNone: () => Effect.void,
41
+ onSome: (expression) => {
42
+ if (expression.callee.type !== "Identifier" || !nativeErrors.has(expression.callee.name) || isDirectDefectArgument(expression)) return Effect.void;
43
+ return ctx.report(make({
44
+ node: expression,
45
+ message: "Avoid native Error constructors for expected failures. Use a tagged error, or pass the Error directly to Effect.die, Cause.die, or Exit.die for an explicit defect."
46
+ }));
47
+ }
48
+ }) };
36
49
  }
37
50
  });
38
51
  //#endregion
@@ -1,37 +1,38 @@
1
+ import { memberNames, narrow } from "../vendor/effect-oxlint/AST.js";
1
2
  import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
3
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
- import { merge } from "../vendor/effect-oxlint/Visitor.js";
4
4
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
5
5
  import "../vendor/effect-oxlint/index.js";
6
- import { makeEffectContextTracker } from "./_effect-context.js";
6
+ import * as Option from "effect/Option";
7
7
  import * as Effect from "effect/Effect";
8
- import * as Ref from "effect/Ref";
9
8
  //#region src/rules/no-new-promise.ts
10
- /**
11
- * Ban `new Promise()`. Context-aware messaging.
12
- *
13
- * Inside Effect.gen/fn: "Use Effect.tryPromise or Effect.async"
14
- * Outside: "Use Effect.tryPromise or wrap with Effect.fn"
15
- *
16
- * Source: language-service/newPromise
17
- */
9
+ const isPromiseIdentifier = (node) => node.type === "Identifier" && "name" in node && node.name === "Promise";
18
10
  const noNewPromise = define({
19
11
  name: "no-new-promise",
20
12
  meta: meta({
21
13
  type: "suggestion",
22
- description: "Avoid new Promise(). Use Effect.async or Effect.tryPromise."
14
+ description: "Avoid Promise APIs. Use Effect concurrency and promise boundaries."
23
15
  }),
24
16
  create: function* () {
25
17
  const ctx = yield* RuleContext;
26
- const [depth, tracker] = yield* makeEffectContextTracker;
27
- return merge(tracker, { NewExpression: (node) => Effect.flatMap(Ref.get(depth), (d) => {
28
- const callee = node["callee"];
29
- if (callee != null && typeof callee === "object" && "type" in callee && callee.type === "Identifier" && "name" in callee && callee.name === "Promise") return ctx.report(make({
30
- node,
31
- message: d > 0 ? "Avoid new Promise() inside Effect.gen/fn. Use Effect.async for callback APIs, Effect.tryPromise for existing promises, or Effect.promise for infallible promises." : "Avoid new Promise(). Use Effect.async for callback APIs, Effect.tryPromise for existing promises — wrap this function with Effect.fn."
32
- }));
33
- return Effect.void;
34
- }) });
18
+ const report = (node) => ctx.report(make({
19
+ node,
20
+ message: "Avoid Promise APIs. Use Effect.async for callbacks and Effect.promise or Effect.tryPromise at promise boundaries."
21
+ }));
22
+ return {
23
+ NewExpression: (node) => Option.match(narrow(node, "NewExpression"), {
24
+ onNone: () => Effect.void,
25
+ onSome: (expression) => isPromiseIdentifier(expression.callee) ? report(expression) : Effect.void
26
+ }),
27
+ CallExpression: (node) => Option.match(narrow(node, "CallExpression"), {
28
+ onNone: () => Effect.void,
29
+ onSome: (call) => {
30
+ if (isPromiseIdentifier(call.callee)) return report(call);
31
+ if (call.callee.type !== "MemberExpression") return Effect.void;
32
+ return Option.getOrUndefined(memberNames(call.callee))?.[0] === "Promise" ? report(call) : Effect.void;
33
+ }
34
+ })
35
+ };
35
36
  }
36
37
  });
37
38
  //#endregion