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