Open overlookmotel opened 2 years ago
Above implemented as a runtime helper in eval-helper branch.
There is one problem with this approach:
The function created by new Function(...)
is sloppy mode. This is required for usage of with() {}
. However, sloppy mode functions treat this
in an annoying way - calling a sloppy mode function with f.call(undefined)
or f.call(null)
results in this
inside the function being the global object. If called with f.call(123)
(or any other primitive value), the value of this
inside the function is boxed (i.e. Number(123)
).
This can be fixed by changing the above to:
return new Function(
firstVarName,
`with (${firstVarName}) {
return function() {
"use strict";
return eval(${code})
};
}`
)(withObj).call(thisValue);
However, this prevents access to arguments
provided by the with() {}
object.
Again, this can be worked around by replacing .call(thisValue)
with .apply(thisValue, argumentsValue)
. As arguments
cannot be used as a var in strict mode, we know that arguments
must be a genuine Arguments object, so .apply()
will work.
However it's still not quite right. arguments
inside eval()
is not the same arguments object as outside. So arguments[0] = 123
inside eval()
will not affect the external arguments
var.
This problem can be solved by replacing the properties of arguments
inside eval()
with getters/setters which refer to the external arguments
var.
It also needs further code to set any additional properties of arguments
object, set its prototype, and call Object.freeze()
etc if that's been applied to the external arguments
object.
All of this is implemented on eval-helper branch.
However, there's still cases which won't work e.g. arguments.x = 456
(where x
is a new property). What the code in eval()
will do is not knowable ahead of time, so could only create getters+setters for properties that exist initially.
delete arguments[0]
also wouldn't apply to the external arguments
object.
It's not possible to use a Proxy
as can't assign to arguments
in strict mode, and this wouldn't be supported on older browsers in any case.
I don't think this is completely solvable.
Two options:
NB Most of the problems listed at the top are solvable by other means, but it's a pain.
I also considered using with () {}
to prevent access to external vars and re-route accesses to the global object as if the var didn't exist. i.e.:
const _hiddenVar = 1;
const withObj = Object.create(null);
Object.defineProperty( withObj, '_hiddenVar', {
get() {
if (!(_hiddenVar in global)) throw new ReferenceError('_hiddenVar is not defined');
return global._hiddenVar;
},
set(v) {
if (!(_hiddenVar in global)) throw new ReferenceError('_hiddenVar is not defined');
global._hiddenVar = v;
}
} );
with (withObj) {
eval('_hiddenVar');
}
Unfortunately, this doesn't work due to differences in behavior with non-existent globals between strict and sloppy mode.
In strict mode, assigning to a non-existent global throws an error (as above). But in sloppy mode assignment succeeds.
Which is the correct behavior depends on whether it's strict or sloppy mode where the assignment is made. As this is happening inside eval()
, and the code being evaluated is unknown until runtime, it's impossible to determine whether an error should be thrown or not without parsing and examining the code being eval()
-ed at runtime.
Obviously shipping a parser in compiled code to deal with these cases is impractical.
Going to have exactly the same problem with arguments
if output is compiled to ES5 (no arrow functions).
Babel converts this:
function f() {
return (x, y) => [ this, arguments, x, y, eval('[this, arguments, x, y]') ];
}
to:
function f() {
var _arguments = arguments, _this = this;
return function (x, y) {
return [_this, _arguments, x, y, eval("[this, arguments, x, y]")];
};
}
We can do a bit better with:
function f() {
return Object.defineProperties(
Function.prototype.bind.apply(
function () {
var y = arguments[--arguments.length];
delete arguments[arguments.length];
var x = arguments[--arguments.length];
delete arguments[arguments.length];
return [ this, arguments, x, y, eval("[this, arguments, x, y]") ];
},
[this].concat( Array.prototype.slice.call(arguments) )
),
{ length: {value: 2}, name: {value: ''} }
);
}
But still we have the same problem with arguments
inside and outside the inner function not being same object, and therefore changes to one not being reflected in the other.
Could add more code to pull the same tricks as the eval shim (creating getters/setters on arguments
inside inner function) but it's never going to do better than the eval shim, and will have the further disadvantage of exposing temp vars inside eval()
.
So this problem is not entirely solvable in ES5 either.
Another problem: How to deal with super
in eval? e.g.:
const obj = {
foo() {
return () => eval('super.foo()');
}
};
Object.setPrototypeOf( obj, {
foo() { return 1; }
} );
export default obj.foo();
eval()
wrapper would need to populate value of super
.
Could replace new Function(...)
in implementation above with something like:
const objWrapper = (0, eval)(`
({
_(${firstVarName}) {
with (${firstVarName}) {
return eval(${code})
}
}
})
`);
Object.setPrototypeOf( objWrapper, Object.getPrototypeOf( superValue ) );
return objWrapper._.call(thisValue, withObj);
However, this assumes the prototype of original obj
is not changed within the eval()
. e.g. this input would defeat it:
const obj = {
foo() {
return () => eval(`
Object.setPrototypeOf( obj, { foo: () => 2 } );
super.foo()
`);
}
};
Object.setPrototypeOf( obj, {
foo() { return 1; }
} );
export default obj.foo();
Only ways to capture the change of prototype would be:
obj
with a trap to catch a change in prototype.Object.setPrototypeOf()
and Object.prototype.__proto__
setter to catch change in prototype.Option (1) is not workable in browsers which don't support Proxy
, so out of the question (unless all browsers which support super
also support Proxy
?)
Option (2) is pretty nasty.
Would need to apply the shim globally to catch obj.__proto__ = ...
.
Shim would look something like:
const watchCallbacks = [];
function protoBeingSet(obj, proto) {
watchCallbacks.forEach( cb => cb(obj, proto) );
}
const protoPropDescriptor = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'),
protoPropSetter = protoPropDescriptor.set;
Object.defineProperty(Object.prototype, '__proto__', {
...protoPropDescriptor,
set(proto) {
protoBeingSet(this, proto);
return protoPropSetter.apply(this, arguments);
}
});
const {setPrototypeOf} = Object;
Object.setPrototypeOf = function(obj, proto) {
protoBeingSet(obj, proto);
return setPrototypeOf.apply(this, arguments);
};
Eval shim would add a callback to watchCallbacks
:
watchCallbacks.push( (alteredObject, newProto) => {
if (alteredObject === superValue) {
Object.setPrototypeOf(objWrapper, newProto);
}
} );
Problem is that need to remove watch callback from the array once eval()
is done to avoid a memory leak. eval()
-ed code may contain async code (e.g. setTimeout()
) so can't assume watcher can be removed immediately after executing eval()
.
Could use a WeakMap
keyed by the tracked object instead of an array to partly solve this, but only if WeakMap
is available in all browsers which support super()
. I don't know if this is the case. And it doesn't fully solve it anyway, since tracked object may still be referenced elsewhere and so the watcher function would be retained even after it's no longer needed.
A lot of problems!
NB The machinery to handle this could be omitted if eval()
is never used in an a method where super
is available (i.e. object method or class method where that class has a super class). So would only bloat output code where it's possible it would be used.
Or could opt just not to support this edge case.
Actually, maybe this would do the trick:
const objWrapper = (0, eval)(`
({
_(${firstVarName}) {
with (${firstVarName}) {
return eval(${code})
}
}
})
`);
Object.setPrototypeOf( objWrapper, superValue );
return objWrapper._.call(thisValue, withObj);
i.e. Set prototype of the wrapper object to the super object, instead of super object's prototype.
Don't need to worry about delete super.foo
in the eval()
-ed code as that's not valid JS.
Not sure if assignment to super would work (super.foo = ...
) as super
is referring to the wrong object. I'm not actually clear on what super assignments do.
NB These problems with super
are difficult to solve even if not using the eval shim (not impossible, but very convoluted).
There are currently various problems with use of
eval()
:eval()
.this
ineval()
whereeval()
is inside an arrow function.eval()
can assign to const external scope vars, where should throw an error.eval()
statement has to be wrapped in(0, eval)(...)
to prevent access to other vars, which prevents source maps working within any of those functions.eval()
. This is inefficient.const [x, y] = f();
becomesvar _temp = f(), x = _temp[0], y = _temp[1];
). These temp vars will be accessible from within theeval()
, where they shouldn't be.Could solve all these problems by running all
eval()
code in global scope and explicitly passing in variables the eval expression can access.Input:
Current output (even with
mangle
option enabled):Could instead output:
Note that:
eval()
code throw errors as they should.this
is properly supported.with
object is named same as one of the vars that thewith
object provides, so it remains inaccessible.This output is much more verbose, but more correct, and the
_eval()
helper would only need to be included in output once even if multipleeval()
s in the application.