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

overlookmotel / livepack / 6890751424

16 Nov 2023 12:17PM UTC coverage: 90.441% (-0.008%) from 90.449%
6890751424

push

github

overlookmotel
Dev: Update dev dependencies

4644 of 4995 branches covered (0.0%)

Branch coverage included in aggregate %.

12585 of 14055 relevant lines covered (89.54%)

12929.78 hits per line

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

93.94
/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,314✔
42
                eval(code) {
2,314✔
43
                        return evalIndirect(code, tracker, filename, blockIdCounter, prefixNum, evalIndirectLocal);
1,906✔
44
                }
1,906✔
45
        }.eval;
2,314✔
46
        const evalDirectLocal = (...args) => evalDirect(
2,314✔
47
                args, filename, blockIdCounter, prefixNum, evalDirectLocal
2,090✔
48
        );
2,314✔
49
        tracker.evalIndirect = evalIndirectLocal;
2,314✔
50
        tracker.evalDirect = evalDirectLocal;
2,314✔
51

2,314✔
52
        // Record eval shim so it can be switched back to `eval` if it's serialized
2,314✔
53
        specialFunctions.set(evalIndirectLocal, {type: 'eval', parent: null, key: filename});
2,314✔
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,090✔
103
        const callArgs = args.slice(0, -5),
2,090✔
104
                code = callArgs[0],
2,090✔
105
                [possibleEval, execEvalSingleArg, execEvalSpread, scopeDefs, isStrict] = args.slice(-5);
2,090✔
106

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

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

2,090✔
115
        // Create blocks
2,090✔
116
        const state = {
2,090✔
117
                currentBlock: undefined,
2,090✔
118
                currentThisBlock: undefined,
2,090✔
119
                currentSuperBlock: undefined
2,090✔
120
        };
2,090✔
121
        let allowNewTarget = false;
2,090✔
122
        for (const [blockId, blockName, scopeId, ...varDefs] of scopeDefs) {
2,090✔
123
                const block = createBlockWithId(blockId, blockName, true, state);
6,706✔
124
                block.scopeIdVarNode = t.numericLiteral(scopeId);
6,706✔
125
                state.currentBlock = block;
6,706✔
126

6,706✔
127
                for (const [varName, isConst, isSilentConst, argNames] of varDefs) {
6,706✔
128
                        if (varName === 'this') {
17,158✔
129
                                createThisBinding(block);
1,866✔
130
                                state.currentThisBlock = block;
1,866✔
131
                        } else if (varName === 'new.target') {
17,158✔
132
                                createNewTargetBinding(block);
1,866✔
133
                                allowNewTarget = true;
1,866✔
134
                        } else {
15,292✔
135
                                // TODO: Also need to set `superIsProto` and create a new temp var for `super` target
13,426✔
136
                                // (the external var could be shadowed inside `eval()` if prefix num is changing)
13,426✔
137
                                if (varName === 'super') state.currentSuperBlock = block;
13,426✔
138

13,426✔
139
                                // Whether var is function is not relevant because it will always be in external scope
13,426✔
140
                                // of the functions it's being recorded on, and value of `isFunction` only has any effect
13,426✔
141
                                // for internal vars
13,426✔
142
                                createBindingWithoutNameCheck(
13,426✔
143
                                        block, varName, {isConst: !!isConst, isSilentConst: !!isSilentConst, argNames}
13,426✔
144
                                );
13,426✔
145
                        }
13,426✔
146
                }
17,158✔
147
        }
6,706✔
148

2,090✔
149
        // Compile to executable code with tracking code inserted.
2,090✔
150
        // If var names prefix inside code has to be different from outside,
2,090✔
151
        // code is wrapped in a function which injects tracker and getScopeId functions:
2,090✔
152
        // `() => foo` -> `(livepack1_tracker, livepack1_getScopeId) => () => foo`
2,090✔
153
        const {
2,090✔
154
                code: codeInstrumented, shouldThrow, internalPrefixNum
2,090✔
155
        } = compile(code, filename, blockIdCounter, externalPrefixNum, false, allowNewTarget, state, isStrict);
2,090✔
156

2,090✔
157
        // Call `eval()` with amended code
2,090✔
158
        let res = execEvalCode(execEvalSingleArg, codeInstrumented, shouldThrow, code, evalDirectLocal);
2,090✔
159

2,090✔
160
        // If was wrapped in a function, create new tracker and inject tracker and `getScopeId` into function
2,090✔
161
        if (internalPrefixNum !== externalPrefixNum) {
2,090✔
162
                const tracker = createTracker(filename, blockIdCounter, internalPrefixNum);
128✔
163
                res = res(tracker, getScopeId);
128✔
164
        }
128✔
165

2,080✔
166
        return res;
2,080✔
167
}
2,090✔
168

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

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

3,986✔
235
        // Instrument code.
3,986✔
236
        // Filename in eval code will be same as host file.
3,986✔
237
        // Block IDs in created code will be higher than block IDs used in this file (to avoid clashes).
3,986✔
238
        // Var name prefix will be kept same as in host file if possible,
3,986✔
239
        // to avoid wrapping in a function unless impossible to avoid.
3,986✔
240
        // Details of vars which can be obtained from external scopes is passed in.
3,986✔
241
        state = {
3,986✔
242
                nextBlockId: blockIdCounter.nextBlockId,
3,986✔
243
                isStrict,
3,986✔
244
                internalVarsPrefixNum: externalPrefixNum,
3,986✔
245
                ...state
3,986✔
246
        };
3,986✔
247
        modifyAst(ast, filename, false, isStrict, undefined, state);
3,986✔
248

3,986✔
249
        // Update next block ID for file
3,986✔
250
        blockIdCounter.nextBlockId = state.nextBlockId;
3,986✔
251

3,986✔
252
        // If indirect `eval`, or direct `eval()` with different prefix nums inside and outside `eval()`,
3,986✔
253
        // wrap in function to inject Livepack's internal vars.
3,986✔
254
        // `123` => `(livepack_tracker, livepack_getScopeId) => eval('123')`.
3,986✔
255
        // Wrapping in a 2nd `eval()` is required to ensure it returns its value.
3,986✔
256
        const internalPrefixNum = state.internalVarsPrefixNum;
3,986✔
257
        if (isIndirectEval || internalPrefixNum !== externalPrefixNum) {
3,996✔
258
                ast = wrapInFunction(ast, internalPrefixNum);
2,034✔
259
        }
2,034✔
260

3,986✔
261
        // Return instrumented code
3,986✔
262
        code = generate(ast, {retainLines: true, compact: true}).code;
3,986✔
263

3,986✔
264
        if (DEBUG) {
3,990!
265
                /* eslint-disable no-console */
×
266
                console.log('----------------------------------------');
×
267
                console.log('TRANSFORMED: eval code in', filename);
×
268
                console.log('----------------------------------------');
×
269
                console.log(code);
×
270
                console.log('');
×
271
                /* eslint-enable no-console */
×
272
        }
×
273

3,986✔
274
        return {code, shouldThrow: false, internalPrefixNum};
3,986✔
275
}
3,996✔
276

62✔
277
function wrapInFunction(ast, internalPrefixNum) {
2,034✔
278
        return t.program([
2,034✔
279
                t.expressionStatement(
2,034✔
280
                        t.arrowFunctionExpression(
2,034✔
281
                                [
2,034✔
282
                                        internalVarNode(TRACKER_VAR_NAME_BODY, internalPrefixNum),
2,034✔
283
                                        internalVarNode(GET_SCOPE_ID_VAR_NAME_BODY, internalPrefixNum)
2,034✔
284
                                ],
2,034✔
285
                                t.callExpression(
2,034✔
286
                                        t.identifier('eval'), [
2,034✔
287
                                                t.stringLiteral(generate(ast, {retainLines: true, compact: true}).code)
2,034✔
288
                                        ]
2,034✔
289
                                )
2,034✔
290
                        )
2,034✔
291
                )
2,034✔
292
        ]);
2,034✔
293
}
2,034✔
294

62✔
295
function internalVarNode(name, prefixNum) {
4,068✔
296
        return t.identifier(`${INTERNAL_VAR_NAMES_PREFIX}${prefixNum || ''}_${name}`);
4,068✔
297
}
4,068✔
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