| // 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 |
| } |