1
votes

I have a Javascript function (got from Function.toString), and I want to wrap all variable declarations with a function (also in Javascript), E.g. const value = 42 to const value = wrapper(42).

First I thought of using RegEx to get the original values and location and then replace them with the wrapped value, but the RegEx got too complex very fast because of needing to thing about multiline strings and objects, for example. Using RegEx would also impact the ease of other people contibuting to the project.

After that I looked into using a module for this, I found Acorn (used by Babel, Svelte. Parses the Javascript into an ESTree, the spec for Javascript Abstract Syntax Trees): https://github.com/acornjs/acorn, but I couldn't find a way of parsing the ESTree back to a Javascript function declaration.

So, is there a way of parsing the ESTree back to a function, or another better sulution?

1
So what if your declaration is followed by another assignment, like let value, x, y; value = 42?trincot

1 Answers

1
votes

You don't really need a function to stringify the tree back into code. Instead, take note of the offsets where the change should occur, and then don't apply the change in the tree, but to the original string.

Here is a demo with the acorn API:

function test () { // The function we want to tamper with
    const value = 42, prefix = "prefix";
    
    let x = 3;
    for (let i = 0; i < 10; i++) {
        x = (x * 997 + value) % 1000;
    }
    return prefix + " " + x;
}

function addInitWrappers(str) { // Returns an updated string
    let ast = acorn.parse(str, {ecmaVersion: 2020});
    
    function* iter(node) {
        if (Object(node) !== node) return; // Primitive
        if (node.type == "VariableDeclaration" && node.kind == "const") {
            for (let {init} of node.declarations) {
                yield init; // yield the offset where this initialisation occurs
            }
        }
        for (let value of Object.values(node)) {
            yield* iter(value);
        }
    }
    
    // Inject the wrapper -- starting at the back
    for (let {start, end} of [...iter(ast)].reverse()) {
        str = str.slice(0, start) + "wrapper(" + str.slice(start, end) + ")" + str.slice(end);
    }
    return str;
}

function wrapper(value) { // A wrapper function to demo with
    return value + 1;
}

console.log("before wrapping test() returns:", test());
let str = test.toString();
str = addInitWrappers(str);
eval(str); // Override the test function with its new definition
console.log("after wrapping test() returns:", test());
<script src="https://cdnjs.cloudflare.com/ajax/libs/acorn/8.7.1/acorn.min.js"></script>