From c985f59a0d450f2f4f8a4ca5261d223c082438f4 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 21 Oct 2026 13:16:36 -0500 Subject: [PATCH] chore(scripts): regenerate the Go standard-library method table from Go's source go-stdlib-methods.ts lists the methods of 130 standard-library types with their parameter and result counts. Its generator lived in a validation kit. scripts/gen-go-stdlib-methods.mjs reads a Go source tree with the engine's Go grammar, counts with the engine's own goSignatureArity, and prints (or --write's) the table for the types it lists, so a later Go release's methods are one command away. Against go1.27.3 it reproduces the table byte for byte. Co-Authored-By: Claude Opus 5.5 --- scripts/gen-go-stdlib-methods.mjs | 188 ++++++++++++++++++++++++++++ src/resolution/go-stdlib-methods.ts | 11 +- 2 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 scripts/gen-go-stdlib-methods.mjs diff --git a/scripts/gen-go-stdlib-methods.mjs b/scripts/gen-go-stdlib-methods.mjs new file mode 100644 index 000000000..88f672b7a --- /dev/null +++ b/scripts/gen-go-stdlib-methods.mjs @@ -0,0 +1,188 @@ +#!/usr/bin/env node +/** + * Regenerate the table in src/resolution/go-stdlib-methods.ts from Go's own + * source: + * + * npm run build + * node scripts/gen-go-stdlib-methods.mjs [--write] + * + * is a Go source tree (a release's checkout of golang/go, say). For + * every type the table lists now — add or drop one by editing the table, then + * rerun — the script reads /src//*.go, tests skipped, + * with the engine's Go grammar, and lists each method as + * `name:params/results`: + * - an interface's methods, and those of the interfaces it embeds; + * - a struct's or other named type's (receiver T or *T), and those its + * embedded fields promote; + * - an alias's, its target's. + * The counts come from the engine's own goSignatureArity, so a table method + * is held to what the arity gate reads off a project method's stored + * signature. Exported names are kept, and an unexported one only where a + * listed interface requires it (`testing.TB`'s `private`). + * + * Prints the table body; --write puts it in the module instead. Exits 1 when + * a listed type isn't found or a signature doesn't read. + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const MODULE = path.join(ROOT, 'src', 'resolution', 'go-stdlib-methods.ts'); + +const goRoot = process.argv[2]; +const write = process.argv.includes('--write'); +if (!goRoot || !fs.existsSync(path.join(goRoot, 'src', 'io'))) { + console.error('usage: gen-go-stdlib-methods.mjs [--write] (a Go source tree, with src/io)'); + process.exit(2); +} +const wasm = path.join(ROOT, 'dist', 'extraction', 'wasm', 'tree-sitter-go.wasm'); +if (!fs.existsSync(wasm)) { + console.error('dist/ is missing: run `npm run build` first'); + process.exit(2); +} +const { goSignatureArity } = await import(pathToFileURL(path.join(ROOT, 'dist', 'resolution', 'callback-synthesizer.js')).href); +const { Parser, Language } = createRequire(path.join(ROOT, 'package.json'))('web-tree-sitter'); + +// The types the table lists, in its order. +const moduleText = fs.readFileSync(MODULE, 'utf8'); +const open = moduleText.indexOf('{', moduleText.indexOf('const GO_STDLIB_METHODS')); +const close = moduleText.indexOf('\n};', open); +const listed = new Function(`return ${moduleText.slice(open, close + 2)}`)(); + +await Parser.init(); +const parser = new Parser(); +parser.setLanguage(await Language.load(wasm)); + +const signature = (node) => { + const params = node.childForFieldName('parameters')?.text; + const result = node.childForFieldName('result')?.text; + return params && result ? `${params} ${result}` : params; +}; + +/** A package's top-level type declarations and the methods declared on each. */ +const packages = new Map(); +function pkg(importPath) { + let found = packages.get(importPath); + if (found) return found; + found = { types: new Map(), methods: new Map() }; + packages.set(importPath, found); + const dir = path.join(goRoot, 'src', importPath); + const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith('.go') && !f.endsWith('_test.go')) : []; + for (const file of files) { + const tree = parser.parse(fs.readFileSync(path.join(dir, file), 'utf8')); + const imports = new Map(); + for (const spec of tree.rootNode.descendantsOfType('import_spec')) { + const ip = spec.childForFieldName('path')?.text.replace(/^"|"$/g, ''); + if (ip) imports.set(spec.childForFieldName('name')?.text ?? ip.split('/').pop(), ip); + } + for (const spec of tree.rootNode.descendantsOfType(['type_spec', 'type_alias'])) { + if (spec.parent?.type !== 'type_declaration' || spec.parent.parent?.type !== 'source_file') continue; + const name = spec.childForFieldName('name')?.text; + if (name && !found.types.has(name)) { + found.types.set(name, { type: spec.childForFieldName('type'), imports, alias: spec.type === 'type_alias' }); + } + } + for (const m of tree.rootNode.namedChildren.filter((c) => c.type === 'method_declaration')) { + const receiver = /\(\s*(?:\w+\s+)?\*?\s*(\w+)/.exec(m.childForFieldName('receiver')?.text ?? '')?.[1]; + const name = m.childForFieldName('name')?.text; + if (!receiver || !name) continue; + const methods = found.methods.get(receiver) ?? found.methods.set(receiver, new Map()).get(receiver); + if (!methods.has(name)) methods.set(name, goSignatureArity(signature(m))); + } + } + return found; +} + +/** Method name → `params/results` for the type `name` the package at `importPath` declares. */ +function methodsOf(importPath, name, seen = new Set()) { + const methods = new Map(); + const key = `${importPath}.${name}`; + if (seen.has(key)) return methods; + seen.add(key); + const p = pkg(importPath); + for (const [m, arity] of p.methods.get(name) ?? []) methods.set(m, arity); + const declared = p.types.get(name); + if (!declared) return methods; + const take = (more) => { + for (const [m, arity] of more) if (!methods.has(m)) methods.set(m, arity); + }; + const embedded = (type, imports) => { + if (type?.type === 'pointer_type') type = type.namedChildren[0]; + if (type?.type === 'generic_type') type = type.childForFieldName('type'); + if (type?.type === 'qualified_type') { + take(methodsOf(imports.get(type.childForFieldName('package').text), type.childForFieldName('name').text, seen)); + } else if (type?.type === 'type_identifier') { + if (type.text === 'error') take(new Map([['Error', '0/1']])); + else take(methodsOf(importPath, type.text, seen)); + } + }; + const type = declared.type; + if (type?.type === 'interface_type') { + for (const element of type.namedChildren) { + if (element.type === 'method_elem') methods.set(element.childForFieldName('name').text, goSignatureArity(signature(element))); + else if (element.type === 'type_elem') { + const terms = element.namedChildren.filter((c) => c.type !== 'comment'); + if (terms.length === 1) embedded(terms[0], declared.imports); + } + } + } else if (type?.type === 'struct_type') { + const fields = type.namedChildren.find((c) => c.type === 'field_declaration_list'); + for (const field of fields?.namedChildren.filter((c) => c.type === 'field_declaration') ?? []) { + if (!field.namedChildren.some((c) => c.type === 'field_identifier')) embedded(field.childForFieldName('type'), declared.imports); + } + } else if (declared.alias) { + embedded(type, declared.imports); + } + return methods; +} + +const EXPORTED = /^\p{Lu}/u; +const generated = []; +const required = new Set(); +let failed = false; +for (const [importPath, types] of Object.entries(listed)) { + for (const name of Object.keys(types)) { + const declared = pkg(importPath).types.get(name); + if (!declared) { + console.error(`not found: ${importPath}.${name}`); + failed = true; + continue; + } + const methods = methodsOf(importPath, name); + generated.push({ importPath, name, methods }); + if (declared.type?.type === 'interface_type') for (const m of methods.keys()) if (!EXPORTED.test(m)) required.add(m); + } +} + +const lines = []; +let current = null; +for (const { importPath, name, methods } of generated) { + if (importPath !== current) { + if (current !== null) lines.push(' },'); + lines.push(` ${/^[a-z]+$/.test(importPath) ? importPath : `'${importPath}'`}: {`); + current = importPath; + } + const kept = [...methods].filter(([m]) => EXPORTED.test(m) || required.has(m)).sort(([a], [b]) => (a < b ? -1 : 1)); + for (const [m, arity] of kept) { + if (arity === null) { + console.error(`signature doesn't read: ${importPath}.${name}.${m}`); + failed = true; + } + } + lines.push(` ${name}: '${kept.map(([m, arity]) => `${m}:${arity}`).join(' ')}',`); +} +lines.push(' },'); +if (failed) process.exit(1); + +if (!write) { + console.log(lines.join('\n')); +} else { + const eol = moduleText.includes('\r\n') ? '\r\n' : '\n'; + const start = moduleText.indexOf('\n', open) + 1; + const end = close + 1; + fs.writeFileSync(MODULE, moduleText.slice(0, start) + lines.join(eol) + eol + moduleText.slice(end)); + console.error(`wrote ${generated.length} types to ${path.relative(ROOT, MODULE)}`); +} diff --git a/src/resolution/go-stdlib-methods.ts b/src/resolution/go-stdlib-methods.ts index a2c85387a..8e9f10e53 100644 --- a/src/resolution/go-stdlib-methods.ts +++ b/src/resolution/go-stdlib-methods.ts @@ -14,11 +14,12 @@ * `testing.TB`'s `private` and `reflect.Type`'s `common` and `uncommon` are * why only an embedding satisfies those outside their own package. * - * Read from Go's own source (go1.27.3). The interfaces are the ones projects - * embed; the structs are the commonly embedded ones that implement one of - * them. A struct whose methods would answer one of these interfaces' names - * with another signature (`http.Header.Write`, `sync.WaitGroup.Done`) is left - * out. + * Read from Go's own source (go1.27.3) by scripts/gen-go-stdlib-methods.mjs, + * which regenerates the table for the types it lists. The interfaces are the + * ones projects embed; the structs are the commonly embedded ones that + * implement one of them. A struct whose methods would answer one of these + * interfaces' names with another signature (`http.Header.Write`, + * `sync.WaitGroup.Done`) is left out. */ const GO_STDLIB_METHODS: Readonly>>> = { io: {