1 | import assertNever from 'assert-never';
|
2 | import {ancestor as walk} from 'babel-walk';
|
3 | import * as t from '@babel/types';
|
4 | import isReferenced from './reference';
|
5 |
|
6 | const isScope = (node: t.Node) => t.isFunctionParent(node) || t.isProgram(node);
|
7 | const isBlockScope = (node: t.Node) =>
|
8 | t.isBlockStatement(node) || isScope(node);
|
9 |
|
10 | const declaresArguments = (node: t.Node) =>
|
11 | t.isFunction(node) && !t.isArrowFunctionExpression(node);
|
12 |
|
13 | const declaresThis = declaresArguments;
|
14 |
|
15 | const LOCALS_SYMBOL = Symbol('locals');
|
16 |
|
17 | const getLocals = (node: t.Node): Set<string> | undefined =>
|
18 | (node as any)[LOCALS_SYMBOL];
|
19 | const declareLocals = (node: t.Node): Set<string> =>
|
20 | ((node as any)[LOCALS_SYMBOL] = (node as any)[LOCALS_SYMBOL] || new Set());
|
21 |
|
22 | const setLocal = (node: t.Node, name: string) => declareLocals(node).add(name);
|
23 |
|
24 |
|
25 |
|
26 | function declareFunction(node: t.Function) {
|
27 | for (const param of node.params) {
|
28 | declarePattern(param, node);
|
29 | }
|
30 | const id = (node as t.FunctionDeclaration).id;
|
31 | if (id) {
|
32 | setLocal(node, id.name);
|
33 | }
|
34 | }
|
35 |
|
36 | function declarePattern(node: t.LVal, parent: t.Node) {
|
37 | switch (node.type) {
|
38 | case 'Identifier':
|
39 | setLocal(parent, node.name);
|
40 | break;
|
41 | case 'ObjectPattern':
|
42 | for (const prop of node.properties) {
|
43 | switch (prop.type) {
|
44 | case 'RestElement':
|
45 | declarePattern(prop.argument, parent);
|
46 | break;
|
47 | case 'ObjectProperty':
|
48 | declarePattern(prop.value as t.LVal, parent);
|
49 | break;
|
50 | default:
|
51 | assertNever(prop);
|
52 | break;
|
53 | }
|
54 | }
|
55 | break;
|
56 | case 'ArrayPattern':
|
57 | for (const element of node.elements) {
|
58 | if (element) declarePattern(element, parent);
|
59 | }
|
60 | break;
|
61 | case 'RestElement':
|
62 | declarePattern(node.argument, parent);
|
63 | break;
|
64 | case 'AssignmentPattern':
|
65 | declarePattern(node.left, parent);
|
66 | break;
|
67 |
|
68 | default:
|
69 | throw new Error('Unrecognized pattern type: ' + node.type);
|
70 | }
|
71 | }
|
72 |
|
73 | function declareModuleSpecifier(
|
74 | node:
|
75 | | t.ImportSpecifier
|
76 | | t.ImportDefaultSpecifier
|
77 | | t.ImportNamespaceSpecifier,
|
78 | _state: unknown,
|
79 | parents: t.Node[],
|
80 | ) {
|
81 | for (let i = parents.length - 2; i >= 0; i--) {
|
82 | if (isScope(parents[i])) {
|
83 | setLocal(parents[i], node.local.name);
|
84 | return;
|
85 | }
|
86 | }
|
87 | }
|
88 |
|
89 | const firstPass = walk({
|
90 | VariableDeclaration(node, _state, parents) {
|
91 | for (let i = parents.length - 2; i >= 0; i--) {
|
92 | if (
|
93 | node.kind === 'var'
|
94 | ? t.isFunctionParent(parents[i])
|
95 | : isBlockScope(parents[i])
|
96 | ) {
|
97 | for (const declaration of node.declarations) {
|
98 | declarePattern(declaration.id, parents[i]);
|
99 | }
|
100 | return;
|
101 | }
|
102 | }
|
103 | },
|
104 | FunctionDeclaration(node, _state, parents) {
|
105 | if (node.id) {
|
106 | for (let i = parents.length - 2; i >= 0; i--) {
|
107 | if (isScope(parents[i])) {
|
108 | setLocal(parents[i], node.id.name);
|
109 | return;
|
110 | }
|
111 | }
|
112 | }
|
113 | },
|
114 | Function: declareFunction,
|
115 | ClassDeclaration(node, _state, parents) {
|
116 | for (let i = parents.length - 2; i >= 0; i--) {
|
117 | if (isScope(parents[i])) {
|
118 | setLocal(parents[i], node.id.name);
|
119 | return;
|
120 | }
|
121 | }
|
122 | },
|
123 | TryStatement(node) {
|
124 | if (node.handler === null) return;
|
125 | if (node.handler.param === null) return;
|
126 | declarePattern(node.handler.param, node.handler);
|
127 | },
|
128 | ImportDefaultSpecifier: declareModuleSpecifier,
|
129 | ImportSpecifier: declareModuleSpecifier,
|
130 | ImportNamespaceSpecifier: declareModuleSpecifier,
|
131 | });
|
132 |
|
133 |
|
134 |
|
135 | const secondPass = walk<{
|
136 | globals: (t.Identifier | t.ThisExpression)[];
|
137 | }>({
|
138 | Identifier(node, state, parents) {
|
139 | const name = node.name;
|
140 | if (name === 'undefined') return;
|
141 |
|
142 | const lastParent = parents[parents.length - 2];
|
143 | if (lastParent) {
|
144 | if (!isReferenced(node, lastParent)) return;
|
145 |
|
146 | for (const parent of parents) {
|
147 | if (name === 'arguments' && declaresArguments(parent)) {
|
148 | return;
|
149 | }
|
150 | if (getLocals(parent)?.has(name)) {
|
151 | return;
|
152 | }
|
153 | }
|
154 | }
|
155 |
|
156 | state.globals.push(node);
|
157 | },
|
158 |
|
159 | ThisExpression(node, state, parents) {
|
160 | for (const parent of parents) {
|
161 | if (declaresThis(parent)) {
|
162 | return;
|
163 | }
|
164 | }
|
165 |
|
166 | state.globals.push(node);
|
167 | },
|
168 | });
|
169 |
|
170 | export default function findGlobals(ast: t.Node) {
|
171 | const globals: (t.Identifier | t.ThisExpression)[] = [];
|
172 |
|
173 |
|
174 | if (!t.isNode(ast)) {
|
175 | throw new TypeError('Source must be a Babylon AST');
|
176 | }
|
177 |
|
178 | firstPass(ast, undefined);
|
179 | secondPass(ast, {globals});
|
180 |
|
181 | const groupedGlobals = new Map<string, (t.Identifier | t.ThisExpression)[]>();
|
182 | for (const node of globals) {
|
183 | const name: string = node.type === 'ThisExpression' ? 'this' : node.name;
|
184 | const existing = groupedGlobals.get(name);
|
185 | if (existing) {
|
186 | existing.push(node);
|
187 | } else {
|
188 | groupedGlobals.set(name, [node]);
|
189 | }
|
190 | }
|
191 |
|
192 | return [...groupedGlobals]
|
193 | .map(([name, nodes]) => ({name, nodes}))
|
194 | .sort((a, b) => (a.name < b.name ? -1 : 1));
|
195 | }
|