-
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathAstTraverser.ts
More file actions
130 lines (107 loc) · 3.01 KB
/
Copy pathAstTraverser.ts
File metadata and controls
130 lines (107 loc) · 3.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import { visitorKeys } from '@typescript-eslint/visitor-keys'
import { TSESTree } from '@typescript-eslint/typescript-estree'
type Node = TSESTree.Node
function isValidNode(x: unknown): x is Node {
return (
typeof x === 'object' &&
x !== null &&
Object.prototype.hasOwnProperty.call(x, 'type') &&
typeof (x as { type: unknown }).type === 'string'
)
}
function getVisitorKeysForNode(
allVisitorKeys: typeof visitorKeys,
node: Node
): readonly (keyof Node)[] {
const keys = allVisitorKeys[node.type]
return (keys ?? []) as never
}
type TraverseOptions = {
[key in Node['type']]?: (
this: AstTraverser,
node: Node & { type: key },
parent: Node | undefined
) => void
} & Partial<{
enter: (this: AstTraverser, node: Node, parent: Node | undefined) => void
exit: (this: AstTraverser, node: Node, parent: Node | undefined) => void
}>
export class AstTraverser {
private readonly allVisitorKeys = visitorKeys
private readonly selectors: TraverseOptions
private readonly setParentPointers: boolean
private stopped: boolean
private skipped: boolean
constructor(selectors: TraverseOptions, setParentPointers = false) {
this.selectors = selectors
this.setParentPointers = setParentPointers
this.stopped = false
this.skipped = false
}
public break(): void {
this.stopped = true
}
public skip(): void {
this.skipped = true
}
public traverse(
node: unknown,
parent?: TSESTree.Node | undefined,
_skipChildren = false
): void {
if (!isValidNode(node)) {
return
}
if (parent === undefined) {
this.stopped = false
}
if (this.setParentPointers) {
node.parent = parent
}
const { enter, exit, [node.type]: onSelector } = this.selectors
this.skipped = false
if (enter) {
enter.call(this, node, parent)
}
if (onSelector) {
// Force the type here because TypeScript won't understand that because
// this was extracted using `[node.type]`, that means that the parameter
// type is correct. This widens the expected type to be a regular node,
// but it's actually node & { type: node.type }
;(
onSelector as (
this: AstTraverser,
node: Node,
parent: Node | undefined
) => void
).call(this, node, parent)
}
if (this.stopped) {
return
}
if (!this.skipped) {
const keys = getVisitorKeysForNode(this.allVisitorKeys, node)
for (const key of keys) {
const childOrChildren = node[key]
const children = Array.isArray(childOrChildren)
? childOrChildren
: [childOrChildren]
for (const child of children) {
if (this.stopped) {
return
}
this.traverse(child, node)
}
}
}
if (this.stopped) {
return
}
if (exit) {
exit.call(this, node, parent)
}
}
}
export function traverse(root: Node, options: TraverseOptions): void {
return new AstTraverser(options).traverse(root)
}