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.
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +18 -0
- package/dist/presets/core.d.ts +43 -0
- package/dist/presets/core.js +43 -0
- package/dist/presets/effect-native.d.ts +28 -0
- package/dist/presets/effect-native.js +28 -0
- package/dist/presets/full.d.ts +60 -0
- package/dist/presets/full.js +60 -0
- package/dist/presets/functional.d.ts +21 -0
- package/dist/presets/functional.js +21 -0
- package/dist/presets/index.d.ts +13 -0
- package/dist/presets/index.js +18 -0
- package/dist/presets/strict.d.ts +69 -0
- package/dist/presets/strict.js +69 -0
- package/dist/presets/v3.d.ts +15 -0
- package/dist/presets/v3.js +15 -0
- package/dist/rules/_effect-context.d.ts +19 -0
- package/dist/rules/_effect-context.js +22 -0
- package/dist/rules/index.d.ts +66 -0
- package/dist/rules/index.js +124 -0
- package/dist/rules/no-arrow-ladder.d.ts +6 -0
- package/dist/rules/no-arrow-ladder.js +61 -0
- package/dist/rules/no-async-function.d.ts +6 -0
- package/dist/rules/no-async-function.js +46 -0
- package/dist/rules/no-catch-all-to-map-error.d.ts +6 -0
- package/dist/rules/no-catch-all-to-map-error.js +67 -0
- package/dist/rules/no-effect-as.d.ts +6 -0
- package/dist/rules/no-effect-as.js +16 -0
- package/dist/rules/no-effect-async.d.ts +6 -0
- package/dist/rules/no-effect-async.js +16 -0
- package/dist/rules/no-effect-bind.d.ts +6 -0
- package/dist/rules/no-effect-bind.js +16 -0
- package/dist/rules/no-effect-do.d.ts +6 -0
- package/dist/rules/no-effect-do.js +16 -0
- package/dist/rules/no-effect-fn-generator.d.ts +6 -0
- package/dist/rules/no-effect-fn-generator.js +30 -0
- package/dist/rules/no-effect-gen-adapter.d.ts +6 -0
- package/dist/rules/no-effect-gen-adapter.js +30 -0
- package/dist/rules/no-effect-map-flatten.d.ts +6 -0
- package/dist/rules/no-effect-map-flatten.js +53 -0
- package/dist/rules/no-effect-map-void.d.ts +6 -0
- package/dist/rules/no-effect-map-void.js +40 -0
- package/dist/rules/no-effect-never.d.ts +6 -0
- package/dist/rules/no-effect-never.js +16 -0
- package/dist/rules/no-effect-orElse-ladder.d.ts +6 -0
- package/dist/rules/no-effect-orElse-ladder.js +53 -0
- package/dist/rules/no-effect-succeed-string.d.ts +6 -0
- package/dist/rules/no-effect-succeed-string.js +30 -0
- package/dist/rules/no-effect-succeed-variable.d.ts +6 -0
- package/dist/rules/no-effect-succeed-variable.js +30 -0
- package/dist/rules/no-effect-succeed-void.d.ts +6 -0
- package/dist/rules/no-effect-succeed-void.js +33 -0
- package/dist/rules/no-effect-sync-console.d.ts +6 -0
- package/dist/rules/no-effect-sync-console.js +63 -0
- package/dist/rules/no-effect-sync-wrapper.d.ts +6 -0
- package/dist/rules/no-effect-sync-wrapper.js +37 -0
- package/dist/rules/no-effect-type-alias.d.ts +6 -0
- package/dist/rules/no-effect-type-alias.js +52 -0
- package/dist/rules/no-effect-wrapper-alias.d.ts +6 -0
- package/dist/rules/no-effect-wrapper-alias.js +69 -0
- package/dist/rules/no-extends-native-error.d.ts +6 -0
- package/dist/rules/no-extends-native-error.js +41 -0
- package/dist/rules/no-flatmap-ladder.d.ts +6 -0
- package/dist/rules/no-flatmap-ladder.js +48 -0
- package/dist/rules/no-fromnullable-coalesce.d.ts +6 -0
- package/dist/rules/no-fromnullable-coalesce.js +36 -0
- package/dist/rules/no-global-error-in-catch.d.ts +6 -0
- package/dist/rules/no-global-error-in-catch.js +75 -0
- package/dist/rules/no-global-error-in-failure.d.ts +6 -0
- package/dist/rules/no-global-error-in-failure.js +54 -0
- package/dist/rules/no-globals.d.ts +6 -0
- package/dist/rules/no-globals.js +164 -0
- package/dist/rules/no-if-statement.d.ts +6 -0
- package/dist/rules/no-if-statement.js +18 -0
- package/dist/rules/no-iife-wrapper.d.ts +6 -0
- package/dist/rules/no-iife-wrapper.js +37 -0
- package/dist/rules/no-inline-provide.d.ts +6 -0
- package/dist/rules/no-inline-provide.js +31 -0
- package/dist/rules/no-instanceof-schema.d.ts +6 -0
- package/dist/rules/no-instanceof-schema.js +35 -0
- package/dist/rules/no-manual-effect-channels.d.ts +6 -0
- package/dist/rules/no-manual-effect-channels.js +40 -0
- package/dist/rules/no-match-effect-branch.d.ts +6 -0
- package/dist/rules/no-match-effect-branch.js +64 -0
- package/dist/rules/no-match-void-branch.d.ts +6 -0
- package/dist/rules/no-match-void-branch.js +50 -0
- package/dist/rules/no-multiple-effect-provide.d.ts +6 -0
- package/dist/rules/no-multiple-effect-provide.js +45 -0
- package/dist/rules/no-nested-effect-call.d.ts +6 -0
- package/dist/rules/no-nested-effect-call.js +97 -0
- package/dist/rules/no-nested-effect-gen.d.ts +6 -0
- package/dist/rules/no-nested-effect-gen.js +46 -0
- package/dist/rules/no-nested-pipe.d.ts +6 -0
- package/dist/rules/no-nested-pipe.js +38 -0
- package/dist/rules/no-new-error.d.ts +6 -0
- package/dist/rules/no-new-error.js +39 -0
- package/dist/rules/no-new-promise.d.ts +6 -0
- package/dist/rules/no-new-promise.js +38 -0
- package/dist/rules/no-node-builtin-import.d.ts +6 -0
- package/dist/rules/no-node-builtin-import.js +61 -0
- package/dist/rules/no-option-as.d.ts +6 -0
- package/dist/rules/no-option-as.js +16 -0
- package/dist/rules/no-option-boolean-normalization.d.ts +6 -0
- package/dist/rules/no-option-boolean-normalization.js +50 -0
- package/dist/rules/no-positional-log-error.d.ts +6 -0
- package/dist/rules/no-positional-log-error.js +52 -0
- package/dist/rules/no-redundant-schema-tag-identifier.d.ts +6 -0
- package/dist/rules/no-redundant-schema-tag-identifier.js +46 -0
- package/dist/rules/no-return-in-arrow.d.ts +6 -0
- package/dist/rules/no-return-in-arrow.js +41 -0
- package/dist/rules/no-return-nullish.d.ts +6 -0
- package/dist/rules/no-return-nullish.js +47 -0
- package/dist/rules/no-run-in-effect-gen.d.ts +6 -0
- package/dist/rules/no-run-in-effect-gen.js +48 -0
- package/dist/rules/no-runtime-run-fork.d.ts +6 -0
- package/dist/rules/no-runtime-run-fork.js +19 -0
- package/dist/rules/no-schema-struct-with-tag.d.ts +6 -0
- package/dist/rules/no-schema-struct-with-tag.js +48 -0
- package/dist/rules/no-schema-union-of-literals.d.ts +6 -0
- package/dist/rules/no-schema-union-of-literals.js +35 -0
- package/dist/rules/no-string-sentinel-const.d.ts +6 -0
- package/dist/rules/no-string-sentinel-const.js +38 -0
- package/dist/rules/no-switch-statement.d.ts +6 -0
- package/dist/rules/no-switch-statement.js +16 -0
- package/dist/rules/no-ternary.d.ts +6 -0
- package/dist/rules/no-ternary.js +30 -0
- package/dist/rules/no-throw-statement.d.ts +6 -0
- package/dist/rules/no-throw-statement.js +34 -0
- package/dist/rules/no-try-catch.d.ts +6 -0
- package/dist/rules/no-try-catch.js +34 -0
- package/dist/rules/no-unnecessary-arrow-block.d.ts +6 -0
- package/dist/rules/no-unnecessary-arrow-block.js +34 -0
- package/dist/rules/no-unnecessary-effect-gen.d.ts +6 -0
- package/dist/rules/no-unnecessary-effect-gen.js +35 -0
- package/dist/rules/no-unnecessary-pipe-chain.d.ts +6 -0
- package/dist/rules/no-unnecessary-pipe-chain.js +36 -0
- package/dist/rules/no-unnecessary-pipe.d.ts +6 -0
- package/dist/rules/no-unnecessary-pipe.js +29 -0
- package/dist/vendor/effect-oxlint/AST.d.ts +199 -0
- package/dist/vendor/effect-oxlint/AST.js +214 -0
- package/dist/vendor/effect-oxlint/Comment.d.ts +55 -0
- package/dist/vendor/effect-oxlint/Comment.js +67 -0
- package/dist/vendor/effect-oxlint/Diagnostic.d.ts +86 -0
- package/dist/vendor/effect-oxlint/Diagnostic.js +115 -0
- package/dist/vendor/effect-oxlint/Plugin.d.ts +38 -0
- package/dist/vendor/effect-oxlint/Plugin.js +45 -0
- package/dist/vendor/effect-oxlint/Rule.d.ts +218 -0
- package/dist/vendor/effect-oxlint/Rule.js +309 -0
- package/dist/vendor/effect-oxlint/RuleContext.d.ts +77 -0
- package/dist/vendor/effect-oxlint/RuleContext.js +74 -0
- package/dist/vendor/effect-oxlint/Scope.d.ts +100 -0
- package/dist/vendor/effect-oxlint/Scope.js +110 -0
- package/dist/vendor/effect-oxlint/SourceCode.d.ts +180 -0
- package/dist/vendor/effect-oxlint/SourceCode.js +203 -0
- package/dist/vendor/effect-oxlint/Testing.d.ts +510 -0
- package/dist/vendor/effect-oxlint/Testing.js +963 -0
- package/dist/vendor/effect-oxlint/Token.d.ts +98 -0
- package/dist/vendor/effect-oxlint/Token.js +103 -0
- package/dist/vendor/effect-oxlint/Visitor.d.ts +166 -0
- package/dist/vendor/effect-oxlint/Visitor.js +138 -0
- package/dist/vendor/effect-oxlint/index.d.ts +13 -0
- package/dist/vendor/effect-oxlint/index.js +12 -0
- package/package.json +24 -5
- package/.effect-lsp.json +0 -60
- package/.oxlintrc.json +0 -33
- package/CHANGELOG.md +0 -55
- package/scripts/add-rule.ts +0 -87
- package/scripts/codegen.ts +0 -108
- package/src/index.ts +0 -9
- package/src/plugin.ts +0 -16
- package/src/presets/core.ts +0 -47
- package/src/presets/effect-native.ts +0 -34
- package/src/presets/full.ts +0 -70
- package/src/presets/functional.ts +0 -18
- package/src/presets/index.ts +0 -5
- package/src/presets/strict.ts +0 -77
- package/src/rules/_effect-context.ts +0 -28
- package/src/rules/index.ts +0 -72
- package/src/rules/no-arrow-ladder.ts +0 -70
- package/src/rules/no-async-function.ts +0 -47
- package/src/rules/no-catch-all-to-map-error.ts +0 -90
- package/src/rules/no-effect-as.ts +0 -14
- package/src/rules/no-effect-async.ts +0 -14
- package/src/rules/no-effect-bind.ts +0 -13
- package/src/rules/no-effect-do.ts +0 -14
- package/src/rules/no-effect-fn-generator.ts +0 -58
- package/src/rules/no-effect-gen-adapter.ts +0 -51
- package/src/rules/no-effect-map-flatten.ts +0 -82
- package/src/rules/no-effect-map-void.ts +0 -80
- package/src/rules/no-effect-never.ts +0 -14
- package/src/rules/no-effect-orElse-ladder.ts +0 -79
- package/src/rules/no-effect-succeed-string.ts +0 -52
- package/src/rules/no-effect-succeed-variable.ts +0 -50
- package/src/rules/no-effect-succeed-void.ts +0 -63
- package/src/rules/no-effect-sync-console.ts +0 -91
- package/src/rules/no-effect-sync-wrapper.ts +0 -67
- package/src/rules/no-effect-type-alias.ts +0 -71
- package/src/rules/no-effect-wrapper-alias.ts +0 -95
- package/src/rules/no-extends-native-error.ts +0 -54
- package/src/rules/no-flatmap-ladder.ts +0 -77
- package/src/rules/no-fromnullable-coalesce.ts +0 -62
- package/src/rules/no-global-error-in-catch.ts +0 -104
- package/src/rules/no-global-error-in-failure.ts +0 -83
- package/src/rules/no-globals.ts +0 -178
- package/src/rules/no-if-statement.ts +0 -16
- package/src/rules/no-iife-wrapper.ts +0 -51
- package/src/rules/no-inline-provide.ts +0 -41
- package/src/rules/no-instanceof-schema.ts +0 -42
- package/src/rules/no-manual-effect-channels.ts +0 -58
- package/src/rules/no-match-effect-branch.ts +0 -82
- package/src/rules/no-match-void-branch.ts +0 -72
- package/src/rules/no-multiple-effect-provide.ts +0 -75
- package/src/rules/no-nested-effect-call.ts +0 -57
- package/src/rules/no-nested-effect-gen.ts +0 -43
- package/src/rules/no-nested-pipe.ts +0 -73
- package/src/rules/no-new-error.ts +0 -53
- package/src/rules/no-new-promise.ts +0 -50
- package/src/rules/no-node-builtin-import.ts +0 -68
- package/src/rules/no-option-as.ts +0 -13
- package/src/rules/no-option-boolean-normalization.ts +0 -88
- package/src/rules/no-positional-log-error.ts +0 -76
- package/src/rules/no-redundant-schema-tag-identifier.ts +0 -62
- package/src/rules/no-return-in-arrow.ts +0 -76
- package/src/rules/no-return-nullish.ts +0 -80
- package/src/rules/no-run-in-effect-gen.ts +0 -66
- package/src/rules/no-runtime-run-fork.ts +0 -14
- package/src/rules/no-schema-struct-with-tag.ts +0 -64
- package/src/rules/no-schema-union-of-literals.ts +0 -48
- package/src/rules/no-string-sentinel-const.ts +0 -49
- package/src/rules/no-switch-statement.ts +0 -13
- package/src/rules/no-ternary.ts +0 -30
- package/src/rules/no-throw-statement.ts +0 -39
- package/src/rules/no-try-catch.ts +0 -39
- package/src/rules/no-unnecessary-arrow-block.ts +0 -51
- package/src/rules/no-unnecessary-effect-gen.ts +0 -83
- package/src/rules/no-unnecessary-pipe-chain.ts +0 -47
- package/src/rules/no-unnecessary-pipe.ts +0 -45
- package/src/vendor/effect-oxlint/AST.ts +0 -516
- package/src/vendor/effect-oxlint/Comment.ts +0 -86
- package/src/vendor/effect-oxlint/Diagnostic.ts +0 -199
- package/src/vendor/effect-oxlint/Plugin.ts +0 -71
- package/src/vendor/effect-oxlint/Rule.ts +0 -548
- package/src/vendor/effect-oxlint/RuleContext.ts +0 -144
- package/src/vendor/effect-oxlint/Scope.ts +0 -177
- package/src/vendor/effect-oxlint/SourceCode.ts +0 -377
- package/src/vendor/effect-oxlint/Testing.ts +0 -1227
- package/src/vendor/effect-oxlint/Token.ts +0 -131
- package/src/vendor/effect-oxlint/Visitor.ts +0 -287
- package/src/vendor/effect-oxlint/index.ts +0 -88
- package/tests/ban-rules.test.ts +0 -68
- package/tests/context-rules.test.ts +0 -263
- package/tests/pattern-rules.test.ts +0 -247
- 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
|
-
);
|