aboutsummaryrefslogtreecommitdiff
path: root/godoc/cmdline.go
blob: 9502ebbe40ae61504d202656d44b7d3b86622964 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package godoc

import (
	"fmt"
	"go/ast"
	"go/build"
	"io"
	"log"
	"os"
	pathpkg "path"
	"path/filepath"
	"regexp"
	"strings"

	"golang.org/x/tools/godoc/vfs"
)

const (
	target    = "/target"
	cmdPrefix = "cmd/"
	srcPrefix = "src/"
	toolsPath = "golang.org/x/tools/cmd/"
)

// CommandLine returns godoc results to w.
// Note that it may add a /target path to fs.
func CommandLine(w io.Writer, fs vfs.NameSpace, pres *Presentation, args []string) error {
	path := args[0]
	srcMode := pres.SrcMode
	cmdMode := strings.HasPrefix(path, cmdPrefix)
	if strings.HasPrefix(path, srcPrefix) {
		path = strings.TrimPrefix(path, srcPrefix)
		srcMode = true
	}
	var abspath, relpath string
	if cmdMode {
		path = strings.TrimPrefix(path, cmdPrefix)
	} else {
		abspath, relpath = paths(fs, pres, path)
	}

	var mode PageInfoMode
	if relpath == builtinPkgPath {
		// the fake built-in package contains unexported identifiers
		mode = NoFiltering | NoTypeAssoc
	}
	if srcMode {
		// only filter exports if we don't have explicit command-line filter arguments
		if len(args) > 1 {
			mode |= NoFiltering
		}
		mode |= ShowSource
	}

	// First, try as package unless forced as command.
	var info *PageInfo
	if !cmdMode {
		info = pres.GetPkgPageInfo(abspath, relpath, mode)
	}

	// Second, try as command (if the path is not absolute).
	var cinfo *PageInfo
	if !filepath.IsAbs(path) {
		// First try go.tools/cmd.
		abspath = pathpkg.Join(pres.PkgFSRoot(), toolsPath+path)
		cinfo = pres.GetCmdPageInfo(abspath, relpath, mode)
		if cinfo.IsEmpty() {
			// Then try $GOROOT/cmd.
			abspath = pathpkg.Join(pres.CmdFSRoot(), path)
			cinfo = pres.GetCmdPageInfo(abspath, relpath, mode)
		}
	}

	// determine what to use
	if info == nil || info.IsEmpty() {
		if cinfo != nil && !cinfo.IsEmpty() {
			// only cinfo exists - switch to cinfo
			info = cinfo
		}
	} else if cinfo != nil && !cinfo.IsEmpty() {
		// both info and cinfo exist - use cinfo if info
		// contains only subdirectory information
		if info.PAst == nil && info.PDoc == nil {
			info = cinfo
		} else if relpath != target {
			// The above check handles the case where an operating system path
			// is provided (see documentation for paths below).  In that case,
			// relpath is set to "/target" (in anticipation of accessing packages there),
			// and is therefore not expected to match a command.
			fmt.Fprintf(w, "use 'godoc %s%s' for documentation on the %s command \n\n", cmdPrefix, relpath, relpath)
		}
	}

	if info == nil {
		return fmt.Errorf("%s: no such directory or package", args[0])
	}
	if info.Err != nil {
		return info.Err
	}

	if info.PDoc != nil && info.PDoc.ImportPath == target {
		// Replace virtual /target with actual argument from command line.
		info.PDoc.ImportPath = args[0]
	}

	// If we have more than one argument, use the remaining arguments for filtering.
	if len(args) > 1 {
		info.IsFiltered = true
		filterInfo(args[1:], info)
	}

	packageText := pres.PackageText
	if pres.HTMLMode {
		packageText = pres.PackageHTML
	}
	if err := packageText.Execute(w, info); err != nil {
		return err
	}
	return nil
}

// paths determines the paths to use.
//
// If we are passed an operating system path like . or ./foo or /foo/bar or c:\mysrc,
// we need to map that path somewhere in the fs name space so that routines
// like getPageInfo will see it.  We use the arbitrarily-chosen virtual path "/target"
// for this.  That is, if we get passed a directory like the above, we map that
// directory so that getPageInfo sees it as /target.
// Returns the absolute and relative paths.
func paths(fs vfs.NameSpace, pres *Presentation, path string) (string, string) {
	if filepath.IsAbs(path) {
		fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace)
		return target, target
	}
	if build.IsLocalImport(path) {
		cwd, _ := os.Getwd() // ignore errors
		path = filepath.Join(cwd, path)
		fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace)
		return target, target
	}
	if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" {
		fs.Bind(target, vfs.OS(bp.Dir), "/", vfs.BindReplace)
		return target, bp.ImportPath
	}
	return pathpkg.Join(pres.PkgFSRoot(), path), path
}

// filterInfo updates info to include only the nodes that match the given
// filter args.
func filterInfo(args []string, info *PageInfo) {
	rx, err := makeRx(args)
	if err != nil {
		log.Fatalf("illegal regular expression from %v: %v", args, err)
	}

	filter := func(s string) bool { return rx.MatchString(s) }
	switch {
	case info.PAst != nil:
		newPAst := map[string]*ast.File{}
		for name, a := range info.PAst {
			cmap := ast.NewCommentMap(info.FSet, a, a.Comments)
			a.Comments = []*ast.CommentGroup{} // remove all comments.
			ast.FilterFile(a, filter)
			if len(a.Decls) > 0 {
				newPAst[name] = a
			}
			for _, d := range a.Decls {
				// add back the comments associated with d only
				comments := cmap.Filter(d).Comments()
				a.Comments = append(a.Comments, comments...)
			}
		}
		info.PAst = newPAst // add only matching files.
	case info.PDoc != nil:
		info.PDoc.Filter(filter)
	}
}

// Does s look like a regular expression?
func isRegexp(s string) bool {
	return strings.IndexAny(s, ".(|)*+?^$[]") >= 0
}

// Make a regular expression of the form
// names[0]|names[1]|...names[len(names)-1].
// Returns an error if the regular expression is illegal.
func makeRx(names []string) (*regexp.Regexp, error) {
	if len(names) == 0 {
		return nil, fmt.Errorf("no expression provided")
	}
	s := ""
	for i, name := range names {
		if i > 0 {
			s += "|"
		}
		if isRegexp(name) {
			s += name
		} else {
			s += "^" + name + "$" // must match exactly
		}
	}
	return regexp.Compile(s)
}