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