diff --git a/cmd/godoc/doc.go b/cmd/godoc/doc.go index 6dda27870e8..b1f45ffb413 100644 --- a/cmd/godoc/doc.go +++ b/cmd/godoc/doc.go @@ -52,13 +52,6 @@ The flags are: Go root directory -http=addr HTTP service address (e.g., '127.0.0.1:6060' or just ':6060') - -analysis=type,pointer - 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 - (significantly slower) - See https://golang.org/lib/godoc/analysis/help.html for details. -templates="" directory containing alternate template files; if set, the directory may provide alternative template files diff --git a/cmd/godoc/godoc_test.go b/cmd/godoc/godoc_test.go index ac6bacd4f60..76568c31d40 100644 --- a/cmd/godoc/godoc_test.go +++ b/cmd/godoc/godoc_test.go @@ -5,11 +5,9 @@ package main_test import ( - "bufio" "bytes" "fmt" "go/build" - "io" "io/ioutil" "net" "net/http" @@ -479,135 +477,3 @@ func TestNoMainModule(t *testing.T) { t.Errorf("stderr contains 'go mod download', is that intentional?\nstderr=%q", stderr.String()) } } - -// Basic integration test for godoc -analysis=type (via HTTP interface). -func TestTypeAnalysis(t *testing.T) { - bin, cleanup := buildGodoc(t) - defer cleanup() - testTypeAnalysis(t, packagestest.GOPATH, bin) - // TODO(golang.org/issue/34473): Add support for type, pointer - // analysis in module mode, then enable its test coverage here. -} -func testTypeAnalysis(t *testing.T, x packagestest.Exporter, bin string) { - if runtime.GOOS == "plan9" { - t.Skip("skipping test on plan9 (issue #11974)") // see comment re: Plan 9 below - } - - // Write a fake GOROOT/GOPATH. - // TODO(golang.org/issue/34473): This test uses import paths without a dot in first - // path element. This is not viable in module mode; import paths will need to change. - e := packagestest.Export(t, x, []packagestest.Module{ - { - Name: "app", - Files: map[string]interface{}{ - "main.go": ` -package main -import "lib" -func main() { print(lib.V) } -`, - }, - }, - { - Name: "lib", - Files: map[string]interface{}{ - "lib.go": ` -package lib -type T struct{} -const C = 3 -var V T -func (T) F() int { return C } -`, - }, - }, - }) - goroot := filepath.Join(e.Temp(), "goroot") - if err := os.Mkdir(goroot, 0755); err != nil { - t.Fatalf("os.Mkdir(%q) failed: %v", goroot, err) - } - defer e.Cleanup() - - // Start the server. - addr := serverAddress(t) - cmd := exec.Command(bin, fmt.Sprintf("-http=%s", addr), "-analysis=type") - cmd.Dir = e.Config.Dir - // Point to an empty GOROOT directory to speed things up - // by not doing type analysis for the entire real GOROOT. - // TODO(golang.org/issue/34473): This test optimization may not be viable in module mode. - cmd.Env = append(e.Config.Env, fmt.Sprintf("GOROOT=%s", goroot)) - cmd.Stdout = os.Stderr - stderr, err := cmd.StderrPipe() - if err != nil { - t.Fatal(err) - } - cmd.Args[0] = "godoc" - if err := cmd.Start(); err != nil { - t.Fatalf("failed to start godoc: %s", err) - } - defer killAndWait(cmd) - waitForServerReady(t, cmd, addr) - - // Wait for type analysis to complete. - reader := bufio.NewReader(stderr) - for { - s, err := reader.ReadString('\n') // on Plan 9 this fails - if err != nil { - t.Fatal(err) - } - fmt.Fprint(os.Stderr, s) - if strings.Contains(s, "Type analysis complete.") { - break - } - } - go io.Copy(os.Stderr, reader) - - t0 := time.Now() - - // Make an HTTP request and check for a regular expression match. - // The patterns are very crude checks that basic type information - // has been annotated onto the source view. -tryagain: - for _, test := range []struct{ url, pattern string }{ - {"/src/lib/lib.go", "L2.*package .*Package docs for lib.*/lib"}, - {"/src/lib/lib.go", "L3.*type .*type info for T.*struct"}, - {"/src/lib/lib.go", "L5.*var V .*type T struct"}, - {"/src/lib/lib.go", "L6.*func .*type T struct.*T.*return .*const C untyped int.*C"}, - - {"/src/app/main.go", "L2.*package .*Package docs for app"}, - {"/src/app/main.go", "L3.*import .*Package docs for lib.*lib"}, - {"/src/app/main.go", "L4.*func main.*package lib.*lib.*var lib.V lib.T.*V"}, - } { - url := fmt.Sprintf("http://%s%s", addr, test.url) - resp, err := http.Get(url) - if err != nil { - t.Errorf("GET %s failed: %s", url, err) - continue - } - body, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - t.Errorf("GET %s: failed to read body: %s (response: %v)", url, err, resp) - continue - } - - if !bytes.Contains(body, []byte("Static analysis features")) { - // Type analysis results usually become available within - // ~4ms after godoc startup (for this input on my machine). - if elapsed := time.Since(t0); elapsed > 500*time.Millisecond { - t.Fatalf("type analysis results still unavailable after %s", elapsed) - } - time.Sleep(10 * time.Millisecond) - goto tryagain - } - - match, err := regexp.Match(test.pattern, body) - if err != nil { - t.Errorf("regexp.Match(%q) failed: %s", test.pattern, err) - continue - } - if !match { - // This is a really ugly failure message. - t.Errorf("GET %s: body doesn't match %q, got:\n%s", - url, test.pattern, string(body)) - } - } -} diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go index 8780f8bee02..9d6ac869cb6 100644 --- a/cmd/godoc/main.go +++ b/cmd/godoc/main.go @@ -25,7 +25,6 @@ import ( "flag" "fmt" "go/build" - exec "golang.org/x/sys/execabs" "io" "log" "net/http" @@ -38,8 +37,9 @@ import ( "runtime" "strings" + exec "golang.org/x/sys/execabs" + "golang.org/x/tools/godoc" - "golang.org/x/tools/godoc/analysis" "golang.org/x/tools/godoc/static" "golang.org/x/tools/godoc/vfs" "golang.org/x/tools/godoc/vfs/gatefs" @@ -59,8 +59,6 @@ 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 when in GOPATH mode (supported: type, pointer). See https://golang.org/lib/godoc/analysis/help.html`) - // network httpAddr = flag.String("http", defaultAddr, "HTTP service address") @@ -208,12 +206,6 @@ func main() { if goModFile != "" { fmt.Printf("using module mode; GOMOD=%s\n", goModFile) - if *analysisFlag != "" { - fmt.Fprintln(os.Stderr, "The -analysis flag is supported only in GOPATH mode at this time.") - fmt.Fprintln(os.Stderr, "See https://golang.org/issue/34473.") - usage() - } - // Detect whether to use vendor mode or not. mainMod, vendorEnabled, err := gocommand.VendorEnabled(context.Background(), gocommand.Invocation{}, &gocommand.Runner{}) if err != nil { @@ -266,20 +258,6 @@ func main() { } } - 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) - } - } - } - var corpus *godoc.Corpus if goModFile != "" { corpus = godoc.NewCorpus(moduleFS{fs}) @@ -376,11 +354,6 @@ func main() { go corpus.RunIndexer() } - // Start type/pointer analysis. - if typeAnalysis || pointerAnalysis { - go analysis.Run(pointerAnalysis, &corpus.Analysis) - } - // Start http server. if *verbose { log.Println("starting HTTP server") diff --git a/godoc/analysis/README b/godoc/analysis/README deleted file mode 100644 index d3e732eb566..00000000000 --- a/godoc/analysis/README +++ /dev/null @@ -1,111 +0,0 @@ - -Type and Pointer Analysis to-do list -==================================== - -Alan Donovan - - -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 ----- - -(*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 package 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: -- 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 guru 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. diff --git a/godoc/analysis/analysis.go b/godoc/analysis/analysis.go index b79286c5d3e..de8e470b1e9 100644 --- a/godoc/analysis/analysis.go +++ b/godoc/analysis/analysis.go @@ -43,24 +43,9 @@ package analysis // import "golang.org/x/tools/godoc/analysis" import ( - "fmt" - "go/build" - "go/scanner" - "go/token" - "go/types" - "html" "io" - "log" - "os" - "path/filepath" "sort" - "strings" "sync" - - "golang.org/x/tools/go/loader" - "golang.org/x/tools/go/pointer" - "golang.org/x/tools/go/ssa" - "golang.org/x/tools/go/ssa/ssautil" ) // -- links ------------------------------------------------------------ @@ -73,53 +58,6 @@ type Link interface { Write(w io.Writer, _ int, start bool) // the godoc.LinkWriter signature } -// An 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, `") - } else { - fmt.Fprintf(w, "") - } -} - -// An 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) { - // causes havoc, not sure why, so use . - if start { - fmt.Fprintf(w, ``, html.EscapeString(e.msg)) - } else { - fmt.Fprintf(w, "") - } -} - // -- fileInfo --------------------------------------------------------- // FileInfo holds analysis information for the source file view. @@ -139,27 +77,6 @@ type fileInfo struct { 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 the file info in external form. // Callers must not mutate its fields. func (fi *fileInfo) get() FileInfo { @@ -191,19 +108,6 @@ type pkgInfo struct { 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 the package info in external form. // Callers must not mutate its fields. func (pi *pkgInfo) get() PackageInfo { @@ -252,13 +156,6 @@ func (res *Result) Status() string { return res.status } -func (res *Result) setStatusf(format string, args ...interface{}) { - res.mu.Lock() - res.status = fmt.Sprintf(format, args...) - log.Printf(format, args...) - res.mu.Unlock() -} - // 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. @@ -293,321 +190,8 @@ func (res *Result) PackageInfo(importPath string) PackageInfo { 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 "defined" (formerly "named") types in the program - ptaConfig pointer.Config - path2url map[string]string // maps openable path to godoc file URL (/src/fmt/print.go) - pcgs map[*ssa.Package]*packageCallGraph -} - -// fileAndOffset returns the file and offset for a given pos. -func (a *analysis) fileAndOffset(pos token.Pos) (fi *fileInfo, offset int) { - return a.fileAndOffsetPosn(a.prog.Fset.Position(pos)) -} - -// fileAndOffsetPosn returns the file and offset for a given position. -func (a *analysis) fileAndOffsetPosn(posn token.Position) (fi *fileInfo, offset int) { - 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] - 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) { - conf := loader.Config{ - AllowErrors: true, - } - - // Silence the default error handler. - // Don't print all errors; we'll report just - // one per errant package later. - conf.TypeChecker.Error = func(e error) {} - - var roots, args []string // roots[i] ends with os.PathSeparator - - // Enumerate packages in $GOROOT. - root := filepath.Join(build.Default.GOROOT, "src") + string(os.PathSeparator) - 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 := filepath.Join(dir, "src") + string(os.PathSeparator) - 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{"golang.org/x/tools/cmd/godoc"} - //args = []string{"fmt"} - - if _, err := conf.FromArgs(args, true); err != nil { - // TODO(adonovan): degrade gracefully, not fail totally. - // (The crippling case is a parse error in an external test file.) - result.setStatusf("Analysis failed: %s.", err) // import error - return - } - - result.setStatusf("Loading and type-checking packages...") - iprog, err := conf.Load() - if iprog != nil { - // Report only the first error of each package. - for _, info := range iprog.AllPackages { - for _, err := range info.Errors { - fmt.Fprintln(os.Stderr, err) - break - } - } - log.Printf("Loaded %d packages.", len(iprog.AllPackages)) - } - if err != nil { - result.setStatusf("Loading failed: %s.\n", err) - return - } - - // Create SSA-form program representation. - // Only the transitively error-free packages are used. - prog := ssautil.CreateProgram(iprog, ssa.GlobalDebug) - - // Create a "testmain" package for each package with tests. - for _, pkg := range prog.AllPackages() { - if testmain := prog.CreateTestMainPackage(pkg); testmain != nil { - log.Printf("Adding tests for %s", pkg.Pkg.Path()) - } - } - - // Build SSA code for bodies of all functions in the whole program. - result.setStatusf("Constructing SSA form...") - prog.Build() - log.Print("SSA construction 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/" plus path relative to GOROOT/src or GOPATH[i]/src. - a.path2url = make(map[string]string) - for _, info := range iprog.AllPackages { - nextfile: - for _, f := range info.Files { - if f.Pos() == 0 { - continue // e.g. files generated by cgo - } - 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/" + filepath.ToSlash(rel) - continue nextfile - } - } - - log.Printf("Can't locate file %s (package %q) beneath any root", - abs, info.Pkg.Path()) - } - } - - // Add links for scanner, parser, type-checker errors. - // TODO(adonovan): fix: these links can overlap with - // identifier markup, causing the renderer to emit some - // characters twice. - errors := make(map[token.Position][]string) - for _, info := range iprog.AllPackages { - for _, err := range info.Errors { - switch err := err.(type) { - case types.Error: - posn := a.prog.Fset.Position(err.Pos) - errors[posn] = append(errors[posn], err.Msg) - case scanner.ErrorList: - for _, e := range err { - errors[e.Pos] = append(errors[e.Pos], e.Msg) - } - default: - log.Printf("Package %q has error (%T) without position: %v\n", - info.Pkg.Path(), err, err) - } - } - } - for posn, errs := range errors { - fi, offset := a.fileAndOffsetPosn(posn) - 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 { - if named, ok := obj.Type().(*types.Named); ok { - a.allNamed = append(a.allNamed, named) - } - } - } - } - log.Print("Computing implements relation...") - 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) - - result.setStatusf("Type analysis complete.") - - if pta { - mainPkgs := ssautil.MainPackages(prog.AllPackages()) - log.Print("Transitively error-free main packages: ", mainPkgs) - 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) - - a.result.setStatusf("Pointer analysis running...") - - ptares, err := pointer.Analyze(&a.ptaConfig) - if err != nil { - // If this happens, it indicates a bug. - a.result.setStatusf("Pointer analysis failed: %s.", err) - return - } - log.Print("Pointer analysis complete.") - - // Add the results of pointer analysis. - - a.result.setStatusf("Computing channel peers...") - a.doChannelPeers(ptares.Queries) - a.result.setStatusf("Computing dynamic call graph edges...") - a.doCallgraph(ptares.CallGraph) - - a.result.setStatusf("Analysis complete.") -} - 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 or $GOPATH/src. -// Derived from from go/ssa/stdlib_test.go -// root must end with os.PathSeparator. -// -// TODO(adonovan): use buildutil.AllPackages when the tree thaws. -func allPackages(root string) []string { - 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 -} diff --git a/godoc/analysis/callgraph.go b/godoc/analysis/callgraph.go deleted file mode 100644 index 492022d3de0..00000000000 --- a/godoc/analysis/callgraph.go +++ /dev/null @@ -1,351 +0,0 @@ -// 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" - "go/types" - "log" - "math/big" - "sort" - - "golang.org/x/tools/go/callgraph" - "golang.org/x/tools/go/ssa" -) - -// 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 a synthetic node such as - } - - // 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.Printf("CALLERS: skipping %s: no pos", callee) - continue - } - - var this *types.Package // for relativizing names - if callee.Pkg != nil { - this = callee.Pkg.Pkg - } - - // 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.Pkg)] = 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.Pkg.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.Pkg - } - - 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.Parent() != nil { - return fmt.Sprintf("%s in %s", - types.TypeString(fn.Signature, types.RelativeTo(this)), - prettyFunc(this, fn.Parent())) - } - if fn.Synthetic != "" && fn.Name() == "init" { - // (This is the actual initializer, not a declared 'func init'). - if fn.Pkg.Pkg == this { - return "package initializer" - } - return fmt.Sprintf("%q package initializer", fn.Pkg.Pkg.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: ""}) - for fn := range pcg.nodeIndex { - nodes = append(nodes, &pcgNode{ - fn: fn, - pretty: prettyFunc(fn.Pkg.Pkg, 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) } diff --git a/godoc/analysis/implements.go b/godoc/analysis/implements.go deleted file mode 100644 index 5a29579892a..00000000000 --- a/godoc/analysis/implements.go +++ /dev/null @@ -1,195 +0,0 @@ -// 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 ( - "go/types" - "sort" - - "golang.org/x/tools/go/types/typeutil" -) - -// computeImplements computes the "implements" relation over all pairs -// of named types in allNamed. -func computeImplements(cache *typeutil.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 -} diff --git a/godoc/analysis/json.go b/godoc/analysis/json.go index f8976187c2c..b6e1e3f96d7 100644 --- a/godoc/analysis/json.go +++ b/godoc/analysis/json.go @@ -11,16 +11,6 @@ type anchorJSON struct { 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 type " (ByKind != "", e.g. "array") // T "implements " (ByKind == "") @@ -43,23 +33,6 @@ type TypeInfoJSON struct { 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". diff --git a/godoc/analysis/peers.go b/godoc/analysis/peers.go deleted file mode 100644 index a742f06cb69..00000000000 --- a/godoc/analysis/peers.go +++ /dev/null @@ -1,154 +0,0 @@ -// 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" - "go/types" - - "golang.org/x/tools/go/pointer" - "golang.org/x/tools/go/ssa" -) - -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().Pkg), - }) - 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 cmd/guru/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 cmd/guru/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 -} diff --git a/godoc/analysis/typeinfo.go b/godoc/analysis/typeinfo.go deleted file mode 100644 index e57683f4719..00000000000 --- a/godoc/analysis/typeinfo.go +++ /dev/null @@ -1,234 +0,0 @@ -// 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" - "go/types" - "reflect" - "strconv" - "strings" - - "golang.org/x/tools/go/loader" - "golang.org/x/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{WordSize: 8, MaxAlign: 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 - qualifier := types.RelativeTo(info.Pkg) - 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.Imported().Name() { - // Assume this is a non-renaming import. - // NB: not true for degenerate renamings: `import foo "foo"`. - pos++ - Len = len(obj.Imported().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(obj, qualifier), - href: a.posURL(pos, Len), - }) - } - - // IMPLEMENTS & METHOD SETS - for _, obj := range info.Defs { - if obj, ok := obj.(*types.TypeName); ok { - if named, ok := obj.Type().(*types.Named); ok { - a.namedType(named, implements) - } - } - } -} - -func (a *analysis) namedType(T *types.Named, implements map[*types.Named]implementsFacts) { - obj := T.Obj() - qualifier := types.RelativeTo(obj.Pkg()) - 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(T, qualifier), - }, - }) - } - - // IMPLEMENTS - if r, ok := implements[T]; ok { - if isInterface(T) { - // "T is implemented by " ... - // "T is implemented by "... - // "T implements "... - group := implGroupJSON{ - Descr: types.TypeString(T, qualifier), - } - // 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 "... - group := implGroupJSON{ - Descr: types.TypeString(T, qualifier), - } - for _, super := range r.from { - addFact(&group, super, false) - } - v.ImplGroups = append(v.ImplGroups, group) - } - if r.fromPtr != nil { - // "*C implements "... - group := implGroupJSON{ - Descr: "*" + types.TypeString(T, qualifier), - } - 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(sel, qualifier), - }) - } - - // 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): Path is not unique! - // It is possible to declare a non-test package called x_test. - a.result.pkgInfo(obj.Pkg().Path()).addType(v) - } -} - -// -- utilities -------------------------------------------------------- - -func isInterface(T types.Type) bool { return types.IsInterface(T) } - -// 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 { - return obj.Pkg().Scope().Lookup(obj.Name()) == obj -}