Skip to content

Commit

Permalink
godoc: show version information for stdlib
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
dhobsd authored and bradfitz committed Jul 17, 2018
1 parent 353464c commit bf9b1d3
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 2 deletions.
4 changes: 4 additions & 0 deletions corpus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions godoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions static/package.html
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ <h2 id="pkg-variables">Variables</h2>
{{$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}}
Expand All @@ -180,6 +182,8 @@ <h2 id="{{$name_html}}">func <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a
{{$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>
Expand All @@ -202,6 +206,8 @@ <h2 id="{{$tname_html}}">type <a href="{{posLink_url $ .Decl}}">{{$tname_html}}<
{{$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}}
Expand All @@ -213,6 +219,8 @@ <h3 id="{{$name_html}}">func <a href="{{posLink_url $ .Decl}}">{{$name_html}}</a
{{$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}}
Expand Down
4 changes: 2 additions & 2 deletions static/static.go

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
211 changes: 211 additions & 0 deletions versions.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit bf9b1d3

Please sign in to comment.