// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
// +build ignore
// The generate command reads all the GOROOT/api/go1.*.txt files and
// generates a single combined manifest.go file containing the Go
// standard library API symbols along with versions.
package main
import (
"bytes"
"cmp"
"errors"
"fmt"
"go/format"
"go/types"
"io/fs"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
"golang.org/x/tools/go/packages"
)
func main() {
pkgs := make(map[string]map[string]symInfo) // package -> symbol -> info
symRE := regexp.MustCompile(`^pkg (\S+).*?, (var|func|type|const|method \([^)]*\)) ([\pL\p{Nd}_]+)(.*)`)
// parse parses symbols out of GOROOT/api/*.txt data, with the specified minor version.
// Errors are reported against filename.
parse := func(filename string, data []byte, minor int) {
for linenum, line := range strings.Split(string(data), "\n") {
if line == "" || strings.HasPrefix(line, "#") {
continue
}
m := symRE.FindStringSubmatch(line)
if m == nil {
log.Fatalf("invalid input: %s:%d: %s", filename, linenum+1, line)
}
path, kind, sym, rest := m[1], m[2], m[3], m[4]
if _, recv, ok := strings.Cut(kind, "method "); ok {
// e.g. "method (*Func) Pos() token.Pos"
kind = "method"
recv := removeTypeParam(recv) // (*Foo[T]) -> (*Foo)
sym = recv + "." + sym // (*T).m
} else if _, field, ok := strings.Cut(rest, " struct, "); ok && kind == "type" {
// e.g. "type ParenExpr struct, Lparen token.Pos"
kind = "field"
name, typ, _ := strings.Cut(field, " ")
// The api script uses the name
// "embedded" (ambiguously) for
// the name of an anonymous field.
if name == "embedded" {
// Strip "*pkg.T" down to "T".
typ = strings.TrimPrefix(typ, "*")
if _, after, ok := strings.Cut(typ, "."); ok {
typ = after
}
typ = removeTypeParam(typ) // embedded Foo[T] -> Foo
name = typ
}
sym += "." + name // T.f
}
symbols, ok := pkgs[path]
if !ok {
symbols = make(map[string]symInfo)
pkgs[path] = symbols
}
// Don't overwrite earlier entries:
// enums are redeclared in later versions
// as their encoding changes;
// deprecations count as updates too.
if _, ok := symbols[sym]; !ok {
symbols[sym] = symInfo{kind, minor}
}
}
}
// Read and parse the GOROOT/api manifests.
for minor := 0; ; minor++ {
base := "go1.txt"
if minor > 0 {
base = fmt.Sprintf("go1.%d.txt", minor)
}
filename := filepath.Join(runtime.GOROOT(), "api", base)
data, err := os.ReadFile(filename)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// All caught up.
// Synthesize one final file from any api/next/*.txt fragments.
// (They are consolidated into a go1.%d file some time between
// the freeze and the first release candidate.)
filenames, err := filepath.Glob(filepath.Join(runtime.GOROOT(), "api/next/*.txt"))
if err != nil {
log.Fatal(err)
}
var next bytes.Buffer
for _, filename := range filenames {
data, err := os.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
next.Write(data)
}
parse(filename, next.Bytes(), minor) // (filename is a lie)
break
}
log.Fatal(err)
}
parse(filename, data, minor)
}
// The APIs of the syscall/js and unsafe packages need to be computed explicitly,
// because they're not included in the GOROOT/api/go1.*.txt files at this time.
pkgs["syscall/js"] = loadSymbols("syscall/js", "GOOS=js", "GOARCH=wasm")
pkgs["unsafe"] = exportedSymbols(types.Unsafe) // TODO(adonovan): set correct versions
// Write the combined manifest.
var buf bytes.Buffer
buf.WriteString(`// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by generate.go. DO NOT EDIT.
package stdlib
var PackageSymbols = map[string][]Symbol{
`)
for _, path := range sortedKeys(pkgs) {
pkg := pkgs[path]
fmt.Fprintf(&buf, "\t%q: {\n", path)
for _, name := range sortedKeys(pkg) {
info := pkg[name]
fmt.Fprintf(&buf, "\t\t{%q, %s, %d},\n",
name, strings.Title(info.kind), info.minor)
}
fmt.Fprintln(&buf, "},")
}
fmt.Fprintln(&buf, "}")
fmtbuf, err := format.Source(buf.Bytes())
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("manifest.go", fmtbuf, 0666); err != nil {
log.Fatal(err)
}
}
type symInfo struct {
kind string // e.g. "func"
minor int // go1.%d
}
// loadSymbols computes the exported symbols in the specified package
// by parsing and type-checking the current source.
func loadSymbols(pkg string, extraEnv ...string) map[string]symInfo {
pkgs, err := packages.Load(&packages.Config{
Mode: packages.NeedTypes,
Env: append(os.Environ(), extraEnv...),
}, pkg)
if err != nil {
log.Fatalln(err)
} else if len(pkgs) != 1 {
log.Fatalf("got %d packages, want one package %q", len(pkgs), pkg)
}
return exportedSymbols(pkgs[0].Types)
}
func exportedSymbols(pkg *types.Package) map[string]symInfo {
symbols := make(map[string]symInfo)
for _, name := range pkg.Scope().Names() {
if obj := pkg.Scope().Lookup(name); obj.Exported() {
var kind string
switch obj.(type) {
case *types.Func, *types.Builtin:
kind = "func"
case *types.Const:
kind = "const"
case *types.Var:
kind = "var"
case *types.TypeName:
kind = "type"
// TODO(adonovan): expand fields and methods of syscall/js.*
default:
log.Fatalf("unexpected object type: %v", obj)
}
symbols[name] = symInfo{kind: kind, minor: 0} // pretend go1.0
}
}
return symbols
}
func sortedKeys[M ~map[K]V, K cmp.Ordered, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
slices.Sort(r)
return r
}
func removeTypeParam(s string) string {
i := strings.IndexByte(s, '[')
j := strings.LastIndexByte(s, ']')
if i > 0 && j > i {
s = s[:i] + s[j+len("["):]
}
return s
}