Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
611 lines (519 sloc) 13.6 KB
// Copyright 2011 Gary Burd
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package doc
import (
"bytes"
"errors"
"go/ast"
"go/build"
"go/doc"
"go/format"
"go/parser"
"go/token"
"io"
"io/ioutil"
"os"
"path"
"regexp"
"sort"
"strings"
"time"
"unicode"
"unicode/utf8"
)
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 referencePat = regexp.MustCompile(`\b(?:go\s+get\s+|goinstall\s+|http://gopkgdoc\.appspot\.com/pkg/|http://go\.pkgdoc\.org/|http://godoc\.org/)([-a-zA-Z0-9~+_./]+)`)
var quotedReferencePat = regexp.MustCompile(`"([-a-zA-Z0-9~+_./]+)"`)
// addReferences adds packages referenced in plain text s.
func addReferences(references map[string]bool, s []byte) {
for _, m := range referencePat.FindAllSubmatch(s, -1) {
p := string(m[1])
if IsValidRemotePath(p) {
references[p] = true
}
}
for _, m := range quotedReferencePat.FindAllSubmatch(s, -1) {
p := string(m[1])
if IsValidRemotePath(p) {
references[p] = true
}
}
}
// builder holds the state used when building the documentation.
type builder struct {
pdoc *Package
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
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,
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{
// Strip suffix and prefix separated by illegal id runes "." and "-".
regexp.MustCompile(`/([^-./]+)[-.](?:go|git)$`),
regexp.MustCompile(`/(?:go)[-.]([^-./]+)$`),
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 {
// 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
break
}
}
}
if pkg == nil {
return nil, errors.New("Failed to match")
}
return pkg, nil
}
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
rawURL string
data []byte
index int
}
func (s *source) Name() string { return s.name }
func (s *source) Size() int64 { return int64(len(s.data)) }
func (s *source) Mode() os.FileMode { return 0 }
func (s *source) ModTime() time.Time { return time.Time{} }
func (s *source) IsDir() bool { return false }
func (s *source) Sys() interface{} { return nil }
func (b *builder) readDir(dir string) ([]os.FileInfo, error) {
if dir != "/" {
panic("unexpected")
}
fis := make([]os.FileInfo, 0, len(b.srcs))
for _, src := range b.srcs {
fis = append(fis, src)
}
return fis, nil
}
func (b *builder) openFile(path string) (io.ReadCloser, error) {
if src, ok := b.srcs[path[1:]]; ok {
return ioutil.NopCloser(bytes.NewReader(src.data)), nil
}
panic("unexpected")
}
// PackageVersion is modified when previously stored packages are invalid.
const PackageVersion = "6"
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
// 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
// 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 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
Bugs []string
// Source.
LineFmt string
BrowseURL string
Files []*File
TestFiles []*File
// Source size in bytes.
SourceSize int
TestSourceSize int
// Imports
Imports []string
TestImports []string
XTestImports []string
// The number of stargazers/watchers
StarCount int
// Filename and content of readme.* files
ReadmeFiles map[string][]byte
}
var goEnvs = []struct{ GOOS, GOARCH string }{
{"linux", "amd64"},
{"darwin", "amd64"},
{"windows", "amd64"},
}
func (b *builder) build(srcs []*source) (*Package, error) {
b.pdoc.Updated = time.Now().UTC()
references := make(map[string]bool)
b.srcs = make(map[string]*source)
for _, src := range srcs {
if strings.HasSuffix(src.name, ".go") {
b.srcs[src.name] = src
} else {
addReferences(references, src.data)
fn := strings.ToLower(src.name)
if fn == "readme" || strings.HasPrefix(fn, "readme.") {
if b.pdoc.ReadmeFiles == nil {
b.pdoc.ReadmeFiles = make(map[string][]byte)
}
b.pdoc.ReadmeFiles[src.name] = src.data
}
}
}
for r := range references {
b.pdoc.References = append(b.pdoc.References, r)
}
if len(b.srcs) == 0 {
return b.pdoc, nil
}
b.fset = token.NewFileSet()
// Find the package and associated files.
ctxt := build.Context{
GOOS: "linux",
GOARCH: "amd64",
CgoEnabled: true,
ReleaseTags: build.Default.ReleaseTags,
JoinPath: path.Join,
IsAbsPath: path.IsAbs,
SplitPathList: func(list string) []string { return strings.Split(list, ":") },
IsDir: func(path string) bool { panic("unexpected") },
HasSubdir: func(root, dir string) (rel string, ok bool) { panic("unexpected") },
ReadDir: func(dir string) (fi []os.FileInfo, err error) { return b.readDir(dir) },
OpenFile: func(path string) (r io.ReadCloser, err error) { return b.openFile(path) },
Compiler: "gc",
}
var err error
var bpkg *build.Package
for _, env := range goEnvs {
ctxt.GOOS = env.GOOS
ctxt.GOARCH = env.GOARCH
bpkg, err = ctxt.ImportDir("/", 0)
if _, ok := err.(*build.NoGoError); !ok {
break
}
}
if err != nil {
if _, ok := err.(*build.NoGoError); !ok {
b.pdoc.Errors = append(b.pdoc.Errors, err.Error())
}
return b.pdoc, nil
}
// Parse the Go files
files := make(map[string]*ast.File)
names := append(bpkg.GoFiles, bpkg.CgoFiles...)
sort.Strings(names)
b.pdoc.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 {
b.pdoc.Errors = append(b.pdoc.Errors, err.Error())
continue
}
src := b.srcs[name]
src.index = i
b.pdoc.Files[i] = &File{Name: name, URL: src.browseURL}
b.pdoc.SourceSize += len(src.data)
files[name] = file
}
apkg, _ := ast.NewPackage(b.fset, files, simpleImporter, nil)
// Find examples in the test files.
names = append(bpkg.TestGoFiles, bpkg.XTestGoFiles...)
sort.Strings(names)
b.pdoc.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 {
b.pdoc.Errors = append(b.pdoc.Errors, err.Error())
continue
}
b.pdoc.TestFiles[i] = &File{Name: name, URL: b.srcs[name].browseURL}
b.pdoc.TestSourceSize += len(b.srcs[name].data)
b.examples = append(b.examples, doc.Examples(file)...)
}
b.vetPackage(apkg)
mode := doc.Mode(0)
if b.pdoc.ImportPath == "builtin" {
mode |= doc.AllDecls
}
dpkg := doc.New(apkg, b.pdoc.ImportPath, mode)
b.pdoc.Name = dpkg.Name
b.pdoc.Doc = strings.TrimRight(dpkg.Doc, " \t\n\r")
b.pdoc.Synopsis = synopsis(b.pdoc.Doc)
b.pdoc.Examples = b.getExamples("")
b.pdoc.IsCmd = bpkg.IsCommand()
b.pdoc.GOOS = ctxt.GOOS
b.pdoc.GOARCH = ctxt.GOARCH
b.pdoc.Consts = b.values(dpkg.Consts)
b.pdoc.Funcs = b.funcs(dpkg.Funcs)
b.pdoc.Types = b.types(dpkg.Types)
b.pdoc.Vars = b.values(dpkg.Vars)
b.pdoc.Notes = b.notes(dpkg.Notes)
b.pdoc.Imports = bpkg.Imports
b.pdoc.TestImports = bpkg.TestImports
b.pdoc.XTestImports = bpkg.XTestImports
return b.pdoc, nil
}
You can’t perform that action at this time.