internal/godoc: split out API version database [generated]

Extract API database into a standalone package.
Generated with rf script below.

[git-generate]
cd internal/godoc
rf '
	mv apiVersions DB
	mv DB.sinceVersionFunc DB.Func
	mv pkgAPIVersions PkgDB
	mv PkgDB.typeSince PkgDB.Type
	mv PkgDB.methodSince PkgDB.Method
	mv PkgDB.funcSince PkgDB.Func
	mv PkgDB.fieldSince PkgDB.Field
	mv parsePackageAPIInfo Load
	mv versionedRow row
	mv versionParser dbParser

	mv DB PkgDB \
		DB.Func \
		Load \
		dbParser \
		dbParser.parseFile \
		row \
		parseRow \
		api.go

	mv versions_test.go api_test.go

	mv api.go api_test.go golang.org/x/website/internal/api
'
cd ../api
rf '
	mv dbParser parser
'

Change-Id: I1bf57608345ae39eb217024c9c4bf1ae6543bd98
Reviewed-on: https://go-review.googlesource.com/c/website/+/296376
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/internal/api/api.go b/internal/api/api.go
new file mode 100644
index 0000000..59e6740
--- /dev/null
+++ b/internal/api/api.go
@@ -0,0 +1,256 @@
+// 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.
+
+//go:build go1.16
+// +build go1.16
+
+// This file caches information about which standard library types, methods,
+// and functions appeared in what version of Go
+
+package api
+
+import (
+	"bufio"
+	"go/build"
+	"os"
+	"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(kind, receiver, name, pkg 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 ""
+}
+
+func Load() (DB, error) {
+	var apiGlob string
+	if os.Getenv("GOROOT") == "" {
+		apiGlob = filepath.Join(build.Default.GOROOT, "api", "go*.txt")
+	} else {
+		apiGlob = filepath.Join(os.Getenv("GOROOT"), "api", "go*.txt")
+	}
+
+	files, err := filepath.Glob(apiGlob)
+	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 := 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(parser)
+	for _, f := range files {
+		if err := vp.parseFile(f); err != nil {
+			return nil, err
+		}
+	}
+	return vp.res, nil
+}
+
+// parser parses $GOROOT/api/go*.txt files and stores them in 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(name string) error {
+	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())
+		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
+}
diff --git a/internal/godoc/versions_test.go b/internal/api/api_test.go
similarity index 91%
rename from internal/godoc/versions_test.go
rename to internal/api/api_test.go
index 1504b61..bfddc5d 100644
--- a/internal/godoc/versions_test.go
+++ b/internal/api/api_test.go
@@ -5,7 +5,7 @@
 //go:build go1.16
 // +build go1.16
 
-package godoc
+package api
 
 import (
 	"go/build"
@@ -15,7 +15,7 @@
 func TestParseVersionRow(t *testing.T) {
 	tests := []struct {
 		row  string
-		want versionedRow
+		want row
 	}{
 		{
 			row: "# comment",
@@ -25,7 +25,7 @@
 		},
 		{
 			row: "pkg archive/tar, type Writer struct",
-			want: versionedRow{
+			want: row{
 				pkg:  "archive/tar",
 				kind: "type",
 				name: "Writer",
@@ -33,7 +33,7 @@
 		},
 		{
 			row: "pkg archive/tar, type Header struct, AccessTime time.Time",
-			want: versionedRow{
+			want: row{
 				pkg:        "archive/tar",
 				kind:       "field",
 				structName: "Header",
@@ -42,7 +42,7 @@
 		},
 		{
 			row: "pkg archive/tar, method (*Reader) Read([]uint8) (int, error)",
-			want: versionedRow{
+			want: row{
 				pkg:  "archive/tar",
 				kind: "method",
 				name: "Read",
@@ -51,7 +51,7 @@
 		},
 		{
 			row: "pkg archive/zip, func FileInfoHeader(os.FileInfo) (*FileHeader, error)",
-			want: versionedRow{
+			want: row{
 				pkg:  "archive/zip",
 				kind: "func",
 				name: "FileInfoHeader",
@@ -59,7 +59,7 @@
 		},
 		{
 			row: "pkg encoding/base32, method (Encoding) WithPadding(int32) *Encoding",
-			want: versionedRow{
+			want: row{
 				pkg:  "encoding/base32",
 				kind: "method",
 				name: "WithPadding",
@@ -71,7 +71,7 @@
 	for i, tt := range tests {
 		got, ok := parseRow(tt.row)
 		if !ok {
-			got = versionedRow{}
+			got = row{}
 		}
 		if got != tt.want {
 			t.Errorf("%d. parseRow(%q) = %+v; want %+v", i, tt.row, got, tt.want)
@@ -91,7 +91,7 @@
 }
 
 func TestAPIVersion(t *testing.T) {
-	av, err := parsePackageAPIInfo()
+	av, err := Load()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -135,7 +135,7 @@
 		if tc.want != "" && !hasTag("go"+tc.want) {
 			continue
 		}
-		if got := av.sinceVersionFunc(tc.kind, tc.receiver, tc.name, tc.pkg); got != tc.want {
+		if got := av.Func(tc.kind, tc.receiver, tc.name, tc.pkg); got != tc.want {
 			t.Errorf(`sinceFunc("%s", "%s", "%s", "%s") = "%s"; want "%s"`, tc.kind, tc.receiver, tc.name, tc.pkg, got, tc.want)
 		}
 	}
diff --git a/internal/godoc/astfuncs.go b/internal/godoc/astfuncs.go
index 48ab123..71398a9 100644
--- a/internal/godoc/astfuncs.go
+++ b/internal/godoc/astfuncs.go
@@ -18,6 +18,7 @@
 	"log"
 	"unicode"
 
+	"golang.org/x/website/internal/api"
 	"golang.org/x/website/internal/texthtml"
 )
 
@@ -61,7 +62,7 @@
 	//           implemented in the printer than here with another layer)
 
 	var pkgName, structName string
-	var apiInfo pkgAPIVersions
+	var apiInfo api.PkgDB
 	if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil &&
 		p.Corpus != nil &&
 		gd.Tok == token.TYPE && len(gd.Specs) != 0 {
@@ -88,8 +89,8 @@
 
 	// Add comments to struct fields saying which Go version introduced them.
 	if structName != "" {
-		fieldSince := apiInfo.fieldSince[structName]
-		typeSince := apiInfo.typeSince[structName]
+		fieldSince := apiInfo.Field[structName]
+		typeSince := apiInfo.Type[structName]
 		// Add/rewrite comments on struct fields to note which Go version added them.
 		var buf2 bytes.Buffer
 		buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10)
diff --git a/internal/godoc/corpus.go b/internal/godoc/corpus.go
index fff776e..6de01d1 100644
--- a/internal/godoc/corpus.go
+++ b/internal/godoc/corpus.go
@@ -12,6 +12,8 @@
 	"io/fs"
 	"sync"
 	"time"
+
+	"golang.org/x/website/internal/api"
 )
 
 // A Corpus holds all the state related to serving and indexing a
@@ -53,7 +55,7 @@
 
 	// pkgAPIInfo contains the information about which package API
 	// features were added in which version of Go.
-	pkgAPIInfo apiVersions
+	pkgAPIInfo api.DB
 }
 
 // NewCorpus returns a new Corpus from a filesystem.
diff --git a/internal/godoc/godoc.go b/internal/godoc/godoc.go
index 4b80021..130baaf 100644
--- a/internal/godoc/godoc.go
+++ b/internal/godoc/godoc.go
@@ -51,7 +51,7 @@
 	p.funcMap = template.FuncMap{
 		// various helpers
 		"filename": filenameFunc,
-		"since":    p.Corpus.pkgAPIInfo.sinceVersionFunc,
+		"since":    p.Corpus.pkgAPIInfo.Func,
 
 		// formatting of AST nodes
 		"node":         p.nodeFunc,
diff --git a/internal/godoc/versions.go b/internal/godoc/versions.go
index e0a16f7..9f18fb4 100644
--- a/internal/godoc/versions.go
+++ b/internal/godoc/versions.go
@@ -11,258 +11,18 @@
 package godoc
 
 import (
-	"bufio"
-	"go/build"
 	"log"
-	"os"
-	"path/filepath"
-	"sort"
-	"strconv"
-	"strings"
-	"unicode"
+
+	"golang.org/x/website/internal/api"
 )
 
-// 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 {
-	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"
-}
-
-// sinceVersionFunc 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 apiVersions) sinceVersionFunc(kind, receiver, name, pkg string) string {
-	pv := v[pkg]
-	switch kind {
-	case "func":
-		return pv.funcSince[name]
-	case "type":
-		return pv.typeSince[name]
-	case "method":
-		return pv.methodSince[receiver][name]
-	}
-	return ""
-}
-
-// 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 $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 {
-	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())
-		if !ok {
-			continue
-		}
-		if vp.res == nil {
-			vp.res = make(apiVersions)
-		}
-		pkgi, ok := vp.res[row.pkg]
-		if !ok {
-			pkgi = pkgAPIVersions{
-				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 "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)
-			}
-			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 == '/' || 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
-}
-
 // InitVersionInfo parses the $GOROOT/api/go*.txt API definition files to discover
 // which API features were added in which Go releases.
 func (c *Corpus) InitVersionInfo() {
 	var err error
-	c.pkgAPIInfo, err = parsePackageAPIInfo()
+	c.pkgAPIInfo, err = api.Load()
 	if err != nil {
 		// TODO: consider making this fatal, after the Go 1.11 cycle.
 		log.Printf("godoc: error parsing API version files: %v", err)
 	}
 }
-
-func parsePackageAPIInfo() (apiVersions, error) {
-	var apiGlob string
-	if os.Getenv("GOROOT") == "" {
-		apiGlob = filepath.Join(build.Default.GOROOT, "api", "go*.txt")
-	} else {
-		apiGlob = filepath.Join(os.Getenv("GOROOT"), "api", "go*.txt")
-	}
-
-	files, err := filepath.Glob(apiGlob)
-	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 := 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 {
-			return nil, err
-		}
-	}
-	return vp.res, nil
-}