| // 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 |
| } |