Files
codegraph/scripts/gen-go-stdlib-methods.mjs
T
Colby MchenryandClaude Opus 5.5 8b5d1d543d chore(scripts): regenerate the Go standard-library method table from Go's source (#2760)
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 <noreply@anthropic.com>
2026-10-21 18:27:03 +00:00

189 lines
8.1 KiB
JavaScript

#!/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 <GOROOT> [--write]
*
* <GOROOT> 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 <GOROOT>/src/<import path>/*.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 <GOROOT> [--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)}`);
}