main
mjs 343 lines 9.65 KB
Raw
1 import { parse } from "@babel/parser";
2 import _traverse from "@babel/traverse";
3 const traverse = _traverse.default || _traverse;
4 import fs from "fs";
5 import path from "path";
6 import fg from "fast-glob";
7 const { globSync } = fg;
8
9 const FIXTURE_DIR = process.argv[2]; // source dir with JS/TS files
10 const OUTPUT_DIR = process.argv[3]; // output dir for JSON files
11
12 if (!FIXTURE_DIR || !OUTPUT_DIR) {
13 console.error(
14 "Usage: node babel-ast-to-json.mjs <fixtures-dir> <output-dir>"
15 );
16 process.exit(1);
17 }
18
19 // Find all fixture source files
20 const fixtures = globSync("**/*.{js,ts,tsx,jsx}", { cwd: FIXTURE_DIR });
21
22 function getScopeKind(babelScope) {
23 const blockType = babelScope.block.type;
24 switch (blockType) {
25 case "Program":
26 return "program";
27 case "FunctionDeclaration":
28 case "FunctionExpression":
29 case "ArrowFunctionExpression":
30 case "ObjectMethod":
31 case "ClassMethod":
32 case "ClassPrivateMethod":
33 return "function";
34 case "BlockStatement":
35 return "block";
36 case "ForStatement":
37 case "ForInStatement":
38 case "ForOfStatement":
39 return "for";
40 case "ClassDeclaration":
41 case "ClassExpression":
42 return "class";
43 case "SwitchStatement":
44 return "switch";
45 case "CatchClause":
46 return "catch";
47 default:
48 return "block";
49 }
50 }
51
52 function getBindingKind(babelKind) {
53 switch (babelKind) {
54 case "var":
55 return "var";
56 case "let":
57 return "let";
58 case "const":
59 return "const";
60 case "param":
61 return "param";
62 case "module":
63 return "module";
64 case "hoisted":
65 return "hoisted";
66 case "local":
67 return "local";
68 default:
69 return "unknown";
70 }
71 }
72
73 function getImportData(binding) {
74 if (binding.path.isImportSpecifier()) {
75 const imported = binding.path.node.imported;
76 return {
77 source: binding.path.parent.source.value,
78 kind: "named",
79 imported: imported.type === "StringLiteral" ? imported.value : imported.name,
80 };
81 } else if (binding.path.isImportDefaultSpecifier()) {
82 return {
83 source: binding.path.parent.source.value,
84 kind: "default",
85 };
86 } else if (binding.path.isImportNamespaceSpecifier()) {
87 return {
88 source: binding.path.parent.source.value,
89 kind: "namespace",
90 };
91 }
92 return null;
93 }
94
95 function collectScopeInfo(ast) {
96 const scopeMap = new Map(); // Babel scope -> ScopeId
97 const bindingMap = new Map(); // Babel binding -> BindingId
98 const scopes = [];
99 const bindings = [];
100 const nodeToScope = {};
101 const nodeToScopeEnd = {};
102 const referenceToBinding = {};
103 const refNodeIdToBinding = {};
104 const nodeIdToScope = {};
105 let nextScopeId = 0;
106 let nextBindingId = 0;
107 let nextNodeId = 1;
108
109 function getOrAssignNodeId(node) {
110 if (node._nodeId == null) {
111 node._nodeId = nextNodeId++;
112 }
113 return node._nodeId;
114 }
115
116 function mapRef(start, bindingId, node) {
117 referenceToBinding[String(start)] = bindingId;
118 const nodeId = getOrAssignNodeId(node);
119 refNodeIdToBinding[String(nodeId)] = bindingId;
120 }
121
122 function ensureScope(babelScope) {
123 if (scopeMap.has(babelScope)) return scopeMap.get(babelScope);
124
125 // Ensure parent is registered first (preorder: parent gets lower ID)
126 if (babelScope.parent) {
127 ensureScope(babelScope.parent);
128 }
129
130 const id = nextScopeId++;
131 scopeMap.set(babelScope, id);
132
133 const parentId = babelScope.parent ? scopeMap.get(babelScope.parent) : null;
134 const kind = getScopeKind(babelScope);
135 const bindingsMap = {};
136
137 // Register all bindings in this scope
138 for (const [name, binding] of Object.entries(babelScope.bindings)) {
139 if (!bindingMap.has(binding)) {
140 const bid = nextBindingId++;
141 bindingMap.set(binding, bid);
142 const declarationNodeId = getOrAssignNodeId(binding.identifier);
143 const bindingData = {
144 id: bid,
145 name,
146 kind: getBindingKind(binding.kind),
147 scope: id,
148 declarationType: binding.path.node.type,
149 declarationStart: binding.identifier.start,
150 declarationNodeId,
151 };
152
153 // Import bindings
154 if (binding.kind === "module") {
155 bindingData.import = getImportData(binding);
156 }
157
158 bindings.push(bindingData);
159 }
160 bindingsMap[name] = bindingMap.get(binding);
161 }
162
163 scopes.push({
164 id,
165 parent: parentId,
166 kind,
167 bindings: bindingsMap,
168 });
169
170 // Record node_to_scope and node_to_scope_end
171 const blockNode = babelScope.block;
172 if (blockNode.start != null) {
173 nodeToScope[String(blockNode.start)] = id;
174 if (blockNode.end != null) {
175 nodeToScopeEnd[String(blockNode.start)] = blockNode.end;
176 }
177 const scopeNodeId = getOrAssignNodeId(blockNode);
178 nodeIdToScope[String(scopeNodeId)] = id;
179 }
180
181 return id;
182 }
183
184 traverse(ast, {
185 enter(path) {
186 ensureScope(path.scope);
187 },
188 Identifier(path) {
189 getOrAssignNodeId(path.node);
190 if (!path.isReferencedIdentifier()) return;
191 const binding = path.scope.getBinding(path.node.name);
192 if (binding && bindingMap.has(binding) && path.node.start != null) {
193 mapRef(path.node.start, bindingMap.get(binding), path.node);
194 }
195 },
196 JSXIdentifier(path) {
197 getOrAssignNodeId(path.node);
198 },
199 AssignmentExpression(path) {
200 const left = path.get("left");
201 if (left.isLVal()) {
202 mapLValToBindings(left, bindingMap);
203 }
204 },
205 UpdateExpression(path) {
206 const argument = path.get("argument");
207 if (argument.isLVal()) {
208 mapLValToBindings(argument, bindingMap);
209 }
210 },
211 });
212
213 // Map identifiers in assignment targets (LVal positions) to their bindings.
214 function mapLValToBindings(lvalPath, bindingMap) {
215 const node = lvalPath.node;
216 if (!node) return;
217 switch (node.type) {
218 case "Identifier": {
219 const binding = lvalPath.scope.getBinding(node.name);
220 if (binding && bindingMap.has(binding) && node.start != null) {
221 mapRef(node.start, bindingMap.get(binding), node);
222 }
223 break;
224 }
225 case "ArrayPattern": {
226 for (const element of lvalPath.get("elements")) {
227 if (element.node) mapLValToBindings(element, bindingMap);
228 }
229 break;
230 }
231 case "ObjectPattern": {
232 for (const property of lvalPath.get("properties")) {
233 if (property.isObjectProperty()) {
234 mapLValToBindings(property.get("value"), bindingMap);
235 } else if (property.isRestElement()) {
236 mapLValToBindings(property, bindingMap);
237 }
238 }
239 break;
240 }
241 case "AssignmentPattern": {
242 mapLValToBindings(lvalPath.get("left"), bindingMap);
243 break;
244 }
245 case "RestElement": {
246 mapLValToBindings(lvalPath.get("argument"), bindingMap);
247 break;
248 }
249 default:
250 break;
251 }
252 }
253
254 // Record declaration identifiers in reference_to_binding and refNodeIdToBinding
255 for (const [binding, bid] of bindingMap) {
256 if (binding.identifier && binding.identifier.start != null) {
257 mapRef(binding.identifier.start, bid, binding.identifier);
258 }
259 }
260
261 const result = {
262 scopes,
263 bindings,
264 nodeToScope,
265 referenceToBinding,
266 programScope: 0,
267 };
268 // Only include new fields when non-empty, matching Rust skip_serializing_if
269 if (Object.keys(nodeToScopeEnd).length > 0) {
270 result.nodeToScopeEnd = nodeToScopeEnd;
271 }
272 if (Object.keys(refNodeIdToBinding).length > 0) {
273 result.refNodeIdToBinding = refNodeIdToBinding;
274 }
275 if (Object.keys(nodeIdToScope).length > 0) {
276 result.nodeIdToScope = nodeIdToScope;
277 }
278 return result;
279 }
280
281 function renameIdentifiers(ast, scopeInfo) {
282 traverse(ast, {
283 Identifier(path) {
284 const nodeId = path.node._nodeId;
285 if (nodeId != null && String(nodeId) in scopeInfo.refNodeIdToBinding) {
286 const bindingId = scopeInfo.refNodeIdToBinding[String(nodeId)];
287 const binding = scopeInfo.bindings[bindingId];
288 path.node.name = `${path.node.name}_${binding.scope}_${bindingId}`;
289 }
290 },
291 });
292 }
293
294 let parsed = 0;
295 let errors = 0;
296
297 for (const fixture of fixtures) {
298 const input = fs.readFileSync(path.join(FIXTURE_DIR, fixture), "utf8");
299 const isFlow = input.includes("@flow");
300
301 const plugins = isFlow ? ["flow", "jsx"] : ["typescript", "jsx"];
302 // Default to module unless there's an indicator it should be script
303 const sourceType = "module";
304
305 try {
306 const ast = parse(input, {
307 sourceFilename: fixture,
308 plugins,
309 sourceType,
310 allowReturnOutsideFunction: true,
311 errorRecovery: true,
312 });
313
314 // Collect scope info first — this assigns _nodeId to Identifier nodes
315 const scopeInfo = collectScopeInfo(ast);
316
317 // Serialize AST after scope collection so _nodeId fields are included
318 const outPath = path.join(OUTPUT_DIR, fixture + ".json");
319 fs.mkdirSync(path.dirname(outPath), { recursive: true });
320 fs.writeFileSync(outPath, JSON.stringify(ast, null, 2));
321
322 // Write scope info
323 const scopeOutPath = path.join(OUTPUT_DIR, fixture + ".scope.json");
324 fs.writeFileSync(scopeOutPath, JSON.stringify(scopeInfo, null, 2));
325
326 // Create renamed AST for scope resolution verification
327 renameIdentifiers(ast, scopeInfo);
328 const renamedOutPath = path.join(OUTPUT_DIR, fixture + ".renamed.json");
329 fs.writeFileSync(renamedOutPath, JSON.stringify(ast, null, 2));
330
331 parsed++;
332 } catch (e) {
333 // Parse errors are expected for some fixtures
334 const outPath = path.join(OUTPUT_DIR, fixture + ".parse-error");
335 fs.mkdirSync(path.dirname(outPath), { recursive: true });
336 fs.writeFileSync(outPath, e.message);
337 errors++;
338 }
339 }
340
341 console.log(
342 `Parsed ${parsed} fixtures, ${errors} parse errors, ${fixtures.length} total`
343 );