go.tools/godoc: server mode: add support for type and pointer analysis.
See analysis.go for overview of new features. See README for known bugs and issues. Much UI polish, testing and optimization work remains, but this is a starting point. Flag: we add a new flag -analysis=type,pointer, default "", for adventurous users only at this stage. Type analysis takes ~10s for stdlib + go.tools; Pointer analysis (currently) takes several minutes. Dependencies: we now include jquery.treeview.js and its GIF images among the resources. (bake.go now handles binary.) LGTM=crawshaw, bgarcia R=crawshaw, bgarcia CC=bradfitz, golang-codereviews https://golang.org/cl/60540044
@ -66,10 +66,13 @@ func readTemplates(p *godoc.Presentation, html bool) {
|
||||
if html || p.HTMLMode {
|
||||
codewalkHTML = readTemplate("codewalk.html")
|
||||
codewalkdirHTML = readTemplate("codewalkdir.html")
|
||||
p.CallGraphHTML = readTemplate("callgraph.html")
|
||||
p.DirlistHTML = readTemplate("dirlist.html")
|
||||
p.ErrorHTML = readTemplate("error.html")
|
||||
p.ExampleHTML = readTemplate("example.html")
|
||||
p.GodocHTML = readTemplate("godoc.html")
|
||||
p.ImplementsHTML = readTemplate("implements.html")
|
||||
p.MethodSetHTML = readTemplate("methodset.html")
|
||||
p.PackageHTML = readTemplate("package.html")
|
||||
p.SearchHTML = readTemplate("search.html")
|
||||
p.SearchDocHTML = readTemplate("searchdoc.html")
|
||||
|
@ -42,8 +42,10 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"code.google.com/p/go.tools/godoc"
|
||||
"code.google.com/p/go.tools/godoc/analysis"
|
||||
"code.google.com/p/go.tools/godoc/static"
|
||||
"code.google.com/p/go.tools/godoc/vfs"
|
||||
"code.google.com/p/go.tools/godoc/vfs/gatefs"
|
||||
@ -64,6 +66,10 @@ var (
|
||||
// file-based index
|
||||
writeIndex = flag.Bool("write_index", false, "write index to a file; the file name must be specified with -index_files")
|
||||
|
||||
analysisFlag = flag.String("analysis", "", `comma-separated list of analyses to perform.
|
||||
"type": display identifier resolution, type info, method sets, 'implements', and static callees.
|
||||
"pointer" display channel peers, callers and dynamic callees. (Slower.)`)
|
||||
|
||||
// network
|
||||
httpAddr = flag.String("http", "", "HTTP service address (e.g., '"+defaultAddr+"')")
|
||||
serverAddr = flag.String("server", "", "webserver address for command line searches")
|
||||
@ -192,6 +198,20 @@ func main() {
|
||||
|
||||
httpMode := *httpAddr != ""
|
||||
|
||||
var typeAnalysis, pointerAnalysis bool
|
||||
if *analysisFlag != "" {
|
||||
for _, a := range strings.Split(*analysisFlag, ",") {
|
||||
switch a {
|
||||
case "type":
|
||||
typeAnalysis = true
|
||||
case "pointer":
|
||||
pointerAnalysis = true
|
||||
default:
|
||||
log.Fatalf("unknown analysis: %s", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
corpus := godoc.NewCorpus(fs)
|
||||
corpus.Verbose = *verbose
|
||||
corpus.MaxResults = *maxResults
|
||||
@ -283,6 +303,11 @@ func main() {
|
||||
go corpus.RunIndexer()
|
||||
}
|
||||
|
||||
// Start type/pointer analysis.
|
||||
if typeAnalysis || pointerAnalysis {
|
||||
go analysis.Run(pointerAnalysis, &corpus.Analysis)
|
||||
}
|
||||
|
||||
// Start http server.
|
||||
if err := http.ListenAndServe(*httpAddr, handler); err != nil {
|
||||
log.Fatalf("ListenAndServe %s: %v", *httpAddr, err)
|
||||
|
121
godoc/analysis/README
Normal file
@ -0,0 +1,121 @@
|
||||
|
||||
Type and Pointer Analysis to-do list
|
||||
====================================
|
||||
|
||||
Alan Donovan <adonovan@google.com>
|
||||
|
||||
|
||||
Overall design
|
||||
--------------
|
||||
|
||||
We should re-run the type and pointer analyses periodically,
|
||||
as we do with the indexer.
|
||||
|
||||
Version skew: how to mitigate the bad effects of stale URLs in old pages?
|
||||
We could record the file's length/CRC32/mtime in the go/loader, and
|
||||
refuse to decorate it with links unless they match at serving time.
|
||||
|
||||
Use the VFS mechanism when (a) enumerating packages and (b) loading
|
||||
them. (Requires planned changes to go/loader.)
|
||||
|
||||
Future work: shard this using map/reduce for larger corpora.
|
||||
|
||||
Testing: how does one test that a web page "looks right"?
|
||||
|
||||
|
||||
Bugs
|
||||
----
|
||||
|
||||
(*go/loader.Program).Load fails if it encounters a single parse error.
|
||||
Make this more robust.
|
||||
|
||||
(*ssa.Program).Create requires transitively error-free packages. We
|
||||
can make this more robust by making the requirement transitively free
|
||||
of "hard" errors; soft errors are fine.
|
||||
|
||||
Markup of compiler errors is slightly buggy because they overlap with
|
||||
other selections (e.g. Idents). Fix.
|
||||
|
||||
|
||||
User Interface
|
||||
--------------
|
||||
|
||||
CALLGRAPH:
|
||||
- Add a search box: given a search node, expand path from each entry
|
||||
point to it.
|
||||
- Cause hovering over a given node to highlight that node, and all
|
||||
nodes that are logically identical to it.
|
||||
- Initially expand the callgraph trees (but not their toggle divs).
|
||||
|
||||
CALLEES:
|
||||
- The '(' links are not very discoverable. Highlight them?
|
||||
|
||||
Type info:
|
||||
- In the source viewer's lower pane, use a toggle div around the
|
||||
IMPLEMENTS and METHODSETS lists, like we do in the pacakge view.
|
||||
Only expand them initially if short.
|
||||
- Include IMPLEMENTS and METHOD SETS information in search index.
|
||||
- URLs in IMPLEMENTS/METHOD SETS always link to source, even from the
|
||||
package docs view. This makes sense for links to non-exported
|
||||
types, but links to exported types and funcs should probably go to
|
||||
other package docs.
|
||||
- Suppress toggle divs for empty method sets.
|
||||
|
||||
Misc:
|
||||
- Add an "analysis help" page explaining the features and UI in more detail.
|
||||
- The [X] button in the lower pane is subject to scrolling.
|
||||
- Should the lower pane be floating? An iframe?
|
||||
When we change document.location by clicking on a link, it will go away.
|
||||
How do we prevent that (a la Gmail's chat windows)?
|
||||
- Progress/status: for each file, display its analysis status, one of:
|
||||
- not in analysis scope
|
||||
- type analysis running...
|
||||
- type analysis complete
|
||||
(+ optionally: there were type errors in this file)
|
||||
And if PTA requested:
|
||||
- type analysis complete; PTA not attempted due to type errors
|
||||
- PTA running...
|
||||
- PTA complete
|
||||
- Scroll the selection into view, e.g. the vertical center, or better
|
||||
still, under the pointer (assuming we have a mouse).
|
||||
|
||||
|
||||
More features
|
||||
-------------
|
||||
|
||||
Display the REFERRERS relation? (Useful but potentially large.)
|
||||
|
||||
Display the INSTANTIATIONS relation? i.e. given a type T, show the set of
|
||||
syntactic constructs that can instantiate it:
|
||||
var x T
|
||||
x := T{...}
|
||||
x = new(T)
|
||||
x = make([]T, n)
|
||||
etc
|
||||
+ all INSTANTIATIONS of all S defined as struct{t T} or [n]T
|
||||
(Potentially a lot of information.)
|
||||
(Add this to oracle too.)
|
||||
|
||||
|
||||
Optimisations
|
||||
-------------
|
||||
|
||||
Each call to addLink takes a (per-file) lock. The locking is
|
||||
fine-grained so server latency isn't terrible, but overall it makes
|
||||
the link computation quite slow. Batch update might be better.
|
||||
|
||||
Memory usage is now about 1.5GB for GOROOT + go.tools. It used to be 700MB.
|
||||
|
||||
Optimize for time and space. The main slowdown is the network I/O
|
||||
time caused by an increase in page size of about 3x: about 2x from
|
||||
HTML, and 0.7--2.1x from JSON (unindented vs indented). The JSON
|
||||
contains a lot of filenames (e.g. 820 copies of 16 distinct
|
||||
filenames). 20% of the HTML is L%d spans (now disabled). The HTML
|
||||
also contains lots of tooltips for long struct/interface types.
|
||||
De-dup or just abbreviate? The actual formatting is very fast.
|
||||
|
||||
The pointer analysis constraint solver is way too slow. We really
|
||||
need to implement the constraint optimizer. Also: performance has
|
||||
been quite unpredictable; I recently optimized it fourfold with
|
||||
type-based label tracking, but the benefit seems to have evaporated.
|
||||
TODO(adonovan): take a look at recent CLs for regressions.
|
549
godoc/analysis/analysis.go
Normal file
@ -0,0 +1,549 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package analysis performs type and pointer analysis
|
||||
// and generates mark-up for the Go source view.
|
||||
//
|
||||
// The Run method populates a Result object by running type and
|
||||
// (optionally) pointer analysis. The Result object is thread-safe
|
||||
// and at all times may be accessed by a serving thread, even as it is
|
||||
// progressively populated as analysis facts are derived.
|
||||
//
|
||||
// The Result is a mapping from each godoc file URL
|
||||
// (e.g. /src/pkg/fmt/print.go) to information about that file. The
|
||||
// information is a list of HTML markup links and a JSON array of
|
||||
// structured data values. Some of the links call client-side
|
||||
// JavaScript functions that index this array.
|
||||
//
|
||||
// The analysis computes mark-up for the following relations:
|
||||
//
|
||||
// IMPORTS: for each ast.ImportSpec, the package that it denotes.
|
||||
//
|
||||
// RESOLUTION: for each ast.Ident, its kind and type, and the location
|
||||
// of its definition.
|
||||
//
|
||||
// METHOD SETS, IMPLEMENTS: for each ast.Ident defining a named type,
|
||||
// its method-set, the set of interfaces it implements or is
|
||||
// implemented by, and its size/align values.
|
||||
//
|
||||
// CALLERS, CALLEES: for each function declaration ('func' token), its
|
||||
// callers, and for each call-site ('(' token), its callees.
|
||||
//
|
||||
// CALLGRAPH: the package docs include an interactive viewer for the
|
||||
// intra-package call graph of "fmt".
|
||||
//
|
||||
// CHANNEL PEERS: for each channel operation make/<-/close, the set of
|
||||
// other channel ops that alias the same channel(s).
|
||||
//
|
||||
// ERRORS: for each locus of a static (go/types) error, the location
|
||||
// is highlighted in red and hover text provides the compiler error
|
||||
// message.
|
||||
//
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/token"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.google.com/p/go.tools/go/loader"
|
||||
"code.google.com/p/go.tools/go/pointer"
|
||||
"code.google.com/p/go.tools/go/ssa"
|
||||
"code.google.com/p/go.tools/go/ssa/ssautil"
|
||||
"code.google.com/p/go.tools/go/types"
|
||||
)
|
||||
|
||||
// -- links ------------------------------------------------------------
|
||||
|
||||
// A Link is an HTML decoration of the bytes [Start, End) of a file.
|
||||
// Write is called before/after those bytes to emit the mark-up.
|
||||
type Link interface {
|
||||
Start() int
|
||||
End() int
|
||||
Write(w io.Writer, _ int, start bool) // the godoc.LinkWriter signature
|
||||
}
|
||||
|
||||
// An <a> element.
|
||||
type aLink struct {
|
||||
start, end int // =godoc.Segment
|
||||
title string // hover text
|
||||
onclick string // JS code (NB: trusted)
|
||||
href string // URL (NB: trusted)
|
||||
}
|
||||
|
||||
func (a aLink) Start() int { return a.start }
|
||||
func (a aLink) End() int { return a.end }
|
||||
func (a aLink) Write(w io.Writer, _ int, start bool) {
|
||||
if start {
|
||||
fmt.Fprintf(w, `<a title='%s'`, html.EscapeString(a.title))
|
||||
if a.onclick != "" {
|
||||
fmt.Fprintf(w, ` onclick='%s'`, html.EscapeString(a.onclick))
|
||||
}
|
||||
if a.href != "" {
|
||||
// TODO(adonovan): I think that in principle, a.href must first be
|
||||
// url.QueryEscape'd, but if I do that, a leading slash becomes "%2F",
|
||||
// which causes the browser to treat the path as relative, not absolute.
|
||||
// WTF?
|
||||
fmt.Fprintf(w, ` href='%s'`, html.EscapeString(a.href))
|
||||
}
|
||||
fmt.Fprintf(w, ">")
|
||||
} else {
|
||||
fmt.Fprintf(w, "</a>")
|
||||
}
|
||||
}
|
||||
|
||||
// An <a class='error'> element.
|
||||
type errorLink struct {
|
||||
start int
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e errorLink) Start() int { return e.start }
|
||||
func (e errorLink) End() int { return e.start + 1 }
|
||||
|
||||
func (e errorLink) Write(w io.Writer, _ int, start bool) {
|
||||
// <span> causes havoc, not sure why, so use <a>.
|
||||
if start {
|
||||
fmt.Fprintf(w, `<a class='error' title='%s'>`, html.EscapeString(e.msg))
|
||||
} else {
|
||||
fmt.Fprintf(w, "</a>")
|
||||
}
|
||||
}
|
||||
|
||||
// -- fileInfo ---------------------------------------------------------
|
||||
|
||||
// A fileInfo is the server's store of hyperlinks and JSON data for a
|
||||
// particular file.
|
||||
type fileInfo struct {
|
||||
mu sync.Mutex
|
||||
data []interface{} // JSON objects
|
||||
links []Link
|
||||
sorted bool
|
||||
hasErrors bool // TODO(adonovan): surface this in the UI
|
||||
}
|
||||
|
||||
// addLink adds a link to the Go source file fi.
|
||||
func (fi *fileInfo) addLink(link Link) {
|
||||
fi.mu.Lock()
|
||||
fi.links = append(fi.links, link)
|
||||
fi.sorted = false
|
||||
if _, ok := link.(errorLink); ok {
|
||||
fi.hasErrors = true
|
||||
}
|
||||
fi.mu.Unlock()
|
||||
}
|
||||
|
||||
// addData adds the structured value x to the JSON data for the Go
|
||||
// source file fi. Its index is returned.
|
||||
func (fi *fileInfo) addData(x interface{}) int {
|
||||
fi.mu.Lock()
|
||||
index := len(fi.data)
|
||||
fi.data = append(fi.data, x)
|
||||
fi.mu.Unlock()
|
||||
return index
|
||||
}
|
||||
|
||||
// get returns new slices containing opaque JSON values and the HTML link markup for fi.
|
||||
// Callers must not mutate the elements.
|
||||
func (fi *fileInfo) get() (data []interface{}, links []Link) {
|
||||
// Copy slices, to avoid races.
|
||||
fi.mu.Lock()
|
||||
data = append(data, fi.data...)
|
||||
if !fi.sorted {
|
||||
sort.Sort(linksByStart(fi.links))
|
||||
fi.sorted = true
|
||||
}
|
||||
links = append(links, fi.links...)
|
||||
fi.mu.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type pkgInfo struct {
|
||||
mu sync.Mutex
|
||||
callGraph []*PCGNodeJSON
|
||||
callGraphIndex map[string]int // keys are (*ssa.Function).RelString()
|
||||
types []*TypeInfoJSON // type info for exported types
|
||||
}
|
||||
|
||||
func (pi *pkgInfo) setCallGraph(callGraph []*PCGNodeJSON, callGraphIndex map[string]int) {
|
||||
pi.mu.Lock()
|
||||
pi.callGraph = callGraph
|
||||
pi.callGraphIndex = callGraphIndex
|
||||
pi.mu.Unlock()
|
||||
}
|
||||
|
||||
func (pi *pkgInfo) addType(t *TypeInfoJSON) {
|
||||
pi.mu.Lock()
|
||||
pi.types = append(pi.types, t)
|
||||
pi.mu.Unlock()
|
||||
}
|
||||
|
||||
// get returns new slices of JSON values for the callgraph and type info for pi.
|
||||
// Callers must not mutate the slice elements or the map.
|
||||
func (pi *pkgInfo) get() (callGraph []*PCGNodeJSON, callGraphIndex map[string]int, types []*TypeInfoJSON) {
|
||||
// Copy slices, to avoid races.
|
||||
pi.mu.Lock()
|
||||
callGraph = append(callGraph, pi.callGraph...)
|
||||
callGraphIndex = pi.callGraphIndex
|
||||
types = append(types, pi.types...)
|
||||
pi.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// -- Result -----------------------------------------------------------
|
||||
|
||||
// Result contains the results of analysis.
|
||||
// The result contains a mapping from filenames to a set of HTML links
|
||||
// and JavaScript data referenced by the links.
|
||||
type Result struct {
|
||||
mu sync.Mutex // guards maps (but not their contents)
|
||||
fileInfos map[string]*fileInfo // keys are godoc file URLs
|
||||
pkgInfos map[string]*pkgInfo // keys are import paths
|
||||
}
|
||||
|
||||
// fileInfo returns the fileInfo for the specified godoc file URL,
|
||||
// constructing it as needed. Thread-safe.
|
||||
func (res *Result) fileInfo(url string) *fileInfo {
|
||||
res.mu.Lock()
|
||||
fi, ok := res.fileInfos[url]
|
||||
if !ok {
|
||||
fi = new(fileInfo)
|
||||
res.fileInfos[url] = fi
|
||||
}
|
||||
res.mu.Unlock()
|
||||
return fi
|
||||
}
|
||||
|
||||
// FileInfo returns new slices containing opaque JSON values and the
|
||||
// HTML link markup for the specified godoc file URL. Thread-safe.
|
||||
// Callers must not mutate the elements.
|
||||
// It returns "zero" if no data is available.
|
||||
//
|
||||
func (res *Result) FileInfo(url string) ([]interface{}, []Link) {
|
||||
return res.fileInfo(url).get()
|
||||
}
|
||||
|
||||
// pkgInfo returns the pkgInfo for the specified import path,
|
||||
// constructing it as needed. Thread-safe.
|
||||
func (res *Result) pkgInfo(importPath string) *pkgInfo {
|
||||
res.mu.Lock()
|
||||
pi, ok := res.pkgInfos[importPath]
|
||||
if !ok {
|
||||
pi = new(pkgInfo)
|
||||
res.pkgInfos[importPath] = pi
|
||||
}
|
||||
res.mu.Unlock()
|
||||
return pi
|
||||
}
|
||||
|
||||
// PackageInfo returns new slices of JSON values for the callgraph and
|
||||
// type info for the specified package. Thread-safe.
|
||||
// Callers must not mutate the elements.
|
||||
// PackageInfo returns "zero" if no data is available.
|
||||
//
|
||||
func (res *Result) PackageInfo(importPath string) ([]*PCGNodeJSON, map[string]int, []*TypeInfoJSON) {
|
||||
return res.pkgInfo(importPath).get()
|
||||
}
|
||||
|
||||
// -- analysis ---------------------------------------------------------
|
||||
|
||||
type analysis struct {
|
||||
result *Result
|
||||
prog *ssa.Program
|
||||
ops []chanOp // all channel ops in program
|
||||
allNamed []*types.Named // all named types in the program
|
||||
ptaConfig pointer.Config
|
||||
path2url map[string]string // maps openable path to godoc file URL (/src/pkg/fmt/print.go)
|
||||
pcgs map[*ssa.Package]*packageCallGraph
|
||||
}
|
||||
|
||||
// fileAndOffset returns the file and offset for a given position.
|
||||
func (a *analysis) fileAndOffset(pos token.Pos) (fi *fileInfo, offset int) {
|
||||
posn := a.prog.Fset.Position(pos)
|
||||
url := a.path2url[posn.Filename]
|
||||
return a.result.fileInfo(url), posn.Offset
|
||||
}
|
||||
|
||||
// posURL returns the URL of the source extent [pos, pos+len).
|
||||
func (a *analysis) posURL(pos token.Pos, len int) string {
|
||||
if pos == token.NoPos {
|
||||
return ""
|
||||
}
|
||||
posn := a.prog.Fset.Position(pos)
|
||||
url := a.path2url[posn.Filename]
|
||||
// The URLs use #L%d fragment ids, but they are just decorative.
|
||||
// Emitting an anchor for each line caused page bloat, so
|
||||
// instead we use onload JS code to jump to the selection.
|
||||
return fmt.Sprintf("%s?s=%d:%d#L%d",
|
||||
url, posn.Offset, posn.Offset+len, posn.Line)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// Run runs program analysis and computes the resulting markup,
|
||||
// populating *result in a thread-safe manner, first with type
|
||||
// information then later with pointer analysis information if
|
||||
// enabled by the pta flag.
|
||||
//
|
||||
func Run(pta bool, result *Result) {
|
||||
result.fileInfos = make(map[string]*fileInfo)
|
||||
result.pkgInfos = make(map[string]*pkgInfo)
|
||||
|
||||
conf := loader.Config{
|
||||
SourceImports: true,
|
||||
AllowTypeErrors: true,
|
||||
}
|
||||
|
||||
errors := make(map[token.Pos][]string)
|
||||
conf.TypeChecker.Error = func(e error) {
|
||||
err := e.(types.Error)
|
||||
errors[err.Pos] = append(errors[err.Pos], err.Msg)
|
||||
}
|
||||
|
||||
var roots, args []string
|
||||
|
||||
// Enumerate packages in $GOROOT.
|
||||
root := runtime.GOROOT() + "/src/pkg/"
|
||||
roots = append(roots, root)
|
||||
args = allPackages(root)
|
||||
log.Printf("GOROOT=%s: %s\n", root, args)
|
||||
|
||||
// Enumerate packages in $GOPATH.
|
||||
for i, dir := range filepath.SplitList(build.Default.GOPATH) {
|
||||
root := dir + "/src/"
|
||||
roots = append(roots, root)
|
||||
pkgs := allPackages(root)
|
||||
log.Printf("GOPATH[%d]=%s: %s\n", i, root, pkgs)
|
||||
args = append(args, pkgs...)
|
||||
}
|
||||
|
||||
// Uncomment to make startup quicker during debugging.
|
||||
//args = []string{"code.google.com/p/go.tools/cmd/godoc"}
|
||||
//args = []string{"fmt"}
|
||||
|
||||
if _, err := conf.FromArgs(args, true); err != nil {
|
||||
log.Print(err) // import error
|
||||
return
|
||||
}
|
||||
|
||||
log.Print("Loading and type-checking packages...")
|
||||
iprog, err := conf.Load()
|
||||
log.Printf("Loaded %d packages.", len(iprog.AllPackages))
|
||||
if err != nil {
|
||||
// TODO(adonovan): loader: don't give up just because
|
||||
// of one parse error.
|
||||
log.Print(err) // parse error in some package
|
||||
return
|
||||
}
|
||||
|
||||
// Create SSA-form program representation.
|
||||
// Only the transitively error-free packages are used.
|
||||
prog := ssa.Create(iprog, ssa.GlobalDebug)
|
||||
|
||||
// Compute the set of main packages, including testmain.
|
||||
allPackages := prog.AllPackages()
|
||||
var mainPkgs []*ssa.Package
|
||||
if testmain := prog.CreateTestMainPackage(allPackages...); testmain != nil {
|
||||
mainPkgs = append(mainPkgs, testmain)
|
||||
}
|
||||
for _, pkg := range allPackages {
|
||||
if pkg.Object.Name() == "main" && pkg.Func("main") != nil {
|
||||
mainPkgs = append(mainPkgs, pkg)
|
||||
}
|
||||
}
|
||||
log.Print("Main packages: ", mainPkgs)
|
||||
|
||||
// Build SSA code for bodies of all functions in the whole program.
|
||||
log.Print("Building SSA...")
|
||||
prog.BuildAll()
|
||||
log.Print("SSA building complete")
|
||||
|
||||
a := analysis{
|
||||
result: result,
|
||||
prog: prog,
|
||||
pcgs: make(map[*ssa.Package]*packageCallGraph),
|
||||
}
|
||||
|
||||
// Build a mapping from openable filenames to godoc file URLs,
|
||||
// i.e. "/src/pkg/" plus path relative to GOROOT/src/pkg or GOPATH[i]/src.
|
||||
a.path2url = make(map[string]string)
|
||||
for _, info := range iprog.AllPackages {
|
||||
for _, f := range info.Files {
|
||||
abs := iprog.Fset.File(f.Pos()).Name()
|
||||
// Find the root to which this file belongs.
|
||||
for _, root := range roots {
|
||||
rel := strings.TrimPrefix(abs, root)
|
||||
if len(rel) < len(abs) {
|
||||
a.path2url[abs] = "/src/pkg/" + rel
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add links for type-checker errors.
|
||||
// TODO(adonovan): fix: these links can overlap with
|
||||
// identifier markup, causing the renderer to emit some
|
||||
// characters twice.
|
||||
for pos, errs := range errors {
|
||||
fi, offset := a.fileAndOffset(pos)
|
||||
fi.addLink(errorLink{
|
||||
start: offset,
|
||||
msg: strings.Join(errs, "\n"),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- type-based analyses ----------
|
||||
|
||||
// Compute the all-pairs IMPLEMENTS relation.
|
||||
// Collect all named types, even local types
|
||||
// (which can have methods via promotion)
|
||||
// and the built-in "error".
|
||||
errorType := types.Universe.Lookup("error").Type().(*types.Named)
|
||||
a.allNamed = append(a.allNamed, errorType)
|
||||
for _, info := range iprog.AllPackages {
|
||||
for _, obj := range info.Defs {
|
||||
if obj, ok := obj.(*types.TypeName); ok {
|
||||
a.allNamed = append(a.allNamed, obj.Type().(*types.Named))
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Print("Computing implements...")
|
||||
facts := computeImplements(&a.prog.MethodSets, a.allNamed)
|
||||
|
||||
// Add the type-based analysis results.
|
||||
log.Print("Extracting type info...")
|
||||
|
||||
for _, info := range iprog.AllPackages {
|
||||
a.doTypeInfo(info, facts)
|
||||
}
|
||||
|
||||
a.visitInstrs(pta)
|
||||
|
||||
log.Print("Extracting type info complete")
|
||||
|
||||
if pta {
|
||||
a.pointer(mainPkgs)
|
||||
}
|
||||
}
|
||||
|
||||
// visitInstrs visits all SSA instructions in the program.
|
||||
func (a *analysis) visitInstrs(pta bool) {
|
||||
log.Print("Visit instructions...")
|
||||
for fn := range ssautil.AllFunctions(a.prog) {
|
||||
for _, b := range fn.Blocks {
|
||||
for _, instr := range b.Instrs {
|
||||
// CALLEES (static)
|
||||
// (Dynamic calls require pointer analysis.)
|
||||
//
|
||||
// We use the SSA representation to find the static callee,
|
||||
// since in many cases it does better than the
|
||||
// types.Info.{Refs,Selection} information. For example:
|
||||
//
|
||||
// defer func(){}() // static call to anon function
|
||||
// f := func(){}; f() // static call to anon function
|
||||
// f := fmt.Println; f() // static call to named function
|
||||
//
|
||||
// The downside is that we get no static callee information
|
||||
// for packages that (transitively) contain errors.
|
||||
if site, ok := instr.(ssa.CallInstruction); ok {
|
||||
if callee := site.Common().StaticCallee(); callee != nil {
|
||||
// TODO(adonovan): callgraph: elide wrappers.
|
||||
// (Do static calls ever go to wrappers?)
|
||||
if site.Common().Pos() != token.NoPos {
|
||||
a.addCallees(site, []*ssa.Function{callee})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !pta {
|
||||
continue
|
||||
}
|
||||
|
||||
// CHANNEL PEERS
|
||||
// Collect send/receive/close instructions in the whole ssa.Program.
|
||||
for _, op := range chanOps(instr) {
|
||||
a.ops = append(a.ops, op)
|
||||
a.ptaConfig.AddQuery(op.ch) // add channel ssa.Value to PTA query
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Print("Visit instructions complete")
|
||||
}
|
||||
|
||||
// pointer runs the pointer analysis.
|
||||
func (a *analysis) pointer(mainPkgs []*ssa.Package) {
|
||||
// Run the pointer analysis and build the complete callgraph.
|
||||
a.ptaConfig.Mains = mainPkgs
|
||||
a.ptaConfig.BuildCallGraph = true
|
||||
a.ptaConfig.Reflection = false // (for now)
|
||||
|
||||
log.Print("Running pointer analysis...")
|
||||
ptares, err := pointer.Analyze(&a.ptaConfig)
|
||||
if err != nil {
|
||||
// If this happens, it indicates a bug.
|
||||
log.Print("Pointer analysis failed: %s", err)
|
||||
return
|
||||
}
|
||||
log.Print("Pointer analysis complete.")
|
||||
|
||||
// Add the results of pointer analysis.
|
||||
|
||||
log.Print("Computing channel peers...")
|
||||
a.doChannelPeers(ptares.Queries)
|
||||
log.Print("Computing dynamic call graph edges...")
|
||||
a.doCallgraph(ptares.CallGraph)
|
||||
|
||||
log.Print("Done")
|
||||
}
|
||||
|
||||
type linksByStart []Link
|
||||
|
||||
func (a linksByStart) Less(i, j int) bool { return a[i].Start() < a[j].Start() }
|
||||
func (a linksByStart) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a linksByStart) Len() int { return len(a) }
|
||||
|
||||
// allPackages returns a new sorted slice of all packages beneath the
|
||||
// specified package root directory, e.g. $GOROOT/src/pkg or $GOPATH/src.
|
||||
// Derived from from go/ssa/stdlib_test.go
|
||||
func allPackages(root string) []string {
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root += "/"
|
||||
}
|
||||
var pkgs []string
|
||||
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return nil // non-existent root directory?
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil // not a directory
|
||||
}
|
||||
// Prune the search if we encounter any of these names:
|
||||
base := filepath.Base(path)
|
||||
if base == "testdata" || strings.HasPrefix(base, ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
pkg := filepath.ToSlash(strings.TrimPrefix(path, root))
|
||||
switch pkg {
|
||||
case "builtin":
|
||||
return filepath.SkipDir
|
||||
case "":
|
||||
return nil // ignore root of tree
|
||||
}
|
||||
pkgs = append(pkgs, pkg)
|
||||
return nil
|
||||
})
|
||||
return pkgs
|
||||
}
|
351
godoc/analysis/callgraph.go
Normal file
@ -0,0 +1,351 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file computes the CALLERS and CALLEES relations from the call
|
||||
// graph. CALLERS/CALLEES information is displayed in the lower pane
|
||||
// when a "func" token or ast.CallExpr.Lparen is clicked, respectively.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"log"
|
||||
"math/big"
|
||||
"sort"
|
||||
|
||||
"code.google.com/p/go.tools/go/callgraph"
|
||||
"code.google.com/p/go.tools/go/ssa"
|
||||
"code.google.com/p/go.tools/go/types"
|
||||
)
|
||||
|
||||
// doCallgraph computes the CALLEES and CALLERS relations.
|
||||
func (a *analysis) doCallgraph(cg *callgraph.Graph) {
|
||||
log.Print("Deleting synthetic nodes...")
|
||||
// TODO(adonovan): opt: DeleteSyntheticNodes is asymptotically
|
||||
// inefficient and can be (unpredictably) slow.
|
||||
cg.DeleteSyntheticNodes()
|
||||
log.Print("Synthetic nodes deleted")
|
||||
|
||||
// Populate nodes of package call graphs (PCGs).
|
||||
for _, n := range cg.Nodes {
|
||||
a.pcgAddNode(n.Func)
|
||||
}
|
||||
// Within each PCG, sort funcs by name.
|
||||
for _, pcg := range a.pcgs {
|
||||
pcg.sortNodes()
|
||||
}
|
||||
|
||||
calledFuncs := make(map[ssa.CallInstruction]map[*ssa.Function]bool)
|
||||
callingSites := make(map[*ssa.Function]map[ssa.CallInstruction]bool)
|
||||
for _, n := range cg.Nodes {
|
||||
for _, e := range n.Out {
|
||||
if e.Site == nil {
|
||||
continue // a call from the <root> node
|
||||
}
|
||||
|
||||
// Add (site pos, callee) to calledFuncs.
|
||||
// (Dynamic calls only.)
|
||||
callee := e.Callee.Func
|
||||
|
||||
a.pcgAddEdge(n.Func, callee)
|
||||
|
||||
if callee.Synthetic != "" {
|
||||
continue // call of a package initializer
|
||||
}
|
||||
|
||||
if e.Site.Common().StaticCallee() == nil {
|
||||
// dynamic call
|
||||
// (CALLEES information for static calls
|
||||
// is computed using SSA information.)
|
||||
lparen := e.Site.Common().Pos()
|
||||
if lparen != token.NoPos {
|
||||
fns := calledFuncs[e.Site]
|
||||
if fns == nil {
|
||||
fns = make(map[*ssa.Function]bool)
|
||||
calledFuncs[e.Site] = fns
|
||||
}
|
||||
fns[callee] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add (callee, site) to callingSites.
|
||||
fns := callingSites[callee]
|
||||
if fns == nil {
|
||||
fns = make(map[ssa.CallInstruction]bool)
|
||||
callingSites[callee] = fns
|
||||
}
|
||||
fns[e.Site] = true
|
||||
}
|
||||
}
|
||||
|
||||
// CALLEES.
|
||||
log.Print("Callees...")
|
||||
for site, fns := range calledFuncs {
|
||||
var funcs funcsByPos
|
||||
for fn := range fns {
|
||||
funcs = append(funcs, fn)
|
||||
}
|
||||
sort.Sort(funcs)
|
||||
|
||||
a.addCallees(site, funcs)
|
||||
}
|
||||
|
||||
// CALLERS
|
||||
log.Print("Callers...")
|
||||
for callee, sites := range callingSites {
|
||||
pos := funcToken(callee)
|
||||
if pos == token.NoPos {
|
||||
log.Print("CALLERS: skipping %s: no pos", callee)
|
||||
continue
|
||||
}
|
||||
|
||||
var this *types.Package // for relativizing names
|
||||
if callee.Pkg != nil {
|
||||
this = callee.Pkg.Object
|
||||
}
|
||||
|
||||
// Compute sites grouped by parent, with text and URLs.
|
||||
sitesByParent := make(map[*ssa.Function]sitesByPos)
|
||||
for site := range sites {
|
||||
fn := site.Parent()
|
||||
sitesByParent[fn] = append(sitesByParent[fn], site)
|
||||
}
|
||||
var funcs funcsByPos
|
||||
for fn := range sitesByParent {
|
||||
funcs = append(funcs, fn)
|
||||
}
|
||||
sort.Sort(funcs)
|
||||
|
||||
v := callersJSON{
|
||||
Callee: callee.String(),
|
||||
Callers: []callerJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
caller := callerJSON{
|
||||
Func: prettyFunc(this, fn),
|
||||
Sites: []anchorJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
sites := sitesByParent[fn]
|
||||
sort.Sort(sites)
|
||||
for _, site := range sites {
|
||||
pos := site.Common().Pos()
|
||||
if pos != token.NoPos {
|
||||
caller.Sites = append(caller.Sites, anchorJSON{
|
||||
Text: fmt.Sprintf("%d", a.prog.Fset.Position(pos).Line),
|
||||
Href: a.posURL(pos, len("(")),
|
||||
})
|
||||
}
|
||||
}
|
||||
v.Callers = append(v.Callers, caller)
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(pos)
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len("func"),
|
||||
title: fmt.Sprintf("%d callers", len(sites)),
|
||||
onclick: fmt.Sprintf("onClickCallers(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
|
||||
// PACKAGE CALLGRAPH
|
||||
log.Print("Package call graph...")
|
||||
for pkg, pcg := range a.pcgs {
|
||||
// Maps (*ssa.Function).RelString() to index in JSON CALLGRAPH array.
|
||||
index := make(map[string]int)
|
||||
|
||||
// Treat exported functions (and exported methods of
|
||||
// exported named types) as roots even if they aren't
|
||||
// actually called from outside the package.
|
||||
for i, n := range pcg.nodes {
|
||||
if i == 0 || n.fn.Object() == nil || !n.fn.Object().Exported() {
|
||||
continue
|
||||
}
|
||||
recv := n.fn.Signature.Recv()
|
||||
if recv == nil || deref(recv.Type()).(*types.Named).Obj().Exported() {
|
||||
roots := &pcg.nodes[0].edges
|
||||
roots.SetBit(roots, i, 1)
|
||||
}
|
||||
index[n.fn.RelString(pkg.Object)] = i
|
||||
}
|
||||
|
||||
json := a.pcgJSON(pcg)
|
||||
|
||||
// TODO(adonovan): pkg.Path() is not unique!
|
||||
// It is possible to declare a non-test package called x_test.
|
||||
a.result.pkgInfo(pkg.Object.Path()).setCallGraph(json, index)
|
||||
}
|
||||
}
|
||||
|
||||
// addCallees adds client data and links for the facts that site calls fns.
|
||||
func (a *analysis) addCallees(site ssa.CallInstruction, fns []*ssa.Function) {
|
||||
v := calleesJSON{
|
||||
Descr: site.Common().Description(),
|
||||
Callees: []anchorJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
var this *types.Package // for relativizing names
|
||||
if p := site.Parent().Package(); p != nil {
|
||||
this = p.Object
|
||||
}
|
||||
|
||||
for _, fn := range fns {
|
||||
v.Callees = append(v.Callees, anchorJSON{
|
||||
Text: prettyFunc(this, fn),
|
||||
Href: a.posURL(funcToken(fn), len("func")),
|
||||
})
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(site.Common().Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len("("),
|
||||
title: fmt.Sprintf("%d callees", len(v.Callees)),
|
||||
onclick: fmt.Sprintf("onClickCallees(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
|
||||
// -- utilities --------------------------------------------------------
|
||||
|
||||
// stable order within packages but undefined across packages.
|
||||
type funcsByPos []*ssa.Function
|
||||
|
||||
func (a funcsByPos) Less(i, j int) bool { return a[i].Pos() < a[j].Pos() }
|
||||
func (a funcsByPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a funcsByPos) Len() int { return len(a) }
|
||||
|
||||
type sitesByPos []ssa.CallInstruction
|
||||
|
||||
func (a sitesByPos) Less(i, j int) bool { return a[i].Common().Pos() < a[j].Common().Pos() }
|
||||
func (a sitesByPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sitesByPos) Len() int { return len(a) }
|
||||
|
||||
func funcToken(fn *ssa.Function) token.Pos {
|
||||
switch syntax := fn.Syntax().(type) {
|
||||
case *ast.FuncLit:
|
||||
return syntax.Type.Func
|
||||
case *ast.FuncDecl:
|
||||
return syntax.Type.Func
|
||||
}
|
||||
return token.NoPos
|
||||
}
|
||||
|
||||
// prettyFunc pretty-prints fn for the user interface.
|
||||
// TODO(adonovan): return HTML so we have more markup freedom.
|
||||
func prettyFunc(this *types.Package, fn *ssa.Function) string {
|
||||
if fn.Enclosing != nil {
|
||||
return fmt.Sprintf("%s in %s",
|
||||
types.TypeString(this, fn.Signature),
|
||||
prettyFunc(this, fn.Enclosing))
|
||||
}
|
||||
if fn.Synthetic != "" && fn.Name() == "init" {
|
||||
// (This is the actual initializer, not a declared 'func init').
|
||||
if fn.Pkg.Object == this {
|
||||
return "package initializer"
|
||||
}
|
||||
return fmt.Sprintf("%q package initializer", fn.Pkg.Object.Path())
|
||||
}
|
||||
return fn.RelString(this)
|
||||
}
|
||||
|
||||
// -- intra-package callgraph ------------------------------------------
|
||||
|
||||
// pcgNode represents a node in the package call graph (PCG).
|
||||
type pcgNode struct {
|
||||
fn *ssa.Function
|
||||
pretty string // cache of prettyFunc(fn)
|
||||
edges big.Int // set of callee func indices
|
||||
}
|
||||
|
||||
// A packageCallGraph represents the intra-package edges of the global call graph.
|
||||
// The zeroth node indicates "all external functions".
|
||||
type packageCallGraph struct {
|
||||
nodeIndex map[*ssa.Function]int // maps func to node index (a small int)
|
||||
nodes []*pcgNode // maps node index to node
|
||||
}
|
||||
|
||||
// sortNodes populates pcg.nodes in name order and updates the nodeIndex.
|
||||
func (pcg *packageCallGraph) sortNodes() {
|
||||
nodes := make([]*pcgNode, 0, len(pcg.nodeIndex))
|
||||
nodes = append(nodes, &pcgNode{fn: nil, pretty: "<external>"})
|
||||
for fn := range pcg.nodeIndex {
|
||||
nodes = append(nodes, &pcgNode{
|
||||
fn: fn,
|
||||
pretty: prettyFunc(fn.Pkg.Object, fn),
|
||||
})
|
||||
}
|
||||
sort.Sort(pcgNodesByPretty(nodes[1:]))
|
||||
for i, n := range nodes {
|
||||
pcg.nodeIndex[n.fn] = i
|
||||
}
|
||||
pcg.nodes = nodes
|
||||
}
|
||||
|
||||
func (pcg *packageCallGraph) addEdge(caller, callee *ssa.Function) {
|
||||
var callerIndex int
|
||||
if caller.Pkg == callee.Pkg {
|
||||
// intra-package edge
|
||||
callerIndex = pcg.nodeIndex[caller]
|
||||
if callerIndex < 1 {
|
||||
panic(caller)
|
||||
}
|
||||
}
|
||||
edges := &pcg.nodes[callerIndex].edges
|
||||
edges.SetBit(edges, pcg.nodeIndex[callee], 1)
|
||||
}
|
||||
|
||||
func (a *analysis) pcgAddNode(fn *ssa.Function) {
|
||||
if fn.Pkg == nil {
|
||||
return
|
||||
}
|
||||
pcg, ok := a.pcgs[fn.Pkg]
|
||||
if !ok {
|
||||
pcg = &packageCallGraph{nodeIndex: make(map[*ssa.Function]int)}
|
||||
a.pcgs[fn.Pkg] = pcg
|
||||
}
|
||||
pcg.nodeIndex[fn] = -1
|
||||
}
|
||||
|
||||
func (a *analysis) pcgAddEdge(caller, callee *ssa.Function) {
|
||||
if callee.Pkg != nil {
|
||||
a.pcgs[callee.Pkg].addEdge(caller, callee)
|
||||
}
|
||||
}
|
||||
|
||||
// pcgJSON returns a new slice of callgraph JSON values.
|
||||
func (a *analysis) pcgJSON(pcg *packageCallGraph) []*PCGNodeJSON {
|
||||
var nodes []*PCGNodeJSON
|
||||
for _, n := range pcg.nodes {
|
||||
|
||||
// TODO(adonovan): why is there no good way to iterate
|
||||
// over the set bits of a big.Int?
|
||||
var callees []int
|
||||
nbits := n.edges.BitLen()
|
||||
for j := 0; j < nbits; j++ {
|
||||
if n.edges.Bit(j) == 1 {
|
||||
callees = append(callees, j)
|
||||
}
|
||||
}
|
||||
|
||||
var pos token.Pos
|
||||
if n.fn != nil {
|
||||
pos = funcToken(n.fn)
|
||||
}
|
||||
nodes = append(nodes, &PCGNodeJSON{
|
||||
Func: anchorJSON{
|
||||
Text: n.pretty,
|
||||
Href: a.posURL(pos, len("func")),
|
||||
},
|
||||
Callees: callees,
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
type pcgNodesByPretty []*pcgNode
|
||||
|
||||
func (a pcgNodesByPretty) Less(i, j int) bool { return a[i].pretty < a[j].pretty }
|
||||
func (a pcgNodesByPretty) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a pcgNodesByPretty) Len() int { return len(a) }
|
194
godoc/analysis/implements.go
Normal file
@ -0,0 +1,194 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file computes the "implements" relation over all pairs of
|
||||
// named types in the program. (The mark-up is done by typeinfo.go.)
|
||||
|
||||
// TODO(adonovan): do we want to report implements(C, I) where C and I
|
||||
// belong to different packages and at least one is not exported?
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"code.google.com/p/go.tools/go/types"
|
||||
)
|
||||
|
||||
// computeImplements computes the "implements" relation over all pairs
|
||||
// of named types in allNamed.
|
||||
func computeImplements(cache *types.MethodSetCache, allNamed []*types.Named) map[*types.Named]implementsFacts {
|
||||
// Information about a single type's method set.
|
||||
type msetInfo struct {
|
||||
typ types.Type
|
||||
mset *types.MethodSet
|
||||
mask1, mask2 uint64
|
||||
}
|
||||
|
||||
initMsetInfo := func(info *msetInfo, typ types.Type) {
|
||||
info.typ = typ
|
||||
info.mset = cache.MethodSet(typ)
|
||||
for i := 0; i < info.mset.Len(); i++ {
|
||||
name := info.mset.At(i).Obj().Name()
|
||||
info.mask1 |= 1 << methodBit(name[0])
|
||||
info.mask2 |= 1 << methodBit(name[len(name)-1])
|
||||
}
|
||||
}
|
||||
|
||||
// satisfies(T, U) reports whether type T satisfies type U.
|
||||
// U must be an interface.
|
||||
//
|
||||
// Since there are thousands of types (and thus millions of
|
||||
// pairs of types) and types.Assignable(T, U) is relatively
|
||||
// expensive, we compute assignability directly from the
|
||||
// method sets. (At least one of T and U must be an
|
||||
// interface.)
|
||||
//
|
||||
// We use a trick (thanks gri!) related to a Bloom filter to
|
||||
// quickly reject most tests, which are false. For each
|
||||
// method set, we precompute a mask, a set of bits, one per
|
||||
// distinct initial byte of each method name. Thus the mask
|
||||
// for io.ReadWriter would be {'R','W'}. AssignableTo(T, U)
|
||||
// cannot be true unless mask(T)&mask(U)==mask(U).
|
||||
//
|
||||
// As with a Bloom filter, we can improve precision by testing
|
||||
// additional hashes, e.g. using the last letter of each
|
||||
// method name, so long as the subset mask property holds.
|
||||
//
|
||||
// When analyzing the standard library, there are about 1e6
|
||||
// calls to satisfies(), of which 0.6% return true. With a
|
||||
// 1-hash filter, 95% of calls avoid the expensive check; with
|
||||
// a 2-hash filter, this grows to 98.2%.
|
||||
satisfies := func(T, U *msetInfo) bool {
|
||||
return T.mask1&U.mask1 == U.mask1 &&
|
||||
T.mask2&U.mask2 == U.mask2 &&
|
||||
containsAllIdsOf(T.mset, U.mset)
|
||||
}
|
||||
|
||||
// Information about a named type N, and perhaps also *N.
|
||||
type namedInfo struct {
|
||||
isInterface bool
|
||||
base msetInfo // N
|
||||
ptr msetInfo // *N, iff N !isInterface
|
||||
}
|
||||
|
||||
var infos []namedInfo
|
||||
|
||||
// Precompute the method sets and their masks.
|
||||
for _, N := range allNamed {
|
||||
var info namedInfo
|
||||
initMsetInfo(&info.base, N)
|
||||
_, info.isInterface = N.Underlying().(*types.Interface)
|
||||
if !info.isInterface {
|
||||
initMsetInfo(&info.ptr, types.NewPointer(N))
|
||||
}
|
||||
|
||||
if info.base.mask1|info.ptr.mask1 == 0 {
|
||||
continue // neither N nor *N has methods
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
facts := make(map[*types.Named]implementsFacts)
|
||||
|
||||
// Test all pairs of distinct named types (T, U).
|
||||
// TODO(adonovan): opt: compute (U, T) at the same time.
|
||||
for t := range infos {
|
||||
T := &infos[t]
|
||||
var to, from, fromPtr []types.Type
|
||||
for u := range infos {
|
||||
if t == u {
|
||||
continue
|
||||
}
|
||||
U := &infos[u]
|
||||
switch {
|
||||
case T.isInterface && U.isInterface:
|
||||
if satisfies(&U.base, &T.base) {
|
||||
to = append(to, U.base.typ)
|
||||
}
|
||||
if satisfies(&T.base, &U.base) {
|
||||
from = append(from, U.base.typ)
|
||||
}
|
||||
case T.isInterface: // U concrete
|
||||
if satisfies(&U.base, &T.base) {
|
||||
to = append(to, U.base.typ)
|
||||
} else if satisfies(&U.ptr, &T.base) {
|
||||
to = append(to, U.ptr.typ)
|
||||
}
|
||||
case U.isInterface: // T concrete
|
||||
if satisfies(&T.base, &U.base) {
|
||||
from = append(from, U.base.typ)
|
||||
} else if satisfies(&T.ptr, &U.base) {
|
||||
fromPtr = append(fromPtr, U.base.typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort types (arbitrarily) to avoid nondeterminism.
|
||||
sort.Sort(typesByString(to))
|
||||
sort.Sort(typesByString(from))
|
||||
sort.Sort(typesByString(fromPtr))
|
||||
|
||||
facts[T.base.typ.(*types.Named)] = implementsFacts{to, from, fromPtr}
|
||||
}
|
||||
|
||||
return facts
|
||||
}
|
||||
|
||||
type implementsFacts struct {
|
||||
to []types.Type // named or ptr-to-named types assignable to interface T
|
||||
from []types.Type // named interfaces assignable from T
|
||||
fromPtr []types.Type // named interfaces assignable only from *T
|
||||
}
|
||||
|
||||
type typesByString []types.Type
|
||||
|
||||
func (p typesByString) Len() int { return len(p) }
|
||||
func (p typesByString) Less(i, j int) bool { return p[i].String() < p[j].String() }
|
||||
func (p typesByString) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// methodBit returns the index of x in [a-zA-Z], or 52 if not found.
|
||||
func methodBit(x byte) uint64 {
|
||||
switch {
|
||||
case 'a' <= x && x <= 'z':
|
||||
return uint64(x - 'a')
|
||||
case 'A' <= x && x <= 'Z':
|
||||
return uint64(26 + x - 'A')
|
||||
}
|
||||
return 52 // all other bytes
|
||||
}
|
||||
|
||||
// containsAllIdsOf reports whether the method identifiers of T are a
|
||||
// superset of those in U. If U belongs to an interface type, the
|
||||
// result is equal to types.Assignable(T, U), but is cheaper to compute.
|
||||
//
|
||||
// TODO(gri): make this a method of *types.MethodSet.
|
||||
//
|
||||
func containsAllIdsOf(T, U *types.MethodSet) bool {
|
||||
t, tlen := 0, T.Len()
|
||||
u, ulen := 0, U.Len()
|
||||
for t < tlen && u < ulen {
|
||||
tMeth := T.At(t).Obj()
|
||||
uMeth := U.At(u).Obj()
|
||||
tId := tMeth.Id()
|
||||
uId := uMeth.Id()
|
||||
if tId > uId {
|
||||
// U has a method T lacks: fail.
|
||||
return false
|
||||
}
|
||||
if tId < uId {
|
||||
// T has a method U lacks: ignore it.
|
||||
t++
|
||||
continue
|
||||
}
|
||||
// U and T both have a method of this Id. Check types.
|
||||
if !types.Identical(tMeth.Type(), uMeth.Type()) {
|
||||
return false // type mismatch
|
||||
}
|
||||
u++
|
||||
t++
|
||||
}
|
||||
return u == ulen
|
||||
}
|
69
godoc/analysis/json.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file defines types used by client-side JavaScript.
|
||||
|
||||
type anchorJSON struct {
|
||||
Text string // HTML
|
||||
Href string // URL
|
||||
}
|
||||
|
||||
type commOpJSON struct {
|
||||
Op anchorJSON
|
||||
Fn string
|
||||
}
|
||||
|
||||
// JavaScript's onClickComm() expects a commJSON.
|
||||
type commJSON struct {
|
||||
Ops []commOpJSON
|
||||
}
|
||||
|
||||
// Indicates one of these forms of fact about a type T:
|
||||
// T "is implemented by <ByKind> type <Other>" (ByKind != "", e.g. "array")
|
||||
// T "implements <Other>" (ByKind == "")
|
||||
type implFactJSON struct {
|
||||
ByKind string `json:",omitempty"`
|
||||
Other anchorJSON
|
||||
}
|
||||
|
||||
// Implements facts are grouped by form, for ease of reading.
|
||||
type implGroupJSON struct {
|
||||
Descr string
|
||||
Facts []implFactJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickIdent() expects a TypeInfoJSON.
|
||||
type TypeInfoJSON struct {
|
||||
Name string // type name
|
||||
Size, Align int64
|
||||
Methods []anchorJSON
|
||||
ImplGroups []implGroupJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickCallees() expects a calleesJSON.
|
||||
type calleesJSON struct {
|
||||
Descr string
|
||||
Callees []anchorJSON // markup for called function
|
||||
}
|
||||
|
||||
type callerJSON struct {
|
||||
Func string
|
||||
Sites []anchorJSON
|
||||
}
|
||||
|
||||
// JavaScript's onClickCallers() expects a callersJSON.
|
||||
type callersJSON struct {
|
||||
Callee string
|
||||
Callers []callerJSON
|
||||
}
|
||||
|
||||
// JavaScript's cgAddChild requires a global array of PCGNodeJSON
|
||||
// called CALLGRAPH, representing the intra-package call graph.
|
||||
// The first element is special and represents "all external callers".
|
||||
type PCGNodeJSON struct {
|
||||
Func anchorJSON
|
||||
Callees []int // indices within CALLGRAPH of nodes called by this one
|
||||
}
|
154
godoc/analysis/peers.go
Normal file
@ -0,0 +1,154 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file computes the channel "peers" relation over all pairs of
|
||||
// channel operations in the program. The peers are displayed in the
|
||||
// lower pane when a channel operation (make, <-, close) is clicked.
|
||||
|
||||
// TODO(adonovan): handle calls to reflect.{Select,Recv,Send,Close} too,
|
||||
// then enable reflection in PTA.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
|
||||
"code.google.com/p/go.tools/go/pointer"
|
||||
"code.google.com/p/go.tools/go/ssa"
|
||||
"code.google.com/p/go.tools/go/types"
|
||||
)
|
||||
|
||||
func (a *analysis) doChannelPeers(ptsets map[ssa.Value]pointer.Pointer) {
|
||||
addSendRecv := func(j *commJSON, op chanOp) {
|
||||
j.Ops = append(j.Ops, commOpJSON{
|
||||
Op: anchorJSON{
|
||||
Text: op.mode,
|
||||
Href: a.posURL(op.pos, op.len),
|
||||
},
|
||||
Fn: prettyFunc(nil, op.fn),
|
||||
})
|
||||
}
|
||||
|
||||
// Build an undirected bipartite multigraph (binary relation)
|
||||
// of MakeChan ops and send/recv/close ops.
|
||||
//
|
||||
// TODO(adonovan): opt: use channel element types to partition
|
||||
// the O(n^2) problem into subproblems.
|
||||
aliasedOps := make(map[*ssa.MakeChan][]chanOp)
|
||||
opToMakes := make(map[chanOp][]*ssa.MakeChan)
|
||||
for _, op := range a.ops {
|
||||
// Combine the PT sets from all contexts.
|
||||
var makes []*ssa.MakeChan // aliased ops
|
||||
ptr, ok := ptsets[op.ch]
|
||||
if !ok {
|
||||
continue // e.g. channel op in dead code
|
||||
}
|
||||
for _, label := range ptr.PointsTo().Labels() {
|
||||
makechan, ok := label.Value().(*ssa.MakeChan)
|
||||
if !ok {
|
||||
continue // skip intrinsically-created channels for now
|
||||
}
|
||||
if makechan.Pos() == token.NoPos {
|
||||
continue // not possible?
|
||||
}
|
||||
makes = append(makes, makechan)
|
||||
aliasedOps[makechan] = append(aliasedOps[makechan], op)
|
||||
}
|
||||
opToMakes[op] = makes
|
||||
}
|
||||
|
||||
// Now that complete relation is built, build links for ops.
|
||||
for _, op := range a.ops {
|
||||
v := commJSON{
|
||||
Ops: []commOpJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
ops := make(map[chanOp]bool)
|
||||
for _, makechan := range opToMakes[op] {
|
||||
v.Ops = append(v.Ops, commOpJSON{
|
||||
Op: anchorJSON{
|
||||
Text: "made",
|
||||
Href: a.posURL(makechan.Pos()-token.Pos(len("make")),
|
||||
len("make")),
|
||||
},
|
||||
Fn: makechan.Parent().RelString(op.fn.Package().Object),
|
||||
})
|
||||
for _, op := range aliasedOps[makechan] {
|
||||
ops[op] = true
|
||||
}
|
||||
}
|
||||
for op := range ops {
|
||||
addSendRecv(&v, op)
|
||||
}
|
||||
|
||||
// Add links for each aliased op.
|
||||
fi, offset := a.fileAndOffset(op.pos)
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + op.len,
|
||||
title: "show channel ops",
|
||||
onclick: fmt.Sprintf("onClickComm(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
// Add links for makechan ops themselves.
|
||||
for makechan, ops := range aliasedOps {
|
||||
v := commJSON{
|
||||
Ops: []commOpJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
for _, op := range ops {
|
||||
addSendRecv(&v, op)
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(makechan.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset - len("make"),
|
||||
end: offset,
|
||||
title: "show channel ops",
|
||||
onclick: fmt.Sprintf("onClickComm(%d)", fi.addData(v)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// -- utilities --------------------------------------------------------
|
||||
|
||||
// chanOp abstracts an ssa.Send, ssa.Unop(ARROW), close(), or a SelectState.
|
||||
// Derived from oracle/peers.go.
|
||||
type chanOp struct {
|
||||
ch ssa.Value
|
||||
mode string // sent|received|closed
|
||||
pos token.Pos
|
||||
len int
|
||||
fn *ssa.Function
|
||||
}
|
||||
|
||||
// chanOps returns a slice of all the channel operations in the instruction.
|
||||
// Derived from oracle/peers.go.
|
||||
func chanOps(instr ssa.Instruction) []chanOp {
|
||||
fn := instr.Parent()
|
||||
var ops []chanOp
|
||||
switch instr := instr.(type) {
|
||||
case *ssa.UnOp:
|
||||
if instr.Op == token.ARROW {
|
||||
// TODO(adonovan): don't assume <-ch; could be 'range ch'.
|
||||
ops = append(ops, chanOp{instr.X, "received", instr.Pos(), len("<-"), fn})
|
||||
}
|
||||
case *ssa.Send:
|
||||
ops = append(ops, chanOp{instr.Chan, "sent", instr.Pos(), len("<-"), fn})
|
||||
case *ssa.Select:
|
||||
for _, st := range instr.States {
|
||||
mode := "received"
|
||||
if st.Dir == types.SendOnly {
|
||||
mode = "sent"
|
||||
}
|
||||
ops = append(ops, chanOp{st.Chan, mode, st.Pos, len("<-"), fn})
|
||||
}
|
||||
case ssa.CallInstruction:
|
||||
call := instr.Common()
|
||||
if blt, ok := call.Value.(*ssa.Builtin); ok && blt.Name() == "close" {
|
||||
pos := instr.Common().Pos()
|
||||
ops = append(ops, chanOp{call.Args[0], "closed", pos - token.Pos(len("close")), len("close("), fn})
|
||||
}
|
||||
}
|
||||
return ops
|
||||
}
|
238
godoc/analysis/typeinfo.go
Normal file
@ -0,0 +1,238 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package analysis
|
||||
|
||||
// This file computes the markup for information from go/types:
|
||||
// IMPORTS, identifier RESOLUTION, METHOD SETS, size/alignment, and
|
||||
// the IMPLEMENTS relation.
|
||||
//
|
||||
// IMPORTS links connect import specs to the documentation for the
|
||||
// imported package.
|
||||
//
|
||||
// RESOLUTION links referring identifiers to their defining
|
||||
// identifier, and adds tooltips for kind and type.
|
||||
//
|
||||
// METHOD SETS, size/alignment, and the IMPLEMENTS relation are
|
||||
// displayed in the lower pane when a type's defining identifier is
|
||||
// clicked.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.google.com/p/go.tools/go/loader"
|
||||
"code.google.com/p/go.tools/go/types"
|
||||
"code.google.com/p/go.tools/go/types/typeutil"
|
||||
)
|
||||
|
||||
// TODO(adonovan): audit to make sure it's safe on ill-typed packages.
|
||||
|
||||
// TODO(adonovan): use same Sizes as loader.Config.
|
||||
var sizes = types.StdSizes{8, 8}
|
||||
|
||||
func (a *analysis) doTypeInfo(info *loader.PackageInfo, implements map[*types.Named]implementsFacts) {
|
||||
// We must not assume the corresponding SSA packages were
|
||||
// created (i.e. were transitively error-free).
|
||||
|
||||
// IMPORTS
|
||||
for _, f := range info.Files {
|
||||
// Package decl.
|
||||
fi, offset := a.fileAndOffset(f.Name.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len(f.Name.Name),
|
||||
title: "Package docs for " + info.Pkg.Path(),
|
||||
// TODO(adonovan): fix: we're putting the untrusted Path()
|
||||
// into a trusted field. What's the appropriate sanitizer?
|
||||
href: "/pkg/" + info.Pkg.Path(),
|
||||
})
|
||||
|
||||
// Import specs.
|
||||
for _, imp := range f.Imports {
|
||||
// Remove quotes.
|
||||
L := int(imp.End()-imp.Path.Pos()) - len(`""`)
|
||||
path, _ := strconv.Unquote(imp.Path.Value)
|
||||
fi, offset := a.fileAndOffset(imp.Path.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset + 1,
|
||||
end: offset + 1 + L,
|
||||
title: "Package docs for " + path,
|
||||
// TODO(adonovan): fix: we're putting the untrusted path
|
||||
// into a trusted field. What's the appropriate sanitizer?
|
||||
href: "/pkg/" + path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RESOLUTION
|
||||
for id, obj := range info.Uses {
|
||||
// Position of the object definition.
|
||||
pos := obj.Pos()
|
||||
Len := len(obj.Name())
|
||||
|
||||
// Correct the position for non-renaming import specs.
|
||||
// import "sync/atomic"
|
||||
// ^^^^^^^^^^^
|
||||
if obj, ok := obj.(*types.PkgName); ok && id.Name == obj.Pkg().Name() {
|
||||
// Assume this is a non-renaming import.
|
||||
// NB: not true for degenerate renamings: `import foo "foo"`.
|
||||
pos++
|
||||
Len = len(obj.Pkg().Path())
|
||||
}
|
||||
|
||||
if obj.Pkg() == nil {
|
||||
continue // don't mark up built-ins.
|
||||
}
|
||||
|
||||
fi, offset := a.fileAndOffset(id.NamePos)
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len(id.Name),
|
||||
title: types.ObjectString(info.Pkg, obj),
|
||||
href: a.posURL(pos, Len),
|
||||
})
|
||||
}
|
||||
|
||||
// IMPLEMENTS & METHOD SETS
|
||||
for _, obj := range info.Defs {
|
||||
if obj, ok := obj.(*types.TypeName); ok {
|
||||
a.namedType(obj, implements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analysis) namedType(obj *types.TypeName, implements map[*types.Named]implementsFacts) {
|
||||
this := obj.Pkg()
|
||||
T := obj.Type().(*types.Named)
|
||||
v := &TypeInfoJSON{
|
||||
Name: obj.Name(),
|
||||
Size: sizes.Sizeof(T),
|
||||
Align: sizes.Alignof(T),
|
||||
Methods: []anchorJSON{}, // (JS wants non-nil)
|
||||
}
|
||||
|
||||
// addFact adds the fact "is implemented by T" (by) or
|
||||
// "implements T" (!by) to group.
|
||||
addFact := func(group *implGroupJSON, T types.Type, by bool) {
|
||||
Tobj := deref(T).(*types.Named).Obj()
|
||||
var byKind string
|
||||
if by {
|
||||
// Show underlying kind of implementing type,
|
||||
// e.g. "slice", "array", "struct".
|
||||
s := reflect.TypeOf(T.Underlying()).String()
|
||||
byKind = strings.ToLower(strings.TrimPrefix(s, "*types."))
|
||||
}
|
||||
group.Facts = append(group.Facts, implFactJSON{
|
||||
ByKind: byKind,
|
||||
Other: anchorJSON{
|
||||
Href: a.posURL(Tobj.Pos(), len(Tobj.Name())),
|
||||
Text: types.TypeString(this, T),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// IMPLEMENTS
|
||||
if r, ok := implements[T]; ok {
|
||||
if isInterface(T) {
|
||||
// "T is implemented by <conc>" ...
|
||||
// "T is implemented by <iface>"...
|
||||
// "T implements <iface>"...
|
||||
group := implGroupJSON{
|
||||
Descr: types.TypeString(this, T),
|
||||
}
|
||||
// Show concrete types first; use two passes.
|
||||
for _, sub := range r.to {
|
||||
if !isInterface(sub) {
|
||||
addFact(&group, sub, true)
|
||||
}
|
||||
}
|
||||
for _, sub := range r.to {
|
||||
if isInterface(sub) {
|
||||
addFact(&group, sub, true)
|
||||
}
|
||||
}
|
||||
for _, super := range r.from {
|
||||
addFact(&group, super, false)
|
||||
}
|
||||
v.ImplGroups = append(v.ImplGroups, group)
|
||||
} else {
|
||||
// T is concrete.
|
||||
if r.from != nil {
|
||||
// "T implements <iface>"...
|
||||
group := implGroupJSON{
|
||||
Descr: types.TypeString(this, T),
|
||||
}
|
||||
for _, super := range r.from {
|
||||
addFact(&group, super, false)
|
||||
}
|
||||
v.ImplGroups = append(v.ImplGroups, group)
|
||||
}
|
||||
if r.fromPtr != nil {
|
||||
// "*C implements <iface>"...
|
||||
group := implGroupJSON{
|
||||
Descr: "*" + types.TypeString(this, T),
|
||||
}
|
||||
for _, psuper := range r.fromPtr {
|
||||
addFact(&group, psuper, false)
|
||||
}
|
||||
v.ImplGroups = append(v.ImplGroups, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// METHOD SETS
|
||||
for _, sel := range typeutil.IntuitiveMethodSet(T, &a.prog.MethodSets) {
|
||||
meth := sel.Obj().(*types.Func)
|
||||
pos := meth.Pos() // may be 0 for error.Error
|
||||
v.Methods = append(v.Methods, anchorJSON{
|
||||
Href: a.posURL(pos, len(meth.Name())),
|
||||
Text: types.SelectionString(this, sel),
|
||||
})
|
||||
}
|
||||
|
||||
// Since there can be many specs per decl, we
|
||||
// can't attach the link to the keyword 'type'
|
||||
// (as we do with 'func'); we use the Ident.
|
||||
fi, offset := a.fileAndOffset(obj.Pos())
|
||||
fi.addLink(aLink{
|
||||
start: offset,
|
||||
end: offset + len(obj.Name()),
|
||||
title: fmt.Sprintf("type info for %s", obj.Name()),
|
||||
onclick: fmt.Sprintf("onClickTypeInfo(%d)", fi.addData(v)),
|
||||
})
|
||||
|
||||
// Add info for exported package-level types to the package info.
|
||||
if obj.Exported() && isPackageLevel(obj) {
|
||||
// TODO(adonovan): this.Path() is not unique!
|
||||
// It is possible to declare a non-test package called x_test.
|
||||
a.result.pkgInfo(this.Path()).addType(v)
|
||||
}
|
||||
}
|
||||
|
||||
// -- utilities --------------------------------------------------------
|
||||
|
||||
func isInterface(T types.Type) bool {
|
||||
_, isI := T.Underlying().(*types.Interface)
|
||||
return isI
|
||||
}
|
||||
|
||||
// deref returns a pointer's element type; otherwise it returns typ.
|
||||
func deref(typ types.Type) types.Type {
|
||||
if p, ok := typ.Underlying().(*types.Pointer); ok {
|
||||
return p.Elem()
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
// isPackageLevel reports whether obj is a package-level object.
|
||||
func isPackageLevel(obj types.Object) bool {
|
||||
// TODO(adonovan): fix go/types bug:
|
||||
// obj.Parent().Parent() == obj.Pkg().Scope()
|
||||
// doesn't work because obj.Parent() gets mutated during
|
||||
// dot-imports.
|
||||
return obj.Pkg().Scope().Lookup(obj.Name()) == obj
|
||||
}
|
@ -9,6 +9,7 @@ import (
|
||||
pathpkg "path"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/go.tools/godoc/analysis"
|
||||
"code.google.com/p/go.tools/godoc/util"
|
||||
"code.google.com/p/go.tools/godoc/vfs"
|
||||
)
|
||||
@ -99,6 +100,9 @@ type Corpus struct {
|
||||
|
||||
// SearchIndex is the search index in use.
|
||||
searchIndex util.RWValue
|
||||
|
||||
// Analysis is the result of type and pointer analysis.
|
||||
Analysis analysis.Result
|
||||
}
|
||||
|
||||
// NewCorpus returns a new Corpus from a filesystem.
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"go/format"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
htmltemplate "html/template"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
@ -89,6 +90,11 @@ func (p *Presentation) initFuncMap() {
|
||||
"example_name": p.example_nameFunc,
|
||||
"example_suffix": p.example_suffixFunc,
|
||||
|
||||
// formatting of analysis information
|
||||
"callgraph_html": p.callgraph_htmlFunc,
|
||||
"implements_html": p.implements_htmlFunc,
|
||||
"methodset_html": p.methodset_htmlFunc,
|
||||
|
||||
// formatting of Notes
|
||||
"noteTitle": noteTitle,
|
||||
}
|
||||
@ -235,6 +241,12 @@ type PageInfo struct {
|
||||
IsMain bool // true for package main
|
||||
IsFiltered bool // true if results were filtered
|
||||
|
||||
// analysis info
|
||||
TypeInfoIndex map[string]int // index of JSON datum for type T (if -analysis=type)
|
||||
AnalysisData htmltemplate.JS // array of TypeInfoJSON values
|
||||
CallGraph htmltemplate.JS // array of PCGNodeJSON values (if -analysis=pointer)
|
||||
CallGraphIndex map[string]int // maps func name to index in CallGraph
|
||||
|
||||
// directory info
|
||||
Dirs *DirList // nil if no directory information
|
||||
DirTime time.Time // directory time stamp
|
||||
@ -456,6 +468,64 @@ func (p *Presentation) example_suffixFunc(name string) string {
|
||||
return suffix
|
||||
}
|
||||
|
||||
// implements_html returns the "> Implements" toggle for a package-level named type.
|
||||
// Its contents are populated from JSON data by client-side JS at load time.
|
||||
func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string {
|
||||
if p.ImplementsHTML == nil {
|
||||
return ""
|
||||
}
|
||||
index, ok := info.TypeInfoIndex[typeName]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// methodset_html returns the "> Method set" toggle for a package-level named type.
|
||||
// Its contents are populated from JSON data by client-side JS at load time.
|
||||
func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string {
|
||||
if p.MethodSetHTML == nil {
|
||||
return ""
|
||||
}
|
||||
index, ok := info.TypeInfoIndex[typeName]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// callgraph_html returns the "> Call graph" toggle for a package-level func.
|
||||
// Its contents are populated from JSON data by client-side JS at load time.
|
||||
func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string {
|
||||
if p.CallGraphHTML == nil {
|
||||
return ""
|
||||
}
|
||||
if recv != "" {
|
||||
// Format must match (*ssa.Function).RelString().
|
||||
name = fmt.Sprintf("(%s).%s", recv, name)
|
||||
}
|
||||
index, ok := info.CallGraphIndex[name]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index})
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func noteTitle(note string) string {
|
||||
return strings.Title(strings.ToLower(note))
|
||||
}
|
||||
|
@ -25,10 +25,13 @@ type Presentation struct {
|
||||
cmdHandler handlerServer
|
||||
pkgHandler handlerServer
|
||||
|
||||
CallGraphHTML,
|
||||
DirlistHTML,
|
||||
ErrorHTML,
|
||||
ExampleHTML,
|
||||
GodocHTML,
|
||||
ImplementsHTML,
|
||||
MethodSetHTML,
|
||||
PackageHTML,
|
||||
PackageText,
|
||||
SearchHTML,
|
||||
|
@ -6,6 +6,7 @@ package godoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
@ -13,6 +14,7 @@ import (
|
||||
"go/doc"
|
||||
"go/token"
|
||||
htmlpkg "html"
|
||||
htmltemplate "html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@ -25,6 +27,7 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/go.tools/godoc/analysis"
|
||||
"code.google.com/p/go.tools/godoc/util"
|
||||
"code.google.com/p/go.tools/godoc/vfs"
|
||||
)
|
||||
@ -256,6 +259,18 @@ func (h *handlerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
tabtitle = "Commands"
|
||||
}
|
||||
|
||||
// Emit JSON array for type information.
|
||||
// TODO(adonovan): issue a "pending..." message if results not ready.
|
||||
var callGraph []*analysis.PCGNodeJSON
|
||||
var typeInfos []*analysis.TypeInfoJSON
|
||||
callGraph, info.CallGraphIndex, typeInfos = h.c.Analysis.PackageInfo(relpath)
|
||||
info.CallGraph = htmltemplate.JS(marshalJSON(callGraph))
|
||||
info.AnalysisData = htmltemplate.JS(marshalJSON(typeInfos))
|
||||
info.TypeInfoIndex = make(map[string]int)
|
||||
for i, ti := range typeInfos {
|
||||
info.TypeInfoIndex[ti.Name] = i
|
||||
}
|
||||
|
||||
h.p.ServePage(w, Page{
|
||||
Title: title,
|
||||
Tabtitle: tabtitle,
|
||||
@ -480,10 +495,25 @@ func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abs
|
||||
return
|
||||
}
|
||||
|
||||
h := r.FormValue("h")
|
||||
s := RangeSelection(r.FormValue("s"))
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("<pre>")
|
||||
FormatText(&buf, src, 1, pathpkg.Ext(abspath) == ".go", r.FormValue("h"), RangeSelection(r.FormValue("s")))
|
||||
buf.WriteString("</pre>")
|
||||
if pathpkg.Ext(abspath) == ".go" {
|
||||
// Find markup links for this file (e.g. "/src/pkg/fmt/print.go").
|
||||
data, links := p.Corpus.Analysis.FileInfo(abspath)
|
||||
buf.WriteString("<script type='text/javascript'>document.ANALYSIS_DATA = ")
|
||||
buf.Write(marshalJSON(data))
|
||||
buf.WriteString(";</script>\n")
|
||||
|
||||
buf.WriteString("<pre>")
|
||||
formatGoSource(&buf, src, links, h, s)
|
||||
buf.WriteString("</pre>")
|
||||
} else {
|
||||
buf.WriteString("<pre>")
|
||||
FormatText(&buf, src, 1, false, h, s)
|
||||
buf.WriteString("</pre>")
|
||||
}
|
||||
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, htmlpkg.EscapeString(relpath))
|
||||
|
||||
p.ServePage(w, Page{
|
||||
@ -493,6 +523,33 @@ func (p *Presentation) serveTextFile(w http.ResponseWriter, r *http.Request, abs
|
||||
})
|
||||
}
|
||||
|
||||
// formatGoSource HTML-escapes Go source text and writes it to w,
|
||||
// decorating it with the specified analysis links.
|
||||
//
|
||||
func formatGoSource(buf *bytes.Buffer, text []byte, links []analysis.Link, pattern string, selection Selection) {
|
||||
var i int
|
||||
var link analysis.Link // shared state of the two funcs below
|
||||
segmentIter := func() (seg Segment) {
|
||||
if i < len(links) {
|
||||
link = links[i]
|
||||
i++
|
||||
seg = Segment{link.Start(), link.End()}
|
||||
}
|
||||
return
|
||||
}
|
||||
linkWriter := func(w io.Writer, offs int, start bool) {
|
||||
link.Write(w, offs, start)
|
||||
}
|
||||
|
||||
comments := tokenSelection(text, token.COMMENT)
|
||||
var highlights Selection
|
||||
if pattern != "" {
|
||||
highlights = regexpSelection(text, pattern)
|
||||
}
|
||||
|
||||
FormatSelections(buf, text, linkWriter, segmentIter, selectionTag, comments, highlights, selection)
|
||||
}
|
||||
|
||||
func (p *Presentation) serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||
if redirect(w, r) {
|
||||
return
|
||||
@ -635,3 +692,18 @@ func (p *Presentation) ServeText(w http.ResponseWriter, text []byte) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Write(text)
|
||||
}
|
||||
|
||||
func marshalJSON(x interface{}) []byte {
|
||||
var data []byte
|
||||
var err error
|
||||
const indentJSON = false // for easier debugging
|
||||
if indentJSON {
|
||||
data, err = json.MarshalIndent(x, "", " ")
|
||||
} else {
|
||||
data, err = json.Marshal(x)
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("json.Marshal failed: %s", err))
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
@ -40,16 +39,19 @@ func bake(files []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !utf8.Valid(b) {
|
||||
return fmt.Errorf("file %s is not valid UTF-8", fn)
|
||||
fmt.Fprintf(w, "\t%q: ", fn)
|
||||
if utf8.Valid(b) {
|
||||
fmt.Fprintf(w, "`%s`", sanitize(b))
|
||||
} else {
|
||||
fmt.Fprintf(w, "%q", b)
|
||||
}
|
||||
fmt.Fprintf(w, "\t%q: `%s`,\n", filepath.Base(fn), sanitize(b))
|
||||
fmt.Fprintln(w, ",\n")
|
||||
}
|
||||
fmt.Fprintln(w, "}")
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
// sanitize prepares a string as a raw string constant.
|
||||
// sanitize prepares a valid UTF-8 string as a raw string constant.
|
||||
func sanitize(b []byte) []byte {
|
||||
// Replace ` with `+"`"+`
|
||||
b = bytes.Replace(b, []byte("`"), []byte("`+\"`\"+`"), -1)
|
||||
|
@ -6,6 +6,7 @@
|
||||
set -e
|
||||
|
||||
STATIC="
|
||||
callgraph.html
|
||||
codewalk.html
|
||||
codewalkdir.html
|
||||
dirlist.html
|
||||
@ -13,7 +14,20 @@ STATIC="
|
||||
example.html
|
||||
godoc.html
|
||||
godocs.js
|
||||
images/minus.gif
|
||||
images/plus.gif
|
||||
images/treeview-black-line.gif
|
||||
images/treeview-black.gif
|
||||
images/treeview-default-line.gif
|
||||
images/treeview-default.gif
|
||||
images/treeview-gray-line.gif
|
||||
images/treeview-gray.gif
|
||||
implements.html
|
||||
jquery.js
|
||||
jquery.treeview.css
|
||||
jquery.treeview.edit.js
|
||||
jquery.treeview.js
|
||||
methodset.html
|
||||
opensearch.xml
|
||||
package.html
|
||||
package.txt
|
||||
|
15
godoc/static/callgraph.html
Normal file
@ -0,0 +1,15 @@
|
||||
<div class="toggle" style="display: none">
|
||||
<div class="collapsed">
|
||||
<p class="exampleHeading toggleButton">▹ <span class="text">Internal call graph</span></p>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<p class="exampleHeading toggleButton">▾ <span class="text">Internal call graph</span></p>
|
||||
<p>
|
||||
This viewer shows the portion of the internal call
|
||||
graph of this package that is reachable from this function.
|
||||
See the <a href='#pkg-callgraph'>package's call
|
||||
graph</a> for more information.
|
||||
</p>
|
||||
<ul style="margin-left: 0.5in" id="callgraph-{{.Index}}" class="treeview"></ul>
|
||||
</div>
|
||||
</div>
|
@ -11,10 +11,12 @@
|
||||
{{if .SearchBox}}
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="godoc" href="/opensearch.xml" />
|
||||
{{end}}
|
||||
<link rel="stylesheet" href="/lib/godoc/jquery.treeview.css">
|
||||
<script type="text/javascript">window.initFuncs = [];</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="mainframe" style="position: fixed; bottom: 0; top:0; overflow: auto; width: 100%;">
|
||||
<div id="topbar"{{if .Title}} class="wide"{{end}}><div class="container">
|
||||
|
||||
<form method="GET" action="/search">
|
||||
@ -83,7 +85,16 @@ and code is licensed under a <a href="/LICENSE">BSD license</a>.<br>
|
||||
</div><!-- .container -->
|
||||
</div><!-- #page -->
|
||||
|
||||
</div><!-- #mainframe -->
|
||||
<div id='lowframe' style="position: absolute; bottom: 0; left: 0; height: 0; width: 100%; border-top: thin solid grey; background-color: white; overflow: auto;">
|
||||
...
|
||||
</div><!-- #lowframe -->
|
||||
|
||||
<!-- TODO(adonovan): load these from <head> using "defer" attribute? -->
|
||||
<script type="text/javascript" src="/lib/godoc/jquery.js"></script>
|
||||
<script type="text/javascript" src="/lib/godoc/jquery.treeview.js"></script>
|
||||
<script type="text/javascript" src="/lib/godoc/jquery.treeview.edit.js"></script>
|
||||
|
||||
{{if .Playground}}
|
||||
<script type="text/javascript" src="/lib/godoc/playground.js"></script>
|
||||
{{end}}
|
||||
|
@ -253,6 +253,8 @@ $(document).ready(function() {
|
||||
setupDropdownPlayground();
|
||||
setupInlinePlayground();
|
||||
fixFocus();
|
||||
setupTypeInfo();
|
||||
setupCallgraphs();
|
||||
toggleHash();
|
||||
addPlusButtons();
|
||||
|
||||
@ -263,4 +265,249 @@ $(document).ready(function() {
|
||||
for (var i = 0; i < window.initFuncs.length; i++) window.initFuncs[i]();
|
||||
});
|
||||
|
||||
// -- analysis ---------------------------------------------------------
|
||||
|
||||
// escapeHTML returns HTML for s, with metacharacters quoted.
|
||||
// It is safe for use in both elements and attributes
|
||||
// (unlike the "set innerText, read innerHTML" trick).
|
||||
function escapeHTML(s) {
|
||||
return s.replace(/&/g, '&').
|
||||
replace(/\"/g, '"').
|
||||
replace(/\'/g, ''').
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// makeAnchor returns HTML for an <a> element, given an anchorJSON object.
|
||||
function makeAnchor(json) {
|
||||
var html = escapeHTML(json.Text);
|
||||
if (json.Href != "") {
|
||||
html = "<a href='" + escapeHTML(json.Href) + "'>" + html + "</a>";
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function showLowFrame(html) {
|
||||
var lowframe = document.getElementById('lowframe');
|
||||
lowframe.style.height = "200px";
|
||||
lowframe.innerHTML = "<p style='text-align: left;'>" + html + "</p>\n" +
|
||||
"<div onclick='hideLowFrame()' style='position: absolute; top: 0; right: 0; cursor: pointer;'>✘</div>"
|
||||
};
|
||||
|
||||
document.hideLowFrame = function() {
|
||||
var lowframe = document.getElementById('lowframe');
|
||||
lowframe.style.height = "0px";
|
||||
}
|
||||
|
||||
// onClickCallers is the onclick action for the 'func' tokens of a
|
||||
// function declaration.
|
||||
document.onClickCallers = function(index) {
|
||||
var data = document.ANALYSIS_DATA[index]
|
||||
if (data.Callers.length == 1 && data.Callers[0].Sites.length == 1) {
|
||||
document.location = data.Callers[0].Sites[0].Href; // jump to sole caller
|
||||
return;
|
||||
}
|
||||
|
||||
var html = "Callers of <code>" + escapeHTML(data.Callee) + "</code>:<br/>\n";
|
||||
for (var i = 0; i < data.Callers.length; i++) {
|
||||
var caller = data.Callers[i];
|
||||
html += "<code>" + escapeHTML(caller.Func) + "</code>";
|
||||
var sites = caller.Sites;
|
||||
if (sites != null && sites.length > 0) {
|
||||
html += " at line ";
|
||||
for (var j = 0; j < sites.length; j++) {
|
||||
if (j > 0) {
|
||||
html += ", ";
|
||||
}
|
||||
html += "<code>" + makeAnchor(sites[j]) + "</code>";
|
||||
}
|
||||
}
|
||||
html += "<br/>\n";
|
||||
}
|
||||
showLowFrame(html);
|
||||
};
|
||||
|
||||
// onClickCallees is the onclick action for the '(' token of a function call.
|
||||
document.onClickCallees = function(index) {
|
||||
var data = document.ANALYSIS_DATA[index]
|
||||
if (data.Callees.length == 1) {
|
||||
document.location = data.Callees[0].Href; // jump to sole callee
|
||||
return;
|
||||
}
|
||||
|
||||
var html = "Callees of this " + escapeHTML(data.Descr) + ":<br/>\n";
|
||||
for (var i = 0; i < data.Callees.length; i++) {
|
||||
html += "<code>" + makeAnchor(data.Callees[i]) + "</code><br/>\n";
|
||||
}
|
||||
showLowFrame(html);
|
||||
};
|
||||
|
||||
// onClickTypeInfo is the onclick action for identifiers declaring a named type.
|
||||
document.onClickTypeInfo = function(index) {
|
||||
var data = document.ANALYSIS_DATA[index];
|
||||
var html = "Type <code>" + data.Name + "</code>: " +
|
||||
" <small>(size=" + data.Size + ", align=" + data.Align + ")</small><br/>\n";
|
||||
html += implementsHTML(data);
|
||||
html += methodsetHTML(data);
|
||||
showLowFrame(html);
|
||||
};
|
||||
|
||||
// implementsHTML returns HTML for the implements relation of the
|
||||
// specified TypeInfoJSON value.
|
||||
function implementsHTML(info) {
|
||||
var html = "";
|
||||
if (info.ImplGroups != null) {
|
||||
for (var i = 0; i < info.ImplGroups.length; i++) {
|
||||
var group = info.ImplGroups[i];
|
||||
var x = "<code>" + escapeHTML(group.Descr) + "</code> ";
|
||||
for (var j = 0; j < group.Facts.length; j++) {
|
||||
var fact = group.Facts[j];
|
||||
var y = "<code>" + makeAnchor(fact.Other) + "</code>";
|
||||
if (fact.ByKind != null) {
|
||||
html += escapeHTML(fact.ByKind) + " type " + y + " implements " + x;
|
||||
} else {
|
||||
html += x + " implements " + y;
|
||||
}
|
||||
html += "<br/>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
// methodsetHTML returns HTML for the methodset of the specified
|
||||
// TypeInfoJSON value.
|
||||
function methodsetHTML(info) {
|
||||
var html = "";
|
||||
if (info.Methods != null) {
|
||||
for (var i = 0; i < info.Methods.length; i++) {
|
||||
html += "<code>" + makeAnchor(info.Methods[i]) + "</code><br/>\n";
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// onClickComm is the onclick action for channel "make" and "<-"
|
||||
// send/receive tokens.
|
||||
document.onClickComm = function(index) {
|
||||
var ops = document.ANALYSIS_DATA[index].Ops
|
||||
if (ops.length == 1) {
|
||||
document.location = ops[0].Op.Href; // jump to sole element
|
||||
return;
|
||||
}
|
||||
|
||||
var html = "Operations on this channel:<br/>\n";
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
html += makeAnchor(ops[i].Op) + " by <code>" + escapeHTML(ops[i].Fn) + "</code><br/>\n";
|
||||
}
|
||||
if (ops.length == 0) {
|
||||
html += "(none)<br/>\n";
|
||||
}
|
||||
showLowFrame(html);
|
||||
};
|
||||
|
||||
$(window).load(function() {
|
||||
// Scroll window so that first selection is visible.
|
||||
// (This means we don't need to emit id='L%d' spans for each line.)
|
||||
// TODO(adonovan): ideally, scroll it so that it's under the pointer,
|
||||
// but I don't know how to get the pointer y coordinate.
|
||||
var elts = document.getElementsByClassName("selection");
|
||||
if (elts.length > 0) {
|
||||
elts[0].scrollIntoView()
|
||||
}
|
||||
});
|
||||
|
||||
// setupTypeInfo populates the "Implements" and "Method set" toggle for
|
||||
// each type in the package doc.
|
||||
function setupTypeInfo() {
|
||||
for (var i in document.ANALYSIS_DATA) {
|
||||
var data = document.ANALYSIS_DATA[i];
|
||||
|
||||
var el = document.getElementById("implements-" + i);
|
||||
if (el != null) {
|
||||
// el != null => data is TypeInfoJSON.
|
||||
if (data.ImplGroups != null) {
|
||||
el.innerHTML = implementsHTML(data);
|
||||
el.parentNode.parentNode.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
var el = document.getElementById("methodset-" + i);
|
||||
if (el != null) {
|
||||
// el != null => data is TypeInfoJSON.
|
||||
if (data.Methods != null) {
|
||||
el.innerHTML = methodsetHTML(data);
|
||||
el.parentNode.parentNode.style.display = "block";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupCallgraphs() {
|
||||
if (document.CALLGRAPH == null) {
|
||||
return
|
||||
}
|
||||
var treeviews = document.getElementsByClassName("treeview");
|
||||
for (var i in treeviews) {
|
||||
var tree = treeviews[i];
|
||||
if (tree.id == null || tree.id.indexOf("callgraph-") != 0) {
|
||||
continue;
|
||||
}
|
||||
var id = tree.id.substring("callgraph-".length);
|
||||
$(tree).treeview({collapsed: true, animated: "fast"});
|
||||
document.cgAddChildren(tree, tree, [id]);
|
||||
tree.parentNode.parentNode.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
document.cgAddChildren = function(tree, ul, indices) {
|
||||
if (indices != null) {
|
||||
for (var i = 0; i < indices.length; i++) {
|
||||
var li = cgAddChild(tree, ul, document.CALLGRAPH[indices[i]]);
|
||||
if (i == indices.length - 1) {
|
||||
$(li).addClass("last");
|
||||
}
|
||||
}
|
||||
}
|
||||
$(tree).treeview({animated: "fast", add: ul});
|
||||
}
|
||||
|
||||
// cgAddChild adds an <li> element for document.CALLGRAPH node cgn to
|
||||
// the parent <ul> element ul. tree is the tree's root <ul> element.
|
||||
function cgAddChild(tree, ul, cgn) {
|
||||
var li = document.createElement("li");
|
||||
ul.appendChild(li);
|
||||
li.className = "closed";
|
||||
|
||||
var code = document.createElement("code");
|
||||
|
||||
if (cgn.Callees != null) {
|
||||
$(li).addClass("expandable");
|
||||
|
||||
// Event handlers and innerHTML updates don't play nicely together,
|
||||
// hence all this explicit DOM manipulation.
|
||||
var hitarea = document.createElement("div");
|
||||
hitarea.className = "hitarea expandable-hitarea";
|
||||
li.appendChild(hitarea);
|
||||
|
||||
li.appendChild(code);
|
||||
|
||||
var childUL = document.createElement("ul");
|
||||
li.appendChild(childUL);
|
||||
childUL.setAttribute('style', "display: none;");
|
||||
|
||||
var onClick = function() {
|
||||
document.cgAddChildren(tree, childUL, cgn.Callees);
|
||||
hitarea.removeEventListener('click', onClick)
|
||||
};
|
||||
hitarea.addEventListener('click', onClick);
|
||||
|
||||
} else {
|
||||
li.appendChild(code);
|
||||
}
|
||||
code.innerHTML += " " + makeAnchor(cgn.Func);
|
||||
return li
|
||||
}
|
||||
|
||||
})();
|
||||
|
BIN
godoc/static/images/minus.gif
Normal file
After Width: | Height: | Size: 837 B |
BIN
godoc/static/images/plus.gif
Normal file
After Width: | Height: | Size: 841 B |
BIN
godoc/static/images/treeview-black-line.gif
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
godoc/static/images/treeview-black.gif
Normal file
After Width: | Height: | Size: 402 B |
BIN
godoc/static/images/treeview-default-line.gif
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
godoc/static/images/treeview-default.gif
Normal file
After Width: | Height: | Size: 400 B |
BIN
godoc/static/images/treeview-gray-line.gif
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
godoc/static/images/treeview-gray.gif
Normal file
After Width: | Height: | Size: 411 B |
9
godoc/static/implements.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="toggle" style="display: none">
|
||||
<div class="collapsed">
|
||||
<p class="exampleHeading toggleButton">▹ <span class="text">Implements</span></p>
|
||||
</div>
|
||||
<div class="expanded">
|
||||
<p class="exampleHeading toggleButton">▾ <span class="text">Implements</span></p>
|
||||
<div style="margin-left: 1in" id='implements-{{.Index}}'>...</div>
|
||||
</div>
|
||||
</div>
|
76
godoc/static/jquery.treeview.css
Normal file
@ -0,0 +1,76 @@
|
||||
/* https://github.com/jzaefferer/jquery-treeview/blob/master/jquery.treeview.css */
|
||||
/* License: MIT. */
|
||||
.treeview, .treeview ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.treeview ul {
|
||||
background-color: white;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.treeview .hitarea {
|
||||
background: url(images/treeview-default.gif) -64px -25px no-repeat;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-left: -16px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* fix for IE6 */
|
||||
* html .hitarea {
|
||||
display: inline;
|
||||
float:none;
|
||||
}
|
||||
|
||||
.treeview li {
|
||||
margin: 0;
|
||||
padding: 3px 0pt 3px 16px;
|
||||
}
|
||||
|
||||
.treeview a.selected {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#treecontrol { margin: 1em 0; display: none; }
|
||||
|
||||
.treeview .hover { color: red; cursor: pointer; }
|
||||
|
||||
.treeview li { background: url(images/treeview-default-line.gif) 0 0 no-repeat; }
|
||||
.treeview li.collapsable, .treeview li.expandable { background-position: 0 -176px; }
|
||||
|
||||
.treeview .expandable-hitarea { background-position: -80px -3px; }
|
||||
|
||||
.treeview li.last { background-position: 0 -1766px }
|
||||
.treeview li.lastCollapsable, .treeview li.lastExpandable { background-image: url(images/treeview-default.gif); }
|
||||
.treeview li.lastCollapsable { background-position: 0 -111px }
|
||||
.treeview li.lastExpandable { background-position: -32px -67px }
|
||||
|
||||
.treeview div.lastCollapsable-hitarea, .treeview div.lastExpandable-hitarea { background-position: 0; }
|
||||
|
||||
.treeview-red li { background-image: url(images/treeview-red-line.gif); }
|
||||
.treeview-red .hitarea, .treeview-red li.lastCollapsable, .treeview-red li.lastExpandable { background-image: url(images/treeview-red.gif); }
|
||||
|
||||
.treeview-black li { background-image: url(images/treeview-black-line.gif); }
|
||||
.treeview-black .hitarea, .treeview-black li.lastCollapsable, .treeview-black li.lastExpandable { background-image: url(images/treeview-black.gif); }
|
||||
|
||||
.treeview-gray li { background-image: url(images/treeview-gray-line.gif); }
|
||||
.treeview-gray .hitarea, .treeview-gray li.lastCollapsable, .treeview-gray li.lastExpandable { background-image: url(images/treeview-gray.gif); }
|
||||
|
||||
.treeview-famfamfam li { background-image: url(images/treeview-famfamfam-line.gif); }
|
||||
.treeview-famfamfam .hitarea, .treeview-famfamfam li.lastCollapsable, .treeview-famfamfam li.lastExpandable { background-image: url(images/treeview-famfamfam.gif); }
|
||||
|
||||
.treeview .placeholder {
|
||||
background: url(images/ajax-loader.gif) 0 0 no-repeat;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filetree li { padding: 3px 0 2px 16px; }
|
||||
.filetree span.folder, .filetree span.file { padding: 1px 0 1px 16px; display: block; }
|
||||
.filetree span.folder { background: url(images/folder.gif) 0 0 no-repeat; }
|
||||
.filetree li.expandable span.folder { background: url(images/folder-closed.gif) 0 0 no-repeat; }
|
||||
.filetree span.file { background: url(images/file.gif) 0 0 no-repeat; }
|
39
godoc/static/jquery.treeview.edit.js
Normal file
@ -0,0 +1,39 @@
|
||||
/* https://github.com/jzaefferer/jquery-treeview/blob/master/jquery.treeview.edit.js */
|
||||
/* License: MIT. */
|
||||
(function($) {
|
||||
var CLASSES = $.treeview.classes;
|
||||
var proxied = $.fn.treeview;
|
||||
$.fn.treeview = function(settings) {
|
||||
settings = $.extend({}, settings);
|
||||
if (settings.add) {
|
||||
return this.trigger("add", [settings.add]);
|
||||
}
|
||||
if (settings.remove) {
|
||||
return this.trigger("remove", [settings.remove]);
|
||||
}
|
||||
return proxied.apply(this, arguments).bind("add", function(event, branches) {
|
||||
$(branches).prev()
|
||||
.removeClass(CLASSES.last)
|
||||
.removeClass(CLASSES.lastCollapsable)
|
||||
.removeClass(CLASSES.lastExpandable)
|
||||
.find(">.hitarea")
|
||||
.removeClass(CLASSES.lastCollapsableHitarea)
|
||||
.removeClass(CLASSES.lastExpandableHitarea);
|
||||
$(branches).find("li").andSelf().prepareBranches(settings).applyClasses(settings, $(this).data("toggler"));
|
||||
}).bind("remove", function(event, branches) {
|
||||
var prev = $(branches).prev();
|
||||
var parent = $(branches).parent();
|
||||
$(branches).remove();
|
||||
prev.filter(":last-child").addClass(CLASSES.last)
|
||||
.filter("." + CLASSES.expandable).replaceClass(CLASSES.last, CLASSES.lastExpandable).end()
|
||||
.find(">.hitarea").replaceClass(CLASSES.expandableHitarea, CLASSES.lastExpandableHitarea).end()
|
||||
.filter("." + CLASSES.collapsable).replaceClass(CLASSES.last, CLASSES.lastCollapsable).end()
|
||||
.find(">.hitarea").replaceClass(CLASSES.collapsableHitarea, CLASSES.lastCollapsableHitarea);
|
||||
if (parent.is(":not(:has(>))") && parent[0] != this) {
|
||||
parent.parent().removeClass(CLASSES.collapsable).removeClass(CLASSES.expandable)
|
||||
parent.siblings(".hitarea").andSelf().remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
})(jQuery);
|
256
godoc/static/jquery.treeview.js
Normal file
@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Treeview 1.4.1 - jQuery plugin to hide and show branches of a tree
|
||||
*
|
||||
* http://bassistance.de/jquery-plugins/jquery-plugin-treeview/
|
||||
* http://docs.jquery.com/Plugins/Treeview
|
||||
*
|
||||
* Copyright (c) 2007 Jörn Zaefferer
|
||||
*
|
||||
* Dual licensed under the MIT and GPL licenses:
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*
|
||||
* Revision: $Id: jquery.treeview.js 5759 2008-07-01 07:50:28Z joern.zaefferer $
|
||||
*
|
||||
*/
|
||||
|
||||
;(function($) {
|
||||
|
||||
// TODO rewrite as a widget, removing all the extra plugins
|
||||
$.extend($.fn, {
|
||||
swapClass: function(c1, c2) {
|
||||
var c1Elements = this.filter('.' + c1);
|
||||
this.filter('.' + c2).removeClass(c2).addClass(c1);
|
||||
c1Elements.removeClass(c1).addClass(c2);
|
||||
return this;
|
||||
},
|
||||
replaceClass: function(c1, c2) {
|
||||
return this.filter('.' + c1).removeClass(c1).addClass(c2).end();
|
||||
},
|
||||
hoverClass: function(className) {
|
||||
className = className || "hover";
|
||||
return this.hover(function() {
|
||||
$(this).addClass(className);
|
||||
}, function() {
|
||||
$(this).removeClass(className);
|
||||
});
|
||||
},
|
||||
heightToggle: function(animated, callback) {
|
||||
animated ?
|
||||
this.animate({ height: "toggle" }, animated, callback) :
|
||||
this.each(function(){
|
||||
jQuery(this)[ jQuery(this).is(":hidden") ? "show" : "hide" ]();
|
||||
if(callback)
|
||||
callback.apply(this, arguments);
|
||||
});
|
||||
},
|
||||
heightHide: function(animated, callback) {
|
||||
if (animated) {
|
||||
this.animate({ height: "hide" }, animated, callback);
|
||||
} else {
|
||||
this.hide();
|
||||
if (callback)
|
||||
this.each(callback);
|
||||
}
|
||||
},
|
||||
prepareBranches: function(settings) {
|
||||
if (!settings.prerendered) {
|
||||
// mark last tree items
|
||||
this.filter(":last-child:not(ul)").addClass(CLASSES.last);
|
||||
// collapse whole tree, or only those marked as closed, anyway except those marked as open
|
||||
this.filter((settings.collapsed ? "" : "." + CLASSES.closed) + ":not(." + CLASSES.open + ")").find(">ul").hide();
|
||||
}
|
||||
// return all items with sublists
|
||||
return this.filter(":has(>ul)");
|
||||
},
|
||||
applyClasses: function(settings, toggler) {
|
||||
// TODO use event delegation
|
||||
this.filter(":has(>ul):not(:has(>a))").find(">span").unbind("click.treeview").bind("click.treeview", function(event) {
|
||||
// don't handle click events on children, eg. checkboxes
|
||||
if ( this == event.target )
|
||||
toggler.apply($(this).next());
|
||||
}).add( $("a", this) ).hoverClass();
|
||||
|
||||
if (!settings.prerendered) {
|
||||
// handle closed ones first
|
||||
this.filter(":has(>ul:hidden)")
|
||||
.addClass(CLASSES.expandable)
|
||||
.replaceClass(CLASSES.last, CLASSES.lastExpandable);
|
||||
|
||||
// handle open ones
|
||||
this.not(":has(>ul:hidden)")
|
||||
.addClass(CLASSES.collapsable)
|
||||
.replaceClass(CLASSES.last, CLASSES.lastCollapsable);
|
||||
|
||||
// create hitarea if not present
|
||||
var hitarea = this.find("div." + CLASSES.hitarea);
|
||||
if (!hitarea.length)
|
||||
hitarea = this.prepend("<div class=\"" + CLASSES.hitarea + "\"/>").find("div." + CLASSES.hitarea);
|
||||
hitarea.removeClass().addClass(CLASSES.hitarea).each(function() {
|
||||
var classes = "";
|
||||
$.each($(this).parent().attr("class").split(" "), function() {
|
||||
classes += this + "-hitarea ";
|
||||
});
|
||||
$(this).addClass( classes );
|
||||
})
|
||||
}
|
||||
|
||||
// apply event to hitarea
|
||||
this.find("div." + CLASSES.hitarea).click( toggler );
|
||||
},
|
||||
treeview: function(settings) {
|
||||
|
||||
settings = $.extend({
|
||||
cookieId: "treeview"
|
||||
}, settings);
|
||||
|
||||
if ( settings.toggle ) {
|
||||
var callback = settings.toggle;
|
||||
settings.toggle = function() {
|
||||
return callback.apply($(this).parent()[0], arguments);
|
||||
};
|
||||
}
|
||||
|
||||
// factory for treecontroller
|
||||
function treeController(tree, control) {
|
||||
// factory for click handlers
|
||||
function handler(filter) {
|
||||
return function() {
|
||||
// reuse toggle event handler, applying the elements to toggle
|
||||
// start searching for all hitareas
|
||||
toggler.apply( $("div." + CLASSES.hitarea, tree).filter(function() {
|
||||
// for plain toggle, no filter is provided, otherwise we need to check the parent element
|
||||
return filter ? $(this).parent("." + filter).length : true;
|
||||
}) );
|
||||
return false;
|
||||
};
|
||||
}
|
||||
// click on first element to collapse tree
|
||||
$("a:eq(0)", control).click( handler(CLASSES.collapsable) );
|
||||
// click on second to expand tree
|
||||
$("a:eq(1)", control).click( handler(CLASSES.expandable) );
|
||||
// click on third to toggle tree
|
||||
$("a:eq(2)", control).click( handler() );
|
||||
}
|
||||
|
||||
// handle toggle event
|
||||
function toggler() {
|
||||
$(this)
|
||||
.parent()
|
||||
// swap classes for hitarea
|
||||
.find(">.hitarea")
|
||||
.swapClass( CLASSES.collapsableHitarea, CLASSES.expandableHitarea )
|
||||
.swapClass( CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea )
|
||||
.end()
|
||||
// swap classes for parent li
|
||||
.swapClass( CLASSES.collapsable, CLASSES.expandable )
|
||||
.swapClass( CLASSES.lastCollapsable, CLASSES.lastExpandable )
|
||||
// find child lists
|
||||
.find( ">ul" )
|
||||
// toggle them
|
||||
.heightToggle( settings.animated, settings.toggle );
|
||||
if ( settings.unique ) {
|
||||
$(this).parent()
|
||||
.siblings()
|
||||
// swap classes for hitarea
|
||||
.find(">.hitarea")
|
||||
.replaceClass( CLASSES.collapsableHitarea, CLASSES.expandableHitarea )
|
||||
.replaceClass( CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea )
|
||||
.end()
|
||||
.replaceClass( CLASSES.collapsable, CLASSES.expandable )
|
||||
.replaceClass( CLASSES.lastCollapsable, CLASSES.lastExpandable )
|
||||
.find( ">ul" )
|
||||
.heightHide( settings.animated, settings.toggle );
|
||||
}
|
||||
}
|
||||
this.data("toggler", toggler);
|
||||
|
||||
function serialize() {
|
||||
function binary(arg) {
|
||||
return arg ? 1 : 0;
|
||||
}
|
||||
var data = [];
|
||||
branches.each(function(i, e) {
|
||||
data[i] = $(e).is(":has(>ul:visible)") ? 1 : 0;
|
||||
});
|
||||
$.cookie(settings.cookieId, data.join(""), settings.cookieOptions );
|
||||
}
|
||||
|
||||
function deserialize() {
|
||||
var stored = $.cookie(settings.cookieId);
|
||||
if ( stored ) {
|
||||
var data = stored.split("");
|
||||
branches.each(function(i, e) {
|
||||
$(e).find(">ul")[ parseInt(data[i]) ? "show" : "hide" ]();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// add treeview class to activate styles
|
||||
this.addClass("treeview");
|
||||
|
||||
// prepare branches and find all tree items with child lists
|
||||
var branches = this.find("li").prepareBranches(settings);
|
||||
|
||||
switch(settings.persist) {
|
||||
case "cookie":
|
||||
var toggleCallback = settings.toggle;
|
||||
settings.toggle = function() {
|
||||
serialize();
|
||||
if (toggleCallback) {
|
||||
toggleCallback.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
deserialize();
|
||||
break;
|
||||
case "location":
|
||||
var current = this.find("a").filter(function() {
|
||||
return this.href.toLowerCase() == location.href.toLowerCase();
|
||||
});
|
||||
if ( current.length ) {
|
||||
// TODO update the open/closed classes
|
||||
var items = current.addClass("selected").parents("ul, li").add( current.next() ).show();
|
||||
if (settings.prerendered) {
|
||||
// if prerendered is on, replicate the basic class swapping
|
||||
items.filter("li")
|
||||
.swapClass( CLASSES.collapsable, CLASSES.expandable )
|
||||
.swapClass( CLASSES.lastCollapsable, CLASSES.lastExpandable )
|
||||
.find(">.hitarea")
|
||||
.swapClass( CLASSES.collapsableHitarea, CLASSES.expandableHitarea )
|
||||
.swapClass( CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea );
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
branches.applyClasses(settings, toggler);
|
||||
|
||||
// if control option is set, create the treecontroller and show it
|
||||
if ( settings.control ) {
|
||||
treeController(this, settings.control);
|
||||
$(settings.control).show();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
// classes used by the plugin
|
||||
// need to be styled via external stylesheet, see first example
|
||||
$.treeview = {};
|
||||
var CLASSES = ($.treeview.classes = {
|
||||
open: "open",
|
||||
closed: "closed",
|
||||
expandable: "expandable",
|
||||
expandableHitarea: "expandable-hitarea",
|
||||
lastExpandableHitarea: "lastExpandable-hitarea",
|
||||
collapsable: "collapsable",
|
||||
collapsableHitarea: "collapsable-hitarea",
|
||||
lastCollapsableHitarea: "lastCollapsable-hitarea",
|
||||
lastCollapsable: "lastCollapsable",
|
||||
lastExpandable: "lastExpandable",
|
||||
last: "last",
|
||||
hitarea: "hitarea"
|
||||
});
|
||||
|
||||
})(jQuery);
|
@ -10,6 +10,11 @@
|
||||
correspond to Go identifiers).
|
||||
-->
|
||||
{{with .PDoc}}
|
||||
<script type='text/javascript'>
|
||||
document.ANALYSIS_DATA = {{$.AnalysisData}};
|
||||
document.CALLGRAPH = {{$.CallGraph}};
|
||||
</script>
|
||||
|
||||
{{if $.IsMain}}
|
||||
{{/* command documentation */}}
|
||||
{{comment_html .Doc}}
|
||||
@ -106,6 +111,44 @@
|
||||
</div><!-- .expanded -->
|
||||
</div><!-- #pkg-index -->
|
||||
|
||||
<div id="pkg-callgraph" class="toggle">
|
||||
<div class="collapsed">
|
||||
<h2 class="toggleButton" title="Click to show Internal Call Graph section">Internal call graph ▹</h2>
|
||||
</div> <!-- .expanded -->
|
||||
<div class="expanded">
|
||||
<h2 class="toggleButton" title="Click to hide Internal Call Graph section">Internal call graph ▾</h2>
|
||||
<p>
|
||||
In the call graph viewer below, each node
|
||||
is a function belonging to this package
|
||||
and its children are the functions it
|
||||
calls—perhaps dynamically.
|
||||
</p>
|
||||
<p>
|
||||
The root nodes are the entry points of the
|
||||
package: functions that may be called from
|
||||
outside the package.
|
||||
There may be non-exported or anonymous
|
||||
functions among them if they are called
|
||||
dynamically from another package.
|
||||
</p>
|
||||
<p>
|
||||
Click a node to visit that function's source code.
|
||||
From there you can visit its callers by
|
||||
clicking its declaring <code>func</code>
|
||||
token.
|
||||
</p>
|
||||
<p>
|
||||
Functions may be omitted if they were
|
||||
determined to be unreachable in the
|
||||
particular programs or tests that were
|
||||
analyzed.
|
||||
</p>
|
||||
<!-- Zero means show all package entry points. -->
|
||||
<ul style="margin-left: 0.5in" id="callgraph-0" class="treeview"></ul>
|
||||
</script>
|
||||
</div>
|
||||
</div> <!-- #pkg-callgraph -->
|
||||
|
||||
{{with .Consts}}
|
||||
<h2 id="pkg-constants">Constants</h2>
|
||||
{{range .}}
|
||||
@ -127,6 +170,8 @@
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
{{comment_html .Doc}}
|
||||
{{example_html $ .Name}}
|
||||
{{callgraph_html $ "" .Name}}
|
||||
|
||||
{{end}}
|
||||
{{range .Types}}
|
||||
{{$tname := .Name}}
|
||||
@ -146,6 +191,8 @@
|
||||
{{end}}
|
||||
|
||||
{{example_html $ $tname}}
|
||||
{{implements_html $ $tname}}
|
||||
{{methodset_html $ $tname}}
|
||||
|
||||
{{range .Funcs}}
|
||||
{{$name_html := html .Name}}
|
||||
@ -153,6 +200,7 @@
|
||||
<pre>{{node_html $ .Decl true}}</pre>
|
||||
{{comment_html .Doc}}
|
||||
{{example_html $ .Name}}
|
||||
{{callgraph_html $ "" .Name}}
|
||||
{{end}}
|
||||
|
||||
{{range .Methods}}
|
||||
@ -162,6 +210,7 @@
|
||||
{{comment_html .Doc}}
|
||||
{{$name := printf "%s_%s" $tname .Name}}
|
||||
{{example_html $ $name}}
|
||||
{{callgraph_html $ .Recv .Name}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
@ -592,3 +592,13 @@ div#playground .output {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
a.error {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background-color: darkred;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 2px 4px 2px 4px; /* TRBL */
|
||||
}
|