blob: 25afc716c074f08748fbdadee650e326091f0ff7 [file] [log] [blame]
// Copyright 2021 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.
//
// This file caches information about which standard library types, methods,
// and functions appeared in what version of Go
//
// Copied from
// https://go.googlesource.com/tools/+/5ab06b02d60653d5a5220fb2d99064055da3bdbd/godoc/versions.go
// with these modifications.
package symbol
import (
"bufio"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"unicode"
"golang.org/x/mod/semver"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/stdlib"
)
// ParseAPIInfo parses apiVersions using contents of the specified directory.
func ParsePackageAPIInfo(files []string) (_ apiVersions, err error) {
defer derrors.Wrap(&err, "ParsePackageAPIInfo")
// Process files in reverse semver order (vx.y.z, vz.y.z-1, ...).
//
// The signature of an identifier may change
// (for example, a function that accepts a type replaced with
// an alias), and so an existing symbol may show up again in
// a later api/vX.Y.Z.txt file. Parsing in reverse version
// order means we end up with the earliest version of Go
// when the symbol was added. See golang.org/issue/44081.
//
ver := func(name string) string {
base := filepath.Base(name)
v := strings.TrimSuffix(base, ".txt")
if strings.HasPrefix(base, "go") {
// stdlib files have the structure goN.txt.
// Get the semantic version.
v = stdlib.VersionForTag(v)
}
return v
}
sort.Slice(files, func(i, j int) bool {
return semver.Compare(ver(files[i]), ver(files[j])) > 0
})
vp := new(versionParser)
for _, f := range files {
if err := vp.parseFile(f); err != nil {
return nil, err
}
}
if len(vp.res) == 0 {
return nil, fmt.Errorf("apiVersions should not be empty")
}
return vp.res, nil
}
// LoadAPIFiles loads data about the API for the given package from dir.
func LoadAPIFiles(pkgPath, dir string) ([]string, error) {
var apiGlob string
if stdlib.Contains(pkgPath) {
apiGlob = filepath.Join(filepath.Clean(runtime.GOROOT()), "api", "go*.txt")
} else {
apiGlob = filepath.Join(dir, pkgPath, "v*.txt")
}
files, err := filepath.Glob(apiGlob)
if err != nil {
return nil, err
}
if len(files) == 0 {
return nil, fmt.Errorf("no files matching %q", apiGlob)
}
return files, nil
}
// apiVersions is a map of packages to information about those packages'
// symbols and when they were added to Go.
//
// Only things added after Go1 are tracked. Version strings are of the
// form "1.1", "1.2", etc.
type apiVersions map[string]pkgAPIVersions // keyed by Go package ("net/http")
// pkgAPIVersions contains information about which version of Go added
// certain package symbols.
//
// Only things added after Go1 are tracked. Version strings are of the
// form "1.1", "1.2", etc.
type pkgAPIVersions struct {
constSince map[string]string
varSince map[string]string
typeSince map[string]string // "Server" -> "1.7"
methodSince map[string]map[string]string // "*Server" ->"Shutdown"->1.8
funcSince map[string]string // "NewServer" -> "1.7"
fieldSince map[string]map[string]string // "ClientTrace" -> "Got1xxResponse" -> "1.11"
}
// versionedRow represents an API feature, a parsed line of a
// $GOROOT/api/go.*txt file.
type versionedRow struct {
pkg string // "net/http"
kind string // "type", "func", "method", "field" TODO: "const", "var"
recv string // for methods, the receiver type ("Server", "*Server")
name string // name of type, (struct) field, func, method
structName string // for struct fields, the outer struct name
}
// versionParser parses $GOROOT/api/go*.txt files and stores them in in its rows field.
type versionParser struct {
res apiVersions // initialized lazily
}
// parseFile parses the named <apidata>/VERSION.txt file.
//
// For each row, it updates the corresponding entry in
// vp.res to VERSION, overwriting any previous value.
func (vp *versionParser) parseFile(filename string) (err error) {
defer derrors.Wrap(&err, "parseFile(%q)", filename)
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
base := filepath.Base(filename)
ver := strings.TrimSuffix(base, ".txt")
sc := bufio.NewScanner(f)
for sc.Scan() {
row, ok := parseRow(sc.Text())
if !ok {
continue
}
if vp.res == nil {
vp.res = make(apiVersions)
}
pkgi, ok := vp.res[row.pkg]
if !ok {
pkgi = pkgAPIVersions{
constSince: make(map[string]string),
varSince: make(map[string]string),
typeSince: make(map[string]string),
methodSince: make(map[string]map[string]string),
funcSince: make(map[string]string),
fieldSince: make(map[string]map[string]string),
}
vp.res[row.pkg] = pkgi
}
switch row.kind {
case "const":
pkgi.constSince[row.name] = ver
case "var":
pkgi.varSince[row.name] = ver
case "func":
pkgi.funcSince[row.name] = ver
case "type":
pkgi.typeSince[row.name] = ver
case "method":
if _, ok := pkgi.methodSince[row.recv]; !ok {
pkgi.methodSince[row.recv] = make(map[string]string)
}
pkgi.methodSince[row.recv][row.name] = ver
case "field":
if _, ok := pkgi.fieldSince[row.structName]; !ok {
pkgi.fieldSince[row.structName] = make(map[string]string)
}
pkgi.fieldSince[row.structName][row.name] = ver
}
}
return sc.Err()
}
func parseRow(s string) (vr versionedRow, ok bool) {
if !strings.HasPrefix(s, "pkg ") {
// Skip comments, blank lines, etc.
return
}
rest := s[len("pkg "):]
endPkg := strings.IndexFunc(rest, func(r rune) bool {
return !(unicode.IsLetter(r) || r == '.' || r == '/' || r == '-' || unicode.IsDigit(r))
})
if endPkg == -1 {
return
}
vr.pkg, rest = rest[:endPkg], rest[endPkg:]
if !strings.HasPrefix(rest, ", ") {
// If the part after the pkg name isn't ", ", then it's a OS/ARCH-dependent line of the form:
// pkg syscall (darwin-amd64), const ImplementsGetwd = false
// We skip those for now.
return
}
rest = rest[len(", "):]
switch {
case strings.HasPrefix(rest, "type "):
rest = rest[len("type "):]
sp := strings.IndexByte(rest, ' ')
if sp == -1 {
return
}
vr.name, rest = rest[:sp], rest[sp+1:]
switch {
case strings.HasPrefix(rest, "struct, "):
rest = rest[len("struct, "):]
if i := strings.IndexByte(rest, ' '); i != -1 {
vr.kind = "field"
vr.structName = vr.name
vr.name = rest[:i]
return vr, true
}
case strings.HasPrefix(rest, "interface, "):
rest = rest[len("interface, "):]
if i := strings.IndexByte(rest, '('); i != -1 {
vr.kind = "method"
vr.recv = vr.name
vr.name = rest[:i]
return vr, true
}
default:
vr.kind = "type"
return vr, true
}
case strings.HasPrefix(rest, "const "):
vr.kind = "const"
rest = rest[len("const "):]
if i := strings.IndexByte(rest, ' '); i != -1 {
vr.name = rest[:i]
return vr, true
}
case strings.HasPrefix(rest, "var "):
vr.kind = "var"
rest = rest[len("var "):]
if i := strings.IndexByte(rest, ' '); i != -1 {
vr.name = rest[:i]
return vr, true
}
case strings.HasPrefix(rest, "func "):
vr.kind = "func"
rest = rest[len("func "):]
if i := strings.IndexByte(rest, '('); i != -1 {
vr.name = rest[:i]
return vr, true
}
case strings.HasPrefix(rest, "method "): // "method (*File) SetModTime(time.Time)"
vr.kind = "method"
rest = rest[len("method "):] // "(*File) SetModTime(time.Time)"
sp := strings.IndexByte(rest, ' ')
if sp == -1 {
return
}
vr.recv = strings.Trim(rest[:sp], "()") // "*File"
vr.recv = strings.TrimPrefix(vr.recv, "*") // "File"
rest = rest[sp+1:] // SetMode(os.FileMode)
paren := strings.IndexByte(rest, '(')
if paren == -1 {
return
}
vr.name = rest[:paren]
return vr, true
}
return // TODO: handle more cases
}