Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-env"],
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
"plugins": []
}
2 changes: 1 addition & 1 deletion .babelrc-test
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["./src/babel-plugin-instrumentation"]
"plugins": ["./lib/plugin"]
}
19 changes: 19 additions & 0 deletions lib/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _helperPluginUtils = require("@babel/helper-plugin-utils");
var _default = exports["default"] = (0, _helperPluginUtils.declare)(function (api) {
api.assertVersion(7);
console.log(api.parse);
return {
visitor: {
FunctionExpression: function FunctionExpression(path) {
var _path$node$id;
console.log((_path$node$id = path.node.id) === null || _path$node$id === void 0 ? void 0 : _path$node$id.name);
}
}
};
});
1 change: 1 addition & 0 deletions lib/visitors/Functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"use strict";
5 changes: 5 additions & 0 deletions lib/visitors/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
380 changes: 191 additions & 189 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"description": "",
"main": "index.js",
"scripts": {
"build:lib": "babel src -d lib -x \".ts\"",
"build:types": "tsc --emitDeclarationOnly",
"check-types": "tsc --noEmit",
"run": "npx babel add.js --out-file out/add.js --quiet --config-file ./.babelrc-test",
"build": "npx babel add.js --out-file out/add.js --quiet --config-file ./.babelrc-test",
"debug": "export PLUGIN_DEBUG=1 && npx --node-options=--inspect-brk babel add.js --out-file out/add.js --quiet --config-file ./.babelrc-test",
"start": "npm run build && node ./out/add.js --config-file ./.babelrc-test",
Expand All @@ -17,9 +21,11 @@
"@babel/preset-env": "^7.24.5"
},
"devDependencies": {
"@babel/types": "^7.24.5",
"@babel/preset-typescript": "^7.24.7",
"@babel/types": "^7.24.7",
"@types/node": "^20.12.12",
"jest": "^29.7.0"
"jest": "^29.7.0",
"typescript": "^5.5.3"
},
"jest": {
"transformIgnorePatterns": [
Expand Down
25 changes: 25 additions & 0 deletions src/instrumentation/AssignmentStatement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { VariableDeclaration, AssignmentExpression, UpdateExpression } from "@babel/types";
import { Statement } from "./Statement";

type AssignmentNodeType = VariableDeclaration | AssignmentExpression | UpdateExpression;

export class AssignmentStatement extends Statement {
public name: string;
public value: any;
public displayValue: any;

constructor(nodeType: AssignmentNodeType["type"], name: string, lineNumber: number, displayValue: any, value: any) {
super(nodeType, lineNumber);

this.name = name;
this.value = value;
this.displayValue = displayValue;
}

log() {
console.debug("\x1b[33m%s\x1b[0m", this.lineNumber + ": Assignment " + name + " = " + JSON.stringify(this.displayValue));
}
}



43 changes: 43 additions & 0 deletions src/instrumentation/FunctionCallStatement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { FunctionExpression, FunctionDeclaration, ArrowFunctionExpression, ObjectMethod, ClassMethod } from "@babel/types";
import { Statement } from "./Statement";

type FunctionNodeType = FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | ObjectMethod | ClassMethod;

export class FunctionCallStatement extends Statement {
public branch: Statement;
public name: string;
public args: Record<string, any>;

/**
* Construct a new FunctionCall Statement
* @param nodeType The type of the AST Node
* @param name The name of the function called
* @param lineNumber The line number in the code that this statement corresponds to
* @param args The arguments passed to the function
*/
constructor(nodeType: FunctionNodeType["type"], name: string, lineNumber: number, args: Record<string, any>) {
super(nodeType, lineNumber);

this.name = name;
this.args = args;
}

/**
* Add a new statement to the chain
* @param {Statement} statement The statement to add
* @return {Statement} The newly added statement
*/
add(statement: Statement): Statement {
this.branch = statement;

// When a function is called we go deeper in the stack
statement.depth = this.depth + 1;
statement.prev = this;

return statement;
}

log() {
console.debug("\x1b[32m%s\x1b[0m", this.lineNumber + ": Function Call " + this.name + "(" + JSON.stringify(this.args) + ")");
}
}
12 changes: 12 additions & 0 deletions src/instrumentation/ProgramStatement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Statement } from "./Statement";

/**
* Represents the entry point into the program
*/
export class ProgramStatement extends Statement {
constructor() {
super("Program", 0);
}

log() {}
}
52 changes: 52 additions & 0 deletions src/instrumentation/ReturnValueStatement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ReturnStatement } from "@babel/types";
import { Statement } from "./Statement";

export class ReturnValueStatement extends Statement {
public value: any;

/**
* Construct a new FunctionCall Statement
* @param nodeType The type of the AST Node
* @param lineNumber The line number in the code that this statement corresponds to
* @param value The value being returned
*/
constructor(nodeType: ReturnStatement["type"], lineNumber: number, value: any) {
super(nodeType, lineNumber);
this.value = value;
}

/**
* Add a new statement to the chain
* @param {Statement} statement The statement to add
* @return {Statement} The newly added statement
*/
add(statement: Statement): Statement {
this.next = statement;

// When we return from a function we go back up the stack
statement.depth = this.depth - 1;
statement.branch_rewind = this;

// Set the previous statement, to the last at the same depth
statement.prev = this.findPrevious();

return statement;
}

/**
* Find the previous Node that was at a higher depth
*/
findPrevious() {
let searchItem = this.prev;

while (searchItem?.depth !== this.depth - 1) {
searchItem = searchItem?.prev;
}

return searchItem;
}

log() {
console.log("\x1b[34m%s\x1b[0m", this.lineNumber + ": Returning " + this.value);
}
}
47 changes: 47 additions & 0 deletions src/instrumentation/Statement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Node } from "@babel/types";
import { stat } from "fs";

export abstract class Statement {
// Next statement in the chain
public next?: Statement;

// Previous statement in the chain
public prev?: Statement;

// The call stack depth of the current statement
public depth: number = 0;

// This is used to allow stepping backwards into a function call from a return statement
public branch_rewind?: Statement;

// The type of this AST Node
protected nodeType: Node["type"] | "Program";

protected lineNumber: number;

/**
* Construct a new Node
* @param nodeType The type of the AST Node
* @param lineNumber The line number in the code that this statement corresponds to
*/
constructor(nodeType: Node["type"] | "Program", lineNumber: number) {
this.nodeType = nodeType;
}

/**
* Add a new statement to the chain
* @param {Statement} statement The statement to add
* @return {Statement} The newly added statement
*/
add(statement: Statement): Statement {
this.next = statement;

// Update the next node to point back to this one
statement.prev = this;
statement.depth = this.depth;

return statement;
}

abstract log(): void;
}
16 changes: 16 additions & 0 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PluginObj } from '@babel/core';
import { declare } from '@babel/helper-plugin-utils';

export default declare(
(api: any): PluginObj => {
api.assertVersion(7);
console.log(api.parse);

return {
visitor: {
FunctionExpression(path) {
console.log(path.node.id?.name);
}
}
}
});
85 changes: 85 additions & 0 deletions src/visitors/Functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { NodePath } from "@babel/core";
import type { FunctionExpression, FunctionDeclaration, ArrowFunctionExpression, ObjectMethod, ClassMethod } from "@babel/types";

import * as utils from "./utils";

type PathType = NodePath<FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | ObjectMethod | ClassMethod>;

/**
* Should we skip this particular AST node?
*/
function skip(name, path) {
// Our internal functions
if (name && name.startsWith("___")) return true;

// Code that we don't have any line numbers for
if (path.node.loc == undefined || path.node.loc.start === undefined || path.node.loc.start.line === undefined) {
return true;
}

return false;
}

function getName(path: PathType) {
const { node, parent } = path;

const name = utils.getName(path);
if (name) {
return name;
}

// Attempt to handle arrow functions inside a class
// class scientificCalculator {
// cos = (degrees) => Math.cos(degress * (Math.PI / 180))
// }
if (["ArrowFunctionExpression", "FunctionExpression"].includes(node.type)) {
// @ts-expect-error
const isInDefineProperty = parent.type === "CallExpression" && parent.callee?.name === "_defineProperty";
// @ts-expect-error
const isInCaptureAssignment = parent.type === "CallExpression" && parent.callee?.name === "___captureAssignment";

// Force this function to be skipped as we end up with duplicate instrumentation calls
// so we'll just use the ArrowFunctionExpression instead
if (node.type === "FunctionExpression" && (isInDefineProperty)) {
return "___";
}

if (node.type === "ArrowFunctionExpression" && (isInDefineProperty || isInCaptureAssignment)) {
// @ts-expect-error
return parent.arguments?.[1]?.value;
}
}
}


export function visit(t, path: PathType, ASTType: string) {
const name = getName(path);
if (skip(name, path)) { return; }

utils.isDebug && console.debug("functions.inject");

const lineNumber = utils.getLineNumber(path);
const parameters = path.node.params.map(p =>
t.objectProperty(t.identifier(p.name), t.identifier(p.name))
);

const captureStart = t.expressionStatement(
t.callExpression(t.identifier("___instrumentFunction"), [
t.stringLiteral(ASTType),
t.stringLiteral(name || "anonymous"), // Use extracted name or default to "anonymous"
t.numericLiteral(lineNumber),
t.objectExpression(parameters)
])
);

if (t.isBlockStatement(path.node.body)) {
path.get("body").unshiftContainer("body", captureStart);
} else {
const body = t.blockStatement([captureStart, t.returnStatement(path.node.body)]);
path.get('body').replaceWith(body);
}

// @ts-expect-error: This is a field that we're adding in
// Mark this node as processed to avoid re-processing
path.node.__processed = true;
}
3 changes: 3 additions & 0 deletions src/visitors/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IVisitor {

}
8 changes: 8 additions & 0 deletions src/visitors/utils/getLineNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Obtain the current line number for the node
* @param {} path The path to the current node
* @return {number} The line number
*/
export default function getLineNumber(path) {
return path.node.loc?.start?.line ?? path.parent.loc?.start?.line;
}
16 changes: 16 additions & 0 deletions src/visitors/utils/getName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NodePath } from "@babel/core";

/**
* Attempt to extract the name of the current node
* @param {} path The path to the current node
* @return {string|undefined} The name of the node
*/
export default function getName(path: NodePath): string | undefined {
const { node, parent } = path;

return node.id?.name ??
node.key?.name ??
parent?.id?.name ??
parent?.key?.name;
}

Loading