1
0
mirror of https://github.com/golang/go synced 2024-11-18 04:14:49 -07:00

godoc: add version info for struct fields

Follow-up to CL 85396, which only did types, funcs, and methods.

This adds version info to struct fields (in the form of small
comments) if the struct field's version is different from the struct
itself, to minimize how often this fires.

Updates golang/go#5778

Change-Id: I34d60326cbef88c108d5c4ca487eeb98b039b16e
Reviewed-on: https://go-review.googlesource.com/124495
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:
Brad Fitzpatrick 2018-07-17 21:23:18 +00:00
parent 57f659e14d
commit 32950ab3be
5 changed files with 176 additions and 41 deletions

View File

@ -255,77 +255,106 @@ func testWeb(t *testing.T, withIndex bool) {
}
tests := []struct {
path string
match []string
dontmatch []string
needIndex bool
path string
contains []string // substring
match []string // regexp
notContains []string
needIndex bool
}{
{
path: "/",
match: []string{"Go is an open source programming language"},
path: "/",
contains: []string{"Go is an open source programming language"},
},
{
path: "/pkg/fmt/",
match: []string{"Package fmt implements formatted I/O"},
path: "/pkg/fmt/",
contains: []string{"Package fmt implements formatted I/O"},
},
{
path: "/src/fmt/",
match: []string{"scan_test.go"},
path: "/src/fmt/",
contains: []string{"scan_test.go"},
},
{
path: "/src/fmt/print.go",
match: []string{"// Println formats using"},
path: "/src/fmt/print.go",
contains: []string{"// Println formats using"},
},
{
path: "/pkg",
match: []string{
contains: []string{
"Standard library",
"Package fmt implements formatted I/O",
},
dontmatch: []string{
notContains: []string{
"internal/syscall",
"cmd/gc",
},
},
{
path: "/pkg/?m=all",
match: []string{
contains: []string{
"Standard library",
"Package fmt implements formatted I/O",
"internal/syscall/?m=all",
},
dontmatch: []string{
notContains: []string{
"cmd/gc",
},
},
{
path: "/search?q=ListenAndServe",
match: []string{
contains: []string{
"/src",
},
dontmatch: []string{
notContains: []string{
"/pkg/bootstrap",
},
needIndex: true,
},
{
path: "/pkg/strings/",
match: []string{
contains: []string{
`href="/src/strings/strings.go"`,
},
},
{
path: "/cmd/compile/internal/amd64/",
match: []string{
contains: []string{
`href="/src/cmd/compile/internal/amd64/ssa.go"`,
},
},
{
path: "/pkg/math/bits/",
match: []string{
contains: []string{
`Added in Go 1.9`,
},
},
{
path: "/pkg/net/",
contains: []string{
`// IPv6 scoped addressing zone; added in Go 1.1`,
},
},
{
path: "/pkg/net/http/httptrace/",
match: []string{
`Got1xxResponse.*// Go 1\.11`,
},
},
// Verify we don't add version info to a struct field added the same time
// as the struct itself:
{
path: "/pkg/net/http/httptrace/",
match: []string{
`(?m)GotFirstResponseByte func\(\)\s*$`,
},
},
// Remove trailing periods before adding semicolons:
{
path: "/pkg/database/sql/",
contains: []string{
"The number of connections currently in use; added in Go 1.11",
"The number of idle connections; added in Go 1.11",
},
},
}
for _, test := range tests {
if test.needIndex && !withIndex {
@ -338,18 +367,28 @@ func testWeb(t *testing.T, withIndex bool) {
continue
}
body, err := ioutil.ReadAll(resp.Body)
strBody := string(body)
resp.Body.Close()
if err != nil {
t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp)
}
isErr := false
for _, substr := range test.match {
for _, substr := range test.contains {
if !bytes.Contains(body, []byte(substr)) {
t.Errorf("GET %s: wanted substring %q in body", url, substr)
isErr = true
}
}
for _, substr := range test.dontmatch {
for _, re := range test.match {
if ok, err := regexp.MatchString(re, strBody); !ok || err != nil {
if err != nil {
t.Fatalf("Bad regexp %q: %v", re, err)
}
t.Errorf("GET %s: wanted to match %s in body", url, re)
isErr = true
}
}
for _, substr := range test.notContains {
if bytes.Contains(body, []byte(substr)) {
t.Errorf("GET %s: didn't want substring %q in body", url, substr)
isErr = true

View File

@ -10,6 +10,7 @@
package godoc // import "golang.org/x/tools/godoc"
import (
"bufio"
"bytes"
"fmt"
"go/ast"
@ -188,13 +189,13 @@ func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string {
func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string {
var buf bytes.Buffer
p.writeNode(&buf, info.FSet, node)
p.writeNode(&buf, info, info.FSet, node)
return buf.String()
}
func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string {
var buf1 bytes.Buffer
p.writeNode(&buf1, info.FSet, node)
p.writeNode(&buf1, info, info.FSet, node)
var buf2 bytes.Buffer
if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks {
@ -891,8 +892,12 @@ func replaceLeadingIndentation(body, oldIndent, newIndent string) string {
return buf.String()
}
// Write an AST node to w.
func (p *Presentation) writeNode(w io.Writer, fset *token.FileSet, x interface{}) {
// writeNode writes the AST node x to w.
//
// The provided fset must be non-nil. The pageInfo is optional. If
// present, the pageInfo is used to add comments to struct fields to
// say which version of Go introduced them.
func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) {
// convert trailing tabs into spaces using a tconv filter
// to ensure a good outcome in most browsers (there may still
// be tabs in comments and strings, but converting those into
@ -901,15 +906,88 @@ func (p *Presentation) writeNode(w io.Writer, fset *token.FileSet, x interface{}
// TODO(gri) rethink printer flags - perhaps tconv can be eliminated
// with an another printer mode (which is more efficiently
// implemented in the printer than here with another layer)
var pkgName, structName string
var apiInfo pkgAPIVersions
if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil &&
p.Corpus != nil &&
gd.Tok == token.TYPE && len(gd.Specs) != 0 {
pkgName = pageInfo.PDoc.ImportPath
if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok {
if _, ok := ts.Type.(*ast.StructType); ok {
structName = ts.Name.Name
}
}
apiInfo = p.Corpus.pkgAPIInfo[pkgName]
}
var out = w
var buf bytes.Buffer
if structName != "" {
out = &buf
}
mode := printer.TabIndent | printer.UseSpaces
err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: w}, fset, x)
err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x)
if err != nil {
log.Print(err)
}
// Add comments to struct fields saying which Go version introducd them.
if structName != "" {
fieldSince := apiInfo.fieldSince[structName]
typeSince := apiInfo.typeSince[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)
bs := bufio.NewScanner(&buf)
for bs.Scan() {
line := bs.Bytes()
field := firstIdent(line)
var since string
if field != "" {
since = fieldSince[field]
if since != "" && since == typeSince {
// Don't highlight field versions if they were the
// same as the struct itself.
since = ""
}
}
if since == "" {
buf2.Write(line)
} else {
if bytes.Contains(line, slashSlash) {
line = bytes.TrimRight(line, " \t.")
buf2.Write(line)
buf2.WriteString("; added in Go ")
} else {
buf2.Write(line)
buf2.WriteString(" // Go ")
}
buf2.WriteString(since)
}
buf2.WriteByte('\n')
}
w.Write(buf2.Bytes())
}
}
var slashSlash = []byte("//")
// WriteNode writes x to w.
// TODO(bgarcia) Is this method needed? It's just a wrapper for p.writeNode.
func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) {
p.writeNode(w, fset, x)
p.writeNode(w, nil, fset, x)
}
// firstIdent returns the first identifier in x.
// This actually parses "identifiers" that begin with numbers too, but we
// never feed it such input, so it's fine.
func firstIdent(x []byte) string {
x = bytes.TrimSpace(x)
i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })
if i == -1 {
return string(x)
}
return string(x[:i])
}

View File

@ -24,7 +24,7 @@ type Snippet struct {
func (p *Presentation) newSnippet(fset *token.FileSet, decl ast.Decl, id *ast.Ident) *Snippet {
// TODO instead of pretty-printing the node, should use the original source instead
var buf1 bytes.Buffer
p.writeNode(&buf1, fset, decl)
p.writeNode(&buf1, nil, fset, decl)
// wrap text with <pre> tag
var buf2 bytes.Buffer
buf2.WriteString("<pre>")

View File

@ -31,8 +31,9 @@ type apiVersions map[string]pkgAPIVersions // keyed by Go package ("net/http")
// 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
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
@ -62,10 +63,11 @@ func (v apiVersions) sinceVersionFunc(kind, receiver, name, pkg string) string {
// 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
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.
@ -100,6 +102,7 @@ func (vp *versionParser) parseFile(name string) error {
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
}
@ -113,6 +116,11 @@ func (vp *versionParser) parseFile(name string) error {
pkgi.methodSince[row.recv] = make(map[string]string)
}
pkgi.methodSince[row.recv][row.name] = ver
case "field":
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()
@ -139,18 +147,23 @@ func parseRow(s string) (vr versionedRow, ok bool) {
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
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
}
return vr, true
case strings.HasPrefix(rest, "func "):
vr.kind = "func"
rest = rest[len("func "):]

View File

@ -27,7 +27,12 @@ func TestParseVersionRow(t *testing.T) {
},
{
row: "pkg archive/tar, type Header struct, AccessTime time.Time",
// TODO: implement; for now we expect nothing
want: versionedRow{
pkg: "archive/tar",
kind: "field",
structName: "Header",
name: "AccessTime",
},
},
{
row: "pkg archive/tar, method (*Reader) Read([]uint8) (int, error)",