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(&regexpFlag, "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