1// Copyright 2013 The rspace Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Doc is a simple document printer that produces the doc comments for its 6// argument symbols, plus a link to the full documentation and a pointer to 7// the source. It has a more Go-like UI than godoc. It can also search for 8// symbols by looking in all packages, and case is ignored. For instance: 9// doc isupper 10// will find unicode.IsUpper. 11// 12// The -pkg flag retrieves package-level doc comments only. 13// 14// Usage: 15// doc pkg.name # "doc io.Writer" 16// doc pkg name # "doc fmt Printf" 17// doc name # "doc isupper" (finds unicode.IsUpper) 18// doc -pkg pkg # "doc fmt" 19// 20// The pkg is the last element of the package path; 21// no slashes (ast.Node not go/ast.Node). 22// 23// Flags 24// -c(onst) -f(unc) -i(nterface) -m(ethod) -s(truct) -t(ype) -v(ar) 25// restrict hits to declarations of the corresponding kind. 26// Flags 27// -doc -src -url 28// restrict printing to the documentation, source path, or godoc URL. 29package finddoc 30 31import ( 32 "bytes" 33 "fmt" 34 "go/ast" 35 "go/parser" 36 "go/printer" 37 "go/token" 38 "go/types" 39 "os" 40 "path" 41 "path/filepath" 42 "regexp" 43 "runtime" 44 "strings" 45 46 "github.com/visualfc/gotools/pkg/command" 47) 48 49const usageDoc = `Find documentation for names. 50usage: 51 doc pkg.name # "doc io.Writer" 52 doc pkg name # "doc fmt Printf" 53 doc name # "doc isupper" finds unicode.IsUpper 54 doc -pkg pkg # "doc fmt" 55 doc -r expr # "doc -r '.*exported'" 56pkg is the last component of any package, e.g. fmt, parser 57name is the name of an exported symbol; case is ignored in matches. 58 59The name may also be a regular expression to select which names 60to match. In regular expression searches, case is ignored and 61the pattern must match the entire name, so ".?print" will match 62Print, Fprint and Sprint but not Fprintf. 63 64Flags 65 -c(onst) -f(unc) -i(nterface) -m(ethod) -s(truct) -t(ype) -v(ar) 66restrict hits to declarations of the corresponding kind. 67Flags 68 -doc -src -url 69restrict printing to the documentation, source path, or godoc URL. 70Flag 71 -r 72takes a single argument (no package), a name or regular expression 73to search for in all packages. 74` 75 76var Command = &command.Command{ 77 Run: runDoc, 78 UsageLine: "finddoc [pkg.name|pkg name|-pkg name]", 79 Short: "golang doc lookup", 80 Long: usageDoc, 81} 82 83var ( 84 // If none is set, all are set. 85 docFlag bool 86 srcFlag bool 87 urlFlag bool 88 regexpFlag bool 89 matchWordFlag bool 90 matchCaseFlag bool 91 constantFlag bool 92 functionFlag bool 93 interfaceFlag bool 94 methodFlag bool 95 packageFlag bool 96 structFlag bool 97 typeFlag bool 98 variableFlag bool 99 urlHeadTag string 100) 101 102func init() { 103 Command.Flag.BoolVar(&docFlag, "doc", false, "restrict output to documentation only") 104 Command.Flag.BoolVar(&srcFlag, "src", false, "restrict output to source file only") 105 Command.Flag.BoolVar(&urlFlag, "url", false, "restrict output to godoc URL only") 106 Command.Flag.BoolVar(®expFlag, "r", false, "single argument is a regular expression for a name") 107 Command.Flag.BoolVar(&matchWordFlag, "word", false, "search match whole word") 108 Command.Flag.BoolVar(&matchCaseFlag, "case", false, "search match case") 109 110 Command.Flag.BoolVar(&constantFlag, "const", false, "show doc for consts only") 111 Command.Flag.BoolVar(&functionFlag, "func", false, "show doc for funcs only") 112 Command.Flag.BoolVar(&interfaceFlag, "interface", false, "show doc for interfaces only") 113 Command.Flag.BoolVar(&methodFlag, "method", false, "show doc for methods only") 114 Command.Flag.BoolVar(&packageFlag, "package", false, "show top-level package doc only") 115 Command.Flag.BoolVar(&structFlag, "struct", false, "show doc for structs only") 116 Command.Flag.BoolVar(&typeFlag, "type", false, "show doc for types only") 117 Command.Flag.BoolVar(&variableFlag, "var", false, "show doc for vars only") 118 119 Command.Flag.BoolVar(&constantFlag, "c", false, "alias for -const") 120 Command.Flag.BoolVar(&functionFlag, "f", false, "alias for -func") 121 Command.Flag.BoolVar(&interfaceFlag, "i", false, "alias for -interface") 122 Command.Flag.BoolVar(&methodFlag, "m", false, "alias for -method") 123 Command.Flag.BoolVar(&packageFlag, "pkg", false, "alias for -package") 124 Command.Flag.BoolVar(&structFlag, "s", false, "alias for -struct") 125 Command.Flag.BoolVar(&typeFlag, "t", false, "alias for -type") 126 Command.Flag.BoolVar(&variableFlag, "v", false, "alias for -var") 127 128 Command.Flag.StringVar(&urlHeadTag, "urltag", "", "url head tag, liteide provate") 129} 130 131func runDoc(cmd *command.Command, args []string) error { 132 if !(constantFlag || functionFlag || interfaceFlag || methodFlag || packageFlag || structFlag || typeFlag || variableFlag) { // none set 133 constantFlag = true 134 functionFlag = true 135 methodFlag = true 136 // Not package! It's special. 137 typeFlag = true 138 variableFlag = true 139 } 140 if !(docFlag || srcFlag || urlFlag) { 141 docFlag = true 142 srcFlag = true 143 urlFlag = true 144 } 145 var pkg, name string 146 switch len(args) { 147 case 1: 148 if packageFlag { 149 pkg = args[0] 150 } else if regexpFlag { 151 name = args[0] 152 } else if strings.Contains(args[0], ".") { 153 pkg, name = split(args[0]) 154 } else { 155 name = args[0] 156 } 157 case 2: 158 if packageFlag { 159 cmd.Usage() 160 return os.ErrInvalid 161 } 162 pkg, name = args[0], args[1] 163 default: 164 cmd.Usage() 165 return os.ErrInvalid 166 } 167 if strings.Contains(pkg, "/") { 168 fmt.Fprintf(os.Stderr, "doc: package name cannot contain slash (TODO)\n") 169 os.Exit(2) 170 } 171 for _, path := range Paths(pkg) { 172 lookInDirectory(path, name) 173 } 174 return nil 175} 176 177var slash = string(filepath.Separator) 178var slashDot = string(filepath.Separator) + "." 179var goRootSrcPkg = filepath.Join(runtime.GOROOT(), "src", "pkg") 180var goRootSrcCmd = filepath.Join(runtime.GOROOT(), "src", "cmd") 181var goPaths = SplitGopath() 182 183func split(arg string) (pkg, name string) { 184 dot := strings.IndexRune(arg, '.') // We know there's one there. 185 return arg[0:dot], arg[dot+1:] 186} 187 188func Paths(pkg string) []string { 189 pkgs := pathsFor(runtime.GOROOT(), pkg) 190 for _, root := range goPaths { 191 pkgs = append(pkgs, pathsFor(root, pkg)...) 192 } 193 return pkgs 194} 195 196func SplitGopath() []string { 197 gopath := os.Getenv("GOPATH") 198 if gopath == "" { 199 return nil 200 } 201 return strings.Split(gopath, string(os.PathListSeparator)) 202} 203 204// pathsFor recursively walks the tree looking for possible directories for the package: 205// those whose basename is pkg. 206func pathsFor(root, pkg string) []string { 207 root = path.Join(root, "src") 208 pkgPaths := make([]string, 0, 10) 209 visit := func(pathName string, f os.FileInfo, err error) error { 210 if err != nil { 211 return nil 212 } 213 // One package per directory. Ignore the files themselves. 214 if !f.IsDir() { 215 return nil 216 } 217 // No .hg or other dot nonsense please. 218 if strings.Contains(pathName, slashDot) { 219 return filepath.SkipDir 220 } 221 // Is the last element of the path correct 222 if pkg == "" || filepath.Base(pathName) == pkg { 223 pkgPaths = append(pkgPaths, pathName) 224 } 225 return nil 226 } 227 228 filepath.Walk(root, visit) 229 return pkgPaths 230} 231 232// lookInDirectory looks in the package (if any) in the directory for the named exported identifier. 233func lookInDirectory(directory, name string) { 234 fset := token.NewFileSet() 235 pkgs, _ := parser.ParseDir(fset, directory, nil, parser.ParseComments) // Ignore the error. 236 for _, pkg := range pkgs { 237 if pkg.Name == "main" || strings.HasSuffix(pkg.Name, "_test") { 238 continue 239 } 240 doPackage(pkg, fset, name) 241 } 242} 243 244// prefixDirectory places the directory name on the beginning of each name in the list. 245func prefixDirectory(directory string, names []string) { 246 if directory != "." { 247 for i, name := range names { 248 names[i] = filepath.Join(directory, name) 249 } 250 } 251} 252 253// File is a wrapper for the state of a file used in the parser. 254// The parse tree walkers are all methods of this type. 255type File struct { 256 fset *token.FileSet 257 name string // Name of file. 258 ident string // Identifier we are searching for. 259 lowerIdent string // lower ident 260 regexp *regexp.Regexp 261 pathPrefix string // Prefix from GOROOT/GOPATH. 262 urlPrefix string // Start of corresponding URL for golang.org or godoc.org. 263 file *ast.File 264 comments ast.CommentMap 265 defs map[*ast.Ident]types.Object 266 doPrint bool 267 found bool 268 allFiles []*File // All files in the package. 269} 270 271// doPackage analyzes the single package constructed from the named files, looking for 272// the definition of ident. 273func doPackage(pkg *ast.Package, fset *token.FileSet, ident string) { 274 var files []*File 275 found := false 276 for name, astFile := range pkg.Files { 277 if packageFlag && astFile.Doc == nil { 278 continue 279 } 280 file := &File{ 281 fset: fset, 282 name: name, 283 ident: ident, 284 lowerIdent: strings.ToLower(ident), 285 file: astFile, 286 comments: ast.NewCommentMap(fset, astFile, astFile.Comments), 287 } 288 if regexpFlag && regexp.QuoteMeta(ident) != ident { 289 // It's a regular expression. 290 var err error 291 file.regexp, err = regexp.Compile("^(?i:" + ident + ")$") 292 if err != nil { 293 fmt.Fprintf(os.Stderr, "regular expression `%s`:", err) 294 os.Exit(2) 295 } 296 } 297 switch { 298 case strings.HasPrefix(name, goRootSrcPkg): 299 file.urlPrefix = "http://golang.org/pkg" 300 file.pathPrefix = goRootSrcPkg 301 case strings.HasPrefix(name, goRootSrcCmd): 302 file.urlPrefix = "http://golang.org/cmd" 303 file.pathPrefix = goRootSrcCmd 304 default: 305 file.urlPrefix = "http://godoc.org" 306 for _, path := range goPaths { 307 p := filepath.Join(path, "src") 308 if strings.HasPrefix(name, p) { 309 file.pathPrefix = p 310 break 311 } 312 } 313 } 314 file.urlPrefix = urlHeadTag + file.urlPrefix 315 files = append(files, file) 316 if found { 317 continue 318 } 319 file.doPrint = false 320 if packageFlag { 321 file.pkgComments() 322 } else { 323 ast.Walk(file, file.file) 324 if file.found { 325 found = true 326 } 327 } 328 } 329 330 if !found { 331 return 332 } 333 334 // By providing the Context with our own error function, it will continue 335 // past the first error. There is no need for that function to do anything. 336 config := types.Config{ 337 Error: func(error) {}, 338 } 339 info := &types.Info{ 340 Defs: make(map[*ast.Ident]types.Object), 341 } 342 path := "" 343 var astFiles []*ast.File 344 for name, astFile := range pkg.Files { 345 if path == "" { 346 path = name 347 } 348 astFiles = append(astFiles, astFile) 349 } 350 config.Check(path, fset, astFiles, info) // Ignore errors. 351 352 // We need to search all files for methods, so record the full list in each file. 353 for _, file := range files { 354 file.allFiles = files 355 } 356 for _, file := range files { 357 file.doPrint = true 358 file.defs = info.Defs 359 if packageFlag { 360 file.pkgComments() 361 } else { 362 ast.Walk(file, file.file) 363 } 364 } 365} 366 367// Visit implements the ast.Visitor interface. 368func (f *File) Visit(node ast.Node) ast.Visitor { 369 switch n := node.(type) { 370 case *ast.GenDecl: 371 // Variables, constants, types. 372 for _, spec := range n.Specs { 373 switch spec := spec.(type) { 374 case *ast.ValueSpec: 375 if constantFlag && n.Tok == token.CONST || variableFlag && n.Tok == token.VAR { 376 for _, ident := range spec.Names { 377 if f.match(ident.Name) { 378 f.printNode(n, ident, f.nameURL(ident.Name)) 379 break 380 } 381 } 382 } 383 case *ast.TypeSpec: 384 // If there is only one Spec, there are probably no parens and the 385 // comment we want appears before the type keyword, bound to 386 // the GenDecl. If the Specs are parenthesized, the comment we want 387 // is bound to the Spec. Hence we dig into the GenDecl to the Spec, 388 // but only if there are no parens. 389 node := ast.Node(n) 390 if n.Lparen.IsValid() { 391 node = spec 392 } 393 if f.match(spec.Name.Name) { 394 if typeFlag { 395 f.printNode(node, spec.Name, f.nameURL(spec.Name.Name)) 396 } else { 397 switch spec.Type.(type) { 398 case *ast.InterfaceType: 399 if interfaceFlag { 400 f.printNode(node, spec.Name, f.nameURL(spec.Name.Name)) 401 } 402 case *ast.StructType: 403 if structFlag { 404 f.printNode(node, spec.Name, f.nameURL(spec.Name.Name)) 405 } 406 } 407 } 408 if f.doPrint && f.defs[spec.Name] != nil && f.defs[spec.Name].Type() != nil { 409 ms := types.NewMethodSet(f.defs[spec.Name].Type()) //.Type().MethodSet() 410 if ms.Len() == 0 { 411 ms = types.NewMethodSet(types.NewPointer(f.defs[spec.Name].Type())) //.MethodSet() 412 } 413 f.methodSet(ms) 414 } 415 } 416 case *ast.ImportSpec: 417 continue // Don't care. 418 } 419 } 420 case *ast.FuncDecl: 421 // Methods, top-level functions. 422 if f.match(n.Name.Name) { 423 n.Body = nil // Do not print the function body. 424 if methodFlag && n.Recv != nil { 425 f.printNode(n, n.Name, f.methodURL(n.Recv.List[0].Type, n.Name.Name)) 426 } else if functionFlag && n.Recv == nil { 427 f.printNode(n, n.Name, f.nameURL(n.Name.Name)) 428 } 429 } 430 } 431 return f 432} 433 434func (f *File) match(name string) bool { 435 // name must be exported. 436 if !ast.IsExported(name) { 437 return false 438 } 439 if f.regexp == nil { 440 if matchWordFlag { 441 if matchCaseFlag { 442 return name == f.ident 443 } 444 return strings.ToLower(name) == f.lowerIdent 445 } else { 446 if matchCaseFlag { 447 return strings.Contains(name, f.ident) 448 } 449 return strings.Contains(strings.ToLower(name), f.lowerIdent) 450 } 451 } 452 return f.regexp.MatchString(name) 453} 454 455func (f *File) printNode(node, ident ast.Node, url string) { 456 if !f.doPrint { 457 f.found = true 458 return 459 } 460 fmt.Printf("%s%s%s", url, f.sourcePos(f.fset.Position(ident.Pos())), f.docs(node)) 461} 462 463func (f *File) docs(node ast.Node) []byte { 464 if !docFlag { 465 return nil 466 } 467 commentedNode := printer.CommentedNode{Node: node} 468 if comments := f.comments.Filter(node).Comments(); comments != nil { 469 commentedNode.Comments = comments 470 } 471 var b bytes.Buffer 472 printer.Fprint(&b, f.fset, &commentedNode) 473 b.Write([]byte("\n\n")) // Add a blank line between entries if we print documentation. 474 return b.Bytes() 475} 476 477func (f *File) pkgComments() { 478 doc := f.file.Doc 479 if doc == nil { 480 return 481 } 482 url := "" 483 if urlFlag { 484 url = f.packageURL() + "\n" 485 } 486 docText := "" 487 if docFlag { 488 docText = fmt.Sprintf("package %s\n%s\n\n", f.file.Name.Name, doc.Text()) 489 } 490 fmt.Printf("%s%s%s", url, f.sourcePos(f.fset.Position(doc.Pos())), docText) 491} 492 493func (f *File) packageURL() string { 494 s := strings.TrimPrefix(f.name, f.pathPrefix) 495 // Now we have a path with a final file name. Drop it. 496 if i := strings.LastIndex(s, slash); i > 0 { 497 s = s[:i+1] 498 } 499 return f.urlPrefix + s 500} 501 502func (f *File) packageName() string { 503 s := strings.TrimPrefix(f.name, f.pathPrefix) 504 // Now we have a path with a final file name. Drop it. 505 if i := strings.LastIndex(s, slash); i > 0 { 506 s = s[:i+1] 507 } 508 s = strings.Trim(s, slash) 509 return filepath.ToSlash(s) 510} 511 512func (f *File) sourcePos(posn token.Position) string { 513 if !srcFlag { 514 return "" 515 } 516 return fmt.Sprintf("%s:%d:\n", posn.Filename, posn.Line) 517} 518 519func (f *File) nameURL(name string) string { 520 if !urlFlag { 521 return "" 522 } 523 return fmt.Sprintf("%s#%s\n", f.packageURL(), name) 524} 525 526func (f *File) methodURL(typ ast.Expr, name string) string { 527 if !urlFlag { 528 return "" 529 } 530 var b bytes.Buffer 531 printer.Fprint(&b, f.fset, typ) 532 typeName := b.Bytes() 533 if len(typeName) > 0 && typeName[0] == '*' { 534 typeName = typeName[1:] 535 } 536 return fmt.Sprintf("%s#%s.%s\n", f.packageURL(), typeName, name) 537} 538 539// Here follows the code to find and print a method (actually a method set, because 540// we want to do only one redundant tree walk, not one per method). 541// It should be much easier than walking the whole tree again, but that's what we must do. 542// TODO. 543 544type method struct { 545 index int // Which doc to write. (Keeps the results sorted) 546 *types.Selection 547} 548 549type methodVisitor struct { 550 *File 551 methods []method 552 docs []string 553} 554 555func (f *File) methodSet(set *types.MethodSet) { 556 // Build the set of things we're looking for. 557 methods := make([]method, 0, set.Len()) 558 docs := make([]string, set.Len()) 559 for i := 0; i < set.Len(); i++ { 560 if ast.IsExported(set.At(i).Obj().Name()) { 561 m := method{ 562 i, 563 set.At(i), 564 } 565 methods = append(methods, m) 566 } 567 } 568 if len(methods) == 0 { 569 return 570 } 571 // Collect the docs. 572 for _, file := range f.allFiles { 573 visitor := &methodVisitor{ 574 File: file, 575 methods: methods, 576 docs: docs, 577 } 578 ast.Walk(visitor, file.file) 579 methods = visitor.methods 580 } 581 // Print them in order. The incoming method set is sorted by name. 582 for _, doc := range docs { 583 if doc != "" { 584 fmt.Print(doc) 585 } 586 } 587} 588 589// Visit implements the ast.Visitor interface. 590func (visitor *methodVisitor) Visit(node ast.Node) ast.Visitor { 591 switch n := node.(type) { 592 case *ast.FuncDecl: 593 for i, method := range visitor.methods { 594 // If this is the right one, the position of the name of its identifier will match. 595 if method.Obj().Pos() == n.Name.Pos() { 596 n.Body = nil // TODO. Ugly - don't print the function body. 597 visitor.docs[method.index] = fmt.Sprintf("%s", visitor.File.docs(n)) 598 // If this was the last method, we're done. 599 if len(visitor.methods) == 1 { 600 return nil 601 } 602 // Drop this one from the list. 603 visitor.methods = append(visitor.methods[:i], visitor.methods[i+1:]...) 604 return visitor 605 } 606 } 607 } 608 return visitor 609} 610