|
|
// 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/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 frontend (scanner/parser/type) error, the
// location is highlighted in red and hover text provides the compiler
// error message.
//
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 ------------------------------------------------------------
// 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 ---------------------------------------------------------
// FileInfo holds analysis information for the source file view.
// Clients must not mutate it.
type FileInfo struct { Data []interface{} // JSON serializable values
Links []Link // HTML link markup
}
// 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 the file info in external form.
// Callers must not mutate its fields.
func (fi *fileInfo) get() FileInfo { var r FileInfo // Copy slices, to avoid races.
fi.mu.Lock() r.Data = append(r.Data, fi.data...) if !fi.sorted { sort.Sort(linksByStart(fi.links)) fi.sorted = true } r.Links = append(r.Links, fi.links...) fi.mu.Unlock() return r }
// PackageInfo holds analysis information for the package view.
// Clients must not mutate it.
type PackageInfo struct { CallGraph []*PCGNodeJSON CallGraphIndex map[string]int Types []*TypeInfoJSON }
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 the package info in external form.
// Callers must not mutate its fields.
func (pi *pkgInfo) get() PackageInfo { var r PackageInfo // Copy slices, to avoid races.
pi.mu.Lock() r.CallGraph = append(r.CallGraph, pi.callGraph...) r.CallGraphIndex = pi.callGraphIndex r.Types = append(r.Types, pi.types...) pi.mu.Unlock() return r }
// -- 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)
status string // global analysis status
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 { if res.fileInfos == nil { res.fileInfos = make(map[string]*fileInfo) } fi = new(fileInfo) res.fileInfos[url] = fi } res.mu.Unlock() return fi }
// Status returns a human-readable description of the current analysis status.
func (res *Result) Status() string { res.mu.Lock() defer res.mu.Unlock() 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.
// It returns "zero" if no data is available.
//
func (res *Result) FileInfo(url string) (fi FileInfo) { 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 { if res.pkgInfos == nil { res.pkgInfos = make(map[string]*pkgInfo) } 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 its fields.
// PackageInfo returns "zero" if no data is available.
//
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 }
|