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

overlookmotel / livepack / 7308909869

23 Dec 2023 02:53PM UTC coverage: 90.423% (-0.03%) from 90.448%
7308909869

push

github

overlookmotel
TODO

4807 of 5168 branches covered (0.0%)

Branch coverage included in aggregate %.

12821 of 14327 relevant lines covered (89.49%)

9003.99 hits per line

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

95.05
/lib/serialize/functions.js
1
/* --------------------
64✔
2
 * livepack module
64✔
3
 * Serialize functions
64✔
4
 * ------------------*/
64✔
5

64✔
6
'use strict';
64✔
7

64✔
8
// Modules
64✔
9
const util = require('util'),
64✔
10
        traverse = require('@babel/traverse').default,
64✔
11
        typeOf = require('native-type-of'),
64✔
12
        assert = require('simple-invariant'),
64✔
13
        upperFirst = require('lodash/upperFirst'),
64✔
14
        {isString} = require('is-it-type'),
64✔
15
        t = require('@babel/types');
64✔
16

64✔
17
// Imports
64✔
18
const {activateTracker, getTrackerResult, trackerError} = require('../shared/tracker.js'),
64✔
19
        {enableWithBypass, disableWithBypass} = require('../shared/with.js'),
64✔
20
        specialFunctions = require('../shared/internal.js').functions,
64✔
21
        {
64✔
22
                TRACKER_COMMENT_PREFIX,
64✔
23
                FN_TYPE_FUNCTION, FN_TYPE_ASYNC_FUNCTION, FN_TYPE_GENERATOR_FUNCTION,
64✔
24
                FN_TYPE_ASYNC_GENERATOR_FUNCTION, FN_TYPE_CLASS, EVAL_PLACEHOLDER
64✔
25
        } = require('../shared/constants.js'),
64✔
26
        {
64✔
27
                createRecord, createFile, createBlock, updateBlockParent, createScope, updateScopeParent,
64✔
28
                createDependency, createAssignment
64✔
29
        } = require('./records.js'),
64✔
30
        {recordIsCircular, deleteFirst} = require('./utils.js'),
64✔
31
        assertBug = require('../shared/assertBug.js');
64✔
32

64✔
33
// Exports
64✔
34

64✔
35
const functionToString = Function.prototype.toString;
64✔
36

64✔
37
/* eslint-disable no-empty-function, prefer-arrow-callback */
64✔
38
const GeneratorPrototype = Object.getPrototypeOf(function*() {}),
64✔
39
        AsyncFunctionPrototype = Object.getPrototypeOf(async function() {}),
64✔
40
        AsyncGeneratorPrototype = Object.getPrototypeOf(async function*() {});
64✔
41
/* eslint-enable no-empty-function, prefer-arrow-callback */
64✔
42

64✔
43
const trackerCommentRegex = new RegExp(
64✔
44
        `/\\*${TRACKER_COMMENT_PREFIX}(\\d+);(${FN_TYPE_FUNCTION}|${FN_TYPE_ASYNC_FUNCTION}|${FN_TYPE_GENERATOR_FUNCTION}|${FN_TYPE_ASYNC_GENERATOR_FUNCTION}|${FN_TYPE_CLASS});(.*?)\\*/`
64✔
45
);
64✔
46

64✔
47
module.exports = {
64✔
48
        serializeFunction(fn, record) {
64✔
49
                // Rename var if function has name
18,178✔
50
                const {varNode} = record,
18,178✔
51
                        fnName = Object.getOwnPropertyDescriptor(fn, 'name')?.value;
18,178✔
52
                if (isString(fnName) && fnName !== '') {
18,178✔
53
                        varNode.name = fnName;
7,216✔
54
                } else if (varNode.name === 'constructor') {
18,178✔
55
                        varNode.name = 'anonymous';
232✔
56
                }
232✔
57

18,178✔
58
                // Handle special functions (including bound functions)
18,178✔
59
                const specialInfo = specialFunctions.get(fn);
18,178✔
60
                if (specialInfo) return this.serializeSpecialFunction(fn, specialInfo, record);
18,178✔
61

16,932✔
62
                // Get function code + info from tracker comment
16,932✔
63
                const js = functionToString.call(fn);
16,932✔
64
                const commentMatch = js.match(trackerCommentRegex);
16,932✔
65

16,932✔
66
                assertBug(
16,932✔
67
                        commentMatch,
16,932✔
68
                        'Failed to serialize function because it is not instrumented',
16,932✔
69
                        "The function is likely in Node's internal codebase",
16,932✔
70
                        () => `Function JS:\n${js}\nTrace:\n${this.getTraceStack()}\n`
16,932✔
71
                );
16,932✔
72

16,932✔
73
                const fnId = +commentMatch[1],
16,932✔
74
                        filename = JSON.parse(`"${commentMatch[3]}"`);
16,932✔
75

16,932✔
76
                // Get/create file
16,932✔
77
                const {files} = this;
16,932✔
78
                const {blocks, functions} = files[filename] || createFile(filename, files);
18,178✔
79

18,178✔
80
                // Parse function code if not encountered before
18,178✔
81
                let fnDef = functions.get(fnId),
18,178✔
82
                        scopeDefs, isClass, isAsync, isGenerator;
18,178✔
83
                const isNotParsedBefore = !fnDef;
18,178✔
84
                if (isNotParsedBefore) {
18,178✔
85
                        const fnType = +commentMatch[2];
15,898✔
86
                        isClass = fnType === FN_TYPE_CLASS;
15,898✔
87
                        isAsync = (fnType & FN_TYPE_ASYNC_FUNCTION) !== 0; // eslint-disable-line no-bitwise
15,898✔
88
                        isGenerator = (fnType & FN_TYPE_GENERATOR_FUNCTION) !== 0; // eslint-disable-line no-bitwise
15,898✔
89
                } else {
18,178✔
90
                        ({scopeDefs, isClass, isAsync, isGenerator} = fnDef);
1,034✔
91
                }
1,034✔
92

16,932✔
93
                // Call function to extract scope values (unless no scope to extract)
16,932✔
94
                let getFnInfo, scopeVars;
16,932✔
95
                if (isNotParsedBefore || scopeDefs.size > 0) {
18,178✔
96
                        ({getFnInfo, scopeVars} = this.trackFunction(fn, isClass, isGenerator, isAsync, js));
16,900✔
97
                } else {
18,178✔
98
                        scopeVars = [];
32✔
99
                }
32✔
100

16,932✔
101
                if (isNotParsedBefore) {
18,038✔
102
                        // Function not encountered before - parse function
15,898✔
103
                        fnDef = this.parseFunction(fn, fnId, getFnInfo, isClass, isAsync, isGenerator, filename);
15,898✔
104
                        scopeDefs = fnDef.scopeDefs;
15,898✔
105

15,898✔
106
                        fnDef.scopes = new Map(); // Keyed by scope object, value of each is function instance's record
15,898✔
107
                        fnDef.virtualBlock = undefined;
15,898✔
108
                        fnDef.internalScopeParams = undefined; // May be set in `processBlock()`
15,898✔
109
                        fnDef.isScopeInternalOnly = false; // May be set in `processBlock()`
15,898✔
110

15,898✔
111
                        functions.set(fnId, fnDef);
15,898✔
112

15,898✔
113
                        // Create blocks
15,898✔
114
                        let parentBlock = this.rootBlock;
15,898✔
115
                        for (const [blockId, scopeDef] of scopeDefs) {
15,898✔
116
                                // Get/create block
8,870✔
117
                                let block = blocks.get(blockId);
8,870✔
118
                                if (!block) {
8,870✔
119
                                        block = createBlock(blockId, scopeDef.blockName || 'anon', parentBlock);
6,316✔
120
                                        blocks.set(blockId, block);
6,316✔
121
                                } else {
8,870✔
122
                                        updateBlockParent(block, parentBlock);
2,554✔
123
                                }
2,554✔
124

8,870✔
125
                                // Record argument names.
8,870✔
126
                                // In sloppy mode, `arguments` and the argument vars themselves are linked.
8,870✔
127
                                // e.g. `function f(x) { arguments[0] = 1; return () => x; }` sets `x` to `1`.
8,870✔
128
                                // Therefore any use of `arguments` makes all the vars in that function's arguments mutable.
8,870✔
129
                                // TODO: Skip this if `arguments` is strict mode.
8,870✔
130
                                // NB: Whether `arguments` is sloppy mode depends on whether function which *originates*
8,870✔
131
                                // `arguments` is strict mode, not on whether function in which `arguments[0] =`
8,870✔
132
                                // appears is strict mode.
8,870✔
133
                                // i.e. `function f(x) { (() => {'use strict'; arguments[0] = 2; })(); return x; }(1) === 2`
8,870✔
134
                                // `arguments` has sloppy mode behaviour because outer function is sloppy mode.
8,870✔
135
                                // TODO: Also skip this if properties of `arguments` are only read from, not mutated.
8,870✔
136
                                const {params} = block,
8,870✔
137
                                        {vars} = scopeDef;
8,870✔
138
                                let {argNames} = block;
8,870✔
139
                                if (!argNames && vars.arguments && fnDef.argNames) {
8,870✔
140
                                        argNames = block.argNames = new Set(fnDef.argNames);
352✔
141

352✔
142
                                        for (const argName of fnDef.argNames) {
352✔
143
                                                const param = params.get(argName);
24✔
144
                                                if (param) param.isMutable = true;
24!
145
                                        }
24✔
146
                                }
352✔
147

8,870✔
148
                                // Add var names to block
8,870✔
149
                                for (const [varName, {isAssignedTo, isFrozenName}] of Object.entries(vars)) {
8,870✔
150
                                        const param = params.get(varName);
11,926✔
151
                                        if (!param) {
11,926✔
152
                                                params.set(varName, {isMutable: isAssignedTo || argNames?.has(varName), isFrozenName});
8,788✔
153
                                        } else {
11,926✔
154
                                                if (isAssignedTo) param.isMutable = true;
3,138✔
155
                                                if (isFrozenName) param.isFrozenName = true;
3,138✔
156
                                        }
3,138✔
157
                                }
11,926✔
158

8,870✔
159
                                parentBlock = block;
8,870✔
160
                        }
8,870✔
161

15,898✔
162
                        // Flag that block contains `eval()` if function does.
15,898✔
163
                        // Blocks above this will have `containsEval` set later on after block tree is finalised.
15,898✔
164
                        // Later functions may cause blocks to be inserted into the block chain above this function later
15,898✔
165
                        // so can't do it now.
15,898✔
166
                        if (fnDef.containsEval) parentBlock.containsEval = true;
15,898✔
167
                }
15,898✔
168

16,932✔
169
                // Record scope values
16,932✔
170
                let block = this.rootBlock,
16,932✔
171
                        scope = this.rootScope;
16,932✔
172
                [...scopeDefs.entries()].forEach(([blockId, {vars}], blockIndex) => {
16,932✔
173
                        const [scopeId, ...values] = scopeVars[blockIndex],
10,192✔
174
                                parentScope = scope;
10,192✔
175

10,192✔
176
                        // Get/create scope
10,192✔
177
                        block = blocks.get(blockId);
10,192✔
178
                        scope = block.scopes.get(scopeId);
10,192✔
179
                        if (!scope) {
10,192✔
180
                                scope = createScope(scopeId, block, parentScope);
7,222✔
181
                        } else {
10,192✔
182
                                updateScopeParent(scope, parentScope);
2,970✔
183
                        }
2,970✔
184

10,192✔
185
                        // Add values to scope
10,192✔
186
                        const {values: scopeValues, record: scopeRecord} = scope;
10,192✔
187
                        Object.entries(vars).forEach(([varName, {isReadFrom}], varIndex) => {
10,192✔
188
                                if (!isReadFrom) return;
13,576✔
189

13,216✔
190
                                let scopeValue = scopeValues[varName];
13,216✔
191
                                if (!scopeValue) {
13,560✔
192
                                        const val = values[varIndex];
10,350✔
193
                                        const valRecord = this.serializeValue(
10,350✔
194
                                                val, varName, `<scope var '${varName}' in function '${fn.name}' (ID ${fnId}) at ${filename}>`
10,350✔
195
                                        );
10,350✔
196

10,350✔
197
                                        // Re-check for existence in `scopeValues` in case value contains another function
10,350✔
198
                                        // in this scope which references this same value. In that case, `scopeValues` may have
10,350✔
199
                                        // been populated during `this.serializeValue()` above.
10,350✔
200
                                        // Need to check otherwise may over-write `isCircular` with false when was previously true.
10,350✔
201
                                        scopeValue = scopeValues[varName];
10,350✔
202
                                        if (!scopeValue) {
10,350✔
203
                                                scopeValues[varName] = {record: valRecord, isCircular: recordIsCircular(valRecord)};
9,900✔
204

9,900✔
205
                                                // Create dependency between scope and value.
9,900✔
206
                                                // Dependency object is missing `.node` and `.key` properties at this stage.
9,900✔
207
                                                // These will be set in `processBlock()`.
9,900✔
208
                                                // `undefined` is ignored as it's a special case - may be omitted from `createScope()` call.
9,900✔
209
                                                if (val !== undefined) createDependency(scopeRecord, valRecord, undefined, undefined);
9,900✔
210
                                        } else if (!scopeValue.isCircular && recordIsCircular(valRecord)) {
10,350!
211
                                                // TODO: I think this should be executed even if first check for existence of
×
212
                                                // `scopeValues[varName]` fails
×
213
                                                scopeValue.isCircular = true;
×
214
                                        }
×
215
                                }
10,350✔
216
                        });
10,192✔
217
                });
16,932✔
218

16,932✔
219
                // Place function in block
16,932✔
220
                let fnScopes = fnDef.scopes;
16,932✔
221
                if (isNotParsedBefore) {
18,074✔
222
                        // First instance of this function encountered
15,880✔
223
                        // Nest function in block
15,880✔
224
                        block.functions.push(fnDef);
15,880✔
225
                } else {
18,178✔
226
                        // Instance of this function encountered before
1,034✔
227
                        let {virtualBlock} = fnDef;
1,034✔
228
                        if (fnScopes.has(scope)) {
1,034✔
229
                                // Same function instantiated twice in same scope.
88✔
230
                                // Create new virtual block to hold function.
88✔
231
                                virtualBlock = createBlock(null, fn.name || 'anonymous', block);
88✔
232
                                if (fnDef.containsEval) virtualBlock.containsEval = true;
88!
233
                                fnDef.virtualBlock = virtualBlock;
88✔
234

88✔
235
                                // Move function into new block
88✔
236
                                virtualBlock.functions.push(fnDef);
88✔
237
                                block.functions = block.functions.filter(thisFnDef => thisFnDef !== fnDef);
88✔
238

88✔
239
                                // Move all previous instances into virtual scopes (one virtual scope for each parent scope)
88✔
240
                                const oldFnScopes = fnScopes;
88✔
241
                                fnScopes = new Map();
88✔
242
                                for (const [instanceScope, instanceRecord] of oldFnScopes) {
88✔
243
                                        const virtualScope = createScope(null, virtualBlock, instanceScope);
88✔
244
                                        fnScopes.set(virtualScope, instanceRecord);
88✔
245
                                }
88✔
246
                                fnDef.scopes = fnScopes;
88✔
247
                        }
88✔
248

1,034✔
249
                        // If some scopes hold multiple instances of this function, use virtual block to hold scope
1,034✔
250
                        if (virtualBlock) scope = createScope(null, virtualBlock, scope);
1,034✔
251
                }
1,034✔
252

16,914✔
253
                // Record this instance of function's record, along with scope
16,914✔
254
                fnScopes.set(scope, record);
16,914✔
255

16,914✔
256
                // Record scope on record
16,914✔
257
                record.scope = scope;
16,914✔
258

16,914✔
259
                // Return node with placeholder for function definition - definition will be added later
16,914✔
260
                return this.wrapFunctionWithProperties(
16,914✔
261
                        fn, record, t.identifier('x'), fnDef.name, fnDef.numParams,
16,914✔
262
                        isClass, fnDef.isClassWithSuperClass, isAsync, isGenerator, fnDef.isArrow, fnDef.isMethod
16,914✔
263
                );
16,914✔
264
        },
64✔
265

64✔
266
        /**
64✔
267
         * Call function with tracker active to extract function info and scope values.
64✔
268
         * @param {Function} fn - Function
64✔
269
         * @param {boolean} isClass - `true` if is class
64✔
270
         * @param {boolean} isGenerator - `true` if is generator function
64✔
271
         * @param {boolean} isAsync - `true` if is async function
64✔
272
         * @param {string} js - Function code
64✔
273
         * @returns {Object} - Object with properties:
64✔
274
         *   {Function} .getFnInfo - Function to call to get info about function
64✔
275
         *   {Array<Array<*>>} .scopeVars - Array of scope vars split by block
64✔
276
         * @throws {Error} - If failed to extract scope vars
64✔
277
         */
64✔
278
        trackFunction(fn, isClass, isGenerator, isAsync, js) {
64✔
279
                // Activate tracker
16,900✔
280
                activateTracker();
16,900✔
281

16,900✔
282
                // Call function to trigger tracker
16,900✔
283
                let res,
16,900✔
284
                        functionThrew = false,
16,900✔
285
                        errorMessage;
16,900✔
286
                try {
16,900✔
287
                        if (isClass) {
16,900✔
288
                                res = new fn(); // eslint-disable-line new-cap
2,192✔
289
                        } else {
16,900✔
290
                                res = fn();
14,708✔
291
                                if (isGenerator) res = res.next();
14,708✔
292
                        }
14,708✔
293
                } catch (err) {
16,900✔
294
                        functionThrew = true;
16,704✔
295
                        if (err !== trackerError) errorMessage = getErrorMessage(err);
16,704!
296
                }
16,704✔
297

16,900✔
298
                // Get result of tracker call.
16,900✔
299
                // Needs to be called even if unexpected error and tracker was not called,
16,900✔
300
                // as it also deactivates the tracker.
16,900✔
301
                const {getFnInfo, getScopes} = getTrackerResult();
16,900✔
302

16,900✔
303
                // Ensure tracker was called in async function or threw in non-async function
16,900✔
304
                if (isAsync) {
16,900✔
305
                        if (res instanceof Promise) {
204✔
306
                                res.catch(() => {}); // Prevent unhandled rejection
196✔
307
                        } else if (!errorMessage && !(isGenerator && functionThrew)) {
204!
308
                                // Async generators throw synchronously if tracker is in params not in function body
×
309
                                errorMessage = 'Expected function to return Promise';
×
310
                        }
×
311
                } else if (!functionThrew) {
16,900!
312
                        errorMessage = 'Expected tracker to throw error';
×
313
                }
×
314

16,900✔
315
                // Call `getScopes()` to get scope vars.
16,900✔
316
                // Enable bypass for any `with` statements, so they don't interfere with getting values of vars.
16,900✔
317
                let scopeVars;
16,900✔
318
                if (!errorMessage) {
16,900✔
319
                        enableWithBypass();
16,900✔
320
                        try {
16,900✔
321
                                scopeVars = getScopes();
16,900✔
322
                        } catch (err) {
16,900!
323
                                errorMessage = getErrorMessage(err);
×
324
                        }
×
325
                        disableWithBypass();
16,900✔
326
                }
16,900✔
327

16,900✔
328
                assertBug(
16,900✔
329
                        !errorMessage,
16,900✔
330
                        'Failed to extract scope vars from function',
16,900✔
331
                        () => `Function JS:\n${js}\nError:${errorMessage}\nTrace:\n${this.getTraceStack()}\n`
16,900✔
332
                );
16,900✔
333

16,900✔
334
                return {getFnInfo, scopeVars};
16,900✔
335
        },
64✔
336

64✔
337
        serializeSpecialFunction(fn, info, record) { // eslint-disable-line consistent-return
64✔
338
                const {type} = info;
1,246✔
339
                if (type === 'require') return this.serializeRequireFunction(info);
1,246✔
340
                if (type === 'eval') return this.serializeEvalFunction(fn);
1,210✔
341
                if (type === 'bound') return this.serializeBoundFunction(fn, info, record);
1,170✔
342
                if (type === 'promisify') return this.serializePromisifyFunction(info, record);
1,082✔
343
                if (type === 'callbackify') return this.serializeCallbackifyFunction(info, record);
1,082✔
344
                if (type === 'debuglog') return this.serializeDebuglogFunction(info, record);
1,066✔
345
                if (type === 'splitAsync') return this.serializeSplitAsyncFunction(info, record);
1,034✔
346
                assertBug(false, `Unexpected special function type '${type}'`);
×
347
        },
64✔
348

64✔
349
        /**
64✔
350
         * Serialize `require()` function. Throws error.
64✔
351
         * @param {Object} info - Special function details
64✔
352
         * @param {string} info.path - File path where `require` function created
64✔
353
         * @returns {undefined}
64✔
354
         * @throws {Error} - Error
64✔
355
         */
64✔
356
        serializeRequireFunction(info) {
64✔
357
                throw new Error(`Cannot serialize \`require\` or \`import\` (in ${info.path})`);
36✔
358
        },
64✔
359

64✔
360
        /**
64✔
361
         * Serialize local `eval` shim function.
64✔
362
         * This is a hacky implementation to substitute local `eval` shim functions which are
64✔
363
         * created for each file with global `eval`.
64✔
364
         * @param {Function} fn - Local `eval` shim function
64✔
365
         * @returns {Object} - Special placeholder signal
64✔
366
         */
64✔
367
        serializeEvalFunction(fn) {
64✔
368
                // Delete record for `eval` shim
40✔
369
                this.records.set(fn);
40✔
370

40✔
371
                // Serialize global `eval` (if not already serialized)
40✔
372
                let {evalRecord} = this;
40✔
373
                if (!evalRecord) {
40✔
374
                        evalRecord = this.serializeValueInner(eval, 'eval'); // eslint-disable-line no-eval
32✔
375
                        this.evalRecord = evalRecord;
32✔
376
                }
32✔
377

40✔
378
                // Return placeholder signal.
40✔
379
                // `serializeValueInner()` will substitute `this.evalRecord` as the returned record.
40✔
380
                return EVAL_PLACEHOLDER;
40✔
381
        },
64✔
382

64✔
383
        /**
64✔
384
         * Serialize a bound function.
64✔
385
         * If there are no circular vars, output `fn.bind(ctx, var0, var1)`.
64✔
386
         * If there are circular vars, create a scope into which bound values can be injected.
64✔
387
         *
64✔
388
         * @param {Function} fn - Function
64✔
389
         * @param {Object} binding - Binding record
64✔
390
         * @param {Object} record - Record for bound function instance
64✔
391
         * @returns {Object} - Node for bound function
64✔
392
         */
64✔
393
        serializeBoundFunction(fn, binding, record) {
64✔
394
                // Serialize unbound function
88✔
395
                const fnVarName = record.varNode.name;
88✔
396
                const unboundFn = binding.fn;
88✔
397
                const unboundFnRecord = this.serializeValue(unboundFn, `${fnVarName}Unbound`, '<unbound>');
88✔
398
                const fnIsCircular = recordIsCircular(unboundFnRecord);
88✔
399

88✔
400
                // Serialize binding values
88✔
401
                let isCircular = fnIsCircular;
88✔
402
                const args = [];
88✔
403
                const valRecords = binding.vars.map((val, index) => {
88✔
404
                        const varName = `${fnVarName}${index === 0 ? 'BoundCtx' : `BoundValue${index - 1}`}`;
280✔
405
                        const valRecord = this.serializeValue(
280✔
406
                                val, varName, index === 0 ? '<bound context>' : `<bound value ${index - 1}>`
280✔
407
                        );
280✔
408
                        if (recordIsCircular(valRecord)) isCircular = true;
280✔
409
                        args[index] = valRecord.varNode;
280✔
410
                        return valRecord;
280✔
411
                });
88✔
412

88✔
413
                // Create `fn.bind(ctx, val0, val1)`
88✔
414
                const bindNode = t.memberExpression(unboundFnRecord.varNode, t.identifier('bind'));
88✔
415
                const callNode = t.callExpression(bindNode, args);
88✔
416

88✔
417
                // If there are circular references, use `createBinding()` to create wrapper
88✔
418
                // and inject bound function later
88✔
419
                let targetRecord, node, numParams;
88✔
420
                if (isCircular) {
88✔
421
                        // Create call to `createBinding` function
48✔
422
                        const createBindingRecord = this.serializeRuntime('createBinding');
48✔
423

48✔
424
                        // Create binding (binding is result of calling `createBinding()`)
48✔
425
                        const bindingRecord = createRecord(`binding${upperFirst(fnVarName)}`);
48✔
426
                        const bindingNode = t.callExpression(createBindingRecord.varNode, []);
48✔
427
                        bindingRecord.node = bindingNode;
48✔
428
                        createDependency(bindingRecord, createBindingRecord, bindingNode, 'callee');
48✔
429

48✔
430
                        // Create node for wrapped bound function - `binding[0]`
48✔
431
                        node = t.memberExpression(bindingRecord.varNode, t.numericLiteral(0), true);
48✔
432
                        createDependency(record, bindingRecord, node, 'object');
48✔
433

48✔
434
                        // Create inject function - `binding[1]`
48✔
435
                        const injectRecord = createRecord(`injectIntoBinding${upperFirst(fnVarName)}`);
48✔
436
                        const injectFnNode = t.memberExpression(bindingRecord.varNode, t.numericLiteral(1), true);
48✔
437
                        injectRecord.node = injectFnNode;
48✔
438
                        createDependency(injectRecord, bindingRecord, injectFnNode, 1);
48✔
439

48✔
440
                        // Create assignment to inject bound function into binding
48✔
441
                        const assignmentNode = t.callExpression(injectRecord.varNode, [callNode]);
48✔
442
                        const assignment = createAssignment(record, assignmentNode);
48✔
443
                        createDependency(assignment, injectRecord, assignmentNode, 'callee');
48✔
444

48✔
445
                        targetRecord = assignment;
48✔
446
                        numParams = 0;
48✔
447
                } else {
88✔
448
                        // No circular references - use `fn.bind(ctx, val0, val1)` as node
40✔
449
                        targetRecord = record;
40✔
450
                        node = callNode;
40✔
451

40✔
452
                        const unboundLength = Math.floor(unboundFn.length);
40✔
453
                        numParams = Number.isNaN(unboundLength)
40✔
454
                                ? 0
40!
455
                                : Math.max(unboundLength - Math.max(args.length - 1, 0), 0);
40✔
456
                }
40✔
457

88✔
458
                // Create dependencies
88✔
459
                createDependency(targetRecord, unboundFnRecord, bindNode, 'object');
88✔
460
                valRecords.forEach((valRecord, index) => createDependency(targetRecord, valRecord, args, index));
88✔
461

88✔
462
                // Return node
88✔
463
                // fn, record, node, name, numParams, isClass, isAsync, isGenerator, isArrowOrBound, isMethod
88✔
464
                return this.wrapFunctionWithProperties(
88✔
465
                        fn, record, node, isCircular ? '' : `bound ${unboundFn.name}`, numParams,
88✔
466
                        false, false, false, false, true, false
88✔
467
                );
88✔
468
        },
64✔
469

64✔
470
        /**
64✔
471
         * `require('util').promisify()` returns functions.
64✔
472
         * These are in Node's internal JS code, and therefore cannot be serialized.
64✔
473
         * They are captured in `lib/init/module.js`.
64✔
474
         * Serialize as a call to `require('util').promisify()` with same arg as original call.
64✔
475
         *
64✔
476
         * @param {Object} info - Special function details
64✔
477
         * @param {Function} info.fn - Function `promisify` was called with
64✔
478
         * @param {Object} record - Record for promisified function instance
64✔
479
         * @returns {Object} - Node for promisified function
64✔
480
         */
64✔
481
        serializePromisifyFunction({fn}, record) {
64✔
482
                const fnVarName = record.varNode.name;
8✔
483
                const fnRecord = this.serializeValue(fn, `${fnVarName}Unpromisified`, '<unpromisified>');
8✔
484

8✔
485
                // TODO: Handle circular references
8✔
486
                assert(!recordIsCircular(fnRecord), 'Cannot handle circular-referenced promisified functions');
8✔
487

8✔
488
                const promisifyRecord = this.serializeValue(util.promisify);
8✔
489
                const node = t.callExpression(promisifyRecord.varNode, [fnRecord.varNode]);
8✔
490
                createDependency(record, promisifyRecord, node, 'callee');
8✔
491
                createDependency(record, fnRecord, node.arguments, 0);
8✔
492

8✔
493
                // TODO: Add additional properties
8✔
494
                return node;
8✔
495
        },
64✔
496

64✔
497
        /**
64✔
498
         * `require('util').callbackify()` returns functions.
64✔
499
         * These are in Node's internal JS code, and therefore cannot be serialized.
64✔
500
         * They are captured in `lib/init/module.js`.
64✔
501
         * Serialize as a call to `require('util').callbackify()` with same arg as original call.
64✔
502
         *
64✔
503
         * @param {Object} info - Special function details
64✔
504
         * @param {Function} info.fn - Function `callbackify` was called with
64✔
505
         * @param {Object} record - Record for callbackified function instance
64✔
506
         * @returns {Object} - Node for callbackified function
64✔
507
         */
64✔
508
        serializeCallbackifyFunction({fn}, record) {
64✔
509
                const fnVarName = record.varNode.name;
8✔
510
                const fnRecord = this.serializeValue(fn, `${fnVarName}Uncallbackified`, '<uncallbackified>');
8✔
511

8✔
512
                // TODO: Handle circular references
8✔
513
                assert(!recordIsCircular(fnRecord), 'Cannot handle circular-referenced uncallbackified functions');
8✔
514

8✔
515
                const callbackifyRecord = this.serializeValue(util.callbackify);
8✔
516
                const node = t.callExpression(callbackifyRecord.varNode, [fnRecord.varNode]);
8✔
517
                createDependency(record, callbackifyRecord, node, 'callee');
8✔
518
                createDependency(record, fnRecord, node.arguments, 0);
8✔
519

8✔
520
                // TODO: Add additional properties
8✔
521
                return node;
8✔
522
        },
64✔
523

64✔
524
        /**
64✔
525
         * `require('util').debuglog()` returns functions.
64✔
526
         * These are in Node's internal JS code, and therefore cannot be serialized.
64✔
527
         * They are captured in `lib/init/functions.js`.
64✔
528
         * Serialize as a call to `require('util').debuglog()` with same args as original call.
64✔
529
         *
64✔
530
         * @param {Object} info - Special function details
64✔
531
         * @param {Array} info.args - Arguments `debuglog` was called with
64✔
532
         * @param {Object} record - Record for debuglog function instance
64✔
533
         * @returns {Object} - Node for debuglog function
64✔
534
         */
64✔
535
        serializeDebuglogFunction(info, record) {
64✔
536
                // TODO: Handle circular references
32✔
537
                const setRecord = this.serializeValue(info.set, 'debuglogSet', '<debuglog set argument>');
32✔
538
                const argumentsNodes = [setRecord.varNode];
32✔
539
                createDependency(record, setRecord, argumentsNodes, 0);
32✔
540

32✔
541
                const {cb} = info;
32✔
542
                if (cb !== undefined) {
32✔
543
                        const cbRecord = this.serializeValue(cb, 'debuglogCb', '<debuglog cb argument>');
16✔
544
                        argumentsNodes[1] = cbRecord.varNode;
16✔
545
                        createDependency(record, cbRecord, argumentsNodes, 1);
16✔
546
                }
16✔
547

32✔
548
                const debuglogRecord = this.serializeValue(util.debuglog);
32✔
549
                const node = t.callExpression(debuglogRecord.varNode, argumentsNodes);
32✔
550
                createDependency(record, debuglogRecord, node, 'callee');
32✔
551

32✔
552
                // TODO: Add additional properties
32✔
553
                return node;
32✔
554
        },
64✔
555

64✔
556
        wrapFunctionWithProperties(
64✔
557
                fn, record, node, name, numParams,
17,002✔
558
                isClass, isClassWithSuperClass, isAsync, isGenerator, isArrowOrBound, isMethod
17,002✔
559
        ) {
17,002✔
560
                // Set default `length` + `name` properties based on function definition
17,002✔
561
                const defaultProps = [
17,002✔
562
                        {name: 'length', value: numParams, writable: false, enumerable: false, configurable: true},
17,002✔
563
                        {name: 'name', value: name, writable: false, enumerable: false, configurable: true}
17,002✔
564
                ];
17,002✔
565

17,002✔
566
                // Get `prototype` property and set default `prototype` prop
17,002✔
567
                const defaultProto = isGenerator
17,002✔
568
                        ? isAsync ? AsyncGeneratorPrototype : GeneratorPrototype
17,002✔
569
                        : isAsync ? AsyncFunctionPrototype : Function.prototype;
17,002✔
570

17,002✔
571
                const proto = Object.getOwnPropertyDescriptor(fn, 'prototype')?.value;
17,002✔
572
                let protoValue;
17,002✔
573
                if (!isArrowOrBound && ((!isAsync && !isMethod) || isGenerator)) {
17,002✔
574
                        // Should have prototype
7,984✔
575
                        if (isGenerator) {
7,984✔
576
                                // Prototype should not have constructor
228✔
577
                                if (
228✔
578
                                        proto
228✔
579
                                                && Object.getPrototypeOf(proto) === defaultProto.prototype
228✔
580
                                                && Object.getOwnPropertyNames(proto).length === 0
228✔
581
                                                && Object.getOwnPropertySymbols(proto).length === 0
228✔
582
                                ) {
228✔
583
                                        const protoRecord = this.records.get(proto);
180✔
584
                                        if (protoRecord) {
180✔
585
                                                // Prototype already serialized - redefine as `fn.prototype`
32✔
586
                                                this.deleteDependencies(protoRecord);
32✔
587
                                                protoRecord.node = this.serializePrototype(protoRecord, record);
32✔
588
                                        }
32✔
589

180✔
590
                                        protoValue = proto;
180✔
591
                                }
180✔
592
                        } else {
7,984✔
593
                                // Prototype should be an object (not an Array, Set etc)
7,756✔
594
                                // and should have constructor - check constructor refers back to function
7,756✔
595
                                if (typeOf(proto) === 'Object') { // eslint-disable-line no-lonely-if
7,756✔
596
                                        const ctorDescriptor = Object.getOwnPropertyDescriptor(proto, 'constructor');
7,684✔
597
                                        if (ctorDescriptor) {
7,684✔
598
                                                const ctor = ctorDescriptor.value;
7,508✔
599
                                                if (ctor === fn) protoValue = proto;
7,508✔
600
                                        }
7,508✔
601
                                }
7,684✔
602
                        }
7,756✔
603

7,984✔
604
                        // Ensure prototype prop over-ridden where has been set to `undefined`
7,984✔
605
                        if (!protoValue && proto === undefined) protoValue = null;
7,984!
606

7,984✔
607
                        // Set default `prototype` property
7,984✔
608
                        defaultProps[2] = {
7,984✔
609
                                name: 'prototype', value: protoValue, writable: !isClass, enumerable: false, configurable: false
7,984✔
610
                        };
7,984✔
611
                }
7,984✔
612

17,002✔
613
                // Flag prototype as prototype
17,002✔
614
                if (proto) this.prototypes.set(proto, protoValue ? record : null);
17,002✔
615

17,002✔
616
                // Wrap with properties
17,002✔
617
                node = this.wrapWithProperties(
17,002✔
618
                        fn, record, node, defaultProto, defaultProps, functionShouldSkipKey, functionShouldForceDescriptor
17,002✔
619
                );
17,002✔
620

17,002✔
621
                // Set var name for `.prototype`
17,002✔
622
                if (protoValue === undefined && proto) {
17,002✔
623
                        const protoRecord = this.records.get(proto);
352✔
624
                        if (protoRecord) this.setPrototypeVarName(protoRecord, record);
352✔
625
                }
352✔
626

17,002✔
627
                // Add methods to prototype + set prototype descriptor
17,002✔
628
                if (protoValue && !isGenerator) {
17,002✔
629
                        // Classes with super class are defined with `extends null`
7,500✔
630
                        const defaultProtoProto = isClassWithSuperClass ? null : Object.prototype;
7,500✔
631

7,500✔
632
                        let protoIsAltered = false;
7,500✔
633
                        const propNames = Object.getOwnPropertyNames(proto);
7,500✔
634
                        if (propNames.length !== 1 || Object.getOwnPropertySymbols(proto).length !== 0) {
7,500✔
635
                                protoIsAltered = true;
1,200✔
636
                        } else if (propNames[0] !== 'constructor') {
7,500!
637
                                protoIsAltered = true;
×
638
                        } else if (Object.getPrototypeOf(proto) !== defaultProtoProto) {
6,300✔
639
                                protoIsAltered = true;
616✔
640
                        } else {
6,300✔
641
                                const ctorDescriptor = Object.getOwnPropertyDescriptor(proto, 'constructor');
5,684✔
642
                                if (!ctorDescriptor.writable || ctorDescriptor.enumerable || !ctorDescriptor.configurable) {
5,684✔
643
                                        protoIsAltered = true;
40✔
644
                                }
40✔
645
                        }
5,684✔
646

7,500✔
647
                        if (protoIsAltered) {
7,500✔
648
                                const protoRecord = this.serializeValue(proto, this.getPrototypeVarName(record), '.prototype');
1,856✔
649
                                this.withTrace(
1,856✔
650
                                        () => this.serializeProperties(
1,856✔
651
                                                proto, protoRecord, null, defaultProtoProto,
1,856✔
652
                                                [{name: 'constructor', value: fn, writable: true, enumerable: false, configurable: true}],
1,856✔
653
                                                undefined, undefined, true
1,856✔
654
                                        ),
1,856✔
655
                                        '.prototype'
1,856✔
656
                                );
1,856✔
657

1,856✔
658
                                const assignment = createAssignment(record, null);
1,856✔
659
                                assignment.dependencies.push({record: protoRecord});
1,856✔
660
                        }
1,856✔
661
                }
7,500✔
662

17,002✔
663
                // Return wrapped function node
17,002✔
664
                return node;
17,002✔
665
        },
64✔
666

64✔
667
        serializePrototype(protoRecord, ctorRecord) {
64✔
668
                this.setPrototypeVarName(protoRecord, ctorRecord);
2,656✔
669
                const node = t.memberExpression(ctorRecord.varNode, t.identifier('prototype'));
2,656✔
670
                createDependency(protoRecord, ctorRecord, node, 'object');
2,656✔
671
                protoRecord.prototypeOf = ctorRecord;
2,656✔
672
                return node;
2,656✔
673
        },
64✔
674

64✔
675
        setPrototypeVarName(protoRecord, fnRecord) {
64✔
676
                protoRecord.varNode.name = this.getPrototypeVarName(fnRecord);
2,992✔
677
        },
64✔
678

64✔
679
        getPrototypeVarName(fnRecord) {
64✔
680
                return `${fnRecord.varNode.name}Prototype`;
4,848✔
681
        },
64✔
682

64✔
683
        deleteDependencies(record) {
64✔
684
                this.deleteOwnDependencies(record);
224✔
685

224✔
686
                const {assignments} = record;
224✔
687
                if (assignments) {
224!
688
                        for (const assignment of assignments) {
×
689
                                this.deleteOwnDependencies(assignment);
×
690
                        }
×
691
                }
×
692
        },
64✔
693

64✔
694
        deleteOwnDependencies(record) {
64✔
695
                const {dependencies} = record;
224✔
696
                if (dependencies.length === 0) return;
224✔
697

160✔
698
                traverse(t.program([t.expressionStatement(record.node)]), {
160✔
699
                        Identifier: (path) => {
160✔
700
                                if (!identifierIsVariable(path)) return;
320✔
701

224✔
702
                                const {node, container, key} = path;
224✔
703
                                // eslint-disable-next-line no-shadow
224✔
704
                                const dependency = deleteFirst(dependencies, dependency => dependency.record.varNode === node),
224✔
705
                                        dependencyRecord = dependency.record,
224✔
706
                                        {dependents} = dependencyRecord;
224✔
707
                                deleteFirst(dependents, dependent => dependent.node === container && dependent.key === key);
224✔
708

224✔
709
                                if (dependents.length === 0) {
320✔
710
                                        this.deleteDependencies(dependencyRecord);
192✔
711
                                        if (dependencyRecord.val) this.records.delete(dependencyRecord.val);
192✔
712
                                }
192✔
713
                        },
160✔
714
                        noScope: true
160✔
715
                });
160✔
716
        }
224✔
717
};
64✔
718

64✔
719
function functionShouldSkipKey(key) {
46,816✔
720
        return key === 'arguments' || key === 'caller';
46,816✔
721
}
46,816✔
722

64✔
723
function functionShouldForceDescriptor(key) {
44,116✔
724
        return key === 'name';
44,116✔
725
}
44,116✔
726

64✔
727
/**
64✔
728
 * Check if identifier is being used as a variable.
64✔
729
 * true: `a`, `a = 1`, `a++`, `{}[a]`, `{}?.[a]`, `function a() {}`,
64✔
730
 *   `{ [a]: 1 }`, `{ [a]() {} }`, `class { [a]() {} }`
64✔
731
 * false: `{}.a`, `{}?.a`, `{a: 1}`, `{ a() {} }`, `class { a() {} }`,
64✔
732
 *   `a: while (0) {}`, `continue a`, `break a`, `import.meta`
64✔
733
 *
64✔
734
 * @param {Object} path - Babel path object for identifier
64✔
735
 * @returns {boolean} - `true` if is used as a variable
64✔
736
 */
64✔
737
function identifierIsVariable(path) {
320✔
738
        const {parentPath} = path;
320✔
739
        return !(parentPath.isMemberExpression({computed: false}) && path.key === 'property')
320✔
740
                && !(parentPath.isOptionalMemberExpression({computed: false}) && path.key === 'property')
320!
741
                && !(parentPath.isObjectProperty({computed: false}) && path.key === 'key')
320!
742
                && !(parentPath.isObjectMethod({computed: false}) && path.key === 'key')
320!
743
                && !(parentPath.isClassMethod({computed: false}) && path.key === 'key')
320!
744
                && !(parentPath.isPrivateName())
320✔
745
                && !(parentPath.isLabeledStatement() && path.key === 'label')
320!
746
                && !(parentPath.isContinueStatement() && path.key === 'label')
320!
747
                && !(parentPath.isBreakStatement() && path.key === 'label')
320!
748
                && !parentPath.isMetaProperty();
320✔
749
}
320✔
750

64✔
751
/**
64✔
752
 * Get error message from error.
64✔
753
 * Handle any input and try to avoid any side-effects.
64✔
754
 * Always returns a non-empty string.
64✔
755
 * @param {*} err - Error thrown
64✔
756
 * @returns {string} - Error message
64✔
757
 */
64✔
758
function getErrorMessage(err) {
×
759
        if (err == null) return 'Unknown error';
×
760

×
761
        try {
×
762
                const desc = Object.getOwnPropertyDescriptor(err, 'message');
×
763
                return desc?.value || 'Unknown error';
×
764
        } catch {
×
765
                return 'Unknown error';
×
766
        }
×
767
}
×
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