github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/godoc/cmdline.go (about)

     1  // Copyright 2013 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 godoc
     6  
     7  import (
     8  	"fmt"
     9  	"go/ast"
    10  	"go/build"
    11  	"io"
    12  	"log"
    13  	"os"
    14  	pathpkg "path"
    15  	"path/filepath"
    16  	"regexp"
    17  	"strings"
    18  
    19  	"golang.org/x/tools/godoc/vfs"
    20  )
    21  
    22  const (
    23  	target    = "/target"
    24  	cmdPrefix = "cmd/"
    25  	srcPrefix = "src/"
    26  	toolsPath = "golang.org/x/tools/cmd/"
    27  )
    28  
    29  // CommandLine returns godoc results to w.
    30  // Note that it may add a /target path to fs.
    31  func CommandLine(w io.Writer, fs vfs.NameSpace, pres *Presentation, args []string) error {
    32  	path := args[0]
    33  	srcMode := pres.SrcMode
    34  	cmdMode := strings.HasPrefix(path, cmdPrefix)
    35  	if strings.HasPrefix(path, srcPrefix) {
    36  		path = strings.TrimPrefix(path, srcPrefix)
    37  		srcMode = true
    38  	}
    39  	var abspath, relpath string
    40  	if cmdMode {
    41  		path = strings.TrimPrefix(path, cmdPrefix)
    42  	} else {
    43  		abspath, relpath = paths(fs, pres, path)
    44  	}
    45  
    46  	var mode PageInfoMode
    47  	if relpath == builtinPkgPath {
    48  		// the fake built-in package contains unexported identifiers
    49  		mode = NoFiltering | NoTypeAssoc
    50  	}
    51  	if srcMode {
    52  		// only filter exports if we don't have explicit command-line filter arguments
    53  		if len(args) > 1 {
    54  			mode |= NoFiltering
    55  		}
    56  		mode |= ShowSource
    57  	}
    58  
    59  	// First, try as package unless forced as command.
    60  	var info *PageInfo
    61  	if !cmdMode {
    62  		info = pres.GetPkgPageInfo(abspath, relpath, mode)
    63  	}
    64  
    65  	// Second, try as command (if the path is not absolute).
    66  	var cinfo *PageInfo
    67  	if !filepath.IsAbs(path) {
    68  		// First try go.tools/cmd.
    69  		abspath = pathpkg.Join(pres.PkgFSRoot(), toolsPath+path)
    70  		cinfo = pres.GetCmdPageInfo(abspath, relpath, mode)
    71  		if cinfo.IsEmpty() {
    72  			// Then try $GOROOT/cmd.
    73  			abspath = pathpkg.Join(pres.CmdFSRoot(), path)
    74  			cinfo = pres.GetCmdPageInfo(abspath, relpath, mode)
    75  		}
    76  	}
    77  
    78  	// determine what to use
    79  	if info == nil || info.IsEmpty() {
    80  		if cinfo != nil && !cinfo.IsEmpty() {
    81  			// only cinfo exists - switch to cinfo
    82  			info = cinfo
    83  		}
    84  	} else if cinfo != nil && !cinfo.IsEmpty() {
    85  		// both info and cinfo exist - use cinfo if info
    86  		// contains only subdirectory information
    87  		if info.PAst == nil && info.PDoc == nil {
    88  			info = cinfo
    89  		} else if relpath != target {
    90  			// The above check handles the case where an operating system path
    91  			// is provided (see documentation for paths below).  In that case,
    92  			// relpath is set to "/target" (in anticipation of accessing packages there),
    93  			// and is therefore not expected to match a command.
    94  			fmt.Fprintf(w, "use 'godoc %s%s' for documentation on the %s command \n\n", cmdPrefix, relpath, relpath)
    95  		}
    96  	}
    97  
    98  	if info == nil {
    99  		return fmt.Errorf("%s: no such directory or package", args[0])
   100  	}
   101  	if info.Err != nil {
   102  		return info.Err
   103  	}
   104  
   105  	if info.PDoc != nil && info.PDoc.ImportPath == target {
   106  		// Replace virtual /target with actual argument from command line.
   107  		info.PDoc.ImportPath = args[0]
   108  	}
   109  
   110  	// If we have more than one argument, use the remaining arguments for filtering.
   111  	if len(args) > 1 {
   112  		info.IsFiltered = true
   113  		filterInfo(args[1:], info)
   114  	}
   115  
   116  	packageText := pres.PackageText
   117  	if pres.HTMLMode {
   118  		packageText = pres.PackageHTML
   119  	}
   120  	if err := packageText.Execute(w, info); err != nil {
   121  		return err
   122  	}
   123  	return nil
   124  }
   125  
   126  // paths determines the paths to use.
   127  //
   128  // If we are passed an operating system path like . or ./foo or /foo/bar or c:\mysrc,
   129  // we need to map that path somewhere in the fs name space so that routines
   130  // like getPageInfo will see it.  We use the arbitrarily-chosen virtual path "/target"
   131  // for this.  That is, if we get passed a directory like the above, we map that
   132  // directory so that getPageInfo sees it as /target.
   133  // Returns the absolute and relative paths.
   134  func paths(fs vfs.NameSpace, pres *Presentation, path string) (string, string) {
   135  	if filepath.IsAbs(path) {
   136  		fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace)
   137  		return target, target
   138  	}
   139  	if build.IsLocalImport(path) {
   140  		cwd, _ := os.Getwd() // ignore errors
   141  		path = filepath.Join(cwd, path)
   142  		fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace)
   143  		return target, target
   144  	}
   145  	if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" {
   146  		fs.Bind(target, vfs.OS(bp.Dir), "/", vfs.BindReplace)
   147  		return target, bp.ImportPath
   148  	}
   149  	return pathpkg.Join(pres.PkgFSRoot(), path), path
   150  }
   151  
   152  // filterInfo updates info to include only the nodes that match the given
   153  // filter args.
   154  func filterInfo(args []string, info *PageInfo) {
   155  	rx, err := makeRx(args)
   156  	if err != nil {
   157  		log.Fatalf("illegal regular expression from %v: %v", args, err)
   158  	}
   159  
   160  	filter := func(s string) bool { return rx.MatchString(s) }
   161  	switch {
   162  	case info.PAst != nil:
   163  		newPAst := map[string]*ast.File{}
   164  		for name, a := range info.PAst {
   165  			cmap := ast.NewCommentMap(info.FSet, a, a.Comments)
   166  			a.Comments = []*ast.CommentGroup{} // remove all comments.
   167  			ast.FilterFile(a, filter)
   168  			if len(a.Decls) > 0 {
   169  				newPAst[name] = a
   170  			}
   171  			for _, d := range a.Decls {
   172  				// add back the comments associated with d only
   173  				comments := cmap.Filter(d).Comments()
   174  				a.Comments = append(a.Comments, comments...)
   175  			}
   176  		}
   177  		info.PAst = newPAst // add only matching files.
   178  	case info.PDoc != nil:
   179  		info.PDoc.Filter(filter)
   180  	}
   181  }
   182  
   183  // Does s look like a regular expression?
   184  func isRegexp(s string) bool {
   185  	return strings.IndexAny(s, ".(|)*+?^$[]") >= 0
   186  }
   187  
   188  // Make a regular expression of the form
   189  // names[0]|names[1]|...names[len(names)-1].
   190  // Returns an error if the regular expression is illegal.
   191  func makeRx(names []string) (*regexp.Regexp, error) {
   192  	if len(names) == 0 {
   193  		return nil, fmt.Errorf("no expression provided")
   194  	}
   195  	s := ""
   196  	for i, name := range names {
   197  		if i > 0 {
   198  			s += "|"
   199  		}
   200  		if isRegexp(name) {
   201  			s += name
   202  		} else {
   203  			s += "^" + name + "$" // must match exactly
   204  		}
   205  	}
   206  	return regexp.Compile(s)
   207  }