oxlint-plugin-effect 0.2.0 → 0.2.1

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 (256) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +13 -0
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.js +4 -0
  4. package/dist/plugin.d.ts +5 -0
  5. package/dist/plugin.js +18 -0
  6. package/dist/presets/core.d.ts +43 -0
  7. package/dist/presets/core.js +43 -0
  8. package/dist/presets/effect-native.d.ts +28 -0
  9. package/dist/presets/effect-native.js +28 -0
  10. package/dist/presets/full.d.ts +60 -0
  11. package/dist/presets/full.js +60 -0
  12. package/dist/presets/functional.d.ts +21 -0
  13. package/dist/presets/functional.js +21 -0
  14. package/dist/presets/index.d.ts +13 -0
  15. package/dist/presets/index.js +18 -0
  16. package/dist/presets/strict.d.ts +69 -0
  17. package/dist/presets/strict.js +69 -0
  18. package/dist/presets/v3.d.ts +15 -0
  19. package/dist/presets/v3.js +15 -0
  20. package/dist/rules/_effect-context.d.ts +19 -0
  21. package/dist/rules/_effect-context.js +22 -0
  22. package/dist/rules/index.d.ts +66 -0
  23. package/dist/rules/index.js +124 -0
  24. package/dist/rules/no-arrow-ladder.d.ts +6 -0
  25. package/dist/rules/no-arrow-ladder.js +61 -0
  26. package/dist/rules/no-async-function.d.ts +6 -0
  27. package/dist/rules/no-async-function.js +46 -0
  28. package/dist/rules/no-catch-all-to-map-error.d.ts +6 -0
  29. package/dist/rules/no-catch-all-to-map-error.js +67 -0
  30. package/dist/rules/no-effect-as.d.ts +6 -0
  31. package/dist/rules/no-effect-as.js +16 -0
  32. package/dist/rules/no-effect-async.d.ts +6 -0
  33. package/dist/rules/no-effect-async.js +16 -0
  34. package/dist/rules/no-effect-bind.d.ts +6 -0
  35. package/dist/rules/no-effect-bind.js +16 -0
  36. package/dist/rules/no-effect-do.d.ts +6 -0
  37. package/dist/rules/no-effect-do.js +16 -0
  38. package/dist/rules/no-effect-fn-generator.d.ts +6 -0
  39. package/dist/rules/no-effect-fn-generator.js +30 -0
  40. package/dist/rules/no-effect-gen-adapter.d.ts +6 -0
  41. package/dist/rules/no-effect-gen-adapter.js +30 -0
  42. package/dist/rules/no-effect-map-flatten.d.ts +6 -0
  43. package/dist/rules/no-effect-map-flatten.js +53 -0
  44. package/dist/rules/no-effect-map-void.d.ts +6 -0
  45. package/dist/rules/no-effect-map-void.js +40 -0
  46. package/dist/rules/no-effect-never.d.ts +6 -0
  47. package/dist/rules/no-effect-never.js +16 -0
  48. package/dist/rules/no-effect-orElse-ladder.d.ts +6 -0
  49. package/dist/rules/no-effect-orElse-ladder.js +53 -0
  50. package/dist/rules/no-effect-succeed-string.d.ts +6 -0
  51. package/dist/rules/no-effect-succeed-string.js +30 -0
  52. package/dist/rules/no-effect-succeed-variable.d.ts +6 -0
  53. package/dist/rules/no-effect-succeed-variable.js +30 -0
  54. package/dist/rules/no-effect-succeed-void.d.ts +6 -0
  55. package/dist/rules/no-effect-succeed-void.js +33 -0
  56. package/dist/rules/no-effect-sync-console.d.ts +6 -0
  57. package/dist/rules/no-effect-sync-console.js +63 -0
  58. package/dist/rules/no-effect-sync-wrapper.d.ts +6 -0
  59. package/dist/rules/no-effect-sync-wrapper.js +37 -0
  60. package/dist/rules/no-effect-type-alias.d.ts +6 -0
  61. package/dist/rules/no-effect-type-alias.js +52 -0
  62. package/dist/rules/no-effect-wrapper-alias.d.ts +6 -0
  63. package/dist/rules/no-effect-wrapper-alias.js +69 -0
  64. package/dist/rules/no-extends-native-error.d.ts +6 -0
  65. package/dist/rules/no-extends-native-error.js +41 -0
  66. package/dist/rules/no-flatmap-ladder.d.ts +6 -0
  67. package/dist/rules/no-flatmap-ladder.js +48 -0
  68. package/dist/rules/no-fromnullable-coalesce.d.ts +6 -0
  69. package/dist/rules/no-fromnullable-coalesce.js +36 -0
  70. package/dist/rules/no-global-error-in-catch.d.ts +6 -0
  71. package/dist/rules/no-global-error-in-catch.js +75 -0
  72. package/dist/rules/no-global-error-in-failure.d.ts +6 -0
  73. package/dist/rules/no-global-error-in-failure.js +54 -0
  74. package/dist/rules/no-globals.d.ts +6 -0
  75. package/dist/rules/no-globals.js +164 -0
  76. package/dist/rules/no-if-statement.d.ts +6 -0
  77. package/dist/rules/no-if-statement.js +18 -0
  78. package/dist/rules/no-iife-wrapper.d.ts +6 -0
  79. package/dist/rules/no-iife-wrapper.js +37 -0
  80. package/dist/rules/no-inline-provide.d.ts +6 -0
  81. package/dist/rules/no-inline-provide.js +31 -0
  82. package/dist/rules/no-instanceof-schema.d.ts +6 -0
  83. package/dist/rules/no-instanceof-schema.js +35 -0
  84. package/dist/rules/no-manual-effect-channels.d.ts +6 -0
  85. package/dist/rules/no-manual-effect-channels.js +40 -0
  86. package/dist/rules/no-match-effect-branch.d.ts +6 -0
  87. package/dist/rules/no-match-effect-branch.js +64 -0
  88. package/dist/rules/no-match-void-branch.d.ts +6 -0
  89. package/dist/rules/no-match-void-branch.js +50 -0
  90. package/dist/rules/no-multiple-effect-provide.d.ts +6 -0
  91. package/dist/rules/no-multiple-effect-provide.js +45 -0
  92. package/dist/rules/no-nested-effect-call.d.ts +6 -0
  93. package/dist/rules/no-nested-effect-call.js +97 -0
  94. package/dist/rules/no-nested-effect-gen.d.ts +6 -0
  95. package/dist/rules/no-nested-effect-gen.js +46 -0
  96. package/dist/rules/no-nested-pipe.d.ts +6 -0
  97. package/dist/rules/no-nested-pipe.js +38 -0
  98. package/dist/rules/no-new-error.d.ts +6 -0
  99. package/dist/rules/no-new-error.js +39 -0
  100. package/dist/rules/no-new-promise.d.ts +6 -0
  101. package/dist/rules/no-new-promise.js +38 -0
  102. package/dist/rules/no-node-builtin-import.d.ts +6 -0
  103. package/dist/rules/no-node-builtin-import.js +61 -0
  104. package/dist/rules/no-option-as.d.ts +6 -0
  105. package/dist/rules/no-option-as.js +16 -0
  106. package/dist/rules/no-option-boolean-normalization.d.ts +6 -0
  107. package/dist/rules/no-option-boolean-normalization.js +50 -0
  108. package/dist/rules/no-positional-log-error.d.ts +6 -0
  109. package/dist/rules/no-positional-log-error.js +52 -0
  110. package/dist/rules/no-redundant-schema-tag-identifier.d.ts +6 -0
  111. package/dist/rules/no-redundant-schema-tag-identifier.js +46 -0
  112. package/dist/rules/no-return-in-arrow.d.ts +6 -0
  113. package/dist/rules/no-return-in-arrow.js +41 -0
  114. package/dist/rules/no-return-nullish.d.ts +6 -0
  115. package/dist/rules/no-return-nullish.js +47 -0
  116. package/dist/rules/no-run-in-effect-gen.d.ts +6 -0
  117. package/dist/rules/no-run-in-effect-gen.js +48 -0
  118. package/dist/rules/no-runtime-run-fork.d.ts +6 -0
  119. package/dist/rules/no-runtime-run-fork.js +19 -0
  120. package/dist/rules/no-schema-struct-with-tag.d.ts +6 -0
  121. package/dist/rules/no-schema-struct-with-tag.js +48 -0
  122. package/dist/rules/no-schema-union-of-literals.d.ts +6 -0
  123. package/dist/rules/no-schema-union-of-literals.js +35 -0
  124. package/dist/rules/no-string-sentinel-const.d.ts +6 -0
  125. package/dist/rules/no-string-sentinel-const.js +38 -0
  126. package/dist/rules/no-switch-statement.d.ts +6 -0
  127. package/dist/rules/no-switch-statement.js +16 -0
  128. package/dist/rules/no-ternary.d.ts +6 -0
  129. package/dist/rules/no-ternary.js +30 -0
  130. package/dist/rules/no-throw-statement.d.ts +6 -0
  131. package/dist/rules/no-throw-statement.js +34 -0
  132. package/dist/rules/no-try-catch.d.ts +6 -0
  133. package/dist/rules/no-try-catch.js +34 -0
  134. package/dist/rules/no-unnecessary-arrow-block.d.ts +6 -0
  135. package/dist/rules/no-unnecessary-arrow-block.js +34 -0
  136. package/dist/rules/no-unnecessary-effect-gen.d.ts +6 -0
  137. package/dist/rules/no-unnecessary-effect-gen.js +35 -0
  138. package/dist/rules/no-unnecessary-pipe-chain.d.ts +6 -0
  139. package/dist/rules/no-unnecessary-pipe-chain.js +36 -0
  140. package/dist/rules/no-unnecessary-pipe.d.ts +6 -0
  141. package/dist/rules/no-unnecessary-pipe.js +29 -0
  142. package/dist/vendor/effect-oxlint/AST.d.ts +199 -0
  143. package/dist/vendor/effect-oxlint/AST.js +214 -0
  144. package/dist/vendor/effect-oxlint/Comment.d.ts +55 -0
  145. package/dist/vendor/effect-oxlint/Comment.js +67 -0
  146. package/dist/vendor/effect-oxlint/Diagnostic.d.ts +86 -0
  147. package/dist/vendor/effect-oxlint/Diagnostic.js +115 -0
  148. package/dist/vendor/effect-oxlint/Plugin.d.ts +38 -0
  149. package/dist/vendor/effect-oxlint/Plugin.js +45 -0
  150. package/dist/vendor/effect-oxlint/Rule.d.ts +218 -0
  151. package/dist/vendor/effect-oxlint/Rule.js +309 -0
  152. package/dist/vendor/effect-oxlint/RuleContext.d.ts +77 -0
  153. package/dist/vendor/effect-oxlint/RuleContext.js +74 -0
  154. package/dist/vendor/effect-oxlint/Scope.d.ts +100 -0
  155. package/dist/vendor/effect-oxlint/Scope.js +110 -0
  156. package/dist/vendor/effect-oxlint/SourceCode.d.ts +180 -0
  157. package/dist/vendor/effect-oxlint/SourceCode.js +203 -0
  158. package/dist/vendor/effect-oxlint/Testing.d.ts +510 -0
  159. package/dist/vendor/effect-oxlint/Testing.js +963 -0
  160. package/dist/vendor/effect-oxlint/Token.d.ts +98 -0
  161. package/dist/vendor/effect-oxlint/Token.js +103 -0
  162. package/dist/vendor/effect-oxlint/Visitor.d.ts +166 -0
  163. package/dist/vendor/effect-oxlint/Visitor.js +138 -0
  164. package/dist/vendor/effect-oxlint/index.d.ts +13 -0
  165. package/dist/vendor/effect-oxlint/index.js +12 -0
  166. package/package.json +24 -5
  167. package/.effect-lsp.json +0 -60
  168. package/.oxlintrc.json +0 -33
  169. package/CHANGELOG.md +0 -55
  170. package/scripts/add-rule.ts +0 -87
  171. package/scripts/codegen.ts +0 -108
  172. package/src/index.ts +0 -9
  173. package/src/plugin.ts +0 -16
  174. package/src/presets/core.ts +0 -47
  175. package/src/presets/effect-native.ts +0 -34
  176. package/src/presets/full.ts +0 -70
  177. package/src/presets/functional.ts +0 -18
  178. package/src/presets/index.ts +0 -5
  179. package/src/presets/strict.ts +0 -77
  180. package/src/rules/_effect-context.ts +0 -28
  181. package/src/rules/index.ts +0 -72
  182. package/src/rules/no-arrow-ladder.ts +0 -70
  183. package/src/rules/no-async-function.ts +0 -47
  184. package/src/rules/no-catch-all-to-map-error.ts +0 -90
  185. package/src/rules/no-effect-as.ts +0 -14
  186. package/src/rules/no-effect-async.ts +0 -14
  187. package/src/rules/no-effect-bind.ts +0 -13
  188. package/src/rules/no-effect-do.ts +0 -14
  189. package/src/rules/no-effect-fn-generator.ts +0 -58
  190. package/src/rules/no-effect-gen-adapter.ts +0 -51
  191. package/src/rules/no-effect-map-flatten.ts +0 -82
  192. package/src/rules/no-effect-map-void.ts +0 -80
  193. package/src/rules/no-effect-never.ts +0 -14
  194. package/src/rules/no-effect-orElse-ladder.ts +0 -79
  195. package/src/rules/no-effect-succeed-string.ts +0 -52
  196. package/src/rules/no-effect-succeed-variable.ts +0 -50
  197. package/src/rules/no-effect-succeed-void.ts +0 -63
  198. package/src/rules/no-effect-sync-console.ts +0 -91
  199. package/src/rules/no-effect-sync-wrapper.ts +0 -67
  200. package/src/rules/no-effect-type-alias.ts +0 -71
  201. package/src/rules/no-effect-wrapper-alias.ts +0 -95
  202. package/src/rules/no-extends-native-error.ts +0 -54
  203. package/src/rules/no-flatmap-ladder.ts +0 -77
  204. package/src/rules/no-fromnullable-coalesce.ts +0 -62
  205. package/src/rules/no-global-error-in-catch.ts +0 -104
  206. package/src/rules/no-global-error-in-failure.ts +0 -83
  207. package/src/rules/no-globals.ts +0 -178
  208. package/src/rules/no-if-statement.ts +0 -16
  209. package/src/rules/no-iife-wrapper.ts +0 -51
  210. package/src/rules/no-inline-provide.ts +0 -41
  211. package/src/rules/no-instanceof-schema.ts +0 -42
  212. package/src/rules/no-manual-effect-channels.ts +0 -58
  213. package/src/rules/no-match-effect-branch.ts +0 -82
  214. package/src/rules/no-match-void-branch.ts +0 -72
  215. package/src/rules/no-multiple-effect-provide.ts +0 -75
  216. package/src/rules/no-nested-effect-call.ts +0 -57
  217. package/src/rules/no-nested-effect-gen.ts +0 -43
  218. package/src/rules/no-nested-pipe.ts +0 -73
  219. package/src/rules/no-new-error.ts +0 -53
  220. package/src/rules/no-new-promise.ts +0 -50
  221. package/src/rules/no-node-builtin-import.ts +0 -68
  222. package/src/rules/no-option-as.ts +0 -13
  223. package/src/rules/no-option-boolean-normalization.ts +0 -88
  224. package/src/rules/no-positional-log-error.ts +0 -76
  225. package/src/rules/no-redundant-schema-tag-identifier.ts +0 -62
  226. package/src/rules/no-return-in-arrow.ts +0 -76
  227. package/src/rules/no-return-nullish.ts +0 -80
  228. package/src/rules/no-run-in-effect-gen.ts +0 -66
  229. package/src/rules/no-runtime-run-fork.ts +0 -14
  230. package/src/rules/no-schema-struct-with-tag.ts +0 -64
  231. package/src/rules/no-schema-union-of-literals.ts +0 -48
  232. package/src/rules/no-string-sentinel-const.ts +0 -49
  233. package/src/rules/no-switch-statement.ts +0 -13
  234. package/src/rules/no-ternary.ts +0 -30
  235. package/src/rules/no-throw-statement.ts +0 -39
  236. package/src/rules/no-try-catch.ts +0 -39
  237. package/src/rules/no-unnecessary-arrow-block.ts +0 -51
  238. package/src/rules/no-unnecessary-effect-gen.ts +0 -83
  239. package/src/rules/no-unnecessary-pipe-chain.ts +0 -47
  240. package/src/rules/no-unnecessary-pipe.ts +0 -45
  241. package/src/vendor/effect-oxlint/AST.ts +0 -516
  242. package/src/vendor/effect-oxlint/Comment.ts +0 -86
  243. package/src/vendor/effect-oxlint/Diagnostic.ts +0 -199
  244. package/src/vendor/effect-oxlint/Plugin.ts +0 -71
  245. package/src/vendor/effect-oxlint/Rule.ts +0 -548
  246. package/src/vendor/effect-oxlint/RuleContext.ts +0 -144
  247. package/src/vendor/effect-oxlint/Scope.ts +0 -177
  248. package/src/vendor/effect-oxlint/SourceCode.ts +0 -377
  249. package/src/vendor/effect-oxlint/Testing.ts +0 -1227
  250. package/src/vendor/effect-oxlint/Token.ts +0 -131
  251. package/src/vendor/effect-oxlint/Visitor.ts +0 -287
  252. package/src/vendor/effect-oxlint/index.ts +0 -88
  253. package/tests/ban-rules.test.ts +0 -68
  254. package/tests/context-rules.test.ts +0 -263
  255. package/tests/pattern-rules.test.ts +0 -247
  256. package/tsconfig.json +0 -15
@@ -1,83 +0,0 @@
1
- /**
2
- * Detect unnecessary `Effect.gen` wrappers with a single `yield*` + return.
3
- *
4
- * ```ts
5
- * // BAD
6
- * Effect.gen(function* () { return yield* someEffect })
7
- *
8
- * // GOOD
9
- * someEffect
10
- * ```
11
- *
12
- * Source: language-service/unnecessaryEffectGen
13
- */
14
- import type { ESTree } from "@oxlint/plugins"
15
- import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
16
- import * as Effect from "effect/Effect"
17
-
18
- export const noUnnecessaryEffectGen = Rule.define({
19
- name: "no-unnecessary-effect-gen",
20
- meta: Rule.meta({
21
- type: "suggestion",
22
- description:
23
- "Unnecessary Effect.gen — single yield can be replaced with the yielded effect directly.",
24
- fixable: "code",
25
- }),
26
- create: function* () {
27
- const ctx = yield* RuleContext
28
- return {
29
- CallExpression: (node) => {
30
- if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "gen")) return Effect.void
31
-
32
- // Check for single-arg: Effect.gen(function*() { ... })
33
- if (!("arguments" in node) || !Array.isArray(node.arguments) || node.arguments.length !== 1)
34
- return Effect.void
35
-
36
- const arg = node.arguments[0]
37
- if (arg == null || typeof arg !== "object" || !("type" in arg)) return Effect.void
38
-
39
- // Must be a generator function expression
40
- if (arg.type !== "FunctionExpression" || !("generator" in arg) || !arg.generator)
41
- return Effect.void
42
-
43
- // Must have exactly one statement in the body
44
- if (
45
- !("body" in arg) ||
46
- arg.body == null ||
47
- typeof arg.body !== "object" ||
48
- !("type" in arg.body) ||
49
- arg.body.type !== "BlockStatement" ||
50
- !("body" in arg.body) ||
51
- !Array.isArray(arg.body.body) ||
52
- arg.body.body.length !== 1
53
- )
54
- return Effect.void
55
-
56
- const stmt = arg.body.body[0]
57
- if (stmt == null || typeof stmt !== "object" || !("type" in stmt)) return Effect.void
58
-
59
- // Must be `return yield* expr`
60
- if (
61
- stmt.type === "ReturnStatement" &&
62
- "argument" in stmt &&
63
- stmt.argument != null &&
64
- typeof stmt.argument === "object" &&
65
- "type" in stmt.argument &&
66
- stmt.argument.type === "YieldExpression" &&
67
- "delegate" in stmt.argument &&
68
- stmt.argument.delegate === true
69
- ) {
70
- return ctx.report(
71
- Diagnostic.make({
72
- node,
73
- message:
74
- "Unnecessary Effect.gen — single yield*. Replace with the yielded effect directly.",
75
- }),
76
- )
77
- }
78
-
79
- return Effect.void
80
- },
81
- }
82
- },
83
- })
@@ -1,47 +0,0 @@
1
- /**
2
- * Ban `x.pipe(f).pipe(g)` — merge into `x.pipe(f, g)`.
3
- *
4
- * Source: language-service/unnecessaryPipeChain
5
- */
6
- import type { ESTree } from "@oxlint/plugins"
7
- import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
8
- import * as Effect from "effect/Effect"
9
-
10
- const isMethodPipe = (node: ESTree.Node): boolean => {
11
- if (node.type !== "CallExpression") return false
12
- const callee = (node as unknown as Record<string, unknown>)["callee"]
13
- if (callee == null || typeof callee !== "object" || !("type" in callee)) return false
14
- if (callee.type !== "MemberExpression") return false
15
- const prop = (callee as unknown as Record<string, unknown>)["property"]
16
- if (prop == null || typeof prop !== "object" || !("type" in prop)) return false
17
- return prop.type === "Identifier" && "name" in prop && prop.name === "pipe"
18
- }
19
-
20
- export const noUnnecessaryPipeChain = Rule.define({
21
- name: "no-unnecessary-pipe-chain",
22
- meta: Rule.meta({
23
- type: "suggestion",
24
- description:
25
- "Avoid chaining .pipe().pipe(). Merge into a single .pipe() call.",
26
- }),
27
- create: function* () {
28
- const ctx = yield* RuleContext
29
- return {
30
- CallExpression: (node) => {
31
- if (!isMethodPipe(node)) return Effect.void
32
- const callee = (node as unknown as Record<string, unknown>)["callee"] as Record<string, unknown>
33
- const obj = callee["object"]
34
- if (obj != null && typeof obj === "object" && isMethodPipe(obj as ESTree.Node)) {
35
- return ctx.report(
36
- Diagnostic.make({
37
- node,
38
- message:
39
- "Unnecessary .pipe() chain. Merge into a single .pipe() call.",
40
- }),
41
- )
42
- }
43
- return Effect.void
44
- },
45
- }
46
- },
47
- })
@@ -1,45 +0,0 @@
1
- /**
2
- * Ban `pipe(x)` with zero pipe arguments.
3
- *
4
- * Just use `x` directly.
5
- *
6
- * Source: language-service/unnecessaryPipe
7
- */
8
- import type { ESTree } from "@oxlint/plugins"
9
- import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
10
- import * as Effect from "effect/Effect"
11
- import * as Option from "effect/Option"
12
-
13
- export const noUnnecessaryPipe = Rule.define({
14
- name: "no-unnecessary-pipe",
15
- meta: Rule.meta({
16
- type: "suggestion",
17
- description: "Unnecessary pipe() with single argument. Use the value directly.",
18
- fixable: "code",
19
- }),
20
- create: function* () {
21
- const ctx = yield* RuleContext
22
- return {
23
- CallExpression: (node) => {
24
- // Match pipe(x) — bare pipe call with exactly one arg
25
- const name = Option.getOrUndefined(AST.calleeName(node as ESTree.CallExpression))
26
- if (name !== "pipe") return Effect.void
27
-
28
- if (
29
- "arguments" in node &&
30
- Array.isArray(node.arguments) &&
31
- node.arguments.length === 1
32
- ) {
33
- return ctx.report(
34
- Diagnostic.make({
35
- node,
36
- message:
37
- "Unnecessary pipe() with single argument. Use the value directly.",
38
- }),
39
- )
40
- }
41
- return Effect.void
42
- },
43
- }
44
- },
45
- })
@@ -1,516 +0,0 @@
1
- /**
2
- * AST pattern matching helpers returning `Option` for safe composition.
3
- *
4
- * Every matcher receives a raw ESTree node and returns `Option<NarrowedType>`
5
- * so callers can chain with `Option.map`, `Option.flatMap`, etc.
6
- *
7
- * @since 0.1.0
8
- */
9
- import type { ESTree } from '@oxlint/plugins';
10
- import * as Arr from 'effect/Array';
11
- import { dual, pipe } from 'effect/Function';
12
- import * as Option from 'effect/Option';
13
- import * as P from 'effect/Predicate';
14
- import * as Result from 'effect/Result';
15
-
16
- // ---------------------------------------------------------------------------
17
- // Internal helpers
18
- // ---------------------------------------------------------------------------
19
-
20
- /** @internal */
21
- const isIdentifier = (
22
- node: unknown
23
- ): node is ESTree.IdentifierName | ESTree.IdentifierReference =>
24
- P.isObject(node) &&
25
- 'type' in node &&
26
- node['type'] === 'Identifier' &&
27
- 'name' in node &&
28
- P.isString(node['name']);
29
-
30
- /** @internal */
31
- const identifierName = (node: unknown): Option.Option<string> =>
32
- isIdentifier(node) ? Option.some(node.name) : Option.none();
33
-
34
- /** @internal */
35
- const isStaticMember = (
36
- node: ESTree.MemberExpression
37
- ): node is ESTree.StaticMemberExpression =>
38
- !node.computed && node.property.type !== 'PrivateIdentifier';
39
-
40
- // ---------------------------------------------------------------------------
41
- // Member expression matching
42
- // ---------------------------------------------------------------------------
43
-
44
- /**
45
- * Match a `MemberExpression` of the form `obj.prop` where `obj` is an
46
- * identifier with the given name and `prop` matches one of the given
47
- * property names.
48
- *
49
- * @example
50
- * ```ts
51
- * // Match JSON.parse or JSON.stringify
52
- * AST.matchMember(node, 'JSON', ['parse', 'stringify'])
53
- * ```
54
- *
55
- * @since 0.1.0
56
- */
57
- export const matchMember: {
58
- (
59
- obj: string,
60
- prop: string | ReadonlyArray<string>
61
- ): (
62
- node: ESTree.MemberExpression
63
- ) => Option.Option<ESTree.StaticMemberExpression>;
64
- (
65
- node: ESTree.MemberExpression,
66
- obj: string,
67
- prop: string | ReadonlyArray<string>
68
- ): Option.Option<ESTree.StaticMemberExpression>;
69
- } = dual(
70
- 3,
71
- (
72
- node: ESTree.MemberExpression,
73
- obj: string,
74
- prop: string | ReadonlyArray<string>
75
- ): Option.Option<ESTree.StaticMemberExpression> => {
76
- if (!isStaticMember(node)) return Option.none();
77
- const props = P.isString(prop) ? [prop] : prop;
78
- return pipe(
79
- identifierName(node.object),
80
- Option.filter((name) => name === obj),
81
- Option.flatMap(() => identifierName(node.property)),
82
- Option.filter((name) => Arr.contains(props, name)),
83
- Option.map(() => node)
84
- );
85
- }
86
- );
87
-
88
- /**
89
- * Check whether a `MemberExpression` is `obj.prop`.
90
- *
91
- * Pure boolean predicate — use `matchMember` when you need the narrowed node.
92
- *
93
- * @since 0.1.0
94
- */
95
- export const isMember: {
96
- (
97
- obj: string,
98
- prop: string | ReadonlyArray<string>
99
- ): (node: ESTree.MemberExpression) => boolean;
100
- (
101
- node: ESTree.MemberExpression,
102
- obj: string,
103
- prop: string | ReadonlyArray<string>
104
- ): boolean;
105
- } = dual(
106
- 3,
107
- (
108
- node: ESTree.MemberExpression,
109
- obj: string,
110
- prop: string | ReadonlyArray<string>
111
- ): boolean => Option.isSome(matchMember(node, obj, prop))
112
- );
113
-
114
- // ---------------------------------------------------------------------------
115
- // Call expression matching
116
- // ---------------------------------------------------------------------------
117
-
118
- /**
119
- * Match a `CallExpression` whose callee is `obj.prop(...)`.
120
- *
121
- * Returns the call expression narrowed to confirm its callee is a
122
- * static member expression.
123
- *
124
- * @example
125
- * ```ts
126
- * AST.matchCallOf(node, 'Effect', 'gen')
127
- * AST.matchCallOf(node, 'Effect', ['fn', 'fnUntraced'])
128
- * ```
129
- *
130
- * @since 0.1.0
131
- */
132
- export const matchCallOf: {
133
- (
134
- obj: string,
135
- prop: string | ReadonlyArray<string>
136
- ): (node: ESTree.CallExpression) => Option.Option<ESTree.CallExpression>;
137
- (
138
- node: ESTree.CallExpression,
139
- obj: string,
140
- prop: string | ReadonlyArray<string>
141
- ): Option.Option<ESTree.CallExpression>;
142
- } = dual(
143
- 3,
144
- (
145
- node: ESTree.CallExpression,
146
- obj: string,
147
- prop: string | ReadonlyArray<string>
148
- ): Option.Option<ESTree.CallExpression> =>
149
- node.callee.type === 'MemberExpression'
150
- ? pipe(
151
- matchMember(node.callee, obj, prop),
152
- Option.map(() => node)
153
- )
154
- : Option.none()
155
- );
156
-
157
- /**
158
- * Boolean predicate: is this `CallExpression` a call of `obj.prop(...)`?
159
- *
160
- * @since 0.1.0
161
- */
162
- export const isCallOf: {
163
- (
164
- obj: string,
165
- prop: string | ReadonlyArray<string>
166
- ): (node: ESTree.CallExpression) => boolean;
167
- (
168
- node: ESTree.CallExpression,
169
- obj: string,
170
- prop: string | ReadonlyArray<string>
171
- ): boolean;
172
- } = dual(
173
- 3,
174
- (
175
- node: ESTree.CallExpression,
176
- obj: string,
177
- prop: string | ReadonlyArray<string>
178
- ): boolean => Option.isSome(matchCallOf(node, obj, prop))
179
- );
180
-
181
- // ---------------------------------------------------------------------------
182
- // Import matching
183
- // ---------------------------------------------------------------------------
184
-
185
- /**
186
- * Match an `ImportDeclaration` whose source matches a string or predicate.
187
- *
188
- * @example
189
- * ```ts
190
- * AST.matchImport(node, 'node:fs')
191
- * AST.matchImport(node, (src) => src.startsWith('node:'))
192
- * ```
193
- *
194
- * @since 0.1.0
195
- */
196
- export const matchImport: {
197
- (
198
- source: string | ((source: string) => boolean)
199
- ): (
200
- node: ESTree.ImportDeclaration
201
- ) => Option.Option<ESTree.ImportDeclaration>;
202
- (
203
- node: ESTree.ImportDeclaration,
204
- source: string | ((source: string) => boolean)
205
- ): Option.Option<ESTree.ImportDeclaration>;
206
- } = dual(
207
- 2,
208
- (
209
- node: ESTree.ImportDeclaration,
210
- source: string | ((source: string) => boolean)
211
- ): Option.Option<ESTree.ImportDeclaration> => {
212
- const src = node.source.value;
213
- const matches = P.isString(source) ? src === source : source(src);
214
- return matches ? Option.some(node) : Option.none();
215
- }
216
- );
217
-
218
- /**
219
- * Boolean predicate: does this `ImportDeclaration` import from the given source?
220
- *
221
- * @since 0.1.0
222
- */
223
- export const isImport: {
224
- (
225
- source: string | ((source: string) => boolean)
226
- ): (node: ESTree.ImportDeclaration) => boolean;
227
- (
228
- node: ESTree.ImportDeclaration,
229
- source: string | ((source: string) => boolean)
230
- ): boolean;
231
- } = dual(
232
- 2,
233
- (
234
- node: ESTree.ImportDeclaration,
235
- source: string | ((source: string) => boolean)
236
- ): boolean => Option.isSome(matchImport(node, source))
237
- );
238
-
239
- // ---------------------------------------------------------------------------
240
- // Identifier extraction
241
- // ---------------------------------------------------------------------------
242
-
243
- /**
244
- * Extract the callee name from a `CallExpression` when the callee is a
245
- * bare identifier (e.g. `fetch(...)`).
246
- *
247
- * @since 0.1.0
248
- */
249
- export const calleeName = (
250
- node: ESTree.CallExpression
251
- ): Option.Option<string> => identifierName(node.callee);
252
-
253
- /**
254
- * Extract the object and property names from a static `MemberExpression`.
255
- *
256
- * Returns `Option<readonly [objectName, propertyName]>`.
257
- *
258
- * @since 0.1.0
259
- */
260
- export const memberNames = (
261
- node: ESTree.MemberExpression
262
- ): Option.Option<readonly [obj: string, prop: string]> =>
263
- node.computed
264
- ? Option.none()
265
- : pipe(
266
- identifierName(node.object),
267
- Option.flatMap((obj) =>
268
- pipe(
269
- identifierName(node.property),
270
- Option.map((prop) => [obj, prop] as const)
271
- )
272
- )
273
- );
274
-
275
- /**
276
- * Extract the import source string from an `ImportDeclaration`.
277
- *
278
- * @since 0.1.0
279
- */
280
- export const importSource = (node: ESTree.ImportDeclaration): string =>
281
- node.source.value;
282
-
283
- // ---------------------------------------------------------------------------
284
- // Object expression helpers
285
- // ---------------------------------------------------------------------------
286
-
287
- /**
288
- * Collect the statically-known key names from an `ObjectExpression`.
289
- *
290
- * Spread elements and computed properties are ignored.
291
- *
292
- * @since 0.1.0
293
- */
294
- export const objectKeys = (
295
- node: ESTree.ObjectExpression
296
- ): ReadonlyArray<string> =>
297
- pipe(
298
- node.properties,
299
- Arr.filterMap((p) => {
300
- if (p.type !== 'Property') return Result.fail(undefined);
301
- return pipe(
302
- identifierName(p.key),
303
- Option.orElse(() =>
304
- p.key.type === 'Literal' && P.isString(p.key.value)
305
- ? Option.some(p.key.value)
306
- : Option.none()
307
- ),
308
- Result.fromOption(() => undefined)
309
- );
310
- })
311
- );
312
-
313
- /**
314
- * Check whether an `ObjectExpression` has a property with the given key.
315
- *
316
- * @since 0.1.0
317
- */
318
- export const objectHasKey: {
319
- (key: string): (node: ESTree.ObjectExpression) => boolean;
320
- (node: ESTree.ObjectExpression, key: string): boolean;
321
- } = dual(2, (node: ESTree.ObjectExpression, key: string): boolean =>
322
- Arr.contains(objectKeys(node), key)
323
- );
324
-
325
- /**
326
- * Get the value expression for a given key in an `ObjectExpression`.
327
- *
328
- * @since 0.1.0
329
- */
330
- export const objectGetValue: {
331
- (
332
- key: string
333
- ): (node: ESTree.ObjectExpression) => Option.Option<ESTree.Expression>;
334
- (
335
- node: ESTree.ObjectExpression,
336
- key: string
337
- ): Option.Option<ESTree.Expression>;
338
- } = dual(
339
- 2,
340
- (
341
- node: ESTree.ObjectExpression,
342
- key: string
343
- ): Option.Option<ESTree.Expression> =>
344
- pipe(
345
- node.properties,
346
- Arr.findFirst(
347
- (p): p is ESTree.ObjectProperty =>
348
- p.type === 'Property' &&
349
- (identifierName(p.key).pipe(
350
- Option.map((n) => n === key),
351
- Option.getOrElse(() => false)
352
- ) ||
353
- (p.key.type === 'Literal' && p.key.value === key))
354
- ),
355
- Option.map((p) => p.value)
356
- )
357
- );
358
-
359
- // ---------------------------------------------------------------------------
360
- // Node narrowing
361
- // ---------------------------------------------------------------------------
362
-
363
- /**
364
- * Narrow an AST node to a specific `type` string, returning `Option<Node>`.
365
- *
366
- * This is a safe alternative to casting — returns `Option.none()` if the
367
- * node's `type` doesn't match.
368
- *
369
- * @example
370
- * ```ts
371
- * AST.narrow(node, 'Identifier') // Option<Node & { type: "Identifier" }>
372
- * AST.narrow(node, 'CallExpression') // Option<Node & { type: "CallExpression" }>
373
- * ```
374
- *
375
- * @since 0.2.0
376
- */
377
- /** @internal Type guard: does the node's `type` match the literal? */
378
- const hasType = <T extends string>(
379
- node: ESTree.Node,
380
- type: T
381
- ): node is ESTree.Node & { readonly type: T } => node.type === type;
382
-
383
- export const narrow: {
384
- <T extends string>(
385
- type: T
386
- ): (node: ESTree.Node) => Option.Option<ESTree.Node & { readonly type: T }>;
387
- <T extends string>(
388
- node: ESTree.Node,
389
- type: T
390
- ): Option.Option<ESTree.Node & { readonly type: T }>;
391
- } = dual(
392
- 2,
393
- <T extends string>(
394
- node: ESTree.Node,
395
- type: T
396
- ): Option.Option<ESTree.Node & { readonly type: T }> =>
397
- hasType(node, type) ? Option.some(node) : Option.none()
398
- );
399
-
400
- // ---------------------------------------------------------------------------
401
- // Member path extraction
402
- // ---------------------------------------------------------------------------
403
-
404
- /**
405
- * Extract the full member path from a (possibly chained) `MemberExpression`.
406
- *
407
- * Walks `a.b.c` → `['a', 'b', 'c']`. Returns `Option.none()` if any
408
- * segment is computed or non-identifier.
409
- *
410
- * @example
411
- * ```ts
412
- * // node is `Effect.gen` → Some(['Effect', 'gen'])
413
- * AST.memberPath(node)
414
- * // node is `a.b.c.d` → Some(['a', 'b', 'c', 'd'])
415
- * AST.memberPath(node)
416
- * // node is `a[b].c` → None (computed segment)
417
- * AST.memberPath(node)
418
- * ```
419
- *
420
- * @since 0.2.0
421
- */
422
- export const memberPath = (
423
- node: ESTree.MemberExpression
424
- ): Option.Option<Arr.NonEmptyReadonlyArray<string>> => {
425
- /** @internal Collect property names from right to left. */
426
- const collect = (
427
- current: ESTree.Expression | ESTree.PrivateIdentifier,
428
- acc: ReadonlyArray<string>
429
- ): Option.Option<Arr.NonEmptyReadonlyArray<string>> => {
430
- if (
431
- !P.isObject(current) ||
432
- !('type' in current) ||
433
- current.type !== 'MemberExpression'
434
- ) {
435
- return pipe(
436
- identifierName(current),
437
- Option.map(
438
- (rootName) =>
439
- [
440
- rootName,
441
- ...acc
442
- ] satisfies Arr.NonEmptyReadonlyArray<string>
443
- )
444
- );
445
- }
446
- if (current.computed) return Option.none();
447
- return pipe(
448
- identifierName(current.property),
449
- Option.flatMap((propName) =>
450
- collect(current.object, [propName, ...acc])
451
- )
452
- );
453
- };
454
- return collect(node, []);
455
- };
456
-
457
- // ---------------------------------------------------------------------------
458
- // Ancestor / parent helpers
459
- // ---------------------------------------------------------------------------
460
-
461
- /** @internal Type guard for objects with a string `type` and optional `parent`. */
462
- const isASTShape = (
463
- value: unknown
464
- ): value is { readonly type: string; readonly parent?: unknown } =>
465
- P.isObject(value) && 'type' in value && P.isString(value['type']);
466
-
467
- /**
468
- * Walk the `.parent` chain and return the first ancestor whose `type`
469
- * matches the given string.
470
- *
471
- * Returns the ancestor as an opaque record — callers should narrow
472
- * via `type` checks or other AST helpers rather than casting.
473
- *
474
- * @since 0.1.0
475
- */
476
- export const findAncestor: {
477
- (
478
- type: string
479
- ): (node: {
480
- readonly parent?: unknown;
481
- }) => Option.Option<{ readonly type: string; readonly parent?: unknown }>;
482
- (
483
- node: { readonly parent?: unknown },
484
- type: string
485
- ): Option.Option<{ readonly type: string; readonly parent?: unknown }>;
486
- } = dual(
487
- 2,
488
- (
489
- node: { readonly parent?: unknown },
490
- type: string
491
- ): Option.Option<{ readonly type: string; readonly parent?: unknown }> => {
492
- const walk = (
493
- current: unknown
494
- ): Option.Option<{
495
- readonly type: string;
496
- readonly parent?: unknown;
497
- }> => {
498
- if (!isASTShape(current)) return Option.none();
499
- if (current.type === type) return Option.some(current);
500
- return walk(current.parent);
501
- };
502
- return walk(node.parent);
503
- }
504
- );
505
-
506
- /**
507
- * Check whether any ancestor of the node has the given `type`.
508
- *
509
- * @since 0.1.0
510
- */
511
- export const hasAncestor: {
512
- (type: string): (node: { readonly parent?: unknown }) => boolean;
513
- (node: { readonly parent?: unknown }, type: string): boolean;
514
- } = dual(2, (node: { readonly parent?: unknown }, type: string): boolean =>
515
- Option.isSome(findAncestor(node, type))
516
- );