1// Copyright 2015 The Go 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 (usually run as go doc) accepts zero, one or two arguments.
6//
7// Zero arguments:
8//
9//	go doc
10//
11// Show the documentation for the package in the current directory.
12//
13// One argument:
14//
15//	go doc <pkg>
16//	go doc <sym>[.<methodOrField>]
17//	go doc [<pkg>.]<sym>[.<methodOrField>]
18//	go doc [<pkg>.][<sym>.]<methodOrField>
19//
20// The first item in this list that succeeds is the one whose documentation
21// is printed. If there is a symbol but no package, the package in the current
22// directory is chosen. However, if the argument begins with a capital
23// letter it is always assumed to be a symbol in the current directory.
24//
25// Two arguments:
26//
27//	go doc <pkg> <sym>[.<methodOrField>]
28//
29// Show the documentation for the package, symbol, and method or field. The
30// first argument must be a full package path. This is similar to the
31// command-line usage for the godoc command.
32//
33// For commands, unless the -cmd flag is present "go doc command"
34// shows only the package-level docs for the package.
35//
36// The -src flag causes doc to print the full source code for the symbol, such
37// as the body of a struct, function or method.
38//
39// The -all flag causes doc to print all documentation for the package and
40// all its visible symbols. The argument must identify a package.
41//
42// For complete documentation, run "go help doc".
43package main
44
45import (
46	"bytes"
47	"flag"
48	"fmt"
49	"go/build"
50	"go/token"
51	"io"
52	"log"
53	"os"
54	"path"
55	"path/filepath"
56	"strings"
57
58	"cmd/internal/telemetry/counter"
59)
60
61var (
62	unexported bool   // -u flag
63	matchCase  bool   // -c flag
64	chdir      string // -C flag
65	showAll    bool   // -all flag
66	showCmd    bool   // -cmd flag
67	showSrc    bool   // -src flag
68	short      bool   // -short flag
69)
70
71// usage is a replacement usage function for the flags package.
72func usage() {
73	fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n")
74	fmt.Fprintf(os.Stderr, "\tgo doc\n")
75	fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n")
76	fmt.Fprintf(os.Stderr, "\tgo doc <sym>[.<methodOrField>]\n")
77	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.]<sym>[.<methodOrField>]\n")
78	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.][<sym>.]<methodOrField>\n")
79	fmt.Fprintf(os.Stderr, "\tgo doc <pkg> <sym>[.<methodOrField>]\n")
80	fmt.Fprintf(os.Stderr, "For more information run\n")
81	fmt.Fprintf(os.Stderr, "\tgo help doc\n\n")
82	fmt.Fprintf(os.Stderr, "Flags:\n")
83	flag.PrintDefaults()
84	os.Exit(2)
85}
86
87func main() {
88	log.SetFlags(0)
89	log.SetPrefix("doc: ")
90	counter.Open()
91	dirsInit()
92	err := do(os.Stdout, flag.CommandLine, os.Args[1:])
93	if err != nil {
94		log.Fatal(err)
95	}
96}
97
98// do is the workhorse, broken out of main to make testing easier.
99func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
100	flagSet.Usage = usage
101	unexported = false
102	matchCase = false
103	flagSet.StringVar(&chdir, "C", "", "change to `dir` before running command")
104	flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported")
105	flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)")
106	flagSet.BoolVar(&showAll, "all", false, "show all documentation for package")
107	flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command")
108	flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol")
109	flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol")
110	flagSet.Parse(args)
111	counter.Inc("doc/invocations")
112	counter.CountFlags("doc/flag:", *flag.CommandLine)
113	if chdir != "" {
114		if err := os.Chdir(chdir); err != nil {
115			return err
116		}
117	}
118	var paths []string
119	var symbol, method string
120	// Loop until something is printed.
121	dirs.Reset()
122	for i := 0; ; i++ {
123		buildPackage, userPath, sym, more := parseArgs(flagSet.Args())
124		if i > 0 && !more { // Ignore the "more" bit on the first iteration.
125			return failMessage(paths, symbol, method)
126		}
127		if buildPackage == nil {
128			return fmt.Errorf("no such package: %s", userPath)
129		}
130
131		// The builtin package needs special treatment: its symbols are lower
132		// case but we want to see them, always.
133		if buildPackage.ImportPath == "builtin" {
134			unexported = true
135		}
136
137		symbol, method = parseSymbol(sym)
138		pkg := parsePackage(writer, buildPackage, userPath)
139		paths = append(paths, pkg.prettyPath())
140
141		defer func() {
142			pkg.flush()
143			e := recover()
144			if e == nil {
145				return
146			}
147			pkgError, ok := e.(PackageError)
148			if ok {
149				err = pkgError
150				return
151			}
152			panic(e)
153		}()
154
155		switch {
156		case symbol == "":
157			pkg.packageDoc() // The package exists, so we got some output.
158			return
159		case method == "":
160			if pkg.symbolDoc(symbol) {
161				return
162			}
163		case pkg.printMethodDoc(symbol, method):
164			return
165		case pkg.printFieldDoc(symbol, method):
166			return
167		}
168	}
169}
170
171// failMessage creates a nicely formatted error message when there is no result to show.
172func failMessage(paths []string, symbol, method string) error {
173	var b bytes.Buffer
174	if len(paths) > 1 {
175		b.WriteString("s")
176	}
177	b.WriteString(" ")
178	for i, path := range paths {
179		if i > 0 {
180			b.WriteString(", ")
181		}
182		b.WriteString(path)
183	}
184	if method == "" {
185		return fmt.Errorf("no symbol %s in package%s", symbol, &b)
186	}
187	return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b)
188}
189
190// parseArgs analyzes the arguments (if any) and returns the package
191// it represents, the part of the argument the user used to identify
192// the path (or "" if it's the current package) and the symbol
193// (possibly with a .method) within that package.
194// parseSymbol is used to analyze the symbol itself.
195// The boolean final argument reports whether it is possible that
196// there may be more directories worth looking at. It will only
197// be true if the package path is a partial match for some directory
198// and there may be more matches. For example, if the argument
199// is rand.Float64, we must scan both crypto/rand and math/rand
200// to find the symbol, and the first call will return crypto/rand, true.
201func parseArgs(args []string) (pkg *build.Package, path, symbol string, more bool) {
202	wd, err := os.Getwd()
203	if err != nil {
204		log.Fatal(err)
205	}
206	if len(args) == 0 {
207		// Easy: current directory.
208		return importDir(wd), "", "", false
209	}
210	arg := args[0]
211	// We have an argument. If it is a directory name beginning with . or ..,
212	// use the absolute path name. This discriminates "./errors" from "errors"
213	// if the current directory contains a non-standard errors package.
214	if isDotSlash(arg) {
215		arg = filepath.Join(wd, arg)
216	}
217	switch len(args) {
218	default:
219		usage()
220	case 1:
221		// Done below.
222	case 2:
223		// Package must be findable and importable.
224		pkg, err := build.Import(args[0], wd, build.ImportComment)
225		if err == nil {
226			return pkg, args[0], args[1], false
227		}
228		for {
229			packagePath, ok := findNextPackage(arg)
230			if !ok {
231				break
232			}
233			if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil {
234				return pkg, arg, args[1], true
235			}
236		}
237		return nil, args[0], args[1], false
238	}
239	// Usual case: one argument.
240	// If it contains slashes, it begins with either a package path
241	// or an absolute directory.
242	// First, is it a complete package path as it is? If so, we are done.
243	// This avoids confusion over package paths that have other
244	// package paths as their prefix.
245	var importErr error
246	if filepath.IsAbs(arg) {
247		pkg, importErr = build.ImportDir(arg, build.ImportComment)
248		if importErr == nil {
249			return pkg, arg, "", false
250		}
251	} else {
252		pkg, importErr = build.Import(arg, wd, build.ImportComment)
253		if importErr == nil {
254			return pkg, arg, "", false
255		}
256	}
257	// Another disambiguator: If the argument starts with an upper
258	// case letter, it can only be a symbol in the current directory.
259	// Kills the problem caused by case-insensitive file systems
260	// matching an upper case name as a package name.
261	if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) {
262		pkg, err := build.ImportDir(".", build.ImportComment)
263		if err == nil {
264			return pkg, "", arg, false
265		}
266	}
267	// If it has a slash, it must be a package path but there is a symbol.
268	// It's the last package path we care about.
269	slash := strings.LastIndex(arg, "/")
270	// There may be periods in the package path before or after the slash
271	// and between a symbol and method.
272	// Split the string at various periods to see what we find.
273	// In general there may be ambiguities but this should almost always
274	// work.
275	var period int
276	// slash+1: if there's no slash, the value is -1 and start is 0; otherwise
277	// start is the byte after the slash.
278	for start := slash + 1; start < len(arg); start = period + 1 {
279		period = strings.Index(arg[start:], ".")
280		symbol := ""
281		if period < 0 {
282			period = len(arg)
283		} else {
284			period += start
285			symbol = arg[period+1:]
286		}
287		// Have we identified a package already?
288		pkg, err := build.Import(arg[0:period], wd, build.ImportComment)
289		if err == nil {
290			return pkg, arg[0:period], symbol, false
291		}
292		// See if we have the basename or tail of a package, as in json for encoding/json
293		// or ivy/value for robpike.io/ivy/value.
294		pkgName := arg[:period]
295		for {
296			path, ok := findNextPackage(pkgName)
297			if !ok {
298				break
299			}
300			if pkg, err = build.ImportDir(path, build.ImportComment); err == nil {
301				return pkg, arg[0:period], symbol, true
302			}
303		}
304		dirs.Reset() // Next iteration of for loop must scan all the directories again.
305	}
306	// If it has a slash, we've failed.
307	if slash >= 0 {
308		// build.Import should always include the path in its error message,
309		// and we should avoid repeating it. Unfortunately, build.Import doesn't
310		// return a structured error. That can't easily be fixed, since it
311		// invokes 'go list' and returns the error text from the loaded package.
312		// TODO(golang.org/issue/34750): load using golang.org/x/tools/go/packages
313		// instead of go/build.
314		importErrStr := importErr.Error()
315		if strings.Contains(importErrStr, arg[:period]) {
316			log.Fatal(importErrStr)
317		} else {
318			log.Fatalf("no such package %s: %s", arg[:period], importErrStr)
319		}
320	}
321	// Guess it's a symbol in the current directory.
322	return importDir(wd), "", arg, false
323}
324
325// dotPaths lists all the dotted paths legal on Unix-like and
326// Windows-like file systems. We check them all, as the chance
327// of error is minute and even on Windows people will use ./
328// sometimes.
329var dotPaths = []string{
330	`./`,
331	`../`,
332	`.\`,
333	`..\`,
334}
335
336// isDotSlash reports whether the path begins with a reference
337// to the local . or .. directory.
338func isDotSlash(arg string) bool {
339	if arg == "." || arg == ".." {
340		return true
341	}
342	for _, dotPath := range dotPaths {
343		if strings.HasPrefix(arg, dotPath) {
344			return true
345		}
346	}
347	return false
348}
349
350// importDir is just an error-catching wrapper for build.ImportDir.
351func importDir(dir string) *build.Package {
352	pkg, err := build.ImportDir(dir, build.ImportComment)
353	if err != nil {
354		log.Fatal(err)
355	}
356	return pkg
357}
358
359// parseSymbol breaks str apart into a symbol and method.
360// Both may be missing or the method may be missing.
361// If present, each must be a valid Go identifier.
362func parseSymbol(str string) (symbol, method string) {
363	if str == "" {
364		return
365	}
366	elem := strings.Split(str, ".")
367	switch len(elem) {
368	case 1:
369	case 2:
370		method = elem[1]
371	default:
372		log.Printf("too many periods in symbol specification")
373		usage()
374	}
375	symbol = elem[0]
376	return
377}
378
379// isExported reports whether the name is an exported identifier.
380// If the unexported flag (-u) is true, isExported returns true because
381// it means that we treat the name as if it is exported.
382func isExported(name string) bool {
383	return unexported || token.IsExported(name)
384}
385
386// findNextPackage returns the next full file name path that matches the
387// (perhaps partial) package path pkg. The boolean reports if any match was found.
388func findNextPackage(pkg string) (string, bool) {
389	if filepath.IsAbs(pkg) {
390		if dirs.offset == 0 {
391			dirs.offset = -1
392			return pkg, true
393		}
394		return "", false
395	}
396	if pkg == "" || token.IsExported(pkg) { // Upper case symbol cannot be a package name.
397		return "", false
398	}
399	pkg = path.Clean(pkg)
400	pkgSuffix := "/" + pkg
401	for {
402		d, ok := dirs.Next()
403		if !ok {
404			return "", false
405		}
406		if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) {
407			return d.dir, true
408		}
409	}
410}
411
412var buildCtx = build.Default
413
414// splitGopath splits $GOPATH into a list of roots.
415func splitGopath() []string {
416	return filepath.SplitList(buildCtx.GOPATH)
417}
418