1
2
3
4
5 package godoc
6
7 import (
8 "bytes"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "go/ast"
13 "go/build"
14 "go/doc"
15 "go/token"
16 htmlpkg "html"
17 htmltemplate "html/template"
18 "io"
19 "io/ioutil"
20 "log"
21 "net/http"
22 "os"
23 pathpkg "path"
24 "path/filepath"
25 "sort"
26 "strings"
27 "text/template"
28 "time"
29
30 "golang.org/x/tools/godoc/analysis"
31 "golang.org/x/tools/godoc/util"
32 "golang.org/x/tools/godoc/vfs"
33 "golang.org/x/tools/internal/typeparams"
34 )
35
36
37
38 type handlerServer struct {
39 p *Presentation
40 c *Corpus
41 pattern string
42 stripPrefix string
43 fsRoot string
44 exclude []string
45 }
46
47 func (s *handlerServer) registerWithMux(mux *http.ServeMux) {
48 mux.Handle(s.pattern, s)
49 }
50
51
52
53
54
55
56
57
58 func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode, goos, goarch string) *PageInfo {
59 info := &PageInfo{Dirname: abspath, Mode: mode}
60
61
62
63
64
65
66
67 ctxt := build.Default
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()
72 }
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))
76 for _, i := range f {
77 if mode&NoFiltering != 0 || i.Name() != "internal" {
78 filtered = append(filtered, i)
79 }
80 }
81 return filtered, err
82 }
83 ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) {
84 data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name))
85 if err != nil {
86 return nil, err
87 }
88 return ioutil.NopCloser(bytes.NewReader(data)), nil
89 }
90
91
92
93
94
95
96 if goos == "" && goarch == "" && relpath == "syscall/js" {
97 goos, goarch = "js", "wasm"
98 }
99 if goos != "" {
100 ctxt.GOOS = goos
101 }
102 if goarch != "" {
103 ctxt.GOARCH = goarch
104 }
105
106 pkginfo, err := ctxt.ImportDir(abspath, 0)
107
108 if _, nogo := err.(*build.NoGoError); err != nil && !nogo {
109 info.Err = err
110 return info
111 }
112
113
114 pkgname := pkginfo.Name
115 pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
116 if len(pkgfiles) == 0 {
117
118
119
120
121
122 pkgname = "main"
123 pkgfiles = pkginfo.IgnoredGoFiles
124 }
125
126
127 if len(pkgfiles) > 0 {
128
129 fset := token.NewFileSet()
130 files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles)
131 if err != nil {
132 info.Err = err
133 return info
134 }
135
136
137 pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil)
138
139
140 info.FSet = fset
141 if mode&ShowSource == 0 {
142
143 var m doc.Mode
144 if mode&NoFiltering != 0 {
145 m |= doc.AllDecls
146 }
147 if mode&AllMethods != 0 {
148 m |= doc.AllMethods
149 }
150 info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m)
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...)
156 t.Consts = nil
157 t.Vars = nil
158 t.Funcs = nil
159 }
160
161
162 sort.Sort(funcsByName(info.PDoc.Funcs))
163 }
164
165
166 testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...)
167 files, err = h.c.parseFiles(fset, relpath, abspath, testfiles)
168 if err != nil {
169 log.Println("parsing examples:", err)
170 }
171 info.Examples = collectExamples(h.c, pkg, files)
172
173
174 if info.PDoc.Notes != nil {
175
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)
181 }
182 info.Notes[m] = n
183 }
184 }
185 }
186 }
187
188 } else {
189
190
191
192 if mode&NoFiltering == 0 {
193 packageExports(fset, pkg)
194 }
195 info.PAst = files
196 }
197 info.IsMain = pkgname == "main"
198 }
199
200
201 var dir *Directory
202 var timestamp time.Time
203 if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil {
204
205
206
207 dir = tree.(*Directory).lookup(abspath)
208 timestamp = ts
209 }
210 if dir == nil {
211
212
213
214
215
216
217 dir = h.c.newDirectory(abspath, 2)
218 timestamp = time.Now()
219 }
220 info.Dirs = dir.listing(true, func(path string) bool { return h.includePath(path, mode) })
221
222 info.DirTime = timestamp
223 info.DirFlat = mode&FlatDir != 0
224
225 return info
226 }
227
228 func (h *handlerServer) includePath(path string, mode PageInfoMode) (r bool) {
229
230 for _, e := range h.exclude {
231 if strings.HasPrefix(path, e) {
232 return false
233 }
234 }
235
236
237 if mode&NoFiltering != 0 {
238 return true
239 }
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" {
243 return false
244 }
245 }
246 }
247 return true
248 }
249
250 type funcsByName []*doc.Func
251
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 }
255
256 func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
257 if redirect(w, r) {
258 return
259 }
260
261 relpath := pathpkg.Clean(r.URL.Path[len(h.stripPrefix)+1:])
262
263 if !h.corpusInitialized() {
264 h.p.ServeError(w, r, relpath, errors.New("Scan is not yet complete. Please retry after a few moments"))
265 return
266 }
267
268 abspath := pathpkg.Join(h.fsRoot, relpath)
269 mode := h.p.GetPageInfoMode(r)
270 if relpath == builtinPkgPath {
271
272
273
274 mode |= NoFiltering | NoTypeAssoc
275 }
276 info := h.GetPageInfo(abspath, relpath, mode, r.FormValue("GOOS"), r.FormValue("GOARCH"))
277 if info.Err != nil {
278 log.Print(info.Err)
279 h.p.ServeError(w, r, relpath, info.Err)
280 return
281 }
282
283 var tabtitle, title, subtitle string
284 switch {
285 case info.PAst != nil:
286 for _, ast := range info.PAst {
287 tabtitle = ast.Name.Name
288 break
289 }
290 case info.PDoc != nil:
291 tabtitle = info.PDoc.Name
292 default:
293 tabtitle = info.Dirname
294 title = "Directory "
295 if h.p.ShowTimestamps {
296 subtitle = "Last update: " + info.DirTime.String()
297 }
298 }
299 if title == "" {
300 if info.IsMain {
301
302 _, tabtitle = pathpkg.Split(relpath)
303 title = "Command "
304 } else {
305 title = "Package "
306 }
307 }
308 title += tabtitle
309
310
311 switch tabtitle {
312 case "/src":
313 title = "Packages"
314 tabtitle = "Packages"
315 case "/src/cmd":
316 title = "Commands"
317 tabtitle = "Commands"
318 }
319
320
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
329 }
330
331 var body []byte
332 if info.Dirname == "/src" {
333 body = applyTemplate(h.p.PackageRootHTML, "packageRootHTML", info)
334 } else {
335 body = applyTemplate(h.p.PackageHTML, "packageHTML", info)
336 }
337 h.p.ServePage(w, Page{
338 Title: title,
339 Tabtitle: tabtitle,
340 Subtitle: subtitle,
341 Body: body,
342 TreeView: hasTreeView,
343 })
344 }
345
346 func (h *handlerServer) corpusInitialized() bool {
347 h.c.initMu.RLock()
348 defer h.c.initMu.RUnlock()
349 return h.c.initDone
350 }
351
352 type PageInfoMode uint
353
354 const (
355 PageInfoModeQueryString = "m"
356
357 NoFiltering PageInfoMode = 1 << iota
358 AllMethods
359 ShowSource
360 FlatDir
361 NoTypeAssoc
362 )
363
364
365 var modeNames = map[string]PageInfoMode{
366 "all": NoFiltering,
367 "methods": AllMethods,
368 "src": ShowSource,
369 "flat": FlatDir,
370 }
371
372
373 func modeQueryString(mode PageInfoMode) string {
374 if modeNames := mode.names(); len(modeNames) > 0 {
375 return "?m=" + strings.Join(modeNames, ",")
376 }
377 return ""
378 }
379
380
381 func (m PageInfoMode) names() []string {
382 var names []string
383 for name, mode := range modeNames {
384 if m&mode != 0 {
385 names = append(names, name)
386 }
387 }
388 sort.Strings(names)
389 return names
390 }
391
392
393
394
395 func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode {
396 var mode PageInfoMode
397 for _, k := range strings.Split(r.FormValue(PageInfoModeQueryString), ",") {
398 if m, found := modeNames[strings.TrimSpace(k)]; found {
399 mode |= m
400 }
401 }
402 if p.AdjustPageInfoMode != nil {
403 mode = p.AdjustPageInfoMode(r, mode)
404 }
405 return mode
406 }
407
408
409
410
411
412
413 func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
414 pkg := imports[path]
415 if pkg == nil {
416
417 pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
418 pkg.Data = ast.NewScope(nil)
419 imports[path] = pkg
420 }
421 return pkg, nil
422 }
423
424
425
426 func globalNames(pkg *ast.Package) map[string]bool {
427 names := make(map[string]bool)
428 for _, file := range pkg.Files {
429 for _, decl := range file.Decls {
430 addNames(names, decl)
431 }
432 }
433 return names
434 }
435
436
437 func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example {
438 var files []*ast.File
439 for _, f := range testfiles {
440 files = append(files, f)
441 }
442
443 var examples []*doc.Example
444 globals := globalNames(pkg)
445 for _, e := range doc.Examples(files...) {
446 name := stripExampleSuffix(e.Name)
447 if name == "" || globals[name] {
448 examples = append(examples, e)
449 } else if c.Verbose {
450 log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name)
451 }
452 }
453
454 return examples
455 }
456
457
458
459 func addNames(names map[string]bool, decl ast.Decl) {
460 switch d := decl.(type) {
461 case *ast.FuncDecl:
462 name := d.Name.Name
463 if d.Recv != nil {
464 r := d.Recv.List[0].Type
465 if rr, isstar := r.(*ast.StarExpr); isstar {
466 r = rr.X
467 }
468
469 var typeName string
470 switch x := r.(type) {
471 case *ast.Ident:
472 typeName = x.Name
473 case *ast.IndexExpr:
474 typeName = x.X.(*ast.Ident).Name
475 case *typeparams.IndexListExpr:
476 typeName = x.X.(*ast.Ident).Name
477 }
478 name = typeName + "_" + name
479 }
480 names[name] = true
481 case *ast.GenDecl:
482 for _, spec := range d.Specs {
483 switch s := spec.(type) {
484 case *ast.TypeSpec:
485 names[s.Name.Name] = true
486 case *ast.ValueSpec:
487 for _, id := range s.Names {
488 names[id.Name] = true
489 }
490 }
491 }
492 }
493 }
494
495
496
497
498
499 func packageExports(fset *token.FileSet, pkg *ast.Package) {
500 for _, src := range pkg.Files {
501 cmap := ast.NewCommentMap(fset, src, src.Comments)
502 ast.FileExports(src)
503 src.Comments = cmap.Filter(src).Comments()
504 }
505 }
506
507 func applyTemplate(t *template.Template, name string, data interface{}) []byte {
508 var buf bytes.Buffer
509 if err := t.Execute(&buf, data); err != nil {
510 log.Printf("%s.Execute: %s", name, err)
511 }
512 return buf.Bytes()
513 }
514
515 type writerCapturesErr struct {
516 w io.Writer
517 err error
518 }
519
520 func (w *writerCapturesErr) Write(p []byte) (int, error) {
521 n, err := w.w.Write(p)
522 if err != nil {
523 w.err = err
524 }
525 return n, err
526 }
527
528
529
530
531
532
533
534 func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
535 w := &writerCapturesErr{w: rw}
536 err := t.Execute(w, data)
537
538
539 if w.err == nil && err != nil {
540
541 log.Printf("%s.Execute: %s", t.Name(), err)
542 }
543 }
544
545 func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
546 canonical := pathpkg.Clean(r.URL.Path)
547 if !strings.HasSuffix(canonical, "/") {
548 canonical += "/"
549 }
550 if r.URL.Path != canonical {
551 url := *r.URL
552 url.Path = canonical
553 http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
554 redirected = true
555 }
556 return
557 }
558
559 func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
560 c := pathpkg.Clean(r.URL.Path)
561 c = strings.TrimRight(c, "/")
562 if r.URL.Path != c {
563 url := *r.URL
564 url.Path = c
565 http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
566 redirected = true
567 }
568 return
569 }
570
571 func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) {
572 src, err := vfs.ReadFile(p.Corpus.fs, abspath)
573 if err != nil {
574 log.Printf("ReadFile: %s", err)
575 p.ServeError(w, r, relpath, err)
576 return
577 }
578
579 if r.FormValue(PageInfoModeQueryString) == "text" {
580 p.ServeText(w, src)
581 return
582 }
583
584 h := r.FormValue("h")
585 s := RangeSelection(r.FormValue("s"))
586
587 var buf bytes.Buffer
588 if pathpkg.Ext(abspath) == ".go" {
589
590 fi := p.Corpus.Analysis.FileInfo(abspath)
591 buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
592 buf.Write(marshalJSON(fi.Data))
593 buf.WriteString(";</script>\n")
594
595 if status := p.Corpus.Analysis.Status(); status != "" {
596 buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a> ")
597
598 fmt.Fprintf(&buf, "<span style='color: grey'>[%s]</span><br/>", htmlpkg.EscapeString(status))
599 }
600
601 buf.WriteString("<pre>")
602 formatGoSource(&buf, src, fi.Links, h, s)
603 buf.WriteString("</pre>")
604 } else {
605 buf.WriteString("<pre>")
606 FormatText(&buf, src, 1, false, h, s)
607 buf.WriteString("</pre>")
608 }
609 fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
610
611 p.ServePage(w, Page{
612 Title: title,
613 SrcPath: relpath,
614 Tabtitle: relpath,
615 Body: buf.Bytes(),
616 })
617 }
618
619
620
621 func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
622
623 saved, buf := buf, new(bytes.Buffer)
624
625 var i int
626 var link analysis.Link
627 segmentIter := func() (seg Segment) {
628 if i < len(links) {
629 link = links[i]
630 i++
631 seg = Segment{link.Start(), link.End()}
632 }
633 return
634 }
635 linkWriter := func(w io.Writer, offs int, start bool) {
636 link.Write(w, offs, start)
637 }
638
639 comments := tokenSelection(text, token.COMMENT)
640 var highlights Selection
641 if pattern != "" {
642 highlights = regexpSelection(text, pattern)
643 }
644
645 FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
646
647
648
649
650
651 n := 1
652 for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) {
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670 fmt.Fprintf(saved, `<span id="L%d" class="ln">%6d </span>`, n, n)
671 n++
672 saved.Write(line)
673 saved.WriteByte('\n')
674 }
675 }
676
677 func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
678 if redirect(w, r) {
679 return
680 }
681
682 list, err := p.Corpus.fs.ReadDir(abspath)
683 if err != nil {
684 p.ServeError(w, r, relpath, err)
685 return
686 }
687
688 p.ServePage(w, Page{
689 Title: "Directory",
690 SrcPath: relpath,
691 Tabtitle: relpath,
692 Body: applyTemplate(p.DirlistHTML, "dirlistHTML", list),
693 })
694 }
695
696 func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
697
698 isMarkdown := false
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 {
702 src = md
703 isMarkdown = true
704 err = nil
705 }
706 }
707 if err != nil {
708 log.Printf("ReadFile: %s", err)
709 p.ServeError(w, r, relpath, err)
710 return
711 }
712
713
714
715 if bytes.HasPrefix(src, doctype) {
716 w.Write(src)
717 return
718 }
719
720
721 meta, src, err := extractMetadata(src)
722 if err != nil {
723 log.Printf("decoding metadata %s: %v", relpath, err)
724 }
725
726 page := Page{
727 Title: meta.Title,
728 Subtitle: meta.Subtitle,
729 }
730
731
732 if meta.Template {
733 tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src))
734 if err != nil {
735 log.Printf("parsing template %s: %v", relpath, err)
736 p.ServeError(w, r, relpath, err)
737 return
738 }
739 var buf bytes.Buffer
740 if err := tmpl.Execute(&buf, page); err != nil {
741 log.Printf("executing template %s: %v", relpath, err)
742 p.ServeError(w, r, relpath, err)
743 return
744 }
745 src = buf.Bytes()
746 }
747
748
749
750 if isMarkdown {
751 html, err := renderMarkdown(src)
752 if err != nil {
753 log.Printf("executing markdown %s: %v", relpath, err)
754 p.ServeError(w, r, relpath, err)
755 return
756 }
757 src = html
758 }
759
760
761 if strings.HasSuffix(abspath, "go_spec.html") {
762 var buf bytes.Buffer
763 Linkify(&buf, src)
764 src = buf.Bytes()
765 }
766
767 page.Body = src
768 p.ServePage(w, page)
769 }
770
771 func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) {
772 p.serveFile(w, r)
773 }
774
775 func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) {
776 if strings.HasSuffix(r.URL.Path, "/index.html") {
777
778
779 http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
780 return
781 }
782
783
784 relpath := r.URL.Path
785 if m := p.Corpus.MetadataFor(relpath); m != nil {
786 if m.Path != relpath {
787
788 http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
789 return
790 }
791
792 relpath = m.filePath
793 }
794
795 abspath := relpath
796 relpath = relpath[1:]
797
798 switch pathpkg.Ext(relpath) {
799 case ".html":
800 p.ServeHTMLDoc(w, r, abspath, relpath)
801 return
802
803 case ".go":
804 p.serveTextFile(w, r, abspath, relpath, "Source file")
805 return
806 }
807
808 dir, err := p.Corpus.fs.Lstat(abspath)
809 if err != nil {
810 log.Print(err)
811 p.ServeError(w, r, relpath, err)
812 return
813 }
814
815 if dir != nil && dir.IsDir() {
816 if redirect(w, r) {
817 return
818 }
819 index := pathpkg.Join(abspath, "index.html")
820 if util.IsTextFile(p.Corpus.fs, index) || util.IsTextFile(p.Corpus.fs, pathpkg.Join(abspath, "index.md")) {
821 p.ServeHTMLDoc(w, r, index, index)
822 return
823 }
824 p.serveDirectory(w, r, abspath, relpath)
825 return
826 }
827
828 if util.IsTextFile(p.Corpus.fs, abspath) {
829 if redirectFile(w, r) {
830 return
831 }
832 p.serveTextFile(w, r, abspath, relpath, "Text file")
833 return
834 }
835
836 p.fileServer.ServeHTTP(w, r)
837 }
838
839 func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
840 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
841 w.Write(text)
842 }
843
844 func marshalJSON(x interface{}) []byte {
845 var data []byte
846 var err error
847 const indentJSON = false
848 if indentJSON {
849 data, err = json.MarshalIndent(x, "", " ")
850 } else {
851 data, err = json.Marshal(x)
852 }
853 if err != nil {
854 panic(fmt.Sprintf("json.Marshal failed: %s", err))
855 }
856 return data
857 }
858
View as plain text