| // 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 |
| } |