Brian Chrzanowski



Debugify (JS)

At TheDayJob™, I write quite a bit of JavaScript, which has its ups and downs.

Other than the wonkiness of JS, the worst part about it is when using a framework that features plenty of callbacks, life cycle hooks, and other things that make control-flow go all over the place. Long story short, to aid in debugging, I had the idea to inject some code before the function that existed in the source. Here's the code.

Usage

Object.getOwnPropertyNames(this).filter(x => typeof this[x] === "function").forEach((v) => {
    this[v] = DEBUGIFY(v);
});

// or explicitly

function = DEBUGIFY(function);

Source

// NOTE (BRIAN) THIS IS A TESTING FUNCTION
function DEBUGIFY(fn) {
    if (typeof fn !== "function") {
        return eval(`() => {}`); // return empty func
    }

    const source = fn.toString();
    const decl = source.substring(0, source.indexOf("{")).match(/\w+\(.*\)/)[0].trim();

    const name = decl.match(/(\w+)\(.*\)/)[1];
    const params = decl.match(/\w+\((.*)\)/)[1];
    const body = source.substring(source.indexOf("{") + 1, source.lastIndexOf("}")).trim();

    const newfnsrc = `(function ${name}(${params}) {
        console.log('${name}');
        function _${name}(${params}) { ${body} };
        return _${name}.apply(this, [${params}]);
    })`;

    return eval(newfnsrc);
}

Explanation

DEBUGIFY relies on two ideas: the availability of eval(), and nested functions.

With eval(), we can attempt to replace functions at runtime with other functions we compose. JavaScript's eval function is usually a bad idea in production, but for debugging I see no reason to not use it.

Nested functions will allow us to define a function that looks like and responds like the original function, but with extra code nested wherever you'd like.

So, DEBUGIFY takes a JavaScript function. If the typeof the function isn't a function, then you've passed something else, and you'll get a no-op function.

If you did pass a function, we'll take the source and parse it out.

We'll use the simple function addone as an example:

function addone(a) { return 1 + a; }

Bootstrapping the procedure requires us to get the source text of the function, which can be obtained by calling toString() on the function. We'll save that in source.

To get the declaration of the function (function name, and parameters), we take everything up to the first {, and match it against /\w+\(.*\)/, matching one or more word chars, parenthesis, and any text in between. At this point, decl would look something like addone(a).

To get the name of the function addone, we'll match against those word characters from earlier. To get params, we'll do the opposite: match against whatever's in the argument list. The body is just everything in between the first and last curly braces.

And now, we have everything to compose a function: the name, the arguments list, and the body.

So, we'll use string interpolation to create our new function's source into newfnsrc.

const newfnsrc = `(function ${name}(${params}) {
    console.log(name);
    function _${name}(${params}) { ${body} };
    return _${name}.apply(this, [${params}]);
})`;

Then, all we have to do is call eval on the new function's source, and return it. It is the case that we wrap it up in () to keep the visibility of the function scoped:

return eval(newfnsrc);

And now, you have a function that prints its own name before executing. In there, you could dump comparisons, more logging, really whatever you want.

So, now, if we use DEBUGIFY and call our new addone function, you'll get something like this:

addone = DEBUGIFY(addone);
const v = addone(1);

> addone
> 2