mirror of https://go.googlesource.com/go
293 lines
6.7 KiB
Go
293 lines
6.7 KiB
Go
// Copyright 2012 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 doc
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/printer"
|
|
"go/token"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
)
|
|
|
|
var update = flag.Bool("update", false, "update golden (.out) files")
|
|
var files = flag.String("files", "", "consider only Go test files matching this regular expression")
|
|
|
|
const dataDir = "testdata"
|
|
|
|
var templateTxt = readTemplate("template.txt")
|
|
|
|
func readTemplate(filename string) *template.Template {
|
|
t := template.New(filename)
|
|
t.Funcs(template.FuncMap{
|
|
"node": nodeFmt,
|
|
"synopsis": synopsisFmt,
|
|
"indent": indentFmt,
|
|
})
|
|
return template.Must(t.ParseFiles(filepath.Join(dataDir, filename)))
|
|
}
|
|
|
|
func nodeFmt(node any, fset *token.FileSet) string {
|
|
var buf bytes.Buffer
|
|
printer.Fprint(&buf, fset, node)
|
|
return strings.ReplaceAll(strings.TrimSpace(buf.String()), "\n", "\n\t")
|
|
}
|
|
|
|
func synopsisFmt(s string) string {
|
|
const n = 64
|
|
if len(s) > n {
|
|
// cut off excess text and go back to a word boundary
|
|
s = s[0:n]
|
|
if i := strings.LastIndexAny(s, "\t\n "); i >= 0 {
|
|
s = s[0:i]
|
|
}
|
|
s = strings.TrimSpace(s) + " ..."
|
|
}
|
|
return "// " + strings.ReplaceAll(s, "\n", " ")
|
|
}
|
|
|
|
func indentFmt(indent, s string) string {
|
|
end := ""
|
|
if strings.HasSuffix(s, "\n") {
|
|
end = "\n"
|
|
s = s[:len(s)-1]
|
|
}
|
|
return indent + strings.ReplaceAll(s, "\n", "\n"+indent) + end
|
|
}
|
|
|
|
func isGoFile(fi fs.FileInfo) bool {
|
|
name := fi.Name()
|
|
return !fi.IsDir() &&
|
|
len(name) > 0 && name[0] != '.' && // ignore .files
|
|
filepath.Ext(name) == ".go"
|
|
}
|
|
|
|
type bundle struct {
|
|
*Package
|
|
FSet *token.FileSet
|
|
}
|
|
|
|
func test(t *testing.T, mode Mode) {
|
|
// determine file filter
|
|
filter := isGoFile
|
|
if *files != "" {
|
|
rx, err := regexp.Compile(*files)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
filter = func(fi fs.FileInfo) bool {
|
|
return isGoFile(fi) && rx.MatchString(fi.Name())
|
|
}
|
|
}
|
|
|
|
// get packages
|
|
fset := token.NewFileSet()
|
|
pkgs, err := parser.ParseDir(fset, dataDir, filter, parser.ParseComments)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// test packages
|
|
for _, pkg := range pkgs {
|
|
t.Run(pkg.Name, func(t *testing.T) {
|
|
importPath := dataDir + "/" + pkg.Name
|
|
var files []*ast.File
|
|
for _, f := range pkg.Files {
|
|
files = append(files, f)
|
|
}
|
|
doc, err := NewFromFiles(fset, files, importPath, mode)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// golden files always use / in filenames - canonicalize them
|
|
for i, filename := range doc.Filenames {
|
|
doc.Filenames[i] = filepath.ToSlash(filename)
|
|
}
|
|
|
|
// print documentation
|
|
var buf bytes.Buffer
|
|
if err := templateTxt.Execute(&buf, bundle{doc, fset}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := buf.Bytes()
|
|
|
|
// update golden file if necessary
|
|
golden := filepath.Join(dataDir, fmt.Sprintf("%s.%d.golden", pkg.Name, mode))
|
|
if *update {
|
|
err := os.WriteFile(golden, got, 0644)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// get golden file
|
|
want, err := os.ReadFile(golden)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// compare
|
|
if !bytes.Equal(got, want) {
|
|
t.Errorf("package %s\n\tgot:\n%s\n\twant:\n%s", pkg.Name, got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test(t *testing.T) {
|
|
t.Run("default", func(t *testing.T) { test(t, 0) })
|
|
t.Run("AllDecls", func(t *testing.T) { test(t, AllDecls) })
|
|
t.Run("AllMethods", func(t *testing.T) { test(t, AllMethods) })
|
|
}
|
|
|
|
func TestFuncs(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
file, err := parser.ParseFile(fset, "funcs.go", strings.NewReader(funcsTestFile), parser.ParseComments)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc, err := NewFromFiles(fset, []*ast.File{file}, "importPath", Mode(0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, f := range doc.Funcs {
|
|
f.Decl = nil
|
|
}
|
|
for _, ty := range doc.Types {
|
|
for _, f := range ty.Funcs {
|
|
f.Decl = nil
|
|
}
|
|
for _, m := range ty.Methods {
|
|
m.Decl = nil
|
|
}
|
|
}
|
|
|
|
compareFuncs := func(t *testing.T, msg string, got, want *Func) {
|
|
// ignore Decl and Examples
|
|
got.Decl = nil
|
|
got.Examples = nil
|
|
if !(got.Doc == want.Doc &&
|
|
got.Name == want.Name &&
|
|
got.Recv == want.Recv &&
|
|
got.Orig == want.Orig &&
|
|
got.Level == want.Level) {
|
|
t.Errorf("%s:\ngot %+v\nwant %+v", msg, got, want)
|
|
}
|
|
}
|
|
|
|
compareSlices(t, "Funcs", doc.Funcs, funcsPackage.Funcs, compareFuncs)
|
|
compareSlices(t, "Types", doc.Types, funcsPackage.Types, func(t *testing.T, msg string, got, want *Type) {
|
|
if got.Name != want.Name {
|
|
t.Errorf("%s.Name: got %q, want %q", msg, got.Name, want.Name)
|
|
} else {
|
|
compareSlices(t, got.Name+".Funcs", got.Funcs, want.Funcs, compareFuncs)
|
|
compareSlices(t, got.Name+".Methods", got.Methods, want.Methods, compareFuncs)
|
|
}
|
|
})
|
|
}
|
|
|
|
func compareSlices[E any](t *testing.T, name string, got, want []E, compareElem func(*testing.T, string, E, E)) {
|
|
if len(got) != len(want) {
|
|
t.Errorf("%s: got %d, want %d", name, len(got), len(want))
|
|
}
|
|
for i := 0; i < len(got) && i < len(want); i++ {
|
|
compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i])
|
|
}
|
|
}
|
|
|
|
const funcsTestFile = `
|
|
package funcs
|
|
|
|
func F() {}
|
|
|
|
type S1 struct {
|
|
S2 // embedded, exported
|
|
s3 // embedded, unexported
|
|
}
|
|
|
|
func NewS1() S1 {return S1{} }
|
|
func NewS1p() *S1 { return &S1{} }
|
|
|
|
func (S1) M1() {}
|
|
func (r S1) M2() {}
|
|
func(S1) m3() {} // unexported not shown
|
|
func (*S1) P1() {} // pointer receiver
|
|
|
|
type S2 int
|
|
func (S2) M3() {} // shown on S2
|
|
|
|
type s3 int
|
|
func (s3) M4() {} // shown on S1
|
|
|
|
type G1[T any] struct {
|
|
*s3
|
|
}
|
|
|
|
func NewG1[T any]() G1[T] { return G1[T]{} }
|
|
|
|
func (G1[T]) MG1() {}
|
|
func (*G1[U]) MG2() {}
|
|
|
|
type G2[T, U any] struct {}
|
|
|
|
func NewG2[T, U any]() G2[T, U] { return G2[T, U]{} }
|
|
|
|
func (G2[T, U]) MG3() {}
|
|
func (*G2[A, B]) MG4() {}
|
|
|
|
|
|
`
|
|
|
|
var funcsPackage = &Package{
|
|
Funcs: []*Func{{Name: "F"}},
|
|
Types: []*Type{
|
|
{
|
|
Name: "G1",
|
|
Funcs: []*Func{{Name: "NewG1"}},
|
|
Methods: []*Func{
|
|
{Name: "M4", Recv: "G1", // TODO: synthesize a param for G1?
|
|
Orig: "s3", Level: 1},
|
|
{Name: "MG1", Recv: "G1[T]", Orig: "G1[T]", Level: 0},
|
|
{Name: "MG2", Recv: "*G1[U]", Orig: "*G1[U]", Level: 0},
|
|
},
|
|
},
|
|
{
|
|
Name: "G2",
|
|
Funcs: []*Func{{Name: "NewG2"}},
|
|
Methods: []*Func{
|
|
{Name: "MG3", Recv: "G2[T, U]", Orig: "G2[T, U]", Level: 0},
|
|
{Name: "MG4", Recv: "*G2[A, B]", Orig: "*G2[A, B]", Level: 0},
|
|
},
|
|
},
|
|
{
|
|
Name: "S1",
|
|
Funcs: []*Func{{Name: "NewS1"}, {Name: "NewS1p"}},
|
|
Methods: []*Func{
|
|
{Name: "M1", Recv: "S1", Orig: "S1", Level: 0},
|
|
{Name: "M2", Recv: "S1", Orig: "S1", Level: 0},
|
|
{Name: "M4", Recv: "S1", Orig: "s3", Level: 1},
|
|
{Name: "P1", Recv: "*S1", Orig: "*S1", Level: 0},
|
|
},
|
|
},
|
|
{
|
|
Name: "S2",
|
|
Methods: []*Func{
|
|
{Name: "M3", Recv: "S2", Orig: "S2", Level: 0},
|
|
},
|
|
},
|
|
},
|
|
}
|