File
Blob: .opencode/tools/cross-reference.ts
| 1 | import { tool } from '@opencode-ai/plugin'; |
| 2 | import path from 'path'; |
| 3 | |
| 4 | export 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 | |
| 140 | interface Hit { |
| 141 | file: string; |
| 142 | line: number; |
| 143 | text: string; |
| 144 | } |
| 145 | |
| 146 | async 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 | |
| 160 | async 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 | |
| 182 | function 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 | |
| 198 | function 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 | |
| 207 | async 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 | |
| 223 | async 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 | } |