// Copyright 2013 gopm authors. // // 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" "go/ast" "go/build" "go/doc" "go/parser" "go/token" "io" "io/ioutil" "os" "path" "runtime" "strings" "unicode" "unicode/utf8" "github.com/gpmgo/gopm/log" ) type sliceWriter struct{ p *[]byte } func (w sliceWriter) Write(p []byte) (int, error) { *w.p = append(*w.p, p...) return len(p), nil } func (w *walker) readDir(dir string) ([]os.FileInfo, error) { if dir != w.ImportPath { panic("unexpected") } fis := make([]os.FileInfo, 0, len(w.srcs)) for _, src := range w.srcs { fis = append(fis, src) } return fis, nil } func (w *walker) openFile(path string) (io.ReadCloser, error) { if strings.HasPrefix(path, w.ImportPath+"/") { if src, ok := w.srcs[path[len(w.ImportPath)+1:]]; ok { return ioutil.NopCloser(bytes.NewReader(src.data)), nil } } panic("unexpected") } 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. Start with the last // element of the path. name := path[strings.LastIndex(path, "/")+1:] // Trim commonly used prefixes and suffixes containing illegal name // runes. name = strings.TrimSuffix(name, ".go") name = strings.TrimSuffix(name, "-go") name = strings.TrimPrefix(name, "go.") name = strings.TrimPrefix(name, "go-") name = strings.TrimPrefix(name, "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. pkg = ast.NewObj(ast.Pkg, name) pkg.Data = ast.NewScope(nil) imports[path] = pkg } return pkg, nil } // build gets imports from source files. func (w *walker) build(srcs []*source, nod *Node) ([]string, error) { // Add source files to walker, I skipped references here. w.srcs = make(map[string]*source) for _, src := range srcs { w.srcs[src.name] = src } w.fset = token.NewFileSet() // Find the package and associated files. ctxt := build.Context{ GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, CgoEnabled: true, 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 w.readDir(dir) }, OpenFile: func(path string) (r io.ReadCloser, err error) { return w.openFile(path) }, Compiler: "gc", } bpkg, err := ctxt.ImportDir(w.ImportPath, 0) // Continue if there are no Go source files; we still want the directory info. _, nogo := err.(*build.NoGoError) if err != nil { if nogo { err = nil } else { log.Warn("walker: %s", err.Error()) return nil, nil } } // Parse the Go files files := make(map[string]*ast.File) for _, name := range append(bpkg.GoFiles, bpkg.CgoFiles...) { file, err := parser.ParseFile(w.fset, name, w.srcs[name].data, parser.ParseComments) if err != nil { //beego.Error("doc.walker.build():", err) continue } files[name] = file } w.ImportPath = strings.Replace(w.ImportPath, "\\", "/", -1) var imports []string for _, v := range bpkg.Imports { // Skip strandard library. if !IsGoRepoPath(v) && (GetProjectPath(v) != GetProjectPath(w.ImportPath)) { imports = append(imports, v) } } apkg, _ := ast.NewPackage(w.fset, files, simpleImporter, nil) mode := doc.Mode(0) if w.ImportPath == "builtin" { mode |= doc.AllDecls } pdoc := doc.New(apkg, w.ImportPath, mode) if nod != nil { nod.Synopsis = Synopsis(pdoc.Doc) if i := strings.Index(nod.Synopsis, "\n"); i > -1 { nod.Synopsis = nod.Synopsis[:i] } } return imports, err } 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 }