Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 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 | |
| 5 | package godoc |
| 6 | |
| 7 | import ( |
| 8 | "bytes" |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 9 | "encoding/json" |
Brad Garcia | efd232e | 2014-01-29 10:53:45 -0500 | [diff] [blame] | 10 | "expvar" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 11 | "fmt" |
| 12 | "go/ast" |
| 13 | "go/build" |
| 14 | "go/doc" |
| 15 | "go/token" |
| 16 | htmlpkg "html" |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 17 | htmltemplate "html/template" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 18 | "io" |
| 19 | "io/ioutil" |
| 20 | "log" |
| 21 | "net/http" |
| 22 | "os" |
| 23 | pathpkg "path" |
| 24 | "path/filepath" |
Andrew Gerrand | 2e6fbd8 | 2013-08-28 09:39:02 +1000 | [diff] [blame] | 25 | "sort" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 26 | "strings" |
| 27 | "text/template" |
| 28 | "time" |
| 29 | |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 30 | "code.google.com/p/go.tools/godoc/analysis" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 31 | "code.google.com/p/go.tools/godoc/util" |
| 32 | "code.google.com/p/go.tools/godoc/vfs" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 33 | ) |
| 34 | |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 35 | // handlerServer is a migration from an old godoc http Handler type. |
| 36 | // This should probably merge into something else. |
| 37 | type handlerServer struct { |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 38 | p *Presentation |
| 39 | c *Corpus // copy of p.Corpus |
| 40 | pattern string // url pattern; e.g. "/pkg/" |
| 41 | fsRoot string // file system root to which the pattern is mapped |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 42 | } |
| 43 | |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 44 | func (s *handlerServer) registerWithMux(mux *http.ServeMux) { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 45 | mux.Handle(s.pattern, s) |
| 46 | } |
| 47 | |
| 48 | // getPageInfo returns the PageInfo for a package directory abspath. If the |
| 49 | // parameter genAST is set, an AST containing only the package exports is |
| 50 | // computed (PageInfo.PAst), otherwise package documentation (PageInfo.Doc) |
| 51 | // is extracted from the AST. If there is no corresponding package in the |
| 52 | // directory, PageInfo.PAst and PageInfo.PDoc are nil. If there are no sub- |
| 53 | // directories, PageInfo.Dirs is nil. If an error occurred, PageInfo.Err is |
| 54 | // set to the respective error but the error is not logged. |
| 55 | // |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 56 | func (h *handlerServer) GetPageInfo(abspath, relpath string, mode PageInfoMode) *PageInfo { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 57 | info := &PageInfo{Dirname: abspath} |
| 58 | |
| 59 | // Restrict to the package files that would be used when building |
| 60 | // the package on this system. This makes sure that if there are |
| 61 | // separate implementations for, say, Windows vs Unix, we don't |
| 62 | // jumble them all together. |
| 63 | // Note: Uses current binary's GOOS/GOARCH. |
| 64 | // To use different pair, such as if we allowed the user to choose, |
| 65 | // set ctxt.GOOS and ctxt.GOARCH before calling ctxt.ImportDir. |
| 66 | ctxt := build.Default |
| 67 | ctxt.IsAbsPath = pathpkg.IsAbs |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 68 | ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) { |
| 69 | return h.c.fs.ReadDir(filepath.ToSlash(dir)) |
| 70 | } |
| 71 | ctxt.OpenFile = func(name string) (r io.ReadCloser, err error) { |
| 72 | data, err := vfs.ReadFile(h.c.fs, filepath.ToSlash(name)) |
| 73 | if err != nil { |
| 74 | return nil, err |
| 75 | } |
| 76 | return ioutil.NopCloser(bytes.NewReader(data)), nil |
| 77 | } |
| 78 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 79 | pkginfo, err := ctxt.ImportDir(abspath, 0) |
| 80 | // continue if there are no Go source files; we still want the directory info |
| 81 | if _, nogo := err.(*build.NoGoError); err != nil && !nogo { |
| 82 | info.Err = err |
| 83 | return info |
| 84 | } |
| 85 | |
| 86 | // collect package files |
| 87 | pkgname := pkginfo.Name |
| 88 | pkgfiles := append(pkginfo.GoFiles, pkginfo.CgoFiles...) |
| 89 | if len(pkgfiles) == 0 { |
| 90 | // Commands written in C have no .go files in the build. |
| 91 | // Instead, documentation may be found in an ignored file. |
| 92 | // The file may be ignored via an explicit +build ignore |
| 93 | // constraint (recommended), or by defining the package |
| 94 | // documentation (historic). |
| 95 | pkgname = "main" // assume package main since pkginfo.Name == "" |
| 96 | pkgfiles = pkginfo.IgnoredGoFiles |
| 97 | } |
| 98 | |
| 99 | // get package information, if any |
| 100 | if len(pkgfiles) > 0 { |
| 101 | // build package AST |
| 102 | fset := token.NewFileSet() |
Brad Garcia | 2ca2bfc | 2014-01-30 06:28:19 -0500 | [diff] [blame] | 103 | files, err := h.c.parseFiles(fset, relpath, abspath, pkgfiles) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 104 | if err != nil { |
| 105 | info.Err = err |
| 106 | return info |
| 107 | } |
| 108 | |
| 109 | // ignore any errors - they are due to unresolved identifiers |
| 110 | pkg, _ := ast.NewPackage(fset, files, poorMansImporter, nil) |
| 111 | |
| 112 | // extract package documentation |
| 113 | info.FSet = fset |
| 114 | if mode&ShowSource == 0 { |
| 115 | // show extracted documentation |
| 116 | var m doc.Mode |
| 117 | if mode&NoFiltering != 0 { |
Andrew Gerrand | 2e6fbd8 | 2013-08-28 09:39:02 +1000 | [diff] [blame] | 118 | m |= doc.AllDecls |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 119 | } |
| 120 | if mode&AllMethods != 0 { |
| 121 | m |= doc.AllMethods |
| 122 | } |
| 123 | info.PDoc = doc.New(pkg, pathpkg.Clean(relpath), m) // no trailing '/' in importpath |
Robert Griesemer | 5351a1c | 2014-02-04 10:26:38 -0800 | [diff] [blame] | 124 | if mode&NoTypeAssoc != 0 { |
Andrew Gerrand | 2e6fbd8 | 2013-08-28 09:39:02 +1000 | [diff] [blame] | 125 | for _, t := range info.PDoc.Types { |
Robert Griesemer | 5351a1c | 2014-02-04 10:26:38 -0800 | [diff] [blame] | 126 | info.PDoc.Consts = append(info.PDoc.Consts, t.Consts...) |
| 127 | info.PDoc.Vars = append(info.PDoc.Vars, t.Vars...) |
Andrew Gerrand | 2e6fbd8 | 2013-08-28 09:39:02 +1000 | [diff] [blame] | 128 | info.PDoc.Funcs = append(info.PDoc.Funcs, t.Funcs...) |
Robert Griesemer | 5351a1c | 2014-02-04 10:26:38 -0800 | [diff] [blame] | 129 | t.Consts = nil |
| 130 | t.Vars = nil |
Andrew Gerrand | 2e6fbd8 | 2013-08-28 09:39:02 +1000 | [diff] [blame] | 131 | t.Funcs = nil |
| 132 | } |
Robert Griesemer | 5351a1c | 2014-02-04 10:26:38 -0800 | [diff] [blame] | 133 | // for now we cannot easily sort consts and vars since |
| 134 | // go/doc.Value doesn't export the order information |
Andrew Gerrand | 2e6fbd8 | 2013-08-28 09:39:02 +1000 | [diff] [blame] | 135 | sort.Sort(funcsByName(info.PDoc.Funcs)) |
| 136 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 137 | |
| 138 | // collect examples |
| 139 | testfiles := append(pkginfo.TestGoFiles, pkginfo.XTestGoFiles...) |
Brad Garcia | 2ca2bfc | 2014-01-30 06:28:19 -0500 | [diff] [blame] | 140 | files, err = h.c.parseFiles(fset, relpath, abspath, testfiles) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 141 | if err != nil { |
| 142 | log.Println("parsing examples:", err) |
| 143 | } |
Brad Fitzpatrick | 88f792c | 2013-11-14 09:01:08 -0800 | [diff] [blame] | 144 | info.Examples = collectExamples(h.c, pkg, files) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 145 | |
| 146 | // collect any notes that we want to show |
| 147 | if info.PDoc.Notes != nil { |
| 148 | // could regexp.Compile only once per godoc, but probably not worth it |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 149 | if rx := h.p.NotesRx; rx != nil { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 150 | for m, n := range info.PDoc.Notes { |
| 151 | if rx.MatchString(m) { |
| 152 | if info.Notes == nil { |
| 153 | info.Notes = make(map[string][]*doc.Note) |
| 154 | } |
| 155 | info.Notes[m] = n |
| 156 | } |
| 157 | } |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | } else { |
| 162 | // show source code |
| 163 | // TODO(gri) Consider eliminating export filtering in this mode, |
| 164 | // or perhaps eliminating the mode altogether. |
| 165 | if mode&NoFiltering == 0 { |
| 166 | packageExports(fset, pkg) |
| 167 | } |
Brad Garcia | 2ca2bfc | 2014-01-30 06:28:19 -0500 | [diff] [blame] | 168 | info.PAst = files |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 169 | } |
| 170 | info.IsMain = pkgname == "main" |
| 171 | } |
| 172 | |
| 173 | // get directory information, if any |
| 174 | var dir *Directory |
| 175 | var timestamp time.Time |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 176 | if tree, ts := h.c.fsTree.Get(); tree != nil && tree.(*Directory) != nil { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 177 | // directory tree is present; lookup respective directory |
| 178 | // (may still fail if the file system was updated and the |
| 179 | // new directory tree has not yet been computed) |
| 180 | dir = tree.(*Directory).lookup(abspath) |
| 181 | timestamp = ts |
| 182 | } |
| 183 | if dir == nil { |
| 184 | // no directory tree present (too early after startup or |
| 185 | // command-line mode); compute one level for this page |
| 186 | // note: cannot use path filter here because in general |
| 187 | // it doesn't contain the FSTree path |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 188 | dir = h.c.newDirectory(abspath, 1) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 189 | timestamp = time.Now() |
| 190 | } |
| 191 | info.Dirs = dir.listing(true) |
| 192 | info.DirTime = timestamp |
| 193 | info.DirFlat = mode&FlatDir != 0 |
| 194 | |
| 195 | return info |
| 196 | } |
| 197 | |
Andrew Gerrand | 2e6fbd8 | 2013-08-28 09:39:02 +1000 | [diff] [blame] | 198 | type funcsByName []*doc.Func |
| 199 | |
| 200 | func (s funcsByName) Len() int { return len(s) } |
| 201 | func (s funcsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
| 202 | func (s funcsByName) Less(i, j int) bool { return s[i].Name < s[j].Name } |
| 203 | |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 204 | func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 205 | if redirect(w, r) { |
| 206 | return |
| 207 | } |
| 208 | |
| 209 | relpath := pathpkg.Clean(r.URL.Path[len(h.pattern):]) |
| 210 | abspath := pathpkg.Join(h.fsRoot, relpath) |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 211 | mode := h.p.GetPageInfoMode(r) |
| 212 | if relpath == builtinPkgPath { |
Robert Griesemer | 5351a1c | 2014-02-04 10:26:38 -0800 | [diff] [blame] | 213 | mode = NoFiltering | NoTypeAssoc |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 214 | } |
| 215 | info := h.GetPageInfo(abspath, relpath, mode) |
| 216 | if info.Err != nil { |
| 217 | log.Print(info.Err) |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 218 | h.p.ServeError(w, r, relpath, info.Err) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 219 | return |
| 220 | } |
| 221 | |
| 222 | if mode&NoHTML != 0 { |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 223 | h.p.ServeText(w, applyTemplate(h.p.PackageText, "packageText", info)) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 224 | return |
| 225 | } |
| 226 | |
| 227 | var tabtitle, title, subtitle string |
| 228 | switch { |
| 229 | case info.PAst != nil: |
Brad Garcia | 2ca2bfc | 2014-01-30 06:28:19 -0500 | [diff] [blame] | 230 | for _, ast := range info.PAst { |
| 231 | tabtitle = ast.Name.Name |
| 232 | break |
| 233 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 234 | case info.PDoc != nil: |
| 235 | tabtitle = info.PDoc.Name |
| 236 | default: |
| 237 | tabtitle = info.Dirname |
| 238 | title = "Directory " |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 239 | if h.p.ShowTimestamps { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 240 | subtitle = "Last update: " + info.DirTime.String() |
| 241 | } |
| 242 | } |
| 243 | if title == "" { |
| 244 | if info.IsMain { |
| 245 | // assume that the directory name is the command name |
| 246 | _, tabtitle = pathpkg.Split(relpath) |
| 247 | title = "Command " |
| 248 | } else { |
| 249 | title = "Package " |
| 250 | } |
| 251 | } |
| 252 | title += tabtitle |
| 253 | |
| 254 | // special cases for top-level package/command directories |
| 255 | switch tabtitle { |
| 256 | case "/src/pkg": |
| 257 | tabtitle = "Packages" |
| 258 | case "/src/cmd": |
| 259 | tabtitle = "Commands" |
| 260 | } |
| 261 | |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 262 | // Emit JSON array for type information. |
| 263 | // TODO(adonovan): issue a "pending..." message if results not ready. |
| 264 | var callGraph []*analysis.PCGNodeJSON |
| 265 | var typeInfos []*analysis.TypeInfoJSON |
| 266 | callGraph, info.CallGraphIndex, typeInfos = h.c.Analysis.PackageInfo(relpath) |
| 267 | info.CallGraph = htmltemplate.JS(marshalJSON(callGraph)) |
| 268 | info.AnalysisData = htmltemplate.JS(marshalJSON(typeInfos)) |
| 269 | info.TypeInfoIndex = make(map[string]int) |
| 270 | for i, ti := range typeInfos { |
| 271 | info.TypeInfoIndex[ti.Name] = i |
| 272 | } |
| 273 | |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 274 | h.p.ServePage(w, Page{ |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 275 | Title: title, |
| 276 | Tabtitle: tabtitle, |
| 277 | Subtitle: subtitle, |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 278 | Body: applyTemplate(h.p.PackageHTML, "packageHTML", info), |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 279 | }) |
| 280 | } |
| 281 | |
| 282 | type PageInfoMode uint |
| 283 | |
| 284 | const ( |
Robert Griesemer | 5351a1c | 2014-02-04 10:26:38 -0800 | [diff] [blame] | 285 | NoFiltering PageInfoMode = 1 << iota // do not filter exports |
| 286 | AllMethods // show all embedded methods |
| 287 | ShowSource // show source code, do not extract documentation |
| 288 | NoHTML // show result in textual form, do not generate HTML |
| 289 | FlatDir // show directory in a flat (non-indented) manner |
| 290 | NoTypeAssoc // don't associate consts, vars, and factory functions with types |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 291 | ) |
| 292 | |
| 293 | // modeNames defines names for each PageInfoMode flag. |
| 294 | var modeNames = map[string]PageInfoMode{ |
| 295 | "all": NoFiltering, |
| 296 | "methods": AllMethods, |
| 297 | "src": ShowSource, |
| 298 | "text": NoHTML, |
| 299 | "flat": FlatDir, |
| 300 | } |
| 301 | |
| 302 | // GetPageInfoMode computes the PageInfoMode flags by analyzing the request |
| 303 | // URL form value "m". It is value is a comma-separated list of mode names |
| 304 | // as defined by modeNames (e.g.: m=src,text). |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 305 | func (p *Presentation) GetPageInfoMode(r *http.Request) PageInfoMode { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 306 | var mode PageInfoMode |
| 307 | for _, k := range strings.Split(r.FormValue("m"), ",") { |
| 308 | if m, found := modeNames[strings.TrimSpace(k)]; found { |
| 309 | mode |= m |
| 310 | } |
| 311 | } |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 312 | if p.AdjustPageInfoMode != nil { |
| 313 | mode = p.AdjustPageInfoMode(r, mode) |
| 314 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 315 | return mode |
| 316 | } |
| 317 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 318 | // poorMansImporter returns a (dummy) package object named |
| 319 | // by the last path component of the provided package path |
| 320 | // (as is the convention for packages). This is sufficient |
| 321 | // to resolve package identifiers without doing an actual |
| 322 | // import. It never returns an error. |
| 323 | // |
| 324 | func poorMansImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { |
| 325 | pkg := imports[path] |
| 326 | if pkg == nil { |
| 327 | // note that strings.LastIndex returns -1 if there is no "/" |
| 328 | pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:]) |
| 329 | pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import |
| 330 | imports[path] = pkg |
| 331 | } |
| 332 | return pkg, nil |
| 333 | } |
| 334 | |
| 335 | // globalNames returns a set of the names declared by all package-level |
| 336 | // declarations. Method names are returned in the form Receiver_Method. |
| 337 | func globalNames(pkg *ast.Package) map[string]bool { |
| 338 | names := make(map[string]bool) |
| 339 | for _, file := range pkg.Files { |
| 340 | for _, decl := range file.Decls { |
| 341 | addNames(names, decl) |
| 342 | } |
| 343 | } |
| 344 | return names |
| 345 | } |
| 346 | |
| 347 | // collectExamples collects examples for pkg from testfiles. |
Brad Fitzpatrick | 88f792c | 2013-11-14 09:01:08 -0800 | [diff] [blame] | 348 | func collectExamples(c *Corpus, pkg *ast.Package, testfiles map[string]*ast.File) []*doc.Example { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 349 | var files []*ast.File |
| 350 | for _, f := range testfiles { |
| 351 | files = append(files, f) |
| 352 | } |
| 353 | |
| 354 | var examples []*doc.Example |
| 355 | globals := globalNames(pkg) |
| 356 | for _, e := range doc.Examples(files...) { |
| 357 | name := stripExampleSuffix(e.Name) |
| 358 | if name == "" || globals[name] { |
| 359 | examples = append(examples, e) |
Brad Fitzpatrick | 88f792c | 2013-11-14 09:01:08 -0800 | [diff] [blame] | 360 | } else if c.Verbose { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 361 | log.Printf("skipping example 'Example%s' because '%s' is not a known function or type", e.Name, e.Name) |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | return examples |
| 366 | } |
| 367 | |
| 368 | // addNames adds the names declared by decl to the names set. |
| 369 | // Method names are added in the form ReceiverTypeName_Method. |
| 370 | func addNames(names map[string]bool, decl ast.Decl) { |
| 371 | switch d := decl.(type) { |
| 372 | case *ast.FuncDecl: |
| 373 | name := d.Name.Name |
| 374 | if d.Recv != nil { |
| 375 | var typeName string |
| 376 | switch r := d.Recv.List[0].Type.(type) { |
| 377 | case *ast.StarExpr: |
| 378 | typeName = r.X.(*ast.Ident).Name |
| 379 | case *ast.Ident: |
| 380 | typeName = r.Name |
| 381 | } |
| 382 | name = typeName + "_" + name |
| 383 | } |
| 384 | names[name] = true |
| 385 | case *ast.GenDecl: |
| 386 | for _, spec := range d.Specs { |
| 387 | switch s := spec.(type) { |
| 388 | case *ast.TypeSpec: |
| 389 | names[s.Name.Name] = true |
| 390 | case *ast.ValueSpec: |
| 391 | for _, id := range s.Names { |
| 392 | names[id.Name] = true |
| 393 | } |
| 394 | } |
| 395 | } |
| 396 | } |
| 397 | } |
| 398 | |
| 399 | // packageExports is a local implementation of ast.PackageExports |
| 400 | // which correctly updates each package file's comment list. |
| 401 | // (The ast.PackageExports signature is frozen, hence the local |
| 402 | // implementation). |
| 403 | // |
| 404 | func packageExports(fset *token.FileSet, pkg *ast.Package) { |
| 405 | for _, src := range pkg.Files { |
| 406 | cmap := ast.NewCommentMap(fset, src, src.Comments) |
| 407 | ast.FileExports(src) |
| 408 | src.Comments = cmap.Filter(src).Comments() |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | func applyTemplate(t *template.Template, name string, data interface{}) []byte { |
| 413 | var buf bytes.Buffer |
| 414 | if err := t.Execute(&buf, data); err != nil { |
| 415 | log.Printf("%s.Execute: %s", name, err) |
| 416 | } |
| 417 | return buf.Bytes() |
| 418 | } |
| 419 | |
Brad Garcia | efd232e | 2014-01-29 10:53:45 -0500 | [diff] [blame] | 420 | type writerCapturesErr struct { |
| 421 | w io.Writer |
| 422 | err error |
| 423 | } |
| 424 | |
| 425 | func (w *writerCapturesErr) Write(p []byte) (int, error) { |
| 426 | n, err := w.w.Write(p) |
| 427 | if err != nil { |
| 428 | w.err = err |
| 429 | } |
| 430 | return n, err |
| 431 | } |
| 432 | |
| 433 | var httpErrors *expvar.Map |
| 434 | |
| 435 | func init() { |
| 436 | httpErrors = expvar.NewMap("httpWriteErrors").Init() |
| 437 | } |
| 438 | |
| 439 | // applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer |
| 440 | // for the call to template.Execute. It uses an io.Writer wrapper to capture |
| 441 | // errors from the underlying http.ResponseWriter. If an error is found, an |
| 442 | // expvar will be incremented. Other template errors will be logged. This is |
| 443 | // done to keep from polluting log files with error messages due to networking |
| 444 | // issues, such as client disconnects and http HEAD protocol violations. |
| 445 | func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) { |
| 446 | w := &writerCapturesErr{w: rw} |
| 447 | err := t.Execute(w, data) |
| 448 | // There are some cases where template.Execute does not return an error when |
| 449 | // rw returns an error, and some where it does. So check w.err first. |
| 450 | if w.err != nil { |
| 451 | // For http errors, increment an expvar. |
| 452 | httpErrors.Add(w.err.Error(), 1) |
| 453 | } else if err != nil { |
| 454 | // Log template errors. |
| 455 | log.Printf("%s.Execute: %s", t.Name(), err) |
| 456 | } |
| 457 | } |
| 458 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 459 | func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) { |
| 460 | canonical := pathpkg.Clean(r.URL.Path) |
| 461 | if !strings.HasSuffix(canonical, "/") { |
| 462 | canonical += "/" |
| 463 | } |
| 464 | if r.URL.Path != canonical { |
| 465 | url := *r.URL |
| 466 | url.Path = canonical |
| 467 | http.Redirect(w, r, url.String(), http.StatusMovedPermanently) |
| 468 | redirected = true |
| 469 | } |
| 470 | return |
| 471 | } |
| 472 | |
| 473 | func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) { |
| 474 | c := pathpkg.Clean(r.URL.Path) |
| 475 | c = strings.TrimRight(c, "/") |
| 476 | if r.URL.Path != c { |
| 477 | url := *r.URL |
| 478 | url.Path = c |
| 479 | http.Redirect(w, r, url.String(), http.StatusMovedPermanently) |
| 480 | redirected = true |
| 481 | } |
| 482 | return |
| 483 | } |
| 484 | |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 485 | func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abspath, relpath, title string) { |
| 486 | src, err := vfs.ReadFile(p.Corpus.fs, abspath) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 487 | if err != nil { |
| 488 | log.Printf("ReadFile: %s", err) |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 489 | p.ServeError(w, r, relpath, err) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 490 | return |
| 491 | } |
| 492 | |
| 493 | if r.FormValue("m") == "text" { |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 494 | p.ServeText(w, src) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 495 | return |
| 496 | } |
| 497 | |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 498 | h := r.FormValue("h") |
| 499 | s := RangeSelection(r.FormValue("s")) |
| 500 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 501 | var buf bytes.Buffer |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 502 | if pathpkg.Ext(abspath) == ".go" { |
| 503 | // Find markup links for this file (e.g. "/src/pkg/fmt/print.go"). |
| 504 | data, links := p.Corpus.Analysis.FileInfo(abspath) |
| 505 | buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ") |
| 506 | buf.Write(marshalJSON(data)) |
| 507 | buf.WriteString(";</script>\n") |
| 508 | |
Alan Donovan | 503140c | 2014-04-16 16:35:08 -0400 | [diff] [blame] | 509 | // TODO(adonovan): indicate whether analysis is |
| 510 | // disabled, pending, completed or failed. |
| 511 | // For now, display help link only if 'completed'. |
| 512 | if links != nil { |
| 513 | buf.WriteString("<a href='/lib/godoc/analysis/help.html'>Static analysis features</a><br/>") |
| 514 | } |
| 515 | |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 516 | buf.WriteString("<pre>") |
| 517 | formatGoSource(&buf, src, links, h, s) |
| 518 | buf.WriteString("</pre>") |
| 519 | } else { |
| 520 | buf.WriteString("<pre>") |
| 521 | FormatText(&buf, src, 1, false, h, s) |
| 522 | buf.WriteString("</pre>") |
| 523 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 524 | fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath)) |
| 525 | |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 526 | p.ServePage(w, Page{ |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 527 | Title: title + " " + relpath, |
| 528 | Tabtitle: relpath, |
| 529 | Body: buf.Bytes(), |
| 530 | }) |
| 531 | } |
| 532 | |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 533 | // formatGoSource HTML-escapes Go source text and writes it to w, |
| 534 | // decorating it with the specified analysis links. |
| 535 | // |
| 536 | func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) { |
Alan Donovan | 7f24a83 | 2014-04-09 15:41:18 -0400 | [diff] [blame] | 537 | // Emit to a temp buffer so that we can add line anchors at the end. |
| 538 | saved, buf := buf, new(bytes.Buffer) |
| 539 | |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 540 | var i int |
| 541 | var link analysis.Link // shared state of the two funcs below |
| 542 | segmentIter := func() (seg Segment) { |
| 543 | if i < len(links) { |
| 544 | link = links[i] |
| 545 | i++ |
| 546 | seg = Segment{link.Start(), link.End()} |
| 547 | } |
| 548 | return |
| 549 | } |
| 550 | linkWriter := func(w io.Writer, offs int, start bool) { |
| 551 | link.Write(w, offs, start) |
| 552 | } |
| 553 | |
| 554 | comments := tokenSelection(text, token.COMMENT) |
| 555 | var highlights Selection |
| 556 | if pattern != "" { |
| 557 | highlights = regexpSelection(text, pattern) |
| 558 | } |
| 559 | |
| 560 | FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection) |
Alan Donovan | 7f24a83 | 2014-04-09 15:41:18 -0400 | [diff] [blame] | 561 | |
| 562 | // Now copy buf to saved, adding line anchors. |
| 563 | |
| 564 | // The lineSelection mechanism can't be composed with our |
| 565 | // linkWriter, so we have to add line spans as another pass. |
| 566 | n := 1 |
| 567 | for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) { |
| 568 | fmt.Fprintf(saved, "<span id=\"L%d\" class=\"ln\">%6d</span>\t", n, n) |
| 569 | n++ |
| 570 | saved.Write(line) |
| 571 | saved.WriteByte('\n') |
| 572 | } |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 573 | } |
| 574 | |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 575 | func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 576 | if redirect(w, r) { |
| 577 | return |
| 578 | } |
| 579 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 580 | list, err := p.Corpus.fs.ReadDir(abspath) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 581 | if err != nil { |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 582 | p.ServeError(w, r, relpath, err) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 583 | return |
| 584 | } |
| 585 | |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 586 | p.ServePage(w, Page{ |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 587 | Title: "Directory " + relpath, |
| 588 | Tabtitle: relpath, |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 589 | Body: applyTemplate(p.DirlistHTML, "dirlistHTML", list), |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 590 | }) |
| 591 | } |
| 592 | |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 593 | func (p *Presentation) ServeHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 594 | // get HTML body contents |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 595 | src, err := vfs.ReadFile(p.Corpus.fs, abspath) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 596 | if err != nil { |
| 597 | log.Printf("ReadFile: %s", err) |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 598 | p.ServeError(w, r, relpath, err) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 599 | return |
| 600 | } |
| 601 | |
| 602 | // if it begins with "<!DOCTYPE " assume it is standalone |
| 603 | // html that doesn't need the template wrapping. |
| 604 | if bytes.HasPrefix(src, doctype) { |
| 605 | w.Write(src) |
| 606 | return |
| 607 | } |
| 608 | |
| 609 | // if it begins with a JSON blob, read in the metadata. |
| 610 | meta, src, err := extractMetadata(src) |
| 611 | if err != nil { |
| 612 | log.Printf("decoding metadata %s: %v", relpath, err) |
| 613 | } |
| 614 | |
| 615 | // evaluate as template if indicated |
| 616 | if meta.Template { |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 617 | tmpl, err := template.New("main").Funcs(p.TemplateFuncs()).Parse(string(src)) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 618 | if err != nil { |
| 619 | log.Printf("parsing template %s: %v", relpath, err) |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 620 | p.ServeError(w, r, relpath, err) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 621 | return |
| 622 | } |
| 623 | var buf bytes.Buffer |
| 624 | if err := tmpl.Execute(&buf, nil); err != nil { |
| 625 | log.Printf("executing template %s: %v", relpath, err) |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 626 | p.ServeError(w, r, relpath, err) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 627 | return |
| 628 | } |
| 629 | src = buf.Bytes() |
| 630 | } |
| 631 | |
| 632 | // if it's the language spec, add tags to EBNF productions |
| 633 | if strings.HasSuffix(abspath, "go_spec.html") { |
| 634 | var buf bytes.Buffer |
| 635 | Linkify(&buf, src) |
| 636 | src = buf.Bytes() |
| 637 | } |
| 638 | |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 639 | p.ServePage(w, Page{ |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 640 | Title: meta.Title, |
| 641 | Subtitle: meta.Subtitle, |
| 642 | Body: src, |
| 643 | }) |
| 644 | } |
| 645 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 646 | func (p *Presentation) ServeFile(w http.ResponseWriter, r *http.Request) { |
| 647 | p.serveFile(w, r) |
| 648 | } |
| 649 | |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 650 | func (p *Presentation) serveFile(w http.ResponseWriter, r *http.Request) { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 651 | relpath := r.URL.Path |
| 652 | |
| 653 | // Check to see if we need to redirect or serve another file. |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 654 | if m := p.Corpus.MetadataFor(relpath); m != nil { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 655 | if m.Path != relpath { |
| 656 | // Redirect to canonical path. |
| 657 | http.Redirect(w, r, m.Path, http.StatusMovedPermanently) |
| 658 | return |
| 659 | } |
| 660 | // Serve from the actual filesystem path. |
| 661 | relpath = m.filePath |
| 662 | } |
| 663 | |
| 664 | abspath := relpath |
| 665 | relpath = relpath[1:] // strip leading slash |
| 666 | |
| 667 | switch pathpkg.Ext(relpath) { |
| 668 | case ".html": |
| 669 | if strings.HasSuffix(relpath, "/index.html") { |
| 670 | // We'll show index.html for the directory. |
| 671 | // Use the dir/ version as canonical instead of dir/index.html. |
| 672 | http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently) |
| 673 | return |
| 674 | } |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 675 | p.ServeHTMLDoc(w, r, abspath, relpath) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 676 | return |
| 677 | |
| 678 | case ".go": |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 679 | p.serveTextFile(w, r, abspath, relpath, "Source file") |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 680 | return |
| 681 | } |
| 682 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 683 | dir, err := p.Corpus.fs.Lstat(abspath) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 684 | if err != nil { |
| 685 | log.Print(err) |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 686 | p.ServeError(w, r, relpath, err) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 687 | return |
| 688 | } |
| 689 | |
| 690 | if dir != nil && dir.IsDir() { |
| 691 | if redirect(w, r) { |
| 692 | return |
| 693 | } |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 694 | if index := pathpkg.Join(abspath, "index.html"); util.IsTextFile(p.Corpus.fs, index) { |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 695 | p.ServeHTMLDoc(w, r, index, index) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 696 | return |
| 697 | } |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 698 | p.serveDirectory(w, r, abspath, relpath) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 699 | return |
| 700 | } |
| 701 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 702 | if util.IsTextFile(p.Corpus.fs, abspath) { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 703 | if redirectFile(w, r) { |
| 704 | return |
| 705 | } |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 706 | p.serveTextFile(w, r, abspath, relpath, "Text file") |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 707 | return |
| 708 | } |
| 709 | |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 710 | p.fileServer.ServeHTTP(w, r) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 711 | } |
| 712 | |
Brad Fitzpatrick | 4fc6323 | 2013-07-18 09:52:45 +1000 | [diff] [blame] | 713 | func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 714 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") |
| 715 | w.Write(text) |
| 716 | } |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 717 | |
| 718 | func marshalJSON(x interface{}) []byte { |
| 719 | var data []byte |
| 720 | var err error |
| 721 | const indentJSON = false // for easier debugging |
| 722 | if indentJSON { |
| 723 | data, err = json.MarshalIndent(x, "", " ") |
| 724 | } else { |
| 725 | data, err = json.Marshal(x) |
| 726 | } |
| 727 | if err != nil { |
| 728 | panic(fmt.Sprintf("json.Marshal failed: %s", err)) |
| 729 | } |
| 730 | return data |
| 731 | } |