@abreen/tada 1.13.7 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +46 -0
  2. package/build/generate-content-assets.ts +39 -185
  3. package/build/pipeline.ts +1 -1
  4. package/build/source-model.ts +2 -14
  5. package/build/types.d.ts +18 -18
  6. package/build/utils/html-comments.ts +3 -0
  7. package/build/utils/jdi-runner/TraceRunner.java +227 -38
  8. package/build/utils/markdown-partials.ts +235 -0
  9. package/build/utils/markdown.ts +12 -1
  10. package/build/utils/python-runner/trace_runner.py +58 -13
  11. package/build/utils/render.ts +14 -12
  12. package/build/utils/trace-core.ts +136 -14
  13. package/build/utils/trace-heap.ts +34 -0
  14. package/build/utils/trace-java.ts +60 -7
  15. package/build/utils/trace-layout.ts +58 -13
  16. package/build/utils/trace-python.ts +6 -2
  17. package/build/utils/trace-svg.ts +54 -60
  18. package/build/utils/trace.ts +193 -78
  19. package/build/watch/build-full.ts +26 -52
  20. package/build/watch/build-helpers.ts +66 -0
  21. package/build/watch/build-incremental.ts +32 -60
  22. package/build/watch/build-result.ts +19 -0
  23. package/build/watch/compiler-types.d.ts +2 -2
  24. package/build/watch/compiler.ts +30 -62
  25. package/build/watch/config-paths.ts +56 -0
  26. package/build/watch/engine.ts +199 -0
  27. package/{watch → build/watch}/fs-commit.ts +1 -3
  28. package/build/watch/index.ts +1 -1
  29. package/build/watch/mutations.ts +23 -25
  30. package/build/watch/planner.ts +14 -78
  31. package/build/watch/runtime.ts +26 -38
  32. package/build/watch/snapshot.ts +26 -47
  33. package/build/watch/types.d.ts +84 -0
  34. package/build/watch/validation.ts +1 -1
  35. package/init/content/lectures/02/_pr1.md +1 -1
  36. package/init/content/lectures/02/index.md +2 -2
  37. package/init/content/lectures/02/subdir/_foobar.md +3 -0
  38. package/init/content/lectures/02/subdir/_pr2.md +3 -3
  39. package/package.json +1 -2
  40. package/src/slides/index.ts +6 -2
  41. package/src/trace/index.ts +49 -12
  42. package/src/trace/style.scss +10 -0
  43. package/src/trace/types.d.ts +16 -4
  44. package/build/utils/include.ts +0 -57
  45. package/build/watch-reachability-state.ts +0 -305
  46. package/init/content/lectures/02/subdir/_foobar.html +0 -3
  47. package/watch/engine.ts +0 -296
  48. package/watch/events.ts +0 -10
  49. package/watch/types.d.ts +0 -103
@@ -5,17 +5,22 @@ import json
5
5
  import os
6
6
  import pdb
7
7
  import sys
8
+ import traceback
8
9
  import types
9
10
 
10
11
 
11
12
  class CaptureStream(io.TextIOBase):
12
- def __init__(self, sink):
13
+ def __init__(self, sink, stream):
13
14
  self._sink = sink
15
+ self._stream = stream
14
16
 
15
17
  def write(self, s):
16
18
  if not isinstance(s, str):
17
19
  s = str(s)
18
- self._sink.append(s)
20
+ if self._sink and self._sink[-1]["stream"] == self._stream:
21
+ self._sink[-1]["text"] += s
22
+ else:
23
+ self._sink.append({"stream": self._stream, "text": s})
19
24
  return len(s)
20
25
 
21
26
  def flush(self):
@@ -28,9 +33,10 @@ class CaptureStream(io.TextIOBase):
28
33
  class TracePdb(pdb.Pdb):
29
34
  SYNTHETIC_FRAME_NAMES = {"<listcomp>", "<dictcomp>", "<setcomp>"}
30
35
 
31
- def __init__(self, target_path, trace_writer, output_chunks):
36
+ def __init__(self, target_path, traced_paths, trace_writer, output_chunks):
32
37
  super().__init__(nosigint=True, readrc=False)
33
38
  self.target_path = os.path.abspath(target_path)
39
+ self.traced_paths = set(os.path.abspath(path) for path in traced_paths)
34
40
  self.trace_writer = trace_writer
35
41
  self.output_chunks = output_chunks
36
42
  self.pending_snapshot = None
@@ -69,18 +75,24 @@ class TracePdb(pdb.Pdb):
69
75
  if self.pending_snapshot is None:
70
76
  return
71
77
  payload = dict(self.pending_snapshot)
72
- payload["stdout"] = self._drain_output()
78
+ payload["output"] = self._drain_output()
73
79
  self.trace_writer.write(json.dumps(payload) + "\n")
74
80
  self.trace_writer.flush()
75
81
  self.pending_snapshot = None
76
82
 
77
83
  def _drain_output(self):
78
84
  if not self.output_chunks:
79
- return ""
80
- output = "".join(self.output_chunks)
85
+ return []
86
+ output = list(self.output_chunks)
81
87
  self.output_chunks.clear()
82
88
  return output
83
89
 
90
+ def emit_crash(self, frame):
91
+ payload = self._capture_snapshot(frame)
92
+ payload["output"] = self._drain_output()
93
+ self.trace_writer.write(json.dumps(payload) + "\n")
94
+ self.trace_writer.flush()
95
+
84
96
  def _capture_snapshot(self, frame):
85
97
  reachable = {}
86
98
  return {
@@ -206,7 +218,7 @@ class TracePdb(pdb.Pdb):
206
218
 
207
219
  def _is_traced_frame(self, frame):
208
220
  return (
209
- os.path.abspath(frame.f_code.co_filename) == self.target_path
221
+ os.path.abspath(frame.f_code.co_filename) in self.traced_paths
210
222
  and frame.f_code.co_name not in self.SYNTHETIC_FRAME_NAMES
211
223
  )
212
224
 
@@ -221,11 +233,14 @@ class TracePdb(pdb.Pdb):
221
233
 
222
234
 
223
235
  def main():
224
- if len(sys.argv) != 2:
225
- print("Usage: trace_runner.py <python_file>", file=sys.stderr)
236
+ if len(sys.argv) < 2:
237
+ print("Usage: trace_runner.py <python_file> [traced_file ...]", file=sys.stderr)
226
238
  sys.exit(1)
227
239
 
228
240
  target_path = os.path.abspath(sys.argv[1])
241
+ traced_paths = [os.path.abspath(path) for path in sys.argv[2:]]
242
+ if target_path not in traced_paths:
243
+ traced_paths.insert(0, target_path)
229
244
  target_dir = os.path.dirname(target_path)
230
245
  target_name = os.path.basename(target_path)
231
246
 
@@ -234,8 +249,9 @@ def main():
234
249
 
235
250
  trace_writer = sys.stdout
236
251
  output_chunks = []
237
- capture = CaptureStream(output_chunks)
238
- debugger = TracePdb(target_path, trace_writer, output_chunks)
252
+ stdout_capture = CaptureStream(output_chunks, "stdout")
253
+ stderr_capture = CaptureStream(output_chunks, "stderr")
254
+ debugger = TracePdb(target_path, traced_paths, trace_writer, output_chunks)
239
255
 
240
256
  code_globals = {
241
257
  "__name__": "__main__",
@@ -253,14 +269,21 @@ def main():
253
269
  source = f.read()
254
270
  compiled = compile(source, target_path, "exec")
255
271
 
256
- sys.stdout = capture
257
- sys.stderr = capture
272
+ sys.stdout = stdout_capture
273
+ sys.stderr = stderr_capture
258
274
 
259
275
  try:
260
276
  debugger.set_step()
261
277
  debugger.runcall(exec, compiled, code_globals, code_globals)
262
278
  except SystemExit:
263
279
  pass
280
+ except BaseException as exc:
281
+ debugger.flush_pending()
282
+ sys.stderr.write(format_target_traceback(exc, set(traced_paths)))
283
+ frame = find_traced_exception_frame(debugger, exc.__traceback__)
284
+ if frame is None:
285
+ raise
286
+ debugger.emit_crash(frame)
264
287
  finally:
265
288
  debugger.flush_pending()
266
289
  finally:
@@ -268,5 +291,27 @@ def main():
268
291
  sys.stderr = original_stderr
269
292
 
270
293
 
294
+ def find_traced_exception_frame(debugger, tb):
295
+ traced = None
296
+ while tb is not None:
297
+ if debugger._is_traced_frame(tb.tb_frame):
298
+ traced = tb.tb_frame
299
+ tb = tb.tb_next
300
+ return traced
301
+
302
+
303
+ def format_target_traceback(exc, traced_paths):
304
+ frames = [
305
+ frame
306
+ for frame in traceback.extract_tb(exc.__traceback__)
307
+ if os.path.abspath(frame.filename) in traced_paths
308
+ ]
309
+ return (
310
+ "Traceback (most recent call last):\n"
311
+ + "".join(traceback.format_list(frames))
312
+ + "".join(traceback.format_exception_only(type(exc), exc))
313
+ )
314
+
315
+
271
316
  if __name__ == "__main__":
272
317
  main()
@@ -25,7 +25,7 @@ import {
25
25
  } from './code';
26
26
  import { extensionIsMarkdown } from './file-types';
27
27
  import { createTraceHelpers } from './trace';
28
- import { createIncludeFunction } from './include';
28
+ import { stripHtmlComments } from './html-comments';
29
29
  import { finalizeHtmlPage } from './final-html';
30
30
  import {
31
31
  createApplyBasePath,
@@ -60,6 +60,8 @@ const log = makeLogger(import.meta.url);
60
60
 
61
61
  const tadaVersion: string = pkg.version;
62
62
 
63
+ export { stripHtmlComments } from './html-comments';
64
+
63
65
  const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
64
66
 
65
67
  function isWatchMode(assetFiles: string[]): boolean {
@@ -433,9 +435,9 @@ function renderPlainTextContent(
433
435
  {
434
436
  manifestUrl: string;
435
437
  artifactId: string;
436
- highlightedSource: string;
438
+ highlightedSources: { file: string; highlightedSource: string }[];
437
439
  totalSteps: number;
438
- mtime: number;
440
+ sourceMtims: Record<string, number>;
439
441
  }
440
442
  >;
441
443
  contentDir?: string;
@@ -459,7 +461,7 @@ function renderPlainTextContent(
459
461
  `${filePath}: slides mode is only supported on Markdown pages`,
460
462
  );
461
463
  }
462
- const md = createMarkdown(siteVariables, {
464
+ const frontMatterMd = createMarkdown(siteVariables, {
463
465
  filePath,
464
466
  slides: pageVariables.slides === true,
465
467
  });
@@ -480,8 +482,8 @@ function renderPlainTextContent(
480
482
  );
481
483
 
482
484
  // Render title and description as inline Markdown
483
- renderInlineField(md, pageVariablesProcessed, 'title');
484
- renderInlineField(md, pageVariablesProcessed, 'description');
485
+ renderInlineField(frontMatterMd, pageVariablesProcessed, 'title');
486
+ renderInlineField(frontMatterMd, pageVariablesProcessed, 'description');
485
487
 
486
488
  resolveAuthor(pageVariablesProcessed, filePath, dependencyCollector);
487
489
 
@@ -526,8 +528,6 @@ function renderPlainTextContent(
526
528
  params.renderTrace = helpers.renderTrace;
527
529
  }
528
530
 
529
- params.include = createIncludeFunction(filePath, params, dependencyCollector);
530
-
531
531
  let html: string;
532
532
  try {
533
533
  html = _.template(strippedContent)(params);
@@ -541,6 +541,12 @@ function renderPlainTextContent(
541
541
  let tocItems: unknown[] | null = null;
542
542
  let alertIds: string[] = [];
543
543
  if (extensionIsMarkdown(ext)) {
544
+ const md = createMarkdown(siteVariables, {
545
+ filePath,
546
+ slides: pageVariables.slides === true,
547
+ templateParams: params,
548
+ dependencyCollector,
549
+ });
544
550
  const env: Record<string, unknown> = { alertIds: [] as string[] };
545
551
  html = md.render(html!, env);
546
552
  tocItems = (env.tocItems as unknown[] | undefined) || null;
@@ -555,10 +561,6 @@ function renderPlainTextContent(
555
561
  };
556
562
  }
557
563
 
558
- export function stripHtmlComments(str: string): string {
559
- return str.replace(/<!---[\s\S]*?-->/g, '');
560
- }
561
-
562
564
  export function renderLiterateJavaPageAsset({
563
565
  filePath,
564
566
  contentDir,
@@ -5,8 +5,14 @@ import { renderCodeSegment } from './code';
5
5
  import { splitLines } from './literate-java';
6
6
  import { normalizeOutputPath } from './paths';
7
7
  import { computeLayout } from './trace-layout';
8
- import { generateStepSvg } from './trace-svg';
9
- import type { TraceChunkEntry, TraceManifest, TraceStep } from '../types';
8
+ import { filterStep, generateStepSvg } from './trace-svg';
9
+ import type {
10
+ TraceChunkEntry,
11
+ TraceHeapObject,
12
+ TraceManifest,
13
+ TraceOutputEvent,
14
+ TraceStep,
15
+ } from '../types';
10
16
 
11
17
  export const DEFAULT_CHUNK_SIZE = 50;
12
18
 
@@ -21,6 +27,10 @@ export interface ChunkTraceOutputResult {
21
27
  outputPaths: string[];
22
28
  }
23
29
 
30
+ function stepOutputEvents(step: TraceStep): TraceOutputEvent[] {
31
+ return step.output ?? [];
32
+ }
33
+
24
34
  function hashTraceFiles(files: { name: string; content: string }[]): string {
25
35
  const hasher = createHash('sha256');
26
36
  for (const file of files) {
@@ -43,13 +53,101 @@ function buildTraceOutputPath(
43
53
  .join('/');
44
54
  }
45
55
 
56
+ function escapeAttr(value: string): string {
57
+ return value
58
+ .replaceAll('&', '&amp;')
59
+ .replaceAll('"', '&quot;')
60
+ .replaceAll('<', '&lt;')
61
+ .replaceAll('>', '&gt;');
62
+ }
63
+
64
+ function framesMatchCaller(previous: TraceStep, current: TraceStep): boolean {
65
+ if (current.stack.length !== previous.stack.length - 1) {
66
+ return false;
67
+ }
68
+
69
+ return current.stack.every((frame, index) => {
70
+ const previousFrame = previous.stack[index + 1];
71
+ return (
72
+ frame.method === previousFrame.method &&
73
+ frame.class === previousFrame.class
74
+ );
75
+ });
76
+ }
77
+
78
+ function addReachableHeapObjects(
79
+ id: string,
80
+ source: Record<string, TraceHeapObject>,
81
+ target: Record<string, TraceHeapObject>,
82
+ ): void {
83
+ const queue = [id];
84
+ const seen = new Set<string>();
85
+
86
+ while (queue.length > 0) {
87
+ const currentId = queue.shift()!;
88
+ if (seen.has(currentId) || !source[currentId]) {
89
+ continue;
90
+ }
91
+ seen.add(currentId);
92
+ target[currentId] ??= source[currentId];
93
+
94
+ const obj = source[currentId];
95
+ if ('elements' in obj) {
96
+ for (const element of obj.elements) {
97
+ if (element.type === 'ref') {
98
+ queue.push(element.id);
99
+ }
100
+ }
101
+ } else if ('fields' in obj) {
102
+ for (const value of Object.values(obj.fields)) {
103
+ if (value.type === 'ref') {
104
+ queue.push(value.id);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ export function bridgeConstructorReturnValues(steps: TraceStep[]): TraceStep[] {
112
+ const bridgedSteps = steps.map(step => ({
113
+ ...step,
114
+ heap: { ...step.heap },
115
+ transientHeapRoots: step.transientHeapRoots
116
+ ? [...step.transientHeapRoots]
117
+ : undefined,
118
+ }));
119
+
120
+ for (let i = 1; i < bridgedSteps.length; i++) {
121
+ const previous = bridgedSteps[i - 1];
122
+ const current = bridgedSteps[i];
123
+ const poppedFrame = previous.stack[0];
124
+ const thisValue = poppedFrame?.locals.this;
125
+
126
+ if (
127
+ poppedFrame?.method !== '<init>' ||
128
+ thisValue?.type !== 'ref' ||
129
+ !framesMatchCaller(previous, current)
130
+ ) {
131
+ continue;
132
+ }
133
+
134
+ addReachableHeapObjects(thisValue.id, previous.heap, current.heap);
135
+ current.transientHeapRoots ??= [];
136
+ if (!current.transientHeapRoots.includes(thisValue.id)) {
137
+ current.transientHeapRoots.push(thisValue.id);
138
+ }
139
+ }
140
+
141
+ return bridgedSteps;
142
+ }
143
+
46
144
  export function chunkTraceOutput(
47
145
  output: string,
48
146
  traceOutputDir: string,
49
147
  relDir: string,
50
148
  traceName: string,
51
- sourceFile: string,
52
- source: string,
149
+ primaryFile: string,
150
+ sources: { file: string; source: string }[],
53
151
  options: ChunkTraceOutputOptions = {},
54
152
  ): ChunkTraceOutputResult {
55
153
  const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
@@ -60,18 +158,29 @@ export function chunkTraceOutput(
60
158
  .split('\n')
61
159
  .filter(l => l.length > 0);
62
160
 
63
- const allSteps: TraceStep[] = lines.map(l => JSON.parse(l));
161
+ const allSteps: TraceStep[] = bridgeConstructorReturnValues(
162
+ lines.map(l => JSON.parse(l)),
163
+ );
64
164
 
65
- const lineToSteps: Record<number, number[]> = {};
165
+ const lineToStepsByFile = new Map<string, Record<number, number[]>>();
166
+ for (const source of sources) {
167
+ lineToStepsByFile.set(source.file, {});
168
+ }
66
169
  for (let i = 0; i < allSteps.length; i++) {
67
170
  const step = allSteps[i];
171
+ const file = path.basename(step.file);
172
+ const lineToSteps = lineToStepsByFile.get(file);
173
+ if (!lineToSteps) {
174
+ continue;
175
+ }
68
176
  if (!lineToSteps[step.line]) {
69
177
  lineToSteps[step.line] = [];
70
178
  }
71
179
  lineToSteps[step.line].push(i);
72
180
  }
73
181
 
74
- const layout = computeLayout(allSteps, ignoreFields);
182
+ const layoutSteps = allSteps.map(step => ({ ...step, ...filterStep(step) }));
183
+ const layout = computeLayout(layoutSteps, ignoreFields);
75
184
 
76
185
  let chunkEntries: TraceChunkEntry[] = [];
77
186
  let chunkIndex = 0;
@@ -79,7 +188,12 @@ export function chunkTraceOutput(
79
188
 
80
189
  for (const step of allSteps) {
81
190
  const svg = generateStepSvg(step, layout);
82
- chunkEntries.push({ line: step.line, stdout: step.stdout, svg });
191
+ chunkEntries.push({
192
+ file: path.basename(step.file),
193
+ line: step.line,
194
+ output: stepOutputEvents(step),
195
+ svg,
196
+ });
83
197
 
84
198
  if (chunkEntries.length >= chunkSize) {
85
199
  chunkFiles.push({
@@ -101,9 +215,11 @@ export function chunkTraceOutput(
101
215
  const manifest: TraceManifest = {
102
216
  totalSteps: allSteps.length,
103
217
  chunkSize,
104
- sourceFile,
105
- source,
106
- lineToSteps,
218
+ primaryFile,
219
+ sources: sources.map(source => ({
220
+ ...source,
221
+ lineToSteps: lineToStepsByFile.get(source.file) ?? {},
222
+ })),
107
223
  };
108
224
 
109
225
  const manifestFile = {
@@ -218,11 +334,11 @@ export function buildManifestUrl({
218
334
  }
219
335
 
220
336
  export function renderTraceWidgetHtml({
221
- highlightedSource,
337
+ highlightedSources,
222
338
  manifestUrl,
223
339
  totalSteps,
224
340
  }: {
225
- highlightedSource: string;
341
+ highlightedSources: { file: string; highlightedSource: string }[];
226
342
  manifestUrl?: string;
227
343
  totalSteps?: number;
228
344
  }): string {
@@ -248,5 +364,11 @@ export function renderTraceWidgetHtml({
248
364
  const wrapperClass = disabled
249
365
  ? 'trace-widget trace-disabled'
250
366
  : 'trace-widget';
251
- return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-source-wrapper"><div class="trace-source">${highlightedSource}</div></div></div></div></div>`;
367
+ const sourcePanels = highlightedSources
368
+ .map((source, index) => {
369
+ const hidden = index === 0 ? '' : ' hidden';
370
+ return `<div class="trace-source" data-trace-source-file="${escapeAttr(source.file)}"${hidden}>${source.highlightedSource}</div>`;
371
+ })
372
+ .join('');
373
+ return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-source-wrapper">${sourcePanels}</div></div></div></div>`;
252
374
  }
@@ -0,0 +1,34 @@
1
+ import type { TraceHeapObject } from '../types';
2
+
3
+ export type TraceHeapScalar = string | number | boolean;
4
+
5
+ const BOXED_PRIMITIVE_TYPES = new Set([
6
+ 'java.lang.Boolean',
7
+ 'java.lang.Byte',
8
+ 'java.lang.Character',
9
+ 'java.lang.Double',
10
+ 'java.lang.Float',
11
+ 'java.lang.Integer',
12
+ 'java.lang.Long',
13
+ 'java.lang.Short',
14
+ ]);
15
+
16
+ export function isInlineHeapObject(obj: TraceHeapObject): boolean {
17
+ return (
18
+ 'value' in obj &&
19
+ (obj.type === 'String' || BOXED_PRIMITIVE_TYPES.has(obj.type))
20
+ );
21
+ }
22
+
23
+ export function formatHeapObjectValue(
24
+ type: string,
25
+ value: TraceHeapScalar,
26
+ ): string {
27
+ if (type === 'String') {
28
+ return `"${value}"`;
29
+ }
30
+ if (type === 'java.lang.Character') {
31
+ return `'${value}'`;
32
+ }
33
+ return String(value);
34
+ }
@@ -2,11 +2,17 @@ import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
4
  import { execFileSync } from 'child_process';
5
+ import { parse as parseJava } from 'java-parser';
5
6
  import { makeLogger } from '../log';
6
7
  import { hasMainMethod, runJavac } from './literate-java';
7
8
 
8
9
  const log = makeLogger(import.meta.url);
9
10
 
11
+ interface CstNode {
12
+ name?: string;
13
+ children?: Record<string, CstNode[]>;
14
+ }
15
+
10
16
  /**
11
17
  * Parse // @trace-ignore comments from Java source code.
12
18
  * Returns a map of className -> fieldName[] for fields that should be
@@ -65,6 +71,42 @@ export function parseIgnoreFields(source: string): Record<string, string[]> {
65
71
  return result;
66
72
  }
67
73
 
74
+ const JAVA_TOP_LEVEL_TYPE_NODES = new Set([
75
+ 'classDeclaration',
76
+ 'interfaceDeclaration',
77
+ 'enumDeclaration',
78
+ 'recordDeclaration',
79
+ ]);
80
+
81
+ export function hasExplicitTopLevelTypeDeclaration(source: string): boolean {
82
+ let cst: CstNode;
83
+ try {
84
+ cst = parseJava(source) as CstNode;
85
+ } catch (err: unknown) {
86
+ log.debug`Failed to parse Java source for unnamed class detection: ${(err as Error).message}`;
87
+ return true;
88
+ }
89
+
90
+ const ordinaryCompilationUnit = cst.children?.ordinaryCompilationUnit?.[0];
91
+ const typeDeclarations =
92
+ ordinaryCompilationUnit?.children?.typeDeclaration ?? [];
93
+
94
+ return typeDeclarations.some(typeDeclaration =>
95
+ Object.keys(typeDeclaration.children ?? {}).some(childName =>
96
+ JAVA_TOP_LEVEL_TYPE_NODES.has(childName),
97
+ ),
98
+ );
99
+ }
100
+
101
+ export function getUnnamedClassNames(javaFilePaths: string[]): string[] {
102
+ return javaFilePaths
103
+ .filter(javaFilePath => {
104
+ const source = fs.readFileSync(javaFilePath, 'utf-8');
105
+ return !hasExplicitTopLevelTypeDeclaration(source);
106
+ })
107
+ .map(javaFilePath => path.parse(javaFilePath).name);
108
+ }
109
+
68
110
  let tracerClassDir: string | null = null;
69
111
 
70
112
  function ensureTracerCompiled(): string {
@@ -87,7 +129,7 @@ function ensureTracerCompiled(): string {
87
129
  return tempDir;
88
130
  }
89
131
 
90
- function compileTargetFile(javaFilePath: string): string {
132
+ function compileTargetFiles(javaFilePaths: string[]): string {
91
133
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-trace-target-'));
92
134
 
93
135
  runJavac(
@@ -96,10 +138,10 @@ function compileTargetFile(javaFilePath: string): string {
96
138
  '-d',
97
139
  tempDir,
98
140
  '-sourcepath',
99
- path.dirname(javaFilePath),
100
- javaFilePath,
141
+ path.dirname(javaFilePaths[0]),
142
+ ...javaFilePaths,
101
143
  ],
102
- { tempDir, label: `Compilation failed for ${javaFilePath}` },
144
+ { tempDir, label: `Compilation failed for ${javaFilePaths[0]}` },
103
145
  );
104
146
 
105
147
  return tempDir;
@@ -116,14 +158,25 @@ export function validateJavaTraceTarget(
116
158
  }
117
159
  }
118
160
 
119
- export function runJavaTrace(javaFilePath: string, className: string): string {
120
- const targetClassDir = compileTargetFile(javaFilePath);
161
+ export function runJavaTrace(
162
+ javaFilePaths: string[],
163
+ className: string,
164
+ ): string {
165
+ const targetClassDir = compileTargetFiles(javaFilePaths);
121
166
 
122
167
  try {
123
168
  const tracerDir = ensureTracerCompiled();
169
+ const unnamedClassNames = getUnnamedClassNames(javaFilePaths);
124
170
  return execFileSync(
125
171
  'java',
126
- ['-cp', tracerDir, 'TraceRunner', className, targetClassDir],
172
+ [
173
+ '-cp',
174
+ tracerDir,
175
+ 'TraceRunner',
176
+ className,
177
+ targetClassDir,
178
+ ...unnamedClassNames,
179
+ ],
127
180
  { timeout: 60000, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 },
128
181
  );
129
182
  } finally {