2

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();
 }));
Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
  • I'm surprised nobody has asked why you're doing this. What purpose does this serve? – Patrick Roberts Jan 03 '18 at 05:48
  • Why dont you cut out the inner body of the function using regex after the stringify operation? – isnvi23h4 Jan 03 '18 at 05:51
  • @patrick it's for testing. Isolated functions means you have a clean scope – Alexander Mills Jan 03 '18 at 06:03
  • I know what an isolated function is, it just seems weird that you're trying to explicitly do what v8 already implicitly does. – Patrick Roberts Jan 03 '18 at 06:07
  • how do you mean? V8 implicitly does that when? – Alexander Mills Jan 03 '18 at 06:09
  • if it's not clear, the only variable available to the functions that are created using `new Function()` is h. All surrounding variables are no longer recognized. – Alexander Mills Jan 03 '18 at 06:10
  • 1
    @AlexanderMills when a function is written without referencing any variables in outer scopes, v8 will automatically flag it as isolated and apply the relevant optimizations. You can confirm this by setting a breakpoint in the function on Chrome. If you do that and look at the available scopes at the breakpoint, you won't have access to variable values in the outer scopes from inside the function scope, since v8 flagged the function as isolated during JIT compilation. – Patrick Roberts Jan 03 '18 at 06:16
  • Right, but in this case, the point of using isolated functions is to prevent the developer from accidentally referencing a variable he did not intend to reference. It guarantees clean scope. I think you are overthinking it. Basically if you omit all outer scope variables, they are the same as you suggest. However if you unintentionally include a reference to an outer scope variable, that's where my isolated scope functions come in handy, because that will throw an error. – Alexander Mills Jan 03 '18 at 06:17
  • @PatrickRoberts I updated the question to illustrate what I am talking about. – Alexander Mills Jan 03 '18 at 06:25

3 Answers3

2

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.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • cool I might steal that..pop that into an NPM module and write some tests and I will make it a dependency of mine if you don't mind – Alexander Mills Jan 03 '18 at 07:50
  • @AlexanderMills that's a lot of free work. I'd rather you just take the snippet and credit it to this answer in an inline comment if you don't mind. – Patrick Roberts Jan 03 '18 at 07:53
  • sure no problem, I feel like you did most of the work already – Alexander Mills Jan 03 '18 at 07:54
  • if you can preface the new answer with more info that would be good - I am not following how it works at a high level – Alexander Mills Jan 03 '18 at 21:27
  • the proxy code seems to work, although it prevents usages of console, which is available to `new Function()` apparently – Alexander Mills Jan 03 '18 at 21:39
  • I guess I don't understand why you need `new Function()`, why not just use a function? – Alexander Mills Jan 03 '18 at 21:47
  • 1
    You still need a `Function` the same reason you needed it in your old answer. Just, instead you're conveniently wrapping it in an extra scope dedicated to intercepting scoped variable references using a `with (new Proxy(...))` statement, and calling the function constructor with a context dedicated to throwing on any sort of usage whatsoever. – Patrick Roberts Jan 04 '18 at 04:31
1

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
klugjo
  • 19,422
  • 8
  • 57
  • 75
  • yeah..good call wrt to the arrow functions with {}...I can't think of a good way to parse those, can you? The problem is the first '{' character could be in the wrong spot if the surrounding ones are omitted. I think a regex could do it. Basically check to see if the first character after the => is { or something else. – Alexander Mills Jan 03 '18 at 06:21
0

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.

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817