mirror of https://go.googlesource.com/go
337 lines
9.6 KiB
Go
337 lines
9.6 KiB
Go
// Copyright 2013 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_test
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/doc"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"internal/diff"
|
|
"internal/txtar"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestExamples(t *testing.T) {
|
|
dir := filepath.Join("testdata", "examples")
|
|
filenames, err := filepath.Glob(filepath.Join(dir, "*.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, filename := range filenames {
|
|
t.Run(strings.TrimSuffix(filepath.Base(filename), ".go"), func(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
astFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
goldenFilename := strings.TrimSuffix(filename, ".go") + ".golden"
|
|
archive, err := txtar.ParseFile(goldenFilename)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
golden := map[string]string{}
|
|
for _, f := range archive.Files {
|
|
golden[f.Name] = strings.TrimSpace(string(f.Data))
|
|
}
|
|
|
|
// Collect the results of doc.Examples in a map keyed by example name.
|
|
examples := map[string]*doc.Example{}
|
|
for _, e := range doc.Examples(astFile) {
|
|
examples[e.Name] = e
|
|
// Treat missing sections in the golden as empty.
|
|
for _, kind := range []string{"Play", "Output"} {
|
|
key := e.Name + "." + kind
|
|
if _, ok := golden[key]; !ok {
|
|
golden[key] = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
// Each section in the golden file corresponds to an example we expect
|
|
// to see.
|
|
for sectionName, want := range golden {
|
|
name, kind, found := strings.Cut(sectionName, ".")
|
|
if !found {
|
|
t.Fatalf("bad section name %q, want EXAMPLE_NAME.KIND", sectionName)
|
|
}
|
|
ex := examples[name]
|
|
if ex == nil {
|
|
t.Fatalf("no example named %q", name)
|
|
}
|
|
|
|
var got string
|
|
switch kind {
|
|
case "Play":
|
|
got = strings.TrimSpace(formatFile(t, fset, ex.Play))
|
|
|
|
case "Output":
|
|
got = strings.TrimSpace(ex.Output)
|
|
default:
|
|
t.Fatalf("bad section kind %q", kind)
|
|
}
|
|
|
|
if got != want {
|
|
t.Errorf("%s mismatch:\n%s", sectionName,
|
|
diff.Diff("want", []byte(want), "got", []byte(got)))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func formatFile(t *testing.T, fset *token.FileSet, n *ast.File) string {
|
|
t.Helper()
|
|
if n == nil {
|
|
return "<nil>"
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := format.Node(&buf, fset, n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// This example illustrates how to use NewFromFiles
|
|
// to compute package documentation with examples.
|
|
func ExampleNewFromFiles() {
|
|
// src and test are two source files that make up
|
|
// a package whose documentation will be computed.
|
|
const src = `
|
|
// This is the package comment.
|
|
package p
|
|
|
|
import "fmt"
|
|
|
|
// This comment is associated with the Greet function.
|
|
func Greet(who string) {
|
|
fmt.Printf("Hello, %s!\n", who)
|
|
}
|
|
`
|
|
const test = `
|
|
package p_test
|
|
|
|
// This comment is associated with the ExampleGreet_world example.
|
|
func ExampleGreet_world() {
|
|
Greet("world")
|
|
}
|
|
`
|
|
|
|
// Create the AST by parsing src and test.
|
|
fset := token.NewFileSet()
|
|
files := []*ast.File{
|
|
mustParse(fset, "src.go", src),
|
|
mustParse(fset, "src_test.go", test),
|
|
}
|
|
|
|
// Compute package documentation with examples.
|
|
p, err := doc.NewFromFiles(fset, files, "example.com/p")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
fmt.Printf("package %s - %s", p.Name, p.Doc)
|
|
fmt.Printf("func %s - %s", p.Funcs[0].Name, p.Funcs[0].Doc)
|
|
fmt.Printf(" ⤷ example with suffix %q - %s", p.Funcs[0].Examples[0].Suffix, p.Funcs[0].Examples[0].Doc)
|
|
|
|
// Output:
|
|
// package p - This is the package comment.
|
|
// func Greet - This comment is associated with the Greet function.
|
|
// ⤷ example with suffix "world" - This comment is associated with the ExampleGreet_world example.
|
|
}
|
|
|
|
func TestClassifyExamples(t *testing.T) {
|
|
const src = `
|
|
package p
|
|
|
|
const Const1 = 0
|
|
var Var1 = 0
|
|
|
|
type (
|
|
Type1 int
|
|
Type1_Foo int
|
|
Type1_foo int
|
|
type2 int
|
|
|
|
Embed struct { Type1 }
|
|
Uembed struct { type2 }
|
|
)
|
|
|
|
func Func1() {}
|
|
func Func1_Foo() {}
|
|
func Func1_foo() {}
|
|
func func2() {}
|
|
|
|
func (Type1) Func1() {}
|
|
func (Type1) Func1_Foo() {}
|
|
func (Type1) Func1_foo() {}
|
|
func (Type1) func2() {}
|
|
|
|
func (type2) Func1() {}
|
|
|
|
type (
|
|
Conflict int
|
|
Conflict_Conflict int
|
|
Conflict_conflict int
|
|
)
|
|
|
|
func (Conflict) Conflict() {}
|
|
|
|
func GFunc[T any]() {}
|
|
|
|
type GType[T any] int
|
|
|
|
func (GType[T]) M() {}
|
|
`
|
|
const test = `
|
|
package p_test
|
|
|
|
func ExampleConst1() {} // invalid - no support for consts and vars
|
|
func ExampleVar1() {} // invalid - no support for consts and vars
|
|
|
|
func Example() {}
|
|
func Example_() {} // invalid - suffix must start with a lower-case letter
|
|
func Example_suffix() {}
|
|
func Example_suffix_xX_X_x() {}
|
|
func Example_世界() {} // invalid - suffix must start with a lower-case letter
|
|
func Example_123() {} // invalid - suffix must start with a lower-case letter
|
|
func Example_BadSuffix() {} // invalid - suffix must start with a lower-case letter
|
|
|
|
func ExampleType1() {}
|
|
func ExampleType1_() {} // invalid - suffix must start with a lower-case letter
|
|
func ExampleType1_suffix() {}
|
|
func ExampleType1_BadSuffix() {} // invalid - suffix must start with a lower-case letter
|
|
func ExampleType1_Foo() {}
|
|
func ExampleType1_Foo_suffix() {}
|
|
func ExampleType1_Foo_BadSuffix() {} // invalid - suffix must start with a lower-case letter
|
|
func ExampleType1_foo() {}
|
|
func ExampleType1_foo_suffix() {}
|
|
func ExampleType1_foo_Suffix() {} // matches Type1, instead of Type1_foo
|
|
func Exampletype2() {} // invalid - cannot match unexported
|
|
|
|
func ExampleFunc1() {}
|
|
func ExampleFunc1_() {} // invalid - suffix must start with a lower-case letter
|
|
func ExampleFunc1_suffix() {}
|
|
func ExampleFunc1_BadSuffix() {} // invalid - suffix must start with a lower-case letter
|
|
func ExampleFunc1_Foo() {}
|
|
func ExampleFunc1_Foo_suffix() {}
|
|
func ExampleFunc1_Foo_BadSuffix() {} // invalid - suffix must start with a lower-case letter
|
|
func ExampleFunc1_foo() {}
|
|
func ExampleFunc1_foo_suffix() {}
|
|
func ExampleFunc1_foo_Suffix() {} // matches Func1, instead of Func1_foo
|
|
func Examplefunc1() {} // invalid - cannot match unexported
|
|
|
|
func ExampleType1_Func1() {}
|
|
func ExampleType1_Func1_() {} // invalid - suffix must start with a lower-case letter
|
|
func ExampleType1_Func1_suffix() {}
|
|
func ExampleType1_Func1_BadSuffix() {} // invalid - suffix must start with a lower-case letter
|
|
func ExampleType1_Func1_Foo() {}
|
|
func ExampleType1_Func1_Foo_suffix() {}
|
|
func ExampleType1_Func1_Foo_BadSuffix() {} // invalid - suffix must start with a lower-case letter
|
|
func ExampleType1_Func1_foo() {}
|
|
func ExampleType1_Func1_foo_suffix() {}
|
|
func ExampleType1_Func1_foo_Suffix() {} // matches Type1.Func1, instead of Type1.Func1_foo
|
|
func ExampleType1_func2() {} // matches Type1, instead of Type1.func2
|
|
|
|
func ExampleEmbed_Func1() {} // invalid - no support for forwarded methods from embedding exported type
|
|
func ExampleUembed_Func1() {} // methods from embedding unexported types are OK
|
|
func ExampleUembed_Func1_suffix() {}
|
|
|
|
func ExampleConflict_Conflict() {} // ambiguous with either Conflict or Conflict_Conflict type
|
|
func ExampleConflict_conflict() {} // ambiguous with either Conflict or Conflict_conflict type
|
|
func ExampleConflict_Conflict_suffix() {} // ambiguous with either Conflict or Conflict_Conflict type
|
|
func ExampleConflict_conflict_suffix() {} // ambiguous with either Conflict or Conflict_conflict type
|
|
|
|
func ExampleGFunc() {}
|
|
func ExampleGFunc_suffix() {}
|
|
|
|
func ExampleGType_M() {}
|
|
func ExampleGType_M_suffix() {}
|
|
`
|
|
|
|
// Parse literal source code as a *doc.Package.
|
|
fset := token.NewFileSet()
|
|
files := []*ast.File{
|
|
mustParse(fset, "src.go", src),
|
|
mustParse(fset, "src_test.go", test),
|
|
}
|
|
p, err := doc.NewFromFiles(fset, files, "example.com/p")
|
|
if err != nil {
|
|
t.Fatalf("doc.NewFromFiles: %v", err)
|
|
}
|
|
|
|
// Collect the association of examples to top-level identifiers.
|
|
got := map[string][]string{}
|
|
got[""] = exampleNames(p.Examples)
|
|
for _, f := range p.Funcs {
|
|
got[f.Name] = exampleNames(f.Examples)
|
|
}
|
|
for _, t := range p.Types {
|
|
got[t.Name] = exampleNames(t.Examples)
|
|
for _, f := range t.Funcs {
|
|
got[f.Name] = exampleNames(f.Examples)
|
|
}
|
|
for _, m := range t.Methods {
|
|
got[t.Name+"."+m.Name] = exampleNames(m.Examples)
|
|
}
|
|
}
|
|
|
|
want := map[string][]string{
|
|
"": {"", "suffix", "suffix_xX_X_x"}, // Package-level examples.
|
|
|
|
"Type1": {"", "foo_Suffix", "func2", "suffix"},
|
|
"Type1_Foo": {"", "suffix"},
|
|
"Type1_foo": {"", "suffix"},
|
|
|
|
"Func1": {"", "foo_Suffix", "suffix"},
|
|
"Func1_Foo": {"", "suffix"},
|
|
"Func1_foo": {"", "suffix"},
|
|
|
|
"Type1.Func1": {"", "foo_Suffix", "suffix"},
|
|
"Type1.Func1_Foo": {"", "suffix"},
|
|
"Type1.Func1_foo": {"", "suffix"},
|
|
|
|
"Uembed.Func1": {"", "suffix"},
|
|
|
|
// These are implementation dependent due to the ambiguous parsing.
|
|
"Conflict_Conflict": {"", "suffix"},
|
|
"Conflict_conflict": {"", "suffix"},
|
|
|
|
"GFunc": {"", "suffix"},
|
|
"GType.M": {"", "suffix"},
|
|
}
|
|
|
|
for id := range got {
|
|
if !reflect.DeepEqual(got[id], want[id]) {
|
|
t.Errorf("classification mismatch for %q:\ngot %q\nwant %q", id, got[id], want[id])
|
|
}
|
|
delete(want, id)
|
|
}
|
|
if len(want) > 0 {
|
|
t.Errorf("did not find:\n%q", want)
|
|
}
|
|
}
|
|
|
|
func exampleNames(exs []*doc.Example) (out []string) {
|
|
for _, ex := range exs {
|
|
out = append(out, ex.Suffix)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func mustParse(fset *token.FileSet, filename, src string) *ast.File {
|
|
f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return f
|
|
}
|