Skip to content
File

Blob: .opencode/tools/jsg-interface.ts

typescript798 lines
1import { tool } from '@opencode-ai/plugin';
2import path from 'path';
3 
4export default tool({
5 description:
6 'Extract the full JSG interface for a C++ class — all methods, properties, constants, ' +
7 'nested types, inheritance, iterators, serialization, and TypeScript overrides. ' +
8 'Returns structured, categorized output instead of raw grep lines. ' +
9 'Use this when you need to understand the complete JS-visible API of a workerd type.',
10 args: {
11 className: tool.schema
12 .string()
13 .describe(
14 "C++ class name (e.g., 'ReadableStream', 'SubtleCrypto', 'Request')"
15 ),
16 },
17 async execute(args, ctx) {
18 const root = ctx.worktree || ctx.directory;
19 const srcDir = path.join(root, 'src');
20 const cls = args.className;
21 
22 // Step 1: Find the file containing JSG_RESOURCE_TYPE(cls) or JSG_STRUCT for this class
23 let headerPath: string | null = null;
24 let registrationType: 'resource' | 'struct' = 'resource';
25 let allResourceFiles: { file: string; line: number }[] = [];
26 
27 try {
28 const resourceHit =
29 await Bun.$`grep -rn --include='*.h' "JSG_RESOURCE_TYPE.${cls}" ${srcDir}`.text();
30 const hits = resourceHit.split('\n').filter(Boolean);
31 // Pick the best match: prefer a file where JSG_RESOURCE_TYPE(ClassName is a top-level
32 // class (not nested). Heuristic: the class declaration `class ClassName` appears in the
33 // file without being inside another class's braces. Simple approach: prefer the file
34 // where the line starts with exactly JSG_RESOURCE_TYPE(ClassName (allowing whitespace).
35 // If ambiguous, return all.
36 const parsed = hits
37 .map((h) => {
38 const m = h.match(/^(.+?):(\d+):(.*)$/);
39 return m
40 ? { file: m[1].trim(), line: parseInt(m[2]), text: m[3] }
41 : null;
42 })
43 .filter(Boolean) as { file: string; line: number; text: string }[];
44 
45 if (parsed.length === 1) {
46 headerPath = parsed[0].file;
47 } else if (parsed.length > 1) {
48 // Score each candidate: prefer files where the class is top-level (not nested)
49 // Check if a `class ClassName` appears near the top of the file (not deep in another class)
50 let best: (typeof parsed)[0] | null = null;
51 let bestScore = -1;
52 
53 for (const p of parsed) {
54 let score = 0;
55 // Prefer files whose name contains the class name
56 if (path.basename(p.file).toLowerCase().includes(cls.toLowerCase())) {
57 score += 10;
58 }
59 // Check if `class ClassName` appears in the file as a top-level or first-level class
60 // by reading a few lines before the JSG_RESOURCE_TYPE match
61 try {
62 const check =
63 await Bun.$`grep -c "^class ${cls}\\b" ${p.file}`.text();
64 const topLevelCount = parseInt(check.trim());
65 // A top-level `class Foo` (no indentation) strongly suggests this is the primary class
66 if (topLevelCount > 0) score += 20;
67 } catch {
68 // grep returned no matches
69 }
70 if (score > bestScore) {
71 bestScore = score;
72 best = p;
73 }
74 }
75 
76 headerPath = best ? best.file : parsed[0].file;
77 // Store all files for reporting
78 allResourceFiles = parsed.map((p) => ({
79 file: path.relative(root, p.file),
80 line: p.line,
81 }));
82 }
83 } catch {
84 // not a resource type, try struct
85 }
86 
87 if (!headerPath) {
88 try {
89 const structHit =
90 await Bun.$`grep -rnl --include='*.h' "JSG_STRUCT.*${cls}" ${srcDir}`.text();
91 const files = structHit.split('\n').filter(Boolean);
92 if (files.length > 0) {
93 headerPath = files[0].trim();
94 registrationType = 'struct';
95 }
96 } catch {
97 // not found
98 }
99 }
100 
101 if (!headerPath) {
102 return `No JSG registration found for \`${cls}\`. The class may not be a JSG-registered type, or it may use a different registration name.`;
103 }
104 
105 // Step 2: Read the file
106 const content = await Bun.file(headerPath).text();
107 const relPath = path.relative(root, headerPath);
108 
109 if (registrationType === 'struct') {
110 return extractStruct(content, cls, relPath);
111 }
112 
113 return extractResourceType(content, cls, relPath, allResourceFiles);
114 },
115});
116 
117// --- Resource type extraction ---
118 
119function extractResourceType(
120 content: string,
121 cls: string,
122 filePath: string,
123 allFiles: { file: string; line: number }[] = []
124): string {
125 // Find the JSG_RESOURCE_TYPE block - match the opening and find the balanced closing brace
126 const lines = content.split('\n');
127 let startLine = -1;
128 let configParam = '';
129 
130 for (let i = 0; i < lines.length; i++) {
131 const m = lines[i].match(
132 new RegExp(`JSG_RESOURCE_TYPE\\s*\\(\\s*${cls}(?:\\s*,\\s*([^)]+))?\\)`)
133 );
134 if (m) {
135 startLine = i;
136 configParam = m[1]?.trim() || '';
137 break;
138 }
139 }
140 
141 if (startLine === -1) {
142 return `Found file \`${filePath}\` but could not locate \`JSG_RESOURCE_TYPE(${cls})\` block.`;
143 }
144 
145 // Find the block — count braces to find the matching close
146 const block = extractBracedBlock(lines, startLine);
147 if (!block) {
148 return `Found \`JSG_RESOURCE_TYPE(${cls})\` at \`${filePath}:${startLine + 1}\` but could not extract the block.`;
149 }
150 
151 // Parse all macro calls from the block
152 const iface = parseInterface(block);
153 
154 // Also check for JSG_MEMORY_INFO, iterator macros, etc. in the broader class
155 const classExtras = extractClassExtras(content, cls);
156 
157 // Build output
158 let out = `## JSG Interface: ${cls}\n\n`;
159 out += `**File:** \`${filePath}:${startLine + 1}\`\n`;
160 if (configParam) {
161 out += `**Config:** \`${configParam}\` (conditional registration)\n`;
162 }
163 if (allFiles.length > 1) {
164 const others = allFiles.filter((f) => f.file !== filePath);
165 out += `**Also registered in:** ${others.map((f) => `\`${f.file}:${f.line}\``).join(', ')} (may be a nested class with the same name)\n`;
166 }
167 out += '\n';
168 
169 // Inheritance
170 if (iface.inherits.length > 0) {
171 out += `### Inheritance\n\n`;
172 for (const i of iface.inherits) {
173 out += `- ${i}\n`;
174 }
175 out += '\n';
176 }
177 
178 // Methods
179 if (iface.methods.length > 0) {
180 out += `### Methods\n\n`;
181 for (const m of iface.methods) {
182 out += `- \`${m.jsName}\``;
183 if (m.cppName !== m.jsName) out += ` → \`${m.cppName}\``;
184 if (m.isStatic) out += ' *(static)*';
185 out += '\n';
186 }
187 out += '\n';
188 }
189 
190 // Properties
191 if (iface.properties.length > 0) {
192 out += `### Properties\n\n`;
193 for (const p of iface.properties) {
194 const flags: string[] = [];
195 if (p.readonly) flags.push('readonly');
196 if (p.lazy) flags.push('lazy');
197 if (p.location === 'instance') flags.push('instance');
198 if (p.location === 'prototype') flags.push('prototype');
199 if (p.location === 'static') flags.push('static');
200 if (p.location === 'inspect') flags.push('inspect-only');
201 out += `- \`${p.jsName}\``;
202 if (p.getter && p.getter !== p.jsName)
203 out += ` → getter: \`${p.getter}\``;
204 if (p.setter) out += `, setter: \`${p.setter}\``;
205 if (flags.length > 0) out += ` *(${flags.join(', ')})*`;
206 out += '\n';
207 }
208 out += '\n';
209 }
210 
211 // Constants
212 if (iface.constants.length > 0) {
213 out += `### Constants\n\n`;
214 for (const c of iface.constants) {
215 out += `- \`${c.jsName}\``;
216 if (c.cppExpr && c.cppExpr !== c.jsName) out += ` = \`${c.cppExpr}\``;
217 out += '\n';
218 }
219 out += '\n';
220 }
221 
222 // Nested types
223 if (iface.nestedTypes.length > 0) {
224 out += `### Nested Types\n\n`;
225 for (const n of iface.nestedTypes) {
226 out += `- \`${n.jsName}\``;
227 if (n.cppType !== n.jsName) out += ` → \`${n.cppType}\``;
228 out += '\n';
229 }
230 out += '\n';
231 }
232 
233 // Iterators
234 if (iface.iterators.length > 0) {
235 out += `### Iterators\n\n`;
236 for (const it of iface.iterators) {
237 out += `- \`${it.kind}\` via \`${it.method}\`\n`;
238 }
239 out += '\n';
240 }
241 
242 // Serialization
243 if (iface.serialization) {
244 out += `### Serialization\n\n`;
245 out += `- ${iface.serialization}\n\n`;
246 }
247 
248 // Callable
249 if (iface.callable) {
250 out += `### Callable\n\n`;
251 out += `- Invocable via \`${iface.callable}\`\n\n`;
252 }
253 
254 // Wildcard property
255 if (iface.wildcardProperty) {
256 out += `### Wildcard Property\n\n`;
257 out += `- Catch-all getter: \`${iface.wildcardProperty}\`\n\n`;
258 }
259 
260 // Dispose
261 if (iface.dispose.length > 0) {
262 out += `### Disposal\n\n`;
263 for (const d of iface.dispose) {
264 out += `- \`${d.kind}\` via \`${d.method}\`\n`;
265 }
266 out += '\n';
267 }
268 
269 // TypeScript overrides
270 if (iface.tsOverrides.length > 0) {
271 out += `### TypeScript Overrides\n\n`;
272 for (const ts of iface.tsOverrides) {
273 out += `- \`${ts.kind}\`: ${ts.content.length > 80 ? ts.content.substring(0, 80) + '...' : ts.content}\n`;
274 }
275 out += '\n';
276 }
277 
278 // JS bundles
279 if (iface.jsBundles.length > 0) {
280 out += `### JS Bundles\n\n`;
281 for (const b of iface.jsBundles) {
282 out += `- \`${b}\`\n`;
283 }
284 out += '\n';
285 }
286 
287 // Class-level extras
288 if (classExtras.hasMemoryInfo) {
289 out += `**Memory tracking:** Has \`JSG_MEMORY_INFO\`\n`;
290 }
291 if (classExtras.iteratorDecls.length > 0) {
292 out += `**Iterator declarations:**\n`;
293 for (const it of classExtras.iteratorDecls) {
294 out += `- \`${it}\`\n`;
295 }
296 }
297 
298 return out;
299}
300 
301// --- Struct extraction ---
302 
303function extractStruct(content: string, cls: string, filePath: string): string {
304 const lines = content.split('\n');
305 let structLine = -1;
306 let fields: string[] = [];
307 
308 for (let i = 0; i < lines.length; i++) {
309 const m = lines[i].match(/JSG_STRUCT\s*\(([^)]*)\)/);
310 if (m && isInClass(content, cls, i)) {
311 structLine = i;
312 fields = m[1]
313 .split(',')
314 .map((f) => f.trim())
315 .filter(Boolean);
316 break;
317 }
318 }
319 
320 // Check for multi-line JSG_STRUCT
321 if (structLine === -1) {
322 for (let i = 0; i < lines.length; i++) {
323 if (lines[i].includes('JSG_STRUCT(') && isInClass(content, cls, i)) {
324 structLine = i;
325 // Gather continuation lines
326 let combined = '';
327 for (let j = i; j < lines.length; j++) {
328 combined += lines[j];
329 if (combined.includes(')')) break;
330 }
331 const m = combined.match(/JSG_STRUCT\s*\(([^)]*)\)/);
332 if (m) {
333 fields = m[1]
334 .split(',')
335 .map((f) => f.trim())
336 .filter(Boolean);
337 }
338 break;
339 }
340 }
341 }
342 
343 let out = `## JSG Struct: ${cls}\n\n`;
344 out += `**File:** \`${filePath}:${structLine + 1}\`\n`;
345 out += `**Type:** Value type (deep-copied to/from JS objects)\n\n`;
346 
347 if (fields.length > 0) {
348 out += `### Fields\n\n`;
349 for (const f of fields) {
350 const jsName = f.startsWith('$') ? f.substring(1) : f;
351 out += `- \`${jsName}\``;
352 if (f.startsWith('$')) out += ` (C++ name: \`${f}\`)`;
353 out += '\n';
354 }
355 out += '\n';
356 }
357 
358 // Check for TS overrides
359 const tsLines = lines.filter(
360 (l) =>
361 l.includes('JSG_STRUCT_TS_OVERRIDE') || l.includes('JSG_STRUCT_TS_DEFINE')
362 );
363 if (tsLines.length > 0) {
364 out += `### TypeScript Overrides\n\n`;
365 for (const l of tsLines) {
366 out += `- \`${l.trim()}\`\n`;
367 }
368 }
369 
370 return out;
371}
372 
373// --- Parsing helpers ---
374 
375interface Method {
376 jsName: string;
377 cppName: string;
378 isStatic: boolean;
379}
380 
381interface Property {
382 jsName: string;
383 getter: string;
384 setter: string;
385 readonly: boolean;
386 lazy: boolean;
387 location: 'instance' | 'prototype' | 'static' | 'inspect';
388}
389 
390interface Constant {
391 jsName: string;
392 cppExpr: string;
393}
394 
395interface NestedType {
396 jsName: string;
397 cppType: string;
398}
399 
400interface Iterator {
401 kind: string;
402 method: string;
403}
404 
405interface Dispose {
406 kind: string;
407 method: string;
408}
409 
410interface TsOverride {
411 kind: string;
412 content: string;
413}
414 
415interface ParsedInterface {
416 inherits: string[];
417 methods: Method[];
418 properties: Property[];
419 constants: Constant[];
420 nestedTypes: NestedType[];
421 iterators: Iterator[];
422 serialization: string | null;
423 callable: string | null;
424 wildcardProperty: string | null;
425 dispose: Dispose[];
426 tsOverrides: TsOverride[];
427 jsBundles: string[];
428}
429 
430function parseInterface(block: string): ParsedInterface {
431 const result: ParsedInterface = {
432 inherits: [],
433 methods: [],
434 properties: [],
435 constants: [],
436 nestedTypes: [],
437 iterators: [],
438 serialization: null,
439 callable: null,
440 wildcardProperty: null,
441 dispose: [],
442 tsOverrides: [],
443 jsBundles: [],
444 };
445 
446 // Normalize: join continuation lines (lines ending with \) and collapse multi-line macros
447 const lines = block.split('\n');
448 
449 for (let i = 0; i < lines.length; i++) {
450 const line = lines[i].trim();
451 if (!line || line.startsWith('//') || line.startsWith('/*')) continue;
452 
453 // JSG_INHERIT
454 let m = line.match(/JSG_INHERIT\s*\(\s*(\w+)\s*\)/);
455 if (m) {
456 result.inherits.push(`JSG_INHERIT(${m[1]})`);
457 continue;
458 }
459 m = line.match(/JSG_INHERIT_INTRINSIC\s*\(\s*([^)]+)\s*\)/);
460 if (m) {
461 result.inherits.push(`JSG_INHERIT_INTRINSIC(${m[1].trim()})`);
462 continue;
463 }
464 
465 // JSG_METHOD / JSG_METHOD_NAMED / JSG_STATIC_METHOD / JSG_STATIC_METHOD_NAMED
466 m = line.match(/JSG_STATIC_METHOD_NAMED\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
467 if (m) {
468 result.methods.push({ jsName: m[1], cppName: m[2], isStatic: true });
469 continue;
470 }
471 m = line.match(/JSG_STATIC_METHOD\s*\(\s*(\w+)\s*\)/);
472 if (m) {
473 result.methods.push({ jsName: m[1], cppName: m[1], isStatic: true });
474 continue;
475 }
476 m = line.match(/JSG_METHOD_NAMED\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
477 if (m) {
478 result.methods.push({ jsName: m[1], cppName: m[2], isStatic: false });
479 continue;
480 }
481 m = line.match(/JSG_METHOD\s*\(\s*(\w+)\s*\)/);
482 if (m) {
483 result.methods.push({ jsName: m[1], cppName: m[1], isStatic: false });
484 continue;
485 }
486 
487 // JSG_CALLABLE
488 m = line.match(/JSG_CALLABLE\s*\(\s*(\w+)\s*\)/);
489 if (m) {
490 result.callable = m[1];
491 continue;
492 }
493 
494 // Properties — order matters: match more specific patterns first
495 // Static readonly
496 m = line.match(
497 /JSG_STATIC_READONLY_PROPERTY_NAMED\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/
498 );
499 if (m) {
500 result.properties.push({
501 jsName: m[1],
502 getter: m[2],
503 setter: '',
504 readonly: true,
505 lazy: false,
506 location: 'static',
507 });
508 continue;
509 }
510 m = line.match(/JSG_STATIC_READONLY_PROPERTY\s*\(\s*(\w+)\s*\)/);
511 if (m) {
512 result.properties.push({
513 jsName: m[1],
514 getter: m[1],
515 setter: '',
516 readonly: true,
517 lazy: false,
518 location: 'static',
519 });
520 continue;
521 }
522 
523 // Lazy instance properties
524 m = line.match(
525 /JSG_LAZY_READONLY_INSTANCE_PROPERTY\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/
526 );
527 if (m) {
528 result.properties.push({
529 jsName: m[1],
530 getter: m[2],
531 setter: '',
532 readonly: true,
533 lazy: true,
534 location: 'instance',
535 });
536 continue;
537 }
538 m = line.match(/JSG_LAZY_INSTANCE_PROPERTY\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
539 if (m) {
540 result.properties.push({
541 jsName: m[1],
542 getter: m[2],
543 setter: '',
544 readonly: false,
545 lazy: true,
546 location: 'instance',
547 });
548 continue;
549 }
550 
551 // Instance properties
552 m = line.match(
553 /JSG_READONLY_INSTANCE_PROPERTY\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/
554 );
555 if (m) {
556 result.properties.push({
557 jsName: m[1],
558 getter: m[2],
559 setter: '',
560 readonly: true,
561 lazy: false,
562 location: 'instance',
563 });
564 continue;
565 }
566 m = line.match(
567 /JSG_INSTANCE_PROPERTY\s*\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)/
568 );
569 if (m) {
570 result.properties.push({
571 jsName: m[1],
572 getter: m[2],
573 setter: m[3],
574 readonly: false,
575 lazy: false,
576 location: 'instance',
577 });
578 continue;
579 }
580 
581 // Prototype properties
582 m = line.match(
583 /JSG_READONLY_PROTOTYPE_PROPERTY\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/
584 );
585 if (m) {
586 result.properties.push({
587 jsName: m[1],
588 getter: m[2],
589 setter: '',
590 readonly: true,
591 lazy: false,
592 location: 'prototype',
593 });
594 continue;
595 }
596 m = line.match(
597 /JSG_PROTOTYPE_PROPERTY\s*\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)/
598 );
599 if (m) {
600 result.properties.push({
601 jsName: m[1],
602 getter: m[2],
603 setter: m[3],
604 readonly: false,
605 lazy: false,
606 location: 'prototype',
607 });
608 continue;
609 }
610 
611 // Inspect property
612 m = line.match(/JSG_INSPECT_PROPERTY\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
613 if (m) {
614 result.properties.push({
615 jsName: m[1],
616 getter: m[2],
617 setter: '',
618 readonly: true,
619 lazy: false,
620 location: 'inspect',
621 });
622 continue;
623 }
624 
625 // Wildcard property
626 m = line.match(/JSG_WILDCARD_PROPERTY\s*\(\s*(\w+)\s*\)/);
627 if (m) {
628 result.wildcardProperty = m[1];
629 continue;
630 }
631 
632 // Constants
633 m = line.match(/JSG_STATIC_CONSTANT_NAMED\s*\(\s*(\w+)\s*,\s*([^)]+)\s*\)/);
634 if (m) {
635 result.constants.push({ jsName: m[1], cppExpr: m[2].trim() });
636 continue;
637 }
638 m = line.match(/JSG_STATIC_CONSTANT\s*\(\s*(\w+)\s*\)/);
639 if (m) {
640 result.constants.push({ jsName: m[1], cppExpr: m[1] });
641 continue;
642 }
643 
644 // Nested types
645 m = line.match(/JSG_NESTED_TYPE_NAMED\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
646 if (m) {
647 result.nestedTypes.push({ cppType: m[1], jsName: m[2] });
648 continue;
649 }
650 m = line.match(/JSG_NESTED_TYPE\s*\(\s*(\w+)\s*\)/);
651 if (m) {
652 result.nestedTypes.push({ cppType: m[1], jsName: m[1] });
653 continue;
654 }
655 
656 // Iterators
657 m = line.match(/JSG_ASYNC_ITERABLE\s*\(\s*(\w+)\s*\)/);
658 if (m) {
659 result.iterators.push({ kind: '[Symbol.asyncIterator]', method: m[1] });
660 continue;
661 }
662 m = line.match(/JSG_ITERABLE\s*\(\s*(\w+)\s*\)/);
663 if (m) {
664 result.iterators.push({ kind: '[Symbol.iterator]', method: m[1] });
665 continue;
666 }
667 
668 // Dispose
669 m = line.match(/JSG_ASYNC_DISPOSE\s*\(\s*(\w+)\s*\)/);
670 if (m) {
671 result.dispose.push({ kind: '[Symbol.asyncDispose]', method: m[1] });
672 continue;
673 }
674 m = line.match(/JSG_DISPOSE\s*\(\s*(\w+)\s*\)/);
675 if (m) {
676 result.dispose.push({ kind: '[Symbol.dispose]', method: m[1] });
677 continue;
678 }
679 
680 // Serialization
681 m = line.match(/JSG_ONEWAY_SERIALIZABLE\s*\(\s*([^)]+)\s*\)/);
682 if (m) {
683 result.serialization = `One-way serializable (tag: ${m[1].trim()})`;
684 continue;
685 }
686 m = line.match(/JSG_SERIALIZABLE\s*\(\s*([^)]+)\s*\)/);
687 if (m) {
688 result.serialization = `Serializable (tag: ${m[1].trim()})`;
689 continue;
690 }
691 
692 // TypeScript overrides — these are multi-line, grab the macro name
693 if (line.includes('JSG_TS_ROOT')) {
694 result.tsOverrides.push({ kind: 'JSG_TS_ROOT', content: '(root type)' });
695 continue;
696 }
697 m = line.match(/JSG_TS_OVERRIDE\s*\((.*)$/);
698 if (m) {
699 const tsContent = gatherMultiLineMacro(lines, i);
700 result.tsOverrides.push({ kind: 'JSG_TS_OVERRIDE', content: tsContent });
701 continue;
702 }
703 m = line.match(/JSG_TS_DEFINE\s*\((.*)$/);
704 if (m) {
705 const tsContent = gatherMultiLineMacro(lines, i);
706 result.tsOverrides.push({ kind: 'JSG_TS_DEFINE', content: tsContent });
707 continue;
708 }
709 
710 // JS bundles
711 m = line.match(/JSG_CONTEXT_JS_BUNDLE\s*\(\s*(\w+)\s*\)/);
712 if (m) {
713 result.jsBundles.push(m[1]);
714 continue;
715 }
716 }
717 
718 return result;
719}
720 
721function extractBracedBlock(lines: string[], startLine: number): string | null {
722 // Find the first { on or after startLine
723 let braceStart = -1;
724 for (let i = startLine; i < Math.min(startLine + 5, lines.length); i++) {
725 if (lines[i].includes('{')) {
726 braceStart = i;
727 break;
728 }
729 }
730 if (braceStart === -1) return null;
731 
732 let depth = 0;
733 let blockLines: string[] = [];
734 for (let i = braceStart; i < lines.length; i++) {
735 for (const ch of lines[i]) {
736 if (ch === '{') depth++;
737 if (ch === '}') depth--;
738 }
739 blockLines.push(lines[i]);
740 if (depth === 0) break;
741 }
742 
743 return blockLines.join('\n');
744}
745 
746function gatherMultiLineMacro(lines: string[], startIdx: number): string {
747 // Gather lines until parens balance
748 let depth = 0;
749 let result = '';
750 for (let i = startIdx; i < lines.length; i++) {
751 const line = lines[i];
752 for (const ch of line) {
753 if (ch === '(') depth++;
754 if (ch === ')') depth--;
755 }
756 result += line.trim() + ' ';
757 if (depth <= 0) break;
758 }
759 // Clean up: extract content between outer parens
760 const m = result.match(/JSG_TS_\w+\s*\((.+)\)\s*;?\s*$/s);
761 return m ? m[1].trim() : result.trim();
762}
763 
764function isInClass(content: string, cls: string, lineIdx: number): boolean {
765 // Simple heuristic: check if there's a `class/struct <cls>` before this line
766 const lines = content.split('\n');
767 for (let i = lineIdx - 1; i >= Math.max(0, lineIdx - 100); i--) {
768 if (lines[i].match(new RegExp(`(?:class|struct)\\s+${cls}\\b`))) {
769 return true;
770 }
771 }
772 return false;
773}
774 
775function extractClassExtras(
776 content: string,
777 cls: string
778): { hasMemoryInfo: boolean; iteratorDecls: string[] } {
779 const hasMemoryInfo = content.includes(`JSG_MEMORY_INFO(${cls})`);
780 const iteratorDecls: string[] = [];
781 
782 const iteratorPatterns = [
783 /JSG_ITERATOR\s*\(\s*\w+\s*,\s*\w+\s*,/g,
784 /JSG_ASYNC_ITERATOR\s*\(\s*\w+\s*,\s*\w+\s*,/g,
785 /JSG_ITERATOR_TYPE\s*\(\s*\w+\s*,/g,
786 /JSG_ASYNC_ITERATOR_TYPE\s*\(\s*\w+\s*,/g,
787 ];
788 
789 for (const pat of iteratorPatterns) {
790 let m;
791 while ((m = pat.exec(content)) !== null) {
792 iteratorDecls.push(m[0].replace(/,\s*$/, ')'));
793 }
794 }
795 
796 return { hasMemoryInfo, iteratorDecls };
797}