github.com/v2fly/tools@v0.100.0/godoc/analysis/analysis.go (about) 1 // Copyright 2014 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package analysis performs type and pointer analysis 6 // and generates mark-up for the Go source view. 7 // 8 // The Run method populates a Result object by running type and 9 // (optionally) pointer analysis. The Result object is thread-safe 10 // and at all times may be accessed by a serving thread, even as it is 11 // progressively populated as analysis facts are derived. 12 // 13 // The Result is a mapping from each godoc file URL 14 // (e.g. /src/fmt/print.go) to information about that file. The 15 // information is a list of HTML markup links and a JSON array of 16 // structured data values. Some of the links call client-side 17 // JavaScript functions that index this array. 18 // 19 // The analysis computes mark-up for the following relations: 20 // 21 // IMPORTS: for each ast.ImportSpec, the package that it denotes. 22 // 23 // RESOLUTION: for each ast.Ident, its kind and type, and the location 24 // of its definition. 25 // 26 // METHOD SETS, IMPLEMENTS: for each ast.Ident defining a named type, 27 // its method-set, the set of interfaces it implements or is 28 // implemented by, and its size/align values. 29 // 30 // CALLERS, CALLEES: for each function declaration ('func' token), its 31 // callers, and for each call-site ('(' token), its callees. 32 // 33 // CALLGRAPH: the package docs include an interactive viewer for the 34 // intra-package call graph of "fmt". 35 // 36 // CHANNEL PEERS: for each channel operation make/<-/close, the set of 37 // other channel ops that alias the same channel(s). 38 // 39 // ERRORS: for each locus of a frontend (scanner/parser/type) error, the 40 // location is highlighted in red and hover text provides the compiler 41 // error message. 42 // 43 package analysis // import "github.com/v2fly/tools/godoc/analysis" 44 45 import ( 46 "fmt" 47 "go/build" 48 "go/scanner" 49 "go/token" 50 "go/types" 51 "html" 52 "io" 53 "log" 54 "os" 55 "path/filepath" 56 "sort" 57 "strings" 58 "sync" 59 60 "github.com/v2fly/tools/go/loader" 61 "github.com/v2fly/tools/go/pointer" 62 "github.com/v2fly/tools/go/ssa" 63 "github.com/v2fly/tools/go/ssa/ssautil" 64 ) 65 66 // -- links ------------------------------------------------------------ 67 68 // A Link is an HTML decoration of the bytes [Start, End) of a file. 69 // Write is called before/after those bytes to emit the mark-up. 70 type Link interface { 71 Start() int 72 End() int 73 Write(w io.Writer, _ int, start bool) // the godoc.LinkWriter signature 74 } 75 76 // An <a> element. 77 type aLink struct { 78 start, end int // =godoc.Segment 79 title string // hover text 80 onclick string // JS code (NB: trusted) 81 href string // URL (NB: trusted) 82 } 83 84 func (a aLink) Start() int { return a.start } 85 func (a aLink) End() int { return a.end } 86 func (a aLink) Write(w io.Writer, _ int, start bool) { 87 if start { 88 fmt.Fprintf(w, `<a title='%s'`, html.EscapeString(a.title)) 89 if a.onclick != "" { 90 fmt.Fprintf(w, ` onclick='%s'`, html.EscapeString(a.onclick)) 91 } 92 if a.href != "" { 93 // TODO(adonovan): I think that in principle, a.href must first be 94 // url.QueryEscape'd, but if I do that, a leading slash becomes "%2F", 95 // which causes the browser to treat the path as relative, not absolute. 96 // WTF? 97 fmt.Fprintf(w, ` href='%s'`, html.EscapeString(a.href)) 98 } 99 fmt.Fprintf(w, ">") 100 } else { 101 fmt.Fprintf(w, "</a>") 102 } 103 } 104 105 // An <a class='error'> element. 106 type errorLink struct { 107 start int 108 msg string 109 } 110 111 func (e errorLink) Start() int { return e.start } 112 func (e errorLink) End() int { return e.start + 1 } 113 114 func (e errorLink) Write(w io.Writer, _ int, start bool) { 115 // <span> causes havoc, not sure why, so use <a>. 116 if start { 117 fmt.Fprintf(w, `<a class='error' title='%s'>`, html.EscapeString(e.msg)) 118 } else { 119 fmt.Fprintf(w, "</a>") 120 } 121 } 122 123 // -- fileInfo --------------------------------------------------------- 124 125 // FileInfo holds analysis information for the source file view. 126 // Clients must not mutate it. 127 type FileInfo struct { 128 Data []interface{} // JSON serializable values 129 Links []Link // HTML link markup 130 } 131 132 // A fileInfo is the server's store of hyperlinks and JSON data for a 133 // particular file. 134 type fileInfo struct { 135 mu sync.Mutex 136 data []interface{} // JSON objects 137 links []Link 138 sorted bool 139 hasErrors bool // TODO(adonovan): surface this in the UI 140 } 141 142 // addLink adds a link to the Go source file fi. 143 func (fi *fileInfo) addLink(link Link) { 144 fi.mu.Lock() 145 fi.links = append(fi.links, link) 146 fi.sorted = false 147 if _, ok := link.(errorLink); ok { 148 fi.hasErrors = true 149 } 150 fi.mu.Unlock() 151 } 152 153 // addData adds the structured value x to the JSON data for the Go 154 // source file fi. Its index is returned. 155 func (fi *fileInfo) addData(x interface{}) int { 156 fi.mu.Lock() 157 index := len(fi.data) 158 fi.data = append(fi.data, x) 159 fi.mu.Unlock() 160 return index 161 } 162 163 // get returns the file info in external form. 164 // Callers must not mutate its fields. 165 func (fi *fileInfo) get() FileInfo { 166 var r FileInfo 167 // Copy slices, to avoid races. 168 fi.mu.Lock() 169 r.Data = append(r.Data, fi.data...) 170 if !fi.sorted { 171 sort.Sort(linksByStart(fi.links)) 172 fi.sorted = true 173 } 174 r.Links = append(r.Links, fi.links...) 175 fi.mu.Unlock() 176 return r 177 } 178 179 // PackageInfo holds analysis information for the package view. 180 // Clients must not mutate it. 181 type PackageInfo struct { 182 CallGraph []*PCGNodeJSON 183 CallGraphIndex map[string]int 184 Types []*TypeInfoJSON 185 } 186 187 type pkgInfo struct { 188 mu sync.Mutex 189 callGraph []*PCGNodeJSON 190 callGraphIndex map[string]int // keys are (*ssa.Function).RelString() 191 types []*TypeInfoJSON // type info for exported types 192 } 193 194 func (pi *pkgInfo) setCallGraph(callGraph []*PCGNodeJSON, callGraphIndex map[string]int) { 195 pi.mu.Lock() 196 pi.callGraph = callGraph 197 pi.callGraphIndex = callGraphIndex 198 pi.mu.Unlock() 199 } 200 201 func (pi *pkgInfo) addType(t *TypeInfoJSON) { 202 pi.mu.Lock() 203 pi.types = append(pi.types, t) 204 pi.mu.Unlock() 205 } 206 207 // get returns the package info in external form. 208 // Callers must not mutate its fields. 209 func (pi *pkgInfo) get() PackageInfo { 210 var r PackageInfo 211 // Copy slices, to avoid races. 212 pi.mu.Lock() 213 r.CallGraph = append(r.CallGraph, pi.callGraph...) 214 r.CallGraphIndex = pi.callGraphIndex 215 r.Types = append(r.Types, pi.types...) 216 pi.mu.Unlock() 217 return r 218 } 219 220 // -- Result ----------------------------------------------------------- 221 222 // Result contains the results of analysis. 223 // The result contains a mapping from filenames to a set of HTML links 224 // and JavaScript data referenced by the links. 225 type Result struct { 226 mu sync.Mutex // guards maps (but not their contents) 227 status string // global analysis status 228 fileInfos map[string]*fileInfo // keys are godoc file URLs 229 pkgInfos map[string]*pkgInfo // keys are import paths 230 } 231 232 // fileInfo returns the fileInfo for the specified godoc file URL, 233 // constructing it as needed. Thread-safe. 234 func (res *Result) fileInfo(url string) *fileInfo { 235 res.mu.Lock() 236 fi, ok := res.fileInfos[url] 237 if !ok { 238 if res.fileInfos == nil { 239 res.fileInfos = make(map[string]*fileInfo) 240 } 241 fi = new(fileInfo) 242 res.fileInfos[url] = fi 243 } 244 res.mu.Unlock() 245 return fi 246 } 247 248 // Status returns a human-readable description of the current analysis status. 249 func (res *Result) Status() string { 250 res.mu.Lock() 251 defer res.mu.Unlock() 252 return res.status 253 } 254 255 func (res *Result) setStatusf(format string, args ...interface{}) { 256 res.mu.Lock() 257 res.status = fmt.Sprintf(format, args...) 258 log.Printf(format, args...) 259 res.mu.Unlock() 260 } 261 262 // FileInfo returns new slices containing opaque JSON values and the 263 // HTML link markup for the specified godoc file URL. Thread-safe. 264 // Callers must not mutate the elements. 265 // It returns "zero" if no data is available. 266 // 267 func (res *Result) FileInfo(url string) (fi FileInfo) { 268 return res.fileInfo(url).get() 269 } 270 271 // pkgInfo returns the pkgInfo for the specified import path, 272 // constructing it as needed. Thread-safe. 273 func (res *Result) pkgInfo(importPath string) *pkgInfo { 274 res.mu.Lock() 275 pi, ok := res.pkgInfos[importPath] 276 if !ok { 277 if res.pkgInfos == nil { 278 res.pkgInfos = make(map[string]*pkgInfo) 279 } 280 pi = new(pkgInfo) 281 res.pkgInfos[importPath] = pi 282 } 283 res.mu.Unlock() 284 return pi 285 } 286 287 // PackageInfo returns new slices of JSON values for the callgraph and 288 // type info for the specified package. Thread-safe. 289 // Callers must not mutate its fields. 290 // PackageInfo returns "zero" if no data is available. 291 // 292 func (res *Result) PackageInfo(importPath string) PackageInfo { 293 return res.pkgInfo(importPath).get() 294 } 295 296 // -- analysis --------------------------------------------------------- 297 298 type analysis struct { 299 result *Result 300 prog *ssa.Program 301 ops []chanOp // all channel ops in program 302 allNamed []*types.Named // all "defined" (formerly "named") types in the program 303 ptaConfig pointer.Config 304 path2url map[string]string // maps openable path to godoc file URL (/src/fmt/print.go) 305 pcgs map[*ssa.Package]*packageCallGraph 306 } 307 308 // fileAndOffset returns the file and offset for a given pos. 309 func (a *analysis) fileAndOffset(pos token.Pos) (fi *fileInfo, offset int) { 310 return a.fileAndOffsetPosn(a.prog.Fset.Position(pos)) 311 } 312 313 // fileAndOffsetPosn returns the file and offset for a given position. 314 func (a *analysis) fileAndOffsetPosn(posn token.Position) (fi *fileInfo, offset int) { 315 url := a.path2url[posn.Filename] 316 return a.result.fileInfo(url), posn.Offset 317 } 318 319 // posURL returns the URL of the source extent [pos, pos+len). 320 func (a *analysis) posURL(pos token.Pos, len int) string { 321 if pos == token.NoPos { 322 return "" 323 } 324 posn := a.prog.Fset.Position(pos) 325 url := a.path2url[posn.Filename] 326 return fmt.Sprintf("%s?s=%d:%d#L%d", 327 url, posn.Offset, posn.Offset+len, posn.Line) 328 } 329 330 // ---------------------------------------------------------------------- 331 332 // Run runs program analysis and computes the resulting markup, 333 // populating *result in a thread-safe manner, first with type 334 // information then later with pointer analysis information if 335 // enabled by the pta flag. 336 // 337 func Run(pta bool, result *Result) { 338 conf := loader.Config{ 339 AllowErrors: true, 340 } 341 342 // Silence the default error handler. 343 // Don't print all errors; we'll report just 344 // one per errant package later. 345 conf.TypeChecker.Error = func(e error) {} 346 347 var roots, args []string // roots[i] ends with os.PathSeparator 348 349 // Enumerate packages in $GOROOT. 350 root := filepath.Join(build.Default.GOROOT, "src") + string(os.PathSeparator) 351 roots = append(roots, root) 352 args = allPackages(root) 353 log.Printf("GOROOT=%s: %s\n", root, args) 354 355 // Enumerate packages in $GOPATH. 356 for i, dir := range filepath.SplitList(build.Default.GOPATH) { 357 root := filepath.Join(dir, "src") + string(os.PathSeparator) 358 roots = append(roots, root) 359 pkgs := allPackages(root) 360 log.Printf("GOPATH[%d]=%s: %s\n", i, root, pkgs) 361 args = append(args, pkgs...) 362 } 363 364 // Uncomment to make startup quicker during debugging. 365 //args = []string{"github.com/v2fly/tools/cmd/godoc"} 366 //args = []string{"fmt"} 367 368 if _, err := conf.FromArgs(args, true); err != nil { 369 // TODO(adonovan): degrade gracefully, not fail totally. 370 // (The crippling case is a parse error in an external test file.) 371 result.setStatusf("Analysis failed: %s.", err) // import error 372 return 373 } 374 375 result.setStatusf("Loading and type-checking packages...") 376 iprog, err := conf.Load() 377 if iprog != nil { 378 // Report only the first error of each package. 379 for _, info := range iprog.AllPackages { 380 for _, err := range info.Errors { 381 fmt.Fprintln(os.Stderr, err) 382 break 383 } 384 } 385 log.Printf("Loaded %d packages.", len(iprog.AllPackages)) 386 } 387 if err != nil { 388 result.setStatusf("Loading failed: %s.\n", err) 389 return 390 } 391 392 // Create SSA-form program representation. 393 // Only the transitively error-free packages are used. 394 prog := ssautil.CreateProgram(iprog, ssa.GlobalDebug) 395 396 // Create a "testmain" package for each package with tests. 397 for _, pkg := range prog.AllPackages() { 398 if testmain := prog.CreateTestMainPackage(pkg); testmain != nil { 399 log.Printf("Adding tests for %s", pkg.Pkg.Path()) 400 } 401 } 402 403 // Build SSA code for bodies of all functions in the whole program. 404 result.setStatusf("Constructing SSA form...") 405 prog.Build() 406 log.Print("SSA construction complete") 407 408 a := analysis{ 409 result: result, 410 prog: prog, 411 pcgs: make(map[*ssa.Package]*packageCallGraph), 412 } 413 414 // Build a mapping from openable filenames to godoc file URLs, 415 // i.e. "/src/" plus path relative to GOROOT/src or GOPATH[i]/src. 416 a.path2url = make(map[string]string) 417 for _, info := range iprog.AllPackages { 418 nextfile: 419 for _, f := range info.Files { 420 if f.Pos() == 0 { 421 continue // e.g. files generated by cgo 422 } 423 abs := iprog.Fset.File(f.Pos()).Name() 424 // Find the root to which this file belongs. 425 for _, root := range roots { 426 rel := strings.TrimPrefix(abs, root) 427 if len(rel) < len(abs) { 428 a.path2url[abs] = "/src/" + filepath.ToSlash(rel) 429 continue nextfile 430 } 431 } 432 433 log.Printf("Can't locate file %s (package %q) beneath any root", 434 abs, info.Pkg.Path()) 435 } 436 } 437 438 // Add links for scanner, parser, type-checker errors. 439 // TODO(adonovan): fix: these links can overlap with 440 // identifier markup, causing the renderer to emit some 441 // characters twice. 442 errors := make(map[token.Position][]string) 443 for _, info := range iprog.AllPackages { 444 for _, err := range info.Errors { 445 switch err := err.(type) { 446 case types.Error: 447 posn := a.prog.Fset.Position(err.Pos) 448 errors[posn] = append(errors[posn], err.Msg) 449 case scanner.ErrorList: 450 for _, e := range err { 451 errors[e.Pos] = append(errors[e.Pos], e.Msg) 452 } 453 default: 454 log.Printf("Package %q has error (%T) without position: %v\n", 455 info.Pkg.Path(), err, err) 456 } 457 } 458 } 459 for posn, errs := range errors { 460 fi, offset := a.fileAndOffsetPosn(posn) 461 fi.addLink(errorLink{ 462 start: offset, 463 msg: strings.Join(errs, "\n"), 464 }) 465 } 466 467 // ---------- type-based analyses ---------- 468 469 // Compute the all-pairs IMPLEMENTS relation. 470 // Collect all named types, even local types 471 // (which can have methods via promotion) 472 // and the built-in "error". 473 errorType := types.Universe.Lookup("error").Type().(*types.Named) 474 a.allNamed = append(a.allNamed, errorType) 475 for _, info := range iprog.AllPackages { 476 for _, obj := range info.Defs { 477 if obj, ok := obj.(*types.TypeName); ok { 478 if named, ok := obj.Type().(*types.Named); ok { 479 a.allNamed = append(a.allNamed, named) 480 } 481 } 482 } 483 } 484 log.Print("Computing implements relation...") 485 facts := computeImplements(&a.prog.MethodSets, a.allNamed) 486 487 // Add the type-based analysis results. 488 log.Print("Extracting type info...") 489 for _, info := range iprog.AllPackages { 490 a.doTypeInfo(info, facts) 491 } 492 493 a.visitInstrs(pta) 494 495 result.setStatusf("Type analysis complete.") 496 497 if pta { 498 mainPkgs := ssautil.MainPackages(prog.AllPackages()) 499 log.Print("Transitively error-free main packages: ", mainPkgs) 500 a.pointer(mainPkgs) 501 } 502 } 503 504 // visitInstrs visits all SSA instructions in the program. 505 func (a *analysis) visitInstrs(pta bool) { 506 log.Print("Visit instructions...") 507 for fn := range ssautil.AllFunctions(a.prog) { 508 for _, b := range fn.Blocks { 509 for _, instr := range b.Instrs { 510 // CALLEES (static) 511 // (Dynamic calls require pointer analysis.) 512 // 513 // We use the SSA representation to find the static callee, 514 // since in many cases it does better than the 515 // types.Info.{Refs,Selection} information. For example: 516 // 517 // defer func(){}() // static call to anon function 518 // f := func(){}; f() // static call to anon function 519 // f := fmt.Println; f() // static call to named function 520 // 521 // The downside is that we get no static callee information 522 // for packages that (transitively) contain errors. 523 if site, ok := instr.(ssa.CallInstruction); ok { 524 if callee := site.Common().StaticCallee(); callee != nil { 525 // TODO(adonovan): callgraph: elide wrappers. 526 // (Do static calls ever go to wrappers?) 527 if site.Common().Pos() != token.NoPos { 528 a.addCallees(site, []*ssa.Function{callee}) 529 } 530 } 531 } 532 533 if !pta { 534 continue 535 } 536 537 // CHANNEL PEERS 538 // Collect send/receive/close instructions in the whole ssa.Program. 539 for _, op := range chanOps(instr) { 540 a.ops = append(a.ops, op) 541 a.ptaConfig.AddQuery(op.ch) // add channel ssa.Value to PTA query 542 } 543 } 544 } 545 } 546 log.Print("Visit instructions complete") 547 } 548 549 // pointer runs the pointer analysis. 550 func (a *analysis) pointer(mainPkgs []*ssa.Package) { 551 // Run the pointer analysis and build the complete callgraph. 552 a.ptaConfig.Mains = mainPkgs 553 a.ptaConfig.BuildCallGraph = true 554 a.ptaConfig.Reflection = false // (for now) 555 556 a.result.setStatusf("Pointer analysis running...") 557 558 ptares, err := pointer.Analyze(&a.ptaConfig) 559 if err != nil { 560 // If this happens, it indicates a bug. 561 a.result.setStatusf("Pointer analysis failed: %s.", err) 562 return 563 } 564 log.Print("Pointer analysis complete.") 565 566 // Add the results of pointer analysis. 567 568 a.result.setStatusf("Computing channel peers...") 569 a.doChannelPeers(ptares.Queries) 570 a.result.setStatusf("Computing dynamic call graph edges...") 571 a.doCallgraph(ptares.CallGraph) 572 573 a.result.setStatusf("Analysis complete.") 574 } 575 576 type linksByStart []Link 577 578 func (a linksByStart) Less(i, j int) bool { return a[i].Start() < a[j].Start() } 579 func (a linksByStart) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 580 func (a linksByStart) Len() int { return len(a) } 581 582 // allPackages returns a new sorted slice of all packages beneath the 583 // specified package root directory, e.g. $GOROOT/src or $GOPATH/src. 584 // Derived from from go/ssa/stdlib_test.go 585 // root must end with os.PathSeparator. 586 // 587 // TODO(adonovan): use buildutil.AllPackages when the tree thaws. 588 func allPackages(root string) []string { 589 var pkgs []string 590 filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 591 if info == nil { 592 return nil // non-existent root directory? 593 } 594 if !info.IsDir() { 595 return nil // not a directory 596 } 597 // Prune the search if we encounter any of these names: 598 base := filepath.Base(path) 599 if base == "testdata" || strings.HasPrefix(base, ".") { 600 return filepath.SkipDir 601 } 602 pkg := filepath.ToSlash(strings.TrimPrefix(path, root)) 603 switch pkg { 604 case "builtin": 605 return filepath.SkipDir 606 case "": 607 return nil // ignore root of tree 608 } 609 pkgs = append(pkgs, pkg) 610 return nil 611 }) 612 return pkgs 613 }