Skip to content
File

Blob: .opencode/tools/cross-reference.ts

typescript288 lines
1import { tool } from '@opencode-ai/plugin';
2import path from 'path';
3 
4export default tool({
5 description:
6 'One-shot cross-reference lookup for a C++ class or symbol in the workerd codebase. ' +
7 'Returns header location, implementation file, JSG registration (methods/properties), ' +
8 'EW_*_ISOLATE_TYPES group, test files, and compat flag gating — all in a single call. ' +
9 'Saves 4-6 separate grep/read calls.',
10 args: {
11 symbol: tool.schema
12 .string()
13 .describe(
14 "Class or symbol name (e.g., 'ReadableStream', 'SubtleCrypto', 'AbortSignal')"
15 ),
16 },
17 async execute(args, ctx) {
18 const root = ctx.worktree || ctx.directory;
19 const sym = args.symbol;
20 const srcDir = path.join(root, 'src');
21 
22 // Run all searches in parallel
23 const [
24 headerHits,
25 implHits,
26 jsgTypeHits,
27 testHits,
28 compatFlagHits,
29 isolateTypeMacro,
30 ] = await Promise.all([
31 grepFiles(srcDir, `class ${sym}\\b`, '*.h'),
32 grepFiles(srcDir, `${sym}::`, '*.c++'),
33 grepFiles(srcDir, `JSG_RESOURCE_TYPE.${sym}`, '*.h'),
34 grepTestFiles(srcDir, sym),
35 grepFiles(srcDir, `flags.get.*().*${sym}\\|${sym}.*flags.get`, '*.h'),
36 findIsolateTypeMacro(srcDir, sym),
37 ]);
38 
39 // Extract JSG methods if we found the registration
40 let jsgMethods: string[] = [];
41 if (jsgTypeHits.length > 0) {
42 jsgMethods = await extractJsgMethods(
43 path.join(root, jsgTypeHits[0].file)
44 );
45 }
46 
47 // Build output
48 let out = `## Cross-reference: ${sym}\n\n`;
49 
50 // Header
51 const primaryHeader = findPrimary(headerHits, (text) =>
52 /class\s+\w+\s*(final\s*)?[:{]/.test(text)
53 );
54 
55 if (primaryHeader) {
56 out += `### Declaration\n\n`;
57 out += `- \`${primaryHeader.file}:${primaryHeader.line}\`: ${primaryHeader.text.trim()}\n`;
58 const others = headerHits.filter(
59 (h) => !(h.file === primaryHeader.file && h.line === primaryHeader.line)
60 );
61 if (others.length > 0) {
62 out += `- Also referenced in: ${others
63 .slice(0, 5)
64 .map((h) => `\`${h.file}:${h.line}\``)
65 .join(', ')}`;
66 if (others.length > 5) out += ` (+${others.length - 5} more)`;
67 out += '\n';
68 }
69 } else if (headerHits.length > 0) {
70 out += `### Header references\n\n`;
71 for (const h of headerHits.slice(0, 5)) {
72 out += `- \`${h.file}:${h.line}\`: ${h.text.trim()}\n`;
73 }
74 } else {
75 out += `### Declaration\n\nNot found in any .h file.\n`;
76 }
77 
78 // Implementation
79 out += `\n### Implementation\n\n`;
80 if (implHits.length > 0) {
81 const implFiles = [...new Set(implHits.map((h) => h.file))];
82 for (const f of implFiles.slice(0, 5)) {
83 const count = implHits.filter((h) => h.file === f).length;
84 out += `- \`${f}\` (${count} references)\n`;
85 }
86 } else {
87 out += `No .c++ files reference \`${sym}::\`.\n`;
88 }
89 
90 // JSG registration
91 out += `\n### JSG Registration\n\n`;
92 if (jsgTypeHits.length > 0) {
93 out += `- Registered in \`${jsgTypeHits[0].file}:${jsgTypeHits[0].line}\`\n`;
94 if (jsgMethods.length > 0) {
95 out += `- Methods/properties:\n`;
96 for (const m of jsgMethods) {
97 out += ` - \`${m}\`\n`;
98 }
99 }
100 } else {
101 out += `No \`JSG_RESOURCE_TYPE(${sym})\` found.\n`;
102 }
103 
104 // Isolate type group
105 out += `\n### Type Registration\n\n`;
106 if (isolateTypeMacro) {
107 out += `- Part of \`${isolateTypeMacro.macro}\` (defined in \`${isolateTypeMacro.file}:${isolateTypeMacro.line}\`)\n`;
108 } else {
109 out += `Not found in any \`EW_*_ISOLATE_TYPES\` macro.\n`;
110 }
111 
112 // Compat flag gating
113 if (compatFlagHits.length > 0) {
114 out += `\n### Compat Flag Gating\n\n`;
115 for (const h of compatFlagHits.slice(0, 5)) {
116 out += `- \`${h.file}:${h.line}\`: ${h.text.trim()}\n`;
117 }
118 }
119 
120 // Tests
121 out += `\n### Tests\n\n`;
122 if (testHits.length > 0) {
123 const testFiles = [...new Set(testHits.map((h) => h.file))];
124 for (const f of testFiles.slice(0, 10)) {
125 out += `- \`${f}\`\n`;
126 }
127 if (testFiles.length > 10) {
128 out += `- ... and ${testFiles.length - 10} more\n`;
129 }
130 } else {
131 out += `No test files found referencing \`${sym}\`.\n`;
132 }
133 
134 return out;
135 },
136});
137 
138// --- grep helpers ---
139 
140interface Hit {
141 file: string;
142 line: number;
143 text: string;
144}
145 
146async function grepFiles(
147 searchDir: string,
148 pattern: string,
149 includeGlob: string
150): Promise<Hit[]> {
151 try {
152 const output =
153 await Bun.$`grep -rnE --include=${includeGlob} -m 50 ${pattern} ${searchDir}`.text();
154 return parseGrepOutput(output, path.dirname(searchDir)); // root = parent of src/
155 } catch {
156 return [];
157 }
158}
159 
160async function grepTestFiles(
161 searchDir: string,
162 symbol: string
163): Promise<Hit[]> {
164 try {
165 // Match test files by name patterns
166 const output =
167 await Bun.$`grep -rnl --include='*test*.js' --include='*test*.ts' --include='*test*.c++' --include='*.wd-test' -m 50 ${symbol} ${searchDir}`.text();
168 const root = path.dirname(searchDir);
169 return output
170 .split('\n')
171 .filter(Boolean)
172 .map((f) => ({
173 file: path.relative(root, f.trim()),
174 line: 0,
175 text: '',
176 }));
177 } catch {
178 return [];
179 }
180}
181 
182function parseGrepOutput(output: string, root: string): Hit[] {
183 const hits: Hit[] = [];
184 for (const line of output.split('\n')) {
185 if (!line.trim()) continue;
186 const m = line.match(/^(.+?):(\d+):(.*)$/);
187 if (m) {
188 hits.push({
189 file: path.relative(root, m[1]),
190 line: parseInt(m[2], 10),
191 text: m[3],
192 });
193 }
194 }
195 return hits;
196}
197 
198function findPrimary(
199 hits: Hit[],
200 predicate: (text: string) => boolean
201): Hit | null {
202 return hits.find((h) => predicate(h.text)) || hits[0] || null;
203}
204 
205// --- JSG method extraction ---
206 
207async function extractJsgMethods(headerPath: string): Promise<string[]> {
208 try {
209 const output =
210 await Bun.$`grep -E 'JSG_METHOD|JSG_READONLY_PROTOTYPE_PROPERTY|JSG_LAZY_INSTANCE_PROPERTY|JSG_STATIC_METHOD|JSG_NESTED_TYPE|JSG_INHERIT|JSG_TS_' ${headerPath}`.text();
211 return output
212 .split('\n')
213 .map((l) => l.trim())
214 .filter((l) => l.length > 0)
215 .slice(0, 30);
216 } catch {
217 return [];
218 }
219}
220 
221// --- Isolate type macro lookup ---
222 
223async function findIsolateTypeMacro(
224 searchDir: string,
225 symbol: string
226): Promise<{ macro: string; file: string; line: number } | null> {
227 const root = path.dirname(searchDir);
228 try {
229 // Find files that define EW_*_ISOLATE_TYPES macros
230 const macroFiles =
231 await Bun.$`grep -rnl --include='*.h' 'EW_.*_ISOLATE_TYPES' ${searchDir}`.text();
232 
233 for (const file of macroFiles.split('\n').filter(Boolean)) {
234 const filePath = file.trim();
235 
236 // Check if this file contains both the macro definition and the symbol
237 let content: string;
238 try {
239 content = await Bun.$`grep -n ${symbol} ${filePath}`.text();
240 } catch {
241 continue; // symbol not in this file
242 }
243 if (!content.trim()) continue;
244 
245 // Find the EW_*_ISOLATE_TYPES definition line
246 let macroDef: string;
247 try {
248 macroDef =
249 await Bun.$`grep -n 'EW_.*_ISOLATE_TYPES' ${filePath}`.text();
250 } catch {
251 continue;
252 }
253 
254 for (const defLine of macroDef.split('\n')) {
255 const dm = defLine.match(/^(\d+):.*(EW_\w+_ISOLATE_TYPES)/);
256 if (!dm) continue;
257 
258 // Check if symbol appears on the same line (inline macro)
259 if (defLine.includes(symbol)) {
260 return {
261 macro: dm[2],
262 file: path.relative(root, filePath),
263 line: parseInt(dm[1], 10),
264 };
265 }
266 
267 // Check continuation lines: symbol within 80 lines of macro def
268 const macroLine = parseInt(dm[1], 10);
269 for (const symLine of content.split('\n')) {
270 const sm = symLine.match(/^(\d+):/);
271 if (!sm) continue;
272 const symLineNum = parseInt(sm[1], 10);
273 if (symLineNum >= macroLine && symLineNum <= macroLine + 80) {
274 return {
275 macro: dm[2],
276 file: path.relative(root, filePath),
277 line: macroLine,
278 };
279 }
280 }
281 }
282 }
283 } catch {
284 // grep failed
285 }
286 return null;
287}