| // Copyright 2013 The rspace Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Doc is a simple document printer that produces the doc comments for its |
| // argument symbols, plus a link to the full documentation and a pointer to |
| // the source. It has a more Go-like UI than godoc. It can also search for |
| // symbols by looking in all packages, and case is ignored. For instance: |
| // doc isupper |
| // will find unicode.IsUpper. |
| // |
| // The -pkg flag retrieves package-level doc comments only. |
| // |
| // Usage: |
| // doc pkg.name # "doc io.Writer" |
| // doc pkg name # "doc fmt Printf" |
| // doc name # "doc isupper" (finds unicode.IsUpper) |
| // doc -pkg pkg # "doc fmt" |
| // |
| // The pkg is the last element of the package path; |
| // no slashes (ast.Node not go/ast.Node). |
| // |
| // Flags |
| // -c(onst) -f(unc) -i(nterface) -m(ethod) -s(truct) -t(ype) -v(ar) |
| // restrict hits to declarations of the corresponding kind. |
| // Flags |
| // -doc -src -url |
| // restrict printing to the documentation, source path, or godoc URL. |
| package finddoc |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/ast" |
| "go/parser" |
| "go/printer" |
| "go/token" |
| "os" |
| "path" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strings" |
| |
| "github.com/visualfc/gotools/command" |
| _ "golang.org/x/tools/go/gcimporter" |
| "golang.org/x/tools/go/types" |
| ) |
| |
| const usageDoc = `Find documentation for names. |
| usage: |
| doc pkg.name # "doc io.Writer" |
| doc pkg name # "doc fmt Printf" |
| doc name # "doc isupper" finds unicode.IsUpper |
| doc -pkg pkg # "doc fmt" |
| doc -r expr # "doc -r '.*exported'" |
| pkg is the last component of any package, e.g. fmt, parser |
| name is the name of an exported symbol; case is ignored in matches. |
| |
| The name may also be a regular expression to select which names |
| to match. In regular expression searches, case is ignored and |
| the pattern must match the entire name, so ".?print" will match |
| Print, Fprint and Sprint but not Fprintf. |
| |
| Flags |
| -c(onst) -f(unc) -i(nterface) -m(ethod) -s(truct) -t(ype) -v(ar) |
| restrict hits to declarations of the corresponding kind. |
| Flags |
| -doc -src -url |
| restrict printing to the documentation, source path, or godoc URL. |
| Flag |
| -r |
| takes a single argument (no package), a name or regular expression |
| to search for in all packages. |
| ` |
| |
| var Command = &command.Command{ |
| Run: runDoc, |
| UsageLine: "finddoc [pkg.name|pkg name|-pkg name]", |
| Short: "golang doc lookup", |
| Long: usageDoc, |
| } |
| |
| var ( |
| // If none is set, all are set. |
| docFlag bool |
| srcFlag bool |
| urlFlag bool |
| regexpFlag bool |
| matchWordFlag bool |
| matchCaseFlag bool |
| constantFlag bool |
| functionFlag bool |
| interfaceFlag bool |
| methodFlag bool |
| packageFlag bool |
| structFlag bool |
| typeFlag bool |
| variableFlag bool |
| urlHeadTag string |
| ) |
| |
| func init() { |
| Command.Flag.BoolVar(&docFlag, "doc", false, "restrict output to documentation only") |
| Command.Flag.BoolVar(&srcFlag, "src", false, "restrict output to source file only") |
| Command.Flag.BoolVar(&urlFlag, "url", false, "restrict output to godoc URL only") |
| Command.Flag.BoolVar(®expFlag, "r", false, "single argument is a regular expression for a name") |
| Command.Flag.BoolVar(&matchWordFlag, "word", false, "search match whole word") |
| Command.Flag.BoolVar(&matchCaseFlag, "case", false, "search match case") |
| |
| Command.Flag.BoolVar(&constantFlag, "const", false, "show doc for consts only") |
| Command.Flag.BoolVar(&functionFlag, "func", false, "show doc for funcs only") |
| Command.Flag.BoolVar(&interfaceFlag, "interface", false, "show doc for interfaces only") |
| Command.Flag.BoolVar(&methodFlag, "method", false, "show doc for methods only") |
| Command.Flag.BoolVar(&packageFlag, "package", false, "show top-level package doc only") |
| Command.Flag.BoolVar(&structFlag, "struct", false, "show doc for structs only") |
| Command.Flag.BoolVar(&typeFlag, "type", false, "show doc for types only") |
| Command.Flag.BoolVar(&variableFlag, "var", false, "show doc for vars only") |
| |
| Command.Flag.BoolVar(&constantFlag, "c", false, "alias for -const") |
| Command.Flag.BoolVar(&functionFlag, "f", false, "alias for -func") |
| Command.Flag.BoolVar(&interfaceFlag, "i", false, "alias for -interface") |
| Command.Flag.BoolVar(&methodFlag, "m", false, "alias for -method") |
| Command.Flag.BoolVar(&packageFlag, "pkg", false, "alias for -package") |
| Command.Flag.BoolVar(&structFlag, "s", false, "alias for -struct") |
| Command.Flag.BoolVar(&typeFlag, "t", false, "alias for -type") |
| Command.Flag.BoolVar(&variableFlag, "v", false, "alias for -var") |
| |
| Command.Flag.StringVar(&urlHeadTag, "urltag", "", "url head tag, liteide provate") |
| } |
| |
| func runDoc(cmd *command.Command, args []string) error { |
| if !(constantFlag || functionFlag || interfaceFlag || methodFlag || packageFlag || structFlag || typeFlag || variableFlag) { // none set |
| constantFlag = true |
| functionFlag = true |
| methodFlag = true |
| // Not package! It's special. |
| typeFlag = true |
| variableFlag = true |
| } |
| if !(docFlag || srcFlag || urlFlag) { |
| docFlag = true |
| srcFlag = true |
| urlFlag = true |
| } |
| var pkg, name string |
| switch len(args) { |
| case 1: |
| if packageFlag { |
| pkg = args[0] |
| } else if regexpFlag { |
| name = args[0] |
| } else if strings.Contains(args[0], ".") { |
| pkg, name = split(args[0]) |
| } else { |
| name = args[0] |
| } |
| case 2: |
| if packageFlag { |
| cmd.Usage() |
| } |
| pkg, name = args[0], args[1] |
| default: |
| cmd.Usage() |
| return os.ErrInvalid |
| } |
| if strings.Contains(pkg, "/") { |
| fmt.Fprintf(os.Stderr, "doc: package name cannot contain slash (TODO)\n") |
| os.Exit(2) |
| } |
| for _, path := range Paths(pkg) { |
| lookInDirectory(path, name) |
| } |
| return nil |
| } |
| |
| var slash = string(filepath.Separator) |
| var slashDot = string(filepath.Separator) + "." |
| var goRootSrcPkg = filepath.Join(runtime.GOROOT(), "src", "pkg") |
| var goRootSrcCmd = filepath.Join(runtime.GOROOT(), "src", "cmd") |
| var goPaths = SplitGopath() |
| |
| func split(arg string) (pkg, name string) { |
| dot := strings.IndexRune(arg, '.') // We know there's one there. |
| return arg[0:dot], arg[dot+1:] |
| } |
| |
| func Paths(pkg string) []string { |
| pkgs := pathsFor(runtime.GOROOT(), pkg) |
| for _, root := range goPaths { |
| pkgs = append(pkgs, pathsFor(root, pkg)...) |
| } |
| return pkgs |
| } |
| |
| func SplitGopath() []string { |
| gopath := os.Getenv("GOPATH") |
| if gopath == "" { |
| return nil |
| } |
| return strings.Split(gopath, string(os.PathListSeparator)) |
| } |
| |
| // pathsFor recursively walks the tree looking for possible directories for the package: |
| // those whose basename is pkg. |
| func pathsFor(root, pkg string) []string { |
| root = path.Join(root, "src") |
| pkgPaths := make([]string, 0, 10) |
| visit := func(pathName string, f os.FileInfo, err error) error { |
| if err != nil { |
| return nil |
| } |
| // One package per directory. Ignore the files themselves. |
| if !f.IsDir() { |
| return nil |
| } |
| // No .hg or other dot nonsense please. |
| if strings.Contains(pathName, slashDot) { |
| return filepath.SkipDir |
| } |
| // Is the last element of the path correct |
| if pkg == "" || filepath.Base(pathName) == pkg { |
| pkgPaths = append(pkgPaths, pathName) |
| } |
| return nil |
| } |
| |
| filepath.Walk(root, visit) |
| return pkgPaths |
| } |
| |
| // lookInDirectory looks in the package (if any) in the directory for the named exported identifier. |
| func lookInDirectory(directory, name string) { |
| fset := token.NewFileSet() |
| pkgs, _ := parser.ParseDir(fset, directory, nil, parser.ParseComments) // Ignore the error. |
| for _, pkg := range pkgs { |
| if pkg.Name == "main" || strings.HasSuffix(pkg.Name, "_test") { |
| continue |
| } |
| doPackage(pkg, fset, name) |
| } |
| } |
| |
| // prefixDirectory places the directory name on the beginning of each name in the list. |
| func prefixDirectory(directory string, names []string) { |
| if directory != "." { |
| for i, name := range names { |
| names[i] = filepath.Join(directory, name) |
| } |
| } |
| } |
| |
| // File is a wrapper for the state of a file used in the parser. |
| // The parse tree walkers are all methods of this type. |
| type File struct { |
| fset *token.FileSet |
| name string // Name of file. |
| ident string // Identifier we are searching for. |
| lowerIdent string // lower ident |
| regexp *regexp.Regexp |
| pathPrefix string // Prefix from GOROOT/GOPATH. |
| urlPrefix string // Start of corresponding URL for golang.org or godoc.org. |
| file *ast.File |
| comments ast.CommentMap |
| defs map[*ast.Ident]types.Object |
| doPrint bool |
| found bool |
| allFiles []*File // All files in the package. |
| } |
| |
| // doPackage analyzes the single package constructed from the named files, looking for |
| // the definition of ident. |
| func doPackage(pkg *ast.Package, fset *token.FileSet, ident string) { |
| var files []*File |
| found := false |
| for name, astFile := range pkg.Files { |
| if packageFlag && astFile.Doc == nil { |
| continue |
| } |
| file := &File{ |
| fset: fset, |
| name: name, |
| ident: ident, |
| lowerIdent: strings.ToLower(ident), |
| file: astFile, |
| comments: ast.NewCommentMap(fset, astFile, astFile.Comments), |
| } |
| if regexpFlag && regexp.QuoteMeta(ident) != ident { |
| // It's a regular expression. |
| var err error |
| file.regexp, err = regexp.Compile("^(?i:" + ident + ")$") |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "regular expression `%s`:", err) |
| os.Exit(2) |
| } |
| } |
| switch { |
| case strings.HasPrefix(name, goRootSrcPkg): |
| file.urlPrefix = "http://golang.org/pkg" |
| file.pathPrefix = goRootSrcPkg |
| case strings.HasPrefix(name, goRootSrcCmd): |
| file.urlPrefix = "http://golang.org/cmd" |
| file.pathPrefix = goRootSrcCmd |
| default: |
| file.urlPrefix = "http://godoc.org" |
| for _, path := range goPaths { |
| p := filepath.Join(path, "src") |
| if strings.HasPrefix(name, p) { |
| file.pathPrefix = p |
| break |
| } |
| } |
| } |
| file.urlPrefix = urlHeadTag + file.urlPrefix |
| files = append(files, file) |
| if found { |
| continue |
| } |
| file.doPrint = false |
| if packageFlag { |
| file.pkgComments() |
| } else { |
| ast.Walk(file, file.file) |
| if file.found { |
| found = true |
| } |
| } |
| } |
| |
| if !found { |
| return |
| } |
| |
| // By providing the Context with our own error function, it will continue |
| // past the first error. There is no need for that function to do anything. |
| config := types.Config{ |
| Error: func(error) {}, |
| } |
| info := &types.Info{ |
| Defs: make(map[*ast.Ident]types.Object), |
| } |
| path := "" |
| var astFiles []*ast.File |
| for name, astFile := range pkg.Files { |
| if path == "" { |
| path = name |
| } |
| astFiles = append(astFiles, astFile) |
| } |
| config.Check(path, fset, astFiles, info) // Ignore errors. |
| |
| // We need to search all files for methods, so record the full list in each file. |
| for _, file := range files { |
| file.allFiles = files |
| } |
| for _, file := range files { |
| file.doPrint = true |
| file.defs = info.Defs |
| if packageFlag { |
| file.pkgComments() |
| } else { |
| ast.Walk(file, file.file) |
| } |
| } |
| } |
| |
| // Visit implements the ast.Visitor interface. |
| func (f *File) Visit(node ast.Node) ast.Visitor { |
| switch n := node.(type) { |
| case *ast.GenDecl: |
| // Variables, constants, types. |
| for _, spec := range n.Specs { |
| switch spec := spec.(type) { |
| case *ast.ValueSpec: |
| if constantFlag && n.Tok == token.CONST || variableFlag && n.Tok == token.VAR { |
| for _, ident := range spec.Names { |
| if f.match(ident.Name) { |
| f.printNode(n, ident, f.nameURL(ident.Name)) |
| break |
| } |
| } |
| } |
| case *ast.TypeSpec: |
| // If there is only one Spec, there are probably no parens and the |
| // comment we want appears before the type keyword, bound to |
| // the GenDecl. If the Specs are parenthesized, the comment we want |
| // is bound to the Spec. Hence we dig into the GenDecl to the Spec, |
| // but only if there are no parens. |
| node := ast.Node(n) |
| if n.Lparen.IsValid() { |
| node = spec |
| } |
| if f.match(spec.Name.Name) { |
| if typeFlag { |
| f.printNode(node, spec.Name, f.nameURL(spec.Name.Name)) |
| } else { |
| switch spec.Type.(type) { |
| case *ast.InterfaceType: |
| if interfaceFlag { |
| f.printNode(node, spec.Name, f.nameURL(spec.Name.Name)) |
| } |
| case *ast.StructType: |
| if structFlag { |
| f.printNode(node, spec.Name, f.nameURL(spec.Name.Name)) |
| } |
| } |
| } |
| if f.doPrint && f.defs[spec.Name] != nil && f.defs[spec.Name].Type() != nil { |
| ms := types.NewMethodSet(f.defs[spec.Name].Type()) //.Type().MethodSet() |
| if ms.Len() == 0 { |
| ms = types.NewMethodSet(types.NewPointer(f.defs[spec.Name].Type())) //.MethodSet() |
| } |
| f.methodSet(ms) |
| } |
| } |
| case *ast.ImportSpec: |
| continue // Don't care. |
| } |
| } |
| case *ast.FuncDecl: |
| // Methods, top-level functions. |
| if f.match(n.Name.Name) { |
| n.Body = nil // Do not print the function body. |
| if methodFlag && n.Recv != nil { |
| f.printNode(n, n.Name, f.methodURL(n.Recv.List[0].Type, n.Name.Name)) |
| } else if functionFlag && n.Recv == nil { |
| f.printNode(n, n.Name, f.nameURL(n.Name.Name)) |
| } |
| } |
| } |
| return f |
| } |
| |
| func (f *File) match(name string) bool { |
| // name must be exported. |
| if !ast.IsExported(name) { |
| return false |
| } |
| if f.regexp == nil { |
| if matchWordFlag { |
| if matchCaseFlag { |
| return name == f.ident |
| } |
| return strings.ToLower(name) == f.lowerIdent |
| } else { |
| if matchCaseFlag { |
| return strings.Contains(name, f.ident) |
| } |
| return strings.Contains(strings.ToLower(name), f.lowerIdent) |
| } |
| } |
| return f.regexp.MatchString(name) |
| } |
| |
| func (f *File) printNode(node, ident ast.Node, url string) { |
| if !f.doPrint { |
| f.found = true |
| return |
| } |
| fmt.Printf("%s%s%s", url, f.sourcePos(f.fset.Position(ident.Pos())), f.docs(node)) |
| } |
| |
| func (f *File) docs(node ast.Node) []byte { |
| if !docFlag { |
| return nil |
| } |
| commentedNode := printer.CommentedNode{Node: node} |
| if comments := f.comments.Filter(node).Comments(); comments != nil { |
| commentedNode.Comments = comments |
| } |
| var b bytes.Buffer |
| printer.Fprint(&b, f.fset, &commentedNode) |
| b.Write([]byte("\n\n")) // Add a blank line between entries if we print documentation. |
| return b.Bytes() |
| } |
| |
| func (f *File) pkgComments() { |
| doc := f.file.Doc |
| if doc == nil { |
| return |
| } |
| url := "" |
| if urlFlag { |
| url = f.packageURL() + "\n" |
| } |
| docText := "" |
| if docFlag { |
| docText = fmt.Sprintf("package %s\n%s\n\n", f.file.Name.Name, doc.Text()) |
| } |
| fmt.Printf("%s%s%s", url, f.sourcePos(f.fset.Position(doc.Pos())), docText) |
| } |
| |
| func (f *File) packageURL() string { |
| s := strings.TrimPrefix(f.name, f.pathPrefix) |
| // Now we have a path with a final file name. Drop it. |
| if i := strings.LastIndex(s, slash); i > 0 { |
| s = s[:i+1] |
| } |
| return f.urlPrefix + s |
| } |
| |
| func (f *File) packageName() string { |
| s := strings.TrimPrefix(f.name, f.pathPrefix) |
| // Now we have a path with a final file name. Drop it. |
| if i := strings.LastIndex(s, slash); i > 0 { |
| s = s[:i+1] |
| } |
| s = strings.Trim(s, slash) |
| return filepath.ToSlash(s) |
| } |
| |
| func (f *File) sourcePos(posn token.Position) string { |
| if !srcFlag { |
| return "" |
| } |
| return fmt.Sprintf("%s:%d:\n", posn.Filename, posn.Line) |
| } |
| |
| func (f *File) nameURL(name string) string { |
| if !urlFlag { |
| return "" |
| } |
| return fmt.Sprintf("%s#%s\n", f.packageURL(), name) |
| } |
| |
| func (f *File) methodURL(typ ast.Expr, name string) string { |
| if !urlFlag { |
| return "" |
| } |
| var b bytes.Buffer |
| printer.Fprint(&b, f.fset, typ) |
| typeName := b.Bytes() |
| if len(typeName) > 0 && typeName[0] == '*' { |
| typeName = typeName[1:] |
| } |
| return fmt.Sprintf("%s#%s.%s\n", f.packageURL(), typeName, name) |
| } |
| |
| // Here follows the code to find and print a method (actually a method set, because |
| // we want to do only one redundant tree walk, not one per method). |
| // It should be much easier than walking the whole tree again, but that's what we must do. |
| // TODO. |
| |
| type method struct { |
| index int // Which doc to write. (Keeps the results sorted) |
| *types.Selection |
| } |
| |
| type methodVisitor struct { |
| *File |
| methods []method |
| docs []string |
| } |
| |
| func (f *File) methodSet(set *types.MethodSet) { |
| // Build the set of things we're looking for. |
| methods := make([]method, 0, set.Len()) |
| docs := make([]string, set.Len()) |
| for i := 0; i < set.Len(); i++ { |
| if ast.IsExported(set.At(i).Obj().Name()) { |
| m := method{ |
| i, |
| set.At(i), |
| } |
| methods = append(methods, m) |
| } |
| } |
| if len(methods) == 0 { |
| return |
| } |
| // Collect the docs. |
| for _, file := range f.allFiles { |
| visitor := &methodVisitor{ |
| File: file, |
| methods: methods, |
| docs: docs, |
| } |
| ast.Walk(visitor, file.file) |
| methods = visitor.methods |
| } |
| // Print them in order. The incoming method set is sorted by name. |
| for _, doc := range docs { |
| if doc != "" { |
| fmt.Print(doc) |
| } |
| } |
| } |
| |
| // Visit implements the ast.Visitor interface. |
| func (visitor *methodVisitor) Visit(node ast.Node) ast.Visitor { |
| switch n := node.(type) { |
| case *ast.FuncDecl: |
| for i, method := range visitor.methods { |
| // If this is the right one, the position of the name of its identifier will match. |
| if method.Obj().Pos() == n.Name.Pos() { |
| n.Body = nil // TODO. Ugly - don't print the function body. |
| visitor.docs[method.index] = fmt.Sprintf("%s", visitor.File.docs(n)) |
| // If this was the last method, we're done. |
| if len(visitor.methods) == 1 { |
| return nil |
| } |
| // Drop this one from the list. |
| visitor.methods = append(visitor.methods[:i], visitor.methods[i+1:]...) |
| return visitor |
| } |
| } |
| } |
| return visitor |
| } |