github.com/april1989/origin-go-tools@v0.0.32/cmd/guru/guru.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 main 6 7 // TODO(adonovan): new queries 8 // - show all statements that may update the selected lvalue 9 // (local, global, field, etc). 10 // - show all places where an object of type T is created 11 // (&T{}, var t T, new(T), new(struct{array [3]T}), etc. 12 13 import ( 14 "encoding/json" 15 "fmt" 16 "go/ast" 17 "go/build" 18 "go/parser" 19 "go/token" 20 "go/types" 21 "io" 22 "log" 23 "path/filepath" 24 "strings" 25 26 "github.com/april1989/origin-go-tools/go/ast/astutil" 27 "github.com/april1989/origin-go-tools/go/buildutil" 28 "github.com/april1989/origin-go-tools/go/loader" 29 "github.com/april1989/origin-go-tools/go/pointer" 30 "github.com/april1989/origin-go-tools/go/ssa" 31 ) 32 33 type printfFunc func(pos interface{}, format string, args ...interface{}) 34 35 // A QueryResult is an item of output. Each query produces a stream of 36 // query results, calling Query.Output for each one. 37 type QueryResult interface { 38 // JSON returns the QueryResult in JSON form. 39 JSON(fset *token.FileSet) []byte 40 41 // PrintPlain prints the QueryResult in plain text form. 42 // The implementation calls printfFunc to print each line of output. 43 PrintPlain(printf printfFunc) 44 } 45 46 // A QueryPos represents the position provided as input to a query: 47 // a textual extent in the program's source code, the AST node it 48 // corresponds to, and the package to which it belongs. 49 // Instances are created by parseQueryPos. 50 type queryPos struct { 51 fset *token.FileSet 52 start, end token.Pos // source extent of query 53 path []ast.Node // AST path from query node to root of ast.File 54 exact bool // 2nd result of PathEnclosingInterval 55 info *loader.PackageInfo // type info for the queried package (nil for fastQueryPos) 56 } 57 58 // TypeString prints type T relative to the query position. 59 func (qpos *queryPos) typeString(T types.Type) string { 60 return types.TypeString(T, types.RelativeTo(qpos.info.Pkg)) 61 } 62 63 // ObjectString prints object obj relative to the query position. 64 func (qpos *queryPos) objectString(obj types.Object) string { 65 return types.ObjectString(obj, types.RelativeTo(qpos.info.Pkg)) 66 } 67 68 // A Query specifies a single guru query. 69 type Query struct { 70 Pos string // query position 71 Build *build.Context // package loading configuration 72 73 // pointer analysis options 74 Scope []string // main packages in (*loader.Config).FromArgs syntax 75 PTALog io.Writer // (optional) pointer-analysis log file 76 Reflection bool // model reflection soundly (currently slow). 77 78 // result-printing function, safe for concurrent use 79 Output func(*token.FileSet, QueryResult) 80 } 81 82 // Run runs an guru query and populates its Fset and Result. 83 func Run(mode string, q *Query) error { 84 switch mode { 85 case "callees": 86 return callees(q) 87 case "callers": 88 return callers(q) 89 case "callstack": 90 return callstack(q) 91 case "peers": 92 return peers(q) 93 case "pointsto": 94 return pointsto(q) 95 case "whicherrs": 96 return whicherrs(q) 97 case "definition": 98 return definition(q) 99 case "describe": 100 return describe(q) 101 case "freevars": 102 return freevars(q) 103 case "implements": 104 return implements(q) 105 case "referrers": 106 return referrers(q) 107 case "what": 108 return what(q) 109 default: 110 return fmt.Errorf("invalid mode: %q", mode) 111 } 112 } 113 114 func setPTAScope(lconf *loader.Config, scope []string) error { 115 pkgs := buildutil.ExpandPatterns(lconf.Build, scope) 116 if len(pkgs) == 0 { 117 return fmt.Errorf("no packages specified for pointer analysis scope") 118 } 119 // The value of each entry in pkgs is true, 120 // giving ImportWithTests (not Import) semantics. 121 lconf.ImportPkgs = pkgs 122 return nil 123 } 124 125 // Create a pointer.Config whose scope is the initial packages of lprog 126 // and their dependencies. 127 func setupPTA(prog *ssa.Program, lprog *loader.Program, ptaLog io.Writer, reflection bool) (*pointer.Config, error) { 128 // For each initial package (specified on the command line), 129 // if it has a main function, analyze that, 130 // otherwise analyze its tests, if any. 131 var mains []*ssa.Package 132 for _, info := range lprog.InitialPackages() { 133 p := prog.Package(info.Pkg) 134 135 // Add package to the pointer analysis scope. 136 if p.Pkg.Name() == "main" && p.Func("main") != nil { 137 mains = append(mains, p) 138 } else if main := prog.CreateTestMainPackage(p); main != nil { 139 mains = append(mains, main) 140 } 141 } 142 if mains == nil { 143 return nil, fmt.Errorf("analysis scope has no main and no tests") 144 } 145 return &pointer.Config{ 146 Log: ptaLog, 147 Reflection: reflection, 148 Mains: mains, 149 }, nil 150 } 151 152 // importQueryPackage finds the package P containing the 153 // query position and tells conf to import it. 154 // It returns the package's path. 155 func importQueryPackage(pos string, conf *loader.Config) (string, error) { 156 fqpos, err := fastQueryPos(conf.Build, pos) 157 if err != nil { 158 return "", err // bad query 159 } 160 filename := fqpos.fset.File(fqpos.start).Name() 161 162 _, importPath, err := guessImportPath(filename, conf.Build) 163 if err != nil { 164 // Can't find GOPATH dir. 165 // Treat the query file as its own package. 166 importPath = "command-line-arguments" 167 conf.CreateFromFilenames(importPath, filename) 168 } else { 169 // Check that it's possible to load the queried package. 170 // (e.g. guru tests contain different 'package' decls in same dir.) 171 // Keep consistent with logic in loader/util.go! 172 cfg2 := *conf.Build 173 cfg2.CgoEnabled = false 174 bp, err := cfg2.Import(importPath, "", 0) 175 if err != nil { 176 return "", err // no files for package 177 } 178 179 switch pkgContainsFile(bp, filename) { 180 case 'T': 181 conf.ImportWithTests(importPath) 182 case 'X': 183 conf.ImportWithTests(importPath) 184 importPath += "_test" // for TypeCheckFuncBodies 185 case 'G': 186 conf.Import(importPath) 187 default: 188 // This happens for ad-hoc packages like 189 // $GOROOT/src/net/http/triv.go. 190 return "", fmt.Errorf("package %q doesn't contain file %s", 191 importPath, filename) 192 } 193 } 194 195 conf.TypeCheckFuncBodies = func(p string) bool { return p == importPath } 196 197 return importPath, nil 198 } 199 200 // pkgContainsFile reports whether file was among the packages Go 201 // files, Test files, eXternal test files, or not found. 202 func pkgContainsFile(bp *build.Package, filename string) byte { 203 for i, files := range [][]string{bp.GoFiles, bp.TestGoFiles, bp.XTestGoFiles} { 204 for _, file := range files { 205 if sameFile(filepath.Join(bp.Dir, file), filename) { 206 return "GTX"[i] 207 } 208 } 209 } 210 return 0 // not found 211 } 212 213 // ParseQueryPos parses the source query position pos and returns the 214 // AST node of the loaded program lprog that it identifies. 215 // If needExact, it must identify a single AST subtree; 216 // this is appropriate for queries that allow fairly arbitrary syntax, 217 // e.g. "describe". 218 // 219 func parseQueryPos(lprog *loader.Program, pos string, needExact bool) (*queryPos, error) { 220 filename, startOffset, endOffset, err := parsePos(pos) 221 if err != nil { 222 return nil, err 223 } 224 225 // Find the named file among those in the loaded program. 226 var file *token.File 227 lprog.Fset.Iterate(func(f *token.File) bool { 228 if sameFile(filename, f.Name()) { 229 file = f 230 return false // done 231 } 232 return true // continue 233 }) 234 if file == nil { 235 return nil, fmt.Errorf("file %s not found in loaded program", filename) 236 } 237 238 start, end, err := fileOffsetToPos(file, startOffset, endOffset) 239 if err != nil { 240 return nil, err 241 } 242 info, path, exact := lprog.PathEnclosingInterval(start, end) 243 if path == nil { 244 return nil, fmt.Errorf("no syntax here") 245 } 246 if needExact && !exact { 247 return nil, fmt.Errorf("ambiguous selection within %s", astutil.NodeDescription(path[0])) 248 } 249 return &queryPos{lprog.Fset, start, end, path, exact, info}, nil 250 } 251 252 // ---------- Utilities ---------- 253 254 // loadWithSoftErrors calls lconf.Load, suppressing "soft" errors. (See Go issue 16530.) 255 // TODO(adonovan): Once the loader has an option to allow soft errors, 256 // replace calls to loadWithSoftErrors with loader calls with that parameter. 257 func loadWithSoftErrors(lconf *loader.Config) (*loader.Program, error) { 258 lconf.AllowErrors = true 259 260 // Ideally we would just return conf.Load() here, but go/types 261 // reports certain "soft" errors that gc does not (Go issue 14596). 262 // As a workaround, we set AllowErrors=true and then duplicate 263 // the loader's error checking but allow soft errors. 264 // It would be nice if the loader API permitted "AllowErrors: soft". 265 prog, err := lconf.Load() 266 if err != nil { 267 return nil, err 268 } 269 var errpkgs []string 270 // Report hard errors in indirectly imported packages. 271 for _, info := range prog.AllPackages { 272 if containsHardErrors(info.Errors) { 273 errpkgs = append(errpkgs, info.Pkg.Path()) 274 } else { 275 // Enable SSA construction for packages containing only soft errors. 276 info.TransitivelyErrorFree = true 277 } 278 } 279 if errpkgs != nil { 280 var more string 281 if len(errpkgs) > 3 { 282 more = fmt.Sprintf(" and %d more", len(errpkgs)-3) 283 errpkgs = errpkgs[:3] 284 } 285 return nil, fmt.Errorf("couldn't load packages due to errors: %s%s", 286 strings.Join(errpkgs, ", "), more) 287 } 288 return prog, err 289 } 290 291 func containsHardErrors(errors []error) bool { 292 for _, err := range errors { 293 if err, ok := err.(types.Error); ok && err.Soft { 294 continue 295 } 296 return true 297 } 298 return false 299 } 300 301 // allowErrors causes type errors to be silently ignored. 302 // (Not suitable if SSA construction follows.) 303 func allowErrors(lconf *loader.Config) { 304 ctxt := *lconf.Build // copy 305 ctxt.CgoEnabled = false 306 lconf.Build = &ctxt 307 lconf.AllowErrors = true 308 // AllErrors makes the parser always return an AST instead of 309 // bailing out after 10 errors and returning an empty ast.File. 310 lconf.ParserMode = parser.AllErrors 311 lconf.TypeChecker.Error = func(err error) {} 312 } 313 314 // ptrAnalysis runs the pointer analysis and returns its result. 315 func ptrAnalysis(conf *pointer.Config) *pointer.Result { 316 result, err := pointer.Analyze(conf) 317 if err != nil { 318 panic(err) // pointer analysis internal error 319 } 320 return result 321 } 322 323 func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } 324 325 // deref returns a pointer's element type; otherwise it returns typ. 326 func deref(typ types.Type) types.Type { 327 if p, ok := typ.Underlying().(*types.Pointer); ok { 328 return p.Elem() 329 } 330 return typ 331 } 332 333 // fprintf prints to w a message of the form "location: message\n" 334 // where location is derived from pos. 335 // 336 // pos must be one of: 337 // - a token.Pos, denoting a position 338 // - an ast.Node, denoting an interval 339 // - anything with a Pos() method: 340 // ssa.Member, ssa.Value, ssa.Instruction, types.Object, pointer.Label, etc. 341 // - a QueryPos, denoting the extent of the user's query. 342 // - nil, meaning no position at all. 343 // 344 // The output format is is compatible with the 'gnu' 345 // compilation-error-regexp in Emacs' compilation mode. 346 // 347 func fprintf(w io.Writer, fset *token.FileSet, pos interface{}, format string, args ...interface{}) { 348 var start, end token.Pos 349 switch pos := pos.(type) { 350 case ast.Node: 351 start = pos.Pos() 352 end = pos.End() 353 case token.Pos: 354 start = pos 355 end = start 356 case *types.PkgName: 357 // The Pos of most PkgName objects does not coincide with an identifier, 358 // so we suppress the usual start+len(name) heuristic for types.Objects. 359 start = pos.Pos() 360 end = start 361 case types.Object: 362 start = pos.Pos() 363 end = start + token.Pos(len(pos.Name())) // heuristic 364 case interface { 365 Pos() token.Pos 366 }: 367 start = pos.Pos() 368 end = start 369 case *queryPos: 370 start = pos.start 371 end = pos.end 372 case nil: 373 // no-op 374 default: 375 panic(fmt.Sprintf("invalid pos: %T", pos)) 376 } 377 378 if sp := fset.Position(start); start == end { 379 // (prints "-: " for token.NoPos) 380 fmt.Fprintf(w, "%s: ", sp) 381 } else { 382 ep := fset.Position(end) 383 // The -1 below is a concession to Emacs's broken use of 384 // inclusive (not half-open) intervals. 385 // Other editors may not want it. 386 // TODO(adonovan): add an -editor=vim|emacs|acme|auto 387 // flag; auto uses EMACS=t / VIM=... / etc env vars. 388 fmt.Fprintf(w, "%s:%d.%d-%d.%d: ", 389 sp.Filename, sp.Line, sp.Column, ep.Line, ep.Column-1) 390 } 391 fmt.Fprintf(w, format, args...) 392 io.WriteString(w, "\n") 393 } 394 395 func toJSON(x interface{}) []byte { 396 b, err := json.MarshalIndent(x, "", "\t") 397 if err != nil { 398 log.Fatalf("JSON error: %v", err) 399 } 400 return b 401 }