as-test 1.1.2 → 1.1.3

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/CHANGELOG.md CHANGED
@@ -4,6 +4,9 @@
4
4
 
5
5
  - update build and run faliures to provide helpful error messages and reproduction commands and instructions
6
6
  - remove confirmation from ast clean
7
+ - feat: make coverage gaps hierarchical and easier to scan, with parent-before-child grouping, tree-style connectors, collapsed nested gaps by default, and `--show-coverage=all` / `--verbose` expansion.
8
+ - feat: add richer coverage point names including `DefaultValue`, `Ternary`, `IfBranch`, `Assignment`, `Loop`, `Return`, and `Throw` so uncovered points describe the actual construct instead of falling back to broad labels.
9
+ - fix: make coverage snippets underline the emitted construct span instead of recomputing from the raw column, so cases like inline `if (...) ...` and assignments highlight the right text.
7
10
 
8
11
  ## 2026-05-12 -v1.1.1
9
12
 
@@ -4,6 +4,10 @@ export class CoverPoint {
4
4
  public line: i32 = 0;
5
5
  public column: i32 = 0;
6
6
  public type: string = "";
7
+ public parentHash: string = "";
8
+ public scopeKind: string = "";
9
+ public scopeName: string = "";
10
+ public depth: i32 = 0;
7
11
  public executed: boolean = false;
8
12
  }
9
13
 
@@ -29,6 +33,10 @@ export function __REGISTER_RAW(
29
33
  line: i32,
30
34
  column: i32,
31
35
  type: string,
36
+ parentHash: string = "",
37
+ scopeKind: string = "",
38
+ scopeName: string = "",
39
+ depth: i32 = 0,
32
40
  ): void {
33
41
  if (Coverage.SN.allIndex.has(hash)) return;
34
42
  const point = new CoverPoint();
@@ -37,6 +45,10 @@ export function __REGISTER_RAW(
37
45
  point.line = line;
38
46
  point.column = column;
39
47
  point.type = type;
48
+ point.parentHash = parentHash;
49
+ point.scopeKind = scopeKind;
50
+ point.scopeName = scopeName;
51
+ point.depth = depth;
40
52
  Coverage.SN.points++;
41
53
  Coverage.SN.allIndex.set(hash, Coverage.SN.all.length);
42
54
  Coverage.SN.all.push(point);
package/assembly/index.ts CHANGED
@@ -548,6 +548,10 @@ class CoveragePointReport {
548
548
  column: i32 = 0;
549
549
  type: string = "";
550
550
  executed: bool = false;
551
+ parentHash: string = "";
552
+ scopeKind: string = "";
553
+ scopeName: string = "";
554
+ depth: i32 = 0;
551
555
 
552
556
  serialize(): string {
553
557
  return (
@@ -563,6 +567,14 @@ class CoveragePointReport {
563
567
  quote(this.type) +
564
568
  ',"executed":' +
565
569
  (this.executed ? "true" : "false") +
570
+ ',"parentHash":' +
571
+ quote(this.parentHash) +
572
+ ',"scopeKind":' +
573
+ quote(this.scopeKind) +
574
+ ',"scopeName":' +
575
+ quote(this.scopeName) +
576
+ ',"depth":' +
577
+ this.depth.toString() +
566
578
  "}"
567
579
  );
568
580
  }
@@ -632,6 +644,10 @@ function toCoveragePointReport(point: CoverPoint): CoveragePointReport {
632
644
  out.column = point.column;
633
645
  out.type = point.type;
634
646
  out.executed = point.executed;
647
+ out.parentHash = point.parentHash;
648
+ out.scopeKind = point.scopeKind;
649
+ out.scopeName = point.scopeName;
650
+ out.depth = point.depth;
635
651
  return out;
636
652
  }
637
653
 
@@ -131,6 +131,8 @@ export class Suite {
131
131
  this.verdict = "ok";
132
132
  } else if (hasSkip) {
133
133
  this.verdict = "skip";
134
+ } else if (isTestCase) {
135
+ this.verdict = "ok";
134
136
  } else {
135
137
  this.verdict = "none";
136
138
  }
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from "as-test";
2
+ // Import JSON and bs directly from json-as.
3
+ // bs import prevents json-as transform from adding broken pnpm paths.
4
+ import { JSON } from "json-as/assembly";
5
+
6
+
7
+ @json
8
+ class SimpleData {
9
+ name: string = "";
10
+ count: i32 = 0;
11
+ }
12
+
13
+
14
+ @json
15
+ class NestedData {
16
+ id: string = "";
17
+ items: string[] = [];
18
+ }
19
+
20
+ describe("JSON", () => {
21
+ describe("stringify", () => {
22
+ it("should serialize a simple class", () => {
23
+ const data = new SimpleData();
24
+ data.name = "test";
25
+ data.count = 42;
26
+
27
+ const json = JSON.stringify(data);
28
+
29
+ expect(json).toBe('{"name":"test","count":42}');
30
+ });
31
+
32
+ it("should serialize a class with arrays", () => {
33
+ const data = new NestedData();
34
+ data.id = "abc123";
35
+ data.items = ["item1", "item2"];
36
+
37
+ const json = JSON.stringify(data);
38
+
39
+ expect(json).toBe('{"id":"abc123","items":["item1","item2"]}');
40
+ });
41
+
42
+ it("should serialize primitive types", () => {
43
+ expect(JSON.stringify<i32>(42)).toBe("42");
44
+ expect(JSON.stringify<bool>(true)).toBe("true");
45
+ expect(JSON.stringify<string>("hello")).toBe('"hello"');
46
+ });
47
+
48
+ it("should serialize arrays", () => {
49
+ const arr: i32[] = [1, 2, 3];
50
+ expect(JSON.stringify(arr)).toBe("[1,2,3]");
51
+ });
52
+ });
53
+
54
+ describe("parse", () => {
55
+ it("should deserialize a simple class", () => {
56
+ const json = '{"name":"test","count":42}';
57
+ const data = JSON.parse<SimpleData>(json);
58
+
59
+ expect(data.name).toBe("test");
60
+ expect(data.count).toBe(42);
61
+ });
62
+
63
+ it("should deserialize a class with arrays", () => {
64
+ const json = '{"id":"abc123","items":["item1","item2"]}';
65
+ const data = JSON.parse<NestedData>(json);
66
+
67
+ expect(data.id).toBe("abc123");
68
+ expect(data.items.length).toBe(2);
69
+ expect(data.items[0]).toBe("item1");
70
+ expect(data.items[1]).toBe("item2");
71
+ });
72
+
73
+ it("should round-trip data correctly", () => {
74
+ const original = new SimpleData();
75
+ original.name = "round-trip";
76
+ original.count = 123;
77
+
78
+ const json = JSON.stringify(original);
79
+ const restored = JSON.parse<SimpleData>(json);
80
+
81
+ expect(restored.name).toBe(original.name);
82
+ expect(restored.count).toBe(original.count);
83
+ });
84
+ });
85
+ });
@@ -719,6 +719,8 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
719
719
  clean: cleanOutput,
720
720
  snapshotEnabled,
721
721
  showCoverage,
722
+ showCoverageAll: Boolean(flags.showCoverageAll),
723
+ verbose: Boolean(flags.verbose),
722
724
  buildTime,
723
725
  snapshotSummary,
724
726
  coverageSummary,
@@ -1204,6 +1206,10 @@ function normalizeCoverage(value) {
1204
1206
  column: Number(p.column ?? 0),
1205
1207
  type: String(p.type ?? ""),
1206
1208
  executed: Boolean(p.executed),
1209
+ parentHash: String(p.parentHash ?? ""),
1210
+ scopeKind: String(p.scopeKind ?? ""),
1211
+ scopeName: String(p.scopeName ?? ""),
1212
+ depth: Number(p.depth ?? 0),
1207
1213
  };
1208
1214
  })
1209
1215
  .filter((point) => point.file.length > 0);
@@ -1475,10 +1481,14 @@ function matchesCoverageTextPattern(value, pattern) {
1475
1481
  return globPatternToRegExp(normalized).test(value);
1476
1482
  }
1477
1483
  function compareCoveragePoints(a, b) {
1484
+ const depthA = a.depth ?? 0;
1485
+ const depthB = b.depth ?? 0;
1478
1486
  if (a.line !== b.line)
1479
1487
  return a.line - b.line;
1480
1488
  if (a.column !== b.column)
1481
1489
  return a.column - b.column;
1490
+ if (depthA !== depthB)
1491
+ return depthA - depthB;
1482
1492
  if (a.type !== b.type)
1483
1493
  return a.type.localeCompare(b.type);
1484
1494
  return a.hash.localeCompare(b.hash);
@@ -4,12 +4,14 @@ export async function executeRunCommand(rawArgs, flags, configPath, selectedMode
4
4
  const suiteSelectors = deps.resolveSuiteSelectors(rawArgs, "run");
5
5
  const listFlags = deps.resolveListFlags(rawArgs, "run");
6
6
  const featureToggles = deps.resolveFeatureToggles(rawArgs, "run");
7
+ const showCoverageMode = deps.resolveShowCoverageMode(rawArgs, "run");
7
8
  const runFlags = {
8
9
  snapshot: !flags.includes("--no-snapshot"),
9
10
  createSnapshots: flags.includes("--create-snapshots"),
10
11
  overwriteSnapshots: flags.includes("--overwrite-snapshots"),
11
12
  clean: flags.includes("--clean"),
12
- showCoverage: flags.includes("--show-coverage"),
13
+ showCoverage: showCoverageMode != undefined,
14
+ showCoverageAll: showCoverageMode == "all",
13
15
  verbose: flags.includes("--verbose"),
14
16
  ...deps.resolveParallelJobs(rawArgs, "run"),
15
17
  coverage: featureToggles.coverage,
@@ -8,12 +8,14 @@ export async function executeTestCommand(rawArgs, flags, configPath, selectedMod
8
8
  tryAs: featureToggles.tryAs,
9
9
  coverage: featureToggles.coverage,
10
10
  };
11
+ const showCoverageMode = deps.resolveShowCoverageMode(rawArgs, "test");
11
12
  const runFlags = {
12
13
  snapshot: !flags.includes("--no-snapshot"),
13
14
  createSnapshots: flags.includes("--create-snapshots"),
14
15
  overwriteSnapshots: flags.includes("--overwrite-snapshots"),
15
16
  clean: flags.includes("--clean"),
16
- showCoverage: flags.includes("--show-coverage"),
17
+ showCoverage: showCoverageMode != undefined,
18
+ showCoverageAll: showCoverageMode == "all",
17
19
  verbose: flags.includes("--verbose"),
18
20
  ...deps.resolveParallelJobs(rawArgs, "test"),
19
21
  coverage: featureToggles.coverage,
@@ -13,7 +13,61 @@ export function describeCoveragePoint(file, line, column, fallbackType) {
13
13
  highlightEnd: 0,
14
14
  };
15
15
  }
16
- const declaration = detectCoverageDeclaration(context.visible);
16
+ const parameter = detectCoverageParameter(context.visible, context.focus, fallbackType);
17
+ if (parameter) {
18
+ return {
19
+ displayType: parameter.type,
20
+ subjectName: parameter.name,
21
+ visible: context.visible,
22
+ focus: context.focus,
23
+ highlightStart: parameter.start,
24
+ highlightEnd: parameter.end,
25
+ };
26
+ }
27
+ const ternary = detectCoverageTernary(context.visible, context.focus, fallbackType);
28
+ if (ternary) {
29
+ return {
30
+ displayType: ternary.type,
31
+ subjectName: null,
32
+ visible: context.visible,
33
+ focus: context.focus,
34
+ highlightStart: ternary.start,
35
+ highlightEnd: ternary.end,
36
+ };
37
+ }
38
+ const ifBranch = detectCoverageIfBranch(context.visible, fallbackType);
39
+ if (ifBranch) {
40
+ return {
41
+ displayType: ifBranch.type,
42
+ subjectName: null,
43
+ visible: context.visible,
44
+ focus: context.focus,
45
+ highlightStart: ifBranch.start,
46
+ highlightEnd: ifBranch.end,
47
+ };
48
+ }
49
+ const assignment = detectCoverageAssignment(context.visible, fallbackType);
50
+ if (assignment) {
51
+ return {
52
+ displayType: assignment.type,
53
+ subjectName: null,
54
+ visible: context.visible,
55
+ focus: context.focus,
56
+ highlightStart: assignment.start,
57
+ highlightEnd: assignment.end,
58
+ };
59
+ }
60
+ const declarationAllowed = fallbackType == "Expression" ||
61
+ fallbackType == "Block" ||
62
+ fallbackType == "Function" ||
63
+ fallbackType == "Method" ||
64
+ fallbackType == "Constructor" ||
65
+ fallbackType == "Variable" ||
66
+ fallbackType == "Property" ||
67
+ fallbackType == "Call";
68
+ const declaration = declarationAllowed
69
+ ? detectCoverageDeclaration(context.visible)
70
+ : null;
17
71
  if (declaration) {
18
72
  const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(context.visible, context.focus);
19
73
  return {
@@ -25,7 +79,10 @@ export function describeCoveragePoint(file, line, column, fallbackType) {
25
79
  highlightEnd,
26
80
  };
27
81
  }
28
- const call = detectCoverageCall(context.visible, context.focus);
82
+ const callAllowed = fallbackType == "Expression" || fallbackType == "Call";
83
+ const call = callAllowed
84
+ ? detectCoverageCall(context.visible, context.focus)
85
+ : null;
29
86
  if (call) {
30
87
  return {
31
88
  displayType: "Call",
@@ -130,6 +187,147 @@ function detectCoverageDeclaration(visible) {
130
187
  }
131
188
  return null;
132
189
  }
190
+ function detectCoverageParameter(visible, focus, fallbackType) {
191
+ const inlineParameter = detectCoverageInlineParameter(visible, focus, fallbackType);
192
+ if (inlineParameter) {
193
+ return inlineParameter;
194
+ }
195
+ const openParen = visible.indexOf("(");
196
+ const closeParen = visible.lastIndexOf(")");
197
+ if (openParen == -1 || closeParen == -1 || closeParen <= openParen) {
198
+ return null;
199
+ }
200
+ if (focus <= openParen || focus >= closeParen) {
201
+ return null;
202
+ }
203
+ const params = visible.slice(openParen + 1, closeParen);
204
+ const matches = [
205
+ ...params.matchAll(/([A-Za-z_]\w*)\s*:\s*[^,)=]+(?:=\s*[^,)]*)?/g),
206
+ ];
207
+ if (!matches.length)
208
+ return null;
209
+ for (const match of matches) {
210
+ const localStart = match.index ?? -1;
211
+ if (localStart == -1)
212
+ continue;
213
+ const localEnd = localStart + match[0].length;
214
+ const absoluteStart = openParen + 1 + localStart;
215
+ const absoluteEnd = openParen + 1 + localEnd;
216
+ if (focus < absoluteStart || focus > absoluteEnd)
217
+ continue;
218
+ const name = match[1] ?? null;
219
+ if (!name)
220
+ return null;
221
+ const nameOffset = match[0].indexOf(name);
222
+ const equalsOffset = match[0].indexOf("=");
223
+ if (fallbackType == "DefaultValue" && equalsOffset != -1) {
224
+ const valueStart = absoluteStart + equalsOffset + 1;
225
+ const valueVisibleStart = skipCoverageWhitespace(visible, valueStart);
226
+ const [start, end] = resolveCoverageHighlightSpan(visible, Math.max(valueVisibleStart, focus));
227
+ return {
228
+ type: "DefaultValue",
229
+ name,
230
+ start,
231
+ end,
232
+ };
233
+ }
234
+ return {
235
+ type: fallbackType == "Parameter" ? "Parameter" : "Property",
236
+ name,
237
+ start: absoluteStart + nameOffset,
238
+ end: absoluteStart + nameOffset + name.length,
239
+ };
240
+ }
241
+ return null;
242
+ }
243
+ function detectCoverageInlineParameter(visible, focus, fallbackType) {
244
+ const match = visible.match(/^([A-Za-z_]\w*)\s*:\s*[^=,]+(?:=\s*[^,]+)?[,]?$/);
245
+ if (!match)
246
+ return null;
247
+ const name = match[1] ?? null;
248
+ if (!name)
249
+ return null;
250
+ const nameStart = visible.indexOf(name);
251
+ const nameEnd = nameStart + name.length;
252
+ const equalsIndex = visible.indexOf("=");
253
+ if (fallbackType == "DefaultValue" && equalsIndex != -1) {
254
+ const valueStart = skipCoverageWhitespace(visible, equalsIndex + 1);
255
+ const [start, end] = resolveCoverageHighlightSpan(visible, Math.max(valueStart, focus));
256
+ return {
257
+ type: "DefaultValue",
258
+ name,
259
+ start,
260
+ end,
261
+ };
262
+ }
263
+ return {
264
+ type: fallbackType == "Parameter" ? "Parameter" : "Property",
265
+ name,
266
+ start: nameStart,
267
+ end: nameEnd,
268
+ };
269
+ }
270
+ function detectCoverageTernary(visible, focus, fallbackType) {
271
+ if (fallbackType != "Ternary" && fallbackType != "LogicalBranch") {
272
+ return null;
273
+ }
274
+ const q = visible.indexOf("?");
275
+ if (q == -1)
276
+ return null;
277
+ if (fallbackType == "LogicalBranch") {
278
+ const [start, end] = resolveCoverageHighlightSpan(visible, focus);
279
+ return { type: "LogicalBranch", start, end };
280
+ }
281
+ const colon = visible.indexOf(":", q + 1);
282
+ if (colon == -1) {
283
+ const [start, end] = resolveCoverageHighlightSpan(visible, focus);
284
+ return { type: "Ternary", start, end };
285
+ }
286
+ const branchStart = focus <= colon ? q + 1 : colon + 1;
287
+ const normalizedStart = skipCoverageWhitespace(visible, branchStart);
288
+ const [start, end] = resolveCoverageHighlightSpan(visible, Math.max(normalizedStart, focus));
289
+ return { type: "Ternary", start, end };
290
+ }
291
+ function detectCoverageIfBranch(visible, fallbackType) {
292
+ if (fallbackType != "IfBranch")
293
+ return null;
294
+ const match = visible.match(/^if\s*\(([^)]*)\)/);
295
+ if (!match)
296
+ return null;
297
+ const full = match[0];
298
+ const condition = match[1] ?? "";
299
+ const openParen = full.indexOf("(");
300
+ const conditionPadding = condition.length
301
+ ? condition.length - condition.trimStart().length
302
+ : 0;
303
+ const conditionStart = openParen == -1 ? -1 : openParen + 1 + conditionPadding;
304
+ if (conditionStart == -1 || !condition.length) {
305
+ return { type: "IfBranch", start: 0, end: full.length };
306
+ }
307
+ return {
308
+ type: "IfBranch",
309
+ start: conditionStart,
310
+ end: conditionStart + condition.length,
311
+ };
312
+ }
313
+ function detectCoverageAssignment(visible, fallbackType) {
314
+ if (fallbackType != "Assignment")
315
+ return null;
316
+ const match = visible.match(/([A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[[^\]]+\])?)\s*(=|\+=|-=|\*=|\*\*=|\/=|%=|<<=|>>=|>>>=|&=|\|=|\^=)/);
317
+ if (!match)
318
+ return null;
319
+ const full = match[0];
320
+ const lhs = match[1] ?? "";
321
+ const operator = match[2] ?? "=";
322
+ const fullStart = visible.indexOf(full);
323
+ const lhsStart = fullStart + full.indexOf(lhs);
324
+ const operatorStart = fullStart + full.lastIndexOf(operator);
325
+ return {
326
+ type: "Assignment",
327
+ start: lhsStart,
328
+ end: operatorStart + operator.length,
329
+ };
330
+ }
133
331
  function detectCoverageCall(visible, focus) {
134
332
  const matches = [...visible.matchAll(/\b([A-Za-z_]\w*)(?:<[^>()]+>)?\s*\(/g)];
135
333
  if (!matches.length)
@@ -171,3 +369,10 @@ function detectCoverageCall(visible, focus) {
171
369
  function isCoverageBoundary(ch) {
172
370
  return /[\s()[\]{}.,;:+\-*/%&|^!?=<>]/.test(ch);
173
371
  }
372
+ function skipCoverageWhitespace(visible, index) {
373
+ let current = Math.max(0, Math.min(visible.length - 1, index));
374
+ while (current < visible.length - 1 && /\s/.test(visible.charAt(current))) {
375
+ current++;
376
+ }
377
+ return current;
378
+ }
package/bin/index.js CHANGED
@@ -78,6 +78,7 @@ else if (COMMANDS.includes(args[0])) {
78
78
  resolveParallelJobs,
79
79
  resolveBrowserOverride,
80
80
  resolveReporterOverride,
81
+ resolveShowCoverageMode,
81
82
  resolveExecutionModes,
82
83
  listExecutionPlan,
83
84
  runRuntimeModes,
@@ -96,6 +97,7 @@ else if (COMMANDS.includes(args[0])) {
96
97
  resolveParallelJobs,
97
98
  resolveBrowserOverride,
98
99
  resolveReporterOverride,
100
+ resolveShowCoverageMode,
99
101
  resolveFuzzOverrides,
100
102
  resolveExecutionModes,
101
103
  listExecutionPlan,
@@ -277,7 +279,7 @@ function printCommandHelp(command) {
277
279
  process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
278
280
  process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
279
281
  process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
280
- process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
282
+ process.stdout.write(" --show-coverage[=all] Print uncovered coverage point details; use =all to expand nested gaps\n");
281
283
  process.stdout.write(" --suite <name[,name...]> Filter results to matching suite names or suite slug paths\n");
282
284
  process.stdout.write(" --suites <name[,name...]> Alias for --suite\n");
283
285
  process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
@@ -305,7 +307,7 @@ function printCommandHelp(command) {
305
307
  process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
306
308
  process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
307
309
  process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
308
- process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
310
+ process.stdout.write(" --show-coverage[=all] Print uncovered coverage point details; use =all to expand nested gaps\n");
309
311
  process.stdout.write(" --suite <name[,name...]> Filter results to matching suite names or suite slug paths\n");
310
312
  process.stdout.write(" --suites <name[,name...]> Alias for --suite\n");
311
313
  process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
@@ -462,8 +464,15 @@ function resolveCommandArgs(rawArgs, command) {
462
464
  arg == "--build-jobs" ||
463
465
  arg == "--run-jobs" ||
464
466
  arg == "--browser" ||
467
+ arg == "--show-coverage" ||
465
468
  arg == "--fuzz-runs" ||
466
469
  arg == "--fuzz-seed") {
470
+ if (arg == "--show-coverage") {
471
+ const next = rawArgs[i + 1];
472
+ if (next == "all")
473
+ i++;
474
+ continue;
475
+ }
467
476
  i++;
468
477
  continue;
469
478
  }
@@ -476,6 +485,7 @@ function resolveCommandArgs(rawArgs, command) {
476
485
  arg.startsWith("--build-jobs=") ||
477
486
  arg.startsWith("--run-jobs=") ||
478
487
  arg.startsWith("--browser=") ||
488
+ arg.startsWith("--show-coverage=") ||
479
489
  arg.startsWith("--fuzz-runs=") ||
480
490
  arg.startsWith("--fuzz-seed=")) {
481
491
  continue;
@@ -762,6 +772,34 @@ function resolveReporterOverride(rawArgs, command) {
762
772
  }
763
773
  return undefined;
764
774
  }
775
+ function resolveShowCoverageMode(rawArgs, command) {
776
+ let seenCommand = false;
777
+ for (let i = 0; i < rawArgs.length; i++) {
778
+ const arg = rawArgs[i];
779
+ if (!seenCommand) {
780
+ if (arg == command)
781
+ seenCommand = true;
782
+ continue;
783
+ }
784
+ if (arg == "--show-coverage") {
785
+ const next = rawArgs[i + 1];
786
+ if (next == "all")
787
+ return "all";
788
+ return "collapsed";
789
+ }
790
+ if (arg.startsWith("--show-coverage=")) {
791
+ const value = arg.slice("--show-coverage=".length).trim();
792
+ if (!value.length) {
793
+ throw new Error("--show-coverage requires a value when using =");
794
+ }
795
+ if (value != "all") {
796
+ throw new Error(`--show-coverage only supports "all" when given a value`);
797
+ }
798
+ return "all";
799
+ }
800
+ }
801
+ return undefined;
802
+ }
765
803
  function resolveJobs(rawArgs, command) {
766
804
  let seenCommand = false;
767
805
  let parallel = false;
@@ -1140,6 +1178,8 @@ async function runTestSequential(runFlags, configPath, selectors, suiteSelectors
1140
1178
  clean: runFlags.clean,
1141
1179
  snapshotEnabled,
1142
1180
  showCoverage: runFlags.showCoverage,
1181
+ showCoverageAll: runFlags.showCoverageAll,
1182
+ verbose: runFlags.verbose,
1143
1183
  buildTime: getMergedIntervalDuration(buildIntervals),
1144
1184
  snapshotSummary: summary.snapshotSummary,
1145
1185
  coverageSummary: summary.coverageSummary,
@@ -1372,6 +1412,8 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors,
1372
1412
  clean: runFlags.clean,
1373
1413
  snapshotEnabled,
1374
1414
  showCoverage: runFlags.showCoverage,
1415
+ showCoverageAll: runFlags.showCoverageAll,
1416
+ verbose: runFlags.verbose,
1375
1417
  buildTime: 0,
1376
1418
  snapshotSummary: summary.snapshotSummary,
1377
1419
  coverageSummary: summary.coverageSummary,
@@ -1435,6 +1477,8 @@ async function runTestModes(runFlags, configPath, selectors, suiteSelectors, fuz
1435
1477
  clean: runFlags.clean,
1436
1478
  snapshotEnabled: effectiveRunFlags.snapshot !== false,
1437
1479
  showCoverage: effectiveRunFlags.showCoverage,
1480
+ showCoverageAll: effectiveRunFlags.showCoverageAll,
1481
+ verbose: effectiveRunFlags.verbose,
1438
1482
  buildTime: modeResult.summary.buildTime +
1439
1483
  getMergedIntervalDuration(collectFuzzBuildIntervals(fuzzResults)),
1440
1484
  snapshotSummary: modeResult.summary.snapshotSummary,
@@ -1572,6 +1616,8 @@ async function runTestMatrix(runFlags, configPath, selectors, suiteSelectors, fu
1572
1616
  clean: runFlags.clean,
1573
1617
  snapshotEnabled,
1574
1618
  showCoverage: runFlags.showCoverage,
1619
+ showCoverageAll: runFlags.showCoverageAll,
1620
+ verbose: runFlags.verbose,
1575
1621
  buildTime: getMergedIntervalDuration(buildIntervals),
1576
1622
  snapshotSummary: summary.snapshotSummary,
1577
1623
  coverageSummary: summary.coverageSummary,
@@ -1664,6 +1710,8 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, suiteSe
1664
1710
  clean: runFlags.clean,
1665
1711
  snapshotEnabled,
1666
1712
  showCoverage: runFlags.showCoverage,
1713
+ showCoverageAll: runFlags.showCoverageAll,
1714
+ verbose: runFlags.verbose,
1667
1715
  buildTime: 0,
1668
1716
  snapshotSummary: summary.snapshotSummary,
1669
1717
  coverageSummary: summary.coverageSummary,
@@ -1778,6 +1826,8 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, suiteSe
1778
1826
  clean: runFlags.clean,
1779
1827
  snapshotEnabled,
1780
1828
  showCoverage: runFlags.showCoverage,
1829
+ showCoverageAll: runFlags.showCoverageAll,
1830
+ verbose: runFlags.verbose,
1781
1831
  buildTime: getMergedIntervalDuration(buildIntervals),
1782
1832
  snapshotSummary: summary.snapshotSummary,
1783
1833
  coverageSummary: summary.coverageSummary,
@@ -1880,6 +1930,8 @@ async function runTestSingleParallel(runFlags, configPath, selectors, suiteSelec
1880
1930
  clean: runFlags.clean,
1881
1931
  snapshotEnabled,
1882
1932
  showCoverage: runFlags.showCoverage,
1933
+ showCoverageAll: runFlags.showCoverageAll,
1934
+ verbose: runFlags.verbose,
1883
1935
  buildTime: getMergedIntervalDuration(buildIntervals),
1884
1936
  snapshotSummary: summary.snapshotSummary,
1885
1937
  coverageSummary: summary.coverageSummary,
@@ -2013,6 +2065,8 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, suiteSelec
2013
2065
  clean: runFlags.clean,
2014
2066
  snapshotEnabled,
2015
2067
  showCoverage: runFlags.showCoverage,
2068
+ showCoverageAll: runFlags.showCoverageAll,
2069
+ verbose: runFlags.verbose,
2016
2070
  buildTime: getMergedIntervalDuration(buildIntervals),
2017
2071
  snapshotSummary: summary.snapshotSummary,
2018
2072
  coverageSummary: summary.coverageSummary,
@@ -3,7 +3,7 @@ import { diff } from "typer-diff";
3
3
  import { readFileSync } from "fs";
4
4
  import * as path from "path";
5
5
  import { formatSpecDisplayPath, formatTime } from "../util.js";
6
- import { describeCoveragePoint, readCoverageSourceLine, resolveCoverageHighlightSpan, } from "../coverage-points.js";
6
+ import { describeCoveragePoint } from "../coverage-points.js";
7
7
  export const createReporter = (context) => {
8
8
  return new DefaultReporter(context);
9
9
  };
@@ -288,7 +288,7 @@ class DefaultReporter {
288
288
  if (event.coverageSummary.enabled) {
289
289
  renderCoverageSummary(event.coverageSummary, event.showCoverage);
290
290
  if (event.showCoverage && event.coverageSummary.uncovered) {
291
- renderCoveragePoints(event.coverageSummary.files);
291
+ renderCoveragePoints(event.coverageSummary.files, Boolean(event.verbose || event.showCoverageAll));
292
292
  }
293
293
  }
294
294
  renderTotals(event.stats, event);
@@ -831,7 +831,7 @@ function renderCoverageSummary(summary, showCoverage) {
831
831
  console.log(chalk.dim(` ... ${ranked.length - 8} more files`));
832
832
  }
833
833
  }
834
- function renderCoveragePoints(files) {
834
+ function renderCoveragePoints(files, expandNested) {
835
835
  console.log("");
836
836
  console.log(chalk.bold("Coverage Gaps"));
837
837
  const sortedFiles = [...files].sort((a, b) => a.file.localeCompare(b.file));
@@ -842,28 +842,124 @@ function renderCoveragePoints(files) {
842
842
  displayType: describeCoveragePoint(point.file, point.line, point.column, point.type).displayType,
843
843
  })));
844
844
  const layout = createCoverageGapLayout(missingPoints);
845
+ let renderedFileCount = 0;
846
+ let collapsedNestedPoints = 0;
845
847
  for (const file of sortedFiles) {
846
- const points = [...file.points].sort((a, b) => {
847
- if (a.line != b.line)
848
- return a.line - b.line;
849
- if (a.column != b.column)
850
- return a.column - b.column;
851
- return a.type.localeCompare(b.type);
852
- });
848
+ const points = [...file.points].sort(compareCoverageGapPoints);
853
849
  const missing = points.filter((point) => !point.executed);
854
850
  if (!missing.length)
855
851
  continue;
852
+ if (renderedFileCount > 0) {
853
+ console.log("");
854
+ }
856
855
  console.log(` ${chalk.bold(toRelativeResultPath(file.file))} ${chalk.dim(`(${missing.length} uncovered)`)}`);
856
+ const pointsByHash = new Map(points.map((point) => [point.hash, point]));
857
+ const childrenByParent = new Map();
858
+ const roots = [];
857
859
  for (const point of points) {
858
- if (point.executed)
859
- continue;
860
- const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
861
- const snippet = formatCoverageSnippet(point.file, point.line, point.column);
862
- const typeLabel = describeCoveragePoint(point.file, point.line, point.column, point.type).displayType.padEnd(layout.typeWidth + 4);
863
- const locationLabel = location.padEnd(layout.locationWidth + 4);
864
- console.log(` ${chalk.red("x")} ${chalk.dim(typeLabel)}${chalk.dim(locationLabel)}${snippet}`);
860
+ const parentHash = point.parentHash ?? "";
861
+ if (parentHash.length && pointsByHash.has(parentHash)) {
862
+ const children = childrenByParent.get(parentHash) ?? [];
863
+ children.push(point);
864
+ childrenByParent.set(parentHash, children);
865
+ }
866
+ else {
867
+ roots.push(point);
868
+ }
869
+ }
870
+ const visibleRoots = roots.filter((point) => shouldRenderCoveragePoint(point, childrenByParent));
871
+ for (let i = 0; i < visibleRoots.length; i++) {
872
+ collapsedNestedPoints += renderCoveragePointTree(visibleRoots[i], childrenByParent, layout, [], i == visibleRoots.length - 1, expandNested);
865
873
  }
874
+ renderedFileCount++;
866
875
  }
876
+ if (!expandNested && collapsedNestedPoints > 0) {
877
+ console.log("");
878
+ console.log(chalk.dim(" Run with --show-coverage=all or --verbose to expand nested coverage gaps."));
879
+ }
880
+ }
881
+ function renderCoveragePointTree(point, childrenByParent, layout, ancestorHasNext, isLast, expandNested) {
882
+ const visibleChildren = [...(childrenByParent.get(point.hash) ?? [])]
883
+ .filter((child) => shouldRenderCoveragePoint(child, childrenByParent))
884
+ .sort(compareCoverageGapPoints);
885
+ const nestedUncoveredCount = countNestedUncoveredPoints(visibleChildren, childrenByParent);
886
+ if (!point.executed) {
887
+ renderCoverageGapLine(point, layout, ancestorHasNext, isLast);
888
+ if (nestedUncoveredCount > 0) {
889
+ if (expandNested) {
890
+ let rendered = 0;
891
+ for (let i = 0; i < visibleChildren.length; i++) {
892
+ rendered += renderCoveragePointTree(visibleChildren[i], childrenByParent, layout, [...ancestorHasNext, !isLast], i == visibleChildren.length - 1, expandNested);
893
+ }
894
+ return 1 + rendered;
895
+ }
896
+ const treePrefix = buildCoverageTreePrefix([...ancestorHasNext, !isLast], true);
897
+ console.log(` ${treePrefix}${chalk.dim(`(+${nestedUncoveredCount} nested uncovered point${nestedUncoveredCount == 1 ? "" : "s"})`)}`);
898
+ return nestedUncoveredCount;
899
+ }
900
+ return 0;
901
+ }
902
+ if (nestedUncoveredCount <= 0)
903
+ return 0;
904
+ renderCoverageScopeHeader(point, layout, ancestorHasNext, isLast);
905
+ let rendered = 0;
906
+ for (let i = 0; i < visibleChildren.length; i++) {
907
+ rendered += renderCoveragePointTree(visibleChildren[i], childrenByParent, layout, [...ancestorHasNext, !isLast], i == visibleChildren.length - 1, expandNested);
908
+ }
909
+ return rendered;
910
+ }
911
+ function shouldRenderCoveragePoint(point, childrenByParent) {
912
+ if (!point.executed)
913
+ return true;
914
+ return (countNestedUncoveredPoints(childrenByParent.get(point.hash) ?? [], childrenByParent) > 0);
915
+ }
916
+ function countNestedUncoveredPoints(points, childrenByParent) {
917
+ let count = 0;
918
+ for (const point of points) {
919
+ if (!point.executed)
920
+ count++;
921
+ count += countNestedUncoveredPoints(childrenByParent.get(point.hash) ?? [], childrenByParent);
922
+ }
923
+ return count;
924
+ }
925
+ function renderCoverageGapLine(point, layout, ancestorHasNext, isLast) {
926
+ const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
927
+ const snippet = formatCoverageSnippet(point.file, point.line, point.column, point.type, ancestorHasNext.length);
928
+ const typeLabel = describeCoveragePoint(point.file, point.line, point.column, point.type).displayType.padEnd(layout.typeWidth + 6);
929
+ const locationLabel = location.padEnd(layout.locationWidth + 6);
930
+ const treePrefix = buildCoverageTreePrefix(ancestorHasNext, isLast);
931
+ const meta = `${typeLabel}${locationLabel}`;
932
+ console.log(` ${treePrefix}${chalk.dim(meta)} ${snippet}`);
933
+ }
934
+ function renderCoverageScopeHeader(point, layout, ancestorHasNext, isLast) {
935
+ const descriptor = describeCoveragePoint(point.file, point.line, point.column, point.type);
936
+ const label = point.scopeKind || descriptor.displayType;
937
+ const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
938
+ const locationLabel = location.padEnd(layout.locationWidth + 6);
939
+ const typeLabel = label.padEnd(layout.typeWidth + 6);
940
+ const snippet = formatCoverageSnippet(point.file, point.line, point.column, point.type, ancestorHasNext.length);
941
+ const treePrefix = buildCoverageTreePrefix(ancestorHasNext, isLast);
942
+ const meta = `${typeLabel}${locationLabel}`;
943
+ console.log(` ${treePrefix}${chalk.dim(meta)} ${chalk.dim(snippet)}`);
944
+ }
945
+ function buildCoverageTreePrefix(ancestorHasNext, isLast) {
946
+ let out = "";
947
+ for (const hasNext of ancestorHasNext) {
948
+ out += hasNext ? "│ " : " ";
949
+ }
950
+ out += isLast ? "└─" : "├─";
951
+ return chalk.dim(out);
952
+ }
953
+ function compareCoverageGapPoints(a, b) {
954
+ if (a.line != b.line)
955
+ return a.line - b.line;
956
+ if (a.column != b.column)
957
+ return a.column - b.column;
958
+ if ((a.depth ?? 0) != (b.depth ?? 0))
959
+ return (a.depth ?? 0) - (b.depth ?? 0);
960
+ if (a.type != b.type)
961
+ return a.type.localeCompare(b.type);
962
+ return a.hash.localeCompare(b.hash);
867
963
  }
868
964
  function renderCoverageBar(percent) {
869
965
  const slots = 12;
@@ -877,32 +973,25 @@ function createCoverageGapLayout(points) {
877
973
  .length), 1),
878
974
  };
879
975
  }
880
- function formatCoverageSnippet(file, line, column) {
881
- const sourceLine = readCoverageSourceLine(file, line);
882
- if (!sourceLine)
883
- return "";
884
- const expanded = sourceLine.replace(/\t/g, " ");
885
- const firstNonWhitespace = expanded.search(/\S/);
886
- if (firstNonWhitespace == -1)
887
- return "";
888
- const visible = expanded.slice(firstNonWhitespace).trimEnd();
976
+ function formatCoverageSnippet(file, line, column, fallbackType, _depth) {
977
+ const descriptor = describeCoveragePoint(file, line, column, fallbackType);
978
+ const visible = descriptor.visible;
889
979
  if (!visible.length)
890
980
  return "";
891
981
  const maxWidth = 72;
892
- const focus = Math.max(0, Math.min(visible.length - 1, Math.max(0, column - 1 - firstNonWhitespace)));
982
+ const focus = Math.max(0, Math.min(visible.length - 1, descriptor.focus));
893
983
  if (visible.length <= maxWidth) {
894
- return styleCoverageSnippetWindow(visible, 0, visible.length, focus);
984
+ return styleCoverageSnippetWindow(visible, 0, visible.length, focus, descriptor.highlightStart, descriptor.highlightEnd);
895
985
  }
896
986
  const start = Math.max(0, Math.min(visible.length - maxWidth, focus - Math.floor(maxWidth / 2)));
897
987
  const end = Math.min(visible.length, start + maxWidth);
898
- return styleCoverageSnippetWindow(visible, start, end, focus);
988
+ return styleCoverageSnippetWindow(visible, start, end, focus, descriptor.highlightStart, descriptor.highlightEnd);
899
989
  }
900
- function styleCoverageSnippetWindow(visible, start, end, focus) {
990
+ function styleCoverageSnippetWindow(visible, start, end, focus, highlightStart, highlightEnd) {
901
991
  const prefix = start > 0 ? "..." : "";
902
992
  const suffix = end < visible.length ? "..." : "";
903
993
  const slice = visible.slice(start, end);
904
994
  const localFocus = Math.max(0, Math.min(slice.length - 1, focus - start));
905
- const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(visible, focus);
906
995
  const localStart = Math.max(0, Math.min(slice.length, highlightStart - start));
907
996
  const localEnd = Math.max(localStart + 1, Math.min(slice.length, highlightEnd - start));
908
997
  if (!slice.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,6 +20,7 @@
20
20
  "as-test": "./",
21
21
  "assemblyscript": "^0.28.17",
22
22
  "assemblyscript-prettier": "^3.0.4",
23
+ "json-as": "^1.3.5",
23
24
  "prettier": "3.8.3",
24
25
  "try-as": "^1.0.1",
25
26
  "typescript": "^6.0.3",
@@ -2,12 +2,6 @@ import { BlockStatement, ExpressionStatement, Node, } from "assemblyscript/dist/
2
2
  import { RangeTransform } from "./range.js";
3
3
  import { isStdlib, SimpleParser } from "./util.js";
4
4
  import { Visitor } from "./visitor.js";
5
- var CoverType;
6
- (function (CoverType) {
7
- CoverType[CoverType["Function"] = 0] = "Function";
8
- CoverType[CoverType["Expression"] = 1] = "Expression";
9
- CoverType[CoverType["Block"] = 2] = "Block";
10
- })(CoverType || (CoverType = {}));
11
5
  const COVERAGE_IGNORED_CALLS = new Set([
12
6
  "beforeAll",
13
7
  "afterAll",
@@ -50,13 +44,106 @@ class CoverPoint {
50
44
  hash = "";
51
45
  line = 0;
52
46
  column = 0;
53
- type;
47
+ type = "Expression";
54
48
  executed = false;
49
+ parentHash = "";
50
+ scopeKind = "";
51
+ scopeName = "";
52
+ depth = 0;
55
53
  }
56
54
  export class CoverageTransform extends Visitor {
57
55
  mustImport = false;
58
56
  points = new Map();
59
57
  globalStatements = [];
58
+ scopeStack = [];
59
+ getCurrentScope() {
60
+ return this.scopeStack.length
61
+ ? this.scopeStack[this.scopeStack.length - 1]
62
+ : null;
63
+ }
64
+ withScope(point, callback) {
65
+ this.scopeStack.push(point);
66
+ try {
67
+ callback();
68
+ }
69
+ finally {
70
+ this.scopeStack.pop();
71
+ }
72
+ }
73
+ assignParentMetadata(point) {
74
+ const currentScope = this.getCurrentScope();
75
+ if (!currentScope)
76
+ return;
77
+ point.parentHash = currentScope.hash;
78
+ point.scopeKind = currentScope.scopeKind;
79
+ point.scopeName = currentScope.scopeName;
80
+ point.depth = currentScope.depth + 1;
81
+ }
82
+ assignScopeMetadata(point, scopeKind, scopeName = null) {
83
+ point.scopeKind = scopeKind;
84
+ point.scopeName = scopeName ?? "";
85
+ }
86
+ createFunctionPoint(path, node, scopeKind, scopeName = null) {
87
+ const funcLc = getLineCol(node);
88
+ const point = new CoverPoint();
89
+ point.line = funcLc?.line;
90
+ point.column = funcLc?.column;
91
+ point.file = path;
92
+ point.type = scopeKind;
93
+ this.assignParentMetadata(point);
94
+ this.assignScopeMetadata(point, scopeKind, scopeName);
95
+ point.hash = hash(point);
96
+ return point;
97
+ }
98
+ createPoint(path, node, type) {
99
+ const lc = getLineCol(node);
100
+ const point = new CoverPoint();
101
+ point.line = lc?.line;
102
+ point.column = lc?.column;
103
+ point.file = path;
104
+ point.type = type;
105
+ this.assignParentMetadata(point);
106
+ point.hash = hash(point);
107
+ return point;
108
+ }
109
+ instrumentStatementBody(path, node, type) {
110
+ const point = this.createPoint(path, node, type);
111
+ const replacer = new RangeTransform(node);
112
+ const registerStmt = createRegisterStatement(point);
113
+ replacer.visit(registerStmt);
114
+ const coverStmt = createCoverStatement(point.hash, node);
115
+ replacer.visit(coverStmt);
116
+ this.globalStatements.push(registerStmt);
117
+ if (node.kind == 31) {
118
+ const block = node;
119
+ block.statements.unshift(coverStmt);
120
+ return block;
121
+ }
122
+ const coverBlock = Node.createBlockStatement([coverStmt], node.range);
123
+ replacer.visit(coverBlock);
124
+ coverBlock.statements.push(node);
125
+ return coverBlock;
126
+ }
127
+ isAssignmentOperator(operator) {
128
+ switch (operator) {
129
+ case 101:
130
+ case 102:
131
+ case 103:
132
+ case 104:
133
+ case 105:
134
+ case 106:
135
+ case 107:
136
+ case 108:
137
+ case 109:
138
+ case 110:
139
+ case 111:
140
+ case 112:
141
+ case 113:
142
+ return true;
143
+ default:
144
+ return false;
145
+ }
146
+ }
60
147
  visitCallExpression(node) {
61
148
  const callName = getCallName(node);
62
149
  if (callName && COVERAGE_IGNORED_CALLS.has(callName)) {
@@ -89,7 +176,8 @@ export class CoverageTransform extends Visitor {
89
176
  point.line = rightLc?.line;
90
177
  point.column = rightLc?.column;
91
178
  point.file = path;
92
- point.type = CoverType.Expression;
179
+ point.type = "LogicalBranch";
180
+ this.assignParentMetadata(point);
93
181
  point.hash = hash(point);
94
182
  const replacer = new RangeTransform(node);
95
183
  const registerStmt = createRegisterStatement(point);
@@ -100,10 +188,25 @@ export class CoverageTransform extends Visitor {
100
188
  this.globalStatements.push(registerStmt);
101
189
  break;
102
190
  }
191
+ default: {
192
+ if (!this.isAssignmentOperator(node.operator))
193
+ break;
194
+ const right = node.right;
195
+ if (isBuiltinCallExpression(right))
196
+ break;
197
+ const point = this.createPoint(path, right, "Assignment");
198
+ const replacer = new RangeTransform(node);
199
+ const registerStmt = createRegisterStatement(point);
200
+ replacer.visit(registerStmt);
201
+ const coverExpression = createCoverExpression(point.hash, right, node);
202
+ replacer.visit(coverExpression);
203
+ node.right = coverExpression;
204
+ this.globalStatements.push(registerStmt);
205
+ break;
206
+ }
103
207
  }
104
208
  }
105
209
  visitMethodDeclaration(node) {
106
- super.visitMethodDeclaration(node);
107
210
  if (node.visited)
108
211
  return;
109
212
  node.visited = true;
@@ -112,13 +215,9 @@ export class CoverageTransform extends Visitor {
112
215
  return;
113
216
  node.body.visited = true;
114
217
  const path = node.range.source.normalizedPath;
115
- const funcLc = getLineCol(node);
116
- const point = new CoverPoint();
117
- point.line = funcLc?.line;
118
- point.column = funcLc?.column;
119
- point.file = path;
120
- point.type = CoverType.Function;
121
- point.hash = hash(point);
218
+ const methodName = getNodeName(node.name);
219
+ const scopeKind = methodName == "constructor" ? "Constructor" : "Method";
220
+ const point = this.createFunctionPoint(path, node, scopeKind, methodName);
122
221
  const replacer = new RangeTransform(node);
123
222
  const registerStmt = createRegisterStatement(point);
124
223
  replacer.visit(registerStmt);
@@ -127,6 +226,13 @@ export class CoverageTransform extends Visitor {
127
226
  const bodyBlock = node.body;
128
227
  bodyBlock.statements.unshift(coverStmt);
129
228
  this.globalStatements.push(registerStmt);
229
+ this.withScope(point, () => {
230
+ this.visit(node.name, node);
231
+ this.visit(node.typeParameters, node);
232
+ this.visit(node.signature, node);
233
+ this.visit(node.decorators, node);
234
+ this.visit(bodyBlock.statements, bodyBlock);
235
+ });
130
236
  }
131
237
  }
132
238
  visitParameter(node) {
@@ -146,7 +252,8 @@ export class CoverageTransform extends Visitor {
146
252
  point.line = paramLc?.line;
147
253
  point.column = paramLc?.column;
148
254
  point.file = path;
149
- point.type = CoverType.Expression;
255
+ point.type = "DefaultValue";
256
+ this.assignParentMetadata(point);
150
257
  point.hash = hash(point);
151
258
  const replacer = new RangeTransform(node);
152
259
  const registerStmt = createRegisterStatement(point);
@@ -158,7 +265,6 @@ export class CoverageTransform extends Visitor {
158
265
  }
159
266
  }
160
267
  visitFunctionDeclaration(node, isDefault) {
161
- super.visitFunctionDeclaration(node, isDefault);
162
268
  if (node.visited)
163
269
  return;
164
270
  node.visited = true;
@@ -167,13 +273,7 @@ export class CoverageTransform extends Visitor {
167
273
  return;
168
274
  node.body.visited = true;
169
275
  const path = node.range.source.normalizedPath;
170
- const funcLc = getLineCol(node);
171
- const point = new CoverPoint();
172
- point.line = funcLc?.line;
173
- point.column = funcLc?.column;
174
- point.file = path;
175
- point.type = CoverType.Function;
176
- point.hash = hash(point);
276
+ const point = this.createFunctionPoint(path, node, "Function", node.name?.text ?? null);
177
277
  const replacer = new RangeTransform(node);
178
278
  const registerStmt = createRegisterStatement(point);
179
279
  replacer.visit(registerStmt);
@@ -202,6 +302,18 @@ export class CoverageTransform extends Visitor {
202
302
  const bodyBlock = node.body;
203
303
  bodyBlock.statements.unshift(coverStmt);
204
304
  }
305
+ this.withScope(point, () => {
306
+ this.visit(node.name, node);
307
+ this.visit(node.decorators, node);
308
+ this.visit(node.typeParameters, node);
309
+ this.visit(node.signature, node);
310
+ if (node.body instanceof BlockStatement) {
311
+ this.visit(node.body.statements, node.body);
312
+ }
313
+ else {
314
+ this.visit(node.body, node);
315
+ }
316
+ });
205
317
  }
206
318
  }
207
319
  }
@@ -217,42 +329,14 @@ export class CoverageTransform extends Visitor {
217
329
  if (ifTrue &&
218
330
  ifTrue.kind !== 31 &&
219
331
  !isBuiltinStatement(ifTrue)) {
220
- const trueLc = getLineCol(ifTrue);
221
- const point = new CoverPoint();
222
- point.line = trueLc?.line;
223
- point.column = trueLc?.column;
224
- point.file = path;
225
- point.type = CoverType.Expression;
226
- point.hash = hash(point);
227
- const replacer = new RangeTransform(ifTrue);
228
- const registerStmt = createRegisterStatement(point);
229
- replacer.visit(registerStmt);
230
- const coverStmt = Node.createBlockStatement([createCoverStatement(point.hash, ifTrue)], ifTrue.range);
231
- replacer.visit(coverStmt);
232
- coverStmt.statements.push(ifTrue);
233
- node.ifTrue = coverStmt;
234
- this.globalStatements.push(registerStmt);
332
+ node.ifTrue = this.instrumentStatementBody(path, ifTrue, "IfBranch");
235
333
  visitIfTrue = true;
236
334
  visitIfFalse = !!ifFalse;
237
335
  }
238
336
  if (ifFalse &&
239
337
  ifFalse.kind !== 31 &&
240
338
  !isBuiltinStatement(ifFalse)) {
241
- const falseLc = getLineCol(ifFalse);
242
- const point = new CoverPoint();
243
- point.line = falseLc?.line;
244
- point.column = falseLc?.column;
245
- point.file = path;
246
- point.type = CoverType.Expression;
247
- point.hash = hash(point);
248
- const replacer = new RangeTransform(ifFalse);
249
- const registerStmt = createRegisterStatement(point);
250
- replacer.visit(registerStmt);
251
- const coverStmt = Node.createBlockStatement([createCoverStatement(point.hash, ifFalse)], ifFalse.range);
252
- replacer.visit(coverStmt);
253
- coverStmt.statements.push(ifFalse);
254
- node.ifFalse = coverStmt;
255
- this.globalStatements.push(registerStmt);
339
+ node.ifFalse = this.instrumentStatementBody(path, ifFalse, "IfBranch");
256
340
  visitIfTrue = true;
257
341
  visitIfFalse = true;
258
342
  }
@@ -289,7 +373,8 @@ export class CoverageTransform extends Visitor {
289
373
  point.line = trueLc?.line;
290
374
  point.column = trueLc?.column;
291
375
  point.file = path;
292
- point.type = CoverType.Expression;
376
+ point.type = "Ternary";
377
+ this.assignParentMetadata(point);
293
378
  point.hash = hash(point);
294
379
  const replacer = new RangeTransform(trueExpression);
295
380
  const registerStmt = createRegisterStatement(point);
@@ -307,7 +392,8 @@ export class CoverageTransform extends Visitor {
307
392
  point.line = falseLc?.line;
308
393
  point.column = falseLc?.column;
309
394
  point.file = path;
310
- point.type = CoverType.Expression;
395
+ point.type = "Ternary";
396
+ this.assignParentMetadata(point);
311
397
  point.hash = hash(point);
312
398
  const replacer = new RangeTransform(falseExpression);
313
399
  const registerStmt = createRegisterStatement(point);
@@ -319,6 +405,72 @@ export class CoverageTransform extends Visitor {
319
405
  }
320
406
  }
321
407
  }
408
+ visitForStatement(node) {
409
+ if (node.visited)
410
+ return;
411
+ node.visited = true;
412
+ const path = node.range.source.normalizedPath;
413
+ node.body = this.instrumentStatementBody(path, node.body, "Loop");
414
+ super.visitForStatement(node);
415
+ }
416
+ visitForOfStatement(node) {
417
+ if (node.visited)
418
+ return;
419
+ node.visited = true;
420
+ const path = node.range.source.normalizedPath;
421
+ node.body = this.instrumentStatementBody(path, node.body, "Loop");
422
+ super.visitForOfStatement(node);
423
+ }
424
+ visitWhileStatement(node) {
425
+ if (node.visited)
426
+ return;
427
+ node.visited = true;
428
+ const path = node.range.source.normalizedPath;
429
+ node.body = this.instrumentStatementBody(path, node.body, "Loop");
430
+ super.visitWhileStatement(node);
431
+ }
432
+ visitDoStatement(node) {
433
+ if (node.visited)
434
+ return;
435
+ node.visited = true;
436
+ const path = node.range.source.normalizedPath;
437
+ node.body = this.instrumentStatementBody(path, node.body, "Loop");
438
+ super.visitDoStatement(node);
439
+ }
440
+ visitReturnStatement(node) {
441
+ if (node.visited)
442
+ return;
443
+ node.visited = true;
444
+ super.visitReturnStatement(node);
445
+ if (!node.value || isBuiltinCallExpression(node.value))
446
+ return;
447
+ const path = node.range.source.normalizedPath;
448
+ const point = this.createPoint(path, node.value, "Return");
449
+ const replacer = new RangeTransform(node);
450
+ const registerStmt = createRegisterStatement(point);
451
+ replacer.visit(registerStmt);
452
+ const coverExpression = createCoverExpression(point.hash, node.value, node);
453
+ replacer.visit(coverExpression);
454
+ node.value = coverExpression;
455
+ this.globalStatements.push(registerStmt);
456
+ }
457
+ visitThrowStatement(node) {
458
+ if (node.visited)
459
+ return;
460
+ node.visited = true;
461
+ super.visitThrowStatement(node);
462
+ if (!node.value || isBuiltinCallExpression(node.value))
463
+ return;
464
+ const path = node.range.source.normalizedPath;
465
+ const point = this.createPoint(path, node.value, "Throw");
466
+ const replacer = new RangeTransform(node);
467
+ const registerStmt = createRegisterStatement(point);
468
+ replacer.visit(registerStmt);
469
+ const coverExpression = createCoverExpression(point.hash, node.value, node);
470
+ replacer.visit(coverExpression);
471
+ node.value = coverExpression;
472
+ this.globalStatements.push(registerStmt);
473
+ }
322
474
  visitSwitchCase(node) {
323
475
  if (node.visited)
324
476
  return;
@@ -329,7 +481,8 @@ export class CoverageTransform extends Visitor {
329
481
  point.line = caseLc?.line;
330
482
  point.column = caseLc?.column;
331
483
  point.file = path;
332
- point.type = CoverType.Block;
484
+ point.type = "SwitchCase";
485
+ this.assignParentMetadata(point);
333
486
  point.hash = hash(point);
334
487
  const replacer = new RangeTransform(node);
335
488
  const registerStmt = createRegisterStatement(point);
@@ -354,7 +507,8 @@ export class CoverageTransform extends Visitor {
354
507
  point.line = blockLc?.line;
355
508
  point.column = blockLc?.column;
356
509
  point.file = path;
357
- point.type = CoverType.Block;
510
+ point.type = "Block";
511
+ this.assignParentMetadata(point);
358
512
  point.hash = hash(point);
359
513
  const replacer = new RangeTransform(node);
360
514
  const registerStmt = createRegisterStatement(point);
@@ -413,6 +567,11 @@ function getExpressionName(node) {
413
567
  return null;
414
568
  }
415
569
  }
570
+ function getNodeName(node) {
571
+ if (!node)
572
+ return null;
573
+ return getExpressionName(node);
574
+ }
416
575
  function djb2Hash(str) {
417
576
  const points = Array.from(str);
418
577
  let h = 5381;
@@ -444,7 +603,7 @@ function getLineCol(node) {
444
603
  };
445
604
  }
446
605
  function createRegisterStatement(point) {
447
- return SimpleParser.parseTopLevelStatement(`__REGISTER_RAW(${asStringLiteral(point.file)}, ${asStringLiteral(point.hash)}, ${point.line}, ${point.column}, ${asStringLiteral(coverTypeToString(point.type))})`);
606
+ return SimpleParser.parseTopLevelStatement(`__REGISTER_RAW(${asStringLiteral(point.file)}, ${asStringLiteral(point.hash)}, ${point.line}, ${point.column}, ${asStringLiteral(point.type)}, ${asStringLiteral(point.parentHash)}, ${asStringLiteral(point.scopeKind)}, ${asStringLiteral(point.scopeName)}, ${point.depth})`);
448
607
  }
449
608
  function createCoverStatement(hashValue, ref) {
450
609
  return Node.createExpressionStatement(createCoverCallExpression(hashValue, ref));
@@ -455,16 +614,6 @@ function createCoverExpression(hashValue, replacement, ref) {
455
614
  function createCoverCallExpression(hashValue, ref) {
456
615
  return Node.createCallExpression(Node.createIdentifierExpression("__COVER", ref.range), null, [Node.createStringLiteralExpression(hashValue, ref.range)], ref.range);
457
616
  }
458
- function coverTypeToString(type) {
459
- switch (type) {
460
- case CoverType.Function:
461
- return "Function";
462
- case CoverType.Expression:
463
- return "Expression";
464
- default:
465
- return "Block";
466
- }
467
- }
468
617
  function asStringLiteral(value) {
469
618
  return JSON.stringify(value);
470
619
  }