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

overlookmotel / livepack / 7305779414

23 Dec 2023 02:56AM UTC coverage: 90.551% (-0.03%) from 90.578%
7305779414

push

github

overlookmotel
Capture `module` objects when function declaration called `module` at top level [fix]

Fixes #566.

4680 of 5034 branches covered (0.0%)

Branch coverage included in aggregate %.

15 of 15 new or added lines in 3 files covered. (100.0%)

35 existing lines in 15 files now uncovered.

12473 of 13909 relevant lines covered (89.68%)

8705.64 hits per line

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

85.68
/lib/instrument/modify.js
1
/* --------------------
62✔
2
 * livepack module
62✔
3
 * Code instrumentation function to instrument AST
62✔
4
 * ------------------*/
62✔
5

62✔
6
'use strict';
62✔
7

62✔
8
// Modules
62✔
9
const {join: pathJoin, parse: pathParse} = require('path'),
62✔
10
        {ensureStatementsHoisted} = require('@babel/helper-module-transforms'),
62✔
11
        t = require('@babel/types');
62✔
12

62✔
13
// Imports
62✔
14
const Program = require('./visitors/program.js'),
62✔
15
        {
62✔
16
                escapeFilename, hoistSloppyFunctionDeclarations, createFunctionInfoFunction
62✔
17
        } = require('./visitors/function.js'),
62✔
18
        {
62✔
19
                createAndEnterBlock, createBindingWithoutNameCheck, createThisBinding, createArgumentsBinding,
62✔
20
                createNewTargetBinding
62✔
21
        } = require('./blocks.js'),
62✔
22
        {insertBlockVarsIntoBlockStatement} = require('./tracking.js'),
62✔
23
        {
62✔
24
                createTrackerVarNode, createGetScopeIdVarNode, createFnInfoVarNode, renameInternalVars
62✔
25
        } = require('./internalVars.js'),
62✔
26
        {visitKey} = require('./visit.js'),
62✔
27
        {hasUseStrictDirective, stringLiteralWithSingleQuotes} = require('./utils.js'),
62✔
28
        {getProp} = require('../shared/functions.js');
62✔
29

62✔
30
// Constants
62✔
31
const INIT_PATH = pathJoin(__dirname, '../init/index.js'),
62✔
32
        TOP_BLOCK_ID = 1;
62✔
33

62✔
34
// Exports
62✔
35

62✔
36
module.exports = modifyAst;
62✔
37

62✔
38
/**
62✔
39
 * Instrument AST.
62✔
40
 * Instrumentation occurs in 2 passes.
62✔
41
 * 1st pass:
62✔
42
 *   - Entire AST is traversed.
62✔
43
 *   - Bindings are created for variable declarations, function/class declarations,
62✔
44
 *     `this`, `arguments` and `super` targets.
62✔
45
 *   - A queue is created of actions to take in 2nd pass.
62✔
46
 *     Actions are added to queue when exiting nodes (rather than when entering them),
62✔
47
 *     so that in 2nd pass deeper nodes are processed first.
62✔
48
 * 2nd pass:
62✔
49
 *   - Bindings are created for sloppy-mode function declarations which are hoisted
62✔
50
 *     (where they are bound can only be determined once all other bindings are known).
62✔
51
 *   - Call the queue which was created in 1st pass, which adds the instrumentation code.
62✔
52
 *   - Add call to `init()` at top of file.
62✔
53
 *   - Insert function info functions.
62✔
54
 *
62✔
55
 * To see how files are instrumented, run Livepack with env var `LIVEPACK_DEBUG_INSTRUMENT` set.
62✔
56
 * e.g. `LIVEPACK_DEBUG_INSTRUMENT=1 npx livepack ...`
62✔
57
 *
62✔
58
 * @param {Object} ast - AST
62✔
59
 * @param {string} filename - File path
62✔
60
 * @param {boolean} isCommonJs - `true` if is CommonJS file
62✔
61
 * @param {boolean} isStrict - `true` if is strict mode code
62✔
62
 * @param {Object} [sources] - Sources object mapping file path to file content
62✔
63
 * @param {Object} [evalState] - State from eval outer context
62✔
64
 * @returns {Object} - Transformed AST
62✔
65
 */
62✔
66
function modifyAst(ast, filename, isCommonJs, isStrict, sources, evalState) {
4,184✔
67
        // Init state object
4,184✔
68
        const secondPassQueue = [];
4,184✔
69
        const state = {
4,184✔
70
                filename,
4,184✔
71
                filenameEscaped: escapeFilename(filename),
4,184✔
72
                nextBlockId: TOP_BLOCK_ID,
4,184✔
73
                currentBlock: undefined,
4,184✔
74
                currentThisBlock: undefined,
4,184✔
75
                currentSuperBlock: undefined,
4✔
76
                currentHoistBlock: undefined,
4✔
77
                fileBlock: undefined,
4✔
78
                programBlock: undefined,
4✔
79
                currentFunction: undefined,
4✔
80
                isStrict,
4✔
81
                currentSuperIsProto: false,
4✔
UNCOV
82
                fileNode: ast,
×
83
                trail: [],
×
84
                sloppyFunctionDeclarations: [],
×
85
                internalVarNodes: [],
×
86
                internalVarsPrefixNum: 0,
×
87
                trackerVarNode: undefined,
×
88
                getScopeIdVarNode: undefined,
×
89
                getSourcesNode: undefined,
×
90
                functions: [],
×
91
                fileContainsFunctionsOrEval: false,
4✔
92
                secondPass: (fn, ...params) => secondPassQueue.push({fn, params})
4✔
93
        };
4✔
94

4✔
95
        if (evalState) Object.assign(state, evalState);
4✔
96

4✔
97
        // Determine if strict mode
4✔
98
        if (!state.isStrict && (ast.program.sourceType === 'module' || hasUseStrictDirective(ast.program))) {
4✔
99
                state.isStrict = true;
4✔
100
        }
4✔
101

4✔
102
        // Create file block.
4✔
103
        const blockName = evalState ? 'eval' : pathParse(filename).name;
4✔
104
        const fileBlock = createAndEnterBlock(blockName, true, state);
4✔
105
        state.fileBlock = fileBlock;
4✔
106

4✔
107
        // Create program block
4✔
108
        const programBlock = createAndEnterBlock(blockName, true, state);
4✔
109
        state.programBlock = programBlock;
4✔
110
        // TODO: If code is a script or in indirect `(0, eval)(...)` in sloppy mode,
4✔
111
        // `var` declarations and function declarations (including async functions + generator functions)
4✔
112
        // create globals, not local bindings.
60✔
113
        // In sloppy-mode direct `eval()` they create bindings in hoist block external to the `eval()`.
60✔
114
        // So in both cases, there shouldn't be a hoist block.
60✔
115
        state.currentHoistBlock = programBlock;
4✔
116

4✔
117
        // Set file block's vars block to program block
4!
118
        fileBlock.varsBlock = programBlock;
4✔
119

4✔
120
        if (isCommonJs) {
4✔
121
                // Create CommonJS var bindings.
4✔
122
                // `this` and `arguments` refer to `this` and `arguments` of the CommonJS wrapper function.
4✔
123
                for (const varName of ['module', 'exports', 'require']) {
4!
124
                        createBindingWithoutNameCheck(fileBlock, varName, {});
4✔
125
                }
4✔
126
                createThisBinding(fileBlock);
4✔
127
                createArgumentsBinding(fileBlock, false, ['exports', 'require', 'module']);
4✔
128
                createNewTargetBinding(fileBlock);
4✔
129
                state.currentThisBlock = fileBlock;
4✔
130
        } else if (!fileBlock.parent) {
4✔
131
                // Either ESM, or script context code, or code from inside indirect eval `(0, eval)()`.
4✔
132
                // NB: Only remaining possibility is direct `eval()` code, where `this` is already defined
4✔
133
                // in parent blocks from the outer context.
4✔
134
                // Create binding for `this` (which will be `undefined` in ESM, or `globalThis` in script context).
4✔
135
                // TODO: Uncomment next line.
4✔
136
                // It is correct, but producing excessively verbose output where indirect eval is used.
4✔
137
                // Need to remove references to `globalThis` where in output the code is run inside `(0, eval)()`
4!
138
                // anyway, so `this` is already `globalThis` and unnecessary to inject it.
×
139
                // https://github.com/overlookmotel/livepack/issues/353
×
140
                // createThisBinding(fileBlock);
×
141

×
142
                state.currentThisBlock = fileBlock;
×
143
        } else if (!state.currentThisBlock) {
✔
144
                // Code from direct `eval()` which doesn't provide a `this` binding.
×
145
                // TODO: This can be removed once `createThisBinding(fileBlock)` above is uncommented,
4✔
146
                // as then `state.currentThisBlock` will always be defined already in parent scope.
4✔
147
                state.currentThisBlock = fileBlock;
4✔
148
        }
4✔
149

4✔
150
        // Instrument AST
4✔
151
        modifyFirstPass(ast, state);
4✔
152
        modifySecondPass(ast, secondPassQueue, !!evalState, sources, state);
4✔
153

4✔
154
        // Pass state back to eval-handling code
4✔
155
        if (evalState) {
4✔
156
                evalState.nextBlockId = state.nextBlockId;
4✔
157
                evalState.internalVarsPrefixNum = state.internalVarsPrefixNum;
4✔
158
        }
4✔
159

4✔
160
        // Return AST
4✔
161
        return ast;
4✔
162
}
4✔
163

4✔
164
/**
4✔
165
 * Instrumentation first pass.
4✔
166
 * If an error is thrown, the error message is augmented with location in code where error occurred.
4✔
167
 * @param {Object} ast - AST
4✔
168
 * @param {Object} state - State object
4✔
169
 * @returns {undefined}
4✔
170
 */
4✔
171
function modifyFirstPass(ast, state) {
4,184✔
172
        try {
4,184✔
173
                visitKey(ast, 'program', Program, state);
4,184✔
174
        } catch (err) {
4,184!
175
                rethrowErrorWithLocation(err, getCurrentNode(state), state);
×
176
        }
×
177
}
4,184✔
178

×
179
/**
×
180
 * Instrumentation second pass.
×
181
 * @param {Object} ast - AST
×
182
 * @param {Array<Object>} secondPassQueue - Queue of functions to call in 2nd pass
×
183
 * @param {boolean} isEvalCode - `true` if code is from within `eval()`
×
184
 * @param {Object} [sources] - Object mapping file paths to source code of file before instrumentation
4✔
185
 * @param {Object} state - State object
4✔
186
 * @returns {undefined}
4✔
187
 */
4✔
188
function modifySecondPass(ast, secondPassQueue, isEvalCode, sources, state) {
4,184!
189
        state.trackerVarNode = createTrackerVarNode(state);
4,184✔
190
        state.getScopeIdVarNode = createGetScopeIdVarNode(state);
4,184✔
191

4,184✔
192
        const programNode = ast.program;
4,184✔
193

4,184✔
194
        // If file contains no functions or `eval`, needs no instrumentation
4,184✔
195
        if (state.fileContainsFunctionsOrEval) {
4,184✔
196
                hoistSloppyFunctionDeclarations(state);
3,168✔
197
                state.getSourcesNode = createFnInfoVarNode(0, state);
3,168✔
198
                processQueue(secondPassQueue, state);
3,168✔
199
                insertBlockVarsIntoBlockStatement(state.programBlock, programNode, state);
4✔
200
                insertFunctionInfoFunctions(programNode, isEvalCode, sources, state);
4✔
201
        }
×
202

×
203
        if (!isEvalCode) insertImportStatement(programNode, state);
✔
204

×
205
        renameInternalVars(state);
4✔
206
}
4✔
207

4✔
208
/**
4✔
209
 * Call queue of functions which were queued in first pass.
4!
210
 * If an error is thrown, the error message is augmented with location in code where error occurred.
62✔
211
 * @param {Array<Object>} secondPassQueue - Queue of functions to call
62✔
212
 * @param {Object} state - State object
62✔
213
 * @returns {undefined}
62✔
214
 */
62✔
215
function processQueue(secondPassQueue, state) {
3,168✔
216
        for (const {fn, params} of secondPassQueue) {
3,168✔
217
                try {
190,288✔
218
                        fn(...params);
190,288✔
219
                } catch (err) {
190,288!
220
                        rethrowErrorWithLocation(err, params[0], state);
×
221
                }
×
222
        }
190,288✔
223
}
3,168✔
224

62✔
225
/**
62✔
226
 * Insert `require` statement at top of file (above `scopeId` definition)
62✔
227
 * to inject Livepack's internal functions.
4✔
228
 * @param {Object} programNode - Program AST node
4✔
229
 * @param {Object} state - State object
4✔
UNCOV
230
 * @returns {undefined}
×
231
 */
×
232
function insertImportStatement(programNode, state) {
1,774✔
233
        // ```
1,774✔
234
        // const [livepack_tracker, livepack_getScopeId]
1,774✔
235
        //   = require('/path/to/app/node_modules/livepack/lib/init/index.js')
1,774✔
236
        //     ('/path/to/app/file.js', require, 100, 0);
1,774✔
237
        // ```
1,774✔
238
        const statementNode = t.variableDeclaration(
1,774✔
239
                'const', [
1,774✔
240
                        t.variableDeclarator(
1,774✔
241
                                t.arrayPattern([state.trackerVarNode, state.getScopeIdVarNode]),
1,774✔
242
                                t.callExpression(
1,774✔
243
                                        t.callExpression(t.identifier('require'), [t.stringLiteral(INIT_PATH)]),
1,774✔
244
                                        [
1,774✔
245
                                                t.stringLiteral(state.filename),
1,774✔
246
                                                t.identifier('require'),
1,774✔
247
                                                t.numericLiteral(state.nextBlockId),
1,774✔
248
                                                t.numericLiteral(state.internalVarsPrefixNum)
1,774✔
249
                                        ]
1,774✔
250
                                )
1,774✔
251
                        )
1,774✔
252
                ]
1,774✔
253
        );
1,774✔
254

1,774✔
255
        // Ensure this remains above headers added by `@babel/plugin-transform-modules-commonjs`
1,774✔
256
        ensureStatementsHoisted([statementNode]);
1,774✔
257

1,774✔
258
        programNode.body.unshift(statementNode);
1,774✔
259
}
1,774✔
260

4✔
261
/**
4✔
262
 * Insert function info functions.
62✔
263
 * @param {Object} programNode - Program AST node
62✔
264
 * @param {boolean} isEvalCode - `true` if is code from inside `eval()`
62✔
265
 * @param {Object} [sources] - Object mapping file path to source code for file
62✔
266
 *   (`undefined` if source maps disabled)
62✔
267
 * @param {Object} state - State object
62✔
268
 * @returns {undefined}
2✔
269
 */
2✔
270
function insertFunctionInfoFunctions(programNode, isEvalCode, sources, state) {
3,168✔
271
        if (state.functions.length === 0) return;
3,168✔
272

3,008✔
273
        const functionInfoNodes = state.functions.map(fn => createFunctionInfoFunction(fn, state));
3,008✔
274

3,008✔
275
        // Insert function to get sources
3,008✔
276
        functionInfoNodes.push(
3,008✔
277
                t.functionDeclaration(state.getSourcesNode, [], t.blockStatement([
3,008!
278
                        t.returnStatement(stringLiteralWithSingleQuotes(sources ? JSON.stringify(sources) : '{}'))
3,168✔
279
                ]))
3,168✔
280
        );
3,168✔
281

3,168✔
282
        if (!isEvalCode || state.isStrict) {
3,168✔
283
                // Insert at bottom of file
1,998✔
284
                programNode.body.push(...functionInfoNodes);
1,998✔
285
        } else {
3,168✔
286
                // Sloppy mode eval code - convert function info functions to const statement and add to top.
1,010✔
287
                // Otherwise function declarations would be hoisted to scope outside the `eval()`,
4✔
288
                // or to global scope in indirect `eval`.
4✔
289
                programNode.body.unshift(t.variableDeclaration('const', functionInfoNodes.map((fnInfoNode) => {
4✔
290
                        fnInfoNode.type = 'FunctionExpression';
2,620✔
291
                        const idNode = fnInfoNode.id;
2,620✔
292
                        fnInfoNode.id = null;
2,620✔
293
                        return t.variableDeclarator(idNode, fnInfoNode);
2,620✔
294
                })));
4✔
295
        }
4✔
296
}
4✔
297

4✔
298
/**
4✔
299
 * Get current AST node, to get location where instrumentation failed and threw error.
4✔
300
 * Uses current function node and trail to get node. If no node at that trail, get parent node.
4✔
301
 * This is needed in case error caused by attempting to visit a null node.
4✔
302
 * @param {Object} state - State object
4✔
303
 * @returns {Object} - Current AST node
4✔
304
 */
4✔
305
function getCurrentNode(state) {
×
306
        const {currentFunction: fn, trail} = state,
×
307
                rootNode = fn ? fn.node : state.fileNode;
×
308
        let node;
×
309
        for (let len = trail.length; len > 0; len--) {
×
310
                node = getProp(rootNode, trail, len);
×
311
                if (node) return node;
×
312
        }
×
313
        return rootNode;
×
314
}
×
315

4✔
316
/**
4✔
317
 * Add location info to error and throw it.
62✔
318
 * @param {*} err - Error thrown
62✔
319
 * @param {Object} node - AST node which was being instrumented when error thrown
62✔
320
 * @param {Object} state - State object
62✔
321
 * @returns {undefined}
62✔
322
 * @throws {Error} - Error with augumented message
62✔
323
 */
62✔
324
function rethrowErrorWithLocation(err, node, state) {
×
325
        if (!err || !(err instanceof Error)) err = new Error('Unknown error');
✔
326

2✔
327
        let location;
2✔
328
        const {loc} = node;
2✔
329
        if (loc) {
2✔
330
                const [line, column] = loc.start ? [loc.start.line, loc.start.column + 1] : ['?', '?'];
2✔
331
                location = `${loc.filename || state.filename}:${line}:${column}`;
2✔
332
        } else {
×
333
                location = state.filename;
×
334
        }
2✔
335
        err.message = `Error instrumenting: ${location}\n${err.message}`;
2✔
336

2✔
337
        throw err;
2✔
338
}
2✔
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