diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c84e69ee..1ffd50ee 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -8,7 +8,7 @@ - [Python](./python.md) - [Common Tools](./common-tools.md) - [Testing in General](./testing-in-general.md) - - [JavaScript]() + - [JavaScript](./javascript.md) - [Nodejs]() - [Rust](./rust.md) - [Contributing](./CONTRIBUTING.md) diff --git a/docs/javascript.md b/docs/javascript.md index 8743ba38..370c9aff 100644 --- a/docs/javascript.md +++ b/docs/javascript.md @@ -1,3 +1,226 @@ # JavaScript -WIP +## `Babeliser` + +Instantiate a new `Babeliser` with an optional [`options` object](https://babeljs.io/docs/babel-parser#options): + +```js +const babelisedCode = new __helpers.Babeliser(code, { + plugins: ["typescript"], +}); +``` + +### `getVariableDeclarations` + +```js +const programProvider = program.provider as AnchorProvider; +``` + +```javascript +const variableDeclaration = babelisedCode + .getVariableDeclarations() + .find((v) => { + return v.declarations?.[0]?.id?.name === "programProvider"; + }); +assert.exists( + variableDeclaration, + "A variable named `programProvider` should exist" +); +const tAsExpression = variableDeclaration.declarations?.[0]?.init; +const { object, property } = tAsExpression.expression; +assert.equal( + object.name, + "program", + "The `programProvider` variable should be assigned `program.provider`" +); +assert.equal( + property.name, + "provider", + "The `programProvider` variable should be assigned `program.provider`" +); +const tAnnotation = tAsExpression.typeAnnotation; +assert.equal( + tAnnotation.typeName.name, + "AnchorProvider", + "The `programProvider` variable should be assigned `program.provider as AnchorProvider`" +); +``` + +### `getFunctionDeclarations` + +```js +export function uploadFile() {} +``` + +```js +const functionDeclaration = babelisedCode + .getFunctionDeclarations() + .find((f) => { + return f.id.name === "uploadFile"; + }); +assert.exists( + functionDeclaration, + "A function named `uploadFile` should exist" +); + +const exports = babelisedCode.getType("ExportNamedDeclaration"); +const functionIsExported = exports.some((e) => { + return ( + e.declaration?.id?.name === "uploadFile" || + e.specifiers?.find((s) => s.exported.name === "uploadFile") + ); +}); +assert.isTrue( + functionIsExported, + "The `uploadFile` function should be exported" +); +``` + +### `generateCode` + +This method is useful when wanting to regenerate code from the AST. This can then be _re-babelised_, and compacted to compare with an expected code string. + +```js +it("example with generateCode", () => { + const [gamePublicKey, _] = PublicKey.findProgramAddressSync( + [Buffer.from("game"), payer.publicKey.toBuffer(), Buffer.from(gameId)], + program.programId + ); +}); +``` + +```js +// Limit scope to `it` CallExpression +const callExpression = babelisedCode.getType("CallExpression").find((c) => { + return c.callee?.name === "it"; +}); +const blockStatement = callExpression?.arguments?.[1]?.body; +// Take body AST, and generate a compacted string +const actualCodeString = babelisedCode.generateCode(blockStatement, { + compact: true, +}); +const expectedCodeString = `const[gamePublicKey,_]=PublicKey.findProgramAddressSync([Buffer.from('game'),payer.publicKey.toBuffer(),Buffer.from(gameId)],program.programId)`; +assert.deepInclude(actualCodeString, expectedCodeString); +``` + +### `getExpressionStatements` + +```js +export async function createAccount() { + transaction.add(tx); +} +``` + +```js +const expressionStatement = babelisedCode + .getExpressionStatements() + .find((e) => { + return ( + e.expression?.callee?.property?.name === "add" && + e.expression?.callee?.object?.name === "transaction" && + e.scope?.join() === "global,createAccount" + ); + }); +const callExpression = expressionStatement?.expression?.arguments?.[0]; +assert.equal( + callExpression?.name, + "tx", + "`tx` should be the first argument to `transaction.add`" +); +``` + +### `getExpressionStatement` + +```js +await main(); +``` + +```js +const mainExpressionStatement = babelisedCode.getExpressionStatement("main"); +assert.exists(mainExpressionStatement, "You should call `main`"); +assert.equal( + mainExpressionStatement?.expression?.type, + "AwaitExpression", + "You should call `main` with `await`" +); +``` + +### `getImportDeclarations` + +```js +import { PublicKey } from "@solana/web3.js"; +``` + +```js +const importDeclaration = babelisedCode.getImportDeclarations().find((i) => { + return i.source.value === "@solana/web3.js"; +}); +assert.exists(importDeclaration, "You should import from `@solana/web3.js`"); +const importSpecifiers = importDeclaration.specifiers.map( + (s) => s.imported.name +); +assert.include( + importSpecifiers, + "PublicKey", + "`PublicKey` should be imported from `@solana/web3.js`" +); +``` + +### `getType` + +```js +const tx = await program.methods.setupGame().rpc(); +``` + +```js +const memberExpression = babelisedCode.getType("MemberExpression").find((m) => { + return ( + m.object?.object?.name === "program" && + m.object?.property?.name === "methods" + ); +}); +assert.exists(memberExpression, "`program.methods.` should exist"); +const { property } = memberExpression; +assert.equal( + property.name, + "setupGame", + "`program.methods.setupGame` should exist" +); +``` + +### `getLineAndColumnFromIndex` + +```ts +const ACCOUNT_SIZE = borsh.serialize( + HelloWorldSchema, + new HelloWorldAccount() +).length; + +export async function createAccount() { + const lamports = await connection.getMinimumBalanceForRentExemption( + ACCOUNT_SIZE + ); +} +``` + +```js +const account = babelisedCode.getVariableDeclarations().find((v) => { + return v.declarations?.[0]?.id?.name === "ACCOUNT_SIZE"; +}); +const createAccount = babelisedCode.getFunctionDeclarations().find((f) => { + return f.id?.name === "createAccount"; +}); + +const { end } = account; +const { start } = createAccount; + +const { line: accountLine } = babelisedCode.getLineAndColumnFromIndex(end); +const { line: createAccountLine } = + babelisedCode.getLineAndColumnFromIndex(start); + +assert.isBelow( + accountLine, + createAccountLine, + `'ACCOUNT_SIZE' declared on line ${accountLine}, but should be declared before ${createAccountLine}` +); +``` diff --git a/lib/__tests__/babeliser.test.ts b/lib/__tests__/babeliser.test.ts new file mode 100644 index 00000000..80978dd6 --- /dev/null +++ b/lib/__tests__/babeliser.test.ts @@ -0,0 +1,469 @@ +/* eslint-disable capitalized-comments */ +/* eslint-disable max-nested-callbacks */ +import { + assertArrowFunctionExpression, + assertBinaryExpression, + assertCallExpression, + assertExpressionStatement, + assertFunctionDeclaration, + assertIdentifier, + assertIfStatement, + assertImportDeclaration, + assertImportDefaultSpecifier, + assertImportSpecifier, + assertMemberExpression, + assertNumericLiteral, + assertOptionalMemberExpression, + assertReturnStatement, + assertUpdateExpression, + assertVariableDeclaration, + assertVariableDeclarator, + IfStatement, + UpdateExpression, +} from "@babel/types"; +import { Babeliser } from "../index"; + +const jsString = ` +import z from "z"; +import { y } from "y"; + +const a = 1; +let b = 2; +var c = 3; +console.log(a + b + c); + +function add(param1, param2) { + const tot = param1 + param2; + let currentWeapon; + return tot; +} + +add(a, b); + +async function sub(param1, param2) { + (() => { + b++; + })(); +} + +await sub(1, 2); + +const complexType = { + a: 1, + b: [1, 2, '3'], + c: { + d: true, + }, + e: () => { + const inner = 24; + } +} + +if (complexType.c?.d) { + const q = complexType.e(); + throw new Error('error'); +} else if (complexType.a === a) { + // Do nothing +} else { + a++; +} + +while (a > 1) { + for (let i = 0; i < c; i++) { + switch (i) { + case 1: + break; + default: + break; + } + } +} +`; + +const t = new Babeliser(jsString); + +describe("Babeliser", () => { + test("should correctly parse the fixture code", () => { + expect(t.parsedCode.program.body.length).toEqual(13); + }); +}); + +describe(`getImportDeclarations`, () => { + test("should find all import declarations", () => { + expect(t.getImportDeclarations().length).toEqual(2); + }); + + test("z import declaration", () => { + const zImportDeclaration = t.getImportDeclarations().find((i) => { + return i.specifiers[0].local.name === "z"; + }); + assertImportDeclaration(zImportDeclaration); + const zImportDefaultSpecifier = zImportDeclaration.specifiers[0]; + assertImportDefaultSpecifier(zImportDefaultSpecifier); + const zSource = zImportDeclaration.source; + expect(zSource.value).toEqual("z"); + }); + + test("y import declaration", () => { + const yImportDeclaration = t.getImportDeclarations().find((i) => { + return i.specifiers[0].local.name === "y"; + }); + assertImportDeclaration(yImportDeclaration); + const yImportSpecifier = yImportDeclaration.specifiers[0]; + assertImportSpecifier(yImportSpecifier); + const yIdentifierLocal = yImportSpecifier.local; + assertIdentifier(yIdentifierLocal); + const yIdentifierImported = yImportSpecifier.imported; + assertIdentifier(yIdentifierImported); + expect(yIdentifierLocal.name).toEqual("y"); + expect(yIdentifierImported.name).toEqual("y"); + const ySource = yImportDeclaration.source; + expect(ySource.value).toEqual("y"); + }); +}); + +describe(`getVariableDeclarations`, () => { + test("should find all variable declarations", () => { + expect(t.getVariableDeclarations().length).toEqual(9); + expect( + t + .getVariableDeclarations() + .filter((v) => v.scope.join() === "global,complexType,e").length, + ).toEqual(1); + }); + + test("a variable declaration", () => { + const aVariableDeclaration = t.getVariableDeclarations().find((v) => { + const variableDeclarator = v.declarations[0]; + assertVariableDeclarator(variableDeclarator); + const identifier = variableDeclarator.id; + assertIdentifier(identifier); + return identifier.name === "a"; + }); + assertVariableDeclaration(aVariableDeclaration); + expect(aVariableDeclaration.kind).toEqual("const"); + expect(aVariableDeclaration.scope.join()).toEqual("global"); + const aNumericLiteral = aVariableDeclaration.declarations[0].init; + assertNumericLiteral(aNumericLiteral); + expect(aNumericLiteral.value).toEqual(1); + }); + + test("b variable declaration", () => { + const bVariableDeclaration = t.getVariableDeclarations().find((v) => { + const variableDeclarator = v.declarations[0]; + assertVariableDeclarator(variableDeclarator); + const identifier = variableDeclarator.id; + assertIdentifier(identifier); + return identifier.name === "b"; + }); + assertVariableDeclaration(bVariableDeclaration); + expect(bVariableDeclaration.kind).toEqual("let"); + expect(bVariableDeclaration.scope.join()).toEqual("global"); + const bNumericLiteral = bVariableDeclaration.declarations[0].init; + assertNumericLiteral(bNumericLiteral); + expect(bNumericLiteral.value).toEqual(2); + }); + + test("c variable declaration", () => { + const cVariableDeclaration = t.getVariableDeclarations().find((v) => { + const variableDeclarator = v.declarations[0]; + assertVariableDeclarator(variableDeclarator); + const identifier = variableDeclarator.id; + assertIdentifier(identifier); + return identifier.name === "c"; + }); + assertVariableDeclaration(cVariableDeclaration); + expect(cVariableDeclaration.kind).toEqual("var"); + expect(cVariableDeclaration.scope.join()).toEqual("global"); + const cNumericLiteral = cVariableDeclaration.declarations[0].init; + assertNumericLiteral(cNumericLiteral); + expect(cNumericLiteral.value).toEqual(3); + }); + + test("complexType variable declaration", () => { + const complexTypeVariableDeclaration = t + .getVariableDeclarations() + .find((v) => { + const variableDeclarator = v.declarations[0]; + assertVariableDeclarator(variableDeclarator); + const identifier = variableDeclarator.id; + assertIdentifier(identifier); + return identifier.name === "complexType"; + }); + assertVariableDeclaration(complexTypeVariableDeclaration); + expect(complexTypeVariableDeclaration.kind).toEqual("const"); + expect(complexTypeVariableDeclaration.scope.join()).toEqual("global"); + }); + test("inner variable declaration", () => { + const innerVariableDeclaration = t.getVariableDeclarations().find((v) => { + const variableDeclarator = v.declarations[0]; + assertVariableDeclarator(variableDeclarator); + const identifier = variableDeclarator.id; + assertIdentifier(identifier); + return identifier.name === "inner"; + }); + assertVariableDeclaration(innerVariableDeclaration); + expect(innerVariableDeclaration.kind).toEqual("const"); + expect(innerVariableDeclaration.scope.join()).toEqual( + "global,complexType,e", + ); + const innerNumericLiteral = innerVariableDeclaration.declarations[0].init; + assertNumericLiteral(innerNumericLiteral); + expect(innerNumericLiteral.value).toEqual(24); + }); +}); + +// EXPRESSION STATEMENTS + +describe(`getExpressionStatements`, () => { + test("should find all expression statements", () => { + expect(t.getExpressionStatements().length).toEqual(6); + }); + + test("console expression statement", () => { + const consoleExpression = t.getExpressionStatements().find((e) => { + const callExpression = e.expression; + assertCallExpression(callExpression); + const memberExpression = callExpression.callee; + assertMemberExpression(memberExpression); + const object = memberExpression.object; + assertIdentifier(object); + const property = memberExpression.property; + assertIdentifier(property); + return object.name === "console" && property.name === "log"; + }); + + const consoleCallExpression = consoleExpression?.expression; + assertCallExpression(consoleCallExpression); + const binaryExpression = consoleCallExpression.arguments[0]; + assertBinaryExpression(binaryExpression); + const binaryExpressionLeft = binaryExpression.left; + assertBinaryExpression(binaryExpressionLeft); + const binaryExpressionLeftLeft = binaryExpressionLeft.left; + assertIdentifier(binaryExpressionLeftLeft); + const binaryExpressionLeftRight = binaryExpressionLeft.right; + assertIdentifier(binaryExpressionLeftRight); + const binaryExpressionRight = binaryExpression.right; + assertIdentifier(binaryExpressionRight); + expect(binaryExpressionLeftLeft.name).toEqual("a"); + expect(binaryExpressionLeftRight.name).toEqual("b"); + expect(binaryExpressionRight.name).toEqual("c"); + }); + + test("add expression statement", () => { + const addExpression = t.getExpressionStatements().find((e) => { + const callExpression = e.expression; + assertCallExpression(callExpression); + const calleeIdentifier = callExpression.callee; + if (calleeIdentifier.type === "Identifier") { + assertIdentifier(calleeIdentifier); + return calleeIdentifier.name === "add"; + } + + return false; + }); + const addCallExpression = addExpression?.expression; + assertCallExpression(addCallExpression); + const addCalleeIdentifier = addCallExpression.callee; + assertIdentifier(addCalleeIdentifier); + expect(addCalleeIdentifier.name).toEqual("add"); + const addArguments = addCallExpression.arguments; + const addArgOneIdentifier = addArguments[0]; + assertIdentifier(addArgOneIdentifier); + expect(addArgOneIdentifier.name).toEqual("a"); + const addArgTwoIdentifier = addArguments[1]; + assertIdentifier(addArgTwoIdentifier); + expect(addArgTwoIdentifier.name).toEqual("b"); + }); +}); + +describe(`getFunctionDeclarations`, () => { + test("should find all function declarations", () => { + expect(t.getFunctionDeclarations().length).toEqual(2); + }); + + test("add function declaration", () => { + const addFunction = t.getFunctionDeclarations().find((f) => { + return f.id?.name === "add"; + }); + assertFunctionDeclaration(addFunction); + expect(addFunction).toBeTruthy(); + const addFunctionParams = addFunction.params; + expect(addFunctionParams.length).toEqual(2); + const addFunctionParamOne = addFunctionParams[0]; + assertIdentifier(addFunctionParamOne); + expect(addFunctionParamOne.name).toEqual("param1"); + const addFunctionParamTwo = addFunctionParams[1]; + assertIdentifier(addFunctionParamTwo); + expect(addFunctionParamTwo.name).toEqual("param2"); + + const totVariable = addFunction.body.body[0]; + assertVariableDeclaration(totVariable); + const totVariableDeclarator = totVariable.declarations[0]; + assertVariableDeclarator(totVariableDeclarator); + const totIdentifier = totVariableDeclarator.id; + assertIdentifier(totIdentifier); + expect(totIdentifier.name).toEqual("tot"); + const totBinaryExpression = totVariableDeclarator.init; + assertBinaryExpression(totBinaryExpression); + const totBinaryLeftIdentifier = totBinaryExpression.left; + assertIdentifier(totBinaryLeftIdentifier); + expect(totBinaryLeftIdentifier.name).toEqual("param1"); + const totBinaryRightIdentifier = totBinaryExpression.right; + assertIdentifier(totBinaryRightIdentifier); + expect(totBinaryRightIdentifier.name).toEqual("param2"); + + const returnStatement = addFunction.body.body.find((b) => { + return b.type === "ReturnStatement"; + }); + assertReturnStatement(returnStatement); + const returnStatementArgument = returnStatement.argument; + assertIdentifier(returnStatementArgument); + expect(returnStatementArgument.name).toEqual("tot"); + }); + + test("sub function declaration", () => { + const subFunctionDeclaration = t.getFunctionDeclarations().find((f) => { + return f.id?.name === "sub"; + }); + assertFunctionDeclaration(subFunctionDeclaration); + expect(subFunctionDeclaration.async).toEqual(true); + }); +}); +// ARROW FUNCTION EXPRESSIONS + +describe(`getArrowFunctionExpressions`, () => { + test("should find all arrow function expressions", () => { + expect(t.getArrowFunctionExpressions().length).toEqual(2); + }); + + test("IIFE arrow function expression", () => { + const iIFEArrowFunctionExpression = t + .getArrowFunctionExpressions() + .find((a) => { + return a.scope.join() === "global,sub"; + }); + assertArrowFunctionExpression(iIFEArrowFunctionExpression); + }); +}); + +describe(`getType`, () => { + test("b update expression", () => { + const bUpdateExpression = t + .getType("UpdateExpression") + .find((u) => { + const updateExpressionArgument = u.argument; + assertIdentifier(updateExpressionArgument); + return updateExpressionArgument.name === "b"; + }); + assertUpdateExpression(bUpdateExpression); + expect(bUpdateExpression.scope.join()).toEqual("global,sub"); + expect(bUpdateExpression.operator).toEqual("++"); + }); + + describe("if statement", () => { + const ifStatement = t.getType("IfStatement")[0]; + test("exists", () => { + assertIfStatement(ifStatement); + expect(ifStatement).toBeTruthy(); + }); + describe(`.test`, () => { + const optionalMemberExpression = ifStatement.test; + assertOptionalMemberExpression(optionalMemberExpression); + test("is optional", () => { + expect(optionalMemberExpression.optional).toBeTruthy(); + }); + describe(`.object`, () => { + const memberExpression = optionalMemberExpression.object; + assertMemberExpression(memberExpression); + + describe(`.object`, () => { + const objectIdentifier = memberExpression.object; + assertIdentifier(objectIdentifier); + test("complexType", () => { + expect(objectIdentifier.name).toEqual("complexType"); + }); + }); + + describe(`.property`, () => { + const propertyIdentifier = memberExpression.property; + assertIdentifier(propertyIdentifier); + test("c", () => { + expect(propertyIdentifier.name).toEqual("c"); + }); + }); + }); + describe(`.property`, () => { + const propertyIdentifier = optionalMemberExpression.property; + test("exists", () => { + assertIdentifier(propertyIdentifier); + expect(propertyIdentifier).toBeTruthy(); + }); + // const ifBlockStatement = ifStatement.consequent; + // const ifAlternate = ifStatement.alternate; + }); + }); + // describe(`.consequent`, () => {}); + // describe(`.alternate`, () => {}); + }); +}); + +describe(`getExpressionStatement`, () => { + describe(`console.log`, () => { + const consoleExpressionStatement = t.getExpressionStatement("console.log"); + test("exists", () => { + expect(consoleExpressionStatement).toBeTruthy(); + }); + }); + describe(`add`, () => { + const addExpressionStatement = t.getExpressionStatement("add"); + test("exists", () => { + expect(addExpressionStatement).toBeTruthy(); + }); + }); + describe(`sub`, () => { + const subExpressionStatement = t.getExpressionStatement("sub"); + test("exists", () => { + expect(subExpressionStatement).toBeTruthy(); + }); + }); +}); + +describe(`generateCode`, () => { + test("should generate code", () => { + const addExpressionStatement = t.getExpressionStatement("add"); + assertExpressionStatement(addExpressionStatement); + const code = t.generateCode(addExpressionStatement); + expect(code).toEqual("add(a, b);"); + }); +}); + +describe(`.scope`, () => { + test("should return the scope", () => { + const addExpressionStatement = t.getExpressionStatement("add"); + assertExpressionStatement(addExpressionStatement); + expect(addExpressionStatement.scope.join()).toEqual("global"); + }); +}); + +describe(`getLineAndColumnFromIndex`, () => { + test("should return the line and column", () => { + const aVariableDeclaration = t.getVariableDeclarations().find((v) => { + const id = v.declarations?.[0]?.id; + assertIdentifier(id); + return id.name === "a"; + }); + assertVariableDeclaration(aVariableDeclaration); + const { start } = aVariableDeclaration; + assertNumber(start); + const lineAndColumn = t.getLineAndColumnFromIndex(start); + assertNumber(lineAndColumn.line); + assertNumber(lineAndColumn.column); + expect(lineAndColumn.line).toEqual(5); + expect(lineAndColumn.column).toEqual(0); + }); +}); + +function assertNumber(n: unknown): asserts n is number { + expect(n).toEqual(expect.any(Number)); +} diff --git a/lib/class/babeliser.ts b/lib/class/babeliser.ts new file mode 100644 index 00000000..ee84fc6b --- /dev/null +++ b/lib/class/babeliser.ts @@ -0,0 +1,197 @@ +import { parse, ParserOptions } from "@babel/parser"; +import generate, { GeneratorOptions } from "@babel/generator"; +import { + ArrowFunctionExpression, + ExpressionStatement, + FunctionDeclaration, + Identifier, + ImportDeclaration, + is, + Node, + VariableDeclaration, + Statement, +} from "@babel/types"; + +type BabeliserOptions = { maxScopeDepth: number }; +type Scope = Array; +type ScopedStatement = Statement & { scope: Scope }; + +export class Babeliser { + public parsedCode: ReturnType; + private maxScopeDepth = 4; + public codeString: string; + constructor( + codeString: string, + options?: Partial, + ) { + this.parsedCode = parse(codeString, { + sourceType: "module", + ...options, + }); + if (options?.maxScopeDepth) { + this.maxScopeDepth = options.maxScopeDepth; + } + + this.codeString = codeString; + } + + public getArrowFunctionExpressions() { + const arrowFunctionDeclarations = + this._recurseBodiesForType( + "ArrowFunctionExpression", + ); + return arrowFunctionDeclarations; + } + + public getExpressionStatements() { + const expressionStatements = + this._recurseBodiesForType("ExpressionStatement"); + return expressionStatements; + } + + public getFunctionDeclarations() { + const functionDeclarations = + this._recurseBodiesForType("FunctionDeclaration"); + return functionDeclarations; + } + + public getImportDeclarations() { + const expressionStatements = + this._recurseBodiesForType("ImportDeclaration"); + return expressionStatements; + } + + public getType(type: string) { + return this._recurseBodiesForType(type); + } + + public getVariableDeclarations() { + const variableDeclarations = + this._recurseBodiesForType("VariableDeclaration"); + return variableDeclarations; + } + + public getExpressionStatement( + name: string, + scope: Scope = ["global"], + ): (ExpressionStatement & { scope: Scope }) | undefined { + const expressionStatements = this.getExpressionStatements().filter((a) => + this._isInScope(a.scope, scope), + ); + const expressionStatement = expressionStatements.find((e) => { + const expression = e.expression; + if (is("CallExpression", expression)) { + if (name.includes(".")) { + const [objectName, methodName] = name.split("."); + const memberExpression = expression.callee; + if (is("MemberExpression", memberExpression)) { + const object = memberExpression.object; + const property = memberExpression.property; + if (is("Identifier", object) && is("Identifier", property)) { + return object.name === objectName && property.name === methodName; + } + } + } + + const identifier = expression.callee; + if (is("Identifier", identifier) && identifier.name === name) { + return true; + } + } + + if (is("AwaitExpression", expression)) { + const callExpression = expression.argument; + if (is("CallExpression", callExpression)) { + const identifier = callExpression.callee; + if (is("Identifier", identifier)) { + return identifier.name === name; + } + } + } + + return false; + }); + return expressionStatement; + } + + public generateCode(ast: Node, options?: GeneratorOptions) { + return generate(ast, options).code; + } + + public getLineAndColumnFromIndex(index: number) { + const linesBeforeIndex = this.codeString.slice(0, index).split("\n"); + const line = linesBeforeIndex.length; + const column = linesBeforeIndex.pop()?.length; + return { line, column }; + } + + private _isInScope(scope: Scope, targetScope: Scope = ["global"]): boolean { + if (targetScope.length === 1 && targetScope[0] === "global") { + return true; + } + + if (scope.length < targetScope.length) { + return false; + } + + const scopeString = scope.join("."); + const targetScopeString = targetScope.join("."); + return scopeString.includes(targetScopeString); + } + + private _recurseBodiesForType(type: string): Array { + const body = this.parsedCode.program.body; + const types = []; + for (const statement of body) { + const a = this._recurse(statement, (a) => a?.type === type, ["global"]); + if (a?.length) { + types.push(...a); + } + } + + // @ts-expect-error There is no easy wat to type this without writing out constraints to the 40+ types + return types; + } + + private _recurse( + // This is kind of a hack, since we're mutating val. It needs to be able to + // have a scope parameter, though it's never passed in with one. + val: Statement & { scope?: Scope }, + isTargetType: (arg: { type: string }) => boolean, + scope: Array, + ): ScopedStatement[] { + const matches: ScopedStatement[] = []; + if (scope.length >= this.maxScopeDepth) { + return matches; + } + + if (val && typeof val === "object") { + if (!Array.isArray(val)) { + val.scope = scope; + } + + if (isTargetType(val)) { + // @ts-expect-error See `val` parameter + matches.push(val); + } + + const currentScope = [...scope]; + const nearestIdentifier: undefined | Identifier = Object.values(val).find( + (v) => v?.type === "Identifier", + ); + if (nearestIdentifier) { + currentScope.push(nearestIdentifier.name); + } + + for (const v of Object.values(val)) { + const mat = this._recurse(v, isTargetType, currentScope); + const toPush = mat?.filter(Boolean).flat(); + if (toPush?.length) { + matches.push(...toPush.flat()); + } + } + } + + return matches; + } +} diff --git a/lib/index.ts b/lib/index.ts index 95f96b40..db88b891 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,5 +1,6 @@ import { strip } from "./strip"; import astHelpers from "../python/py_helpers.py"; +export { Babeliser } from "./class/babeliser"; declare global { // eslint-disable-next-line no-var diff --git a/package.json b/package.json index 4380716b..6c9b3a53 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "7.26.0", "@types/jest": "29.5.14", + "@babel/types": "7.26.10", + "@types/babel__generator": "7.6.8", "@typescript-eslint/eslint-plugin": "^6.13.1", "@typescript-eslint/parser": "^6.13.1", "babel-jest": "^29.7.0", @@ -81,6 +83,8 @@ "repository": "git@github.com:freeCodeCamp/curriculum-helpers.git", "license": "BSD-3-Clause", "dependencies": { + "@babel/generator": "7.x", + "@babel/parser": "7.x", "browserify": "^17.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6aea8fe..48013c11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@babel/generator': + specifier: 7.x + version: 7.26.9 + '@babel/parser': + specifier: 7.x + version: 7.26.9 browserify: specifier: ^17.0.0 version: 17.0.1 @@ -24,6 +30,12 @@ importers: '@babel/preset-typescript': specifier: 7.26.0 version: 7.26.0(@babel/core@7.26.10) + '@babel/types': + specifier: 7.26.10 + version: 7.26.10 + '@types/babel__generator': + specifier: 7.6.8 + version: 7.6.8 '@types/jest': specifier: 29.5.14 version: 29.5.14 @@ -716,10 +728,6 @@ packages: resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.26.9': - resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} - engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -3515,14 +3523,14 @@ snapshots: '@babel/generator@7.26.9': dependencies: '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.25.9': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@babel/helper-compilation-targets@7.26.5': dependencies: @@ -3566,7 +3574,7 @@ snapshots: '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 transitivePeerDependencies: - supports-color @@ -3588,7 +3596,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@babel/helper-plugin-utils@7.26.5': {} @@ -3613,7 +3621,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.25.9': dependencies: '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 transitivePeerDependencies: - supports-color @@ -3627,7 +3635,7 @@ snapshots: dependencies: '@babel/template': 7.26.9 '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 transitivePeerDependencies: - supports-color @@ -3642,7 +3650,7 @@ snapshots: '@babel/parser@7.26.9': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.10)': dependencies: @@ -4049,7 +4057,7 @@ snapshots: '@babel/helper-module-imports': 7.25.9 '@babel/helper-plugin-utils': 7.26.5 '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 transitivePeerDependencies: - supports-color @@ -4217,7 +4225,7 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 esutils: 2.0.3 '@babel/preset-react@7.26.3(@babel/core@7.26.10)': @@ -4271,7 +4279,7 @@ snapshots: '@babel/generator': 7.26.9 '@babel/parser': 7.26.9 '@babel/template': 7.26.9 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: @@ -4282,11 +4290,6 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@babel/types@7.26.9': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@bcoe/v8-coverage@0.2.3': {} '@discoveryjs/json-ext@0.5.7': {} @@ -4555,23 +4558,23 @@ snapshots: '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@types/eslint-scope@3.7.7': dependencies: @@ -4968,7 +4971,7 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: '@babel/template': 7.26.9 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 @@ -6370,7 +6373,7 @@ snapshots: '@babel/generator': 7.26.9 '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.10) - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 diff --git a/tsconfig.json b/tsconfig.json index 14b7746d..46ce2fb8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "declaration": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "strict": true + "strict": true, + "moduleResolution": "Node10" } }