supercov 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/bin/native.js +87 -0
  2. package/bin/supercov.js +20 -3
  3. package/dist/agentJson.d.ts +2 -1
  4. package/dist/agentJson.d.ts.map +1 -1
  5. package/dist/agentJson.js +18 -0
  6. package/dist/agentJson.js.map +1 -1
  7. package/dist/analyze.d.ts.map +1 -1
  8. package/dist/analyze.js +9 -4
  9. package/dist/analyze.js.map +1 -1
  10. package/dist/cli.js +115 -24
  11. package/dist/cli.js.map +1 -1
  12. package/dist/directInstrumenter.d.ts.map +1 -1
  13. package/dist/directInstrumenter.js +39 -19
  14. package/dist/directInstrumenter.js.map +1 -1
  15. package/dist/engineEvidence.d.ts +9 -0
  16. package/dist/engineEvidence.d.ts.map +1 -0
  17. package/dist/engineEvidence.js +31 -0
  18. package/dist/engineEvidence.js.map +1 -0
  19. package/dist/engineInstrumenter.d.ts +16 -0
  20. package/dist/engineInstrumenter.d.ts.map +1 -0
  21. package/dist/engineInstrumenter.js +50 -0
  22. package/dist/engineInstrumenter.js.map +1 -0
  23. package/dist/engineProcess.d.ts +11 -0
  24. package/dist/engineProcess.d.ts.map +1 -0
  25. package/dist/engineProcess.js +59 -0
  26. package/dist/engineProcess.js.map +1 -0
  27. package/dist/evidenceArchive.d.ts +1 -0
  28. package/dist/evidenceArchive.d.ts.map +1 -1
  29. package/dist/evidenceArchive.js +59 -13
  30. package/dist/evidenceArchive.js.map +1 -1
  31. package/dist/instrumenter.d.ts +8 -1
  32. package/dist/instrumenter.d.ts.map +1 -1
  33. package/dist/instrumenter.js +478 -80
  34. package/dist/instrumenter.js.map +1 -1
  35. package/dist/integrity.d.ts.map +1 -1
  36. package/dist/integrity.js +9 -1
  37. package/dist/integrity.js.map +1 -1
  38. package/dist/launchSupervisor.d.ts.map +1 -1
  39. package/dist/launchSupervisor.js +3 -0
  40. package/dist/launchSupervisor.js.map +1 -1
  41. package/dist/merge.d.ts.map +1 -1
  42. package/dist/merge.js +9 -6
  43. package/dist/merge.js.map +1 -1
  44. package/dist/nodeAssertionInstrumenter.d.ts +2 -2
  45. package/dist/nodeAssertionInstrumenter.d.ts.map +1 -1
  46. package/dist/nodeAssertionInstrumenter.js +12 -4
  47. package/dist/nodeAssertionInstrumenter.js.map +1 -1
  48. package/dist/nodeTest.d.ts.map +1 -1
  49. package/dist/nodeTest.js +13 -4
  50. package/dist/nodeTest.js.map +1 -1
  51. package/dist/playwright.d.ts.map +1 -1
  52. package/dist/playwright.js +177 -89
  53. package/dist/playwright.js.map +1 -1
  54. package/dist/processDiagnostics.d.ts +34 -0
  55. package/dist/processDiagnostics.d.ts.map +1 -0
  56. package/dist/processDiagnostics.js +155 -0
  57. package/dist/processDiagnostics.js.map +1 -0
  58. package/dist/project.d.ts +1 -0
  59. package/dist/project.d.ts.map +1 -1
  60. package/dist/project.js +39 -4
  61. package/dist/project.js.map +1 -1
  62. package/dist/query.d.ts +11 -0
  63. package/dist/query.d.ts.map +1 -1
  64. package/dist/query.js +224 -11
  65. package/dist/query.js.map +1 -1
  66. package/dist/register.mjs +52 -0
  67. package/dist/register.mjs.map +1 -1
  68. package/dist/resolve-loader.d.mts.map +1 -1
  69. package/dist/resolve-loader.mjs +8 -2
  70. package/dist/resolve-loader.mjs.map +1 -1
  71. package/dist/runAnalysis.d.ts.map +1 -1
  72. package/dist/runAnalysis.js +3 -1
  73. package/dist/runAnalysis.js.map +1 -1
  74. package/dist/runnerEvidence.d.ts +2 -2
  75. package/dist/runnerEvidence.d.ts.map +1 -1
  76. package/dist/runnerEvidence.js +6 -4
  77. package/dist/runnerEvidence.js.map +1 -1
  78. package/dist/runtime.d.ts +56 -4
  79. package/dist/runtime.d.ts.map +1 -1
  80. package/dist/runtime.js +308 -27
  81. package/dist/runtime.js.map +1 -1
  82. package/dist/sourceDiscovery.d.ts.map +1 -1
  83. package/dist/sourceDiscovery.js +6 -2
  84. package/dist/sourceDiscovery.js.map +1 -1
  85. package/dist/transport.js +2 -2
  86. package/dist/transport.js.map +1 -1
  87. package/dist/vitePlugin.d.ts.map +1 -1
  88. package/dist/vitePlugin.js +21 -7
  89. package/dist/vitePlugin.js.map +1 -1
  90. package/dist/vitest.js +38 -7
  91. package/dist/vitest.js.map +1 -1
  92. package/dist/waivers.d.ts +52 -0
  93. package/dist/waivers.d.ts.map +1 -0
  94. package/dist/waivers.js +121 -0
  95. package/dist/waivers.js.map +1 -0
  96. package/dist/workspace.d.ts +36 -0
  97. package/dist/workspace.d.ts.map +1 -1
  98. package/dist/workspace.js +212 -36
  99. package/dist/workspace.js.map +1 -1
  100. package/package.json +29 -3
@@ -8,6 +8,14 @@ const BEGIN = "__supercovMcdcBegin";
8
8
  const CONDITION = "__supercovMcdcCondition";
9
9
  const END = "__supercovMcdcEnd";
10
10
  const HIT = "__supercovCoverageHit";
11
+ const REGISTER_V2 = "__supercovRegisterProbeV2";
12
+ const END_V2 = "__supercovMcdcEndV2";
13
+ const HIT_V2 = "__supercovCoverageHitV2";
14
+ const FILE_V2 = "__supercovProbeFileV2";
15
+ const CLOCK_V2 = "__supercovProbeClockV2";
16
+ const HITS_V2 = "__supercovProbeHitsV2";
17
+ const DECISIONS_V2 = "__supercovProbeDecisionsV2";
18
+ const COMPLETE_V2 = "__supercovProbeCompleteV2";
11
19
  const SELECTION_BEGIN = "__supercovSelectionBegin";
12
20
  const SELECTION_RIGHT = "__supercovSelectionRight";
13
21
  const SELECTION_END = "__supercovSelectionEnd";
@@ -40,6 +48,26 @@ function sourceFor(code, node) {
40
48
  }
41
49
  return generate(node).code;
42
50
  }
51
+ function restoreClonedOffsets(original, clone) {
52
+ clone.start = original.start;
53
+ clone.end = original.end;
54
+ const keys = t.VISITOR_KEYS[original.type] ?? [];
55
+ for (const key of keys) {
56
+ const originalChild = original[key];
57
+ const clonedChild = clone[key];
58
+ if (Array.isArray(originalChild) && Array.isArray(clonedChild)) {
59
+ for (let index = 0; index < originalChild.length; index += 1) {
60
+ const sourceNode = originalChild[index];
61
+ const targetNode = clonedChild[index];
62
+ if (t.isNode(sourceNode) && t.isNode(targetNode))
63
+ restoreClonedOffsets(sourceNode, targetNode);
64
+ }
65
+ }
66
+ else if (t.isNode(originalChild) && t.isNode(clonedChild)) {
67
+ restoreClonedOffsets(originalChild, clonedChild);
68
+ }
69
+ }
70
+ }
43
71
  function stableId(file, kind, node, suffix = "") {
44
72
  return createHash("sha256")
45
73
  .update(`${file}:${kind}:${node.start ?? 0}:${node.end ?? 0}:${suffix}`)
@@ -68,6 +96,82 @@ function collectConditions(node, conditions) {
68
96
  }
69
97
  conditions.push(node);
70
98
  }
99
+ function probeV2ReachableVectorCount(node, conditions) {
100
+ const indices = new WeakMap();
101
+ for (const [index, condition] of conditions.entries())
102
+ indices.set(condition, index);
103
+ const vectors = new Set();
104
+ const evaluate = (expression, assignment, encoded) => {
105
+ if (t.isLogicalExpression(expression) &&
106
+ (expression.operator === "&&" || expression.operator === "||")) {
107
+ const left = evaluate(expression.left, assignment, encoded);
108
+ return expression.operator === "&&"
109
+ ? left && evaluate(expression.right, assignment, encoded)
110
+ : left || evaluate(expression.right, assignment, encoded);
111
+ }
112
+ if (t.isUnaryExpression(expression, { operator: "!" }) &&
113
+ hasCompoundBooleanDecision(expression.argument))
114
+ return !evaluate(expression.argument, assignment, encoded);
115
+ const index = indices.get(expression);
116
+ if (index === undefined)
117
+ throw new Error("probe v2 condition index missing");
118
+ const value = (assignment & (1 << index)) !== 0;
119
+ encoded.value += (value ? 2 : 1) * 3 ** index;
120
+ return value;
121
+ };
122
+ for (let assignment = 0; assignment < 2 ** conditions.length; assignment += 1) {
123
+ const encoded = { value: 0 };
124
+ const outcome = evaluate(node, assignment, encoded);
125
+ vectors.add(encoded.value * 2 + (outcome ? 1 : 0));
126
+ }
127
+ return vectors.size;
128
+ }
129
+ function markDecisionLogicalNodes(node, decisions) {
130
+ if (t.isLogicalExpression(node) &&
131
+ (node.operator === "&&" || node.operator === "||")) {
132
+ decisions.add(node);
133
+ markDecisionLogicalNodes(node.left, decisions);
134
+ markDecisionLogicalNodes(node.right, decisions);
135
+ return;
136
+ }
137
+ if (t.isUnaryExpression(node, { operator: "!" }) &&
138
+ hasCompoundBooleanDecision(node.argument))
139
+ markDecisionLogicalNodes(node.argument, decisions);
140
+ }
141
+ function hasNestedCoverageSurface(node) {
142
+ let found = false;
143
+ const visit = (candidate) => {
144
+ if (found)
145
+ return;
146
+ if (t.isLogicalExpression(candidate) ||
147
+ t.isConditionalExpression(candidate) ||
148
+ t.isOptionalMemberExpression(candidate) ||
149
+ t.isOptionalCallExpression(candidate) ||
150
+ t.isFunction(candidate) ||
151
+ t.isClassExpression(candidate) ||
152
+ (t.isAssignmentExpression(candidate) &&
153
+ ["&&=", "||=", "??="].includes(candidate.operator))) {
154
+ found = true;
155
+ return;
156
+ }
157
+ for (const key of t.VISITOR_KEYS[candidate.type] ?? []) {
158
+ const child = candidate[key];
159
+ if (Array.isArray(child)) {
160
+ for (const item of child)
161
+ if (item && typeof item === "object" && "type" in item)
162
+ visit(item);
163
+ }
164
+ else if (child && typeof child === "object" && "type" in child) {
165
+ visit(child);
166
+ }
167
+ }
168
+ };
169
+ visit(node);
170
+ return found;
171
+ }
172
+ function isCoverageTransparentDecision(conditions) {
173
+ return conditions.every((condition) => !hasNestedCoverageSurface(condition));
174
+ }
71
175
  function instrumentConditions(node, frameId, nextIndex, decisionLogicalNodes, conditionRuntimeName) {
72
176
  if (t.isLogicalExpression(node) &&
73
177
  (node.operator === "&&" || node.operator === "||")) {
@@ -87,7 +191,38 @@ function instrumentConditions(node, frameId, nextIndex, decisionLogicalNodes, co
87
191
  node,
88
192
  ]);
89
193
  }
90
- function hitStatement(id, hitRuntimeName) {
194
+ function instrumentConditionsV2(node, frameId, conditionTemps, nextIndex, decisionLogicalNodes) {
195
+ if (t.isLogicalExpression(node) &&
196
+ (node.operator === "&&" || node.operator === "||")) {
197
+ const logical = t.logicalExpression(node.operator, instrumentConditionsV2(node.left, frameId, conditionTemps, nextIndex, decisionLogicalNodes), instrumentConditionsV2(node.right, frameId, conditionTemps, nextIndex, decisionLogicalNodes));
198
+ decisionLogicalNodes.add(logical);
199
+ return logical;
200
+ }
201
+ if (t.isUnaryExpression(node, { operator: "!" }) &&
202
+ hasCompoundBooleanDecision(node.argument)) {
203
+ return t.unaryExpression("!", instrumentConditionsV2(node.argument, frameId, conditionTemps, nextIndex, decisionLogicalNodes), true);
204
+ }
205
+ const index = nextIndex.value;
206
+ nextIndex.value += 1;
207
+ const temporary = conditionTemps[index];
208
+ const weight = 3 ** index;
209
+ return t.sequenceExpression([
210
+ t.assignmentExpression("=", t.cloneNode(temporary), node),
211
+ t.assignmentExpression("+=", t.cloneNode(frameId), t.conditionalExpression(t.cloneNode(temporary), t.numericLiteral(weight * 2), t.numericLiteral(weight))),
212
+ t.cloneNode(temporary),
213
+ ]);
214
+ }
215
+ function hitStatement(id, hitRuntimeName, v2) {
216
+ if (v2) {
217
+ const fileState = t.cloneNode(v2.fileState);
218
+ const clock = t.cloneNode(v2.clock);
219
+ return t.expressionStatement(t.logicalExpression("||", t.binaryExpression("===", t.memberExpression(t.cloneNode(v2.hitEpochs), t.numericLiteral(v2.pointIndex), true), v2.epoch
220
+ ? t.cloneNode(v2.epoch)
221
+ : t.memberExpression(t.cloneNode(clock), t.identifier("epoch"))), t.callExpression(t.identifier(hitRuntimeName), [
222
+ t.cloneNode(fileState),
223
+ t.numericLiteral(v2.pointIndex),
224
+ ])));
225
+ }
91
226
  return t.expressionStatement(t.callExpression(t.identifier(hitRuntimeName), [t.stringLiteral(id)]));
92
227
  }
93
228
  function allocatedRuntimeNames(code) {
@@ -96,6 +231,14 @@ function allocatedRuntimeNames(code) {
96
231
  CONDITION,
97
232
  END,
98
233
  HIT,
234
+ REGISTER_V2,
235
+ END_V2,
236
+ HIT_V2,
237
+ FILE_V2,
238
+ CLOCK_V2,
239
+ HITS_V2,
240
+ DECISIONS_V2,
241
+ COMPLETE_V2,
99
242
  SELECTION_BEGIN,
100
243
  SELECTION_RIGHT,
101
244
  SELECTION_END,
@@ -151,6 +294,16 @@ function isExecutableStatement(node) {
151
294
  }
152
295
  return !("declare" in node && node.declare === true);
153
296
  }
297
+ function executablePointNode(node) {
298
+ if (t.isExportNamedDeclaration(node) || t.isExportDefaultDeclaration(node)) {
299
+ const declaration = node.declaration;
300
+ return declaration && t.isStatement(declaration) &&
301
+ isExecutableStatement(declaration)
302
+ ? declaration
303
+ : undefined;
304
+ }
305
+ return isExecutableStatement(node) ? node : undefined;
306
+ }
154
307
  function isAnonymousFunctionDefinition(node) {
155
308
  return (t.isArrowFunctionExpression(node) ||
156
309
  (t.isFunctionExpression(node) && node.id === null) ||
@@ -222,12 +375,24 @@ function isInsideFunctionParameter(path) {
222
375
  }
223
376
  return false;
224
377
  }
225
- export function instrumentMcdc(code, file) {
378
+ /** Number encoding remains exact while both 3^n and its doubled outcome index are safe. */
379
+ export const PROBE_V2_MAX_CONDITIONS = 32;
380
+ export const PROBE_V2_DENSE_CONDITION_LIMIT = 6;
381
+ export function instrumentMcdc(code, file, options = {}) {
226
382
  const names = allocatedRuntimeNames(code);
227
383
  const BEGIN = names["__supercovMcdcBegin"];
228
384
  const CONDITION = names["__supercovMcdcCondition"];
229
385
  const END = names["__supercovMcdcEnd"];
230
386
  const HIT = names["__supercovCoverageHit"];
387
+ const REGISTER_V2 = names["__supercovRegisterProbeV2"];
388
+ const END_V2 = names["__supercovMcdcEndV2"];
389
+ const HIT_V2 = names["__supercovCoverageHitV2"];
390
+ const FILE_V2 = names["__supercovProbeFileV2"];
391
+ const CLOCK_V2 = names["__supercovProbeClockV2"];
392
+ const HITS_V2 = names["__supercovProbeHitsV2"];
393
+ const DECISIONS_V2 = names["__supercovProbeDecisionsV2"];
394
+ const COMPLETE_V2 = names["__supercovProbeCompleteV2"];
395
+ const probeVersion = options.probeVersion ?? 1;
231
396
  const SELECTION_BEGIN = names["__supercovSelectionBegin"];
232
397
  const SELECTION_RIGHT = names["__supercovSelectionRight"];
233
398
  const SELECTION_END = names["__supercovSelectionEnd"];
@@ -258,6 +423,7 @@ export function instrumentMcdc(code, file) {
258
423
  plugins: parserPlugins,
259
424
  });
260
425
  const decisions = [];
426
+ const decisionVectorCounts = [];
261
427
  const points = [];
262
428
  const branches = [];
263
429
  const limitations = [];
@@ -304,28 +470,103 @@ export function instrumentMcdc(code, file) {
304
470
  },
305
471
  });
306
472
  const isUnsafeInstrumentationContext = (path) => isWithinSourceSensitiveFunction(path) || isInsideWithStatement(path);
473
+ // A synchronous function cannot suspend while its body is executing. A
474
+ // nested coverage scope restores the caller's epoch before returning, so
475
+ // interior probes can compare against one scalar captured at entry instead
476
+ // of repeatedly loading the shared clock object. Async functions and
477
+ // generators deliberately keep the shared clock: either may resume under a
478
+ // later attribution epoch. Parameter initializers also use the shared clock
479
+ // because they execute before this body-local declaration.
480
+ const functionEpochs = new WeakMap();
481
+ const functionDecisionCaches = new WeakMap();
482
+ const decisionCacheDeclarations = [];
483
+ if (probeVersion === 2) {
484
+ traverse(ast, {
485
+ Function(path) {
486
+ const node = path.node;
487
+ if (node.async ||
488
+ node.generator ||
489
+ !t.isBlockStatement(node.body) ||
490
+ isUnsafeInstrumentationContext(path))
491
+ return;
492
+ const epoch = path.scope.generateUidIdentifier("supercovEpoch");
493
+ const declaration = t.variableDeclaration("const", [
494
+ t.variableDeclarator(t.cloneNode(epoch), t.memberExpression(t.identifier(CLOCK_V2), t.identifier("epoch"))),
495
+ ]);
496
+ generatedStatements.add(declaration);
497
+ node.body.body.unshift(declaration);
498
+ functionEpochs.set(node, epoch);
499
+ },
500
+ });
501
+ }
502
+ const functionOwnerForPath = (path) => {
503
+ const owner = path.findParent((parent) => parent.isFunction());
504
+ return owner?.isFunction() ? owner : undefined;
505
+ };
506
+ const epochForPath = (path) => {
507
+ const owner = functionOwnerForPath(path);
508
+ return owner ? functionEpochs.get(owner.node) : undefined;
509
+ };
510
+ const localDecisionForPath = (path) => {
511
+ const owner = functionOwnerForPath(path);
512
+ if (!owner ||
513
+ !functionEpochs.has(owner.node) ||
514
+ !t.isBlockStatement(owner.node.body))
515
+ return undefined;
516
+ let cache = functionDecisionCaches.get(owner.node);
517
+ if (!cache) {
518
+ cache = {
519
+ bits: owner.scope.generateUidIdentifier("supercovComplete"),
520
+ next: 0,
521
+ };
522
+ functionDecisionCaches.set(owner.node, cache);
523
+ decisionCacheDeclarations.push({
524
+ body: owner.node.body,
525
+ bits: cache.bits,
526
+ });
527
+ }
528
+ const bit = cache.next++;
529
+ return bit < 30 ? { bits: cache.bits, mask: 2 ** bit } : undefined;
530
+ };
307
531
  // Record executable statements before adding any instrumentation statements.
532
+ const instrumentedStatements = new WeakSet();
308
533
  traverse(ast, {
309
534
  Statement(path) {
310
535
  const node = path.node;
311
- if (!node.loc ||
536
+ if (instrumentedStatements.has(node))
537
+ return;
538
+ if ((path.parentPath.isExportNamedDeclaration() ||
539
+ path.parentPath.isExportDefaultDeclaration()) &&
540
+ path.parentPath.node.declaration === node)
541
+ return;
542
+ const pointNode = executablePointNode(node);
543
+ if (!pointNode?.loc ||
312
544
  isUnsafeInstrumentationContext(path) ||
313
545
  generatedStatements.has(node) ||
314
- !isExecutableStatement(node))
546
+ !isExecutableStatement(pointNode))
315
547
  return;
316
548
  if (path.parentPath.isLabeledStatement())
317
549
  return;
318
- const id = stableId(file, "statement", node);
550
+ const id = stableId(file, "statement", pointNode);
319
551
  points.push({
320
552
  id,
321
553
  kind: "statement",
322
554
  file,
323
- line: node.loc.start.line,
324
- column: node.loc.start.column + 1,
325
- source: sourceFor(code, node),
555
+ line: pointNode.loc.start.line,
556
+ column: pointNode.loc.start.column + 1,
557
+ source: sourceFor(code, pointNode),
326
558
  });
327
- const probe = hitStatement(id, HIT);
559
+ const probe = hitStatement(id, probeVersion === 2 ? HIT_V2 : HIT, probeVersion === 2
560
+ ? {
561
+ fileState: t.identifier(FILE_V2),
562
+ clock: t.identifier(CLOCK_V2),
563
+ hitEpochs: t.identifier(HITS_V2),
564
+ pointIndex: points.length - 1,
565
+ epoch: epochForPath(path),
566
+ }
567
+ : undefined);
328
568
  generatedStatements.add(probe);
569
+ instrumentedStatements.add(node);
329
570
  if (path.parentPath.isProgram() ||
330
571
  path.parentPath.isBlockStatement() ||
331
572
  path.parentPath.isSwitchCase()) {
@@ -339,7 +580,6 @@ export function instrumentMcdc(code, file) {
339
580
  path.key === "consequent" ||
340
581
  path.key === "alternate") {
341
582
  path.replaceWith(t.blockStatement([probe, node]));
342
- path.skip();
343
583
  }
344
584
  },
345
585
  });
@@ -443,10 +683,14 @@ export function instrumentMcdc(code, file) {
443
683
  for (const site of chain.sites) {
444
684
  chain.path.scope.push({ id: t.cloneNode(site.frame), kind: "let" });
445
685
  const callee = site.node.callee;
446
- if (t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) {
686
+ if (t.isMemberExpression(callee) ||
687
+ t.isOptionalMemberExpression(callee)) {
447
688
  if (t.isPrivateName(callee.property)) {
448
689
  if (t.isExpression(callee.object))
449
690
  callee.object = t.callExpression(t.identifier(OPTIONAL_CALL_REACHED), [t.cloneNode(site.frame), callee.object]);
691
+ site.node.arguments.unshift(t.spreadElement(t.callExpression(t.identifier(OPTIONAL_CALL_CONTINUED), [
692
+ t.cloneNode(site.frame),
693
+ ])));
450
694
  continue;
451
695
  }
452
696
  const property = callee.computed
@@ -540,8 +784,6 @@ export function instrumentMcdc(code, file) {
540
784
  Function(path) {
541
785
  if (isUnsafeInstrumentationContext(path))
542
786
  return;
543
- if (!t.isBlockStatement(path.node.body))
544
- return;
545
787
  const entries = [];
546
788
  const visitPattern = (pattern) => {
547
789
  if (t.isTSParameterProperty(pattern)) {
@@ -600,7 +842,19 @@ export function instrumentMcdc(code, file) {
600
842
  };
601
843
  for (const parameter of path.node.params)
602
844
  visitPattern(parameter);
603
- path.node.body.body.unshift(...entries);
845
+ if (entries.length === 0)
846
+ return;
847
+ if (t.isBlockStatement(path.node.body)) {
848
+ path.node.body.body.unshift(...entries);
849
+ return;
850
+ }
851
+ if (!path.isArrowFunctionExpression())
852
+ throw new Error("Only arrow functions may have expression bodies");
853
+ const value = path.node.body;
854
+ path.node.body = t.blockStatement([
855
+ ...entries,
856
+ t.returnStatement(value),
857
+ ]);
604
858
  },
605
859
  });
606
860
  // Destructuring declarations use the same token protocol. For loop-binding
@@ -701,6 +955,8 @@ export function instrumentMcdc(code, file) {
701
955
  });
702
956
  // Try/catch and enumeration loops use frames finalized in `finally`, so
703
957
  // return, break, continue, rejection, and throw paths remain observable.
958
+ const tryTargets = new WeakMap();
959
+ const enumerationTargets = new WeakMap();
704
960
  traverse(ast, {
705
961
  TryStatement(path) {
706
962
  if (isUnsafeInstrumentationContext(path))
@@ -723,23 +979,7 @@ export function instrumentMcdc(code, file) {
723
979
  { id: catchId, label: "catch entered" },
724
980
  ],
725
981
  });
726
- const frameId = path.scope.generateUidIdentifier("supercovTryFrame");
727
- const frameScope = path.scope.getFunctionParent() ?? path.scope.getProgramParent();
728
- frameScope.push({ id: t.cloneNode(frameId), kind: "let" });
729
- path.insertBefore(t.expressionStatement(t.assignmentExpression("=", t.cloneNode(frameId), t.callExpression(t.identifier(TRY_BEGIN), [
730
- t.stringLiteral(successId),
731
- t.stringLiteral(catchId),
732
- ]))));
733
- node.handler.body.body.unshift(t.expressionStatement(t.callExpression(t.identifier(TRY_CATCH), [
734
- t.cloneNode(frameId),
735
- t.identifier("undefined"),
736
- ])));
737
- const end = t.expressionStatement(t.callExpression(t.identifier(TRY_END), [t.cloneNode(frameId)]));
738
- if (node.finalizer)
739
- node.finalizer.body.unshift(end);
740
- else
741
- node.finalizer = t.blockStatement([end]);
742
- path.skip();
982
+ tryTargets.set(node, { successId, catchId });
743
983
  },
744
984
  "ForInStatement|ForOfStatement"(path) {
745
985
  if (isUnsafeInstrumentationContext(path))
@@ -763,39 +1003,86 @@ export function instrumentMcdc(code, file) {
763
1003
  { id: enteredId, label: "one or more iterations" },
764
1004
  ],
765
1005
  });
766
- const frameId = path.scope.generateUidIdentifier("supercovLoopFrame");
767
- const frameScope = path.scope.getFunctionParent() ?? path.scope.getProgramParent();
768
- frameScope.push({ id: t.cloneNode(frameId), kind: "let" });
769
- const assignment = t.expressionStatement(t.assignmentExpression("=", t.cloneNode(frameId), t.callExpression(t.identifier(LOOP_BEGIN), [
770
- t.stringLiteral(zeroId),
771
- t.stringLiteral(enteredId),
772
- ])));
773
- const loop = t.cloneNode(node, true);
774
- loop.body = t.isBlockStatement(loop.body)
775
- ? loop.body
776
- : t.blockStatement([loop.body]);
777
- loop.body.body.unshift(t.expressionStatement(t.callExpression(t.identifier(LOOP_ENTERED), [
778
- t.cloneNode(frameId),
779
- ])));
780
- let loopStatement = loop;
781
- let replacementPath = path;
782
- while (replacementPath.parentPath?.isLabeledStatement() &&
783
- replacementPath.parentPath.node.body === replacementPath.node) {
784
- loopStatement = t.labeledStatement(t.cloneNode(replacementPath.parentPath.node.label), loopStatement);
785
- replacementPath = replacementPath.parentPath;
786
- }
787
- const wrapped = t.tryStatement(t.blockStatement([loopStatement]), null, t.blockStatement([
788
- t.expressionStatement(t.callExpression(t.identifier(LOOP_END), [t.cloneNode(frameId)])),
789
- ]));
790
- if (replacementPath === path) {
791
- path.insertBefore(assignment);
792
- path.replaceWith(wrapped);
1006
+ enumerationTargets.set(node, { zeroId, enteredId });
1007
+ },
1008
+ });
1009
+ // Transform bottom-up after the denominator is frozen. This keeps nested
1010
+ // try/loop nodes attached to the tree while they are rewritten; mutating an
1011
+ // outer path first can leave a nested Babel path detached while still
1012
+ // recording an obligation that can never emit evidence.
1013
+ traverse(ast, {
1014
+ TryStatement: {
1015
+ exit(path) {
1016
+ if (isUnsafeInstrumentationContext(path))
1017
+ return;
1018
+ const node = path.node;
1019
+ const target = tryTargets.get(node);
1020
+ if (!target || !node.handler)
1021
+ return;
1022
+ const frameId = path.scope.generateUidIdentifier("supercovTryFrame");
1023
+ const frameScope = path.scope.getFunctionParent() ?? path.scope.getProgramParent();
1024
+ frameScope.push({ id: t.cloneNode(frameId), kind: "let" });
1025
+ path.insertBefore(t.expressionStatement(t.assignmentExpression("=", t.cloneNode(frameId), t.callExpression(t.identifier(TRY_BEGIN), [
1026
+ t.stringLiteral(target.successId),
1027
+ t.stringLiteral(target.catchId),
1028
+ ]))));
1029
+ node.handler.body.body.unshift(t.expressionStatement(t.callExpression(t.identifier(TRY_CATCH), [
1030
+ t.cloneNode(frameId),
1031
+ t.identifier("undefined"),
1032
+ ])));
1033
+ const end = t.expressionStatement(t.callExpression(t.identifier(TRY_END), [t.cloneNode(frameId)]));
1034
+ if (node.finalizer)
1035
+ node.finalizer.body.unshift(end);
1036
+ else
1037
+ node.finalizer = t.blockStatement([end]);
793
1038
  path.skip();
794
- }
795
- else {
796
- replacementPath.replaceWith(t.blockStatement([assignment, wrapped]));
797
- replacementPath.skip();
798
- }
1039
+ },
1040
+ },
1041
+ "ForInStatement|ForOfStatement": {
1042
+ exit(path) {
1043
+ if (isUnsafeInstrumentationContext(path))
1044
+ return;
1045
+ const node = path.node;
1046
+ const target = enumerationTargets.get(node);
1047
+ if (!target)
1048
+ return;
1049
+ const frameId = path.scope.generateUidIdentifier("supercovLoopFrame");
1050
+ const frameScope = path.scope.getFunctionParent() ?? path.scope.getProgramParent();
1051
+ frameScope.push({ id: t.cloneNode(frameId), kind: "let" });
1052
+ const assignment = t.expressionStatement(t.assignmentExpression("=", t.cloneNode(frameId), t.callExpression(t.identifier(LOOP_BEGIN), [
1053
+ t.stringLiteral(target.zeroId),
1054
+ t.stringLiteral(target.enteredId),
1055
+ ])));
1056
+ const loop = t.cloneNode(node, true);
1057
+ restoreClonedOffsets(node, loop);
1058
+ loop.body = t.isBlockStatement(loop.body)
1059
+ ? loop.body
1060
+ : t.blockStatement([loop.body]);
1061
+ loop.body.body.unshift(t.expressionStatement(probeVersion === 2
1062
+ ? t.assignmentExpression("=", t.memberExpression(t.cloneNode(frameId), t.identifier("entered")), t.booleanLiteral(true))
1063
+ : t.callExpression(t.identifier(LOOP_ENTERED), [
1064
+ t.cloneNode(frameId),
1065
+ ])));
1066
+ let loopStatement = loop;
1067
+ let replacementPath = path;
1068
+ while (replacementPath.parentPath?.isLabeledStatement() &&
1069
+ replacementPath.parentPath.node.body === replacementPath.node) {
1070
+ loopStatement = t.labeledStatement(t.cloneNode(replacementPath.parentPath.node.label), loopStatement);
1071
+ replacementPath = replacementPath.parentPath;
1072
+ }
1073
+ const wrapped = t.tryStatement(t.blockStatement([loopStatement]), null, t.blockStatement([
1074
+ t.expressionStatement(t.callExpression(t.identifier(LOOP_END), [t.cloneNode(frameId)])),
1075
+ ]));
1076
+ if (replacementPath === path) {
1077
+ path.insertBefore(assignment);
1078
+ path.replaceWith(wrapped);
1079
+ path.skip();
1080
+ }
1081
+ else {
1082
+ replacementPath.replaceWith(t.blockStatement([assignment, wrapped]));
1083
+ replacementPath.skip();
1084
+ }
1085
+ },
799
1086
  },
800
1087
  });
801
1088
  // Function entry is separate from statement coverage so empty functions and
@@ -817,7 +1104,14 @@ export function instrumentMcdc(code, file) {
817
1104
  source: sourceFor(code, node),
818
1105
  ...(functionLabel(path) ? { label: functionLabel(path) } : {}),
819
1106
  });
820
- const probe = hitStatement(id, HIT);
1107
+ const probe = hitStatement(id, probeVersion === 2 ? HIT_V2 : HIT, probeVersion === 2
1108
+ ? {
1109
+ fileState: t.identifier(FILE_V2),
1110
+ clock: t.identifier(CLOCK_V2),
1111
+ hitEpochs: t.identifier(HITS_V2),
1112
+ pointIndex: points.length - 1,
1113
+ }
1114
+ : undefined);
821
1115
  generatedStatements.add(probe);
822
1116
  if (t.isBlockStatement(node.body)) {
823
1117
  node.body.body.unshift(probe);
@@ -844,6 +1138,7 @@ export function instrumentMcdc(code, file) {
844
1138
  conditions: originalConditions.map((condition) => sourceFor(code, condition)),
845
1139
  kind,
846
1140
  };
1141
+ const decisionIndex = decisions.length;
847
1142
  decisions.push(meta);
848
1143
  // A loop predicate's path scope may be represented by Babel as the loop
849
1144
  // body's block. Declaring the frame there puts it after the predicate that
@@ -856,29 +1151,99 @@ export function instrumentMcdc(code, file) {
856
1151
  path.scope.parent
857
1152
  ? path.scope.parent
858
1153
  : path.scope;
859
- const frameScope = evaluationScope.getFunctionParent() ??
860
- evaluationScope.getProgramParent();
1154
+ const frameScope = evaluationScope.getFunctionParent() ?? evaluationScope.getProgramParent();
861
1155
  const frameId = frameScope.generateUidIdentifier("supercovMcdcFrame");
862
- if (!inlineFrame)
1156
+ const useV2 = probeVersion === 2 &&
1157
+ originalConditions.length <= PROBE_V2_MAX_CONDITIONS;
1158
+ const conditionTemps = useV2
1159
+ ? originalConditions.map(() => frameScope.generateUidIdentifier("supercovMcdcValue"))
1160
+ : [];
1161
+ const resultTemp = useV2
1162
+ ? frameScope.generateUidIdentifier("supercovMcdcResult")
1163
+ : undefined;
1164
+ const useDenseV2 = useV2 && originalConditions.length <= PROBE_V2_DENSE_CONDITION_LIMIT;
1165
+ const useSaturatedV2 = useDenseV2 && isCoverageTransparentDecision(originalConditions);
1166
+ const originalDecision = useSaturatedV2
1167
+ ? t.cloneNode(path.node, true)
1168
+ : undefined;
1169
+ if (originalDecision)
1170
+ markDecisionLogicalNodes(originalDecision, decisionLogicalNodes);
1171
+ decisionVectorCounts.push(useSaturatedV2
1172
+ ? probeV2ReachableVectorCount(path.node, originalConditions)
1173
+ : 0);
1174
+ const decisionEpoch = inlineFrame ? undefined : epochForPath(path);
1175
+ const localComplete = useSaturatedV2 && !inlineFrame ? localDecisionForPath(path) : undefined;
1176
+ const vectorTemp = useDenseV2
1177
+ ? frameScope.generateUidIdentifier("supercovMcdcVector")
1178
+ : undefined;
1179
+ if (!inlineFrame) {
863
1180
  frameScope.push({ id: t.cloneNode(frameId), kind: "let" });
864
- const instrumented = instrumentConditions(path.node, frameId, { value: 0 }, decisionLogicalNodes, CONDITION);
1181
+ for (const temporary of conditionTemps)
1182
+ frameScope.push({ id: t.cloneNode(temporary), kind: "let" });
1183
+ if (resultTemp)
1184
+ frameScope.push({ id: t.cloneNode(resultTemp), kind: "let" });
1185
+ if (vectorTemp)
1186
+ frameScope.push({ id: t.cloneNode(vectorTemp), kind: "let" });
1187
+ }
1188
+ const instrumented = useV2
1189
+ ? instrumentConditionsV2(path.node, frameId, conditionTemps, { value: 0 }, decisionLogicalNodes)
1190
+ : instrumentConditions(path.node, frameId, { value: 0 }, decisionLogicalNodes, CONDITION);
865
1191
  const begin = t.callExpression(t.identifier(BEGIN), [
866
1192
  t.stringLiteral(id),
867
1193
  t.valueToNode(meta),
868
1194
  ]);
869
- const assignFrame = t.assignmentExpression("=", t.cloneNode(frameId), begin);
870
- const end = t.callExpression(t.identifier(END), [
871
- t.cloneNode(frameId),
872
- instrumented,
873
- ]);
874
- path.replaceWith(inlineFrame
1195
+ const assignFrame = t.assignmentExpression("=", t.cloneNode(frameId), useV2 ? t.numericLiteral(0) : begin);
1196
+ const end = useV2
1197
+ ? t.callExpression(t.identifier(END_V2), [
1198
+ t.identifier(FILE_V2),
1199
+ t.numericLiteral(decisionIndex),
1200
+ t.cloneNode(frameId),
1201
+ t.cloneNode(resultTemp),
1202
+ ])
1203
+ : t.callExpression(t.identifier(END), [
1204
+ t.cloneNode(frameId),
1205
+ instrumented,
1206
+ ]);
1207
+ const v2Tail = useV2
1208
+ ? [
1209
+ t.assignmentExpression("=", t.cloneNode(resultTemp), instrumented),
1210
+ ...(useDenseV2
1211
+ ? [
1212
+ t.assignmentExpression("=", t.cloneNode(vectorTemp), t.binaryExpression("+", t.binaryExpression("*", t.cloneNode(frameId), t.numericLiteral(2)), t.conditionalExpression(t.cloneNode(resultTemp), t.numericLiteral(1), t.numericLiteral(0)))),
1213
+ t.logicalExpression("||", t.binaryExpression("===", t.memberExpression(t.memberExpression(t.identifier(DECISIONS_V2), t.numericLiteral(decisionIndex), true), t.cloneNode(vectorTemp), true), decisionEpoch
1214
+ ? t.cloneNode(decisionEpoch)
1215
+ : t.memberExpression(t.identifier(CLOCK_V2), t.identifier("epoch"))), end),
1216
+ t.cloneNode(resultTemp),
1217
+ ]
1218
+ : [end]),
1219
+ ]
1220
+ : [end];
1221
+ const observedDecision = inlineFrame
875
1222
  ? t.callExpression(t.arrowFunctionExpression([], t.blockStatement([
876
1223
  t.variableDeclaration("let", [
877
- t.variableDeclarator(t.cloneNode(frameId), begin),
1224
+ t.variableDeclarator(t.cloneNode(frameId), useV2 ? t.numericLiteral(0) : begin),
1225
+ ...conditionTemps.map((temporary) => t.variableDeclarator(t.cloneNode(temporary))),
1226
+ ...(resultTemp
1227
+ ? [t.variableDeclarator(t.cloneNode(resultTemp))]
1228
+ : []),
1229
+ ...(vectorTemp
1230
+ ? [t.variableDeclarator(t.cloneNode(vectorTemp))]
1231
+ : []),
878
1232
  ]),
879
- t.returnStatement(end),
1233
+ t.returnStatement(useV2 ? t.sequenceExpression(v2Tail) : end),
880
1234
  ])), [])
881
- : t.sequenceExpression([assignFrame, end]));
1235
+ : t.sequenceExpression([assignFrame, ...(useV2 ? v2Tail : [end])]);
1236
+ const completeCheck = useSaturatedV2
1237
+ ? t.binaryExpression("===", t.memberExpression(t.identifier(COMPLETE_V2), t.numericLiteral(decisionIndex), true), decisionEpoch
1238
+ ? t.cloneNode(decisionEpoch)
1239
+ : t.memberExpression(t.identifier(CLOCK_V2), t.identifier("epoch")))
1240
+ : undefined;
1241
+ const completeFastPath = completeCheck && localComplete
1242
+ ? t.logicalExpression("||", t.binaryExpression("!==", t.binaryExpression("&", t.cloneNode(localComplete.bits), t.numericLiteral(localComplete.mask)), t.numericLiteral(0)), t.logicalExpression("&&", completeCheck, t.assignmentExpression("|=", t.cloneNode(localComplete.bits), t.numericLiteral(localComplete.mask))))
1243
+ : completeCheck;
1244
+ path.replaceWith(useSaturatedV2
1245
+ ? t.conditionalExpression(completeFastPath, originalDecision, observedDecision)
1246
+ : observedDecision);
882
1247
  path.skip();
883
1248
  };
884
1249
  traverse(ast, {
@@ -1158,14 +1523,17 @@ export function instrumentMcdc(code, file) {
1158
1523
  if (isUnsafeInstrumentationContext(path))
1159
1524
  return;
1160
1525
  const callee = path.node.callee;
1161
- const property = (t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
1526
+ const property = (t.isMemberExpression(callee) ||
1527
+ t.isOptionalMemberExpression(callee)) &&
1162
1528
  !callee.computed &&
1163
1529
  t.isIdentifier(callee.property)
1164
1530
  ? callee.property.name
1165
1531
  : undefined;
1166
1532
  const identifier = t.isIdentifier(callee) ? callee.name : property;
1167
1533
  let callbackIndex = -1;
1168
- if ((property === "on" || property === "once" || property === "addListener") &&
1534
+ if ((property === "on" ||
1535
+ property === "once" ||
1536
+ property === "addListener") &&
1169
1537
  t.isStringLiteral(path.node.arguments[0]) &&
1170
1538
  ["request", "upgrade", "connection"].includes(path.node.arguments[0].value)) {
1171
1539
  callbackIndex = 1;
@@ -1194,6 +1562,13 @@ export function instrumentMcdc(code, file) {
1194
1562
  usesRequestPhaseHandler = true;
1195
1563
  },
1196
1564
  });
1565
+ // Decision cache identifiers are discovered during instrumentation. Insert
1566
+ // their declarations only after all source traversals so no live NodePath is
1567
+ // shifted while a source statement is being rewritten.
1568
+ for (const { body, bits } of decisionCacheDeclarations)
1569
+ body.body.unshift(t.variableDeclaration("let", [
1570
+ t.variableDeclarator(t.cloneNode(bits), t.numericLiteral(0)),
1571
+ ]));
1197
1572
  const manifest = {
1198
1573
  decisions,
1199
1574
  points,
@@ -1209,6 +1584,13 @@ export function instrumentMcdc(code, file) {
1209
1584
  t.importSpecifier(t.identifier(CONDITION), t.identifier("mcdcCondition")),
1210
1585
  t.importSpecifier(t.identifier(END), t.identifier("mcdcEnd")),
1211
1586
  t.importSpecifier(t.identifier(HIT), t.identifier("coverageHit")),
1587
+ ...(probeVersion === 2
1588
+ ? [
1589
+ t.importSpecifier(t.identifier(REGISTER_V2), t.identifier("registerProbeV2")),
1590
+ t.importSpecifier(t.identifier(END_V2), t.identifier("mcdcEndV2")),
1591
+ t.importSpecifier(t.identifier(HIT_V2), t.identifier("coverageHitV2")),
1592
+ ]
1593
+ : []),
1212
1594
  t.importSpecifier(t.identifier(SELECTION_BEGIN), t.identifier("selectionBegin")),
1213
1595
  t.importSpecifier(t.identifier(SELECTION_RIGHT), t.identifier("selectionRight")),
1214
1596
  t.importSpecifier(t.identifier(SELECTION_END), t.identifier("selectionEnd")),
@@ -1230,7 +1612,23 @@ export function instrumentMcdc(code, file) {
1230
1612
  t.importSpecifier(t.identifier(WITH_REQUEST_PHASE), t.identifier("withRequestPhase")),
1231
1613
  ]
1232
1614
  : []),
1233
- ], t.stringLiteral(RUNTIME_MODULE)));
1615
+ ], t.stringLiteral(RUNTIME_MODULE)), ...(probeVersion === 2
1616
+ ? [
1617
+ t.variableDeclaration("const", [
1618
+ t.variableDeclarator(t.identifier(FILE_V2), t.callExpression(t.identifier(REGISTER_V2), [
1619
+ t.valueToNode({
1620
+ decisions,
1621
+ pointIds: points.map((point) => point.id),
1622
+ decisionVectorCounts,
1623
+ }),
1624
+ ])),
1625
+ t.variableDeclarator(t.identifier(CLOCK_V2), t.memberExpression(t.identifier(FILE_V2), t.identifier("clock"))),
1626
+ t.variableDeclarator(t.identifier(HITS_V2), t.memberExpression(t.identifier(FILE_V2), t.identifier("hitEpochs"))),
1627
+ t.variableDeclarator(t.identifier(DECISIONS_V2), t.memberExpression(t.identifier(FILE_V2), t.identifier("decisionEpochs"))),
1628
+ t.variableDeclarator(t.identifier(COMPLETE_V2), t.memberExpression(t.identifier(FILE_V2), t.identifier("decisionCompleteEpochs"))),
1629
+ ]),
1630
+ ]
1631
+ : []));
1234
1632
  }
1235
1633
  const output = generate({
1236
1634
  ...ast,