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.
17 htmltemplate "html/template"
30 "golang.org/x/tools/godoc/analysis"
31 "golang.org/x/tools/godoc/util"
32 "golang.org/x/tools/godoc/vfs"
35 // handlerServer is a migration from an old godoc http Handler type.
36 // This should probably merge into something else.
37 type handlerServer struct {
39 c *Corpus // copy of p.Corpus
40 pattern string // url pattern; e.g. "/pkg/"
41 stripPrefix string // prefix to strip from import path; e.g. "pkg/"
42 fsRoot string // file system root to which the pattern is mapped; e.g. "/src"
43 exclude []string // file system paths to exclude; e.g. "/src/cmd"
46 func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
47 mux.Handle(s.pattern, s)
50 // GetPageInfo returns the PageInfo for a package directory abspath. If the
51 // parameter genAST is set, an AST containing only the package exports is
52 // computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc)
53 // is extracted from the AST. If there is no corresponding package in the
54 // directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub-
55 // directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is
56 // set to the respective error but the error is not logged.
58 func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode, goos, goarch string) *PageInfo {
59 info := &PageInfo{Dirname: abspath, Mode: mode}
61 // Restrict to the package files that would be used when building
62 // the package on this system. This makes sure that if there are
63 // separate implementations for, say, Windows vs Unix, we don't
64 // jumble them all together.
65 // Note: If goos/goarch aren't set, the current binary's GOOS/GOARCH
68 ctxt.IsAbsPath = pathpkg.IsAbs
69 ctxt.IsDir = func(path string) bool {
70 fi, err := h.c.fs.Stat(filepath.ToSlash(path))
71 return err == nil && fi.IsDir()
73 ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
74 f, err := h.c.fs.ReadDir(filepath.ToSlash(dir))
75 filtered := make([]os.FileInfo, 0, len(f))
77 if mode&NoFiltering != 0 || i.Name() != "internal" {
78 filtered = append(filtered, i)
83 ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
84 data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
88 return ioutil.NopCloser(bytes.NewReader(data)), nil
91 // Make the syscall/js package always visible by default.
92 // It defaults to the host's GOOS/GOARCH, and golang.org's
93 // linux/amd64 means the wasm syscall/js package was blank.
94 // And you can't run godoc on js/wasm anyway, so host defaults
95 // don't make sense here.
96 if goos == "" && goarch == "" && relpath == "syscall/js" {
97 goos, goarch = "js", "wasm"
106 pkginfo, err := ctxt.ImportDir(abspath, 0)
107 // continue if there are no Go source files; we still want the directory info
108 if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
113 // collect package files
114 pkgname := pkginfo.Name
115 pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
116 if len(pkgfiles) == 0 {
117 // Commands written in C have no .go files in the build.
118 // Instead, documentation may be found in an ignored file.
119 // The file may be ignored via an explicit +build ignore
120 // constraint (recommended), or by defining the package
121 // documentation (historic).
122 pkgname = "main" // assume package main since pkginfo.Name == ""
123 pkgfiles = pkginfo.IgnoredGoFiles
126 // get package information, if any
127 if len(pkgfiles) > 0 {
129 fset := token.NewFileSet()
130 files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles)
136 // ignore any errors - they are due to unresolved identifiers
137 pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
139 // extract package documentation
141 if mode&ShowSource == 0 {
142 // show extracted documentation
144 if mode&NoFiltering != 0 {
147 if mode&AllMethods != 0 {
150 info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath
151 if mode&NoTypeAssoc != 0 {
152 for _, t := range info.PDoc.Types {
153 info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...)
154 info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...)
155 info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...)
160 // for now we cannot easily sort consts and vars since
161 // go/doc.Value doesn't export the order information
162 sort.Sort(funcsByName(info.PDoc.Funcs))
166 testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
167 files, err = h.c.parseFiles(fset, relpath, abspath, testfiles)
169 log.Println("parsing examples:", err)
171 info.Examples = collectExamples(h.c, pkg, files)
173 // collect any notes that we want to show
174 if info.PDoc.Notes != nil {
175 // could regexp.Compile only once per godoc, but probably not worth it
176 if rx := h.p.NotesRx; rx != nil {
177 for m, n := range info.PDoc.Notes {
178 if rx.MatchString(m) {
179 if info.Notes == nil {
180 info.Notes = make(map[string][]*doc.Note)
190 // TODO(gri) Consider eliminating export filtering in this mode,
191 // or perhaps eliminating the mode altogether.
192 if mode&NoFiltering == 0 {
193 packageExports(fset, pkg)
197 info.IsMain = pkgname == "main"
200 // get directory information, if any
202 var timestamp time.Time
203 if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
204 // directory tree is present; lookup respective directory
205 // (may still fail if the file system was updated and the
206 // new directory tree has not yet been computed)
207 dir = tree.(*Directory).lookup(abspath)
211 // TODO(agnivade): handle this case better, now since there is no CLI mode.
212 // no directory tree present (happens in command-line mode);
213 // compute 2 levels for this page. The second level is to
214 // get the synopses of sub-directories.
215 // note: cannot use path filter here because in general
216 // it doesn't contain the FSTree path
217 dir = h.c.newDirectory(abspath, 2)
218 timestamp = time.Now()
220 info.Dirs = dir.listing(true, func(path string) bool { return h.includePath(path, mode) })
222 info.DirTime = timestamp
223 info.DirFlat = mode&FlatDir != 0
228 func (h *handlerServer) includePath(path string, mode PageInfoMode) (r bool) {
229 // if the path is under one of the exclusion paths, don't list.
230 for _, e := range h.exclude {
231 if strings.HasPrefix(path, e) {
236 // if the path includes 'internal', don't list unless we are in the NoFiltering mode.
237 if mode&NoFiltering != 0 {
240 if strings.Contains(path, "internal") || strings.Contains(path, "vendor") {
241 for _, c := range strings.Split(filepath.Clean(path), string(os.PathSeparator)) {
242 if c == "internal" || c == "vendor" {
250 type funcsByName []*doc.Func
252 func (s funcsByName) Len() int { return len(s) }
253 func (s funcsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
254 func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
256 func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
261 relpath := pathpkg.Clean(r.URL.Path[len(h.stripPrefix)+1:])
263 if !h.corpusInitialized() {
264 h.p.ServeError(w, r, relpath, errors.New("Scan is not yet complete. Please retry after a few moments"))
268 abspath := pathpkg.Join(h.fsRoot, relpath)
269 mode := h.p.GetPageInfoMode(r)
270 if relpath == builtinPkgPath {
271 // The fake built-in package contains unexported identifiers,
272 // but we want to show them. Also, disable type association,
273 // since it's not helpful for this fake package (see issue 6645).
274 mode |= NoFiltering | NoTypeAssoc
276 info := h.GetPageInfo(abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
279 h.p.ServeError(w, r, relpath, info.Err)
283 var tabtitle, title, subtitle string
285 case info.PAst != nil:
286 for _, ast := range info.PAst {
287 tabtitle = ast.Name.Name
290 case info.PDoc != nil:
291 tabtitle = info.PDoc.Name
293 tabtitle = info.Dirname
295 if h.p.ShowTimestamps {
296 subtitle = "Last update: " + info.DirTime.String()
301 // assume that the directory name is the command name
302 _, tabtitle = pathpkg.Split(relpath)
310 // special cases for top-level package/command directories
314 tabtitle = "Packages"
317 tabtitle = "Commands"
320 // Emit JSON array for type information.
321 pi := h.c.Analysis.PackageInfo(relpath)
322 hasTreeView := len(pi.CallGraph) != 0
323 info.CallGraphIndex = pi.CallGraphIndex
324 info.CallGraph = htmltemplate.JS(marshalJSON(pi.CallGraph))
325 info.AnalysisData = htmltemplate.JS(marshalJSON(pi.Types))
326 info.TypeInfoIndex = make(map[string]int)
327 for i, ti := range pi.Types {
328 info.TypeInfoIndex[ti.Name] = i
331 info.GoogleCN = googleCN(r)
333 if info.Dirname == "/src" {
334 body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
336 body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
338 h.p.ServePage(w, Page{
343 GoogleCN: info.GoogleCN,
344 TreeView: hasTreeView,
348 func (h *handlerServer) corpusInitialized() bool {
350 defer h.c.initMu.RUnlock()
354 type PageInfoMode uint
357 PageInfoModeQueryString = "m" // query string where PageInfoMode is stored
359 NoFiltering PageInfoMode = 1 << iota // do not filter exports
360 AllMethods // show all embedded methods
361 ShowSource // show source code, do not extract documentation
362 FlatDir // show directory in a flat (non-indented) manner
363 NoTypeAssoc // don't associate consts, vars, and factory functions with types (not exposed via ?m= query parameter, used for package builtin, see issue 6645)
366 // modeNames defines names for each PageInfoMode flag.
367 var modeNames = map[string]PageInfoMode{
369 "methods": AllMethods,
374 // generate a query string for persisting PageInfoMode between pages.
375 func modeQueryString(mode PageInfoMode) string {
376 if modeNames := mode.names(); len(modeNames) > 0 {
377 return "?m=" + strings.Join(modeNames, ",")
382 // alphabetically sorted names of active flags for a PageInfoMode.
383 func (m PageInfoMode) names() []string {
385 for name, mode := range modeNames {
387 names = append(names, name)
394 // GetPageInfoMode computes the PageInfoMode flags by analyzing the request
395 // URL form value "m". It is value is a comma-separated list of mode names
396 // as defined by modeNames (e.g.: m=src,text).
397 func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
398 var mode PageInfoMode
399 for _, k := range strings.Split(r.FormValue(PageInfoModeQueryString), ",") {
400 if m, found := modeNames[strings.TrimSpace(k)]; found {
404 if p.AdjustPageInfoMode != nil {
405 mode = p.AdjustPageInfoMode(r, mode)
410 // poorMansImporter returns a (dummy) package object named
411 // by the last path component of the provided package path
412 // (as is the convention for packages). This is sufficient
413 // to resolve package identifiers without doing an actual
414 // import. It never returns an error.
416 func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
419 // note that strings.LastIndex returns -1 if there is no "/"
420 pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
421 pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
427 // globalNames returns a set of the names declared by all package-level
428 // declarations. Method names are returned in the form Receiver_Method.
429 func globalNames(pkg *ast.Package) map[string]bool {
430 names := make(map[string]bool)
431 for _, file := range pkg.Files {
432 for _, decl := range file.Decls {
433 addNames(names, decl)
439 // collectExamples collects examples for pkg from testfiles.
440 func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
441 var files []*ast.File
442 for _, f := range testfiles {
443 files = append(files, f)
446 var examples []*doc.Example
447 globals := globalNames(pkg)
448 for _, e := range doc.Examples(files...) {
449 name := stripExampleSuffix(e.Name)
450 if name == "" || globals[name] {
451 examples = append(examples, e)
452 } else if c.Verbose {
453 log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
460 // addNames adds the names declared by decl to the names set.
461 // Method names are added in the form ReceiverTypeName_Method.
462 func addNames(names map[string]bool, decl ast.Decl) {
463 switch d := decl.(type) {
468 switch r := d.Recv.List[0].Type.(type) {
470 typeName = r.X.(*ast.Ident).Name
474 name = typeName + "_" + name
478 for _, spec := range d.Specs {
479 switch s := spec.(type) {
481 names[s.Name.Name] = true
483 for _, id := range s.Names {
484 names[id.Name] = true
491 // packageExports is a local implementation of ast.PackageExports
492 // which correctly updates each package file's comment list.
493 // (The ast.PackageExports signature is frozen, hence the local
496 func packageExports(fset *token.FileSet, pkg *ast.Package) {
497 for _, src := range pkg.Files {
498 cmap := ast.NewCommentMap(fset, src, src.Comments)
500 src.Comments = cmap.Filter(src).Comments()
504 func applyTemplate(t *template.Template, name string, data interface{}) []byte {
506 if err := t.Execute(&buf, data); err != nil {
507 log.Printf("%s.Execute: %s", name, err)
512 type writerCapturesErr struct {
517 func (w *writerCapturesErr) Write(p []byte) (int, error) {
518 n, err := w.w.Write(p)
525 // applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
526 // for the call to template.Execute. It uses an io.Writer wrapper to capture
527 // errors from the underlying http.ResponseWriter. Errors are logged only when
528 // they come from the template processing and not the Writer; this avoid
529 // polluting log files with error messages due to networking issues, such as
530 // client disconnects and http HEAD protocol violations.
531 func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
532 w := &writerCapturesErr{w: rw}
533 err := t.Execute(w, data)
534 // There are some cases where template.Execute does not return an error when
535 // rw returns an error, and some where it does. So check w.err first.
536 if w.err == nil && err != nil {
537 // Log template errors.
538 log.Printf("%s.Execute: %s", t.Name(), err)
542 func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
543 canonical := pathpkg.Clean(r.URL.Path)
544 if !strings.HasSuffix(canonical, "/") {
547 if r.URL.Path != canonical {
550 http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
556 func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
557 c := pathpkg.Clean(r.URL.Path)
558 c = strings.TrimRight(c, "/")
562 http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
568 func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
569 src, err := vfs.ReadFile(p.Corpus.fs, abspath)
571 log.Printf("ReadFile: %s", err)
572 p.ServeError(w, r, relpath, err)
576 if r.FormValue(PageInfoModeQueryString) == "text" {
581 h := r.FormValue("h")
582 s := RangeSelection(r.FormValue("s"))
585 if pathpkg.Ext(abspath) == ".go" {
586 // Find markup links for this file (e.g. "/src/fmt/print.go").
587 fi := p.Corpus.Analysis.FileInfo(abspath)
588 buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
589 buf.Write(marshalJSON(fi.Data))
590 buf.WriteString(";</script>\n")
592 if status := p.Corpus.Analysis.Status(); status != "" {
593 buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ")
594 // TODO(adonovan): show analysis status at per-file granularity.
595 fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status))
598 buf.WriteString("<pre>")
599 formatGoSource(&buf, src, fi.Links, h, s)
600 buf.WriteString("</pre>")
602 buf.WriteString("<pre>")
603 FormatText(&buf, src, 1, false, h, s)
604 buf.WriteString("</pre>")
606 fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
613 GoogleCN: googleCN(r),
617 // formatGoSource HTML-escapes Go source text and writes it to w,
618 // decorating it with the specified analysis links.
620 func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
621 // Emit to a temp buffer so that we can add line anchors at the end.
622 saved, buf := buf, new(bytes.Buffer)
625 var link analysis.Link // shared state of the two funcs below
626 segmentIter := func() (seg Segment) {
630 seg = Segment{link.Start(), link.End()}
634 linkWriter := func(w io.Writer, offs int, start bool) {
635 link.Write(w, offs, start)
638 comments := tokenSelection(text, token.COMMENT)
639 var highlights Selection
641 highlights = regexpSelection(text, pattern)
644 FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
646 // Now copy buf to saved, adding line anchors.
648 // The lineSelection mechanism can't be composed with our
649 // linkWriter, so we have to add line spans as another pass.
651 for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
652 // The line numbers are inserted into the document via a CSS ::before
653 // pseudo-element. This prevents them from being copied when users
654 // highlight and copy text.
655 // ::before is supported in 98% of browsers: https://caniuse.com/#feat=css-gencontent
656 // This is also the trick Github uses to hide line numbers.
658 // The first tab for the code snippet needs to start in column 9, so
659 // it indents a full 8 spaces, hence the two nbsp's. Otherwise the tab
660 // character only indents a short amount.
662 // Due to rounding and font width Firefox might not treat 8 rendered
663 // characters as 8 characters wide, and subsequently may treat the tab
664 // character in the 9th position as moving the width from (7.5 or so) up
666 // https://github.com/webcompat/web-bugs/issues/17530#issuecomment-402675091
667 // for a fuller explanation. The solution is to add a CSS class to
668 // explicitly declare the width to be 8 characters.
669 fmt.Fprintf(saved, `<span id="L%d" class="ln">%6d </span>`, n, n)
672 saved.WriteByte('\n')
676 func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
681 list, err := p.Corpus.fs.ReadDir(abspath)
683 p.ServeError(w, r, relpath, err)
691 Body: applyTemplate(p.DirlistHTML, "dirlistHTML", list),
692 GoogleCN: googleCN(r),
696 func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
697 // get HTML body contents
699 src, err := vfs.ReadFile(p.Corpus.fs, abspath)
700 if err != nil && strings.HasSuffix(abspath, ".html") {
701 if md, errMD := vfs.ReadFile(p.Corpus.fs, strings.TrimSuffix(abspath, ".html")+".md"); errMD == nil {
708 log.Printf("ReadFile: %s", err)
709 p.ServeError(w, r, relpath, err)
713 // if it begins with "<!DOCTYPE " assume it is standalone
714 // html that doesn't need the template wrapping.
715 if bytes.HasPrefix(src, doctype) {
720 // if it begins with a JSON blob, read in the metadata.
721 meta, src, err := extractMetadata(src)
723 log.Printf("decoding metadata %s: %v", relpath, err)
728 Subtitle: meta.Subtitle,
729 GoogleCN: googleCN(r),
732 // evaluate as template if indicated
734 tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
736 log.Printf("parsing template %s: %v", relpath, err)
737 p.ServeError(w, r, relpath, err)
741 if err := tmpl.Execute(&buf, page); err != nil {
742 log.Printf("executing template %s: %v", relpath, err)
743 p.ServeError(w, r, relpath, err)
749 // Apply markdown as indicated.
750 // (Note template applies before Markdown.)
752 html, err := renderMarkdown(src)
754 log.Printf("executing markdown %s: %v", relpath, err)
755 p.ServeError(w, r, relpath, err)
761 // if it's the language spec, add tags to EBNF productions
762 if strings.HasSuffix(abspath, "go_spec.html") {
772 func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
776 func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
777 if strings.HasSuffix(r.URL.Path, "/index.html") {
778 // We'll show index.html for the directory.
779 // Use the dir/ version as canonical instead of dir/index.html.
780 http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
784 // Check to see if we need to redirect or serve another file.
785 relpath := r.URL.Path
786 if m := p.Corpus.MetadataFor(relpath); m != nil {
787 if m.Path != relpath {
788 // Redirect to canonical path.
789 http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
792 // Serve from the actual filesystem path.
797 relpath = relpath[1:] // strip leading slash
799 switch pathpkg.Ext(relpath) {
801 p.ServeHTMLDoc(w, r, abspath, relpath)
805 p.serveTextFile(w, r, abspath, relpath, "Source file")
809 dir, err := p.Corpus.fs.Lstat(abspath)
812 p.ServeError(w, r, relpath, err)
816 if dir != nil && dir.IsDir() {
820 index := pathpkg.Join(abspath, "index.html")
821 if util.IsTextFile(p.Corpus.fs, index) || util.IsTextFile(p.Corpus.fs, pathpkg.Join(abspath, "index.md")) {
822 p.ServeHTMLDoc(w, r, index, index)
825 p.serveDirectory(w, r, abspath, relpath)
829 if util.IsTextFile(p.Corpus.fs, abspath) {
830 if redirectFile(w, r) {
833 p.serveTextFile(w, r, abspath, relpath, "Text file")
837 p.fileServer.ServeHTTP(w, r)
840 func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
841 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
845 func marshalJSON(x interface{}) []byte {
848 const indentJSON = false // for easier debugging
850 data, err = json.MarshalIndent(x, "", " ")
852 data, err = json.Marshal(x)
855 panic(fmt.Sprintf("json.Marshal failed: %s", err))