godoc: show version information for stdlib

This change reads $GOROOT/api/go1.*.txt when godoc starts and caches
information about which versions of Go introduce functions, types, and
methods. This information is displayed currently only in HTML output.
Functions, types, and methods introduced as part of Go 1 are not
annotated, as their presence at that version is implied.

This change does not address constants or variables, and completely
ignores the syscall package. The former are future work, the latter is
likely an exercise in futility. In all cases, this is because the story
around displaying the version information is not well developed.

Fixes golang/go#5778

Change-Id: Ieb3cc0da7b18e195bc9c443f14fd8a82e8b2bbf8
Reviewed-on: https://go-review.googlesource.com/85396
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Devon O'Dell <dhobsd@google.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Devon H. O'Dell 2017-12-22 17:30:39 -08:00 committed by Brad Fitzpatrick
parent d74aaa1f57
commit 57f659e14d
9 changed files with 350 additions and 2 deletions

View File

@ -320,6 +320,12 @@ func testWeb(t *testing.T, withIndex bool) {
`href="/src/cmd/compile/internal/amd64/ssa.go"`,
},
},
{
path: "/pkg/math/bits/",
match: []string{
`Added in Go 1.9`,
},
},
}
for _, test := range tests {
if test.needIndex && !withIndex {

View File

@ -250,6 +250,10 @@ func main() {
}
}
// Initialize the version info before readTemplates, which saves
// the map value in a method value.
corpus.InitVersionInfo()
pres = godoc.NewPresentation(corpus)
pres.TabWidth = *tabWidth
pres.ShowTimestamps = *showTimestamps

View File

@ -108,6 +108,10 @@ type Corpus struct {
// flag to check whether a corpus is initialized or not
initMu sync.RWMutex
initDone bool
// pkgAPIInfo contains the information about which package API
// features were added in which version of Go.
pkgAPIInfo apiVersions
}
// NewCorpus returns a new Corpus from a filesystem.

View File

@ -61,6 +61,7 @@ func (p *Presentation) initFuncMap() {
// various helpers
"filename": filenameFunc,
"repeat": strings.Repeat,
"since": p.Corpus.pkgAPIInfo.sinceVersionFunc,
// access to FileInfos (directory listings)
"fileInfoName": fileInfoNameFunc,

View File

@ -168,6 +168,8 @@
{{$name_html := html .Name}}
<h2 id="{{$name_html}}">func <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a>
<a class="permalink" href="#{{$name_html}}">&#xb6;</a>
{{$since := since "func" "" .Name $.PDoc.ImportPath}}
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
</h2>
<pre>{{node_html $ .Decl true}}</pre>
{{comment_html .Doc}}
@ -180,6 +182,8 @@
{{$tname_html := html .Name}}
<h2 id="{{$tname_html}}">type <a href="{{posLink_url $ .Decl}}">{{$tname_html}}</a>
<a class="permalink" href="#{{$tname_html}}">&#xb6;</a>
{{$since := since "type" "" .Name $.PDoc.ImportPath}}
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
</h2>
{{comment_html .Doc}}
<pre>{{node_html $ .Decl true}}</pre>
@ -202,6 +206,8 @@
{{$name_html := html .Name}}
<h3 id="{{$name_html}}">func <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a>
<a class="permalink" href="#{{$name_html}}">&#xb6;</a>
{{$since := since "func" "" .Name $.PDoc.ImportPath}}
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
</h3>
<pre>{{node_html $ .Decl true}}</pre>
{{comment_html .Doc}}
@ -213,6 +219,8 @@
{{$name_html := html .Name}}
<h3 id="{{$tname_html}}.{{$name_html}}">func ({{html .Recv}}) <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a>
<a class="permalink" href="#{{$tname_html}}.{{$name_html}}">&#xb6;</a>
{{$since := since "method" .Recv .Name $.PDoc.ImportPath}}
{{if $since}}<span title="Added in Go {{$since}}">{{$since}}</span>{{end}}
</h3>
<pre>{{node_html $ .Decl true}}</pre>
{{comment_html .Doc}}

File diff suppressed because one or more lines are too long

View File

@ -103,12 +103,17 @@ h2 {
padding: 0.5rem;
line-height: 1.25;
font-weight: normal;
overflow: auto;
overflow-wrap: break-word;
}
h2 a {
font-weight: bold;
}
h3 {
font-size: 1.25rem;
line-height: 1.25;
overflow: auto;
overflow-wrap: break-word;
}
h3,
h4 {
@ -122,6 +127,14 @@ h4 {
margin: 0;
}
h2 > span,
h3 > span {
float: right;
margin: 0 25px 0 0;
font-weight: normal;
color: #5279C7;
}
dl {
margin: 1.25rem;
}

211
godoc/versions.go Normal file
View File

@ -0,0 +1,211 @@
// 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 godoc
import (
"bufio"
"go/build"
"log"
"os"
"path/filepath"
"strings"
"unicode"
)
// 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"
}
// 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", TODO: "const", "var"
recv string // for methods, the receiver type ("Server", "*Server")
name string // name of type, func, or method
}
// versionParser parses $GOROOT/api/go*.txt files and stores them in in its rows field.
type versionParser struct {
res apiVersions // initialized lazily
}
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()
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),
}
vp.res[row.pkg] = pkgi
}
switch row.kind {
case "func":
pkgi.funcSince[row.name] = ver
case "type":
pkgi.typeSince[row.name] = ver
case "method":
if _, ok := pkgi.methodSince[row.recv]; !ok {
pkgi.methodSince[row.recv] = make(map[string]string)
}
pkgi.methodSince[row.recv][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 == '/') })
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 "):
vr.kind = "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, ") {
// TODO: handle struct fields
return
}
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()
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
}
vp := new(versionParser)
for _, f := range files {
if err := vp.parseFile(f); err != nil {
return nil, err
}
}
return vp.res, nil
}

101
godoc/versions_test.go Normal file
View File

@ -0,0 +1,101 @@
// 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.
package godoc
import "testing"
func TestParseVersionRow(t *testing.T) {
tests := []struct {
row string
want versionedRow
}{
{
row: "# comment",
},
{
row: "",
},
{
row: "pkg archive/tar, type Writer struct",
want: versionedRow{
pkg: "archive/tar",
kind: "type",
name: "Writer",
},
},
{
row: "pkg archive/tar, type Header struct, AccessTime time.Time",
// TODO: implement; for now we expect nothing
},
{
row: "pkg archive/tar, method (*Reader) Read([]uint8) (int, error)",
want: versionedRow{
pkg: "archive/tar",
kind: "method",
name: "Read",
recv: "*Reader",
},
},
{
row: "pkg archive/zip, func FileInfoHeader(os.FileInfo) (*FileHeader, error)",
want: versionedRow{
pkg: "archive/zip",
kind: "func",
name: "FileInfoHeader",
},
},
}
for i, tt := range tests {
got, ok := parseRow(tt.row)
if !ok {
got = versionedRow{}
}
if got != tt.want {
t.Errorf("%d. parseRow(%q) = %+v; want %+v", i, tt.row, got, tt.want)
}
}
}
func TestAPIVersion(t *testing.T) {
av, err := parsePackageAPIInfo()
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
kind string
pkg string
name string
receiver string
want string
}{
// Things that were added post-1.0 should appear
{"func", "archive/tar", "FileInfoHeader", "", "1.1"},
{"type", "bufio", "Scanner", "", "1.1"},
{"method", "bufio", "WriteTo", "*Reader", "1.1"},
{"func", "bytes", "LastIndexByte", "", "1.5"},
{"type", "crypto", "Decrypter", "", "1.5"},
{"method", "crypto/rsa", "Decrypt", "*PrivateKey", "1.5"},
{"method", "debug/dwarf", "GoString", "Class", "1.5"},
{"func", "os", "IsTimeout", "", "1.10"},
{"type", "strings", "Builder", "", "1.10"},
{"method", "strings", "WriteString", "*Builder", "1.10"},
// Things from package syscall should never appear
{"func", "syscall", "FchFlags", "", ""},
{"type", "syscall", "Inet4Pktinfo", "", ""},
// Things added in Go 1 should never appear
{"func", "archive/tar", "NewReader", "", ""},
{"type", "archive/tar", "Header", "", ""},
{"method", "archive/tar", "Next", "*Reader", ""},
} {
if got := av.sinceVersionFunc(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)
}
}
}