// Copyright 2011 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.// Extract example functions from file ASTs.package docimport ()// An Example represents an example function found in a test source file.typeExamplestruct { Name string// name of the item being exemplified (including optional suffix) Suffix string// example suffix, without leading '_' (only populated by NewFromFiles) Doc string// example function doc string Code ast.Node Play *ast.File// a whole program version of the example Comments []*ast.CommentGroup Output string// expected output Unordered bool EmptyOutput bool// expect empty output Order int// original source code order}// Examples returns the examples found in testFiles, sorted by Name field.// The Order fields record the order in which the examples were encountered.// The Suffix field is not populated when Examples is called directly, it is// only populated by [NewFromFiles] for examples it finds in _test.go files.//// Playable Examples must be in a package whose name ends in "_test".// An Example is "playable" (the Play field is non-nil) in either of these// circumstances:// - The example function is self-contained: the function references only// identifiers from other packages (or predeclared identifiers, such as// "int") and the test file does not include a dot import.// - The entire test file is the example: the file contains exactly one// example function, zero test, fuzz test, or benchmark function, and at// least one top-level function, type, variable, or constant declaration// other than the example function.func ( ...*ast.File) []*Example {var []*Examplefor , := range { := false// file contains tests, fuzz test, or benchmarks := 0// number of non-import declarations in the filevar []*Examplefor , := range .Decls {if , := .(*ast.GenDecl); && .Tok != token.IMPORT { ++continue } , := .(*ast.FuncDecl)if ! || .Recv != nil {continue } ++ := .Name.NameifisTest(, "Test") || isTest(, "Benchmark") || isTest(, "Fuzz") { = truecontinue }if !isTest(, "Example") {continue }if := .Type.Params; len(.List) != 0 {continue// function has params; not a valid example }if .Body == nil { // ast.File.Body nil dereference (see issue 28044)continue }varstringif .Doc != nil { = .Doc.Text() } , , := exampleOutput(.Body, .Comments) = append(, &Example{Name: [len("Example"):],Doc: ,Code: .Body,Play: playExample(, ),Comments: .Comments,Output: ,Unordered: ,EmptyOutput: == "" && ,Order: len(), }) }if ! && > 1 && len() == 1 {// If this file only has one example function, some // other top-level declarations, and no tests or // benchmarks, use the whole file as the example. [0].Code = [0].Play = playExampleFile() } = append(, ...) }// sort by nameslices.SortFunc(, func(, *Example) int {returncmp.Compare(.Name, .Name) })return}var outputPrefix = lazyregexp.New(`(?i)^[[:space:]]*(unordered )?output:`)// Extracts the expected output and whether there was a valid output comment.func exampleOutput( *ast.BlockStmt, []*ast.CommentGroup) ( string, , bool) {if , := lastComment(, ); != nil {// test that it begins with the correct prefix := .Text()if := outputPrefix.FindStringSubmatchIndex(); != nil {if [2] != -1 { = true } = [[1]:]// Strip zero or more spaces followed by \n or a single space. = strings.TrimLeft(, " ")iflen() > 0 && [0] == '\n' { = [1:] }return , , true } }return"", false, false// no suitable comment found}// isTest tells whether name looks like a test, example, fuzz test, or// benchmark. It is a Test (say) if there is a character after Test that is not// a lower-case letter. (We don't want Testiness.)func isTest(, string) bool {if !strings.HasPrefix(, ) {returnfalse }iflen() == len() { // "Test" is okreturntrue } , := utf8.DecodeRuneInString([len():])return !unicode.IsLower()}// playExample synthesizes a new *ast.File based on the provided// file with the provided function body as the body of main.func playExample( *ast.File, *ast.FuncDecl) *ast.File { := .Bodyif !strings.HasSuffix(.Name.Name, "_test") {// We don't support examples that are part of the // greater package (yet).returnnil }// Collect top-level declarations in the file. := make(map[*ast.Object]ast.Decl) := make(map[string][]ast.Decl)for , := range .Decls {switch d := .(type) {case *ast.FuncDecl:if .Recv == nil { [.Name.Obj] = } else {iflen(.Recv.List) == 1 { := .Recv.List[0].Type , := baseTypeName() [] = append([], ) } }case *ast.GenDecl:for , := range .Specs {switch s := .(type) {case *ast.TypeSpec: [.Name.Obj] = case *ast.ValueSpec:for , := range .Names { [.Obj] = } } } } }// Find unresolved identifiers and uses of top-level declarations. , := findDeclsAndUnresolved(, , )// Remove predeclared identifiers from unresolved list.for := range {ifpredeclaredTypes[] || predeclaredConstants[] || predeclaredFuncs[] {delete(, ) } }// Use unresolved identifiers to determine the imports used by this // example. The heuristic assumes package names match base import // paths for imports w/o renames (should be good enough most of the time).var []ast.Specvar []ast.Spec// _ imports// To preserve the blank lines between groups of imports, find the // start position of each group, and assign that position to all // imports from that group. := findImportGroupStarts(.Imports) := func( *ast.ImportSpec) token.Pos {for , := range {if .Path.ValuePos < {return [-1] } }return [len()-1] }for , := range .Imports { , := strconv.Unquote(.Path.Value)if != nil {continue }if == "syscall/js" {// We don't support examples that import syscall/js, // because the package syscall/js is not available in the playground.returnnil } := path.Base()if .Name != nil { = .Name.Nameswitch {case"_": = append(, )continuecase".":// We can't resolve dot imports (yet).returnnil } }if [] {// Copy the spec and its path to avoid modifying the original. := * := *.Path .Path = & .Path.ValuePos = (&) = append(, &)delete(, ) } }// If there are other unresolved identifiers, give up because this // synthesized file is not going to build.iflen() > 0 {returnnil }// Include documentation belonging to blank imports.var []*ast.CommentGroupfor , := range {if := .(*ast.ImportSpec).Doc; != nil { = append(, ) } }// Include comments that are inside the function body.for , := range .Comments {if .Pos() <= .Pos() && .End() <= .End() { = append(, ) } }// Strip the "Output:" or "Unordered output:" comment and adjust body // end position. , = stripOutputComment(, )// Include documentation belonging to dependent declarations.for , := range {switch d := .(type) {case *ast.GenDecl:if .Doc != nil { = append(, .Doc) }case *ast.FuncDecl:if .Doc != nil { = append(, .Doc) } } }// Synthesize import declaration. := &ast.GenDecl{Tok: token.IMPORT,Lparen: 1, // Need non-zero Lparen and Rparen so that printerRparen: 1, // treats this as a factored import. } .Specs = append(, ...)// Synthesize main function. := &ast.FuncDecl{Name: ast.NewIdent("main"),Type: .Type,Body: , } := make([]ast.Decl, 0, 2+len()) = append(, ) = append(, ...) = append(, )slices.SortFunc(, func(, ast.Decl) int {returncmp.Compare(.Pos(), .Pos()) })slices.SortFunc(, func(, *ast.CommentGroup) int {returncmp.Compare(.Pos(), .Pos()) })// Synthesize file.return &ast.File{Name: ast.NewIdent("main"),Decls: ,Comments: , }}// findDeclsAndUnresolved returns all the top-level declarations mentioned in// the body, and a set of unresolved symbols (those that appear in the body but// have no declaration in the program).//// topDecls maps objects to the top-level declaration declaring them (not// necessarily obj.Decl, as obj.Decl will be a Spec for GenDecls, but// topDecls[obj] will be the GenDecl itself).func findDeclsAndUnresolved( ast.Node, map[*ast.Object]ast.Decl, map[string][]ast.Decl) ([]ast.Decl, map[string]bool) {// This function recursively finds every top-level declaration used // transitively by the body, populating usedDecls and usedObjs. Then it // trims down the declarations to include only the symbols actually // referenced by the body. := make(map[string]bool)var []ast.Decl := make(map[ast.Decl]bool) // set of top-level decls reachable from the body := make(map[*ast.Object]bool) // set of objects reachable from the body (each declared by a usedDecl)varfunc(ast.Node) bool = func( ast.Node) bool {switch e := .(type) {case *ast.Ident:if .Obj == nil && .Name != "_" { [.Name] = true } elseif := [.Obj]; != nil { [.Obj] = trueif ![] { [] = true = append(, ) } }returntruecase *ast.SelectorExpr:// For selector expressions, only inspect the left hand side. // (For an expression like fmt.Println, only add "fmt" to the // set of unresolved names, not "Println".)ast.Inspect(.X, )returnfalsecase *ast.KeyValueExpr:// For key value expressions, only inspect the value // as the key should be resolved by the type of the // composite literal.ast.Inspect(.Value, )returnfalse }returntrue } := func( *ast.FieldList) {if != nil {for , := range .List {ast.Inspect(.Type, ) } } }// Find the decls immediately referenced by body.ast.Inspect(, )// Now loop over them, adding to the list when we find a new decl that the // body depends on. Keep going until we don't find anything new.for := 0; < len(); ++ {switch d := [].(type) {case *ast.FuncDecl:// Inspect type parameters. (.Type.TypeParams)// Inspect types of parameters and results. See #28492. (.Type.Params) (.Type.Results)// Functions might not have a body. See #42706.if .Body != nil {ast.Inspect(.Body, ) }case *ast.GenDecl:for , := range .Specs {switch s := .(type) {case *ast.TypeSpec: (.TypeParams)ast.Inspect(.Type, ) = append(, [.Name.Name]...)case *ast.ValueSpec:if .Type != nil {ast.Inspect(.Type, ) }for , := range .Values {ast.Inspect(, ) } } } } }// Some decls include multiple specs, such as a variable declaration with // multiple variables on the same line, or a parenthesized declaration. Trim // the declarations to include only the specs that are actually mentioned. // However, if there is a constant group with iota, leave it all: later // constant declarations in the group may have no value and so cannot stand // on their own, and removing any constant from the group could change the // values of subsequent ones. // See testdata/examples/iota.go for a minimal example.var []ast.Declfor , := range {switch d := .(type) {case *ast.FuncDecl: = append(, )case *ast.GenDecl: := false// does any spec have iota?// Collect all Specs that were mentioned in the example.var []ast.Specfor , := range .Specs {switch s := .(type) {case *ast.TypeSpec:if [.Name.Obj] { = append(, ) }case *ast.ValueSpec:if ! { = hasIota() }// A ValueSpec may have multiple names (e.g. "var a, b int"). // Keep only the names that were mentioned in the example. // Exception: the multiple names have a single initializer (which // would be a function call with multiple return values). In that // case, keep everything.iflen(.Names) > 1 && len(.Values) == 1 { = append(, )continue } := * .Names = nil .Values = nilfor , := range .Names {if [.Obj] { .Names = append(.Names, )if .Values != nil { .Values = append(.Values, .Values[]) } } }iflen(.Names) > 0 { = append(, &) } } }iflen() > 0 {// Constant with iota? Keep it all.if .Tok == token.CONST && { = append(, ) } else {// Synthesize a GenDecl with just the Specs we need. := * // copy the GenDecl .Specs = iflen() == 1 {// Remove grouping parens if there is only one spec. .Lparen = 0 } = append(, &) } } } }return , }func hasIota( ast.Spec) bool { := falseast.Inspect(, func( ast.Node) bool {// Check that this is the special built-in "iota" identifier, not // a user-defined shadow.if , := .(*ast.Ident); && .Name == "iota" && .Obj == nil { = truereturnfalse }returntrue })return}// findImportGroupStarts finds the start positions of each sequence of import// specs that are not separated by a blank line.func findImportGroupStarts( []*ast.ImportSpec) []token.Pos { := findImportGroupStarts1() := make([]token.Pos, len())for , := range { [] = .Pos() }return}// Helper for findImportGroupStarts to ease testing.func findImportGroupStarts1( []*ast.ImportSpec) []*ast.ImportSpec {// Copy to avoid mutation. := make([]*ast.ImportSpec, len())copy(, )// Assume the imports are sorted by position.slices.SortFunc(, func(, *ast.ImportSpec) int {returncmp.Compare(.Pos(), .Pos()) })// Assume gofmt has been applied, so there is a blank line between adjacent imps // if and only if they are more than 2 positions apart (newline, tab).var []*ast.ImportSpec := token.Pos(-2)for , := range {if .Pos()- > 2 { = append(, ) } = .End()// Account for end-of-line comments.if .Comment != nil { = .Comment.End() } }return}// playExampleFile takes a whole file example and synthesizes a new *ast.File// such that the example is function main in package main.func playExampleFile( *ast.File) *ast.File {// Strip copyright comment if present. := .Commentsiflen() > 0 && strings.HasPrefix([0].Text(), "Copyright") { = [1:] }// Copy declaration slice, rewriting the ExampleX function to main.var []ast.Declfor , := range .Decls {if , := .(*ast.FuncDecl); && isTest(.Name.Name, "Example") {// Copy the FuncDecl, as it may be used elsewhere. := * .Name = ast.NewIdent("main") .Body, = stripOutputComment(.Body, ) = & } = append(, ) }// Copy the File, as it may be used elsewhere. := * .Name = ast.NewIdent("main") .Decls = .Comments = return &}// stripOutputComment finds and removes the "Output:" or "Unordered output:"// comment from body and comments, and adjusts the body block's end position.func stripOutputComment( *ast.BlockStmt, []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {// Do nothing if there is no "Output:" or "Unordered output:" comment. , := lastComment(, )if == nil || !outputPrefix.MatchString(.Text()) {return , }// Copy body and comments, as the originals may be used elsewhere. := &ast.BlockStmt{Lbrace: .Lbrace,List: .List,Rbrace: .Pos(), } := make([]*ast.CommentGroup, len()-1)copy(, [:])copy([:], [+1:])return , }// lastComment returns the last comment inside the provided block.func lastComment( *ast.BlockStmt, []*ast.CommentGroup) ( int, *ast.CommentGroup) {if == nil {return } , := .Pos(), .End()for , := range {if .Pos() < {continue }if .End() > {break } , = , }return}// classifyExamples classifies examples and assigns them to the Examples field// of the relevant Func, Type, or Package that the example is associated with.//// The classification process is ambiguous in some cases://// - ExampleFoo_Bar matches a type named Foo_Bar// or a method named Foo.Bar.// - ExampleFoo_bar matches a type named Foo_bar// or Foo (with a "bar" suffix).//// Examples with malformed names are not associated with anything.func classifyExamples( *Package, []*Example) {iflen() == 0 {return }// Mapping of names for funcs, types, and methods to the example listing. := make(map[string]*[]*Example) [""] = &.Examples// package-level examples have an empty namefor , := range .Funcs {if !token.IsExported(.Name) {continue } [.Name] = &.Examples }for , := range .Types {if !token.IsExported(.Name) {continue } [.Name] = &.Examplesfor , := range .Funcs {if !token.IsExported(.Name) {continue } [.Name] = &.Examples }for , := range .Methods {if !token.IsExported(.Name) {continue } [strings.TrimPrefix(nameWithoutInst(.Recv), "*")+"_"+.Name] = &.Examples } }// Group each example with the associated func, type, or method.for , := range {// Consider all possible split points for the suffix // by starting at the end of string (no suffix case), // then trying all positions that contain a '_' character. // // An association is made on the first successful match. // Examples with malformed names that match nothing are skipped.for := len(.Name); >= 0; = strings.LastIndexByte(.Name[:], '_') { , , := splitExampleName(.Name, )if ! {continue } , := []if ! {continue } .Suffix = * = append(*, )break } }// Sort list of example according to the user-specified suffix name.for , := range {slices.SortFunc(*, func(, *Example) int {returncmp.Compare(.Suffix, .Suffix) }) }}// nameWithoutInst returns name if name has no brackets. If name contains// brackets, then it returns name with all the contents between (and including)// the outermost left and right bracket removed.//// Adapted from debug/gosym/symtab.go:Sym.nameWithoutInst.func nameWithoutInst( string) string { := strings.Index(, "[")if < 0 {return } := strings.LastIndex(, "]")if < 0 {// Malformed name, should contain closing bracket too.return }return [0:] + [+1:]}// splitExampleName attempts to split example name s at index i,// and reports if that produces a valid split. The suffix may be// absent. Otherwise, it must start with a lower-case letter and// be preceded by '_'.//// One of i == len(s) or s[i] == '_' must be true.func splitExampleName( string, int) (, string, bool) {if == len() {return , "", true }if == len()-1 {return"", "", false } , = [:], [+1:]return , , isExampleSuffix()}func isExampleSuffix( string) bool { , := utf8.DecodeRuneInString()return > 0 && unicode.IsLower()}
The pages are generated with Goldsv0.7.0-preview. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.