• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

overlookmotel / livepack / 6896912539

16 Nov 2023 09:48PM UTC coverage: 90.469% (+0.03%) from 90.441%
6896912539

push

github

overlookmotel
Support `super` in `eval()` [fix]

Fixes #308.

4704 of 5056 branches covered (0.0%)

Branch coverage included in aggregate %.

26 of 37 new or added lines in 4 files covered. (70.27%)

10 existing lines in 2 files now uncovered.

12629 of 14103 relevant lines covered (89.55%)

13072.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.29
/lib/init/eval.js
1
/* --------------------
62✔
2
 * livepack module
62✔
3
 * Replacements for `eval` which instrument code before executing.
62✔
4
 * ------------------*/
62✔
5

62✔
6
'use strict';
62✔
7

62✔
8
// Modules
62✔
9
const generate = require('@babel/generator').default,
62✔
10
        {isString} = require('is-it-type'),
62✔
11
        t = require('@babel/types');
62✔
12

62✔
13
// Imports
62✔
14
const createTracker = require('./tracker.js'),
62✔
15
        getScopeId = require('./getScopeId.js'),
62✔
16
        {parseImpl} = require('../instrument/instrument.js'),
62✔
17
        modifyAst = require('../instrument/modify.js'),
62✔
18
        {
62✔
19
                createBlockWithId, createThisBinding, createNewTargetBinding, createBindingWithoutNameCheck
62✔
20
        } = require('../instrument/blocks.js'),
62✔
21
        {
62✔
22
                INTERNAL_VAR_NAMES_PREFIX, TRACKER_VAR_NAME_BODY, GET_SCOPE_ID_VAR_NAME_BODY
62✔
23
        } = require('../shared/constants.js'),
62✔
24
        specialFunctions = require('../shared/internal.js').functions,
62✔
25
        assertBug = require('../shared/assertBug.js');
62✔
26

62✔
27
// Constants
62✔
28
const DEBUG = !!process.env.LIVEPACK_DEBUG_INSTRUMENT;
62✔
29

62✔
30
// Exports
62✔
31

62✔
32
/**
62✔
33
 * Add eval methods to tracker.
62✔
34
 * @param {Function} tracker - Tracker function for file
62✔
35
 * @param {string} filename - File path
62✔
36
 * @param {Object} blockIdCounter - Block ID counter for file
62✔
37
 * @param {number} prefixNum - Internal vars prefix num
62✔
38
 * @returns {undefined}
62✔
39
 */
62✔
40
module.exports = function addEvalFunctionsToTracker(tracker, filename, blockIdCounter, prefixNum) {
62✔
41
        const evalIndirectLocal = {
2,666✔
42
                eval(code) {
2,666✔
43
                        return evalIndirect(code, tracker, filename, blockIdCounter, prefixNum, evalIndirectLocal);
1,906✔
44
                }
1,906✔
45
        }.eval;
2,666✔
46
        const evalDirectLocal = (...args) => evalDirect(
2,666✔
47
                args, filename, blockIdCounter, prefixNum, evalDirectLocal
2,538✔
48
        );
2,666✔
49
        tracker.evalIndirect = evalIndirectLocal;
2,666✔
50
        tracker.evalDirect = evalDirectLocal;
2,666✔
51

2,666✔
52
        // Record eval shim so it can be switched back to `eval` if it's serialized
2,666✔
53
        specialFunctions.set(evalIndirectLocal, {type: 'eval', parent: null, key: filename});
2,666✔
54
};
62✔
55

62✔
56
/**
62✔
57
 * Shimmed version of `eval` exposed as `livepack_tracker.evalIndirect`.
62✔
58
 * Instrumentation replaces an uses of `eval` which are not `eval()` calls with this.
62✔
59
 * Instruments code before executing it.
62✔
60
 * @param {*} code - Argument to `eval`
62✔
61
 * @param {Function} tracker - Tracker function for file
62✔
62
 * @param {string} filename - File path
62✔
63
 * @param {Object} blockIdCounter - Block ID counter for file
62✔
64
 * @param {number} externalPrefixNum - Internal vars prefix num outside `eval`
62✔
65
 * @param {Function} evalIndirectLocal - Function which was called (used for stack traces if error)
62✔
66
 * @returns {*} - Result of `eval()` call
62✔
67
 */
62✔
68
function evalIndirect(code, tracker, filename, blockIdCounter, externalPrefixNum, evalIndirectLocal) {
1,906✔
69
        // If `code` arg is not a string, eval it unchanged - it won't be evaluated as code
1,906✔
70
        // eslint-disable-next-line no-eval
1,906✔
71
        if (!isString(code)) return execEvalCode(eval, code, false, code, evalIndirectLocal);
1,906!
72

1,906✔
73
        // Compile code with no external scope.
1,906✔
74
        // Code returned is for a function which takes arguments `(livepack_tracker, livepack_getScopeId)`.
1,906✔
75
        const {code: fnCode, shouldThrow, internalPrefixNum} = compile(
1,906✔
76
                code, filename, blockIdCounter, externalPrefixNum, true, false, undefined, [], false
1,906✔
77
        );
1,906✔
78

1,906✔
79
        // If prefix num inside `eval` is different from outside, create new tracker
1,906✔
80
        if (internalPrefixNum !== externalPrefixNum) {
1,906!
81
                tracker = createTracker(filename, blockIdCounter, internalPrefixNum);
×
82
        }
×
83

1,906✔
84
        // `eval()` code without external scope and inject in Livepack's vars
1,906✔
85
        // eslint-disable-next-line no-eval
1,906✔
86
        const fn = execEvalCode(eval, fnCode, shouldThrow, code, evalIndirectLocal);
1,906✔
87
        return fn(tracker, getScopeId);
1,906✔
88
}
1,906✔
89

62✔
90
/**
62✔
91
 * Shimmed version of `eval` exposed as `livepack_tracker.evalDirect`.
62✔
92
 * Instrumentation replaces any uses of `eval` which are direct `eval()` calls with this.
62✔
93
 * Instrumentation also passes details of all vars accessible from outside `eval()`.
62✔
94
 * Reconstruct blocks, then instrument code before executing it.
62✔
95
 * @param {Array<*>} args - Arguments `eval()` called with
62✔
96
 * @param {string} filename - File path
62✔
97
 * @param {Object} blockIdCounter - Block ID counter for file
62✔
98
 * @param {number} externalPrefixNum - Internal vars prefix num outside `eval`
62✔
99
 * @param {Function} evalDirectLocal - Function which was called (used for stack traces if error)
62✔
100
 * @returns {*} - Result of `eval()` call
62✔
101
 */
62✔
102
function evalDirect(args, filename, blockIdCounter, externalPrefixNum, evalDirectLocal) {
2,538✔
103
        const callArgs = args.slice(0, -6),
2,538✔
104
                code = callArgs[0],
2,538✔
105
                [
2,538✔
106
                        possibleEval, execEvalSingleArg, execEvalSpread, scopeDefs, isStrict, superIsProto
2,538✔
107
                ] = args.slice(-6);
2,538✔
108

2,538✔
109
        // If var `eval` where `eval()` is called is not global `eval`,
2,538✔
110
        // call `eval()` with original args unaltered
2,538✔
111
        // eslint-disable-next-line no-eval
2,538✔
112
        if (possibleEval !== eval) return execEvalCode(execEvalSpread, callArgs, false, code, evalDirectLocal);
2,538!
113

2,538✔
114
        // If `code` arg is not a string, eval it unchanged - it won't be evaluated as code
2,538✔
115
        if (!isString(code)) return execEvalCode(execEvalSingleArg, code, false, code, evalDirectLocal);
2,538!
116

2,538✔
117
        // Create blocks
2,538✔
118
        const state = {
2,538✔
119
                currentBlock: undefined,
2,538✔
120
                currentThisBlock: undefined,
2,538✔
121
                currentSuperBlock: undefined,
2,538✔
122
                currentSuperIsProto: false
2,538✔
123
        };
2,538✔
124
        const tempVars = [];
2,538✔
125
        let allowNewTarget = false;
2,538✔
126
        for (const [blockId, blockName, scopeId, ...varDefs] of scopeDefs) {
2,538✔
127
                const block = createBlockWithId(blockId, blockName, true, state);
9,378✔
128
                block.scopeIdVarNode = t.numericLiteral(scopeId);
9,378✔
129
                state.currentBlock = block;
9,378✔
130

9,378✔
131
                for (const [varName, isConst, isSilentConst, argNames, tempVarValue] of varDefs) {
9,378✔
132
                        if (varName === 'this') {
23,062✔
133
                                createThisBinding(block);
2,314✔
134
                                state.currentThisBlock = block;
2,314✔
135
                        } else if (varName === 'new.target') {
23,062✔
136
                                createNewTargetBinding(block);
2,314✔
137
                                allowNewTarget = true;
2,314✔
138
                        } else if (varName === 'super') {
20,748✔
139
                                // Don't create binding - `super` binding is created lazily
672✔
140
                                state.currentSuperBlock = block;
672✔
141
                                state.currentSuperIsProto = superIsProto;
672✔
142
                                tempVars.push({value: tempVarValue, block, varName});
672✔
143
                        } else {
18,434✔
144
                                // Whether var is function is not relevant because it will always be in external scope
17,762✔
145
                                // of the functions it's being recorded on, and value of `isFunction` only has any effect
17,762✔
146
                                // for internal vars
17,762✔
147
                                createBindingWithoutNameCheck(
17,762✔
148
                                        block, varName, {isConst: !!isConst, isSilentConst: !!isSilentConst, argNames}
17,762✔
149
                                );
17,762✔
150

17,762✔
151
                                if (tempVarValue) tempVars.push({value: tempVarValue, block, varName});
17,762!
152
                        }
17,762✔
153
                }
23,062✔
154
        }
9,378✔
155

2,538✔
156
        // Compile to executable code with tracking code inserted.
2,538✔
157
        // If var names prefix inside code has to be different from outside,
2,538✔
158
        // code is wrapped in a function which injects tracker and getScopeId functions, and temp vars:
2,538✔
159
        // `() => foo` -> `(livepack1_tracker, livepack1_getScopeId) => () => foo`
2,538✔
160
        const {code: codeInstrumented, shouldThrow, internalPrefixNum, tempVars: tempVarsUsed} = compile(
2,538✔
161
                code, filename, blockIdCounter, externalPrefixNum, false, allowNewTarget, state, tempVars, isStrict
2,538✔
162
        );
2,538✔
163

2,538✔
164
        // Call `eval()` with amended code
2,538✔
165
        let res = execEvalCode(execEvalSingleArg, codeInstrumented, shouldThrow, code, evalDirectLocal);
2,538✔
166

2,538✔
167
        // If was wrapped in a function, create new tracker and inject tracker and `getScopeId`
2,538✔
168
        // and/or any temp vars into function
2,538✔
169
        const params = internalPrefixNum !== externalPrefixNum
2,538✔
170
                ? [createTracker(filename, blockIdCounter, internalPrefixNum), getScopeId]
2,538✔
171
                : [];
2,538✔
172
        if (tempVarsUsed.length > 0) params.push(...tempVarsUsed.map(tempVar => tempVar.value));
2,538✔
173
        if (params.length > 0) res = res(...params);
2,532✔
174

2,528✔
175
        return res;
2,528✔
176
}
2,538✔
177

62✔
178
/**
62✔
179
 * Execute eval call.
62✔
180
 * If `shouldThrowSyntaxError` is set and `eval()` doesn't throw, throw an error.
62✔
181
 * This is to check that if parsing the code threw an error, it is indeed a genuine syntax error.
62✔
182
 * @param {Function} exec - Function to call
62✔
183
 * @param {*} arg - Argument to call `exec` with
62✔
184
 * @param {boolean} shouldThrowSyntaxError - `true` if `exec(arg)` should throw
62✔
185
 * @param {string} code - Code being executed (for error message)
62✔
186
 * @param {Function} fn - Wrapper function
62✔
187
 * @returns {*} - Return value of `exec(arg)`
62✔
188
 * @throws {*} - Error thrown by `exec(arg)` or internal error if should have thrown but didn't
62✔
189
 */
62✔
190
function execEvalCode(exec, arg, shouldThrowSyntaxError, code, fn) {
4,444✔
191
        let hasThrown = false;
4,444✔
192
        try {
4,444✔
193
                return exec(arg);
4,444✔
194
        } catch (err) {
4,444✔
195
                hasThrown = true;
10✔
196
                Error.captureStackTrace(err, fn);
10✔
197
                throw err;
10✔
198
        } finally {
4,444!
199
                assertBug(
4,444✔
200
                        !shouldThrowSyntaxError || hasThrown,
4,444✔
201
                        'Failed to parse `eval` expression',
4,444✔
202
                        () => `Eval expression: ${JSON.stringify(code)}`
4,444✔
203
                );
4,444✔
204
        }
4,444✔
205
}
4,444✔
206

62✔
207
/**
62✔
208
 * Instrument code.
62✔
209
 * If is indirect `eval`, wraps the code in a function which takes `livepack_tracker`
62✔
210
 * and `livepack_getScopeId` arguments. This is to inject Livepack's internal functions into scope.
62✔
211
 * If is direct `eval()` and internal vars prefix number outside `eval()` and inside `eval()` differ,
62✔
212
 * wrap in an IIFE to inject the internal vars with new names:
62✔
213
 * `((livepack20_tracker, livepack20_getScopeId) => { ... })(livepack_tracker, livepack_getScopeId)`
62✔
214
 *
62✔
215
 * @param {string} code - Code string passed to `eval()`
62✔
216
 * @param {string} filename - File path
62✔
217
 * @param {Object} blockIdCounter - Block ID counter for file
62✔
218
 * @param {number} externalPrefixNum - Internal vars prefix num outside `eval()`
62✔
219
 * @param {boolean} isIndirectEval - `true` if is indirect eval
62✔
220
 * @param {boolean} allowNewTarget - `true` if `new.target` is legal at top level
62✔
221
 * @param {Object} [state] - State to initialize instrumentation state (only if direct `eval()` call)
62✔
222
 * @param {Array<Object>} tempVars - Array of temp vars to be injected
62✔
223
 * @param {boolean} isStrict - `true` if environment outside `eval()` is strict mode
62✔
224
 * @returns {Object} - Object with properties:
62✔
225
 *   {string} .code - Instrumented code (or input code if parsing failed)
62✔
226
 *   {boolean} .shouldThrow - `true` if could not parse code, so calling `eval()` with this code
62✔
227
 *     should throw syntax error
62✔
228
 *   {number} .internalPrefixNum - Internal vars prefix num inside `eval()`
62✔
229
 *   {Array<Object>} .tempVars - Array of temp vars to be injected
62✔
230
 */
62✔
231
function compile(
4,444✔
232
        code, filename, blockIdCounter, externalPrefixNum,
4,444✔
233
        isIndirectEval, allowNewTarget, state, tempVars, isStrict
4,444✔
234
) {
4,444✔
235
        // Parse code.
4,444✔
236
        // If parsing fails, swallow error. Expression will be passed to `eval()`
4,444✔
237
        // which should throw - this maintains native errors.
4,444✔
238
        const allowSuper = !!state?.currentSuperBlock;
4,444✔
239
        let ast;
4,444✔
240
        try {
4,444✔
241
                ast = parseImpl(
4,444✔
242
                        code, filename, false, false, allowNewTarget, allowSuper, false, isStrict, false, undefined
4,444✔
243
                ).ast;
4,444✔
244
        } catch (err) {
4,444✔
245
                return {code, shouldThrow: true, internalPrefixNum: externalPrefixNum, tempVars};
10✔
246
        }
10✔
247

4,434✔
248
        // Instrument code.
4,434✔
249
        // Filename in eval code will be same as host file.
4,434✔
250
        // Block IDs in created code will be higher than block IDs used in this file (to avoid clashes).
4,434✔
251
        // Var name prefix will be kept same as in host file if possible,
4,434✔
252
        // to avoid wrapping in a function unless impossible to avoid.
4,434✔
253
        // Details of vars which can be obtained from external scopes is passed in.
4,434✔
254
        state = {
4,434✔
255
                nextBlockId: blockIdCounter.nextBlockId,
4,434✔
256
                isStrict,
4,434✔
257
                internalVarsPrefixNum: externalPrefixNum,
4,434✔
258
                ...state
4,434✔
259
        };
4,434✔
260
        modifyAst(ast, filename, false, isStrict, undefined, state);
4,434✔
261

4,434✔
262
        // Update next block ID for file
4,434✔
263
        blockIdCounter.nextBlockId = state.nextBlockId;
4,434✔
264

4,434✔
265
        // If indirect `eval`, or direct `eval()` with different prefix nums inside and outside `eval()`,
4,434✔
266
        // wrap in function to inject Livepack's internal vars.
4,434✔
267
        // `123` => `(livepack_tracker, livepack_getScopeId) => eval('123')`.
4,434✔
268
        // Wrapping in a 2nd `eval()` is required to ensure it returns its value.
4,434✔
269
        const internalPrefixNum = state.internalVarsPrefixNum;
4,434✔
270
        const params = (isIndirectEval || internalPrefixNum !== externalPrefixNum)
4,444✔
271
                ? [
4,444✔
272
                        internalVarNode(TRACKER_VAR_NAME_BODY, internalPrefixNum),
2,258✔
273
                        internalVarNode(GET_SCOPE_ID_VAR_NAME_BODY, internalPrefixNum)
2,258✔
274
                ]
2,258✔
275
                : [];
4,444✔
276

4,444✔
277
        // Filter out any temp vars which aren't used in `eval`-ed code
4,444✔
278
        if (tempVars.length > 0) {
4,444✔
279
                tempVars = tempVars.filter((tempVar) => {
672✔
280
                        const varNode = tempVar.block.bindings[tempVar.varName]?.varNode;
672✔
281
                        if (!varNode) return false;
672✔
282
                        params.push(varNode);
304✔
283
                        return true;
304✔
284
                });
672✔
285
        }
672✔
286

4,434✔
287
        if (params.length > 0) ast = wrapInFunction(ast, params);
4,438✔
288

4,434✔
289
        // Return instrumented code
4,434✔
290
        code = generate(ast, {retainLines: true, compact: true}).code;
4,434✔
291

4,434✔
292
        if (DEBUG) {
4,438!
UNCOV
293
                /* eslint-disable no-console */
×
UNCOV
294
                console.log('----------------------------------------');
×
UNCOV
295
                console.log('TRANSFORMED: eval code in', filename);
×
UNCOV
296
                console.log('----------------------------------------');
×
UNCOV
297
                console.log(code);
×
UNCOV
298
                console.log('');
×
UNCOV
299
                /* eslint-enable no-console */
×
UNCOV
300
        }
×
301

4,434✔
302
        return {code, shouldThrow: false, internalPrefixNum, tempVars};
4,434✔
303
}
4,444✔
304

62✔
305
function wrapInFunction(ast, params) {
2,418✔
306
        return t.program([
2,418✔
307
                t.expressionStatement(
2,418✔
308
                        t.arrowFunctionExpression(
2,418✔
309
                                params,
2,418✔
310
                                t.callExpression(
2,418✔
311
                                        t.identifier('eval'), [
2,418✔
312
                                                t.stringLiteral(generate(ast, {retainLines: true, compact: true}).code)
2,418✔
313
                                        ]
2,418✔
314
                                )
2,418✔
315
                        )
2,418✔
316
                )
2,418✔
317
        ]);
2,418✔
318
}
2,418✔
319

62✔
320
function internalVarNode(name, prefixNum) {
4,516✔
321
        return t.identifier(`${INTERNAL_VAR_NAMES_PREFIX}${prefixNum || ''}_${name}`);
4,516✔
322
}
4,516✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc