supercov 0.0.8 → 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 (117) hide show
  1. package/README.md +14 -1
  2. package/bin/native.js +87 -0
  3. package/bin/supercov.js +20 -3
  4. package/dist/agentJson.d.ts +2 -1
  5. package/dist/agentJson.d.ts.map +1 -1
  6. package/dist/agentJson.js +18 -0
  7. package/dist/agentJson.js.map +1 -1
  8. package/dist/analyze.d.ts.map +1 -1
  9. package/dist/analyze.js +9 -5
  10. package/dist/analyze.js.map +1 -1
  11. package/dist/cli.js +126 -23
  12. package/dist/cli.js.map +1 -1
  13. package/dist/directInstrumenter.d.ts.map +1 -1
  14. package/dist/directInstrumenter.js +39 -19
  15. package/dist/directInstrumenter.js.map +1 -1
  16. package/dist/engineEvidence.d.ts +9 -0
  17. package/dist/engineEvidence.d.ts.map +1 -0
  18. package/dist/engineEvidence.js +31 -0
  19. package/dist/engineEvidence.js.map +1 -0
  20. package/dist/engineInstrumenter.d.ts +16 -0
  21. package/dist/engineInstrumenter.d.ts.map +1 -0
  22. package/dist/engineInstrumenter.js +50 -0
  23. package/dist/engineInstrumenter.js.map +1 -0
  24. package/dist/engineProcess.d.ts +11 -0
  25. package/dist/engineProcess.d.ts.map +1 -0
  26. package/dist/engineProcess.js +59 -0
  27. package/dist/engineProcess.js.map +1 -0
  28. package/dist/evidenceArchive.d.ts +1 -0
  29. package/dist/evidenceArchive.d.ts.map +1 -1
  30. package/dist/evidenceArchive.js +59 -13
  31. package/dist/evidenceArchive.js.map +1 -1
  32. package/dist/instrumenter.d.ts +8 -1
  33. package/dist/instrumenter.d.ts.map +1 -1
  34. package/dist/instrumenter.js +585 -113
  35. package/dist/instrumenter.js.map +1 -1
  36. package/dist/integrity.d.ts.map +1 -1
  37. package/dist/integrity.js +20 -1
  38. package/dist/integrity.js.map +1 -1
  39. package/dist/launchSupervisor.d.ts.map +1 -1
  40. package/dist/launchSupervisor.js +3 -0
  41. package/dist/launchSupervisor.js.map +1 -1
  42. package/dist/merge.d.ts.map +1 -1
  43. package/dist/merge.js +9 -6
  44. package/dist/merge.js.map +1 -1
  45. package/dist/nodeAssert.d.ts +31 -0
  46. package/dist/nodeAssert.d.ts.map +1 -0
  47. package/dist/nodeAssert.js +30 -0
  48. package/dist/nodeAssert.js.map +1 -0
  49. package/dist/nodeAssertAdapter.d.ts +6 -0
  50. package/dist/nodeAssertAdapter.d.ts.map +1 -0
  51. package/dist/nodeAssertAdapter.js +94 -0
  52. package/dist/nodeAssertAdapter.js.map +1 -0
  53. package/dist/nodeAssertStrict.d.ts +31 -0
  54. package/dist/nodeAssertStrict.d.ts.map +1 -0
  55. package/dist/nodeAssertStrict.js +28 -0
  56. package/dist/nodeAssertStrict.js.map +1 -0
  57. package/dist/nodeAssertionInstrumenter.d.ts +8 -0
  58. package/dist/nodeAssertionInstrumenter.d.ts.map +1 -0
  59. package/dist/nodeAssertionInstrumenter.js +271 -0
  60. package/dist/nodeAssertionInstrumenter.js.map +1 -0
  61. package/dist/nodeTest.d.ts.map +1 -1
  62. package/dist/nodeTest.js +14 -5
  63. package/dist/nodeTest.js.map +1 -1
  64. package/dist/playwright.d.ts.map +1 -1
  65. package/dist/playwright.js +177 -89
  66. package/dist/playwright.js.map +1 -1
  67. package/dist/processDiagnostics.d.ts +34 -0
  68. package/dist/processDiagnostics.d.ts.map +1 -0
  69. package/dist/processDiagnostics.js +155 -0
  70. package/dist/processDiagnostics.js.map +1 -0
  71. package/dist/project.d.ts +1 -0
  72. package/dist/project.d.ts.map +1 -1
  73. package/dist/project.js +39 -4
  74. package/dist/project.js.map +1 -1
  75. package/dist/query.d.ts +11 -0
  76. package/dist/query.d.ts.map +1 -1
  77. package/dist/query.js +224 -11
  78. package/dist/query.js.map +1 -1
  79. package/dist/register.mjs +97 -4
  80. package/dist/register.mjs.map +1 -1
  81. package/dist/resolve-loader.d.mts.map +1 -1
  82. package/dist/resolve-loader.mjs +16 -2
  83. package/dist/resolve-loader.mjs.map +1 -1
  84. package/dist/runAnalysis.d.ts.map +1 -1
  85. package/dist/runAnalysis.js +3 -1
  86. package/dist/runAnalysis.js.map +1 -1
  87. package/dist/runnerEvidence.d.ts +3 -3
  88. package/dist/runnerEvidence.d.ts.map +1 -1
  89. package/dist/runnerEvidence.js +7 -4
  90. package/dist/runnerEvidence.js.map +1 -1
  91. package/dist/runtime.d.ts +77 -4
  92. package/dist/runtime.d.ts.map +1 -1
  93. package/dist/runtime.js +395 -26
  94. package/dist/runtime.js.map +1 -1
  95. package/dist/runtimeIsolation.d.ts +3 -0
  96. package/dist/runtimeIsolation.d.ts.map +1 -0
  97. package/dist/runtimeIsolation.js +9 -0
  98. package/dist/runtimeIsolation.js.map +1 -0
  99. package/dist/sourceDiscovery.d.ts.map +1 -1
  100. package/dist/sourceDiscovery.js +7 -2
  101. package/dist/sourceDiscovery.js.map +1 -1
  102. package/dist/transport.js +2 -2
  103. package/dist/transport.js.map +1 -1
  104. package/dist/vitePlugin.d.ts.map +1 -1
  105. package/dist/vitePlugin.js +21 -7
  106. package/dist/vitePlugin.js.map +1 -1
  107. package/dist/vitest.js +38 -7
  108. package/dist/vitest.js.map +1 -1
  109. package/dist/waivers.d.ts +52 -0
  110. package/dist/waivers.d.ts.map +1 -0
  111. package/dist/waivers.js +121 -0
  112. package/dist/waivers.js.map +1 -0
  113. package/dist/workspace.d.ts +36 -0
  114. package/dist/workspace.d.ts.map +1 -1
  115. package/dist/workspace.js +216 -37
  116. package/dist/workspace.js.map +1 -1
  117. package/package.json +29 -3
@@ -8,11 +8,23 @@ 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";
14
22
  const WITH_REQUEST_PHASE = "__supercovWithRequestPhase";
15
23
  const OPTIONAL_SELECT = "__supercovOptionalSelect";
24
+ const OPTIONAL_CALL_BEGIN = "__supercovOptionalCallBegin";
25
+ const OPTIONAL_CALL_REACHED = "__supercovOptionalCallReached";
26
+ const OPTIONAL_CALL_CONTINUED = "__supercovOptionalCallContinued";
27
+ const OPTIONAL_CALL_END = "__supercovOptionalCallEnd";
16
28
  const DEFAULT_SELECTED = "__supercovDefaultSelected";
17
29
  const DEFAULT_ENTERED = "__supercovDefaultEntered";
18
30
  const TRY_BEGIN = "__supercovTryBegin";
@@ -36,6 +48,26 @@ function sourceFor(code, node) {
36
48
  }
37
49
  return generate(node).code;
38
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
+ }
39
71
  function stableId(file, kind, node, suffix = "") {
40
72
  return createHash("sha256")
41
73
  .update(`${file}:${kind}:${node.start ?? 0}:${node.end ?? 0}:${suffix}`)
@@ -64,6 +96,82 @@ function collectConditions(node, conditions) {
64
96
  }
65
97
  conditions.push(node);
66
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
+ }
67
175
  function instrumentConditions(node, frameId, nextIndex, decisionLogicalNodes, conditionRuntimeName) {
68
176
  if (t.isLogicalExpression(node) &&
69
177
  (node.operator === "&&" || node.operator === "||")) {
@@ -83,7 +191,38 @@ function instrumentConditions(node, frameId, nextIndex, decisionLogicalNodes, co
83
191
  node,
84
192
  ]);
85
193
  }
86
- 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
+ }
87
226
  return t.expressionStatement(t.callExpression(t.identifier(hitRuntimeName), [t.stringLiteral(id)]));
88
227
  }
89
228
  function allocatedRuntimeNames(code) {
@@ -92,11 +231,23 @@ function allocatedRuntimeNames(code) {
92
231
  CONDITION,
93
232
  END,
94
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,
95
242
  SELECTION_BEGIN,
96
243
  SELECTION_RIGHT,
97
244
  SELECTION_END,
98
245
  WITH_REQUEST_PHASE,
99
246
  OPTIONAL_SELECT,
247
+ OPTIONAL_CALL_BEGIN,
248
+ OPTIONAL_CALL_REACHED,
249
+ OPTIONAL_CALL_CONTINUED,
250
+ OPTIONAL_CALL_END,
100
251
  DEFAULT_SELECTED,
101
252
  DEFAULT_ENTERED,
102
253
  TRY_BEGIN,
@@ -143,6 +294,16 @@ function isExecutableStatement(node) {
143
294
  }
144
295
  return !("declare" in node && node.declare === true);
145
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
+ }
146
307
  function isAnonymousFunctionDefinition(node) {
147
308
  return (t.isArrowFunctionExpression(node) ||
148
309
  (t.isFunctionExpression(node) && node.id === null) ||
@@ -214,17 +375,33 @@ function isInsideFunctionParameter(path) {
214
375
  }
215
376
  return false;
216
377
  }
217
- 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 = {}) {
218
382
  const names = allocatedRuntimeNames(code);
219
383
  const BEGIN = names["__supercovMcdcBegin"];
220
384
  const CONDITION = names["__supercovMcdcCondition"];
221
385
  const END = names["__supercovMcdcEnd"];
222
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;
223
396
  const SELECTION_BEGIN = names["__supercovSelectionBegin"];
224
397
  const SELECTION_RIGHT = names["__supercovSelectionRight"];
225
398
  const SELECTION_END = names["__supercovSelectionEnd"];
226
399
  const WITH_REQUEST_PHASE = names["__supercovWithRequestPhase"];
227
400
  const OPTIONAL_SELECT = names["__supercovOptionalSelect"];
401
+ const OPTIONAL_CALL_BEGIN = names["__supercovOptionalCallBegin"];
402
+ const OPTIONAL_CALL_REACHED = names["__supercovOptionalCallReached"];
403
+ const OPTIONAL_CALL_CONTINUED = names["__supercovOptionalCallContinued"];
404
+ const OPTIONAL_CALL_END = names["__supercovOptionalCallEnd"];
228
405
  const DEFAULT_SELECTED = names["__supercovDefaultSelected"];
229
406
  const DEFAULT_ENTERED = names["__supercovDefaultEntered"];
230
407
  const TRY_BEGIN = names["__supercovTryBegin"];
@@ -246,6 +423,7 @@ export function instrumentMcdc(code, file) {
246
423
  plugins: parserPlugins,
247
424
  });
248
425
  const decisions = [];
426
+ const decisionVectorCounts = [];
249
427
  const points = [];
250
428
  const branches = [];
251
429
  const limitations = [];
@@ -292,28 +470,103 @@ export function instrumentMcdc(code, file) {
292
470
  },
293
471
  });
294
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
+ };
295
531
  // Record executable statements before adding any instrumentation statements.
532
+ const instrumentedStatements = new WeakSet();
296
533
  traverse(ast, {
297
534
  Statement(path) {
298
535
  const node = path.node;
299
- 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 ||
300
544
  isUnsafeInstrumentationContext(path) ||
301
545
  generatedStatements.has(node) ||
302
- !isExecutableStatement(node))
546
+ !isExecutableStatement(pointNode))
303
547
  return;
304
548
  if (path.parentPath.isLabeledStatement())
305
549
  return;
306
- const id = stableId(file, "statement", node);
550
+ const id = stableId(file, "statement", pointNode);
307
551
  points.push({
308
552
  id,
309
553
  kind: "statement",
310
554
  file,
311
- line: node.loc.start.line,
312
- column: node.loc.start.column + 1,
313
- source: sourceFor(code, node),
555
+ line: pointNode.loc.start.line,
556
+ column: pointNode.loc.start.column + 1,
557
+ source: sourceFor(code, pointNode),
314
558
  });
315
- 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);
316
568
  generatedStatements.add(probe);
569
+ instrumentedStatements.add(node);
317
570
  if (path.parentPath.isProgram() ||
318
571
  path.parentPath.isBlockStatement() ||
319
572
  path.parentPath.isSwitchCase()) {
@@ -327,7 +580,6 @@ export function instrumentMcdc(code, file) {
327
580
  path.key === "consequent" ||
328
581
  path.key === "alternate") {
329
582
  path.replaceWith(t.blockStatement([probe, node]));
330
- path.skip();
331
583
  }
332
584
  },
333
585
  });
@@ -367,47 +619,109 @@ export function instrumentMcdc(code, file) {
367
619
  return;
368
620
  path.node.object = instrumentOptionalOperand(path, path.node.object);
369
621
  },
622
+ });
623
+ const optionalCallChains = new Map();
624
+ traverse(ast, {
370
625
  OptionalCallExpression(path) {
371
- if (isUnsafeInstrumentationContext(path))
372
- return;
373
- if (!path.node.optional || !t.isExpression(path.node.callee))
626
+ if (isUnsafeInstrumentationContext(path) ||
627
+ !path.node.optional ||
628
+ !path.node.loc ||
629
+ !t.isExpression(path.node.callee))
374
630
  return;
375
631
  const callee = path.node.callee;
376
- if (t.isOptionalMemberExpression(callee)) {
377
- if (path.node.loc) {
378
- limitations.push({
379
- id: stableId(file, "semantic-safety", path.node, "optional-chain-call"),
380
- kind: "semantic-safety",
381
- file,
382
- line: path.node.loc.start.line,
383
- column: path.node.loc.start.column + 1,
384
- source: sourceFor(code, path.node),
385
- reason: "optional calls whose receiver is itself an optional chain are left native to preserve chain short-circuiting and method receiver semantics",
386
- });
387
- }
388
- return;
389
- }
390
- if (t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) {
391
- // Rewriting `object.method?.()` through `.call` preserves `this` for
392
- // the immediate invocation but breaks the specification's optional
393
- // chain continuation (`object.method?.()()`). Leave it native until a
394
- // probe can preserve the chain's internal short-circuit target.
395
- if (path.node.loc) {
396
- limitations.push({
397
- id: stableId(file, "semantic-safety", path.node, "optional-method-call"),
398
- kind: "semantic-safety",
399
- file,
400
- line: path.node.loc.start.line,
401
- column: path.node.loc.start.column + 1,
402
- source: sourceFor(code, path.node),
403
- reason: "optional method calls are left native to preserve receiver and whole-chain short-circuit semantics",
404
- });
405
- }
632
+ if (t.isOptionalMemberExpression(callee) &&
633
+ t.isPrivateName(callee.property)) {
634
+ limitations.push({
635
+ id: stableId(file, "semantic-safety", path.node, "optional-private-call"),
636
+ kind: "semantic-safety",
637
+ file,
638
+ line: path.node.loc.start.line,
639
+ column: path.node.loc.start.column + 1,
640
+ source: sourceFor(code, path.node),
641
+ reason: "optional private-method calls are left native because private names cannot be wrapped as computed reference keys",
642
+ });
406
643
  return;
407
644
  }
408
- path.node.callee = instrumentOptionalOperand(path, callee);
645
+ let root = path;
646
+ while ((root.parentPath.isOptionalCallExpression() &&
647
+ root.parentPath.node.callee === root.node) ||
648
+ (root.parentPath.isOptionalMemberExpression() &&
649
+ root.parentPath.node.object === root.node))
650
+ root = root.parentPath;
651
+ if (root.parentPath.isUnaryExpression({ operator: "delete" }) &&
652
+ root.parentPath.node.argument === root.node)
653
+ root = root.parentPath;
654
+ const id = stableId(file, "optional-chain", path.node, "call");
655
+ const shortId = `${id}:short`;
656
+ const continuedId = `${id}:continued`;
657
+ branches.push({
658
+ id,
659
+ kind: "optional-chain",
660
+ file,
661
+ line: path.node.loc.start.line,
662
+ column: path.node.loc.start.column + 1,
663
+ source: sourceFor(code, path.node),
664
+ alternatives: [
665
+ { id: shortId, label: "nullish / short-circuited" },
666
+ { id: continuedId, label: "non-nullish / continued" },
667
+ ],
668
+ });
669
+ const chain = optionalCallChains.get(root.node) ?? {
670
+ path: root,
671
+ sites: [],
672
+ };
673
+ chain.sites.push({
674
+ node: path.node,
675
+ frame: root.scope.generateUidIdentifier("optionalCall"),
676
+ shortId,
677
+ continuedId,
678
+ });
679
+ optionalCallChains.set(root.node, chain);
409
680
  },
410
681
  });
682
+ for (const chain of optionalCallChains.values()) {
683
+ for (const site of chain.sites) {
684
+ chain.path.scope.push({ id: t.cloneNode(site.frame), kind: "let" });
685
+ const callee = site.node.callee;
686
+ if (t.isMemberExpression(callee) ||
687
+ t.isOptionalMemberExpression(callee)) {
688
+ if (t.isPrivateName(callee.property)) {
689
+ if (t.isExpression(callee.object))
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
+ ])));
694
+ continue;
695
+ }
696
+ const property = callee.computed
697
+ ? callee.property
698
+ : t.isIdentifier(callee.property)
699
+ ? t.stringLiteral(callee.property.name)
700
+ : callee.property;
701
+ callee.computed = true;
702
+ callee.property = t.callExpression(t.identifier(OPTIONAL_CALL_REACHED), [t.cloneNode(site.frame), property]);
703
+ }
704
+ else {
705
+ site.node.callee = t.callExpression(t.identifier(OPTIONAL_CALL_REACHED), [t.cloneNode(site.frame), callee]);
706
+ }
707
+ site.node.arguments.unshift(t.spreadElement(t.callExpression(t.identifier(OPTIONAL_CALL_CONTINUED), [
708
+ t.cloneNode(site.frame),
709
+ ])));
710
+ }
711
+ let measured = chain.path.node;
712
+ for (const site of [...chain.sites].reverse())
713
+ measured = t.callExpression(t.identifier(OPTIONAL_CALL_END), [
714
+ t.cloneNode(site.frame),
715
+ measured,
716
+ ]);
717
+ chain.path.replaceWith(t.sequenceExpression([
718
+ ...chain.sites.map((site) => t.assignmentExpression("=", t.cloneNode(site.frame), t.callExpression(t.identifier(OPTIONAL_CALL_BEGIN), [
719
+ t.stringLiteral(site.shortId),
720
+ t.stringLiteral(site.continuedId),
721
+ ]))),
722
+ measured,
723
+ ]));
724
+ }
411
725
  // Logical assignments have the same short/right split as value-selection
412
726
  // expressions, but wrapping only the RHS preserves one-time LHS evaluation.
413
727
  traverse(ast, {
@@ -470,8 +784,6 @@ export function instrumentMcdc(code, file) {
470
784
  Function(path) {
471
785
  if (isUnsafeInstrumentationContext(path))
472
786
  return;
473
- if (!t.isBlockStatement(path.node.body))
474
- return;
475
787
  const entries = [];
476
788
  const visitPattern = (pattern) => {
477
789
  if (t.isTSParameterProperty(pattern)) {
@@ -530,7 +842,19 @@ export function instrumentMcdc(code, file) {
530
842
  };
531
843
  for (const parameter of path.node.params)
532
844
  visitPattern(parameter);
533
- 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
+ ]);
534
858
  },
535
859
  });
536
860
  // Destructuring declarations use the same token protocol. For loop-binding
@@ -631,6 +955,8 @@ export function instrumentMcdc(code, file) {
631
955
  });
632
956
  // Try/catch and enumeration loops use frames finalized in `finally`, so
633
957
  // return, break, continue, rejection, and throw paths remain observable.
958
+ const tryTargets = new WeakMap();
959
+ const enumerationTargets = new WeakMap();
634
960
  traverse(ast, {
635
961
  TryStatement(path) {
636
962
  if (isUnsafeInstrumentationContext(path))
@@ -653,23 +979,7 @@ export function instrumentMcdc(code, file) {
653
979
  { id: catchId, label: "catch entered" },
654
980
  ],
655
981
  });
656
- const frameId = path.scope.generateUidIdentifier("supercovTryFrame");
657
- const frameScope = path.scope.getFunctionParent() ?? path.scope.getProgramParent();
658
- frameScope.push({ id: t.cloneNode(frameId), kind: "let" });
659
- path.insertBefore(t.expressionStatement(t.assignmentExpression("=", t.cloneNode(frameId), t.callExpression(t.identifier(TRY_BEGIN), [
660
- t.stringLiteral(successId),
661
- t.stringLiteral(catchId),
662
- ]))));
663
- node.handler.body.body.unshift(t.expressionStatement(t.callExpression(t.identifier(TRY_CATCH), [
664
- t.cloneNode(frameId),
665
- t.identifier("undefined"),
666
- ])));
667
- const end = t.expressionStatement(t.callExpression(t.identifier(TRY_END), [t.cloneNode(frameId)]));
668
- if (node.finalizer)
669
- node.finalizer.body.unshift(end);
670
- else
671
- node.finalizer = t.blockStatement([end]);
672
- path.skip();
982
+ tryTargets.set(node, { successId, catchId });
673
983
  },
674
984
  "ForInStatement|ForOfStatement"(path) {
675
985
  if (isUnsafeInstrumentationContext(path))
@@ -693,39 +1003,86 @@ export function instrumentMcdc(code, file) {
693
1003
  { id: enteredId, label: "one or more iterations" },
694
1004
  ],
695
1005
  });
696
- const frameId = path.scope.generateUidIdentifier("supercovLoopFrame");
697
- const frameScope = path.scope.getFunctionParent() ?? path.scope.getProgramParent();
698
- frameScope.push({ id: t.cloneNode(frameId), kind: "let" });
699
- const assignment = t.expressionStatement(t.assignmentExpression("=", t.cloneNode(frameId), t.callExpression(t.identifier(LOOP_BEGIN), [
700
- t.stringLiteral(zeroId),
701
- t.stringLiteral(enteredId),
702
- ])));
703
- const loop = t.cloneNode(node, true);
704
- loop.body = t.isBlockStatement(loop.body)
705
- ? loop.body
706
- : t.blockStatement([loop.body]);
707
- loop.body.body.unshift(t.expressionStatement(t.callExpression(t.identifier(LOOP_ENTERED), [
708
- t.cloneNode(frameId),
709
- ])));
710
- let loopStatement = loop;
711
- let replacementPath = path;
712
- while (replacementPath.parentPath?.isLabeledStatement() &&
713
- replacementPath.parentPath.node.body === replacementPath.node) {
714
- loopStatement = t.labeledStatement(t.cloneNode(replacementPath.parentPath.node.label), loopStatement);
715
- replacementPath = replacementPath.parentPath;
716
- }
717
- const wrapped = t.tryStatement(t.blockStatement([loopStatement]), null, t.blockStatement([
718
- t.expressionStatement(t.callExpression(t.identifier(LOOP_END), [t.cloneNode(frameId)])),
719
- ]));
720
- if (replacementPath === path) {
721
- path.insertBefore(assignment);
722
- 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]);
723
1038
  path.skip();
724
- }
725
- else {
726
- replacementPath.replaceWith(t.blockStatement([assignment, wrapped]));
727
- replacementPath.skip();
728
- }
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
+ },
729
1086
  },
730
1087
  });
731
1088
  // Function entry is separate from statement coverage so empty functions and
@@ -747,7 +1104,14 @@ export function instrumentMcdc(code, file) {
747
1104
  source: sourceFor(code, node),
748
1105
  ...(functionLabel(path) ? { label: functionLabel(path) } : {}),
749
1106
  });
750
- 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);
751
1115
  generatedStatements.add(probe);
752
1116
  if (t.isBlockStatement(node.body)) {
753
1117
  node.body.body.unshift(probe);
@@ -774,6 +1138,7 @@ export function instrumentMcdc(code, file) {
774
1138
  conditions: originalConditions.map((condition) => sourceFor(code, condition)),
775
1139
  kind,
776
1140
  };
1141
+ const decisionIndex = decisions.length;
777
1142
  decisions.push(meta);
778
1143
  // A loop predicate's path scope may be represented by Babel as the loop
779
1144
  // body's block. Declaring the frame there puts it after the predicate that
@@ -786,29 +1151,99 @@ export function instrumentMcdc(code, file) {
786
1151
  path.scope.parent
787
1152
  ? path.scope.parent
788
1153
  : path.scope;
789
- const frameScope = evaluationScope.getFunctionParent() ??
790
- evaluationScope.getProgramParent();
1154
+ const frameScope = evaluationScope.getFunctionParent() ?? evaluationScope.getProgramParent();
791
1155
  const frameId = frameScope.generateUidIdentifier("supercovMcdcFrame");
792
- 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) {
793
1180
  frameScope.push({ id: t.cloneNode(frameId), kind: "let" });
794
- 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);
795
1191
  const begin = t.callExpression(t.identifier(BEGIN), [
796
1192
  t.stringLiteral(id),
797
1193
  t.valueToNode(meta),
798
1194
  ]);
799
- const assignFrame = t.assignmentExpression("=", t.cloneNode(frameId), begin);
800
- const end = t.callExpression(t.identifier(END), [
801
- t.cloneNode(frameId),
802
- instrumented,
803
- ]);
804
- 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
805
1222
  ? t.callExpression(t.arrowFunctionExpression([], t.blockStatement([
806
1223
  t.variableDeclaration("let", [
807
- 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
+ : []),
808
1232
  ]),
809
- t.returnStatement(end),
1233
+ t.returnStatement(useV2 ? t.sequenceExpression(v2Tail) : end),
810
1234
  ])), [])
811
- : 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);
812
1247
  path.skip();
813
1248
  };
814
1249
  traverse(ast, {
@@ -1088,14 +1523,17 @@ export function instrumentMcdc(code, file) {
1088
1523
  if (isUnsafeInstrumentationContext(path))
1089
1524
  return;
1090
1525
  const callee = path.node.callee;
1091
- const property = (t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
1526
+ const property = (t.isMemberExpression(callee) ||
1527
+ t.isOptionalMemberExpression(callee)) &&
1092
1528
  !callee.computed &&
1093
1529
  t.isIdentifier(callee.property)
1094
1530
  ? callee.property.name
1095
1531
  : undefined;
1096
1532
  const identifier = t.isIdentifier(callee) ? callee.name : property;
1097
1533
  let callbackIndex = -1;
1098
- if ((property === "on" || property === "once" || property === "addListener") &&
1534
+ if ((property === "on" ||
1535
+ property === "once" ||
1536
+ property === "addListener") &&
1099
1537
  t.isStringLiteral(path.node.arguments[0]) &&
1100
1538
  ["request", "upgrade", "connection"].includes(path.node.arguments[0].value)) {
1101
1539
  callbackIndex = 1;
@@ -1124,6 +1562,13 @@ export function instrumentMcdc(code, file) {
1124
1562
  usesRequestPhaseHandler = true;
1125
1563
  },
1126
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
+ ]));
1127
1572
  const manifest = {
1128
1573
  decisions,
1129
1574
  points,
@@ -1139,10 +1584,21 @@ export function instrumentMcdc(code, file) {
1139
1584
  t.importSpecifier(t.identifier(CONDITION), t.identifier("mcdcCondition")),
1140
1585
  t.importSpecifier(t.identifier(END), t.identifier("mcdcEnd")),
1141
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
+ : []),
1142
1594
  t.importSpecifier(t.identifier(SELECTION_BEGIN), t.identifier("selectionBegin")),
1143
1595
  t.importSpecifier(t.identifier(SELECTION_RIGHT), t.identifier("selectionRight")),
1144
1596
  t.importSpecifier(t.identifier(SELECTION_END), t.identifier("selectionEnd")),
1145
1597
  t.importSpecifier(t.identifier(OPTIONAL_SELECT), t.identifier("optionalSelect")),
1598
+ t.importSpecifier(t.identifier(OPTIONAL_CALL_BEGIN), t.identifier("optionalCallBegin")),
1599
+ t.importSpecifier(t.identifier(OPTIONAL_CALL_REACHED), t.identifier("optionalCallReached")),
1600
+ t.importSpecifier(t.identifier(OPTIONAL_CALL_CONTINUED), t.identifier("optionalCallContinued")),
1601
+ t.importSpecifier(t.identifier(OPTIONAL_CALL_END), t.identifier("optionalCallEnd")),
1146
1602
  t.importSpecifier(t.identifier(DEFAULT_SELECTED), t.identifier("defaultSelected")),
1147
1603
  t.importSpecifier(t.identifier(DEFAULT_ENTERED), t.identifier("defaultEntered")),
1148
1604
  t.importSpecifier(t.identifier(TRY_BEGIN), t.identifier("tryBegin")),
@@ -1156,7 +1612,23 @@ export function instrumentMcdc(code, file) {
1156
1612
  t.importSpecifier(t.identifier(WITH_REQUEST_PHASE), t.identifier("withRequestPhase")),
1157
1613
  ]
1158
1614
  : []),
1159
- ], 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
+ : []));
1160
1632
  }
1161
1633
  const output = generate({
1162
1634
  ...ast,