godoc: show earliest version when identifier was added
CL 85396 implemented parsePackageAPIInfo with the idea that each
identifier shows up in exactly one of api/go*.txt files, when it
was added. We now know that it may show up more than once, when
the signature changes (generally in a compatible way, such as
when existing types are replaced with aliases to an equivalent
type).
Modify the algorithm to parse the api/go*.txt files in reverse
order, in order to find and display the earliest Go version when
each identifier was first added.
Fixes golang/go#44081.
Updates golang/go#5778.
Change-Id: I83171fd8c161d703f284011375d74b824c279d41
Reviewed-on: https://go-review.googlesource.com/c/tools/+/289089
Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Trust: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/godoc/versions.go b/godoc/versions.go
index f03c714..7342858 100644
--- a/godoc/versions.go
+++ b/godoc/versions.go
@@ -13,6 +13,8 @@
"log"
"os"
"path/filepath"
+ "sort"
+ "strconv"
"strings"
"unicode"
)
@@ -75,18 +77,22 @@
res apiVersions // 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 *versionParser) parseFile(name string) error {
- base := filepath.Base(name)
- ver := strings.TrimPrefix(strings.TrimSuffix(base, ".txt"), "go")
- if ver == "1" {
- return nil
- }
f, err := os.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())
@@ -108,15 +114,31 @@
}
switch row.kind {
case "func":
+ if ver == "1" {
+ delete(pkgi.funcSince, row.name)
+ break
+ }
pkgi.funcSince[row.name] = ver
case "type":
+ if ver == "1" {
+ delete(pkgi.typeSince, row.name)
+ break
+ }
pkgi.typeSince[row.name] = ver
case "method":
+ if ver == "1" {
+ delete(pkgi.methodSince[row.recv], row.name)
+ break
+ }
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 ver == "1" {
+ delete(pkgi.fieldSince[row.structName], row.name)
+ break
+ }
if _, ok := pkgi.fieldSince[row.structName]; !ok {
pkgi.fieldSince[row.structName] = make(map[string]string)
}
@@ -214,6 +236,25 @@
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 := filepath.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(versionParser)
for _, f := range files {
if err := vp.parseFile(f); err != nil {
diff --git a/godoc/versions_test.go b/godoc/versions_test.go
index ad2d5e4..bfc05f6 100644
--- a/godoc/versions_test.go
+++ b/godoc/versions_test.go
@@ -113,6 +113,13 @@
{"type", "strings", "Builder", "", "1.10"},
{"method", "strings", "WriteString", "*Builder", "1.10"},
+ // Should get the earliest Go version when an identifier
+ // was initially added, rather than a later version when
+ // it may have been updated. See issue 44081.
+ {"func", "os", "Chmod", "", ""}, // Go 1 era function, updated in Go 1.16.
+ {"method", "os", "Readdir", "*File", ""}, // Go 1 era method, updated in Go 1.16.
+ {"method", "os", "ReadDir", "*File", "1.16"}, // New to Go 1.16.
+
// Things from package syscall should never appear
{"func", "syscall", "FchFlags", "", ""},
{"type", "syscall", "Inet4Pktinfo", "", ""},