| // Copyright 2018 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 | |
| package api | |
| import ( | |
| "bufio" | |
| "io/fs" | |
| "path" | |
| "path/filepath" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "unicode" | |
| ) | |
| // DB 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 DB map[string]PkgDB // keyed by Go package ("net/http") | |
| // PkgDB 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 PkgDB struct { | |
| Type map[string]string // "Server" -> "1.7" | |
| Method map[string]map[string]string // "*Server" ->"Shutdown"->1.8 | |
| Func map[string]string // "NewServer" -> "1.7" | |
| Field map[string]map[string]string // "ClientTrace" -> "Got1xxResponse" -> "1.11" | |
| } | |
| // Func returns a string (such as "1.7") specifying which Go | |
| // version introduced a symbol, unless it was introduced in Go1, in | |
| // which case it returns the empty string. | |
| // | |
| // The kind is one of "type", "method", or "func". | |
| // | |
| // The receiver is only used for "methods" and specifies the receiver type, | |
| // such as "*Server". | |
| // | |
| // The name is the symbol name ("Server") and the pkg is the package | |
| // ("net/http"). | |
| func (v DB) Func(pkg, kind, receiver, name string) string { | |
| pv := v[pkg] | |
| switch kind { | |
| case "func": | |
| return pv.Func[name] | |
| case "type": | |
| return pv.Type[name] | |
| case "method": | |
| return pv.Method[receiver][name] | |
| } | |
| return "" | |
| } | |
| // Load loads a database from fsys's api/go*.txt files. | |
| // Typically, fsys should be the root of a Go repository (a $GOROOT). | |
| func Load(fsys fs.FS) (DB, error) { | |
| files, err := fs.Glob(fsys, "api/go*.txt") | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Process files in go1.n, go1.n-1, ..., go1.2, go1.1, go1 order. | |
| // | |
| // It's rare, but 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/go1.N.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) int { | |
| base := path.Base(name) | |
| ver := strings.TrimPrefix(strings.TrimSuffix(base, ".txt"), "go1.") | |
| if ver == "go1" { | |
| return 0 | |
| } | |
| v, _ := strconv.Atoi(ver) | |
| return v | |
| } | |
| sort.Slice(files, func(i, j int) bool { return ver(files[i]) > ver(files[j]) }) | |
| vp := new(parser) | |
| for _, f := range files { | |
| if err := vp.parseFile(fsys, f); err != nil { | |
| return nil, err | |
| } | |
| } | |
| return vp.res, nil | |
| } | |
| // parser parses $GOROOT/api/go*.txt files and stores them in its rows field. | |
| type parser struct { | |
| res DB // initialized lazily | |
| } | |
| // parseFile parses the named $GOROOT/api/goVERSION.txt file. | |
| // | |
| // For each row, it updates the corresponding entry in | |
| // vp.res to VERSION, overwriting any previous value. | |
| // As a special case, if goVERSION is "go1", it deletes | |
| // from the map instead. | |
| func (vp *parser) parseFile(fsys fs.FS, name string) error { | |
| f, err := fsys.Open(name) | |
| if err != nil { | |
| return err | |
| } | |
| defer f.Close() | |
| base := filepath.Base(name) | |
| ver := strings.TrimPrefix(strings.TrimSuffix(base, ".txt"), "go") | |
| sc := bufio.NewScanner(f) | |
| for sc.Scan() { | |
| row, ok := parseRow(sc.Text()) | |
| if !ok { | |
| continue | |
| } | |
| if vp.res == nil { | |
| vp.res = make(DB) | |
| } | |
| pkgi, ok := vp.res[row.pkg] | |
| if !ok { | |
| pkgi = PkgDB{ | |
| Type: make(map[string]string), | |
| Method: make(map[string]map[string]string), | |
| Func: make(map[string]string), | |
| Field: make(map[string]map[string]string), | |
| } | |
| vp.res[row.pkg] = pkgi | |
| } | |
| switch row.kind { | |
| case "func": | |
| if ver == "1" { | |
| delete(pkgi.Func, row.name) | |
| break | |
| } | |
| pkgi.Func[row.name] = ver | |
| case "type": | |
| if ver == "1" { | |
| delete(pkgi.Type, row.name) | |
| break | |
| } | |
| pkgi.Type[row.name] = ver | |
| case "method": | |
| if ver == "1" { | |
| delete(pkgi.Method[row.recv], row.name) | |
| break | |
| } | |
| if _, ok := pkgi.Method[row.recv]; !ok { | |
| pkgi.Method[row.recv] = make(map[string]string) | |
| } | |
| pkgi.Method[row.recv][row.name] = ver | |
| case "field": | |
| if ver == "1" { | |
| delete(pkgi.Field[row.structName], row.name) | |
| break | |
| } | |
| if _, ok := pkgi.Field[row.structName]; !ok { | |
| pkgi.Field[row.structName] = make(map[string]string) | |
| } | |
| pkgi.Field[row.structName][row.name] = ver | |
| } | |
| } | |
| return sc.Err() | |
| } | |
| // row represents an API feature, a parsed line of a | |
| // $GOROOT/api/go.*txt file. | |
| type row 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 | |
| } | |
| func parseRow(s string) (vr row, 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 == '/' || 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:] | |
| if !strings.HasPrefix(rest, "struct, ") { | |
| vr.kind = "type" | |
| return vr, true | |
| } | |
| 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, "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" | |
| 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 | |
| } |