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

overlookmotel / livepack / 7309490893

23 Dec 2023 05:09PM UTC coverage: 90.151% (-0.4%) from 90.525%
7309490893

push

github

overlookmotel
Rename top level `require` functions in CommonJS WIP

4693 of 5075 branches covered (0.0%)

Branch coverage included in aggregate %.

30 of 68 new or added lines in 4 files covered. (44.12%)

36 existing lines in 2 files now uncovered.

12479 of 13973 relevant lines covered (89.31%)

8656.4 hits per line

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

82.88
/lib/instrument/visitors/function.js
1
/* --------------------
62✔
2
 * livepack module
62✔
3
 * Code instrumentation visitor for functions
62✔
4
 * ------------------*/
62✔
5

62✔
6
'use strict';
62✔
7

62✔
8
// Export
62✔
9
module.exports = {
62✔
10
        ArrowFunctionExpression,
62✔
11
        FunctionDeclaration,
62✔
12
        FunctionExpression,
62✔
13
        createAndEnterFunctionOrClassNameBlock,
62✔
14
        visitFunctionOrMethod,
62✔
15
        visitFunctionParams,
62✔
16
        visitFunctionBody,
62✔
17
        removeUnnecessaryUseStrictDirectives,
62✔
18
        createFunction,
62✔
19
        getFunctionType,
62✔
20
        escapeFilename,
62✔
21
        withStrictModeState,
62✔
22
        hoistSloppyFunctionDeclarations,
62✔
23
        createTrackerCall,
62✔
24
        insertTrackerComment,
62✔
25
        createFunctionInfoFunction
62✔
26
};
62✔
27

62✔
28
// Modules
62✔
29
const t = require('@babel/types');
62✔
30

62✔
31
// Imports
62✔
32
const Statement = require('./statement.js'),
62✔
33
        Expression = require('./expression.js'),
62✔
34
        {IdentifierLet, AssigneeLet} = require('./assignee.js'),
62✔
35
        {visitKey, visitKeyContainer} = require('../visit.js'),
62✔
36
        {
62✔
37
                createBlock, createAndEnterBlock, createBinding, createThisBinding, createArgumentsBinding,
62✔
38
                createNewTargetBinding, getOrCreateExternalVar
62✔
39
        } = require('../blocks.js'),
62✔
40
        {insertTrackerCodeIntoFunction} = require('../tracking.js'),
62✔
41
        {createFnInfoVarNode, createTempVarNode, addToInternalVars} = require('../internalVars.js'),
62✔
42
        {insertComment, hasUseStrictDirective, stringLiteralWithSingleQuotes} = require('../utils.js'),
62✔
43
        {combineArraysWithDedup} = require('../../shared/functions.js'),
62✔
44
        {
62✔
45
                FN_TYPE_FUNCTION, FN_TYPE_ASYNC_FUNCTION, FN_TYPE_GENERATOR_FUNCTION,
62✔
46
                FN_TYPE_ASYNC_GENERATOR_FUNCTION, TRACKER_COMMENT_PREFIX,
62✔
47
                CONST_VIOLATION_NEEDS_VAR, CONST_VIOLATION_NEEDS_NO_VAR
62✔
48
        } = require('../../shared/constants.js');
62✔
49

62✔
50
// Exports
62✔
51

62✔
52
/**
62✔
53
 * Visitor for arrow function.
62✔
54
 * @param {Object} node - Arrow function AST node
62✔
55
 * @param {Object} state - State object
62✔
56
 * @param {Object|Array} parent - Parent AST node/container
62✔
57
 * @param {string|number} key - Node's key on parent AST node/container
62✔
58
 * @returns {undefined}
62✔
59
 */
62✔
60
function ArrowFunctionExpression(node, state, parent, key) {
9,382✔
61
        // Tracker comment is inserted later to avoid it getting relocated if function has complex params
9,382✔
62
        // which are later relocated to inside function body
9,382✔
63
        visitFunction(node, parent, key, undefined, false, node.body.type === 'BlockStatement', state);
9,382✔
64
}
4✔
65

4✔
66
/**
4✔
67
 * Visitor for function declaration.
4✔
68
 * @param {Object} node - Function declaration AST node
4✔
69
 * @param {Object} state - State object
4✔
70
 * @param {Object|Array} parent - Parent AST node/container
4✔
71
 * @param {string|number} key - Node's key on parent AST node/container
4✔
72
 * @returns {undefined}
4✔
73
 */
4✔
74
function FunctionDeclaration(node, state, parent, key) {
4,072✔
75
        // Visit function
4,072✔
76
        const fnName = node.id.name;
4,072✔
77
        const fn = visitFunction(node, parent, key, fnName, true, true, state);
4,072✔
78

4,072✔
79
        // Create binding for function name
4,072✔
80
        // TODO: Whether the value of the function is hoisted depends on whether is a top level statement
4,072✔
81
        // (including in a labeled statement)
4,072✔
82
        // `() => { console.log(typeof x); function x() {} }` -> Logs 'function'
4,072✔
83
        // `() => { console.log(typeof x); q: function x() {} }` -> Logs 'function'
4,072✔
84
        // `() => { console.log(typeof x); if (true) function x() {} }` -> Logs 'undefined'
8✔
85
        const {currentBlock: block, currentHoistBlock: hoistBlock} = state;
8✔
86
        let binding = block.bindings.get(fnName);
8✔
87
        if (!binding) {
✔
88
                const isTopLevel = block === hoistBlock;
×
89
                binding = createBinding(block, fnName, {isVar: isTopLevel, isFrozenName: true}, state);
8✔
90
                state.currentFunction?.reservedVarNames.add(fnName);
8✔
91

8✔
92
                // If is a sloppy-mode function declaration which may need to be hoisted, record that.
8✔
93
                // Determine whether to hoist after 1st pass is complete, once all other bindings created.
8✔
94
                if (!isTopLevel && !state.isStrict && !node.async && !node.generator) {
8✔
95
                        state.sloppyFunctionDeclarations.push({
8✔
96
                                varName: fnName,
8✔
97
                                binding,
8✔
98
                                block,
8✔
99
                                hoistBlock,
8✔
100
                                parentFn: state.currentFunction
8✔
101
                        });
8✔
102
                }
8✔
103
        } else if (!binding.isFrozenName) {
8✔
104
                // Existing binding is a `var` declaration.
8✔
105
                // Flag binding as frozen so `var` declarations' identifiers don't get renamed.
8✔
106
                binding.isFrozenName = true;
8✔
107
                binding.trails.length = 0;
8✔
108
        }
8✔
109

8✔
110
        // Insert tracker comment
8✔
111
        insertFunctionDeclarationOrExpressionTrackerComment(fn, node, state);
8✔
112

8✔
113
        // If top-level function declaration called 'require' in CommonJS, rename it in 2nd pass
8!
114
        if (
8✔
115
                fnName === 'require' && block === state.programBlock
8!
116
                && state.programBlock.parent.bindings.has('require')
8!
117
        ) state.secondPass(renameRequireFunctionDeclaration, node, state);
8!
118
}
×
119

8✔
UNCOV
120
/**
×
121
 * Visitor for function expression.
8!
UNCOV
122
 * @param {Object} node - Function expression AST node
×
UNCOV
123
 * @param {Object} state - State object
×
UNCOV
124
 * @param {Object|Array} parent - Parent AST node/container
×
125
 * @param {string|number} key - Node's key on parent AST node/container
8!
UNCOV
126
 * @returns {undefined}
×
127
 */
×
128
function FunctionExpression(node, state, parent, key) {
2,758✔
129
        // If not anonymous, create and enter name block
2,758✔
130
        let nameBlock, fnName;
2,758✔
131
        const idNode = node.id;
2,758✔
132
        if (idNode) {
2,758✔
133
                fnName = idNode.name;
810✔
134
                nameBlock = createAndEnterFunctionOrClassNameBlock(fnName, true, state);
810✔
135
        }
810✔
136

2,758✔
137
        // Visit function
2,758✔
138
        const fn = visitFunction(node, parent, key, fnName, true, true, state);
2,758✔
139

2,758✔
140
        if (nameBlock) {
2,758!
141
                // Exit name block
810✔
142
                state.currentBlock = nameBlock.parent;
810!
143

810✔
144
                // If contains `eval()`, change function ID to make name block be considered internal to function.
810!
145
                // An external var representing the function is pointless as its name will be frozen anyway
810✔
146
                // due to being in scope of the `eval()`.
810✔
147
                if (fn.containsEval) fn.id = nameBlock.id;
810✔
148
        }
810✔
149

2,758✔
150
        // Insert tracker comment
2,758✔
151
        insertFunctionDeclarationOrExpressionTrackerComment(fn, node, state);
2,758✔
152
}
2,758✔
153

2✔
154
/**
2✔
155
 * Create block for function/class name accessed within the function/class.
2✔
156
 * @param {string} fnName - Function name
2✔
157
 * @param {boolean} isSilentConst - `true` if assignment to function name silently fails in sloppy mode
2✔
158
 * @param {Object} state - State object
×
UNCOV
159
 * @returns {Object} - Block object
×
UNCOV
160
 */
×
161
function createAndEnterFunctionOrClassNameBlock(fnName, isSilentConst, state) {
1,628✔
162
        const block = createAndEnterBlock(fnName, false, state);
1,628✔
163
        createBinding(block, fnName, {isConst: true, isSilentConst, isFrozenName: true}, state);
1,628!
164
        state.currentFunction?.reservedVarNames.add(fnName);
1,628✔
165
        return block;
1,628✔
166
}
1,628✔
167

2✔
168
/**
2✔
169
 * Visit function declaration, function expression or arrow function.
2!
170
 * @param {Object} node - Function declaration, function expression or arrow function AST node
×
171
 * @param {Object|Array} parent - Parent AST node/container
×
172
 * @param {string|number} key - Node's key on parent AST node/container
×
173
 * @param {string} [fnName] - Function name (`undefined` if unnamed)
×
174
 * @param {boolean} isFullFunction - `true` if is not arrow function
2✔
175
 * @param {boolean} hasBodyBlock - `true` if has body block (only arrow functions can not)
62✔
176
 * @param {Object} state - State object
62✔
177
 * @returns {Object} - Function object
62✔
178
 */
62✔
179
function visitFunction(node, parent, key, fnName, isFullFunction, hasBodyBlock, state) {
16,212✔
180
        return withStrictModeState(
16,212✔
181
                node, hasBodyBlock, state,
16,212✔
182
                (isStrict, isEnteringStrict) => visitFunctionOrMethod(
16,212✔
183
                        node, parent, key, fnName, isStrict, isEnteringStrict, isFullFunction, hasBodyBlock, state
16,212✔
184
                )
10✔
185
        );
10✔
186
}
10✔
187

10✔
188
/**
10✔
189
 * Visit function or method.
10✔
190
 * @param {Object} fnNode - Function declaration, function expression, arrow function,
10✔
191
 *   class method, class private method or object method AST node
10✔
192
 * @param {Object|Array} parent - Parent AST node/container
10✔
193
 * @param {string|number} key - Node's key on parent AST node/container
10✔
194
 * @param {string} [fnName] - Function name (`undefined` if unnamed)
10!
195
 * @param {boolean} isStrict - `true` if function is strict mode
62✔
196
 * @param {boolean} isEnteringStrict - `true` if function is strict mode and parent is sloppy mode
62✔
197
 * @param {boolean} isFullFunction - `true` if is not arrow function
62✔
198
 * @param {boolean} hasBodyBlock - `true` if has body block (only arrow functions can not)
62✔
199
 * @param {Object} state - State object
62✔
200
 * @returns {Object} - Function object
62✔
201
 */
62✔
202
function visitFunctionOrMethod(
25,460✔
203
        fnNode, parent, key, fnName, isStrict, isEnteringStrict, isFullFunction, hasBodyBlock, state
25,460✔
204
) {
25,460✔
205
        // Create params block. If is full function, create bindings for `this` + `new.target`.
25,460✔
206
        const paramsBlock = createAndEnterBlock(fnName, false, state);
25,460✔
207
        let parentThisBlock;
14✔
208
        if (isFullFunction) {
14✔
209
                createThisBinding(paramsBlock);
14✔
210
                createNewTargetBinding(paramsBlock);
14✔
211
                parentThisBlock = state.currentThisBlock;
14✔
212
                state.currentThisBlock = paramsBlock;
14✔
213
        }
×
214

×
UNCOV
215
        // Create and enter function
×
216
        const fn = createFunction(paramsBlock.id, fnNode, isStrict, state);
14✔
217
        state.currentFunction = fn;
14✔
218
        const parentTrail = state.trail;
14✔
219
        state.trail = [];
14✔
220

14✔
221
        // Create body block or make params block a vars block
14✔
222
        let bodyBlock;
14✔
223
        if (hasBodyBlock) {
14✔
224
                bodyBlock = createBlock(fnName, true, state);
14✔
225
                paramsBlock.varsBlock = bodyBlock;
14✔
226
        } else {
✔
227
                paramsBlock.varsBlock = paramsBlock;
×
228
        }
×
229

×
230
        // Visit params
×
231
        const argNames = visitFunctionParams(fn, fnNode, paramsBlock, bodyBlock, isStrict, state);
14✔
232

14✔
233
        // If is full function, create binding for `arguments` in params block.
14✔
234
        // Param called `arguments` takes precedence.
25,460✔
235
        if (isFullFunction && !paramsBlock.bindings.has('arguments')) {
25,460✔
236
                createArgumentsBinding(paramsBlock, isStrict, argNames);
16,074✔
237
        }
16,074✔
238

25,460✔
239
        // Visit body
25,460✔
240
        if (bodyBlock) {
25,460✔
241
                visitFunctionBody(fnNode, bodyBlock, state);
19,782✔
242
        } else {
25,460✔
243
                visitKey(fnNode, 'body', Expression, state);
5,678✔
244
        }
5,678✔
245

25,460✔
246
        // Serialize AST
25,460✔
247
        fn.astJson = serializeFunctionAst(fnNode, hasBodyBlock, isStrict, isEnteringStrict);
25,460✔
248

25,460✔
249
        // Exit function
25,460✔
250
        state.currentBlock = paramsBlock.parent;
25,460✔
251
        if (isFullFunction) state.currentThisBlock = parentThisBlock;
25,460✔
252
        state.currentFunction = fn.parent;
25,460✔
253
        state.trail = parentTrail;
25,460✔
254

25,460✔
255
        // Temporarily remove from AST so is not included in parent function's serialized AST
25,460✔
256
        parent[key] = null;
25,460✔
257

25,460✔
258
        // Queue instrumentation of function in 2nd pass
25,460✔
259
        state.secondPass(instrumentFunction, fnNode, fn, parent, key, paramsBlock, bodyBlock, state);
25,460✔
260

26✔
261
        return fn;
26✔
262
}
26✔
263

26✔
264
/**
26✔
265
 * Visit function params.
26✔
266
 * Caller must ensure `state.currentBlock` is params block,
26✔
267
 * and `state.currentThisBlock` is params block if is a full function.
26✔
268
 * Caller must set `state.currentBlock` and `state.currentThisBlock` back afterwards.
26✔
269
 * @param {Object} fn - Function object
×
270
 * @param {Object} fnNode - Function/method AST node
×
271
 * @param {Object} paramsBlock - Block object for function params
×
272
 * @param {Object} [bodyBlock] - Block object for body block, if function's body is a statement block
26✔
273
 * @param {boolean} isStrict - `true` if function is strict mode
26✔
274
 * @param {Object} state - State object
26✔
275
 * @returns {Array<string>} - Array of argument names which are linked to argument vars.
26✔
276
 *   Will be empty array if strict mode, or arguments are not all simple identifiers.
22✔
277
 */
22✔
278
function visitFunctionParams(fn, fnNode, paramsBlock, bodyBlock, isStrict, state) {
25,886✔
279
        // Visit params and find first complex param (i.e. param which is not just an identifier).
25,886✔
280
        // Do not consider `...x` to be complex.
25,886✔
281
        // Also get array of params which are linked to `arguments`.
25,886✔
282
        // i.e. `((x) => { arguments[0] = 2; return x; })(1) === 2`
25,886✔
283
        // `arguments` is only linked in sloppy mode functions with no complex params.
25,886✔
284
        const argNames = [];
25,886✔
285
        let hasLinkedArguments = !isStrict,
25,886✔
286
                firstComplexParamIndex;
25,886✔
287
        visitKeyContainer(fnNode, 'params', (paramNode, _state, paramNodes, index) => {
25,886✔
288
                if (paramNode.type === 'Identifier') {
21,542✔
289
                        if (hasLinkedArguments) argNames.push(paramNode.name);
19,270✔
290
                        IdentifierLet(paramNode, state, paramNodes, index);
19,270✔
291
                } else {
21,542✔
292
                        if (firstComplexParamIndex === undefined) {
2,272✔
293
                                hasLinkedArguments = false;
2,074✔
294
                                argNames.length = 0;
2,074✔
295
                                if (paramNode.type !== 'RestElement' || paramNode.argument.type !== 'Identifier') {
2,074✔
296
                                        firstComplexParamIndex = index;
1,938✔
297

1,938✔
298
                                        // Make params block the vars block. Tracker call and scope ID var will go in params.
1,938✔
299
                                        if (bodyBlock) paramsBlock.varsBlock = bodyBlock.varsBlock = paramsBlock;
1,938✔
300
                                }
1,938✔
301
                        }
2,074✔
302

2,272✔
303
                        AssigneeLet(paramNode, state, paramNodes, index);
2,272✔
304
                }
2,272✔
305
        }, state);
25,886✔
306

25,886✔
307
        // Record index of first complex param
25,886✔
308
        fn.firstComplexParamIndex = firstComplexParamIndex;
25,886✔
309

25,886✔
310
        // Return array of params which are linked to `arguments`
25,886✔
311
        return argNames;
25,886✔
312
}
25,886✔
313

26✔
314
/**
26✔
315
 * Visit function body of a function with a statement block as body.
26✔
316
 * Creates and enters a block for function body.
26✔
317
 * Caller must set `state.currentBlock` back afterwards.
62✔
318
 * @param {Object} fnNode - Function/method AST node
62✔
319
 * @param {Object} bodyBlock - Block object for body block
62✔
320
 * @param {Object} state - State object
62✔
321
 * @returns {undefined}
62✔
322
 */
62✔
323
function visitFunctionBody(fnNode, bodyBlock, state) {
20,208✔
324
        state.currentBlock = bodyBlock;
20,208✔
325
        const parentHoistBlock = state.currentHoistBlock;
20,208✔
326
        state.currentHoistBlock = bodyBlock;
20,208✔
327
        visitKey(fnNode, 'body', FunctionBodyBlock, state);
20,208✔
328
        state.currentHoistBlock = parentHoistBlock;
20,208✔
329
}
20,208✔
330

62✔
331
/**
62✔
332
 * Visitor for function body block.
62✔
333
 * @param {Object} node - Function body block AST node
62✔
334
 * @param {Object} state - State object
62✔
335
 * @returns {undefined}
62✔
336
 */
62✔
337
function FunctionBodyBlock(node, state) {
20,208✔
338
        visitKeyContainer(node, 'body', Statement, state);
20,208✔
339
}
20,208✔
340

62✔
341
/**
62✔
342
 * Serialize function AST to JSON.
62✔
343
 * Remove unnecessary 'use strict' directives if present.
28✔
344
 * Do not mutate the node passed in, to ensure these changes only affect the serialized AST,
28✔
345
 * and not the instrumented output.
28✔
346
 * @param {Object} fnNode - Function AST node
28✔
347
 * @param {boolean} hasBodyBlock - `true` if has body block (only arrow functions can not)
28✔
348
 * @param {boolean} isStrict - `true` if function is strict mode
28✔
349
 * @param {boolean} isEnteringStrict - `true` if function transitions into strict mode
28✔
350
 *   (and therefore needs to retain a 'use strict' directive)
28✔
351
 * @returns {string} - Function AST as JSON
28✔
352
 */
28✔
353
function serializeFunctionAst(fnNode, hasBodyBlock, isStrict, isEnteringStrict) {
25,460✔
354
        // Remove unnecessary 'use strict' directives
25,460✔
355
        if (hasBodyBlock && isStrict) {
25,460✔
356
                const alteredFnNode = removeUnnecessaryUseStrictDirectives(fnNode, !isEnteringStrict);
16,912✔
357
                if (alteredFnNode) fnNode = alteredFnNode;
8✔
358
        }
8✔
359

8✔
360
        // Stringify AST to JSON
8✔
361
        return JSON.stringify(fnNode);
8✔
362
}
8✔
363

8✔
364
/**
8✔
365
 * Remove unnecessary 'use strict' directives from function body.
8✔
366
 * Relocate any comments from deleted directives.
×
367
 * Do not mutate input - clone if needs alteration.
×
UNCOV
368
 * Return `undefined` if no alteration necessary.
×
369
 * @param {Object} fnNode - Function AST node
8✔
370
 * @param {boolean} needsNoDirective - `true` if function already strict mode from outer environment
8!
371
 * @returns {Object|undefined} - Altered function AST node, or `undefined` if no directives removed
8✔
372
 */
2✔
373
function removeUnnecessaryUseStrictDirectives(fnNode, needsNoDirective) {
17,338✔
374
        // If no directives, return `undefined`
17,338!
375
        let bodyNode = fnNode.body;
17,338✔
376
        const directiveNodes = bodyNode.directives;
17,338✔
377
        if (directiveNodes.length === 0) return undefined;
17,338✔
378

286✔
379
        // Remove unnecessary directives
286✔
380
        const newDirectiveNodes = [],
286✔
381
                commentNodes = [];
286✔
382
        for (let directiveNode of directiveNodes) {
5,068✔
383
                if (directiveNode.value.value === 'use strict') {
322✔
384
                        if (needsNoDirective) {
250✔
385
                                commentNodes.push(
22✔
386
                                        ...(directiveNode.leadingComments || []),
22✔
387
                                        ...(directiveNode.trailingComments || [])
22✔
388
                                );
22✔
389
                                continue;
22✔
390
                        }
22✔
391
                        needsNoDirective = true;
228✔
392
                }
228✔
393

300✔
394
                // Add any comments removed from previous directives
300✔
395
                if (commentNodes.length !== 0) {
322!
396
                        directiveNode = {
×
397
                                ...directiveNode,
×
398
                                leadingComments: combineArraysWithDedup(commentNodes, directiveNode.leadingComments)
×
399
                        };
×
400
                        commentNodes.length = 0;
×
401
                }
×
402

300✔
403
                newDirectiveNodes.push(directiveNode);
300✔
404
        }
300✔
405

286✔
406
        // If no directives need to be removed, return `undefined`
286✔
407
        if (newDirectiveNodes.length === directiveNodes.length) return undefined;
412✔
408

24✔
409
        // Clone function node
24✔
410
        bodyNode = {...bodyNode, directives: newDirectiveNodes};
24✔
411
        fnNode = {...fnNode, body: bodyNode};
24✔
412

×
413
        // Add any remaining comments to first statement / last directive / function body
24✔
414
        if (commentNodes.length !== 0) {
24!
415
                let statementNodes = bodyNode.body;
24✔
416
                if (statementNodes.length !== 0) {
24✔
417
                        statementNodes = bodyNode.body = [...statementNodes];
24✔
418
                        const firstStatementNode = statementNodes[0];
×
419
                        statementNodes[0] = {
×
420
                                ...firstStatementNode,
×
421
                                leadingComments: combineArraysWithDedup(commentNodes, firstStatementNode.leadingComments)
×
UNCOV
422
                        };
✔
423
                } else if (newDirectiveNodes.length !== 0) {
24✔
424
                        const lastDirectiveNode = newDirectiveNodes[newDirectiveNodes.length - 1];
24✔
425
                        newDirectiveNodes[newDirectiveNodes.length - 1] = {
24✔
UNCOV
426
                                ...lastDirectiveNode,
×
427
                                trailingComments: combineArraysWithDedup(lastDirectiveNode.trailingComments, commentNodes)
×
428
                        };
×
429
                } else {
×
430
                        bodyNode.innerComments = combineArraysWithDedup(bodyNode.innerComments, commentNodes);
×
431
                }
×
432
        }
×
433

22✔
434
        // Return clone of function node
22✔
435
        return fnNode;
22✔
436
}
17,338✔
437

62✔
438
/**
62✔
439
 * Create object representing function.
62✔
440
 * @param {number} id - Function ID
62✔
441
 * @param {Object} node - Function/method/class AST node
62✔
442
 * @param {boolean} isStrict - `true` if function is strict mode
62✔
443
 * @param {Object} state - State object
62✔
444
 * @returns {Object} - Function object
62✔
445
 */
62✔
446
function createFunction(id, node, isStrict, state) {
26,462✔
447
        const parentFunction = state.currentFunction;
26,462✔
448
        const fn = {
26,462✔
449
                id,
26,462✔
450
                node,
26,462✔
451
                astJson: undefined,
26,462✔
452
                isStrict,
26,462✔
453
                parent: parentFunction,
26✔
454
                children: [],
26✔
455
                trail: parentFunction ? [...state.trail] : undefined,
26✔
456
                bindings: [],
26✔
457
                externalVars: new Map(), // Keyed by block
26✔
458
                scopes: undefined,
26✔
459
                globalVarNames: new Set(),
26✔
460
                reservedVarNames: new Set(),
26✔
UNCOV
461
                amendments: [],
×
UNCOV
462
                superIsProto: false,
×
463
                containsEval: false,
26✔
464
                containsImport: false,
26✔
465
                hasSuperClass: false,
26✔
466
                firstComplexParamIndex: undefined
26✔
467
        };
22✔
468

22✔
469
        if (parentFunction) parentFunction.children.push(fn);
22✔
470

22✔
471
        state.functions.push(fn);
22✔
472
        state.fileContainsFunctionsOrEval = true;
26!
473

26✔
474
        return fn;
26✔
475
}
26✔
476

26✔
477
/**
26✔
478
 * Get type code for function.
26✔
479
 * @param {Object} fnNode - Function AST node
62✔
480
 * @returns {string} - Function type code
62✔
481
 */
62✔
482
function getFunctionType(fnNode) {
16,078✔
483
        return fnNode.async
16,078✔
484
                ? fnNode.generator
16,078✔
485
                        ? FN_TYPE_ASYNC_GENERATOR_FUNCTION
180✔
486
                        : FN_TYPE_ASYNC_FUNCTION
180✔
487
                : fnNode.generator
16,078✔
488
                        ? FN_TYPE_GENERATOR_FUNCTION
15,898✔
489
                        : FN_TYPE_FUNCTION;
16,078✔
490
}
16,078✔
491

62✔
492
/**
62✔
493
 * Escape filename so it can be safely included in tracker comment.
62✔
494
 * @param {string} filename - File path
62✔
495
 * @returns {string} - Filename escaped
62✔
496
 */
62✔
497
function escapeFilename(filename) {
4,208✔
498
        // Encode filename as JSON to escape non-ascii chars.
24✔
499
        // Convert `*/` to `*\/` so does not terminate comment early.
24✔
500
        // `JSON.parse('"*\/"')` is `'*/'` so it needs no special unescaping.
24✔
501
        return JSON.stringify(filename).slice(1, -1).replace(/\*\//g, '*\\/');
24✔
502
}
24✔
503

24✔
504
/**
24✔
505
 * Determine if function is strict mode. If it is, set `state.isStrict` flag.
24✔
506
 * Call visitor with strict/sloppy mode flags.
24✔
507
 * @param {Object} fnNode - Function or method AST node
24!
508
 * @param {boolean} hasBodyBlock - `true` if has body block (only arrow functions can not)
×
509
 * @param {Object} state - State object
×
510
 * @param {Function} visit - Visitor function
×
511
 * @returns {*} - Visitor function's return value
×
512
 */
×
513
function withStrictModeState(fnNode, hasBodyBlock, state, visit) {
21,208✔
514
        // Get if strict mode
21,208✔
515
        let {isStrict} = state,
21,208✔
516
                isEnteringStrict = false;
21,208✔
517
        if (!isStrict && hasBodyBlock && hasUseStrictDirective(fnNode.body)) {
21,208✔
518
                isStrict = isEnteringStrict = state.isStrict = true;
228✔
519
        }
228✔
520

21,208✔
521
        // Call visitor
21,208✔
522
        const res = visit(isStrict, isEnteringStrict);
21,208✔
523

21,208✔
524
        // If entered strict mode, exit it again
21,208✔
525
        if (isEnteringStrict) state.isStrict = false;
21,208✔
526

21,208✔
527
        // Return visitor's return value
21,208✔
528
        return res;
21,208✔
529
}
21,208✔
530

×
531
/**
×
532
 * Hoist sloppy function declarations if they can be hoisted.
×
533
 * Function declarations are hoisted to the top block in parent function body or program
×
534
 * if they are defined in sloppy mode (i.e. outside environment is sloppy mode, regardless of
×
535
 * whether function itself strict mode) and they are not async or generator functions.
×
536
 *
×
537
 * They can only be hoisted if:
×
538
 *   1. No `const`, `let` or class declaration with same name in block they'd be hoisted to.
×
539
 *   2. No parameter in parent function with same name.
×
540
 *   3. No other bindings in intermediate blocks
×
541
 *      (including other function declarations which are themselves hoisted)
×
542
 *
×
543
 * When a function declaration is hoisted, it produces two separate bindings:
×
544
 *   1. Binding in block where originally defined.
×
545
 *   2. Binding in block it's hoisted to.
×
546
 *
×
547
 * See https://github.com/babel/babel/pull/14203#issuecomment-1038187168
×
548
 *
×
549
 * @param {Object} state - State object
×
550
 * @returns {undefined}
×
551
 */
×
552
function hoistSloppyFunctionDeclarations(state) {
3,168✔
553
        for (const declaration of state.sloppyFunctionDeclarations) {
3,168✔
554
                hoistSloppyFunctionDeclaration(declaration);
16✔
555
        }
16✔
556
}
3,168✔
557

×
558
/**
×
559
 * Hoist function declaration, if it can be hoisted.
×
UNCOV
560
 * Should only be called for functions which:
×
561
 *   1. are not already in the top block in parent function / program
62✔
562
 *   2. are not async or generator functions
62✔
563
 *
62✔
564
 * @param {Object} declaration - Declaration object
62✔
565
 * @param {string} declaration.varName - Function name
62✔
566
 * @param {Object} declaration.binding - Binding object for binding in block where originally declared
62✔
567
 * @param {Object} declaration.block - Block object for block where originally declared
36✔
568
 * @param {Object} declaration.hoistBlock - Block object for block where could be hoisted to
36✔
569
 * @param {Object} [declaration.parentFn] - Function object for parent function
36✔
570
 * @returns {undefined}
36✔
571
 */
36✔
572
function hoistSloppyFunctionDeclaration(declaration) {
16✔
573
        const {varName, hoistBlock} = declaration;
16!
574

16✔
575
        // Do not hoist if existing const, let or class declaration binding in hoist block.
16✔
576
        // Also exit if binding exists and already has frozen name. In that case, function may or may not be
16✔
577
        // hoisted, but it doesn't matter either way, as only implication of hoisting is to freeze var name.
16✔
578
        const hoistBlockBinding = hoistBlock.bindings.get(varName);
16!
579
        if (hoistBlockBinding && (!hoistBlockBinding.isVar || hoistBlockBinding.isFrozenName)) return;
16!
580

16✔
581
        // If parent function's params include var with same name, do not hoist.
16✔
582
        // NB: `hoistBlock.parent` here is either parent function params block or file block.
16✔
583
        // In CommonJS code, file block is CommonJS wrapper function's params, and in any other context
22✔
584
        // file block contains no bindings except `this`. So it's safe to treat as a params block in all cases.
22✔
585
        // NB: `paramsBlockBinding.argNames` check is to avoid the pseudo-param `arguments` blocking hoisting.
22✔
UNCOV
586
        // An actual param called `arguments` *does* prevent hoisting.
×
587
        // e.g. `function f(arguments) { { function arguments() {} } return arguments; }`
22✔
588
        // returns the value outer function is called with, not the inner function.
16✔
589
        const paramsBlockBinding = hoistBlock.parent.bindings.get(varName);
16✔
590
        if (paramsBlockBinding && !paramsBlockBinding.argNames) return;
16!
591

4✔
592
        // If any binding in intermediate blocks, do not hoist.
4✔
593
        // NB: This includes function declarations which will be themselves hoisted.
4✔
594
        let inBetweenBlock = declaration.block.parent;
4✔
595
        while (inBetweenBlock !== hoistBlock) {
4!
596
                if (inBetweenBlock.bindings.has(varName)) return;
4✔
597
                inBetweenBlock = inBetweenBlock.parent;
4✔
598
        }
4✔
599

4✔
600
        // Can be hoisted
4✔
601
        if (!hoistBlockBinding) {
4!
602
                hoistBlock.bindings.set(varName, {...declaration.binding, isVar: true});
×
603
        } else {
16✔
604
                // Existing binding is a `var` declaration.
16✔
605
                // Flag binding as frozen so `var` declarations' identifiers don't get renamed.
16✔
606
                hoistBlockBinding.isFrozenName = true;
16✔
607
                hoistBlockBinding.trails.length = 0;
16✔
608
        }
16✔
609
}
16✔
610

62✔
611
/**
62✔
612
 * Instrument function.
62✔
613
 * @param {Object} node - Function or method AST node
62✔
614
 * @param {Object} fn - Function object
62✔
615
 * @param {Object|Array} parent - Parent AST node/container
62✔
616
 * @param {string|number} key - Node's key on parent AST node/container
16✔
617
 * @param {Object} paramsBlock - Function's params block object
16✔
618
 * @param {Object} [bodyBlock] - Function's body block object (if it has one, arrow functions may not)
16✔
619
 * @param {Object} state - State object
16✔
620
 * @returns {undefined}
16✔
621
 */
16✔
622
function instrumentFunction(node, fn, parent, key, paramsBlock, bodyBlock, state) {
25,460!
623
        // Restore to original place in AST
25,460✔
624
        parent[key] = node;
25,460✔
625

25,460✔
626
        // Insert tracker call and block vars (`livepack_scopeId` and `livepack_temp`) into function.
25,460✔
627
        // NB: `bodyBlock` here may actually be params block if is an arrow function with no body.
25,460!
628
        const trackerNode = createTrackerCall(fn, state);
25,460✔
629
        insertTrackerCodeIntoFunction(fn, node, paramsBlock, bodyBlock, trackerNode, state);
25,460✔
630

25,460✔
631
        if (node.type === 'ArrowFunctionExpression') insertArrowFunctionTrackerComment(fn, node, state);
25,460✔
632
}
25,460✔
633

62✔
634
/**
62✔
635
 * Create tracker call.
62✔
636
 * Create scopes array, and copy external vars down to parent function.
62✔
637
 * @param {Object} fn - Function object
62✔
638
 * @param {Object} state - State object
62✔
639
 * @returns {Object} - AST node for tracker call
62✔
640
 */
62✔
641
function createTrackerCall(fn, state) {
26,462✔
642
        // Sort scopes in ascending order of block ID
26,462✔
643
        const scopes = [...fn.externalVars].map(([block, vars]) => ({block, vars}))
26,462✔
644
                .sort((scope1, scope2) => (scope1.block.id > scope2.block.id ? 1 : -1));
26,462✔
645
        fn.scopes = scopes;
26,462✔
646

26,462✔
647
        // Add external vars to parent function where external to that function too
26,462✔
648
        const parentFunction = fn.parent;
26,462✔
649
        if (parentFunction) {
26,462✔
650
                // External vars
17,180✔
651
                for (const {block, vars} of scopes) {
17,180✔
652
                        if (block.id >= parentFunction.id) break;
11,608✔
653
                        for (const [varName, {binding}] of vars) {
11,582✔
654
                                getOrCreateExternalVar(parentFunction, block, varName, binding);
2✔
655
                        }
2✔
656
                }
2✔
657
        }
2✔
658

2✔
659
        // `livepack_tracker(livepack_getFnInfo_3, () => [[livepack_scopeId_2, x]])`
2✔
UNCOV
660
        return t.callExpression(state.trackerVarNode, [
×
661
                createFnInfoVarNode(fn.id, state),
2✔
662
                t.arrowFunctionExpression([], t.arrayExpression(
2!
663
                        scopes.map(scope => t.arrayExpression([
26,462✔
664
                                scope.block.varsBlock.scopeIdVarNode,
19,126✔
665
                                ...[...scope.vars.values()].map(blockVar => blockVar.binding.varNode)
19,126✔
666
                        ]))
26,462✔
667
                ))
26,462✔
668
        ]);
26,462✔
669
}
26,462✔
670

62✔
671
/**
62✔
672
 * Insert tracker comment into function declaration/expression.
62✔
673
 * @param {Object} fn - Function object
62✔
674
 * @param {Object} node - Function declaration or expression AST node
62✔
675
 * @param {Object} state - State object
62✔
676
 * @returns {undefined}
62✔
677
 */
62✔
678
function insertFunctionDeclarationOrExpressionTrackerComment(fn, node, state) {
6,830✔
679
        // Insert tracker comment before identifier, or before 1st param or before function body
6,830✔
680
        const commentHolderNode = node.id || (node.params.length !== 0 ? node.params[0] : node.body);
6,830✔
681
        insertTrackerComment(fn.id, getFunctionType(node), commentHolderNode, 'leading', state);
6,830✔
682
}
×
683

×
684
/**
×
685
 * Insert tracker comment into arrow function.
×
686
 * @param {Object} fn - Function object
×
687
 * @param {Object} node - Arrow function AST node
×
688
 * @param {Object} state - State object
×
689
 * @returns {undefined}
×
690
 */
×
691
function insertArrowFunctionTrackerComment(fn, node, state) {
9,382✔
692
        // Insert tracker comment before first param. If no params, before function body.
9,382✔
693
        const paramNodes = node.params;
9,382✔
694
        insertTrackerComment(
9,382✔
695
                fn.id, node.async ? FN_TYPE_ASYNC_FUNCTION : FN_TYPE_FUNCTION,
9,382✔
696
                paramNodes.length !== 0 ? paramNodes[0] : node.body, 'leading', state
9,382✔
697
        );
9,382✔
698
}
9,382✔
699

×
NEW
700
/**
×
NEW
701
 * Rename top-level function declaration in CommonJS file called `require` to a temp name.
×
NEW
702
 * Otherwise, this prevents loading Livepack's `init` code.
×
NEW
703
 * At end of instrumentation, a `var require =` statement will be added at top of file
×
NEW
704
 * to restore the binding.
×
NEW
705
 * @param {Object} fnNode - Function declaration AST node
×
NEW
706
 * @param {Object} state - State object
×
NEW
707
 * @returns {undefined}
×
NEW
708
 */
×
NEW
709
function renameRequireFunctionDeclaration(fnNode, state) {
×
NEW
710
        // Use same temp var name for all `require` functions in this file
×
NEW
711
        const tempVarNode = state.requireAliasTempVarNode
×
NEW
712
                || (state.requireAliasTempVarNode = createTempVarNode(state));
×
NEW
713

×
NEW
714
        // Set `name` of existing identifier, rather than replacing with `tempVarNode`
×
NEW
715
        // to preserve any attached comments (including tracker comment)
×
NEW
716
        fnNode.id.name = tempVarNode.name;
×
NEW
717
        addToInternalVars(fnNode.id, state);
×
NEW
718
}
×
NEW
719

×
720
/**
×
721
 * Insert tracker comment.
×
722
 * @param {number} fnId - Function ID
×
723
 * @param {string} fnType - Function type
×
724
 * @param {Object} commentHolderNode - AST node to attach comment to
×
725
 * @param {string} commentType - 'leading' / 'inner' / 'trailing'
×
726
 * @param {Object} state - State object
×
727
 * @returns {undefined}
×
728
 */
×
729
function insertTrackerComment(fnId, fnType, commentHolderNode, commentType, state) {
26,462✔
730
        insertComment(
26,462✔
731
                commentHolderNode, commentType,
26,462✔
732
                `${TRACKER_COMMENT_PREFIX}${fnId};${fnType};${state.filenameEscaped}`
26,462✔
733
        );
26,462✔
734
}
26,462✔
735

×
736
/**
×
737
 * Create function info function containing function AST, details of internal and external vars etc.
×
738
 * @param {Object} fn - Function object
×
739
 * @param {Object} state - State object
×
740
 * @returns {Object} - AST node for function info function
×
741
 */
×
742
function createFunctionInfoFunction(fn, state) {
26,462✔
743
        // Compile internal vars and reserved var names
26,462✔
744
        const internalVars = new Map(),
26,462✔
745
                {reservedVarNames} = fn;
26,462✔
746
        for (const {name: varName, trails, isFrozenName} of fn.bindings) {
26,462✔
747
                if (isFrozenName) {
43,504✔
748
                        reservedVarNames.add(varName);
1,596✔
749
                } else {
43,504✔
750
                        const internalVar = internalVars.get(varName);
41,908✔
751
                        if (internalVar) {
41,908✔
752
                                internalVar.push(...trails);
516✔
753
                        } else {
41,908✔
754
                                internalVars.set(varName, [...trails]);
41,392✔
755
                        }
41,392✔
756
                }
41,908✔
757
        }
43,504✔
758

26,462✔
759
        // Compile amendments.
26,462✔
760
        // Reverse order so deepest are first.
26,462✔
761
        // Remove the need for an internal var if binding has frozen name.
26✔
762
        let amendments;
26✔
763
        if (fn.amendments.length > 0) {
26✔
764
                amendments = fn.amendments.map(({type, blockId, trail, binding}) => {
26✔
UNCOV
765
                        if (type === CONST_VIOLATION_NEEDS_VAR && binding.isFrozenName) {
×
766
                                type = CONST_VIOLATION_NEEDS_NO_VAR;
×
767
                        }
×
768
                        return [type, blockId, ...trail];
1,372✔
769
                }).reverse();
26✔
770
        }
26✔
771

26✔
772
        // Create JSON function info string
26✔
773
        const {children} = fn;
26✔
774
        let argNames;
26✔
775
        let json = JSON.stringify({
26✔
776
                scopes: fn.scopes.map(({block, vars}) => ({
26✔
777
                        blockId: block.id,
19,126✔
778
                        blockName: block.name,
19,126✔
779
                        vars: Object.fromEntries([...vars].map(([varName, varProps]) => {
19,126✔
780
                                if (varName === 'arguments') argNames = varProps.binding.argNames;
33,068✔
781
                                return [
33,068✔
782
                                        varName,
33,068✔
783
                                        {
33,068✔
784
                                                isReadFrom: varProps.isReadFrom || undefined,
33,068✔
785
                                                isAssignedTo: varProps.isAssignedTo || undefined,
33,068✔
786
                                                isFrozenInternalName: varProps.binding.isFrozenName || undefined,
33,068✔
787
                                                trails: varProps.trails
33,068✔
788
                                        }
33,068✔
789
                                ];
33,068✔
790
                        }))
19,126✔
791
                })),
26,462✔
792
                isStrict: fn.isStrict || undefined,
26,462✔
793
                superIsProto: fn.superIsProto || undefined,
26,462✔
794
                containsEval: fn.containsEval || undefined,
36✔
795
                containsImport: fn.containsImport || undefined,
36✔
796
                argNames,
36✔
797
                internalVars: Object.fromEntries(internalVars),
36✔
798
                reservedVarNames: reservedVarNames.size !== 0 ? [...reservedVarNames] : undefined,
36✔
799
                globalVarNames: fn.globalVarNames.size !== 0 ? [...fn.globalVarNames] : undefined,
36✔
800
                amendments,
4✔
801
                hasSuperClass: fn.hasSuperClass || undefined,
4✔
802
                childFns: children.map(childFn => childFn.trail)
4✔
803
        });
4✔
804

4✔
805
        // Add AST JSON to JSON
4✔
806
        json = `${json.slice(0, -1)},"ast":${fn.astJson}}`;
4✔
807

4✔
808
        // Create function returning JSON string and references to function info function for child fns.
36✔
UNCOV
809
        // Make output as single-quoted string for shorter output (not escaping every `"`).
×
810
        return t.functionDeclaration(createFnInfoVarNode(fn.id, state), [], t.blockStatement([
36✔
811
                t.returnStatement(t.arrayExpression([
36✔
812
                        stringLiteralWithSingleQuotes(json),
36✔
813
                        t.arrayExpression(children.map(childFn => createFnInfoVarNode(childFn.id, state))),
36!
UNCOV
814
                        state.getSourcesNode
×
815
                ]))
×
816
        ]));
×
817
}
×
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