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", "", ""},