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

overlookmotel / livepack / 6956398548

22 Nov 2023 11:03AM UTC coverage: 90.321% (-0.001%) from 90.322%
6956398548

push

github

overlookmotel
WIP 1

4746 of 5103 branches covered (0.0%)

Branch coverage included in aggregate %.

163 of 193 new or added lines in 11 files covered. (84.46%)

92 existing lines in 8 files now uncovered.

12667 of 14176 relevant lines covered (89.36%)

12765.82 hits per line

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

84.64
/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
        visitFunctionParamsAndBody,
62✔
16
        removeUnnecessaryUseStrictDirectives,
62✔
17
        createFunction,
62✔
18
        getFunctionType,
62✔
19
        escapeFilename,
62✔
20
        withStrictModeState,
62✔
21
        hoistSloppyFunctionDeclarations,
62✔
22
        instrumentFunctionOrClassConstructor,
62✔
23
        insertTrackerComment,
62✔
24
        createFunctionInfoFunction
62✔
25
};
62✔
26

62✔
27
// Modules
62✔
28
const mapValues = require('lodash/mapValues'),
62✔
29
        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
        {insertTrackerCodeIntoFunctionBody, insertTrackerCodeIntoFunctionParams} = require('../tracking.js'),
62✔
41
        {createFnInfoVarNode} = 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) {
11,242✔
61
        // Tracker comment is inserted later to avoid it getting relocated if function has complex params
11,242✔
62
        // which are later relocated to inside function body
11,242✔
63
        visitFunction(node, parent, key, undefined, false, node.body.type === 'BlockStatement', state);
11,242✔
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,314✔
75
        // Visit function
4,314✔
76
        const fnName = node.id.name;
4,314✔
77
        const fn = visitFunction(node, parent, key, fnName, true, true, state);
4,314✔
78

4,314✔
79
        // Create binding for function name
4,314✔
80
        // TODO: Whether the value of the function is hoisted depends on whether is a top level statement
4,314✔
81
        // (including in a labeled statement)
4,314✔
82
        // `() => { console.log(typeof x); function x() {} }` -> Logs 'function'
4,314✔
83
        // `() => { console.log(typeof x); q: function x() {} }` -> Logs 'function'
4,314✔
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[fnName];
8✔
UNCOV
87
        if (!binding) {
✔
88
                const isTopLevel = block === hoistBlock;
×
89
                binding = createBinding(block, fnName, {isVar: isTopLevel, isFrozenName: true}, state);
8✔
90

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

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

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

3,054✔
130
        // Visit function
3,054✔
131
        const fn = visitFunction(node, parent, key, fnName, true, true, state);
3,054✔
132

3,054✔
133
        if (nameBlock) {
3,054✔
134
                // Exit name block
850✔
135
                state.currentBlock = nameBlock.parent;
850✔
136

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

3,054✔
143
        // Insert tracker comment
3,054✔
144
        insertFunctionDeclarationOrExpressionTrackerComment(fn, node, state);
3,054✔
145
}
2✔
146

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

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

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

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

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

×
222
        // Visit params + body
×
223
        const argNames = visitFunctionParamsAndBody(fn, fnNode, paramsBlock, bodyBlock, isStrict, state);
14✔
224

14✔
225
        // If is full function, create binding for `arguments` in params block.
27,818✔
226
        // Param called `arguments` takes precedence.
27,818✔
227
        if (isFullFunction) {
27,818✔
228
                if (!paramsBlock.bindings.arguments) createArgumentsBinding(paramsBlock, isStrict, argNames);
16,576✔
229
                state.currentThisBlock = parentThisBlock;
16,576✔
230
        }
16,576✔
231

27,818✔
232
        // Serialize AST
27,818✔
233
        fn.astJson = serializeFunctionAst(fnNode, hasBodyBlock, isStrict, isEnteringStrict);
27,818✔
234

27,818✔
235
        // Exit function
27,818✔
236
        state.currentBlock = paramsBlock.parent;
27,818✔
237
        state.currentFunction = fn.parent;
27,818✔
238
        state.trail = parentTrail;
27,818✔
239

27,818✔
240
        // Temporarily remove from AST so is not included in parent function's serialized AST
27,818✔
241
        parent[key] = null;
27,818✔
242

27,818✔
243
        // Queue instrumentation of function in 2nd pass
27,818✔
244
        state.secondPass(instrumentFunction, fnNode, fn, parent, key, paramsBlock, bodyBlock, state);
27,818✔
245

27,818✔
246
        return fn;
27,818✔
247
}
27,818✔
248

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

2,308✔
283
                                        // Make params block the vars block. Tracker call and scope ID var will go in params.
2,308✔
284
                                        if (bodyBlock) paramsBlock.varsBlock = bodyBlock.varsBlock = paramsBlock;
2,308✔
285
                                }
2,308✔
286
                        }
2,460✔
287

2,682✔
288
                        AssigneeLet(paramNode, state, paramNodes, index);
2,682✔
289
                }
2,682✔
290
        }, state);
28,278✔
291

28,278✔
292
        // Record index of first complex param
28,278✔
293
        fn.firstComplexParamIndex = firstComplexParamIndex;
28,278✔
294

28,278✔
295
        // Visit body
28,278✔
296
        if (bodyBlock) {
28,278✔
297
                state.currentBlock = bodyBlock;
20,914✔
298
                const parentHoistBlock = state.currentHoistBlock;
20,914✔
299
                state.currentHoistBlock = bodyBlock;
20,914✔
300
                visitKey(fnNode, 'body', FunctionBodyBlock, state);
20,914✔
301
                state.currentHoistBlock = parentHoistBlock;
20,914✔
302
        } else {
28,278✔
303
                visitKey(fnNode, 'body', Expression, state);
7,364✔
304
        }
7,364✔
305

28,278✔
306
        // Return array of params which are linked to `arguments`
28,278✔
307
        return argNames;
28,278✔
308
}
28,278✔
309

62✔
310
/**
62✔
311
 * Visitor for function body block.
62✔
312
 * @param {Object} node - Function body block AST node
62✔
313
 * @param {Object} state - State object
62✔
314
 * @returns {undefined}
62✔
315
 */
62✔
316
function FunctionBodyBlock(node, state) {
20,914✔
317
        visitKeyContainer(node, 'body', Statement, state);
20,914✔
318
}
20,914✔
319

62✔
320
/**
62✔
321
 * Serialize function AST to JSON.
62✔
322
 * Remove unnecessary 'use strict' directives if present.
62✔
323
 * Do not mutate the node passed in, to ensure these changes only affect the serialized AST,
62✔
324
 * and not the instrumented output.
62✔
325
 * @param {Object} fnNode - Function AST node
62✔
326
 * @param {boolean} hasBodyBlock - `true` if has body block (only arrow functions can not)
62✔
327
 * @param {boolean} isStrict - `true` if function is strict mode
28✔
328
 * @param {boolean} isEnteringStrict - `true` if function transitions into strict mode
28✔
UNCOV
329
 *   (and therefore needs to retain a 'use strict' directive)
×
330
 * @returns {string} - Function AST as JSON
28✔
331
 */
28✔
332
function serializeFunctionAst(fnNode, hasBodyBlock, isStrict, isEnteringStrict) {
27,818✔
333
        // Remove unnecessary 'use strict' directives
27,818✔
334
        if (hasBodyBlock && isStrict) {
27,818✔
335
                const alteredFnNode = removeUnnecessaryUseStrictDirectives(fnNode, !isEnteringStrict);
16,886✔
336
                if (alteredFnNode) fnNode = alteredFnNode;
16,886✔
337
        }
16,886✔
338

27,818✔
339
        // Stringify AST to JSON
27,818✔
340
        return JSON.stringify(fnNode);
27,818✔
341
}
27,818✔
342

28✔
343
/**
28✔
344
 * Remove unnecessary 'use strict' directives from function body.
28✔
345
 * Relocate any comments from deleted directives.
28✔
346
 * Do not mutate input - clone if needs alteration.
28✔
347
 * Return `undefined` if no alteration necessary.
8✔
348
 * @param {Object} fnNode - Function AST node
8✔
349
 * @param {boolean} needsNoDirective - `true` if function already strict mode from outer environment
8✔
UNCOV
350
 * @returns {Object|undefined} - Altered function AST node, or `undefined` if no directives removed
×
351
 */
×
352
function removeUnnecessaryUseStrictDirectives(fnNode, needsNoDirective) {
17,346✔
353
        // If no directives, return `undefined`
17,346!
354
        let bodyNode = fnNode.body;
17,346✔
355
        const directiveNodes = bodyNode.directives;
17,346✔
356
        if (directiveNodes.length === 0) return undefined;
17,346✔
357

350✔
358
        // Remove unnecessary directives
350✔
359
        const newDirectiveNodes = [],
350✔
360
                commentNodes = [];
350✔
361
        for (let directiveNode of directiveNodes) {
5,058✔
362
                if (directiveNode.value.value === 'use strict') {
386!
363
                        if (needsNoDirective) {
314✔
364
                                commentNodes.push(
22✔
365
                                        ...(directiveNode.leadingComments || []),
22✔
366
                                        ...(directiveNode.trailingComments || [])
22✔
367
                                );
22✔
368
                                continue;
22✔
369
                        }
22✔
370
                        needsNoDirective = true;
292✔
371
                }
292✔
372

364✔
373
                // Add any comments removed from previous directives
364✔
374
                if (commentNodes.length !== 0) {
386!
UNCOV
375
                        directiveNode = {
×
376
                                ...directiveNode,
×
377
                                leadingComments: combineArraysWithDedup(commentNodes, directiveNode.leadingComments)
×
378
                        };
×
379
                        commentNodes.length = 0;
✔
380
                }
×
381

364✔
382
                newDirectiveNodes.push(directiveNode);
364✔
383
        }
364✔
384

350✔
385
        // If no directives need to be removed, return `undefined`
350✔
386
        if (newDirectiveNodes.length === directiveNodes.length) return undefined;
478✔
387

22✔
388
        // Clone function node
22✔
389
        bodyNode = {...bodyNode, directives: newDirectiveNodes};
22✔
390
        fnNode = {...fnNode, body: bodyNode};
22✔
391

22✔
392
        // Add any remaining comments to first statement / last directive / function body
22✔
393
        if (commentNodes.length !== 0) {
436!
UNCOV
394
                let statementNodes = bodyNode.body;
×
395
                if (statementNodes.length !== 0) {
×
396
                        statementNodes = bodyNode.body = [...statementNodes];
✔
397
                        const firstStatementNode = statementNodes[0];
24✔
398
                        statementNodes[0] = {
24✔
399
                                ...firstStatementNode,
24✔
400
                                leadingComments: combineArraysWithDedup(commentNodes, firstStatementNode.leadingComments)
24✔
UNCOV
401
                        };
×
402
                } else if (newDirectiveNodes.length !== 0) {
24✔
UNCOV
403
                        const lastDirectiveNode = newDirectiveNodes[newDirectiveNodes.length - 1];
×
404
                        newDirectiveNodes[newDirectiveNodes.length - 1] = {
×
405
                                ...lastDirectiveNode,
×
406
                                trailingComments: combineArraysWithDedup(lastDirectiveNode.trailingComments, commentNodes)
×
407
                        };
×
408
                } else {
×
409
                        bodyNode.innerComments = combineArraysWithDedup(bodyNode.innerComments, commentNodes);
×
410
                }
×
411
        }
×
412

22✔
413
        // Return clone of function node
22✔
414
        return fnNode;
22✔
415
}
17,346✔
416

62✔
417
/**
62✔
418
 * Create object representing function.
62✔
419
 * @param {number} id - Function ID
62✔
420
 * @param {Object} node - Function/method/class AST node
62✔
421
 * @param {boolean} isStrict - `true` if function is strict mode
62✔
422
 * @param {Object} state - State object
62✔
423
 * @returns {Object} - Function object
26✔
424
 */
26✔
425
function createFunction(id, node, isStrict, state) {
28,904✔
426
        const parentFunction = state.currentFunction;
28,904✔
427
        const fn = {
28,904✔
428
                id,
28,904✔
429
                node,
28,904✔
430
                astJson: undefined,
28,904✔
431
                isStrict,
28,904✔
432
                parent: parentFunction,
28,904✔
UNCOV
433
                children: [],
×
434
                trail: parentFunction ? [...state.trail] : undefined,
28,904✔
435
                bindings: [],
28,904✔
436
                externalVars: new Map(), // Keyed by block
28,904✔
437
                scopes: undefined,
28,904✔
438
                globalVarNames: new Set(),
28,904✔
439
                amendments: [],
28,904✔
440
                superIsProto: false,
28,904✔
441
                containsEval: false,
28,904!
442
                containsImport: false,
28,904✔
443
                hasSuperClass: false,
28,904✔
444
                firstSuperStatementIndex: undefined,
28,904✔
445
                returnsSuper: false,
28,904✔
446
                hasThisBindingForSuper: false,
28,904✔
447
                firstComplexParamIndex: undefined
28,904✔
448
        };
28,904✔
449

28,904✔
450
        if (parentFunction) parentFunction.children.push(fn);
28,904✔
451

28,904✔
452
        state.functions.push(fn);
28,904✔
453
        state.fileContainsFunctionsOrEval = true;
28,904✔
454

28,904✔
455
        return fn;
28,904✔
456
}
28,904✔
457

62✔
458
/**
62✔
459
 * Get type code for function.
62✔
460
 * @param {Object} fnNode - Function AST node
62✔
461
 * @returns {string} - Function type code
62✔
462
 */
62✔
463
function getFunctionType(fnNode) {
16,576✔
464
        return fnNode.async
16,576✔
465
                ? fnNode.generator
16,576✔
466
                        ? FN_TYPE_ASYNC_GENERATOR_FUNCTION
176✔
467
                        : FN_TYPE_ASYNC_FUNCTION
24✔
468
                : fnNode.generator
24✔
469
                        ? FN_TYPE_GENERATOR_FUNCTION
24✔
470
                        : FN_TYPE_FUNCTION;
24✔
471
}
24✔
472

24✔
473
/**
24✔
474
 * Escape filename so it can be safely included in tracker comment.
24✔
475
 * @param {string} filename - File path
24✔
476
 * @returns {string} - Filename escaped
24✔
477
 */
24✔
478
function escapeFilename(filename) {
6,812✔
479
        // Encode filename as JSON to escape non-ascii chars.
6,812✔
480
        // Convert `*/` to `*\/` so does not terminate comment early.
6,812✔
481
        // `JSON.parse('"*\/"')` is `'*/'` so it needs no special unescaping.
6,812!
482
        return JSON.stringify(filename).slice(1, -1).replace(/\*\//g, '*\\/');
6,812✔
483
}
6,812✔
UNCOV
484

×
485
/**
×
486
 * Determine if function is strict mode. If it is, set `state.isStrict` flag.
×
487
 * Call visitor with strict/sloppy mode flags.
×
488
 * @param {Object} fnNode - Function or method AST node
×
489
 * @param {boolean} hasBodyBlock - `true` if has body block (only arrow functions can not)
×
490
 * @param {Object} state - State object
×
491
 * @param {Function} visit - Visitor function
×
492
 * @returns {*} - Visitor function's return value
×
493
 */
×
494
function withStrictModeState(fnNode, hasBodyBlock, state, visit) {
23,518✔
495
        // Get if strict mode
23,518✔
496
        let {isStrict} = state,
23,518✔
497
                isEnteringStrict = false;
23,518✔
498
        if (!isStrict && hasBodyBlock && hasUseStrictDirective(fnNode.body)) {
23,518✔
499
                isStrict = isEnteringStrict = state.isStrict = true;
292✔
500
        }
292✔
501

23,518✔
502
        // Call visitor
23,518✔
503
        const res = visit(isStrict, isEnteringStrict);
23,518✔
504

23,518✔
505
        // If entered strict mode, exit it again
23,518✔
506
        if (isEnteringStrict) state.isStrict = false;
23,518✔
507

23,518✔
508
        // Return visitor's return value
23,518✔
509
        return res;
23,518✔
510
}
23,518✔
UNCOV
511

×
512
/**
×
513
 * Hoist sloppy function declarations if they can be hoisted.
×
514
 * Function declarations are hoisted to the top block in parent function body or program
×
515
 * if they are defined in sloppy mode (i.e. outside environment is sloppy mode, regardless of
×
516
 * whether function itself strict mode) and they are not async or generator functions.
×
517
 *
×
518
 * They can only be hoisted if:
×
519
 *   1. No `const`, `let` or class declaration with same name in block they'd be hoisted to.
×
520
 *   2. No parameter in parent function with same name.
×
521
 *   3. No other bindings in intermediate blocks
×
522
 *      (including other function declarations which are themselves hoisted)
×
523
 *
×
524
 * When a function declaration is hoisted, it produces two separate bindings:
×
525
 *   1. Binding in block where originally defined.
×
526
 *   2. Binding in block it's hoisted to.
×
527
 *
×
528
 * See https://github.com/babel/babel/pull/14203#issuecomment-1038187168
×
529
 *
×
530
 * @param {Object} state - State object
×
531
 * @returns {undefined}
24✔
532
 */
62✔
533
function hoistSloppyFunctionDeclarations(state) {
5,240✔
534
        for (const declaration of state.sloppyFunctionDeclarations) {
5,240✔
535
                hoistSloppyFunctionDeclaration(declaration);
32✔
536
        }
32✔
537
}
5,240✔
538

62✔
539
/**
62✔
540
 * Hoist function declaration, if it can be hoisted.
62✔
541
 * Should only be called for functions which:
62✔
542
 *   1. are not already in the top block in parent function / program
62✔
543
 *   2. are not async or generator functions
36✔
544
 *
36✔
545
 * @param {Object} declaration - Declaration object
36✔
546
 * @param {string} declaration.varName - Function name
36✔
547
 * @param {Object} declaration.binding - Binding object for binding in block where originally declared
36✔
548
 * @param {Object} declaration.block - Block object for block where originally declared
36!
549
 * @param {Object} declaration.hoistBlock - Block object for block where could be hoisted to
36✔
550
 * @param {Object} [declaration.parentFn] - Function object for parent function
36✔
551
 * @returns {undefined}
36✔
552
 */
36✔
553
function hoistSloppyFunctionDeclaration(declaration) {
32✔
554
        const {varName, hoistBlock} = declaration;
32✔
555

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

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

22✔
568
        // If any binding in intermediate blocks, do not hoist.
22✔
569
        // NB: This includes function declarations which will be themselves hoisted.
32✔
570
        let inBetweenBlock = declaration.block.parent;
32✔
571
        while (inBetweenBlock !== hoistBlock) {
32!
572
                if (inBetweenBlock.bindings[varName]) return;
4✔
573
                inBetweenBlock = inBetweenBlock.parent;
4✔
574
        }
4✔
575

4✔
576
        // Can be hoisted
4✔
577
        if (!hoistBlockBinding) {
4!
578
                hoistBlock.bindings[varName] = {...declaration.binding, isVar: true};
4✔
579
        } else if (!hoistBlockBinding.isFrozenName) {
4✔
580
                // Existing binding is a `var` declaration.
4✔
581
                // Flag binding as frozen so `var` declarations' identifiers don't get renamed.
4✔
582
                hoistBlockBinding.isFrozenName = true;
4✔
583
                hoistBlockBinding.trails.length = 0;
32✔
584
        }
32✔
585
}
32✔
586

62✔
587
/**
62✔
588
 * Instrument function.
62✔
589
 * @param {Object} node - Function or method AST node
62✔
590
 * @param {Object} fn - Function object
62✔
591
 * @param {Object|Array} parent - Parent AST node/container
62✔
592
 * @param {string|number} key - Node's key on parent AST node/container
62✔
593
 * @param {Object} paramsBlock - Function's params block object
62✔
594
 * @param {Object} [bodyBlock] - Function's body block object (if it has one, arrow functions may not)
62✔
595
 * @param {Object} state - State object
16✔
596
 * @returns {undefined}
16✔
597
 */
16✔
598
function instrumentFunction(node, fn, parent, key, paramsBlock, bodyBlock, state) {
27,818✔
599
        // Restore to original place in AST
27,818✔
UNCOV
600
        parent[key] = node;
×
601

×
602
        instrumentFunctionOrClassConstructor(node, fn, paramsBlock, bodyBlock, state);
27,818✔
603

27,818✔
604
        if (node.type === 'ArrowFunctionExpression') insertArrowFunctionTrackerComment(fn, node, state);
27,818!
605
}
27,818✔
UNCOV
606

×
607
/**
×
608
 * Instrument function, method or class constructor.
16✔
609
 *   - Insert `livepack_tracker()` call in function body.
16✔
610
 *   - Insert block vars (`livepack_scopeId`, `livepack_temp`) in function body or params.
16!
611
 *   - Create function info function containing function AST, details of internal and external vars etc.
62✔
612
 *
62✔
613
 * @param {Object} node - Function, method, or class constructor AST node
62✔
614
 * @param {Object} fn - Function object
62✔
615
 * @param {Object} paramsBlock - Function's params block object
62✔
616
 * @param {Object} bodyBlock - Function's body block object
62✔
617
 * @param {Object} state - State object
62✔
618
 * @returns {undefined}
62✔
619
 */
62✔
620
function instrumentFunctionOrClassConstructor(node, fn, paramsBlock, bodyBlock, state) {
28,904✔
621
        // NB `bodyBlock` here may actually be params block if is an arrow function with no body.
28,904✔
622
        // Sort scopes in ascending order of block ID.
28,904✔
623
        const scopes = [...fn.externalVars].map(([block, vars]) => ({block, vars}))
28,904✔
624
                .sort((scope1, scope2) => (scope1.block.id > scope2.block.id ? 1 : -1));
28,904✔
625
        fn.scopes = scopes;
28,904✔
626

28,904✔
627
        // Add external vars to parent function where external to that function too
28,904✔
628
        const parentFunction = fn.parent;
28,904✔
629
        if (parentFunction) {
28,904✔
630
                // External vars
17,442✔
631
                const {id: parentFunctionId, externalVars: parentExternalVars} = parentFunction;
17,442✔
632
                for (const {block, vars} of scopes) {
2✔
633
                        if (block.id >= parentFunctionId) break;
2✔
634
                        for (const [varName, {binding}] of Object.entries(vars)) {
2✔
635
                                getOrCreateExternalVar(parentExternalVars, block, varName, binding);
2✔
636
                        }
2✔
637
                }
2✔
638
        }
2✔
639

2✔
640
        // Insert tracker call and block vars (`livepack_scopeId` and `livepack_temp`) into function.
2!
641
        // If function has complex params, insert into params. Otherwise, into function body.
28,904✔
642
        const trackerNode = createTrackerCall(fn.id, scopes, state);
28,904✔
643

28,904✔
644
        const {firstComplexParamIndex} = fn;
28,904✔
645
        if (firstComplexParamIndex === undefined) {
28,904✔
646
                insertTrackerCodeIntoFunctionBody(node, paramsBlock, bodyBlock, trackerNode, state);
26,596✔
647
        } else {
28,904✔
648
                insertTrackerCodeIntoFunctionParams(
2,308✔
649
                        node, paramsBlock, bodyBlock, trackerNode, firstComplexParamIndex, state
2,308✔
650
                );
2,308✔
651
        }
2,308✔
652
}
28,904✔
653

62✔
654
/**
62✔
655
 * Create tracker call.
62✔
656
 * @param {number} fnId - Function ID
62✔
657
 * @param {Array<Object>} scopes - Array of scope objects in ascending order of block ID
62✔
658
 * @param {Object} state - State object
62✔
659
 * @returns {undefined}
62✔
660
 */
62✔
661
function createTrackerCall(fnId, scopes, state) {
28,904✔
UNCOV
662
        // `livepack_tracker(livepack_getFnInfo_3, () => [[livepack_scopeId_2, x]]);`
×
663
        return t.callExpression(state.trackerVarNode, [
×
664
                createFnInfoVarNode(fnId, state),
×
665
                t.arrowFunctionExpression([], t.arrayExpression(
×
666
                        scopes.map(scope => t.arrayExpression([
✔
667
                                scope.block.varsBlock.scopeIdVarNode,
23,348✔
668
                                ...Object.values(scope.vars).map(blockVar => blockVar.binding.varNode)
23,348✔
UNCOV
669
                        ]))
×
670
                ))
×
671
        ]);
×
672
}
×
673

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

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

×
703
/**
×
704
 * Insert tracker comment.
×
705
 * @param {number} fnId - Function ID
×
706
 * @param {string} fnType - Function type
×
707
 * @param {Object} commentHolderNode - AST node to attach comment to
×
708
 * @param {string} commentType - 'leading' / 'inner' / 'trailing'
62✔
709
 * @param {Object} state - State object
62✔
710
 * @returns {undefined}
62✔
711
 */
62✔
712
function insertTrackerComment(fnId, fnType, commentHolderNode, commentType, state) {
28,904✔
713
        insertComment(
28,904✔
714
                commentHolderNode, commentType,
28,904✔
715
                `${TRACKER_COMMENT_PREFIX}${fnId};${fnType};${state.filenameEscaped}`
28,904✔
716
        );
28,904✔
717
}
28,904✔
718

62✔
719
/**
62✔
720
 * Create function info function containing function AST, details of internal and external vars etc.
62✔
721
 * @param {Object} fn - Function object
62✔
722
 * @param {Object} state - State object
62✔
723
 * @returns {Object} - AST node for function info function
62✔
724
 */
26✔
725
function createFunctionInfoFunction(fn, state) {
28,904✔
726
        // Compile internal vars and reserved var names
28,904✔
727
        const internalVars = Object.create(null),
28,904✔
728
                reservedVarNames = new Set();
28,904✔
UNCOV
729
        for (const {name: varName, trails, isFrozenName} of fn.bindings) {
✔
730
                if (isFrozenName) {
45,132✔
731
                        reservedVarNames.add(varName);
1,492✔
732
                } else {
45,132✔
733
                        const internalVar = internalVars[varName] || (internalVars[varName] = []);
43,640✔
734
                        internalVar.push(...trails);
43,640✔
735
                }
43,640✔
736
        }
45,132✔
737

28,904✔
738
        // Compile amendments.
28,904✔
739
        // Reverse order so deepest are first.
28,904✔
740
        // Remove the need for an internal var if binding has frozen name.
28,904✔
741
        let amendments;
28,904✔
742
        if (fn.amendments.length > 0) {
28,904✔
743
                amendments = fn.amendments.map(({type, blockId, trail, binding}) => {
1,518✔
744
                        if (type === CONST_VIOLATION_NEEDS_VAR && binding.isFrozenName) {
1,608!
NEW
745
                                type = CONST_VIOLATION_NEEDS_NO_VAR;
×
NEW
746
                        }
×
747
                        return [type, blockId, ...trail];
1,608✔
748
                }).reverse();
1,518✔
749
        }
1,518✔
750

28,904✔
751
        // Create JSON function info string
28,904✔
752
        const {children} = fn;
28,904✔
753
        let argNames;
28,904✔
754
        let json = JSON.stringify({
28,904✔
755
                scopes: fn.scopes.map(({block, vars}) => ({
28,904✔
756
                        blockId: block.id,
23,348✔
757
                        blockName: block.name,
23,348✔
758
                        vars: mapValues(vars, (varProps, varName) => {
23,348✔
759
                                if (varName === 'arguments') argNames = varProps.binding.argNames;
40,188✔
760
                                return {
36✔
761
                                        isReadFrom: varProps.isReadFrom || undefined,
36✔
762
                                        isAssignedTo: varProps.isAssignedTo || undefined,
36✔
763
                                        isFrozenName: varProps.isFrozenName || undefined,
36✔
764
                                        isFrozenInternalName: varProps.binding.isFrozenName || undefined,
36✔
UNCOV
765
                                        trails: varProps.trails
×
UNCOV
766
                                };
×
UNCOV
767
                        })
×
UNCOV
768
                })),
×
769
                isStrict: fn.isStrict || undefined,
36✔
770
                superIsProto: fn.superIsProto || undefined,
36✔
771
                containsEval: fn.containsEval || undefined,
36✔
772
                containsImport: fn.containsImport || undefined,
36✔
773
                argNames,
36✔
774
                internalVars,
36✔
775
                reservedVarNames: reservedVarNames.size !== 0 ? [...reservedVarNames] : undefined,
36✔
776
                globalVarNames: fn.globalVarNames.size !== 0 ? [...fn.globalVarNames] : undefined,
8✔
777
                amendments,
8✔
778
                hasSuperClass: fn.hasSuperClass || undefined,
8✔
779
                firstSuperStatementIndex: fn.firstSuperStatementIndex,
36✔
780
                returnsSuper: fn.returnsSuper || undefined,
✔
781
                childFns: children.map(childFn => childFn.trail)
✔
782
        });
×
783

×
UNCOV
784
        // Add AST JSON to JSON
×
785
        json = `${json.slice(0, -1)},"ast":${fn.astJson}}`;
36✔
786

36✔
787
        // Create function returning JSON string and references to function info function for child fns.
36✔
788
        // Make output as single-quoted string for shorter output (not escaping every `"`).
36!
UNCOV
789
        return t.functionDeclaration(createFnInfoVarNode(fn.id, state), [], t.blockStatement([
×
UNCOV
790
                t.returnStatement(t.arrayExpression([
×
UNCOV
791
                        stringLiteralWithSingleQuotes(json),
×
UNCOV
792
                        t.arrayExpression(children.map(childFn => createFnInfoVarNode(childFn.id, state))),
✔
UNCOV
793
                        state.getSourcesNode
×
UNCOV
794
                ]))
×
795
        ]));
×
796
}
×
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

© 2024 Coveralls, Inc