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, fragment, err := objectPath(userPath, pkg, symbol, method)
   286  				if err != nil {
   287  					return err
   288  				}
   289  				return doPkgsite(path, fragment)
   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  // returns a path followed by a fragment (or an error)
   309  func objectPath(userPath string, pkg *Package, symbol, method string) (string, string, error) {
   310  	var err error
   311  	path := pkg.build.ImportPath
   312  	if path == "." {
   313  		// go/build couldn't determine the import path, probably
   314  		// because this was a relative path into a module. Use
   315  		// go list to get the import path.
   316  		path, err = runCmd(nil, "go", "list", userPath)
   317  		if err != nil {
   318  			return "", "", err
   319  		}
   320  	}
   321  
   322  	object := symbol
   323  	if symbol != "" && method != "" {
   324  		object = symbol + "." + method
   325  	}
   326  	return path, object, nil
   327  }
   328  
   329  // failMessage creates a nicely formatted error message when there is no result to show.
   330  func failMessage(paths []string, symbol, method string) error {
   331  	var b bytes.Buffer
   332  	if len(paths) > 1 {
   333  		b.WriteString("s")
   334  	}
   335  	b.WriteString(" ")
   336  	for i, path := range paths {
   337  		if i > 0 {
   338  			b.WriteString(", ")
   339  		}
   340  		b.WriteString(path)
   341  	}
   342  	if method == "" {
   343  		return fmt.Errorf("no symbol %s in package%s", symbol, &b)
   344  	}
   345  	return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b)
   346  }
   347  
   348  // parseArgs analyzes the arguments (if any) and returns the package
   349  // it represents, the part of the argument the user used to identify
   350  // the path (or "" if it's the current package) and the symbol
   351  // (possibly with a .method) within that package.
   352  // parseSymbol is used to analyze the symbol itself.
   353  // The boolean final argument reports whether it is possible that
   354  // there may be more directories worth looking at. It will only
   355  // be true if the package path is a partial match for some directory
   356  // and there may be more matches. For example, if the argument
   357  // is rand.Float64, we must scan both crypto/rand and math/rand
   358  // to find the symbol, and the first call will return crypto/rand, true.
   359  func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path, symbol string, more bool) {
   360  	wd, err := os.Getwd()
   361  	if err != nil {
   362  		log.Fatal(err)
   363  	}
   364  	if len(args) == 0 {
   365  		// Easy: current directory.
   366  		return importDir(wd), "", "", false
   367  	}
   368  	arg := args[0]
   369  	// We have an argument. If it is a directory name beginning with . or ..,
   370  	// use the absolute path name. This discriminates "./errors" from "errors"
   371  	// if the current directory contains a non-standard errors package.
   372  	if isDotSlash(arg) {
   373  		arg = filepath.Join(wd, arg)
   374  	}
   375  	switch len(args) {
   376  	default:
   377  		usage(flagSet)
   378  	case 1:
   379  		// Done below.
   380  	case 2:
   381  		// Package must be findable and importable.
   382  		pkg, err := build.Import(args[0], wd, build.ImportComment)
   383  		if err == nil {
   384  			return pkg, args[0], args[1], false
   385  		}
   386  		for {
   387  			packagePath, ok := findNextPackage(arg)
   388  			if !ok {
   389  				break
   390  			}
   391  			if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil {
   392  				return pkg, arg, args[1], true
   393  			}
   394  		}
   395  		return nil, args[0], args[1], false
   396  	}
   397  	// Usual case: one argument.
   398  	// If it contains slashes, it begins with either a package path
   399  	// or an absolute directory.
   400  	// First, is it a complete package path as it is? If so, we are done.
   401  	// This avoids confusion over package paths that have other
   402  	// package paths as their prefix.
   403  	var importErr error
   404  	if filepath.IsAbs(arg) {
   405  		pkg, importErr = build.ImportDir(arg, build.ImportComment)
   406  		if importErr == nil {
   407  			return pkg, arg, "", false
   408  		}
   409  	} else {
   410  		pkg, importErr = build.Import(arg, wd, build.ImportComment)
   411  		if importErr == nil {
   412  			return pkg, arg, "", false
   413  		}
   414  	}
   415  	// Another disambiguator: If the argument starts with an upper
   416  	// case letter, it can only be a symbol in the current directory.
   417  	// Kills the problem caused by case-insensitive file systems
   418  	// matching an upper case name as a package name.
   419  	if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) {
   420  		pkg, err := build.ImportDir(".", build.ImportComment)
   421  		if err == nil {
   422  			return pkg, "", arg, false
   423  		}
   424  	}
   425  	// If it has a slash, it must be a package path but there is a symbol.
   426  	// It's the last package path we care about.
   427  	slash := strings.LastIndex(arg, "/")
   428  	// There may be periods in the package path before or after the slash
   429  	// and between a symbol and method.
   430  	// Split the string at various periods to see what we find.
   431  	// In general there may be ambiguities but this should almost always
   432  	// work.
   433  	var period int
   434  	// slash+1: if there's no slash, the value is -1 and start is 0; otherwise
   435  	// start is the byte after the slash.
   436  	for start := slash + 1; start < len(arg); start = period + 1 {
   437  		period = strings.Index(arg[start:], ".")
   438  		symbol := ""
   439  		if period < 0 {
   440  			period = len(arg)
   441  		} else {
   442  			period += start
   443  			symbol = arg[period+1:]
   444  		}
   445  		// Have we identified a package already?
   446  		pkg, err := build.Import(arg[0:period], wd, build.ImportComment)
   447  		if err == nil {
   448  			return pkg, arg[0:period], symbol, false
   449  		}
   450  		// See if we have the basename or tail of a package, as in json for encoding/json
   451  		// or ivy/value for robpike.io/ivy/value.
   452  		pkgName := arg[:period]
   453  		for {
   454  			path, ok := findNextPackage(pkgName)
   455  			if !ok {
   456  				break
   457  			}
   458  			if pkg, err = build.ImportDir(path, build.ImportComment); err == nil {
   459  				return pkg, arg[0:period], symbol, true
   460  			}
   461  		}
   462  		dirs.Reset() // Next iteration of for loop must scan all the directories again.
   463  	}
   464  	// If it has a slash, we've failed.
   465  	if slash >= 0 {
   466  		// build.Import should always include the path in its error message,
   467  		// and we should avoid repeating it. Unfortunately, build.Import doesn't
   468  		// return a structured error. That can't easily be fixed, since it
   469  		// invokes 'go list' and returns the error text from the loaded package.
   470  		// TODO(golang.org/issue/34750): load using golang.org/x/tools/go/packages
   471  		// instead of go/build.
   472  		importErrStr := importErr.Error()
   473  		if strings.Contains(importErrStr, arg[:period]) {
   474  			log.Fatal(importErrStr)
   475  		} else {
   476  			log.Fatalf("no such package %s: %s", arg[:period], importErrStr)
   477  		}
   478  	}
   479  	// Guess it's a symbol in the current directory.
   480  	return importDir(wd), "", arg, false
   481  }
   482  
   483  // dotPaths lists all the dotted paths legal on Unix-like and
   484  // Windows-like file systems. We check them all, as the chance
   485  // of error is minute and even on Windows people will use ./
   486  // sometimes.
   487  var dotPaths = []string{
   488  	`./`,
   489  	`../`,
   490  	`.\`,
   491  	`..\`,
   492  }
   493  
   494  // isDotSlash reports whether the path begins with a reference
   495  // to the local . or .. directory.
   496  func isDotSlash(arg string) bool {
   497  	if arg == "." || arg == ".." {
   498  		return true
   499  	}
   500  	for _, dotPath := range dotPaths {
   501  		if strings.HasPrefix(arg, dotPath) {
   502  			return true
   503  		}
   504  	}
   505  	return false
   506  }
   507  
   508  // importDir is just an error-catching wrapper for build.ImportDir.
   509  func importDir(dir string) *build.Package {
   510  	pkg, err := build.ImportDir(dir, build.ImportComment)
   511  	if err != nil {
   512  		log.Fatal(err)
   513  	}
   514  	return pkg
   515  }
   516  
   517  // parseSymbol breaks str apart into a symbol and method.
   518  // Both may be missing or the method may be missing.
   519  // If present, each must be a valid Go identifier.
   520  func parseSymbol(flagSet *flag.FlagSet, str string) (symbol, method string) {
   521  	if str == "" {
   522  		return
   523  	}
   524  	elem := strings.Split(str, ".")
   525  	switch len(elem) {
   526  	case 1:
   527  	case 2:
   528  		method = elem[1]
   529  	default:
   530  		log.Printf("too many periods in symbol specification")
   531  		usage(flagSet)
   532  	}
   533  	symbol = elem[0]
   534  	return
   535  }
   536  
   537  // isExported reports whether the name is an exported identifier.
   538  // If the unexported flag (-u) is true, isExported returns true because
   539  // it means that we treat the name as if it is exported.
   540  func isExported(name string) bool {
   541  	return unexported || token.IsExported(name)
   542  }
   543  
   544  // findNextPackage returns the next full file name path that matches the
   545  // (perhaps partial) package path pkg. The boolean reports if any match was found.
   546  func findNextPackage(pkg string) (string, bool) {
   547  	if filepath.IsAbs(pkg) {
   548  		if dirs.offset == 0 {
   549  			dirs.offset = -1
   550  			return pkg, true
   551  		}
   552  		return "", false
   553  	}
   554  	if pkg == "" || token.IsExported(pkg) { // Upper case symbol cannot be a package name.
   555  		return "", false
   556  	}
   557  	pkg = path.Clean(pkg)
   558  	pkgSuffix := "/" + pkg
   559  	for {
   560  		d, ok := dirs.Next()
   561  		if !ok {
   562  			return "", false
   563  		}
   564  		if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) {
   565  			return d.dir, true
   566  		}
   567  	}
   568  }
   569  
   570  var buildCtx = build.Default
   571  
   572  // splitGopath splits $GOPATH into a list of roots.
   573  func splitGopath() []string {
   574  	return filepath.SplitList(buildCtx.GOPATH)
   575  }
   576  

View as plain text