| // 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 or at |
| // https://developers.google.com/open-source/licenses/bsd. |
| |
| package doc |
| |
| import ( |
| "bytes" |
| "errors" |
| "go/ast" |
| "go/build" |
| "go/doc" |
| "go/format" |
| "go/parser" |
| "go/token" |
| "regexp" |
| "sort" |
| "strings" |
| "time" |
| "unicode" |
| "unicode/utf8" |
| |
| "github.com/golang/gddo/gosrc" |
| ) |
| |
| func startsWithUppercase(s string) bool { |
| r, _ := utf8.DecodeRuneInString(s) |
| return unicode.IsUpper(r) |
| } |
| |
| var badSynopsisPrefixes = []string{ |
| "Autogenerated by Thrift Compiler", |
| "Automatically generated ", |
| "Auto-generated by ", |
| "Copyright ", |
| "COPYRIGHT ", |
| `THE SOFTWARE IS PROVIDED "AS IS"`, |
| "TODO: ", |
| "vim:", |
| } |
| |
| // synopsis extracts the first sentence from s. All runs of whitespace are |
| // replaced by a single space. |
| func synopsis(s string) string { |
| |
| parts := strings.SplitN(s, "\n\n", 2) |
| s = parts[0] |
| |
| var buf []byte |
| const ( |
| other = iota |
| period |
| space |
| ) |
| last := space |
| Loop: |
| for i := 0; i < len(s); i++ { |
| b := s[i] |
| switch b { |
| case ' ', '\t', '\r', '\n': |
| switch last { |
| case period: |
| break Loop |
| case other: |
| buf = append(buf, ' ') |
| last = space |
| } |
| case '.': |
| last = period |
| buf = append(buf, b) |
| default: |
| last = other |
| buf = append(buf, b) |
| } |
| } |
| |
| // Ensure that synopsis fits an App Engine datastore text property. |
| const m = 400 |
| if len(buf) > m { |
| buf = buf[:m] |
| if i := bytes.LastIndex(buf, []byte{' '}); i >= 0 { |
| buf = buf[:i] |
| } |
| buf = append(buf, " ..."...) |
| } |
| |
| s = string(buf) |
| |
| r, n := utf8.DecodeRuneInString(s) |
| if n < 0 || unicode.IsPunct(r) || unicode.IsSymbol(r) { |
| // ignore Markdown headings, editor settings, Go build constraints, and * in poorly formatted block comments. |
| s = "" |
| } else { |
| for _, prefix := range badSynopsisPrefixes { |
| if strings.HasPrefix(s, prefix) { |
| s = "" |
| break |
| } |
| } |
| } |
| |
| return s |
| } |
| |
| var referencesPats = []*regexp.Regexp{ |
| regexp.MustCompile(`"([-a-zA-Z0-9~+_./]+)"`), // quoted path |
| regexp.MustCompile(`https://drone\.io/([-a-zA-Z0-9~+_./]+)/status\.png`), |
| regexp.MustCompile(`\b(?:` + strings.Join([]string{ |
| `go\s+get\s+`, |
| `goinstall\s+`, |
| regexp.QuoteMeta("http://godoc.org/"), |
| regexp.QuoteMeta("http://gopkgdoc.appspot.com/pkg/"), |
| regexp.QuoteMeta("http://go.pkgdoc.org/"), |
| regexp.QuoteMeta("http://gowalker.org/"), |
| }, "|") + `)([-a-zA-Z0-9~+_./]+)`), |
| } |
| |
| // addReferences adds packages referenced in plain text s. |
| func addReferences(references map[string]bool, s []byte) { |
| for _, pat := range referencesPats { |
| for _, m := range pat.FindAllSubmatch(s, -1) { |
| p := string(m[1]) |
| if gosrc.IsValidRemotePath(p) { |
| references[p] = true |
| } |
| } |
| } |
| } |
| |
| type byFuncName []*doc.Func |
| |
| func (s byFuncName) Len() int { return len(s) } |
| func (s byFuncName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
| func (s byFuncName) Less(i, j int) bool { return s[i].Name < s[j].Name } |
| |
| func removeAssociations(dpkg *doc.Package) { |
| for _, t := range dpkg.Types { |
| dpkg.Funcs = append(dpkg.Funcs, t.Funcs...) |
| t.Funcs = nil |
| } |
| sort.Sort(byFuncName(dpkg.Funcs)) |
| } |
| |
| // builder holds the state used when building the documentation. |
| type builder struct { |
| srcs map[string]*source |
| fset *token.FileSet |
| examples []*doc.Example |
| buf []byte // scratch space for printNode method. |
| } |
| |
| type Value struct { |
| Decl Code |
| Pos Pos |
| Doc string |
| } |
| |
| func (b *builder) values(vdocs []*doc.Value) []*Value { |
| var result []*Value |
| for _, d := range vdocs { |
| result = append(result, &Value{ |
| Decl: b.printDecl(d.Decl), |
| Pos: b.position(d.Decl), |
| Doc: d.Doc, |
| }) |
| } |
| return result |
| } |
| |
| type Note struct { |
| Pos Pos |
| UID string |
| Body string |
| } |
| |
| type posNode token.Pos |
| |
| func (p posNode) Pos() token.Pos { return token.Pos(p) } |
| func (p posNode) End() token.Pos { return token.Pos(p) } |
| |
| func (b *builder) notes(gnotes map[string][]*doc.Note) map[string][]*Note { |
| if len(gnotes) == 0 { |
| return nil |
| } |
| notes := make(map[string][]*Note) |
| for tag, gvalues := range gnotes { |
| values := make([]*Note, len(gvalues)) |
| for i := range gvalues { |
| values[i] = &Note{ |
| Pos: b.position(posNode(gvalues[i].Pos)), |
| UID: gvalues[i].UID, |
| Body: strings.TrimSpace(gvalues[i].Body), |
| } |
| } |
| notes[tag] = values |
| } |
| return notes |
| } |
| |
| type Example struct { |
| Name string |
| Doc string |
| Code Code |
| Play string |
| Output string |
| } |
| |
| var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*output:`) |
| |
| func (b *builder) getExamples(name string) []*Example { |
| var docs []*Example |
| for _, e := range b.examples { |
| if !strings.HasPrefix(e.Name, name) { |
| continue |
| } |
| n := e.Name[len(name):] |
| if n != "" { |
| if i := strings.LastIndex(n, "_"); i != 0 { |
| continue |
| } |
| n = n[1:] |
| if startsWithUppercase(n) { |
| continue |
| } |
| n = strings.Title(n) |
| } |
| |
| code, output := b.printExample(e) |
| |
| play := "" |
| if e.Play != nil { |
| b.buf = b.buf[:0] |
| if err := format.Node(sliceWriter{&b.buf}, b.fset, e.Play); err != nil { |
| play = err.Error() |
| } else { |
| play = string(b.buf) |
| } |
| } |
| |
| docs = append(docs, &Example{ |
| Name: n, |
| Doc: e.Doc, |
| Code: code, |
| Output: output, |
| Play: play}) |
| } |
| return docs |
| } |
| |
| type Func struct { |
| Decl Code |
| Pos Pos |
| Doc string |
| Name string |
| Recv string // Actual receiver "T" or "*T". |
| Orig string // Original receiver "T" or "*T". This can be different from Recv due to embedding. |
| Examples []*Example |
| } |
| |
| func (b *builder) funcs(fdocs []*doc.Func) []*Func { |
| var result []*Func |
| for _, d := range fdocs { |
| var exampleName string |
| switch { |
| case d.Recv == "": |
| exampleName = d.Name |
| case d.Recv[0] == '*': |
| exampleName = d.Recv[1:] + "_" + d.Name |
| default: |
| exampleName = d.Recv + "_" + d.Name |
| } |
| result = append(result, &Func{ |
| Decl: b.printDecl(d.Decl), |
| Pos: b.position(d.Decl), |
| Doc: d.Doc, |
| Name: d.Name, |
| Recv: d.Recv, |
| Orig: d.Orig, |
| Examples: b.getExamples(exampleName), |
| }) |
| } |
| return result |
| } |
| |
| type Type struct { |
| Doc string |
| Name string |
| Decl Code |
| Pos Pos |
| Consts []*Value |
| Vars []*Value |
| Funcs []*Func |
| Methods []*Func |
| Examples []*Example |
| } |
| |
| func (b *builder) types(tdocs []*doc.Type) []*Type { |
| var result []*Type |
| for _, d := range tdocs { |
| result = append(result, &Type{ |
| Doc: d.Doc, |
| Name: d.Name, |
| Decl: b.printDecl(d.Decl), |
| Pos: b.position(d.Decl), |
| Consts: b.values(d.Consts), |
| Vars: b.values(d.Vars), |
| Funcs: b.funcs(d.Funcs), |
| Methods: b.funcs(d.Methods), |
| Examples: b.getExamples(d.Name), |
| }) |
| } |
| return result |
| } |
| |
| var packageNamePats = []*regexp.Regexp{ |
| // Last element with .suffix removed. |
| regexp.MustCompile(`/([^-./]+)[-.](?:git|svn|hg|bzr|v\d+)$`), |
| |
| // Last element with "go" prefix or suffix removed. |
| regexp.MustCompile(`/([^-./]+)[-.]go$`), |
| regexp.MustCompile(`/go[-.]([^-./]+)$`), |
| |
| // Special cases for popular repos. |
| regexp.MustCompile(`^code\.google\.com/p/google-api-go-client/([^/]+)/v[^/]+$`), |
| regexp.MustCompile(`^code\.google\.com/p/biogo\.([^/]+)$`), |
| |
| // It's also common for the last element of the path to contain an |
| // extra "go" prefix, but not always. TODO: examine unresolved ids to |
| // detect when trimming the "go" prefix is appropriate. |
| |
| // Last component of path. |
| regexp.MustCompile(`([^/]+)$`), |
| } |
| |
| func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { |
| pkg := imports[path] |
| if pkg != nil { |
| return pkg, nil |
| } |
| |
| // Guess the package name without importing it. |
| for _, pat := range packageNamePats { |
| m := pat.FindStringSubmatch(path) |
| if m != nil { |
| pkg = ast.NewObj(ast.Pkg, m[1]) |
| pkg.Data = ast.NewScope(nil) |
| imports[path] = pkg |
| return pkg, nil |
| } |
| } |
| |
| return nil, errors.New("package not found") |
| } |
| |
| type File struct { |
| Name string |
| URL string |
| } |
| |
| type Pos struct { |
| Line int32 // 0 if not valid. |
| N uint16 // number of lines - 1 |
| File int16 // index in Package.Files |
| } |
| |
| type source struct { |
| name string |
| browseURL string |
| data []byte |
| index int |
| } |
| |
| // PackageVersion is modified when previously stored packages are invalid. |
| const PackageVersion = "8" |
| |
| type Package struct { |
| // The import path for this package. |
| ImportPath string |
| |
| // Import path prefix for all packages in the project. |
| ProjectRoot string |
| |
| // Name of the project. |
| ProjectName string |
| |
| // Project home page. |
| ProjectURL string |
| |
| // Errors found when fetching or parsing this package. |
| Errors []string |
| |
| // Packages referenced in README files. |
| References []string |
| |
| // Version control system: git, hg, bzr, ... |
| VCS string |
| |
| // Version control: active or suppressed. |
| Status gosrc.DirectoryStatus |
| |
| // Whether the package is a fork of another one. |
| Fork bool |
| |
| // How many stars (for a GitHub project) or followers (for a BitBucket |
| // project) the repository of this package has. |
| Stars int |
| |
| // The time this object was created. |
| Updated time.Time |
| |
| // Cache validation tag. This tag is not necessarily an HTTP entity tag. |
| // The tag is "" if there is no meaningful cache validation for the VCS. |
| Etag string |
| |
| // Subdirectories, possibly containing Go code. |
| Subdirectories []string |
| |
| // Package name or "" if no package for this import path. The proceeding |
| // fields are set even if a package is not found for the import path. |
| Name string |
| |
| // Synopsis and full documentation for the package. |
| Synopsis string |
| Doc string |
| |
| // Format this package as a command. |
| IsCmd bool |
| |
| // True if package documentation is incomplete. |
| Truncated bool |
| |
| // Environment |
| GOOS, GOARCH string |
| |
| // Top-level declarations. |
| Consts []*Value |
| Funcs []*Func |
| Types []*Type |
| Vars []*Value |
| |
| // Package examples |
| Examples []*Example |
| |
| Notes map[string][]*Note |
| |
| // Source. |
| LineFmt string |
| BrowseURL string |
| Files []*File |
| TestFiles []*File |
| |
| // Source size in bytes. |
| SourceSize int |
| TestSourceSize int |
| |
| // Imports |
| Imports []string |
| TestImports []string |
| XTestImports []string |
| } |
| |
| var goEnvs = []struct{ GOOS, GOARCH string }{ |
| {"linux", "amd64"}, |
| {"darwin", "amd64"}, |
| {"windows", "amd64"}, |
| {"linux", "js"}, |
| } |
| |
| // SetDefaultGOOS sets given GOOS value as default one to use when building |
| // package documents. SetDefaultGOOS has no effect on some windows-only |
| // packages. |
| func SetDefaultGOOS(goos string) { |
| if goos == "" { |
| return |
| } |
| var i int |
| for ; i < len(goEnvs); i++ { |
| if goEnvs[i].GOOS == goos { |
| break |
| } |
| } |
| switch i { |
| case 0: |
| return |
| case len(goEnvs): |
| env := goEnvs[0] |
| env.GOOS = goos |
| goEnvs = append(goEnvs, env) |
| } |
| goEnvs[0], goEnvs[i] = goEnvs[i], goEnvs[0] |
| } |
| |
| var windowsOnlyPackages = map[string]bool{ |
| "internal/syscall/windows": true, |
| "internal/syscall/windows/registry": true, |
| "golang.org/x/exp/shiny/driver/internal/win32": true, |
| "golang.org/x/exp/shiny/driver/windriver": true, |
| "golang.org/x/sys/windows": true, |
| "golang.org/x/sys/windows/registry": true, |
| } |
| |
| func newPackage(dir *gosrc.Directory) (*Package, error) { |
| |
| pkg := &Package{ |
| Updated: time.Now().UTC(), |
| LineFmt: dir.LineFmt, |
| ImportPath: dir.ImportPath, |
| ProjectRoot: dir.ProjectRoot, |
| ProjectName: dir.ProjectName, |
| ProjectURL: dir.ProjectURL, |
| BrowseURL: dir.BrowseURL, |
| Etag: PackageVersion + "-" + dir.Etag, |
| VCS: dir.VCS, |
| Status: dir.Status, |
| Subdirectories: dir.Subdirectories, |
| Fork: dir.Fork, |
| Stars: dir.Stars, |
| } |
| |
| var b builder |
| b.srcs = make(map[string]*source) |
| references := make(map[string]bool) |
| for _, file := range dir.Files { |
| if strings.HasSuffix(file.Name, ".go") { |
| gosrc.OverwriteLineComments(file.Data) |
| b.srcs[file.Name] = &source{name: file.Name, browseURL: file.BrowseURL, data: file.Data} |
| } else { |
| addReferences(references, file.Data) |
| } |
| } |
| |
| for r := range references { |
| pkg.References = append(pkg.References, r) |
| } |
| |
| if len(b.srcs) == 0 { |
| return pkg, nil |
| } |
| |
| b.fset = token.NewFileSet() |
| |
| // Find the package and associated files. |
| |
| ctxt := build.Context{ |
| GOOS: "linux", |
| GOARCH: "amd64", |
| CgoEnabled: true, |
| ReleaseTags: build.Default.ReleaseTags, |
| BuildTags: build.Default.BuildTags, |
| Compiler: "gc", |
| } |
| |
| var err error |
| var bpkg *build.Package |
| |
| for _, env := range goEnvs { |
| // Some packages should be always displayed as GOOS=windows (see issue #16509 for details). |
| // TODO: remove this once issue #16509 is resolved. |
| if windowsOnlyPackages[dir.ImportPath] && env.GOOS != "windows" { |
| continue |
| } |
| |
| ctxt.GOOS = env.GOOS |
| ctxt.GOARCH = env.GOARCH |
| bpkg, err = dir.Import(&ctxt, build.ImportComment) |
| if _, ok := err.(*build.NoGoError); !ok { |
| break |
| } |
| } |
| if err != nil { |
| if _, ok := err.(*build.NoGoError); !ok { |
| pkg.Errors = append(pkg.Errors, err.Error()) |
| } |
| return pkg, nil |
| } |
| |
| if bpkg.ImportComment != "" && bpkg.ImportComment != dir.ImportPath { |
| return nil, gosrc.NotFoundError{ |
| Message: "not at canonical import path", |
| Redirect: bpkg.ImportComment, |
| } |
| } |
| |
| // Parse the Go files |
| |
| files := make(map[string]*ast.File) |
| names := append(bpkg.GoFiles, bpkg.CgoFiles...) |
| sort.Strings(names) |
| pkg.Files = make([]*File, len(names)) |
| for i, name := range names { |
| file, err := parser.ParseFile(b.fset, name, b.srcs[name].data, parser.ParseComments) |
| if err != nil { |
| pkg.Errors = append(pkg.Errors, err.Error()) |
| } else { |
| files[name] = file |
| } |
| src := b.srcs[name] |
| src.index = i |
| pkg.Files[i] = &File{Name: name, URL: src.browseURL} |
| pkg.SourceSize += len(src.data) |
| } |
| |
| apkg, _ := ast.NewPackage(b.fset, files, simpleImporter, nil) |
| |
| // Find examples in the test files. |
| |
| names = append(bpkg.TestGoFiles, bpkg.XTestGoFiles...) |
| sort.Strings(names) |
| pkg.TestFiles = make([]*File, len(names)) |
| for i, name := range names { |
| file, err := parser.ParseFile(b.fset, name, b.srcs[name].data, parser.ParseComments) |
| if err != nil { |
| pkg.Errors = append(pkg.Errors, err.Error()) |
| } else { |
| b.examples = append(b.examples, doc.Examples(file)...) |
| } |
| pkg.TestFiles[i] = &File{Name: name, URL: b.srcs[name].browseURL} |
| pkg.TestSourceSize += len(b.srcs[name].data) |
| } |
| |
| b.vetPackage(pkg, apkg) |
| |
| mode := doc.Mode(0) |
| if pkg.ImportPath == "builtin" { |
| mode |= doc.AllDecls |
| } |
| |
| dpkg := doc.New(apkg, pkg.ImportPath, mode) |
| |
| if pkg.ImportPath == "builtin" { |
| removeAssociations(dpkg) |
| } |
| |
| pkg.Name = dpkg.Name |
| pkg.Doc = strings.TrimRight(dpkg.Doc, " \t\n\r") |
| pkg.Synopsis = synopsis(pkg.Doc) |
| |
| pkg.Examples = b.getExamples("") |
| pkg.IsCmd = bpkg.IsCommand() |
| pkg.GOOS = ctxt.GOOS |
| pkg.GOARCH = ctxt.GOARCH |
| |
| pkg.Consts = b.values(dpkg.Consts) |
| pkg.Funcs = b.funcs(dpkg.Funcs) |
| pkg.Types = b.types(dpkg.Types) |
| pkg.Vars = b.values(dpkg.Vars) |
| pkg.Notes = b.notes(dpkg.Notes) |
| |
| pkg.Imports = bpkg.Imports |
| pkg.TestImports = bpkg.TestImports |
| pkg.XTestImports = bpkg.XTestImports |
| |
| return pkg, nil |
| } |