blob: ca7cae4a483342f00274a9cc18b0e0be3b54714a [file] [log] [blame]
// Copyright 2017 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
//go:generate go run regen.go
package stack
import (
"fmt"
"html/template"
"io"
"log"
"net/url"
"regexp"
"runtime"
"strings"
"time"
)
// ToHTML formats the aggregated buckets as HTML to the writer.
//
// Use footer to add custom HTML at the bottom of the page.
func (a *Aggregated) ToHTML(w io.Writer, footer template.HTML) error {
data := map[string]interface{}{
"Aggregated": a,
"Footer": footer,
"Snapshot": a.Snapshot,
}
return toHTML(w, data)
}
// ToHTML formats the snapshot as HTML to the writer.
//
// Use footer to add custom HTML at the bottom of the page.
func (s *Snapshot) ToHTML(w io.Writer, footer template.HTML) error {
data := map[string]interface{}{
"Footer": footer,
"Snapshot": s,
}
return toHTML(w, data)
}
// Private stuff.
func toHTML(w io.Writer, data map[string]interface{}) error {
m := template.FuncMap{
"funcClass": funcClass,
"minus": minus,
"pkgURL": pkgURL,
"srcURL": srcURL,
"symbol": symbol,
}
data["Favicon"] = favicon
data["GOMAXPROCS"] = runtime.GOMAXPROCS(0)
data["Now"] = time.Now().Truncate(time.Second)
data["Version"] = runtime.Version()
t, err := template.New("t").Funcs(m).Parse(indexHTML)
if err != nil {
return err
}
return t.Execute(w, data)
}
var reMethodSymbol = regexp.MustCompile(`^\(\*?([^)]+)\)(\..+)$`)
func funcClass(c *Call) template.HTML {
if c.Func.IsPkgMain {
return "FuncMain Exported"
}
s := c.Location.String()
if c.Func.IsExported {
s += " Exported"
}
/* #nosec G203 */
return template.HTML("Func") + template.HTML(template.HTMLEscapeString(s))
}
func minus(i, j int) int {
return i - j
}
// pkgURL returns a link to the godoc for the call.
func pkgURL(c *Call) template.URL {
imp := c.ImportPath
// Check for vendored code first.
if i := strings.Index(imp, "/vendor/"); i != -1 {
imp = imp[i+8:]
}
ip := escape(imp)
if ip == "" {
return ""
}
url := template.URL("")
if c.Location == Stdlib {
// This always links to the latest release, past releases are not online.
// That's somewhat unfortunate.
url = "https://golang.org/pkg/"
} else {
// TODO(maruel): Leverage Location.
// Use pkg.go.dev when there's a version (go module) and godoc.org when
// there's none (implies branch master).
_, branch := getSrcBranchURL(c)
if branch == "master" || branch == "" {
url = "https://godoc.org/"
} else {
url = "https://pkg.go.dev/"
}
}
if c.Func.IsExported {
return url + ip + template.URL("#") + symbol(&c.Func)
}
return url + ip
}
// srcURL returns an URL to the sources.
//
// TODO(maruel): Support custom local godoc server as it serves files too.
func srcURL(c *Call) template.URL {
url, _ := getSrcBranchURL(c)
return url
}
func escape(s string) template.URL {
// That's the only way I found to get the kind of escaping I wanted, where
// '/' is not escaped.
u := url.URL{Path: s}
/* #nosec G203 */
return template.URL(u.EscapedPath())
}
// getSrcBranchURL returns a link to the source on the web and the tag name for
// the package version, if possible.
func getSrcBranchURL(c *Call) (template.URL, template.URL) {
tag := ""
if c.Location == Stdlib {
// TODO(maruel): This is not strictly speaking correct. The remote could be
// running a different Go version from the current executable.
ver := runtime.Version()
const devel = "devel +"
if strings.HasPrefix(ver, devel) {
ver = ver[len(devel) : len(devel)+10]
}
tag = url.QueryEscape(ver)
/* #nosec G203 */
return template.URL(fmt.Sprintf("https://github.com/golang/go/blob/%s/src/%s#L%d", tag, escape(c.RelSrcPath), c.Line)), template.URL(tag)
}
// TODO(maruel): Leverage Location.
if rel := c.RelSrcPath; rel != "" {
// Check for vendored code first.
if i := strings.Index(rel, "/vendor/"); i != -1 {
rel = rel[i+8:]
}
// Specialized support for github and golang.org. This will cover a fair
// share of the URLs, but it'd be nice to support others too. Please submit
// a PR (including a unit test that I was too lazy to add yet).
switch host, rest := splitHost(rel); host {
case "github.com":
if parts := strings.SplitN(rest, "/", 3); len(parts) == 3 {
p, srcTag, tag := splitTag(parts[1])
url := fmt.Sprintf("https://github.com/%s/%s/blob/%s/%s#L%d", escape(parts[0]), p, srcTag, escape(parts[2]), c.Line)
/* #nosec G203 */
return template.URL(url), tag
}
log.Printf("problematic github.com URL: %q", rel)
case "golang.org":
// https://github.com/golang/build/blob/HEAD/repos/repos.go lists all
// the golang.org/x/<foo> packages.
if parts := strings.SplitN(rest, "/", 3); len(parts) == 3 && parts[0] == "x" {
// parts is: "x", <project@version>, <path inside the repo>.
p, srcTag, tag := splitTag(parts[1])
// The source of truth is are actually go.googlesource.com, but
// github.com has nicer syntax highlighting.
url := fmt.Sprintf("https://github.com/golang/%s/blob/%s/%s#L%d", p, srcTag, escape(parts[2]), c.Line)
/* #nosec G203 */
return template.URL(url), tag
}
log.Printf("problematic golang.org URL: %q", rel)
default:
// For example gopkg.in. In this case there's no known way to find the
// link to the source files, but we can still try to extract the version
// if fetched from a go module.
// Do a best effort to find a version by searching for a '@'.
if i := strings.IndexByte(rel, '@'); i != -1 {
if j := strings.IndexByte(rel[i:], '/'); j != -1 {
tag = rel[i+1 : i+j]
}
}
}
}
if c.LocalSrcPath != "" {
/* #nosec G203 */
return template.URL("file:///" + escape(c.LocalSrcPath)), template.URL(tag)
}
if c.RemoteSrcPath != "" {
/* #nosec G203 */
return template.URL("file:///" + escape(c.RemoteSrcPath)), template.URL(tag)
}
return "", ""
}
func splitHost(s string) (string, string) {
parts := strings.SplitN(s, "/", 2)
if len(parts) != 2 {
return parts[0], ""
}
return parts[0], parts[1]
}
// "v0.0.0-20200223170610-d5e6a3e2c0ae"
var reVersion = regexp.MustCompile(`v\d+\.\d+\.\d+\-\d+\-([a-f0-9]+)`)
func splitTag(s string) (string, string, template.URL) {
// Default to branch master for non-versioned dependencies. It's not
// optimal but it's better than nothing?
// TODO(maruel): Replace with HEAD.
i := strings.IndexByte(s, '@')
if i == -1 {
// No tag was found.
return s, "master", "master"
}
// We got a versioned go module.
tag := s[i+1:]
srcTag := tag
if m := reVersion.FindStringSubmatch(tag); len(m) != 0 {
srcTag = m[1]
}
/* #nosec G203 */
return s[:i], url.QueryEscape(srcTag), template.URL(url.QueryEscape(tag))
}
// symbol is the hashtag to use to refer to the symbol when looking at
// documentation.
//
// All of godoc/gddo, pkg.go.dev and golang.org/godoc use the same symbol
// reference format.
func symbol(f *Func) template.URL {
s := f.Name
if reMethodSymbol.MatchString(s) {
// Transform the method form.
s = reMethodSymbol.ReplaceAllString(s, "$1$2")
}
/* #nosec G203 */
return template.URL(url.QueryEscape(s))
}