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

overlookmotel / livepack / 7305779414

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

push

github

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

Fixes #566.

4680 of 5034 branches covered (0.0%)

Branch coverage included in aggregate %.

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

35 existing lines in 15 files now uncovered.

12473 of 13909 relevant lines covered (89.68%)

8705.64 hits per line

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

88.44
/lib/instrument/tracking.js
1
/* --------------------
62✔
2
 * livepack module
62✔
3
 * Code instrumentation functions to add vars and tracker to functions/blocks
62✔
4
 * ------------------*/
62✔
5

62✔
6
'use strict';
62✔
7

62✔
8
// Export
62✔
9
module.exports = {
62✔
10
        insertBlockVarsIntoBlockStatement,
62✔
11
        insertTrackerCodeIntoFunction
62✔
12
};
62✔
13

62✔
14
// Modules
62✔
15
const t = require('@babel/types');
62✔
16

62✔
17
// Imports
62✔
18
const {createTempVarNode} = require('./internalVars.js'),
62✔
19
        {copyLocAndComments} = require('./utils.js');
62✔
20

62✔
21
// Exports
62✔
22

62✔
23
/**
62✔
24
 * Insert block var declarations at start of block statement.
62✔
25
 * @param {Object} block - Block object
62✔
26
 * @param {Object} blockNode - Statement block / program AST node
62✔
27
 * @param {Object} state - State object
62✔
28
 * @returns {undefined}
62✔
29
 */
62✔
30
function insertBlockVarsIntoBlockStatement(block, blockNode, state) {
27,858✔
31
        const {scopeIdVarNode} = block;
27,858✔
32
        if (!scopeIdVarNode) return;
27,858✔
33

5,566✔
34
        // `const livepack_scopeId_1 = livepack_getScopeId()`
5,566✔
35
        const statementNodes = [
5,566✔
36
                t.variableDeclaration(
5,566✔
37
                        'const', [t.variableDeclarator(scopeIdVarNode, t.callExpression(state.getScopeIdVarNode, []))]
5,566✔
38
                )
34✔
39
        ];
34✔
40

34✔
41
        // `let livepack_temp_2, livepack_temp_3`
34✔
42
        const {tempVarNodes} = block;
34✔
43
        if (tempVarNodes) {
34✔
44
                statementNodes[1] = t.variableDeclaration(
34✔
45
                        'let', tempVarNodes.map(tempVarNode => t.variableDeclarator(tempVarNode, null))
34✔
46
                );
2✔
47
        }
2✔
48

2✔
49
        blockNode.body.unshift(...statementNodes);
2✔
50
}
2✔
51

2✔
52
/**
2✔
53
 * Insert tracker code, scope ID var and temp vars into function.
2✔
54
 * If function has complex params, insert into params. Otherwise, into function body.
2✔
55
 * @param {Object} fn - Function object
2✔
56
 * @param {Object} fnNode - Function AST node
34!
57
 * @param {Object} paramsBlock - Function params block object
×
58
 * @param {Object} [bodyBlock] - Function body block object
×
59
 *   (`undefined` if arrow function with no body block)
×
60
 * @param {Object} [trackerNode] - AST node for tracker call (or undefined if no tracker to insert)
×
61
 * @param {Object} state - State object
34✔
62
 * @returns {undefined}
2✔
63
 */
2✔
64
function insertTrackerCodeIntoFunction(fn, fnNode, paramsBlock, bodyBlock, trackerNode, state) {
26,428✔
65
        const {firstComplexParamIndex} = fn;
26,428✔
66
        if (firstComplexParamIndex === undefined) {
26,428✔
67
                insertTrackerCodeIntoFunctionBody(fnNode, paramsBlock, bodyBlock, trackerNode, state);
24,492✔
68
        } else {
26,428✔
69
                insertTrackerCodeIntoFunctionParams(
1,936✔
70
                        fnNode, paramsBlock, bodyBlock, trackerNode, firstComplexParamIndex, state
1,936✔
71
                );
1,936✔
72
        }
1,936✔
73
}
26,428✔
74

62✔
75
/**
62✔
76
 * Insert tracker code, scope ID var and temp vars into function body.
62✔
77
 * @param {Object} fnNode - Function AST node
62✔
78
 * @param {Object} paramsBlock - Function params block object
62✔
79
 * @param {Object} [bodyBlock] - Function body block object
62✔
80
 *   (`undefined` if arrow function with no body block)
34✔
81
 * @param {Object} [trackerNode] - AST node for tracker call (or undefined if no tracker to insert)
34✔
82
 * @param {Object} state - State object
34✔
83
 * @returns {undefined}
×
84
 */
×
85
function insertTrackerCodeIntoFunctionBody(fnNode, paramsBlock, bodyBlock, trackerNode, state) {
24,492✔
86
        // Exit if no tracker, scope ID var, or temp vars to insert
24,492✔
87
        const block = bodyBlock || paramsBlock;
24,492✔
88
        if (!trackerNode && !block.scopeIdVarNode) return;
24,492!
89

24,492✔
90
        // If body is not a block statement, convert it to one
24,492✔
91
        let bodyNode = fnNode.body;
24,492✔
92
        if (!bodyBlock) bodyNode = fnNode.body = t.blockStatement([t.returnStatement(bodyNode)]);
24,492✔
93

24,492✔
94
        // Insert scope ID and temp var nodes
24,492✔
95
        insertBlockVarsIntoBlockStatement(block, bodyNode, state);
24,492✔
96

24,492✔
97
        // Insert tracker call
24,492✔
98
        if (trackerNode) bodyNode.body.unshift(t.expressionStatement(trackerNode));
24,492✔
99
}
24,492✔
100

62✔
101
/**
62✔
102
 * Insert tracker code, scope ID var and temp vars into function params.
62✔
103
 *
62✔
104
 * Objectives:
62✔
105
 *   - Insert tracker call before any code in function params which could produce side effects.
62✔
106
 *   - Initialize scope ID and temp vars before they may be used
62✔
107
 *     (including coping with scenario where error thrown in params)
32✔
108
 *   - Allow functions defined in params to access other params.
32✔
109
 *   - Allow functions defined in params to access vars outside function which are shadowed in
32✔
110
 *     function body.
32✔
111
 *   - Do not move initialization of params into function body, to:
32✔
112
 *     1. Preserve behavior of generator functions which execute params upon calling function
32!
113
 *        but only execute function body when `.next()` called on generator.
32✔
114
 *     2. Avoid complications with hoisted `var` statements and sloppy function declarations.
32✔
115
 *
32✔
116
 * See https://github.com/overlookmotel/livepack/issues/108
32✔
117
 *
32✔
118
 * Solution is to add an object pattern rest element to end of params.
32✔
119
 * Tracker call is added as key for object property and is first to be evaluated.
62✔
120
 * `livepack_tracker()` returns a dummy Symbol, so this property will never exist
62✔
121
 * on the rest object being destructured.
62✔
122
 * Any complex params are moved into an array deconstruction expression and will be evaluated
62✔
123
 * in original order.
62✔
124
 * If function already has a rest element, it's merged with the substitute.
62✔
125
 *
62✔
126
 * `(x = 1, {y}, ...z) => () => [x, y, z]` ->
62✔
127
 * `(
62✔
128
 *   livepack_temp1 = void 0, // `= void 0` preserves `fn.length = 0`
62✔
129
 *   livepack_temp2,
62✔
130
 *   ...{
62✔
131
 *     [ livepack_tracker(...) ]: [
62✔
132
 *       livepack_scopeId_1,
62✔
133
 *       livepack_temp3,
62✔
134
 *       x = 1,
62✔
135
 *       {y}
62✔
136
 *     ] = [
62✔
137
 *       livepack_getScopeId(),
62✔
138
 *       () => z = livepack_getScopeId.toRest(z) // Convert `z` from object to array,
62✔
139
 *       livepack_temp1,
62✔
140
 *       livepack_temp2
62✔
141
 *     ],
62✔
142
 *     ...z
62✔
143
 *   }
62✔
144
 * ) => ( livepack_temp3(), () => [x, y, z] )`
62✔
145
 *
62✔
146
 * @param {Object} fnNode - Function AST node
62✔
147
 * @param {Object} paramsBlock - Function params block object
62✔
148
 * @param {Object} [bodyBlock] - Function body block object
62✔
149
 *   (`undefined` if arrow function with no body block)
62✔
150
 * @param {Object} [trackerNode] - AST node for tracker call (or undefined if no tracker to insert)
62✔
151
 * @param {number} firstComplexParamIndex - Index of first param which is a complex param
62✔
152
 * @param {Object} state - State object
62✔
153
 * @returns {undefined}
62✔
154
 */
62✔
155
function insertTrackerCodeIntoFunctionParams(
1,936✔
156
        fnNode, paramsBlock, bodyBlock, trackerNode, firstComplexParamIndex, state
1,936✔
157
) {
1,936✔
158
        // Exit if no tracker, scope ID var, or temp vars to insert
1,936✔
159
        const {scopeIdVarNode} = paramsBlock;
1,936✔
160
        if (!trackerNode && !scopeIdVarNode) return;
1,936!
161

1,936✔
162
        // Create object pattern to use as rest element.
1,936✔
163
        // `{ [ livepack_tracker(...) ]: [] = [] }`
1,936✔
164
        // Assignments will be added to each side of `[] = []` to init vars and assign to them.
1,936✔
165
        // If no tracker to insert (class constructor where class has prototype properties),
1,936✔
166
        // create `livepack_tracker()` call with no arguments.
1,936✔
167
        const leftNodes = [],
1,936✔
168
                rightNodes = [],
1,936✔
169
                propNodes = [
1,936✔
170
                        t.objectProperty(
1,936✔
171
                                trackerNode || t.callExpression(state.trackerVarNode, []),
1,936✔
172
                                t.assignmentPattern(t.arrayPattern(leftNodes), t.arrayExpression(rightNodes)),
1,936✔
173
                                true
1,936✔
174
                        )
1,936✔
175
                ],
1,936✔
176
                objNode = t.objectPattern(propNodes);
1,936✔
177

1,936✔
178
        function addAssignment(leftNode, rightNode) {
1,936✔
179
                leftNodes.push(leftNode);
2,516✔
180
                rightNodes.push(rightNode);
2,516✔
181
        }
2,516✔
182

1,936✔
183
        // Insert assignments for scope ID var node and temp var nodes
1,936✔
184
        if (scopeIdVarNode) {
2✔
185
                addAssignment(scopeIdVarNode, t.callExpression(state.getScopeIdVarNode, []));
2✔
186

2✔
187
                const {tempVarNodes} = paramsBlock;
2✔
188
                if (tempVarNodes) tempVarNodes.forEach(tempVarNode => addAssignment(tempVarNode, null));
2✔
189
        }
2✔
UNCOV
190

×
UNCOV
191
        // Flatten rest array param if present.
×
192
        // `(w, ...[x, {y}, ...z])` => `(w, x, {y}, ...z)`
2✔
193
        // `(x, ...[, , y, , z, , ])` => `(x, , , y, , z)`
2✔
194
        // `(...[x, ...[y, ...z]]])` => `(x, y, ...z)`
2✔
195
        // `(...[...[...x]]])` => `(...x)`
2✔
196
        // `(...[])` => `()`
2✔
197
        // If first param replacing rest array would cause function's `.length` to increase,
2!
198
        // record that it needs to have a default added to prevent this
2✔
199
        const paramNodes = fnNode.params;
2✔
200
        let lastParamIndex = paramNodes.length - 1,
2✔
201
                firstLengthTrucatingIndex = Infinity;
2✔
202
        if (flattenRestArray(paramNodes, lastParamIndex, objNode)) {
2✔
203
                const paramNode = paramNodes[firstComplexParamIndex];
2✔
204
                if (
2✔
205
                        paramNode === null || (paramNode.type !== 'AssignmentPattern' && paramNode.type !== 'RestElement')
2!
206
                ) {
✔
207
                        firstLengthTrucatingIndex = lastParamIndex;
2✔
208
                }
2✔
209
                lastParamIndex = paramNodes.length - 1;
2✔
210
        }
2✔
211

2✔
212
        // Handle rest element
2✔
213
        if (lastParamIndex !== -1) {
2✔
214
                const paramNode = paramNodes[lastParamIndex];
2✔
215
                if (paramNode.type === 'RestElement') {
8✔
216
                        const argumentNode = paramNode.argument;
8✔
217
                        if (argumentNode.type === 'Identifier') {
8✔
218
                                // Capture rest arguments as an object.
8✔
219
                                // Create a function which uses `livepack_getScopeId.toRest()` to convert it to an array.
2✔
220
                                // Insert call to this function into function body.
2✔
221
                                // `(...x) => x` -> `(...{
2!
222
                                //   [livepack_tracker(...)]: [livepack_temp_1] = [() => x = livepack_getScopeId.toRest(x)],
×
223
                                //   ...x
×
224
                                // }) => (livepack_temp_1(), x)`
×
225
                                // NB: Function to convert to array is placed inside params rather than in function body
×
226
                                // in case the var is shadowed inside function body by a function declaration.
×
227
                                // e.g. `(getX = () => x, ...x) => { function x() {}; return getX; }`
×
228
                                const convertToArrayNode = createTempVarNode(state);
2✔
229
                                addAssignment(
2✔
230
                                        convertToArrayNode,
2✔
231
                                        // `() => x = livepack_getScopeId.toRest(x)`
2✔
232
                                        t.arrowFunctionExpression(
2✔
233
                                                [],
2✔
234
                                                t.assignmentExpression('=', argumentNode, t.callExpression(
2✔
235
                                                        t.memberExpression(state.getScopeIdVarNode, t.identifier('toRest')),
2✔
236
                                                        [argumentNode]
2✔
237
                                                ))
2✔
238
                                        )
2✔
239
                                );
2✔
240

2✔
241
                                // Add call in function body
2✔
242
                                const callNode = t.callExpression(convertToArrayNode, []);
2✔
243
                                if (bodyBlock) {
2✔
244
                                        fnNode.body.body.unshift(t.expressionStatement(callNode));
2✔
245
                                } else {
2✔
246
                                        fnNode.body = t.sequenceExpression([callNode, fnNode.body]);
2!
247
                                }
×
248

×
249
                                // Add rest element to object pattern
×
250
                                propNodes.push(paramNode);
×
251
                        } else {
✔
252
                                // ObjectPattern - add properties to replacement object pattern
×
253
                                copyLocAndComments(objNode, paramNode);
×
254
                                propNodes.push(...argumentNode.properties);
2✔
255
                        }
2✔
256

2✔
257
                        // Remove rest element
2✔
258
                        paramNodes.pop();
2✔
259
                        lastParamIndex--;
2✔
260
                }
2✔
261
        }
2✔
262

2✔
263
        // Convert all other params to assignments in object pattern
2✔
264
        for (let index = firstComplexParamIndex; index <= lastParamIndex; index++) {
2✔
265
                // Replace param with a temp var
2✔
266
                const paramNode = paramNodes[index],
2✔
267
                        tempVarNode = createTempVarNode(state);
2✔
268
                paramNodes[index] = tempVarNode;
2✔
269

2✔
270
                // Add assignment to object pattern.
2✔
271
                // NB: Function params cannot be `null` usually, but `flattenRestArray()` can produce them.
2✔
272
                if (paramNode) {
2✔
273
                        addAssignment(paramNode, tempVarNode);
2✔
274
                        if (paramNode.type === 'AssignmentPattern' && firstLengthTrucatingIndex > index) {
2✔
275
                                firstLengthTrucatingIndex = index;
2✔
276
                        }
2✔
277
                }
2✔
278
        }
2✔
279

2✔
280
        // If above operations would change function's `.length` property by replacing what were
2✔
281
        // complex params with simple ones, add a default `= void 0` to param
2✔
282
        // to reduce `.length` to as it was originally
2✔
283
        if (firstLengthTrucatingIndex !== Infinity) {
2✔
284
                paramNodes[firstLengthTrucatingIndex] = t.assignmentPattern(
2✔
285
                        paramNodes[firstLengthTrucatingIndex], t.unaryExpression('void', t.numericLiteral(0))
2✔
286
                );
2✔
287
        }
2✔
288

2✔
289
        // Add rest object pattern to function params
2✔
290
        paramNodes.push(t.restElement(objNode));
2✔
291
}
2✔
292

2✔
293
/**
2✔
294
 * Flatten rest array.
2✔
295
 * If last element of rest array is itself a rest array, recursively flatten.
2✔
296
 * Mutates `paramNodes` passed in.
2✔
297
 * e.g. `...[x, y]` -> `x, y`, `...[...[x]]` -> `x`
2✔
298
 * @param {Array<Object>} paramNodes - Function params or rest argument elements AST nodes
2✔
299
 * @param {number} index - `paramNodes.length - 1`
2!
UNCOV
300
 * @param {Object} objNode - Object pattern AST node
×
301
 * @returns {boolean} - `true` if flattened a rest array
2✔
302
 */
2✔
303
function flattenRestArray(paramNodes, index, objNode) {
1,984✔
304
        // Exit if not a rest array
1,984!
305
        const paramNode = paramNodes[index];
1,984✔
306
        if (paramNode.type !== 'RestElement') return false;
1,984✔
307
        const argumentNode = paramNode.argument;
120✔
308
        if (argumentNode.type !== 'ArrayPattern') return false;
858✔
309

48✔
310
        // Copy comments from array pattern as it's going to be deleted
48✔
311
        copyLocAndComments(objNode, argumentNode);
48✔
312

48✔
313
        // Remove empty elements from end of array pattern. Exit if none remaining.
48✔
314
        const elementNodes = argumentNode.elements;
48✔
315
        let lastElementIndex = elementNodes.length - 1;
48✔
316
        while (true) { // eslint-disable-line no-constant-condition
48✔
317
                if (lastElementIndex === -1) {
48!
318
                        paramNodes.pop();
×
319
                        return true;
×
320
                }
×
321
                if (elementNodes[lastElementIndex]) break;
48✔
322
                elementNodes.pop();
×
323
                lastElementIndex--;
×
324
        }
×
325

48✔
326
        // Recusively flatten
48✔
327
        flattenRestArray(elementNodes, lastElementIndex, objNode);
48✔
328
        paramNodes.splice(index, 1, ...elementNodes);
48✔
329

48✔
330
        return true;
48✔
331
}
1,984✔
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