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

overlookmotel / livepack / 7073617108

02 Dec 2023 05:32PM UTC coverage: 90.271% (-0.1%) from 90.367%
7073617108

push

github

overlookmotel
Don't transpile `super()` in class constructors [fix]

4630 of 4985 branches covered (0.0%)

Branch coverage included in aggregate %.

58 of 64 new or added lines in 8 files covered. (90.63%)

80 existing lines in 8 files now uncovered.

12480 of 13969 relevant lines covered (89.34%)

13035.86 hits per line

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

85.25
/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
        instrumentFunctionOrClassConstructor,
62✔
24
        insertTrackerComment,
62✔
25
        createFunctionInfoFunction
62✔
26
};
62✔
27

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

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

62✔
51
// Exports
62✔
52

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

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

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

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

8✔
111
        // Insert tracker comment
8✔
112
        insertFunctionDeclarationOrExpressionTrackerComment(fn, node, state);
8!
113
}
×
114

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

3,422✔
132
        // Visit function
3,422✔
133
        const fn = visitFunction(node, parent, key, fnName, true, true, state);
3,422✔
134

3,422✔
135
        if (nameBlock) {
3,422✔
136
                // Exit name block
850✔
137
                state.currentBlock = nameBlock.parent;
850✔
138

850✔
139
                // If contains `eval()`, change function ID to make name block be considered internal to function.
850✔
140
                // An external var representing the function is pointless as its name will be frozen anyway
850✔
141
                // due to being in scope of the `eval()`.
850✔
142
                if (fn.containsEval) fn.id = nameBlock.id;
850✔
143
        }
850✔
144

3,422✔
145
        // Insert tracker comment
3,422✔
146
        insertFunctionDeclarationOrExpressionTrackerComment(fn, node, state);
3,422✔
147
}
2✔
148

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

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

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

×
210
        // Create and enter function
14✔
211
        const fn = createFunction(paramsBlock.id, fnNode, isStrict, state);
14✔
212
        state.currentFunction = fn;
14✔
213
        const parentTrail = state.trail;
14✔
214
        state.trail = [];
14✔
215

14✔
216
        // Create body block or make params block a vars block
14✔
217
        let bodyBlock;
14✔
218
        if (hasBodyBlock) {
14✔
219
                bodyBlock = createBlock(fnName, true, state);
14✔
220
                paramsBlock.varsBlock = bodyBlock;
×
221
        } else {
✔
222
                paramsBlock.varsBlock = paramsBlock;
×
223
        }
×
224

×
225
        // Visit params
×
226
        const argNames = visitFunctionParams(fn, fnNode, paramsBlock, bodyBlock, isStrict, state);
14✔
227

14✔
228
        // If is full function, create binding for `arguments` in params block.
28,588✔
229
        // Param called `arguments` takes precedence.
28,588✔
230
        if (isFullFunction && !paramsBlock.bindings.arguments) {
28,588✔
231
                createArgumentsBinding(paramsBlock, isStrict, argNames);
17,032✔
232
        }
17,032✔
233

28,588✔
234
        // Visit body
28,588✔
235
        if (bodyBlock) {
28,588✔
236
                visitFunctionBody(fnNode, bodyBlock, state);
20,998✔
237
        } else {
28,588✔
238
                visitKey(fnNode, 'body', Expression, state);
7,590✔
239
        }
7,590✔
240

28,588✔
241
        // Serialize AST
28,588✔
242
        fn.astJson = serializeFunctionAst(fnNode, hasBodyBlock, isStrict, isEnteringStrict);
28,588✔
243

28,588✔
244
        // Exit function
28,588✔
245
        state.currentBlock = paramsBlock.parent;
28,588✔
246
        if (isFullFunction) state.currentThisBlock = parentThisBlock;
28,588✔
247
        state.currentFunction = fn.parent;
28,588✔
248
        state.trail = parentTrail;
28,588✔
249

28,588✔
250
        // Temporarily remove from AST so is not included in parent function's serialized AST
28,588✔
251
        parent[key] = null;
28,588✔
252

28,588✔
253
        // Queue instrumentation of function in 2nd pass
28,588✔
254
        state.secondPass(instrumentFunction, fnNode, fn, parent, key, paramsBlock, bodyBlock, state);
28,588✔
255

26✔
256
        return fn;
26✔
257
}
26✔
258

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

2,372✔
293
                                        // Make params block the vars block. Tracker call and scope ID var will go in params.
2,372✔
294
                                        if (bodyBlock) paramsBlock.varsBlock = bodyBlock.varsBlock = paramsBlock;
2,372✔
295
                                }
2,372✔
296
                        }
2,524✔
297

2,794✔
298
                        AssigneeLet(paramNode, state, paramNodes, index);
2,794✔
299
                }
2,794✔
300
        }, state);
29,054✔
301

29,054✔
302
        // Record index of first complex param
29,054✔
303
        fn.firstComplexParamIndex = firstComplexParamIndex;
29,054✔
304

29,054✔
305
        // Return array of params which are linked to `arguments`
29,054✔
306
        return argNames;
29,054✔
307
}
29,054✔
308

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

62✔
326
/**
62✔
327
 * Visitor for function body block.
62✔
328
 * @param {Object} node - Function body block AST node
62✔
329
 * @param {Object} state - State object
62✔
330
 * @returns {undefined}
62✔
331
 */
62✔
332
function FunctionBodyBlock(node, state) {
21,464✔
333
        visitKeyContainer(node, 'body', Statement, state);
21,464✔
334
}
21,464✔
335

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

8✔
355
        // Stringify AST to JSON
8✔
356
        return JSON.stringify(fnNode);
8✔
357
}
8✔
358

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

366✔
374
        // Remove unnecessary directives
366✔
375
        const newDirectiveNodes = [],
366✔
376
                commentNodes = [];
366✔
377
        for (let directiveNode of directiveNodes) {
5,132✔
378
                if (directiveNode.value.value === 'use strict') {
402✔
379
                        if (needsNoDirective) {
330✔
380
                                commentNodes.push(
22✔
381
                                        ...(directiveNode.leadingComments || []),
22✔
382
                                        ...(directiveNode.trailingComments || [])
22✔
383
                                );
22✔
384
                                continue;
22✔
385
                        }
22✔
386
                        needsNoDirective = true;
308✔
387
                }
308✔
388

380✔
389
                // Add any comments removed from previous directives
380✔
390
                if (commentNodes.length !== 0) {
402!
391
                        directiveNode = {
×
392
                                ...directiveNode,
×
393
                                leadingComments: combineArraysWithDedup(commentNodes, directiveNode.leadingComments)
×
394
                        };
×
395
                        commentNodes.length = 0;
×
396
                }
×
397

380✔
398
                newDirectiveNodes.push(directiveNode);
380✔
399
        }
380✔
400

366✔
401
        // If no directives need to be removed, return `undefined`
366✔
402
        if (newDirectiveNodes.length === directiveNodes.length) return undefined;
24✔
403

24✔
404
        // Clone function node
24✔
405
        bodyNode = {...bodyNode, directives: newDirectiveNodes};
24✔
406
        fnNode = {...fnNode, body: bodyNode};
24✔
407

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

22✔
429
        // Return clone of function node
22✔
430
        return fnNode;
22✔
431
}
17,496✔
432

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

22✔
464
        if (parentFunction) parentFunction.children.push(fn);
22✔
465

22✔
466
        state.functions.push(fn);
22!
467
        state.fileContainsFunctionsOrEval = true;
26✔
468

26✔
469
        return fn;
26✔
470
}
26✔
471

26✔
472
/**
26✔
473
 * Get type code for function.
62✔
474
 * @param {Object} fnNode - Function AST node
62✔
475
 * @returns {string} - Function type code
62✔
476
 */
62✔
477
function getFunctionType(fnNode) {
17,036✔
478
        return fnNode.async
17,036✔
479
                ? fnNode.generator
17,036✔
480
                        ? FN_TYPE_ASYNC_GENERATOR_FUNCTION
176✔
481
                        : FN_TYPE_ASYNC_FUNCTION
176✔
482
                : fnNode.generator
17,036✔
483
                        ? FN_TYPE_GENERATOR_FUNCTION
16,860✔
484
                        : FN_TYPE_FUNCTION;
17,036✔
485
}
17,036✔
486

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

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

24,260✔
516
        // Call visitor
24,260✔
517
        const res = visit(isStrict, isEnteringStrict);
24,260✔
518

24,260✔
519
        // If entered strict mode, exit it again
24,260✔
520
        if (isEnteringStrict) state.isStrict = false;
24,260✔
521

24,260✔
522
        // Return visitor's return value
24,260✔
523
        return res;
24,260✔
524
}
24,260✔
525

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

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

32✔
570
        // Do not hoist if existing const, let or class declaration binding in hoist block
32✔
571
        const hoistBlockBinding = hoistBlock.bindings[varName];
32✔
572
        if (hoistBlockBinding && !hoistBlockBinding.isVar) return;
32!
573

32✔
574
        // If parent function's params include var with same name, do not hoist.
32✔
575
        // NB: `hoistBlock.parent` here is either parent function params block or file block.
32!
576
        // In CommonJS code, file block is CommonJS wrapper function's params, and in any other context
32✔
577
        // file block contains no bindings except `this`. So it's safe to treat as a params block in all cases.
32✔
578
        // NB: `paramsBlockBinding.argNames` check is to avoid the pseudo-param `arguments` blocking hoisting.
22✔
579
        const paramsBlockBinding = hoistBlock.parent.bindings[varName];
22✔
580
        if (paramsBlockBinding && !paramsBlockBinding.argNames) return;
×
581

×
582
        // If any binding in intermediate blocks, do not hoist.
×
583
        // NB: This includes function declarations which will be themselves hoisted.
22!
584
        let inBetweenBlock = declaration.block.parent;
22✔
585
        while (inBetweenBlock !== hoistBlock) {
22!
586
                if (inBetweenBlock.bindings[varName]) return;
✔
587
                inBetweenBlock = inBetweenBlock.parent;
×
588
        }
×
589

32✔
590
        // Can be hoisted
32✔
591
        if (!hoistBlockBinding) {
32!
592
                hoistBlock.bindings[varName] = {...declaration.binding, isVar: true};
✔
593
        } else if (!hoistBlockBinding.isFrozenName) {
4✔
594
                // Existing binding is a `var` declaration.
4✔
595
                // Flag binding as frozen so `var` declarations' identifiers don't get renamed.
4✔
596
                hoistBlockBinding.isFrozenName = true;
4✔
597
                hoistBlockBinding.trails.length = 0;
4✔
598
        }
4✔
599
}
4✔
600

4✔
601
/**
4✔
602
 * Instrument function.
4✔
603
 * @param {Object} node - Function or method AST node
4✔
604
 * @param {Object} fn - Function object
4✔
605
 * @param {Object|Array} parent - Parent AST node/container
62✔
606
 * @param {string|number} key - Node's key on parent AST node/container
62✔
607
 * @param {Object} paramsBlock - Function's params block object
62✔
608
 * @param {Object} [bodyBlock] - Function's body block object (if it has one, arrow functions may not)
62✔
609
 * @param {Object} state - State object
62✔
610
 * @returns {undefined}
62✔
611
 */
62✔
612
function instrumentFunction(node, fn, parent, key, paramsBlock, bodyBlock, state) {
28,588✔
613
        // Restore to original place in AST
16✔
614
        parent[key] = node;
16✔
615

16✔
616
        instrumentFunctionOrClassConstructor(node, fn, paramsBlock, bodyBlock, state);
16✔
617

16✔
618
        if (node.type === 'ArrowFunctionExpression') insertArrowFunctionTrackerComment(fn, node, state);
16✔
619
}
×
620

×
621
/**
×
622
 * Instrument function, method or class constructor.
16✔
623
 *   - Insert `livepack_tracker()` call in function body.
16!
624
 *   - Insert block vars (`livepack_scopeId`, `livepack_temp`) in function body or params.
16!
625
 *   - Create function info function containing function AST, details of internal and external vars etc.
16✔
626
 *
16✔
627
 * @param {Object} node - Function, method, or class constructor AST node
16!
628
 * @param {Object} fn - Function object
16✔
629
 * @param {Object} paramsBlock - Function's params block object
62✔
630
 * @param {Object} bodyBlock - Function's body block object
62✔
631
 * @param {Object} state - State object
62✔
632
 * @returns {undefined}
62✔
633
 */
62✔
634
function instrumentFunctionOrClassConstructor(node, fn, paramsBlock, bodyBlock, state) {
29,688✔
635
        // NB `bodyBlock` here may actually be params block if is an arrow function with no body.
29,688✔
636
        // Sort scopes in ascending order of block ID.
29,688✔
637
        const scopes = [...fn.externalVars].map(([block, vars]) => ({block, vars}))
29,688✔
638
                .sort((scope1, scope2) => (scope1.block.id > scope2.block.id ? 1 : -1));
29,688✔
639
        fn.scopes = scopes;
29,688✔
640

29,688✔
641
        // Add external vars to parent function where external to that function too
29,688✔
642
        const parentFunction = fn.parent;
29,688✔
643
        if (parentFunction) {
29,688✔
644
                // External vars
17,858✔
645
                for (const {block, vars} of scopes) {
17,858✔
646
                        if (block.id >= parentFunction.id) break;
12,856✔
647
                        for (const [varName, {binding}] of Object.entries(vars)) {
12,856✔
648
                                getOrCreateExternalVar(parentFunction, block, varName, binding);
11,782✔
649
                        }
11,782✔
650
                }
6,380✔
651
        }
17,858✔
652

29,688✔
653
        // Insert tracker call and block vars (`livepack_scopeId` and `livepack_temp`) into function.
29,688✔
654
        // If function has complex params, insert into params. Otherwise, into function body.
2✔
655
        const trackerNode = createTrackerCall(fn.id, scopes, state);
2✔
656

×
657
        const {firstComplexParamIndex} = fn;
2✔
658
        if (firstComplexParamIndex === undefined) {
2!
659
                insertTrackerCodeIntoFunctionBody(node, paramsBlock, bodyBlock, trackerNode, state);
27,316✔
660
        } else {
29,688✔
661
                insertTrackerCodeIntoFunctionParams(
2,372✔
662
                        node, paramsBlock, bodyBlock, trackerNode, firstComplexParamIndex, state
2,372✔
663
                );
2,372✔
664
        }
2,372✔
665
}
29,688✔
666

62✔
667
/**
62✔
668
 * Create tracker call.
62✔
669
 * @param {number} fnId - Function ID
62✔
670
 * @param {Array<Object>} scopes - Array of scope objects in ascending order of block ID
62✔
671
 * @param {Object} state - State object
62✔
672
 * @returns {undefined}
62✔
673
 */
62✔
674
function createTrackerCall(fnId, scopes, state) {
29,688✔
675
        // `livepack_tracker(livepack_getFnInfo_3, () => [[livepack_scopeId_2, x]]);`
29,688✔
676
        return t.callExpression(state.trackerVarNode, [
29,688✔
677
                createFnInfoVarNode(fnId, state),
29,688✔
678
                t.arrowFunctionExpression([], t.arrayExpression(
29,688✔
679
                        scopes.map(scope => t.arrayExpression([
29,688✔
680
                                scope.block.varsBlock.scopeIdVarNode,
23,200✔
681
                                ...Object.values(scope.vars).map(blockVar => blockVar.binding.varNode)
✔
682
                        ]))
×
683
                ))
×
684
        ]);
×
685
}
×
686

×
687
/**
×
688
 * Insert tracker comment into function declaration/expression.
×
689
 * @param {Object} fn - Function object
×
690
 * @param {Object} node - Function declaration or expression AST node
×
691
 * @param {Object} state - State object
×
692
 * @returns {undefined}
×
693
 */
×
694
function insertFunctionDeclarationOrExpressionTrackerComment(fn, node, state) {
7,736✔
695
        // Insert tracker comment before identifier, or before 1st param or before function body
7,736✔
696
        const commentHolderNode = node.id || (node.params.length !== 0 ? node.params[0] : node.body);
7,736✔
697
        insertTrackerComment(fn.id, getFunctionType(node), commentHolderNode, 'leading', state);
7,736✔
698
}
7,736✔
699

×
700
/**
×
701
 * Insert tracker comment into arrow function.
×
702
 * @param {Object} fn - Function object
×
703
 * @param {Object} node - Arrow function AST node
×
704
 * @param {Object} state - State object
×
705
 * @returns {undefined}
×
706
 */
×
707
function insertArrowFunctionTrackerComment(fn, node, state) {
11,552✔
708
        // Insert tracker comment before first param. If no params, before function body.
11,552✔
709
        const paramNodes = node.params;
11,552✔
710
        insertTrackerComment(
11,552✔
711
                fn.id, node.async ? FN_TYPE_ASYNC_FUNCTION : FN_TYPE_FUNCTION,
11,552✔
712
                paramNodes.length !== 0 ? paramNodes[0] : node.body, 'leading', state
11,552✔
713
        );
11,552✔
714
}
11,552✔
715

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

62✔
732
/**
62✔
733
 * Create function info function containing function AST, details of internal and external vars etc.
62✔
734
 * @param {Object} fn - Function object
62✔
735
 * @param {Object} state - State object
62✔
736
 * @returns {Object} - AST node for function info function
62✔
737
 */
62✔
738
function createFunctionInfoFunction(fn, state) {
29,688✔
739
        // Compile internal vars and reserved var names
29,688✔
740
        const internalVars = Object.create(null),
29,688✔
741
                {reservedVarNames} = fn;
29,688✔
742
        for (const {name: varName, trails, isFrozenName} of fn.bindings) {
29,688✔
743
                if (isFrozenName) {
26✔
744
                        reservedVarNames.add(varName);
26✔
745
                } else {
26✔
746
                        const internalVar = internalVars[varName] || (internalVars[varName] = []);
26✔
747
                        internalVar.push(...trails);
26✔
748
                }
26✔
749
        }
26✔
750

26✔
751
        // Compile amendments.
26✔
752
        // Reverse order so deepest are first.
×
753
        // Remove the need for an internal var if binding has frozen name.
26✔
754
        let amendments;
26✔
755
        if (fn.amendments.length > 0) {
26✔
756
                amendments = fn.amendments.map(({type, blockId, trail, binding}) => {
26✔
757
                        if (type === CONST_VIOLATION_NEEDS_VAR && binding.isFrozenName) {
1,546!
758
                                type = CONST_VIOLATION_NEEDS_NO_VAR;
×
759
                        }
×
760
                        return [type, blockId, ...trail];
1,546✔
761
                }).reverse();
1,466✔
762
        }
1,466✔
763

29,688✔
764
        // Create JSON function info string
29,688✔
765
        const {children} = fn;
29,688✔
766
        let argNames;
29,688✔
767
        let json = JSON.stringify({
29,688✔
768
                scopes: fn.scopes.map(({block, vars}) => ({
29,688✔
769
                        blockId: block.id,
23,200✔
770
                        blockName: block.name,
23,200✔
771
                        vars: mapValues(vars, (varProps, varName) => {
23,200✔
772
                                if (varName === 'arguments') argNames = varProps.binding.argNames;
40,432✔
773
                                return {
40,432✔
774
                                        isReadFrom: varProps.isReadFrom || undefined,
40,432✔
775
                                        isAssignedTo: varProps.isAssignedTo || undefined,
40,432✔
776
                                        isFrozenInternalName: varProps.binding.isFrozenName || undefined,
40,432✔
777
                                        trails: varProps.trails
40,432✔
778
                                };
40,432✔
779
                        })
23,200✔
780
                })),
36✔
781
                isStrict: fn.isStrict || undefined,
36✔
782
                superIsProto: fn.superIsProto || undefined,
36✔
783
                containsEval: fn.containsEval || undefined,
36✔
784
                containsImport: fn.containsImport || undefined,
36✔
785
                argNames,
36✔
786
                internalVars,
36✔
787
                reservedVarNames: reservedVarNames.size !== 0 ? [...reservedVarNames] : undefined,
36✔
788
                globalVarNames: fn.globalVarNames.size !== 0 ? [...fn.globalVarNames] : undefined,
36✔
789
                amendments,
36✔
790
                hasSuperClass: fn.hasSuperClass || undefined,
36✔
791
                childFns: children.map(childFn => childFn.trail)
36✔
792
        });
36✔
793

36✔
794
        // Add AST JSON to JSON
36✔
795
        json = `${json.slice(0, -1)},"ast":${fn.astJson}}`;
36✔
796

4✔
797
        // Create function returning JSON string and references to function info function for child fns.
4✔
798
        // Make output as single-quoted string for shorter output (not escaping every `"`).
4✔
799
        return t.functionDeclaration(createFnInfoVarNode(fn.id, state), [], t.blockStatement([
36✔
800
                t.returnStatement(t.arrayExpression([
×
UNCOV
801
                        stringLiteralWithSingleQuotes(json),
×
802
                        t.arrayExpression(children.map(childFn => createFnInfoVarNode(childFn.id, state))),
36✔
803
                        state.getSourcesNode
36✔
804
                ]))
36✔
805
        ]));
36✔
806
}
36✔
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