Source file src/cmd/go/internal/doc/doc.go

     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  // Package doc implements the “go doc” command.
     6  package doc
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"flag"
    12  	"fmt"
    13  	"go/build"
    14  	"go/token"
    15  	"io"
    16  	"log"
    17  	"os"
    18  	"os/exec"
    19  	"path"
    20  	"path/filepath"
    21  	"strings"
    22  
    23  	"cmd/go/internal/base"
    24  	"cmd/internal/telemetry/counter"
    25  )
    26  
    27  var CmdDoc = &base.Command{
    28  	Run:         runDoc,
    29  	UsageLine:   "go doc [doc flags] [package|[package.]symbol[.methodOrField]]",
    30  	CustomFlags: true,
    31  	Short:       "show documentation for package or symbol",
    32  	Long: `
    33  Doc prints the documentation comments associated with the item identified by its
    34  arguments (a package, const, func, type, var, method, or struct field)
    35  followed by a one-line summary of each of the first-level items "under"
    36  that item (package-level declarations for a package, methods for a type,
    37  etc.).
    38  
    39  Doc accepts zero, one, or two arguments.
    40  
    41  Given no arguments, that is, when run as
    42  
    43  	go doc
    44  
    45  it prints the package documentation for the package in the current directory.
    46  If the package is a command (package main), the exported symbols of the package
    47  are elided from the presentation unless the -cmd flag is provided.
    48  
    49  When run with one argument, the argument is treated as a Go-syntax-like
    50  representation of the item to be documented. What the argument selects depends
    51  on what is installed in GOROOT and GOPATH, as well as the form of the argument,
    52  which is schematically one of these:
    53  
    54  	go doc <pkg>
    55  	go doc <sym>[.<methodOrField>]
    56  	go doc [<pkg>.]<sym>[.<methodOrField>]
    57  	go doc [<pkg>.][<sym>.]<methodOrField>
    58  
    59  The first item in this list matched by the argument is the one whose documentation
    60  is printed. (See the examples below.) However, if the argument starts with a capital
    61  letter it is assumed to identify a symbol or method in the current directory.
    62  
    63  For packages, the order of scanning is determined lexically in breadth-first order.
    64  That is, the package presented is the one that matches the search and is nearest
    65  the root and lexically first at its level of the hierarchy. The GOROOT tree is
    66  always scanned in its entirety before GOPATH.
    67  
    68  If there is no package specified or matched, the package in the current
    69  directory is selected, so "go doc Foo" shows the documentation for symbol Foo in
    70  the current package.
    71  
    72  The package path must be either a qualified path or a proper suffix of a
    73  path. The go tool's usual package mechanism does not apply: package path
    74  elements like . and ... are not implemented by go doc.
    75  
    76  When run with two arguments, the first is a package path (full path or suffix),
    77  and the second is a symbol, or symbol with method or struct field:
    78  
    79  	go doc <pkg> <sym>[.<methodOrField>]
    80  
    81  In all forms, when matching symbols, lower-case letters in the argument match
    82  either case but upper-case letters match exactly. This means that there may be
    83  multiple matches of a lower-case argument in a package if different symbols have
    84  different cases. If this occurs, documentation for all matches is printed.
    85  
    86  Examples:
    87  	go doc
    88  		Show documentation for current package.
    89  	go doc -http
    90  		Serve HTML documentation over HTTP for the current package.
    91  	go doc Foo
    92  		Show documentation for Foo in the current package.
    93  		(Foo starts with a capital letter so it cannot match
    94  		a package path.)
    95  	go doc encoding/json
    96  		Show documentation for the encoding/json package.
    97  	go doc json
    98  		Shorthand for encoding/json.
    99  	go doc json.Number (or go doc json.number)
   100  		Show documentation and method summary for json.Number.
   101  	go doc json.Number.Int64 (or go doc json.number.int64)
   102  		Show documentation for json.Number's Int64 method.
   103  	go doc cmd/doc
   104  		Show package docs for the doc command.
   105  	go doc -cmd cmd/doc
   106  		Show package docs and exported symbols within the doc command.
   107  	go doc template.new
   108  		Show documentation for html/template's New function.
   109  		(html/template is lexically before text/template)
   110  	go doc text/template.new # One argument
   111  		Show documentation for text/template's New function.
   112  	go doc text/template new # Two arguments
   113  		Show documentation for text/template's New function.
   114  
   115  	At least in the current tree, these invocations all print the
   116  	documentation for json.Decoder's Decode method:
   117  
   118  	go doc json.Decoder.Decode
   119  	go doc json.decoder.decode
   120  	go doc json.decode
   121  	cd go/src/encoding/json; go doc decode
   122  
   123  Flags:
   124  	-all
   125  		Show all the documentation for the package.
   126  	-c
   127  		Respect case when matching symbols.
   128  	-cmd
   129  		Treat a command (package main) like a regular package.
   130  		Otherwise package main's exported symbols are hidden
   131  		when showing the package's top-level documentation.
   132    	-http
   133  		Serve HTML docs over HTTP.
   134  	-short
   135  		One-line representation for each symbol.
   136  	-src
   137  		Show the full source code for the symbol. This will
   138  		display the full Go source of its declaration and
   139  		definition, such as a function definition (including
   140  		the body), type declaration or enclosing const
   141  		block. The output may therefore include unexported
   142  		details.
   143  	-u
   144  		Show documentation for unexported as well as exported
   145  		symbols, methods, and fields.
   146  `,
   147  }
   148  
   149  func runDoc(ctx context.Context, cmd *base.Command, args []string) {
   150  	log.SetFlags(0)
   151  	log.SetPrefix("doc: ")
   152  	dirsInit()
   153  	var flagSet flag.FlagSet
   154  	err := do(os.Stdout, &flagSet, args)
   155  	if err != nil {
   156  		log.Fatal(err)
   157  	}
   158  }
   159  
   160  var (
   161  	unexported bool   // -u flag
   162  	matchCase  bool   // -c flag
   163  	chdir      string // -C flag
   164  	showAll    bool   // -all flag
   165  	showCmd    bool   // -cmd flag
   166  	showSrc    bool   // -src flag
   167  	short      bool   // -short flag
   168  	serveHTTP  bool   // -http flag
   169  )
   170  
   171  // usage is a replacement usage function for the flags package.
   172  func usage(flagSet *flag.FlagSet) {
   173  	fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n")
   174  	fmt.Fprintf(os.Stderr, "\tgo doc\n")
   175  	fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n")
   176  	fmt.Fprintf(os.Stderr, "\tgo doc <sym>[.<methodOrField>]\n")
   177  	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.]<sym>[.<methodOrField>]\n")
   178  	fmt.Fprintf(os.Stderr, "\tgo doc [<pkg>.][<sym>.]<methodOrField>\n")
   179  	fmt.Fprintf(os.Stderr, "\tgo doc <pkg> <sym>[.<methodOrField>]\n")
   180  	fmt.Fprintf(os.Stderr, "For more information run\n")
   181  	fmt.Fprintf(os.Stderr, "\tgo help doc\n\n")
   182  	fmt.Fprintf(os.Stderr, "Flags:\n")
   183  	flagSet.PrintDefaults()
   184  	os.Exit(2)
   185  }
   186  
   187  // do is the workhorse, broken out of runDoc to make testing easier.
   188  func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
   189  	flagSet.Usage = func() { usage(flagSet) }
   190  	unexported = false
   191  	matchCase = false
   192  	flagSet.StringVar(&chdir, "C", "", "change to `dir` before running command")
   193  	flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported")
   194  	flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)")
   195  	flagSet.BoolVar(&showAll, "all", false, "show all documentation for package")
   196  	flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command")
   197  	flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol")
   198  	flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol")
   199  	flagSet.BoolVar(&serveHTTP, "http", false, "serve HTML docs over HTTP")
   200  	flagSet.Parse(args)
   201  	counter.CountFlags("doc/flag:", *flag.CommandLine)
   202  	if chdir != "" {
   203  		if err := os.Chdir(chdir); err != nil {
   204  			return err
   205  		}
   206  	}
   207  	if serveHTTP {
   208  		// Special case: if there are no arguments, try to go to an appropriate page
   209  		// depending on whether we're in a module or workspace. The pkgsite homepage
   210  		// is often not the most useful page.
   211  		if len(flagSet.Args()) == 0 {
   212  			mod, err := runCmd(append(os.Environ(), "GOWORK=off"), "go", "list", "-m")
   213  			if err == nil && mod != "" && mod != "command-line-arguments" {
   214  				// If there's a module, go to the module's doc page.
   215  				return doPkgsite(mod)
   216  			}
   217  			gowork, err := runCmd(nil, "go", "env", "GOWORK")
   218  			if err == nil && gowork != "" {
   219  				// Outside a module, but in a workspace, go to the home page
   220  				// with links to each of the modules' pages.
   221  				return doPkgsite("")
   222  			}
   223  			// Outside a module or workspace, go to the documentation for the standard library.
   224  			return doPkgsite("std")
   225  		}
   226  
   227  		// If args are provided, we need to figure out which page to open on the pkgsite
   228  		// instance. Run the logic below to determine a match for a symbol, method,
   229  		// or field, but don't actually print the documentation to the output.
   230  		writer = io.Discard
   231  	}
   232  	var paths []string
   233  	var symbol, method string
   234  	// Loop until something is printed.
   235  	dirs.Reset()
   236  	for i := 0; ; i++ {
   237  		buildPackage, userPath, sym, more := parseArgs(flagSet, flagSet.Args())
   238  		if i > 0 && !more { // Ignore the "more" bit on the first iteration.
   239  			return failMessage(paths, symbol, method)
   240  		}
   241  		if buildPackage == nil {
   242  			return fmt.Errorf("no such package: %s", userPath)
   243  		}
   244  
   245  		// The builtin package needs special treatment: its symbols are lower
   246  		// case but we want to see them, always.
   247  		if buildPackage.ImportPath == "builtin" {
   248  			unexported = true
   249  		}
   250  
   251  		symbol, method = parseSymbol(flagSet, sym)
   252  		pkg := parsePackage(writer, buildPackage, userPath)
   253  		paths = append(paths, pkg.prettyPath())
   254  
   255  		defer func() {
   256  			pkg.flush()
   257  			e := recover()
   258  			if e == nil {
   259  				return
   260  			}
   261  			pkgError, ok := e.(PackageError)
   262  			if ok {
   263  				err = pkgError
   264  				return
   265  			}
   266  			panic(e)
   267  		}()
   268  
   269  		var found bool
   270  		switch {
   271  		case symbol == "":
   272  			pkg.packageDoc() // The package exists, so we got some output.
   273  			found = true
   274  		case method == "":
   275  			if pkg.symbolDoc(symbol) {
   276  				found = true
   277  			}
   278  		case pkg.printMethodDoc(symbol, method):
   279  			found = true
   280  		case pkg.printFieldDoc(symbol, method):
   281  			found = true
   282  		}
   283  		if found {
   284  			if serveHTTP {
   285  				path, err := objectPath(userPath, pkg, symbol, method)
   286  				if err != nil {
   287  					return err
   288  				}
   289  				return doPkgsite(path)
   290  			}
   291  			return nil
   292  		}
   293  	}
   294  }
   295  
   296  func runCmd(env []string, cmdline ...string) (string, error) {
   297  	var stdout, stderr strings.Builder
   298  	cmd := exec.Command(cmdline[0], cmdline[1:]...)
   299  	cmd.Env = env
   300  	cmd.Stdout = &stdout
   301  	cmd.Stderr = &stderr
   302  	if err := cmd.Run(); err != nil {
   303  		return "", fmt.Errorf("go doc: %s: %v\n%s\n", strings.Join(cmdline, " "), err, stderr.String())
   304  	}
   305  	return strings.TrimSpace(stdout.String()), nil
   306  }
   307  
   308  func objectPath(userPath string, pkg *Package, symbol, method string) (string, error) {
   309  	var err error
   310  	path := pkg.build.ImportPath
   311  	if path == "." {
   312  		// go/build couldn't determine the import path, probably
   313  		// because this was a relative path into a module. Use
   314  		// go list to get the import path.
   315  		path, err = runCmd(nil, "go", "list", userPath)
   316  		if err != nil {
   317  			return "", err
   318  		}
   319  	}
   320  
   321  	object := symbol
   322  	if symbol != "" && method != "" {
   323  		object = symbol + "." + method
   324  	}
   325  	if object != "" {
   326  		path = path + "#" + object
   327  	}
   328  	return path, nil
   329  }
   330  
   331  // failMessage creates a nicely formatted error message when there is no result to show.
   332  func failMessage(paths []string, symbol, method string) error {
   333  	var b bytes.Buffer
   334  	if len(paths) > 1 {
   335  		b.WriteString("s")
   336  	}
   337  	b.WriteString(" ")
   338  	for i, path := range paths {
   339  		if i > 0 {
   340  			b.WriteString(", ")
   341  		}
   342  		b.WriteString(path)
   343  	}
   344  	if method == "" {
   345  		return fmt.Errorf("no symbol %s in package%s", symbol, &b)
   346  	}
   347  	return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b)
   348  }
   349  
   350  // parseArgs analyzes the arguments (if any) and returns the package
   351  // it represents, the part of the argument the user used to identify
   352  // the path (or "" if it's the current package) and the symbol
   353  // (possibly with a .method) within that package.
   354  // parseSymbol is used to analyze the symbol itself.
   355  // The boolean final argument reports whether it is possible that
   356  // there may be more directories worth looking at. It will only
   357  // be true if the package path is a partial match for some directory
   358  // and there may be more matches. For example, if the argument
   359  // is rand.Float64, we must scan both crypto/rand and math/rand
   360  // to find the symbol, and the first call will return crypto/rand, true.
   361  func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path, symbol string, more bool) {
   362  	wd, err := os.Getwd()
   363  	if err != nil {
   364  		log.Fatal(err)
   365  	}
   366  	if len(args) == 0 {
   367  		// Easy: current directory.
   368  		return importDir(wd), "", "", false
   369  	}
   370  	arg := args[0]
   371  	// We have an argument. If it is a directory name beginning with . or ..,
   372  	// use the absolute path name. This discriminates "./errors" from "errors"
   373  	// if the current directory contains a non-standard errors package.
   374  	if isDotSlash(arg) {
   375  		arg = filepath.Join(wd, arg)
   376  	}
   377  	switch len(args) {
   378  	default:
   379  		usage(flagSet)
   380  	case 1:
   381  		// Done below.
   382  	case 2:
   383  		// Package must be findable and importable.
   384  		pkg, err := build.Import(args[0], wd, build.ImportComment)
   385  		if err == nil {
   386  			return pkg, args[0], args[1], false
   387  		}
   388  		for {
   389  			packagePath, ok := findNextPackage(arg)
   390  			if !ok {
   391  				break
   392  			}
   393  			if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil {
   394  				return pkg, arg, args[1], true
   395  			}
   396  		}
   397  		return nil, args[0], args[1], false
   398  	}
   399  	// Usual case: one argument.
   400  	// If it contains slashes, it begins with either a package path
   401  	// or an absolute directory.
   402  	// First, is it a complete package path as it is? If so, we are done.
   403  	// This avoids confusion over package paths that have other
   404  	// package paths as their prefix.
   405  	var importErr error
   406  	if filepath.IsAbs(arg) {
   407  		pkg, importErr = build.ImportDir(arg, build.ImportComment)
   408  		if importErr == nil {
   409  			return pkg, arg, "", false
   410  		}
   411  	} else {
   412  		pkg, importErr = build.Import(arg, wd, build.ImportComment)
   413  		if importErr == nil {
   414  			return pkg, arg, "", false
   415  		}
   416  	}
   417  	// Another disambiguator: If the argument starts with an upper
   418  	// case letter, it can only be a symbol in the current directory.
   419  	// Kills the problem caused by case-insensitive file systems
   420  	// matching an upper case name as a package name.
   421  	if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) {
   422  		pkg, err := build.ImportDir(".", build.ImportComment)
   423  		if err == nil {
   424  			return pkg, "", arg, false
   425  		}
   426  	}
   427  	// If it has a slash, it must be a package path but there is a symbol.
   428  	// It's the last package path we care about.
   429  	slash := strings.LastIndex(arg, "/")
   430  	// There may be periods in the package path before or after the slash
   431  	// and between a symbol and method.
   432  	// Split the string at various periods to see what we find.
   433  	// In general there may be ambiguities but this should almost always
   434  	// work.
   435  	var period int
   436  	// slash+1: if there's no slash, the value is -1 and start is 0; otherwise
   437  	// start is the byte after the slash.
   438  	for start := slash + 1; start < len(arg); start = period + 1 {
   439  		period = strings.Index(arg[start:], ".")
   440  		symbol := ""
   441  		if period < 0 {
   442  			period = len(arg)
   443  		} else {
   444  			period += start
   445  			symbol = arg[period+1:]
   446  		}
   447  		// Have we identified a package already?
   448  		pkg, err := build.Import(arg[0:period], wd, build.ImportComment)
   449  		if err == nil {
   450  			return pkg, arg[0:period], symbol, false
   451  		}
   452  		// See if we have the basename or tail of a package, as in json for encoding/json
   453  		// or ivy/value for robpike.io/ivy/value.
   454  		pkgName := arg[:period]
   455  		for {
   456  			path, ok := findNextPackage(pkgName)
   457  			if !ok {
   458  				break
   459  			}
   460  			if pkg, err = build.ImportDir(path, build.ImportComment); err == nil {
   461  				return pkg, arg[0:period], symbol, true
   462  			}
   463  		}
   464  		dirs.Reset() // Next iteration of for loop must scan all the directories again.
   465  	}
   466  	// If it has a slash, we've failed.
   467  	if slash >= 0 {
   468  		// build.Import should always include the path in its error message,
   469  		// and we should avoid repeating it. Unfortunately, build.Import doesn't
   470  		// return a structured error. That can't easily be fixed, since it
   471  		// invokes 'go list' and returns the error text from the loaded package.
   472  		// TODO(golang.org/issue/34750): load using golang.org/x/tools/go/packages
   473  		// instead of go/build.
   474  		importErrStr := importErr.Error()
   475  		if strings.Contains(importErrStr, arg[:period]) {
   476  			log.Fatal(importErrStr)
   477  		} else {
   478  			log.Fatalf("no such package %s: %s", arg[:period], importErrStr)
   479  		}
   480  	}
   481  	// Guess it's a symbol in the current directory.
   482  	return importDir(wd), "", arg, false
   483  }
   484  
   485  // dotPaths lists all the dotted paths legal on Unix-like and
   486  // Windows-like file systems. We check them all, as the chance
   487  // of error is minute and even on Windows people will use ./
   488  // sometimes.
   489  var dotPaths = []string{
   490  	`./`,
   491  	`../`,
   492  	`.\`,
   493  	`..\`,
   494  }
   495  
   496  // isDotSlash reports whether the path begins with a reference
   497  // to the local . or .. directory.
   498  func isDotSlash(arg string) bool {
   499  	if arg == "." || arg == ".." {
   500  		return true
   501  	}
   502  	for _, dotPath := range dotPaths {
   503  		if strings.HasPrefix(arg, dotPath) {
   504  			return true
   505  		}
   506  	}
   507  	return false
   508  }
   509  
   510  // importDir is just an error-catching wrapper for build.ImportDir.
   511  func importDir(dir string) *build.Package {
   512  	pkg, err := build.ImportDir(dir, build.ImportComment)
   513  	if err != nil {
   514  		log.Fatal(err)
   515  	}
   516  	return pkg
   517  }
   518  
   519  // parseSymbol breaks str apart into a symbol and method.
   520  // Both may be missing or the method may be missing.
   521  // If present, each must be a valid Go identifier.
   522  func parseSymbol(flagSet *flag.FlagSet, str string) (symbol, method string) {
   523  	if str == "" {
   524  		return
   525  	}
   526  	elem := strings.Split(str, ".")
   527  	switch len(elem) {
   528  	case 1:
   529  	case 2:
   530  		method = elem[1]
   531  	default:
   532  		log.Printf("too many periods in symbol specification")
   533  		usage(flagSet)
   534  	}
   535  	symbol = elem[0]
   536  	return
   537  }
   538  
   539  // isExported reports whether the name is an exported identifier.
   540  // If the unexported flag (-u) is true, isExported returns true because
   541  // it means that we treat the name as if it is exported.
   542  func isExported(name string) bool {
   543  	return unexported || token.IsExported(name)
   544  }
   545  
   546  // findNextPackage returns the next full file name path that matches the
   547  // (perhaps partial) package path pkg. The boolean reports if any match was found.
   548  func findNextPackage(pkg string) (string, bool) {
   549  	if filepath.IsAbs(pkg) {
   550  		if dirs.offset == 0 {
   551  			dirs.offset = -1
   552  			return pkg, true
   553  		}
   554  		return "", false
   555  	}
   556  	if pkg == "" || token.IsExported(pkg) { // Upper case symbol cannot be a package name.
   557  		return "", false
   558  	}
   559  	pkg = path.Clean(pkg)
   560  	pkgSuffix := "/" + pkg
   561  	for {
   562  		d, ok := dirs.Next()
   563  		if !ok {
   564  			return "", false
   565  		}
   566  		if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) {
   567  			return d.dir, true
   568  		}
   569  	}
   570  }
   571  
   572  var buildCtx = build.Default
   573  
   574  // splitGopath splits $GOPATH into a list of roots.
   575  func splitGopath() []string {
   576  	return filepath.SplitList(buildCtx.GOPATH)
   577  }
   578  

View as plain text