Alexander Mills
Alexander Mills

Reputation: 100010

Create helper function to run a function in an isolated scope

This code works:

  it.cb(h => {
    console.log(h);
    h.ctn();
  });

  it.cb(new Function(
    'h', [
      'console.log(h)',
      'h.ctn()'
    ]
    .join(';')
  ));

these two test cases are basically identical. But constructing a string with array like that is cumbersome, and you can't get static analysis. So what I was thinking of doing was something like this:

 it.cb(isolated(h => {
    console.log(h);
    h.ctn();
 }));

where isolated is a helper function that looks something like:

const isolated = function(fn){
   const str = fn.toString();
   const paramNames = getParamNames(str);
   return new Function(...paramNames.concat(str));
};

the biggest problem is that Function.prototype.toString() gives you the whole function. Does anyone know of a good way to just get the function body from the string representation of the function?

Update: PRoberts was asking what the purpose of this is, the purpose is simply:

 const foo = 3;

 it.cb(isolated(h => {
    console.log(foo);  // this will throw "ReferenceError: foo is not defined"
    h.ctn();
 }));

Upvotes: 2

Views: 748

Answers (3)

Patrick Roberts
Patrick Roberts

Reputation: 51876

I wrote a version of isolated() that handles any non-binded user-defined function expression and throws custom errors for scoped accesses:

function isolated (fn) {
  return new Function(`
    with (new Proxy({}, {
      has () { return true; },
      get (target, property) {
        if (typeof property !== 'string') return target[property];
        throw new ReferenceError(property + ' accessed from isolated scope');
      },
      set (target, property) {
        throw new ReferenceError(property + ' accessed from isolated scope');
      }
    })) return ${Function.prototype.toString.call(fn)}
  `).call(new Proxy(function () {}, new Proxy({}, {
    get() { throw new ReferenceError('this accessed from isolated scope'); }
  })));
}

// test functions
[
  () => arguments, // fail
  () => this, // pass, no way to intercept this
  () => this.foo, // fail
  () => this.foo = 'bar', // fail
  () => this(), // fail
  () => new this, // fail
  h => h, // pass
  h => i, // fail
  (a, b) => b > a ? b : a, // pass
].forEach(fn => {
  const isolate = isolated(fn);
  console.log(isolate.toString());

  try {
    isolate();
    console.log('passed');
  } catch (error) {
    console.log(`${error.name}: ${error.message}`);
  }
})

This implementation is somewhat simpler, and therefore much less error-prone than attempting to parse the parameters and body of a user-defined function.

The with statement is a relatively simplistic means of catching any scoped references within the forcibly isolated function and throwing a ReferenceError. It does so by inserting a Proxy intermediate into the scope with a get trap that intercepts the scoped variable name that was accessed.

The Proxy that is passed as the context of the function was the only part that was a bit tricky to implement, and also incomplete. It was necessary because the Proxy provided as the scope to the with statement does not intercept accesses to the this keyword, so the context must also be wrapped explicitly in order to intercept and throw on any indirect usage of this inside an isolated arrow function.

Upvotes: 2

Alexander Mills
Alexander Mills

Reputation: 100010

Alright this works, that wasn't too hard. We just assume the first and last parens are the outline of function body.

const isolated = function(fn){
  const str = fn.toString();
  const first = str.indexOf('{') + 1;
  const last = str.lastIndexOf('}');
  const body = str.substr(first, last-first);
  const paramNames = ['h'];
  return new Function(...paramNames.concat(body));
};

above we assume the only argument is called "h", but you will need to find function arguments parser. I have used require('function-arguments') in the past.

Upvotes: 0

klugjo
klugjo

Reputation: 20885

I would simply use indexOf('{') and lastIndexOf('}').

const yourFunction = h => {
    console.log(h);
    h.ctn();
};

const fnText = yourFunction.toString();
const body = fnText.substring(fnText.indexOf('{') + 1, fnText.lastIndexOf('}'));

console.log(body);

Knowing that this will not cover arrow functions without a body:

const fn = k => k + 1

Upvotes: 1

Related Questions