Skip to content

Commit d6d1a3c

Browse files
feat: basic js ast explorer (#155)
* stash in commit, because i forget about stashes * feat: basic js ast explorer * add variable_declarator condition to getCalls * refactor: format * fix(deps): add missing dependencies --------- Co-authored-by: Oliver Eyton-Williams <[email protected]>
1 parent d177e70 commit d6d1a3c

File tree

5 files changed

+301
-32
lines changed

5 files changed

+301
-32
lines changed

lib/__tests__/tower.test.ts

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Tower, generate } from "../index";
2+
3+
const code = `
4+
import { readFile } from "fs/promises";
5+
6+
async function main() {
7+
doSyncStuff();
8+
9+
const a = await doAsyncStuff(1);
10+
console.log(a);
11+
}
12+
13+
// Test
14+
async function doAsyncStuff(num) {
15+
// Another test
16+
const file = await readFile("file_loc" + num, "utf-8");
17+
return file;
18+
}
19+
20+
const a = [];
21+
22+
const b = {
23+
c: () => {
24+
const c = 1;
25+
},
26+
"example-a": {
27+
a: 1
28+
}
29+
}
30+
31+
const { c, d, e } = b;
32+
33+
const [f, g] = [c, d];
34+
35+
let h, i;
36+
37+
a.map(function (e) {});
38+
39+
function doSyncStuff() {
40+
console.log("stuff");
41+
}
42+
43+
main();
44+
`;
45+
46+
const t = new Tower(code);
47+
48+
describe("tower", () => {
49+
describe("getFunction", () => {
50+
it("works", () => {
51+
const main = t.getFunction("main").generate;
52+
expect(main).toEqual(
53+
`async function main() {
54+
doSyncStuff();
55+
const a = await doAsyncStuff(1);
56+
console.log(a);
57+
}
58+
59+
// Test`,
60+
);
61+
});
62+
});
63+
describe("getVariable", () => {
64+
it("works", () => {
65+
const a = t.getFunction("main").getVariable("a").generate;
66+
expect(a).toEqual("const a = await doAsyncStuff(1);");
67+
const b = t.getVariable("b").generate;
68+
expect(b).toEqual(`const b = {
69+
c: () => {
70+
const c = 1;
71+
},
72+
"example-a": {
73+
a: 1
74+
}
75+
};`);
76+
const file = t.getFunction("doAsyncStuff").getVariable("file").generate;
77+
expect(file).toEqual(
78+
'// Another test\nconst file = await readFile("file_loc" + num, "utf-8");',
79+
);
80+
});
81+
});
82+
describe("getCalls", () => {
83+
it("works", () => {
84+
const aMap = t.getCalls("a.map");
85+
expect(aMap).toHaveLength(1);
86+
const map = aMap.at(0);
87+
// @ts-expect-error - expression does exist.
88+
const argumes = map?.ast.expression?.arguments;
89+
expect(generate(argumes.at(0), { compact: true }).code).toEqual(
90+
"function(e){}",
91+
);
92+
});
93+
});
94+
});

lib/class/tower.ts

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { parse, ParserOptions } from "@babel/parser";
2+
import generate from "@babel/generator";
3+
import {
4+
ExpressionStatement,
5+
FunctionDeclaration,
6+
is,
7+
Node,
8+
VariableDeclaration,
9+
} from "@babel/types";
10+
11+
export { generate };
12+
13+
export class Tower<T extends Node> {
14+
public ast: Node;
15+
constructor(stringOrAST: string | T, options?: Partial<ParserOptions>) {
16+
if (typeof stringOrAST === "string") {
17+
const parsedThing = parse(stringOrAST, {
18+
sourceType: "module",
19+
...options,
20+
});
21+
this.ast = parsedThing.program;
22+
} else {
23+
this.ast = stringOrAST;
24+
}
25+
}
26+
27+
// Get all the given types at the current scope
28+
private getType<T extends Node>(type: string, name: string): Tower<T> {
29+
const body = this.extractBody(this.ast);
30+
const ast = body.find((node) => {
31+
if (node.type === type) {
32+
if (is("FunctionDeclaration", node)) {
33+
return node.id?.name === name;
34+
}
35+
36+
if (is("VariableDeclaration", node)) {
37+
const variableDeclarator = node.declarations[0];
38+
if (!is("VariableDeclarator", variableDeclarator)) {
39+
return false;
40+
}
41+
42+
const identifier = variableDeclarator.id;
43+
if (!is("Identifier", identifier)) {
44+
return false;
45+
}
46+
47+
return identifier.name === name;
48+
}
49+
}
50+
51+
return false;
52+
});
53+
if (!ast) {
54+
throw new Error(`No AST found with name ${name}`);
55+
}
56+
57+
assertIsType<T>(ast);
58+
return new Tower<T>(ast);
59+
}
60+
61+
public getFunction(name: string): Tower<FunctionDeclaration> {
62+
return this.getType("FunctionDeclaration", name);
63+
}
64+
65+
public getVariable(name: string): Tower<VariableDeclaration> {
66+
return this.getType("VariableDeclaration", name);
67+
}
68+
69+
public getCalls(callSite: string): Array<Tower<ExpressionStatement>> {
70+
const body = this.extractBody(this.ast);
71+
const calls = body.filter((node) => {
72+
if (is("ExpressionStatement", node)) {
73+
const expression = node.expression;
74+
if (is("CallExpression", expression)) {
75+
const callee = expression.callee;
76+
77+
switch (callee.type) {
78+
case "Identifier":
79+
return callee.name === callSite;
80+
case "MemberExpression":
81+
return generate(callee).code === callSite;
82+
default:
83+
return true;
84+
}
85+
}
86+
}
87+
88+
if (is("VariableDeclarator", node)) {
89+
const init = node.init;
90+
if (is("CallExpression", init)) {
91+
const callee = init.callee;
92+
93+
switch (callee.type) {
94+
case "Identifier":
95+
return callee.name === callSite;
96+
case "MemberExpression":
97+
return generate(callee).code === callSite;
98+
default:
99+
return true;
100+
}
101+
}
102+
}
103+
104+
return false;
105+
});
106+
assertIsType<ExpressionStatement[]>(calls);
107+
return calls.map((call) => new Tower<ExpressionStatement>(call));
108+
}
109+
110+
private extractBody(ast: Node): Node[] {
111+
switch (ast.type) {
112+
case "Program":
113+
return ast.body;
114+
case "FunctionDeclaration":
115+
return ast.body.body;
116+
case "VariableDeclaration":
117+
return ast.declarations;
118+
case "ArrowFunctionExpression":
119+
// eslint-disable-next-line no-case-declarations
120+
const blockStatement = ast.body;
121+
if (is("BlockStatement", blockStatement)) {
122+
return blockStatement.body;
123+
}
124+
125+
throw new Error(`Unimplemented for ${ast.type}`);
126+
default:
127+
throw new Error(`Unimplemented for ${ast.type}`);
128+
}
129+
}
130+
131+
public get generate(): string {
132+
return generate(this.ast).code;
133+
}
134+
135+
public get compact(): string {
136+
return generate(this.ast, { compact: true }).code;
137+
}
138+
}
139+
140+
function assertIsType<T extends Node | Node[]>(
141+
ast: Node | Node[],
142+
): asserts ast is T {}

lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { strip } from "./strip";
22
import astHelpers from "../python/py_helpers.py";
3+
export { Tower, generate } from "./class/tower";
34
export { Babeliser } from "./class/babeliser";
45

56
declare global {

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,17 @@
7676
"build": "NODE_ENV=production webpack",
7777
"clean": "rm -rf dist",
7878
"lint": "eslint lib --max-warnings 0 && prettier lib --check && tsc --noEmit",
79+
"format": "prettier --write lib",
7980
"test": "jest",
8081
"prepublishOnly": "pnpm clean && pnpm build",
8182
"webpack": "webpack"
8283
},
8384
"repository": "[email protected]:freeCodeCamp/curriculum-helpers.git",
8485
"license": "BSD-3-Clause",
8586
"dependencies": {
86-
"@babel/generator": "7.x",
87-
"@babel/parser": "7.x",
87+
"@babel/generator": "7.26.9",
88+
"@babel/parser": "7.26.9",
89+
"@babel/types": "7.26.9",
8890
"browserify": "^17.0.0"
8991
}
9092
}

0 commit comments

Comments
 (0)