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.
- package/README.md +14 -1
- package/bin/native.js +87 -0
- package/bin/supercov.js +20 -3
- package/dist/agentJson.d.ts +2 -1
- package/dist/agentJson.d.ts.map +1 -1
- package/dist/agentJson.js +18 -0
- package/dist/agentJson.js.map +1 -1
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +9 -5
- package/dist/analyze.js.map +1 -1
- package/dist/cli.js +126 -23
- package/dist/cli.js.map +1 -1
- package/dist/directInstrumenter.d.ts.map +1 -1
- package/dist/directInstrumenter.js +39 -19
- package/dist/directInstrumenter.js.map +1 -1
- package/dist/engineEvidence.d.ts +9 -0
- package/dist/engineEvidence.d.ts.map +1 -0
- package/dist/engineEvidence.js +31 -0
- package/dist/engineEvidence.js.map +1 -0
- package/dist/engineInstrumenter.d.ts +16 -0
- package/dist/engineInstrumenter.d.ts.map +1 -0
- package/dist/engineInstrumenter.js +50 -0
- package/dist/engineInstrumenter.js.map +1 -0
- package/dist/engineProcess.d.ts +11 -0
- package/dist/engineProcess.d.ts.map +1 -0
- package/dist/engineProcess.js +59 -0
- package/dist/engineProcess.js.map +1 -0
- package/dist/evidenceArchive.d.ts +1 -0
- package/dist/evidenceArchive.d.ts.map +1 -1
- package/dist/evidenceArchive.js +59 -13
- package/dist/evidenceArchive.js.map +1 -1
- package/dist/instrumenter.d.ts +8 -1
- package/dist/instrumenter.d.ts.map +1 -1
- package/dist/instrumenter.js +585 -113
- package/dist/instrumenter.js.map +1 -1
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +20 -1
- package/dist/integrity.js.map +1 -1
- package/dist/launchSupervisor.d.ts.map +1 -1
- package/dist/launchSupervisor.js +3 -0
- package/dist/launchSupervisor.js.map +1 -1
- package/dist/merge.d.ts.map +1 -1
- package/dist/merge.js +9 -6
- package/dist/merge.js.map +1 -1
- package/dist/nodeAssert.d.ts +31 -0
- package/dist/nodeAssert.d.ts.map +1 -0
- package/dist/nodeAssert.js +30 -0
- package/dist/nodeAssert.js.map +1 -0
- package/dist/nodeAssertAdapter.d.ts +6 -0
- package/dist/nodeAssertAdapter.d.ts.map +1 -0
- package/dist/nodeAssertAdapter.js +94 -0
- package/dist/nodeAssertAdapter.js.map +1 -0
- package/dist/nodeAssertStrict.d.ts +31 -0
- package/dist/nodeAssertStrict.d.ts.map +1 -0
- package/dist/nodeAssertStrict.js +28 -0
- package/dist/nodeAssertStrict.js.map +1 -0
- package/dist/nodeAssertionInstrumenter.d.ts +8 -0
- package/dist/nodeAssertionInstrumenter.d.ts.map +1 -0
- package/dist/nodeAssertionInstrumenter.js +271 -0
- package/dist/nodeAssertionInstrumenter.js.map +1 -0
- package/dist/nodeTest.d.ts.map +1 -1
- package/dist/nodeTest.js +14 -5
- package/dist/nodeTest.js.map +1 -1
- package/dist/playwright.d.ts.map +1 -1
- package/dist/playwright.js +177 -89
- package/dist/playwright.js.map +1 -1
- package/dist/processDiagnostics.d.ts +34 -0
- package/dist/processDiagnostics.d.ts.map +1 -0
- package/dist/processDiagnostics.js +155 -0
- package/dist/processDiagnostics.js.map +1 -0
- package/dist/project.d.ts +1 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +39 -4
- package/dist/project.js.map +1 -1
- package/dist/query.d.ts +11 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +224 -11
- package/dist/query.js.map +1 -1
- package/dist/register.mjs +97 -4
- package/dist/register.mjs.map +1 -1
- package/dist/resolve-loader.d.mts.map +1 -1
- package/dist/resolve-loader.mjs +16 -2
- package/dist/resolve-loader.mjs.map +1 -1
- package/dist/runAnalysis.d.ts.map +1 -1
- package/dist/runAnalysis.js +3 -1
- package/dist/runAnalysis.js.map +1 -1
- package/dist/runnerEvidence.d.ts +3 -3
- package/dist/runnerEvidence.d.ts.map +1 -1
- package/dist/runnerEvidence.js +7 -4
- package/dist/runnerEvidence.js.map +1 -1
- package/dist/runtime.d.ts +77 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +395 -26
- package/dist/runtime.js.map +1 -1
- package/dist/runtimeIsolation.d.ts +3 -0
- package/dist/runtimeIsolation.d.ts.map +1 -0
- package/dist/runtimeIsolation.js +9 -0
- package/dist/runtimeIsolation.js.map +1 -0
- package/dist/sourceDiscovery.d.ts.map +1 -1
- package/dist/sourceDiscovery.js +7 -2
- package/dist/sourceDiscovery.js.map +1 -1
- package/dist/transport.js +2 -2
- package/dist/transport.js.map +1 -1
- package/dist/vitePlugin.d.ts.map +1 -1
- package/dist/vitePlugin.js +21 -7
- package/dist/vitePlugin.js.map +1 -1
- package/dist/vitest.js +38 -7
- package/dist/vitest.js.map +1 -1
- package/dist/waivers.d.ts +52 -0
- package/dist/waivers.d.ts.map +1 -0
- package/dist/waivers.js +121 -0
- package/dist/waivers.js.map +1 -0
- package/dist/workspace.d.ts +36 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +216 -37
- package/dist/workspace.js.map +1 -1
- package/package.json +29 -3
package/dist/instrumenter.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
546
|
+
!isExecutableStatement(pointNode))
|
|
303
547
|
return;
|
|
304
548
|
if (path.parentPath.isLabeledStatement())
|
|
305
549
|
return;
|
|
306
|
-
const id = stableId(file, "statement",
|
|
550
|
+
const id = stableId(file, "statement", pointNode);
|
|
307
551
|
points.push({
|
|
308
552
|
id,
|
|
309
553
|
kind: "statement",
|
|
310
554
|
file,
|
|
311
|
-
line:
|
|
312
|
-
column:
|
|
313
|
-
source: sourceFor(code,
|
|
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
|
-
|
|
373
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
801
|
-
t.
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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) ||
|
|
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" ||
|
|
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,
|