| 1 | // Copyright 2018 The Go Authors. All rights reserved. |
|---|---|
| 2 | // Use of this source code is governed by a BSD-style |
| 3 | // license that can be found in the LICENSE file. |
| 4 | |
| 5 | // This file caches information about which standard library types, methods, |
| 6 | // and functions appeared in what version of Go |
| 7 | |
| 8 | package godoc |
| 9 | |
| 10 | import ( |
| 11 | "bufio" |
| 12 | "go/build" |
| 13 | "log" |
| 14 | "os" |
| 15 | "path/filepath" |
| 16 | "sort" |
| 17 | "strconv" |
| 18 | "strings" |
| 19 | "unicode" |
| 20 | ) |
| 21 | |
| 22 | // apiVersions is a map of packages to information about those packages' |
| 23 | // symbols and when they were added to Go. |
| 24 | // |
| 25 | // Only things added after Go1 are tracked. Version strings are of the |
| 26 | // form "1.1", "1.2", etc. |
| 27 | type apiVersions map[string]pkgAPIVersions // keyed by Go package ("net/http") |
| 28 | |
| 29 | // pkgAPIVersions contains information about which version of Go added |
| 30 | // certain package symbols. |
| 31 | // |
| 32 | // Only things added after Go1 are tracked. Version strings are of the |
| 33 | // form "1.1", "1.2", etc. |
| 34 | type pkgAPIVersions struct { |
| 35 | typeSince map[string]string // "Server" -> "1.7" |
| 36 | methodSince map[string]map[string]string // "*Server" ->"Shutdown"->1.8 |
| 37 | funcSince map[string]string // "NewServer" -> "1.7" |
| 38 | fieldSince map[string]map[string]string // "ClientTrace" -> "Got1xxResponse" -> "1.11" |
| 39 | } |
| 40 | |
| 41 | // sinceVersionFunc returns a string (such as "1.7") specifying which Go |
| 42 | // version introduced a symbol, unless it was introduced in Go1, in |
| 43 | // which case it returns the empty string. |
| 44 | // |
| 45 | // The kind is one of "type", "method", or "func". |
| 46 | // |
| 47 | // The receiver is only used for "methods" and specifies the receiver type, |
| 48 | // such as "*Server". |
| 49 | // |
| 50 | // The name is the symbol name ("Server") and the pkg is the package |
| 51 | // ("net/http"). |
| 52 | func (v apiVersions) sinceVersionFunc(kind, receiver, name, pkg string) string { |
| 53 | pv := v[pkg] |
| 54 | switch kind { |
| 55 | case "func": |
| 56 | return pv.funcSince[name] |
| 57 | case "type": |
| 58 | return pv.typeSince[name] |
| 59 | case "method": |
| 60 | return pv.methodSince[receiver][name] |
| 61 | } |
| 62 | return "" |
| 63 | } |
| 64 | |
| 65 | // versionedRow represents an API feature, a parsed line of a |
| 66 | // $GOROOT/api/go.*txt file. |
| 67 | type versionedRow struct { |
| 68 | pkg string // "net/http" |
| 69 | kind string // "type", "func", "method", "field" TODO: "const", "var" |
| 70 | recv string // for methods, the receiver type ("Server", "*Server") |
| 71 | name string // name of type, (struct) field, func, method |
| 72 | structName string // for struct fields, the outer struct name |
| 73 | } |
| 74 | |
| 75 | // versionParser parses $GOROOT/api/go*.txt files and stores them in in its rows field. |
| 76 | type versionParser struct { |
| 77 | res apiVersions // initialized lazily |
| 78 | } |
| 79 | |
| 80 | // parseFile parses the named $GOROOT/api/goVERSION.txt file. |
| 81 | // |
| 82 | // For each row, it updates the corresponding entry in |
| 83 | // vp.res to VERSION, overwriting any previous value. |
| 84 | // As a special case, if goVERSION is "go1", it deletes |
| 85 | // from the map instead. |
| 86 | func (vp *versionParser) parseFile(name string) error { |
| 87 | f, err := os.Open(name) |
| 88 | if err != nil { |
| 89 | return err |
| 90 | } |
| 91 | defer f.Close() |
| 92 | |
| 93 | base := filepath.Base(name) |
| 94 | ver := strings.TrimPrefix(strings.TrimSuffix(base, ".txt"), "go") |
| 95 | |
| 96 | sc := bufio.NewScanner(f) |
| 97 | for sc.Scan() { |
| 98 | row, ok := parseRow(sc.Text()) |
| 99 | if !ok { |
| 100 | continue |
| 101 | } |
| 102 | if vp.res == nil { |
| 103 | vp.res = make(apiVersions) |
| 104 | } |
| 105 | pkgi, ok := vp.res[row.pkg] |
| 106 | if !ok { |
| 107 | pkgi = pkgAPIVersions{ |
| 108 | typeSince: make(map[string]string), |
| 109 | methodSince: make(map[string]map[string]string), |
| 110 | funcSince: make(map[string]string), |
| 111 | fieldSince: make(map[string]map[string]string), |
| 112 | } |
| 113 | vp.res[row.pkg] = pkgi |
| 114 | } |
| 115 | switch row.kind { |
| 116 | case "func": |
| 117 | if ver == "1" { |
| 118 | delete(pkgi.funcSince, row.name) |
| 119 | break |
| 120 | } |
| 121 | pkgi.funcSince[row.name] = ver |
| 122 | case "type": |
| 123 | if ver == "1" { |
| 124 | delete(pkgi.typeSince, row.name) |
| 125 | break |
| 126 | } |
| 127 | pkgi.typeSince[row.name] = ver |
| 128 | case "method": |
| 129 | if ver == "1" { |
| 130 | delete(pkgi.methodSince[row.recv], row.name) |
| 131 | break |
| 132 | } |
| 133 | if _, ok := pkgi.methodSince[row.recv]; !ok { |
| 134 | pkgi.methodSince[row.recv] = make(map[string]string) |
| 135 | } |
| 136 | pkgi.methodSince[row.recv][row.name] = ver |
| 137 | case "field": |
| 138 | if ver == "1" { |
| 139 | delete(pkgi.fieldSince[row.structName], row.name) |
| 140 | break |
| 141 | } |
| 142 | if _, ok := pkgi.fieldSince[row.structName]; !ok { |
| 143 | pkgi.fieldSince[row.structName] = make(map[string]string) |
| 144 | } |
| 145 | pkgi.fieldSince[row.structName][row.name] = ver |
| 146 | } |
| 147 | } |
| 148 | return sc.Err() |
| 149 | } |
| 150 | |
| 151 | func parseRow(s string) (vr versionedRow, ok bool) { |
| 152 | if !strings.HasPrefix(s, "pkg ") { |
| 153 | // Skip comments, blank lines, etc. |
| 154 | return |
| 155 | } |
| 156 | rest := s[len("pkg "):] |
| 157 | endPkg := strings.IndexFunc(rest, func(r rune) bool { return !(unicode.IsLetter(r) || r == '/' || unicode.IsDigit(r)) }) |
| 158 | if endPkg == -1 { |
| 159 | return |
| 160 | } |
| 161 | vr.pkg, rest = rest[:endPkg], rest[endPkg:] |
| 162 | if !strings.HasPrefix(rest, ", ") { |
| 163 | // If the part after the pkg name isn't ", ", then it's a OS/ARCH-dependent line of the form: |
| 164 | // pkg syscall (darwin-amd64), const ImplementsGetwd = false |
| 165 | // We skip those for now. |
| 166 | return |
| 167 | } |
| 168 | rest = rest[len(", "):] |
| 169 | |
| 170 | switch { |
| 171 | case strings.HasPrefix(rest, "type "): |
| 172 | rest = rest[len("type "):] |
| 173 | sp := strings.IndexByte(rest, ' ') |
| 174 | if sp == -1 { |
| 175 | return |
| 176 | } |
| 177 | vr.name, rest = rest[:sp], rest[sp+1:] |
| 178 | if !strings.HasPrefix(rest, "struct, ") { |
| 179 | vr.kind = "type" |
| 180 | return vr, true |
| 181 | } |
| 182 | rest = rest[len("struct, "):] |
| 183 | if i := strings.IndexByte(rest, ' '); i != -1 { |
| 184 | vr.kind = "field" |
| 185 | vr.structName = vr.name |
| 186 | vr.name = rest[:i] |
| 187 | return vr, true |
| 188 | } |
| 189 | case strings.HasPrefix(rest, "func "): |
| 190 | vr.kind = "func" |
| 191 | rest = rest[len("func "):] |
| 192 | if i := strings.IndexByte(rest, '('); i != -1 { |
| 193 | vr.name = rest[:i] |
| 194 | return vr, true |
| 195 | } |
| 196 | case strings.HasPrefix(rest, "method "): // "method (*File) SetModTime(time.Time)" |
| 197 | vr.kind = "method" |
| 198 | rest = rest[len("method "):] // "(*File) SetModTime(time.Time)" |
| 199 | sp := strings.IndexByte(rest, ' ') |
| 200 | if sp == -1 { |
| 201 | return |
| 202 | } |
| 203 | vr.recv = strings.Trim(rest[:sp], "()") // "*File" |
| 204 | rest = rest[sp+1:] // SetMode(os.FileMode) |
| 205 | paren := strings.IndexByte(rest, '(') |
| 206 | if paren == -1 { |
| 207 | return |
| 208 | } |
| 209 | vr.name = rest[:paren] |
| 210 | return vr, true |
| 211 | } |
| 212 | return // TODO: handle more cases |
| 213 | } |
| 214 | |
| 215 | // InitVersionInfo parses the $GOROOT/api/go*.txt API definition files to discover |
| 216 | // which API features were added in which Go releases. |
| 217 | func (c *Corpus) InitVersionInfo() { |
| 218 | var err error |
| 219 | c.pkgAPIInfo, err = parsePackageAPIInfo() |
| 220 | if err != nil { |
| 221 | // TODO: consider making this fatal, after the Go 1.11 cycle. |
| 222 | log.Printf("godoc: error parsing API version files: %v", err) |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | func parsePackageAPIInfo() (apiVersions, error) { |
| 227 | var apiGlob string |
| 228 | if os.Getenv("GOROOT") == "" { |
| 229 | apiGlob = filepath.Join(build.Default.GOROOT, "api", "go*.txt") |
| 230 | } else { |
| 231 | apiGlob = filepath.Join(os.Getenv("GOROOT"), "api", "go*.txt") |
| 232 | } |
| 233 | |
| 234 | files, err := filepath.Glob(apiGlob) |
| 235 | if err != nil { |
| 236 | return nil, err |
| 237 | } |
| 238 | |
| 239 | // Process files in go1.n, go1.n-1, ..., go1.2, go1.1, go1 order. |
| 240 | // |
| 241 | // It's rare, but the signature of an identifier may change |
| 242 | // (for example, a function that accepts a type replaced with |
| 243 | // an alias), and so an existing symbol may show up again in |
| 244 | // a later api/go1.N.txt file. Parsing in reverse version |
| 245 | // order means we end up with the earliest version of Go |
| 246 | // when the symbol was added. See golang.org/issue/44081. |
| 247 | // |
| 248 | ver := func(name string) int { |
| 249 | base := filepath.Base(name) |
| 250 | ver := strings.TrimPrefix(strings.TrimSuffix(base, ".txt"), "go1.") |
| 251 | if ver == "go1" { |
| 252 | return 0 |
| 253 | } |
| 254 | v, _ := strconv.Atoi(ver) |
| 255 | return v |
| 256 | } |
| 257 | sort.Slice(files, func(i, j int) bool { return ver(files[i]) > ver(files[j]) }) |
| 258 | vp := new(versionParser) |
| 259 | for _, f := range files { |
| 260 | if err := vp.parseFile(f); err != nil { |
| 261 | return nil, err |
| 262 | } |
| 263 | } |
| 264 | return vp.res, nil |
| 265 | } |
| 266 |
Members