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

overlookmotel / livepack / 6373315401

01 Oct 2023 09:02PM UTC coverage: 67.542% (+0.002%) from 67.54%
6373315401

push

github

overlookmotel
Optimize serializing ArrayBuffers for zero values [perf]

1728 of 1991 branches covered (0.0%)

Branch coverage included in aggregate %.

19 of 19 new or added lines in 1 file covered. (100.0%)

10570 of 16217 relevant lines covered (65.18%)

2063.69 hits per line

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

20.3
/lib/serialize/parseFunction.js
1
/* --------------------
31✔
2
 * livepack module
31✔
3
 * Parse function code
31✔
4
 * ------------------*/
31✔
5

31✔
6
'use strict';
31✔
7

31✔
8
// Modules
31✔
9
const pathJoin = require('path').join,
31✔
10
        {isString} = require('is-it-type'),
31✔
11
        mapValues = require('lodash/mapValues'),
31✔
12
        t = require('@babel/types');
31✔
13

31✔
14
// Imports
31✔
15
const {isJsIdentifier, isNumberKey, setAddFrom} = require('./utils.js'),
31✔
16
        {
31✔
17
                isReservedVarName, createArrayOrPush, combineArraysWithDedup,
31✔
18
                getProp, getProps, setProp, traverseAll
31✔
19
        } = require('../shared/functions.js'),
31✔
20
        {
31✔
21
                SUPER_CALL, SUPER_EXPRESSION, CONST_VIOLATION_CONST, CONST_VIOLATION_FUNCTION_SILENT
31✔
22
        } = require('../shared/constants.js'),
31✔
23
        assertBug = require('../shared/assertBug.js');
31✔
24

31✔
25
// Constants
31✔
26
const RUNTIME_DIR_PATH = pathJoin(__dirname, '../runtime/');
31✔
27

31✔
28
// Exports
31✔
29

31✔
30
/**
31✔
31
 * Assemble AST for function code, identify Identifier nodes referring to internal/external variables,
31✔
32
 * replace const violations, transpile `super`, get other info about function.
31✔
33
 * Most of this information is pre-prepared by instrumentation, just needs some late processing here.
31✔
34
 *
31✔
35
 * @this {Object} Serializer
31✔
36
 * @param {Function} fn - Function
31✔
37
 * @param {number} fnId - Function ID
31✔
38
 * @param {Function} getFunctionInfo - Function info getter function
31✔
39
 * @param {boolean} isClass - `true` if is a class
31✔
40
 * @param {boolean} isAsync - `true` if is an async function
31✔
41
 * @param {boolean} isGenerator - `true` if is a generator function
31✔
42
 * @param {string} filename - File path
31✔
43
 * @returns {Object} - Function definition object with props:
31✔
44
 *   {Object} .node - AST node for function
31✔
45
 *   {Map} .scopeDefs - Scope definitions map, keyed by block ID
31✔
46
 *   {Object} .externalVars - Object keyed by var name, values are arrays of identifier nodes
31✔
47
 *   {Object} .internalVars - Object keyed by var name, values are arrays of identifier nodes
31✔
48
 *   {Set<string>} .globalVarNames - Set of names of global vars used
31✔
49
 *   {Set<string>} .functionNames - Set of function names used
31✔
50
 *   {string} .name - `.name` of created function
31✔
51
 *   {number} .numParams - `.length` of created function
31✔
52
 *   {boolean} .isClass - `true` if is class
31✔
53
 *   {boolean} .isAsync - `true` if is async function
31✔
54
 *   {boolean} .isGenerator - `true` if is generator
31✔
55
 *   {boolean} .isArrow - `true` if is arrow function
31✔
56
 *   {boolean} .isMethod - `true` if is a method
31✔
57
 *   {boolean|null} .isStrict - `true` if is strict mode, `false` if sloppy, `null` if indeterminate
31✔
58
 *     (classes and runtime functions are indeterminate)
31✔
59
 *   {boolean} .containsEval - `true` if contains direct `eval()`
31✔
60
 *   {Array<string>|undefined} .argNames - Array of `arguments` var names
31✔
61
 * @throws {Error} - If function contains `import`
31✔
62
 */
31✔
63
module.exports = function parseFunction(
31✔
64
        fn, fnId, getFunctionInfo, isClass, isAsync, isGenerator, filename
×
65
) {
×
66
        // Get function info and AST from info getter function
×
67
        const [fnInfoJson, getChildFnInfos, getSources] = getFunctionInfo();
×
68
        const fnInfo = JSON.parse(fnInfoJson);
×
69

×
70
        // Throw error if function contains `import()`
×
71
        if (fnInfo.containsImport) {
×
72
                const fnName = Object.getOwnPropertyDescriptor(fn, 'name')?.value,
×
73
                        fnDesc = (isString(fnName) && fnName !== '') ? `function '${fnName}'` : 'anonymous function';
×
74
                throw new Error(`Cannot serialize function containing \`import\` (${fnDesc} in '${filename}')`);
×
75
        }
×
76

×
77
        // Assemble scope definitions
×
78
        const scopeDefs = new Map(),
×
79
                externalVars = Object.create(null);
×
80
        for (const {blockId, blockName, vars} of fnInfo.scopes) {
×
81
                scopeDefs.set(blockId, {
×
82
                        blockName,
×
83
                        vars: mapValues(vars, (varProps, varName) => {
×
84
                                externalVars[varName] = [];
×
85
                                return {isReadFrom: !!varProps.isReadFrom, isAssignedTo: !!varProps.isAssignedTo};
×
86
                        })
×
87
                });
×
88
        }
×
89

×
90
        // Add child functions into AST, get external/internal var nodes, and get amendments to be made
×
91
        // (const violations / incidences of `super` to be transpiled)
×
92
        const internalVars = Object.create(null),
×
93
                globalVarNames = new Set(),
×
94
                functionNames = new Set(),
×
95
                amendments = [];
×
96
        resolveFunctionInfo(
×
97
                fnInfo, getChildFnInfos, false, fnId, scopeDefs,
×
98
                externalVars, internalVars, globalVarNames, functionNames, amendments
×
99
        );
×
100

×
101
        let node = fnInfo.ast;
×
102

×
103
        // If source maps enabled, add source files to `sourceFiles` map and set `loc.filename` for all nodes.
×
104
        // Create `copyLoc()` function to copy location info for AST nodes befing replaced.
×
105
        let copyLoc;
×
106
        if (this.options.sourceMaps) {
×
107
                const {filesHaveSourcesFor} = this;
×
108
                if (!filesHaveSourcesFor.has(filename)) {
×
109
                        Object.assign(this.sourceFiles, JSON.parse(getSources()));
×
110
                        filesHaveSourcesFor.add(filename);
×
111
                }
×
112

×
113
                traverseAll(node, ({loc}) => {
×
114
                        if (loc && !loc.filename) loc.filename = filename;
×
115
                });
×
116

×
117
                copyLoc = (destNode, srcNode) => {
×
118
                        destNode.start = srcNode.start;
×
119
                        destNode.end = srcNode.end;
×
120
                        destNode.loc = srcNode.loc;
×
121
                        return destNode;
×
122
                };
×
123
        } else {
×
124
                copyLoc = destNode => destNode;
×
125
        }
×
126

×
127
        // Get whether strict mode and remove use strict directive
×
128
        let isStrict;
×
129
        if (!isClass) {
×
130
                isStrict = !!fnInfo.isStrict;
×
131
                if (isStrict) {
×
132
                        removeUseStrictDirective(node);
×
133
                } else if (filename.startsWith(RUNTIME_DIR_PATH)) {
×
134
                        // Runtime functions are treated as indeterminate strict/sloppy mode unless explicitly strict
×
135
                        isStrict = null;
×
136
                }
×
137
        }
×
138

×
139
        // Create `super` var node if required
×
140
        let superVarNode;
×
141
        if (externalVars.super) {
×
142
                superVarNode = t.identifier('super');
×
143
                externalVars.super[0] = superVarNode;
×
144
                globalVarNames.add('Reflect');
×
145
                globalVarNames.add('Object');
×
146
        }
×
147

×
148
        // Conform function/class, get function name
×
149
        let isMethod, name, thisVarNode, constructorStatementNodes, firstSuperStatementIndex,
×
150
                paramNodes = node.params;
×
151
        const {type} = node,
×
152
                isArrow = type === 'ArrowFunctionExpression',
×
153
                containsEval = !!fnInfo.containsEval;
×
154
        if (isArrow) {
×
155
                // Arrow function
×
156
                name = '';
×
157
                isMethod = false;
×
158

×
159
                // If contains `super`, needs `this` too
×
160
                if (externalVars.super) {
×
161
                        thisVarNode = t.identifier('this');
×
162
                        externalVars.this.push(thisVarNode);
×
163
                }
×
164
        } else {
×
165
                // Class, method or function declaration/expression.
×
166
                // Get function name.
×
167
                name = (Object.getOwnPropertyDescriptor(fn, 'name') || {}).value;
×
168
                if (!isString(name)) name = '';
×
169

×
170
                // Identify if method and convert class method to object method
×
171
                if (type === 'ClassMethod') {
×
172
                        node.type = 'ObjectMethod';
×
173
                        node.static = undefined;
×
174
                        isMethod = true;
×
175
                } else if (type === 'ObjectMethod') {
×
176
                        isMethod = true;
×
177
                }
×
178

×
179
                if (isMethod) {
×
180
                        // Class/object method.
×
181
                        // Convert getter/setter to plain method, and disable computed keys.
×
182
                        node.kind = 'method';
×
183
                        node.computed = false;
×
184

×
185
                        // Wrap in object.
×
186
                        // NB: Must be defined as method, to avoid it having prototype.
×
187
                        let keyNode, accessComputed;
×
188
                        if (!name) {
×
189
                                name = 'a';
×
190
                                keyNode = t.identifier('a');
×
191
                                accessComputed = false;
×
192
                        } else if (isJsIdentifier(name)) {
×
193
                                keyNode = t.identifier(name);
×
194
                                accessComputed = false;
×
195
                        } else {
×
196
                                keyNode = isNumberKey(name) ? t.numericLiteral(name * 1) : t.stringLiteral(name);
×
197
                                accessComputed = true;
×
198
                        }
×
199

×
200
                        node.key = copyLoc(keyNode, node.key || {});
×
201
                        node = t.memberExpression(t.objectExpression([node]), keyNode, accessComputed);
×
202

×
203
                        // `this` within method is actual `this`
×
204
                        thisVarNode = t.thisExpression();
×
205
                } else {
×
206
                        // Set function name
×
207
                        const idNode = node.id;
×
208
                        if (containsEval) {
×
209
                                // Function contains `eval()`.
×
210
                                // Cannot change name from original as would create a new var in scope of `eval()`.
×
211
                                // If is a named function expression, name must be retained, to maintain const violation
×
212
                                // error behavior (error in strict mode, silent failure in sloppy mode).
×
213
                                // If is a named function declaration, name must be removed to allow read/write access
×
214
                                // to external var holding function in upper scope.
×
215
                                if (idNode) {
×
216
                                        name = idNode.name;
×
217
                                        if (externalVars[name]) name = '';
×
218
                                } else {
×
219
                                        name = '';
×
220
                                }
×
221
                        } else if (
×
222
                                name && (!isJsIdentifier(name) || isReservedVarName(name) || globalVarNames.has(name))
×
223
                        ) {
×
224
                                // Name cannot be used as is illegal, or would block access to a global var
×
225
                                name = '';
×
226
                        }
×
227

×
228
                        if (!name) {
×
229
                                node.id = null;
×
230
                        } else {
×
231
                                if (!idNode || idNode.name !== name) {
×
232
                                        node.id = t.identifier(name);
×
233
                                        if (idNode) copyLoc(node.id, idNode);
×
234
                                }
×
235
                                functionNames.add(name);
×
236
                        }
×
237

×
238
                        if (isClass) {
×
239
                                // Class
×
240
                                if (type === 'ClassDeclaration') node.type = 'ClassExpression';
×
241

×
242
                                // Classes have indeterminate strict/sloppy status - never require conforming to strict/sloppy
×
243
                                // as they're automatically strict.
×
244
                                isStrict = null;
×
245

×
246
                                // Remove all members except constructor and prototype properties
×
247
                                let constructorNode;
×
248
                                const classBodyNode = node.body;
×
249
                                const memberNodes = classBodyNode.body = classBodyNode.body.filter((memberNode) => {
×
250
                                        if (!memberNode) return false;
×
251
                                        const memberType = memberNode.type;
×
252
                                        if (memberType === 'ClassMethod') {
×
253
                                                constructorNode = memberNode;
×
254
                                                return true;
×
255
                                        }
×
256
                                        if (memberType === 'StaticBlock') return false;
×
257
                                        return !memberNode.static;
×
258
                                });
×
259
                                paramNodes = constructorNode ? constructorNode.params : [];
×
260

×
261
                                if (fnInfo.hasSuperClass) {
×
262
                                        // Remove `extends`
×
263
                                        node.superClass = null;
×
264

×
265
                                        // Add constructor if none present
×
266
                                        if (!constructorNode) {
×
267
                                                // `constructor(...args) {
×
268
                                                //   return Reflect.construct(Object.getPrototypeOf(super$0), args, super$0);
×
269
                                                // }`
×
270
                                                const argsVarNode = t.identifier('args');
×
271
                                                internalVars.args = [argsVarNode];
×
272
                                                // TODO: Can using `.unshift()` interfere with replacing const violations further on?
×
273
                                                // Indexes of any class properties will be altered.
×
274
                                                memberNodes.unshift(t.classMethod(
×
275
                                                        'constructor',
×
276
                                                        t.identifier('constructor'),
×
277
                                                        [t.restElement(argsVarNode)],
×
278
                                                        t.blockStatement([
×
279
                                                                t.returnStatement(
×
280
                                                                        t.callExpression(
×
281
                                                                                t.memberExpression(t.identifier('Reflect'), t.identifier('construct')),
×
282
                                                                                [
×
283
                                                                                        t.callExpression(
×
284
                                                                                                t.memberExpression(t.identifier('Object'), t.identifier('getPrototypeOf')),
×
285
                                                                                                [superVarNode]
×
286
                                                                                        ),
×
287
                                                                                        argsVarNode,
×
288
                                                                                        superVarNode
×
289
                                                                                ]
×
290
                                                                        )
×
291
                                                                )
×
292
                                                        ])
×
293
                                                ));
×
294
                                        } else {
×
295
                                                // Class has constructor - prepare for transpiling `super()`
×
296
                                                const thisInternalVars = internalVars.this;
×
297
                                                if (thisInternalVars) {
×
298
                                                        // Constructor contains `this` or `super` expression (e.g. `super.foo()`).
×
299
                                                        // Var for `this$0` is going to be needed.
×
300
                                                        thisVarNode = t.identifier('this');
×
301
                                                        thisInternalVars.push(thisVarNode);
×
302
                                                }
×
303

×
304
                                                firstSuperStatementIndex = fnInfo.firstSuperStatementIndex;
×
305
                                                constructorStatementNodes = constructorNode.body.body;
×
306
                                        }
×
307
                                } else {
×
308
                                        // Class which doesn't extend a super class. `this` within constructor is actual `this`.
×
309
                                        thisVarNode = t.thisExpression();
×
310
                                }
×
311
                        } else {
×
312
                                // Function declaration/expression. `this` is actual `this`.
×
313
                                if (type === 'FunctionDeclaration') node.type = 'FunctionExpression';
×
314
                                thisVarNode = t.thisExpression();
×
315
                        }
×
316
                }
×
317
        }
×
318

×
319
        // Replace const violations + `super`.
×
320
        // Amendments are in reverse order:
×
321
        // - Nested functions before their parent.
×
322
        // - Within a function, deeper nested expressions/statements before their parent.
×
323
        // - Within a function, later statements before earlier statements.
×
324
        // Reverse order ensures correct handling of nested amendments
×
325
        // e.g. 2 const violations `c = c2 = 1` or const violation + super `super.foo(c = 1)`.
×
326
        const superIsProto = isClass || fnInfo.superIsProto;
×
327
        for (const amendment of amendments) {
×
328
                const {type: amendmentType, trail, trailNodes} = amendment;
×
329
                if (amendmentType === SUPER_CALL) {
×
330
                        replaceSuperCall(
×
331
                                trail, trailNodes, superVarNode, thisVarNode, firstSuperStatementIndex, copyLoc
×
332
                        );
×
333
                } else if (amendmentType === SUPER_EXPRESSION) {
×
334
                        replaceSuperExpression(
×
335
                                trail, trailNodes, superVarNode, thisVarNode, superIsProto, internalVars, copyLoc
×
336
                        );
×
337
                } else if (amendmentType === CONST_VIOLATION_FUNCTION_SILENT) {
×
338
                        replaceConstViolation(trail, trailNodes, true, internalVars, copyLoc);
×
339
                } else {
×
340
                        // amendmentType === CONST_VIOLATION_CONST || amendmentType === CONST_VIOLATION_FUNCTION_THROWING
×
341
                        replaceConstViolation(trail, trailNodes, false, internalVars, copyLoc);
×
342
                }
×
343
        }
×
344

×
345
        // If is class extending a super class, insert `let this$0;` and `return this$0;` statements
×
346
        // if required
×
347
        if (constructorStatementNodes && thisVarNode) {
×
348
                // Add `let this$0` at start of constructor
×
349
                if (firstSuperStatementIndex === undefined) {
×
350
                        constructorStatementNodes.unshift(
×
351
                                t.variableDeclaration('let', [t.variableDeclarator(thisVarNode, null)])
×
352
                        );
×
353
                }
×
354

×
355
                // Add `return this$0` to end of constructor
×
356
                if (!fnInfo.returnsSuper) constructorStatementNodes.push(t.returnStatement(thisVarNode));
×
357
        }
×
358

×
359
        // Determine what value of `fn.length` will be
×
360
        let numParams = 0;
×
361
        for (const {type: paramNodeType} of paramNodes) {
×
362
                if (paramNodeType === 'RestElement' || paramNodeType === 'AssignmentPattern') break;
×
363
                numParams++;
×
364
        }
×
365

×
366
        // Return function definition object
×
367
        return {
×
368
                node,
×
369
                scopeDefs,
×
370
                externalVars,
×
371
                internalVars,
×
372
                globalVarNames,
×
373
                functionNames,
×
374
                name,
×
375
                numParams,
×
376
                isClass,
×
377
                isAsync,
×
378
                isGenerator,
×
379
                isArrow,
×
380
                isMethod,
×
381
                isStrict,
×
382
                containsEval,
×
383
                argNames: fnInfo.argNames
×
384
        };
×
385
};
31✔
386

31✔
387
/**
31✔
388
 * Remove 'use strict' directive from function.
31✔
389
 * @param {Object} fnNode - Function AST node
31✔
390
 * @returns {undefined}
31✔
391
 */
31✔
392
function removeUseStrictDirective(fnNode) {
×
393
        const bodyNode = fnNode.body;
×
394
        if (bodyNode.type !== 'BlockStatement') return;
×
395
        const directiveNodes = bodyNode.directives;
×
396
        if (directiveNodes.length === 0) return;
×
397
        const index = directiveNodes.findIndex(directiveNode => directiveNode.value.value === 'use strict');
×
398
        if (index === -1) return;
×
399

×
400
        // Relocate any comments attached to directive
×
401
        const directiveNode = directiveNodes[index];
×
402
        const commentNodes = [
×
403
                ...(directiveNode.leadingComments || []),
×
404
                ...(directiveNode.trailingComments || [])
×
405
        ];
×
406
        if (commentNodes.length !== 0) {
×
407
                const statementNodes = bodyNode.body;
×
408
                if (index !== 0) {
×
409
                        directiveNodes[index - 1].trailingComments = combineArraysWithDedup(
×
410
                                directiveNodes[index - 1].trailingComments, commentNodes
×
411
                        );
×
412
                } else if (index !== directiveNodes.length - 1) {
×
413
                        directiveNodes[index + 1].leadingComments = combineArraysWithDedup(
×
414
                                commentNodes, directiveNodes[index + 1].leadingComments
×
415
                        );
×
416
                } else if (statementNodes.length !== 0) {
×
417
                        statementNodes[0].leadingComments = combineArraysWithDedup(
×
418
                                commentNodes, statementNodes[0].leadingComments
×
419
                        );
×
420
                } else {
×
421
                        bodyNode.innerComments = combineArraysWithDedup(bodyNode.innerComments, commentNodes);
×
422
                }
×
423
        }
×
424

×
425
        // Remove directive
×
426
        directiveNodes.splice(index, 1);
×
427
}
×
428

31✔
429
/**
31✔
430
 * Replace const violation expression with expression which throws.
31✔
431
 * e.g. `c = f()` -> `f(), (() => {const c = 0; c = 0;})()`
31✔
432
 * If is silent violation (function expression name assigned to in sloppy mode),
31✔
433
 * just remove the assignment.
31✔
434
 * e.g. `c += f()` -> `c + f()`
31✔
435
 *
31✔
436
 * @param {Array<string|number>} trail - Keys trail
31✔
437
 * @param {Array<Object>} trailNodes - Trail nodes
31✔
438
 * @param {boolean} isSilent - `true` if is a silent violation (i.e. doesn't throw)
31✔
439
 * @param {Object} internalVars - Map of internal vars, keyed by var name
31✔
440
 * @param {Function} copyLoc - Function to copy location from old to new AST node
31✔
441
 * @returns {undefined}
31✔
442
 */
31✔
443
function replaceConstViolation(trail, trailNodes, isSilent, internalVars, copyLoc) {
×
444
        const trailLen = trail.length,
×
445
                node = trailNodes[trailLen],
×
446
                parentNode = trailNodes[trailLen - 1];
×
447
        function replaceParent(replacementNode) {
×
448
                trailNodes[trailLen - 2][trail[trailLen - 2]] = copyLoc(replacementNode, parentNode);
×
449
        }
×
450

×
451
        const {type} = parentNode;
×
452
        if (type === 'AssignmentExpression') {
×
453
                const {operator} = parentNode;
×
454
                if (operator === '=') {
×
455
                        // `c = x` -> `x, throw()`
×
456
                        replaceParent(
×
457
                                createConstViolationThrowNode(parentNode.right, node.name, isSilent, internalVars)
×
458
                        );
×
459
                } else if (['&&=', '||=', '??='].includes(operator)) {
×
460
                        // `c &&= x` -> `c && (x, throw())`
×
461
                        replaceParent(
×
462
                                t.logicalExpression(
×
463
                                        operator.slice(0, -1),
×
464
                                        node,
×
465
                                        createConstViolationThrowNode(parentNode.right, node.name, isSilent, internalVars)
×
466
                                )
×
467
                        );
×
468
                } else {
×
469
                        // Other assignment operator e.g. `+=`, `-=`, `>>>=`
×
470
                        // `c += x` -> `c + x, throw()`
×
471
                        replaceParent(
×
472
                                createConstViolationThrowNode(
×
473
                                        t.binaryExpression(operator.slice(0, -1), node, parentNode.right),
×
474
                                        node.name, isSilent, internalVars
×
475
                                )
×
476
                        );
×
477
                }
×
478
        } else if (type === 'UpdateExpression') {
×
479
                // `c++` -> `+c, throw()`
×
480
                replaceParent(
×
481
                        createConstViolationThrowNode(t.unaryExpression('+', node), node.name, isSilent, internalVars)
×
482
                );
×
483
        } else if (type === 'AssignmentPattern') {
×
484
                // `[c = x] = []`
×
485
                // -> `[(e => ({set a(v) {v === void 0 && e(); const c = 0; c = 0;}}))(() => x).a] = []`
×
486
                // `{c = x} = {}`
×
487
                // -> `{c: (e => ({set a(v) {v === void 0 && e(); const c = 0; c = 0;}}))(() => x).a} = {}`
×
488
                replaceParent(
×
489
                        createConstViolationThrowPatternNode(node.name, parentNode.right, isSilent, internalVars)
×
490
                );
×
491
        } else {
×
492
                assertBug(
×
493
                        type === 'ForOfStatement' || type === 'ForInStatement'
×
494
                                || type === 'ObjectProperty' || type === 'RestElement'
×
495
                                || trailNodes[trailLen - 2].type === 'ArrayPattern',
×
496
                        `Unexpected const violation node type ${type}`
×
497
                );
×
498

×
499
                // `for (c of [1]) { foo(); }`
×
500
                // -> `for ({ set a(v) { const c = 0; c = 0; } }.a of [1]) { foo(); }`
×
501
                // `{x: c} = {}` -> `{x: {set a(v) { const c = 0; c = 0; }}.a} = {}`
×
502
                // `[...c] = []` -> `[...{set a(v) { const c = 0; c = 0; }}.a] = []`
×
503
                // `{...c} = {}` -> `{...{set a(v) { const c = 0; c = 0; }}.a} = {}`
×
504
                // `[c] = [1]` -> `[{set a(v) { const c = 0; c = 0; }}.a] = [1]`
×
505
                parentNode[trail[trailLen - 1]] = copyLoc(
×
506
                        createConstViolationThrowAssignNode(node.name, isSilent, internalVars), node
×
507
                );
×
508
        }
×
509
}
×
510

31✔
511
/**
31✔
512
 * Create expression node which throws 'Assignment to constant variable' TypeError.
31✔
513
 * @param {Object} node - AST node to wrap
31✔
514
 * @param {string} name - Var name
31✔
515
 * @param {boolean} isSilent - `true` if violation does not throw
31✔
516
 * @param {Object} internalVars - Map of internal vars, keyed by var name
31✔
517
 * @returns {Object} - AST Node object
31✔
518
 */
31✔
519
function createConstViolationThrowNode(node, name, isSilent, internalVars) {
×
520
        // If silent failure, return node unchanged
×
521
        if (isSilent) return node;
×
522

×
523
        // `(x, (() => { const c = 0; c = 0; })())`
×
524
        return t.sequenceExpression([
×
525
                node,
×
526
                t.callExpression(
×
527
                        t.arrowFunctionExpression([], createConstViolationThrowBlockNode(name, internalVars)), []
×
528
                )
×
529
        ]);
×
530
}
×
531

31✔
532
/**
31✔
533
 * Create expression node which throws when assigned to.
31✔
534
 * @param {string} name - Var name
31✔
535
 * @param {boolean} isSilent - `true` if violation does not throw
31✔
536
 * @param {Object} internalVars - Map of internal vars, keyed by var name
31✔
537
 * @returns {Object} - AST Node object
31✔
538
 */
31✔
539
function createConstViolationThrowAssignNode(name, isSilent, internalVars) {
×
540
        // If silent failure, return expression which assignment to is a no-op
×
541
        if (isSilent) {
×
542
                // `{}.a`
×
543
                return t.memberExpression(t.objectExpression([]), t.identifier('a'));
×
544
        }
×
545

×
546
        // `{ set a(v) { const c = 0; c = 0; } }.a`
×
547
        return t.memberExpression(
×
548
                t.objectExpression([
×
549
                        t.objectMethod(
×
550
                                'set',
×
551
                                t.identifier('a'),
×
552
                                [createTempVarNode(name === 'v' ? '_v' : 'v', internalVars)],
×
553
                                createConstViolationThrowBlockNode(name, internalVars)
×
554
                        )
×
555
                ]),
×
556
                t.identifier('a')
×
557
        );
×
558
}
×
559

31✔
560
/**
31✔
561
 * Create expression node to replace an assignment pattern which:
31✔
562
 * 1. Evaluates right hand side of pattern expression only if incoming assignment value is `undefined`
31✔
563
 * 2. Throws when assigned to
31✔
564
 *
31✔
565
 * @param {string} name - Var name
31✔
566
 * @param {Object} rightNode - AST Node for right-hand side of assignment pattern
31✔
567
 * @param {boolean} isSilent - `true` if violation does not throw
31✔
568
 * @param {Object} internalVars - Map of internal vars, keyed by var name
31✔
569
 * @returns {Object} - AST Node object
31✔
570
 */
31✔
571
function createConstViolationThrowPatternNode(name, rightNode, isSilent, internalVars) {
×
572
        const valueVarNode = createTempVarNode(name === 'v' ? '_v' : 'v', internalVars),
×
573
                rightGetterVarNode = createTempVarNode(name === 'e' ? '_e' : 'e', internalVars);
×
574

×
575
        // `v === void 0 && e()`
×
576
        const statementNodes = [t.expressionStatement(t.logicalExpression(
×
577
                '&&',
×
578
                t.binaryExpression('===', valueVarNode, t.unaryExpression('void', t.numericLiteral(0))),
×
579
                t.callExpression(rightGetterVarNode, [])
×
580
        ))];
×
581

×
582
        if (!isSilent) statementNodes.push(...createConstViolationThrowStatementNodes(name, internalVars));
×
583

×
584
        // `(e => ({ set a(v) { v === void 0 && e(); const c = 0; c = 0; }}))(() => x).a`
×
585
        return t.memberExpression(
×
586
                t.callExpression(
×
587
                        t.arrowFunctionExpression(
×
588
                                [rightGetterVarNode],
×
589
                                t.objectExpression([
×
590
                                        t.objectMethod('set', t.identifier('a'), [valueVarNode], t.blockStatement(statementNodes))
×
591
                                ])
×
592
                        ),
×
593
                        [t.arrowFunctionExpression([], rightNode)]
×
594
                ),
×
595
                t.identifier('a')
×
596
        );
×
597
}
×
598

31✔
599
/**
31✔
600
 * Create block statement which throws 'Assignment to constant variable' TypeError.
31✔
601
 * @param {string} name - Var name
31✔
602
 * @param {Object} internalVars - Map of internal vars, keyed by var name
31✔
603
 * @returns {Object} - AST Node object
31✔
604
 */
31✔
605
function createConstViolationThrowBlockNode(name, internalVars) {
×
606
        // `{ const c = 0; c = 0; }`
×
607
        return t.blockStatement(createConstViolationThrowStatementNodes(name, internalVars));
×
608
}
×
609

31✔
610
/**
31✔
611
 * Create statements which throw 'Assignment to constant variable' TypeError.
31✔
612
 * @param {string} name - Var name
31✔
613
 * @param {Object} internalVars - Map of internal vars, keyed by var name
31✔
614
 * @returns {Array<Object>} - Array of AST Node objects
31✔
615
 */
31✔
616
function createConstViolationThrowStatementNodes(name, internalVars) {
×
617
        // `const c = 0; c = 0;`
×
618
        const varNode = createTempVarNode(name, internalVars);
×
619
        return [
×
620
                t.variableDeclaration('const', [t.variableDeclarator(varNode, t.numericLiteral(0))]),
×
621
                t.expressionStatement(t.assignmentExpression('=', varNode, t.numericLiteral(0)))
×
622
        ];
×
623
}
×
624

31✔
625
/**
31✔
626
 * Replace `super()` call with transpiled version.
31✔
627
 * @param {Array<string|number>} trail - Keys trail
31✔
628
 * @param {Array<Object>} trailNodes - Trail nodes
31✔
629
 * @param {Object} superVarNode - Var node for home object for `super`
31✔
630
 * @param {Object} [thisVarNode] - Var node for `this` (Identifier)
31✔
631
 * @param {number} [firstSuperStatementIndex] - Index of first top-level `super()` statement
31✔
632
 * @param {Function} copyLoc - Function to copy location from old to new AST node
31✔
633
 * @returns {undefined}
31✔
634
 */
31✔
635
function replaceSuperCall(
×
636
        trail, trailNodes, superVarNode, thisVarNode, firstSuperStatementIndex, copyLoc
×
637
) {
×
638
        const trailLen = trail.length,
×
639
                callNode = trailNodes[trailLen - 1];
×
640

×
641
        // Convert to `super(x, y)` -> `Reflect.construct(Object.getPrototypeOf(super$0), [x, y], super$0)`.
×
642
        // NB: Cannot optimize `super(...x)` to `Reflect.construct(Object.getPrototypeOf(super$0), x, super$0)`
×
643
        // in case `x` is an iterator not an array.
×
644
        const argumentsNodes = callNode.arguments;
×
645
        callNode.callee = copyLoc(
×
646
                t.memberExpression(t.identifier('Reflect'), t.identifier('construct')),
×
647
                callNode.callee
×
648
        );
×
649
        callNode.arguments = [
×
650
                t.callExpression(
×
651
                        t.memberExpression(t.identifier('Object'), t.identifier('getPrototypeOf')),
×
652
                        [superVarNode]
×
653
                ),
×
654
                t.arrayExpression(argumentsNodes),
×
655
                superVarNode
×
656
        ];
×
657

×
658
        // If is `return super()`, just substitute transpiled super
×
659
        // `return super(x)` -> `return Reflect.construct(Object.getPrototypeOf(super$0), [x], super$0)`
×
660
        const grandParentNode = trailNodes[trailLen - 2],
×
661
                grandParentType = grandParentNode.type;
×
662
        if (grandParentType === 'ReturnStatement') {
×
663
                if (thisVarNode) {
×
664
                        grandParentNode[trail[trailLen - 2]] = copyLoc(
×
665
                                t.assignmentExpression('=', thisVarNode, callNode),
×
666
                                callNode
×
667
                        );
×
668
                }
×
669
                return;
×
670
        }
×
671

×
672
        // If is a top-level statement in constructor
×
673
        // (i.e. is statement which is purely `super()`, directly in constructor body block):
×
674
        // - Last statement: `return Reflect.construct(...)`
×
675
        //   or if `this` used in constructor: `return this$0 = Reflect.construct(...)`
×
676
        // - First statement: `const this$0 = Reflect.construct(...)`
×
677
        // - Any other statement: `this$0 = Reflect.construct(...)`
×
678
        if (grandParentType === 'ExpressionStatement') {
×
679
                const fnType = trailNodes[0].type;
×
680
                if ((fnType === 'ClassDeclaration' || fnType === 'ClassExpression') && trail.length === 8) {
×
681
                        // Top-level statement in constructor
×
682
                        const statementNodes = trailNodes[5],
×
683
                                statementIndex = trail[5];
×
684
                        if (statementIndex === statementNodes.length - 1) {
×
685
                                // Last statement in constructor
×
686
                                // `return Reflect.construct(...);` or `return this$0 = Reflect.construct(...);`
×
687
                                statementNodes[statementIndex] = copyLoc(
×
688
                                        t.returnStatement(thisVarNode ? t.assignmentExpression('=', thisVarNode, callNode) : callNode),
×
689
                                        callNode
×
690
                                );
×
691
                                return;
×
692
                        }
×
693

×
694
                        if (statementIndex === firstSuperStatementIndex) {
×
695
                                // First `super()` statement - replace with `const this$0 = Reflect.construct(...);`
×
696
                                statementNodes[statementIndex] = copyLoc(
×
697
                                        t.variableDeclaration('const', [t.variableDeclarator(thisVarNode, callNode)]),
×
698
                                        grandParentNode
×
699
                                );
×
700
                                return;
×
701
                        }
×
702
                }
×
703
        }
×
704

×
705
        // Replace with `this$0 = Reflect.construct(...)`
×
706
        grandParentNode[trail[trailLen - 2]] = copyLoc(
×
707
                t.assignmentExpression('=', thisVarNode, callNode), callNode
×
708
        );
×
709
}
×
710

31✔
711
/**
31✔
712
 * Replace `super` expression with transpiled version.
31✔
713
 * `super.prop` / `super[prop]` / `super.prop(...)` / `super[prop](...)` /
31✔
714
 * `super.prop = ...` / `super[prop] = ...`
31✔
715
 *
31✔
716
 * @param {Array<string|number>} trail - Keys trail
31✔
717
 * @param {Array<Object>} trailNodes - Trail nodes
31✔
718
 * @param {Object} superVarNode - Var node for home object for `super`
31✔
719
 * @param {Object} [thisVarNode] - Var node for `this` (may be Identifier or ThisExpression)
31✔
720
 * @param {boolean} superIsProto - `true` if `super` is within context of class prototype method
31✔
721
 * @param {Object} internalVars - Map of internal vars, keyed by var name
31✔
722
 * @param {Function} copyLoc - Function to copy location from old to new AST node
31✔
723
 * @returns {undefined}
31✔
724
 */
31✔
725
function replaceSuperExpression(
×
726
        trail, trailNodes, superVarNode, thisVarNode, superIsProto, internalVars, copyLoc
×
727
) {
×
728
        const trailLen = trail.length,
×
729
                expressionNode = trailNodes[trailLen - 1];
×
730

×
731
        // `super.prop` -> `Reflect.get(Object.getPrototypeOf(super$0.prototype), 'prop', this$0)`
×
732
        // or in static class method `Reflect.get(Object.getPrototypeOf(super$0), 'prop', this$0)`
×
733
        let propNode = expressionNode.property;
×
734
        if (!expressionNode.computed) propNode = copyLoc(t.stringLiteral(propNode.name), propNode);
×
735

×
736
        let replacementNode = t.callExpression(
×
737
                t.memberExpression(t.identifier('Reflect'), t.identifier('get')),
×
738
                [
×
739
                        t.callExpression(
×
740
                                t.memberExpression(t.identifier('Object'), t.identifier('getPrototypeOf')),
×
741
                                [
×
742
                                        superIsProto
×
743
                                                ? t.memberExpression(superVarNode, t.identifier('prototype'))
×
744
                                                : superVarNode
×
745
                                ]
×
746
                        ),
×
747
                        propNode,
×
748
                        thisVarNode
×
749
                ]
×
750
        );
×
751

×
752
        // `super.prop = ...` -> `Reflect.set(Object.getPrototypeOf(super$0.prototype), 'prop', value, this$0)`
×
753
        const parentNode = trailNodes[trailLen - 2],
×
754
                parentKey = trail[trailLen - 2],
×
755
                parentType = parentNode.type;
×
756
        if (parentType === 'AssignmentExpression' && parentKey === 'left') {
×
757
                // Convert `Reflect.get(...)` to `Reflect.set(...)`
×
758
                replacementNode.callee.property.name = 'set';
×
759

×
760
                const valueNode = parentNode.right,
×
761
                        grandParentNode = trailNodes[trailLen - 3];
×
762
                if (grandParentNode.type === 'ExpressionStatement') {
×
763
                        // Standalone expression - add value to `Reflect.set()` arguments
×
764
                        replacementNode.arguments.splice(2, 0, valueNode);
×
765
                } else {
×
766
                        // Not standalone expression - wrap in function which return values.
×
767
                        // This avoids evaluating the value expression twice.
×
768
                        // `x = super.foo = y();` =>
×
769
                        // `x = ((key, value) => (Reflect.set(..., key, value, this$0), value))('foo', y());`
×
770
                        // NB: Key is passed to function as argument too as it could be a complex expression
×
771
                        // referencing many vars. Evaluating it outside the temp closure ensures no var name clashes.
×
772
                        const keyVarNode = createTempVarNode('key', internalVars),
×
773
                                valueVarNode = createTempVarNode('value', internalVars);
×
774
                        replacementNode.arguments[1] = keyVarNode;
×
775
                        replacementNode.arguments.splice(2, 0, valueVarNode);
×
776
                        replacementNode = t.callExpression(
×
777
                                t.arrowFunctionExpression(
×
778
                                        [keyVarNode, valueVarNode],
×
779
                                        t.sequenceExpression([replacementNode, valueVarNode])
×
780
                                ),
×
781
                                [propNode, valueNode]
×
782
                        );
×
783
                }
×
784

×
785
                grandParentNode[trail[trailLen - 3]] = copyLoc(replacementNode, parentNode);
×
786
                return;
×
787
        }
×
788

×
789
        // `super.prop(x, y)` -> `Reflect.get(...).call(this$0, x, y)`
×
790
        if (parentType === 'CallExpression') {
×
791
                replacementNode = t.memberExpression(replacementNode, t.identifier('call'));
×
792
                parentNode.arguments.unshift(thisVarNode);
×
793
        }
×
794
        copyLoc(replacementNode, expressionNode);
×
795

×
796
        parentNode[parentKey] = replacementNode;
×
797
}
×
798

31✔
799
/**
31✔
800
 * Create temp var node and add to internal vars.
31✔
801
 * @param {string} name - Var name
31✔
802
 * @param {Object} internalVars - Map of internal vars, keyed by var name
31✔
803
 * @returns {Object} - Identifier AST node
31✔
804
 */
31✔
805
function createTempVarNode(name, internalVars) {
×
806
        const node = t.identifier(name);
×
807
        createArrayOrPush(internalVars, name, node);
×
808
        return node;
×
809
}
×
810

31✔
811
/**
31✔
812
 * Add ASTs of nested functions to AST.
31✔
813
 * A function which has other functions nested within it will have left those nodes defined as `null`
31✔
814
 * in JSON-serialized AST. Get those child functions' ASTs from their own `getFnInfo()` functions
31✔
815
 * and insert them into the parent AST.
31✔
816
 * @param {Object} fnInfo - Function info object which was encoded as JSON to fn info getter function
31✔
817
 * @param {Array<Function>} getInfos - Function info getter functions for child functions
31✔
818
 * @param {boolean} isNestedFunction - `true` if is nested function
31✔
819
 * @param {number} fnId - Block ID of function being serialized
31✔
820
 * @param {Map} scopeDefs - Scope definitions map, keyed by block ID
31✔
821
 * @param {Object} externalVars - Map of var name to array of var nodes
31✔
822
 * @param {Object} internalVars - Map of var name to array of var nodes
31✔
823
 * @param {Set} globalVarNames - Set of global var names
31✔
824
 * @param {Set} functionNames - Set of function names
31✔
825
 * @param {Array<Object>} amendments - Array of amendments (const violations or `super`)
31✔
826
 * @returns {undefined}
31✔
827
 */
31✔
828
function resolveFunctionInfo(
×
829
        fnInfo, getInfos, isNestedFunction, fnId, scopeDefs,
×
830
        externalVars, internalVars, globalVarNames, functionNames, amendments
×
831
) {
×
832
        // Init internal vars.
×
833
        // Converting trails to nodes needs to be done after child functions added into AST
×
834
        // because some vars recorded as internal to this function may be within the ASTs of child functions.
×
835
        // These are class `extends` clauses and computed method keys.
×
836
        // Initializing properties of `internalVars` needs to happen before processing child functions,
×
837
        // as external vars in child functions can become internal vars in this function.
×
838
        const internalVarTrails = [];
×
839
        for (const [varName, trails] of Object.entries(fnInfo.internalVars)) {
×
840
                if (!internalVars[varName]) internalVars[varName] = [];
×
841
                internalVarTrails.push({varName, trails});
×
842
        }
×
843

×
844
        // Process child functions
×
845
        const fnNode = fnInfo.ast;
×
846
        fnInfo.childFns.forEach((trail, index) => {
×
847
                const [childJson, childGetInfos] = getInfos[index](),
×
848
                        childInfo = JSON.parse(childJson);
×
849
                resolveFunctionInfo(
×
850
                        childInfo, childGetInfos, true, fnId, scopeDefs,
×
851
                        externalVars, internalVars, globalVarNames, functionNames, amendments
×
852
                );
×
853

×
854
                // Insert child function's AST into this function's AST
×
855
                setProp(fnNode, trail, childInfo.ast);
×
856
        });
×
857

×
858
        // Record function name
×
859
        if (isNestedFunction) {
×
860
                const idNode = fnNode.id;
×
861
                if (idNode) functionNames.add(idNode.name);
×
862
        }
×
863

×
864
        // Get external var nodes
×
865
        for (const scope of fnInfo.scopes) {
×
866
                const {blockId} = scope;
×
867
                if (blockId < fnId) {
×
868
                        // External var
×
869
                        for (const [varName, {isReadFrom, isAssignedTo, trails}] of Object.entries(scope.vars)) {
×
870
                                if (isNestedFunction) {
×
871
                                        const scopeDefVar = scopeDefs.get(blockId).vars[varName];
×
872
                                        if (isReadFrom) scopeDefVar.isReadFrom = true;
×
873
                                        if (isAssignedTo) scopeDefVar.isAssignedTo = true;
×
874
                                }
×
875

×
876
                                externalVars[varName].push(...trailsToNodes(fnNode, trails, varName));
×
877
                        }
×
878
                } else {
×
879
                        // Var which is external to current function, but internal to function being serialized
×
880
                        for (const [varName, {isFunction, trails}] of Object.entries(scope.vars)) {
×
881
                                if (!isFunction) internalVars[varName]?.push(...trailsToNodes(fnNode, trails, varName));
×
882
                        }
×
883
                }
×
884
        }
×
885

×
886
        // Get internal var nodes
×
887
        for (const {varName, trails} of internalVarTrails) {
×
888
                internalVars[varName].push(...trailsToNodes(fnNode, trails, varName));
×
889
        }
×
890

×
891
        // Get global var names
×
892
        const thisGlobalVarNames = fnInfo.globalVarNames;
×
893
        if (thisGlobalVarNames) setAddFrom(globalVarNames, thisGlobalVarNames);
×
894

×
895
        // Get amendments (const violations and `super`).
×
896
        // Ignore amendments which refer to internal vars.
×
897
        const thisAmendments = fnInfo.amendments;
×
898
        if (thisAmendments) {
×
899
                for (const [type, blockId, ...trail] of thisAmendments) {
×
900
                        if (blockId < fnId) {
×
901
                                amendments.push({type, trail, trailNodes: getProps(fnNode, trail)});
×
902
                        } else if (type === CONST_VIOLATION_CONST) {
×
903
                                // Const violation where var is internal to function. Add to internal vars instead.
×
904
                                // Ignore CONST_VIOLATION_FUNCTION_THROWING and CONST_VIOLATION_FUNCTION_SILENT violation types
×
905
                                // because they refer to function names, which are not treated as internal vars.
×
906
                                const node = getProp(fnNode, trail);
×
907
                                internalVars[node.name].push(node);
×
908
                        }
×
909
                }
×
910
        }
×
911
}
×
912

31✔
913
/**
31✔
914
 * Get nodes for trails.
31✔
915
 * Convert `ThisExpression`s to `Identifier`s.
31✔
916
 * @param {Object} fnNode - AST node for function
31✔
917
 * @param {Array<Array>} trails - Trails
31✔
918
 * @param {string} varName - Var name
31✔
919
 * @returns {Array<Object>} - AST nodes specified by trails
31✔
920
 */
31✔
921
function trailsToNodes(fnNode, trails, varName) {
×
922
        return varName === 'this'
×
923
                ? trails.map((trail) => {
×
924
                        const node = getProp(fnNode, trail);
×
925
                        node.type = 'Identifier';
×
926
                        node.name = 'this';
×
927
                        return node;
×
928
                })
×
929
                : trails.map(trail => getProp(fnNode, trail));
×
930
}
×
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