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