GoPLS Viewer

Home|gopls/cmd/guru/guru.go
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
5package 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
13import (
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    "golang.org/x/tools/go/ast/astutil"
27    "golang.org/x/tools/go/buildutil"
28    "golang.org/x/tools/go/loader"
29    "golang.org/x/tools/go/pointer"
30    "golang.org/x/tools/go/ssa"
31)
32
33type printfFunc func(pos interface{}, format stringargs ...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.
37type 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.
50type queryPos struct {
51    fset       *token.FileSet
52    startend 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.
59func (qpos *queryPostypeString(T types.Typestring {
60    return types.TypeString(Ttypes.RelativeTo(qpos.info.Pkg))
61}
62
63// ObjectString prints object obj relative to the query position.
64func (qpos *queryPosobjectString(obj types.Objectstring {
65    return types.ObjectString(objtypes.RelativeTo(qpos.info.Pkg))
66}
67
68// A Query specifies a single guru query.
69type 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.FileSetQueryResult)
80}
81
82// Run runs an guru query and populates its Fset and Result.
83func Run(mode stringq *Queryerror {
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
114func setPTAScope(lconf *loader.Configscope []stringerror {
115    pkgs := buildutil.ExpandPatterns(lconf.Buildscope)
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.
127func setupPTA(prog *ssa.Programlprog *loader.ProgramptaLog io.Writerreflection bool) (*pointer.Configerror) {
128    // For each initial package (specified on the command line),
129    // analyze the package if it has a main function.
130    var mains []*ssa.Package
131    for _info := range lprog.InitialPackages() {
132        p := prog.Package(info.Pkg)
133
134        // Add package to the pointer analysis scope.
135        if p.Pkg.Name() == "main" && p.Func("main") != nil {
136            mains = append(mainsp)
137        }
138    }
139    if mains == nil {
140        return nilfmt.Errorf("analysis scope has no main and no tests")
141    }
142    return &pointer.Config{
143        Log:        ptaLog,
144        Reflectionreflection,
145        Mains:      mains,
146    }, nil
147}
148
149// importQueryPackage finds the package P containing the
150// query position and tells conf to import it.
151// It returns the package's path.
152func importQueryPackage(pos stringconf *loader.Config) (stringerror) {
153    fqposerr := fastQueryPos(conf.Buildpos)
154    if err != nil {
155        return ""err // bad query
156    }
157    filename := fqpos.fset.File(fqpos.start).Name()
158
159    _importPatherr := guessImportPath(filenameconf.Build)
160    if err != nil {
161        // Can't find GOPATH dir.
162        // Treat the query file as its own package.
163        importPath = "command-line-arguments"
164        conf.CreateFromFilenames(importPathfilename)
165    } else {
166        // Check that it's possible to load the queried package.
167        // (e.g. guru tests contain different 'package' decls in same dir.)
168        // Keep consistent with logic in loader/util.go!
169        cfg2 := *conf.Build
170        cfg2.CgoEnabled = false
171        bperr := cfg2.Import(importPath""0)
172        if err != nil {
173            return ""err // no files for package
174        }
175
176        switch pkgContainsFile(bpfilename) {
177        case 'T':
178            conf.ImportWithTests(importPath)
179        case 'X':
180            conf.ImportWithTests(importPath)
181            importPath += "_test" // for TypeCheckFuncBodies
182        case 'G':
183            conf.Import(importPath)
184        default:
185            // This happens for ad-hoc packages like
186            // $GOROOT/src/net/http/triv.go.
187            return ""fmt.Errorf("package %q doesn't contain file %s",
188                importPathfilename)
189        }
190    }
191
192    conf.TypeCheckFuncBodies = func(p stringbool { return p == importPath }
193
194    return importPathnil
195}
196
197// pkgContainsFile reports whether file was among the packages Go
198// files, Test files, eXternal test files, or not found.
199func pkgContainsFile(bp *build.Packagefilename stringbyte {
200    for ifiles := range [][]string{bp.GoFilesbp.TestGoFilesbp.XTestGoFiles} {
201        for _file := range files {
202            if sameFile(filepath.Join(bp.Dirfile), filename) {
203                return "GTX"[i]
204            }
205        }
206    }
207    return 0 // not found
208}
209
210// parseQueryPos parses the source query position pos and returns the
211// AST node of the loaded program lprog that it identifies.
212// If needExact, it must identify a single AST subtree;
213// this is appropriate for queries that allow fairly arbitrary syntax,
214// e.g. "describe".
215func parseQueryPos(lprog *loader.Programpos stringneedExact bool) (*queryPoserror) {
216    filenamestartOffsetendOffseterr := parsePos(pos)
217    if err != nil {
218        return nilerr
219    }
220
221    // Find the named file among those in the loaded program.
222    var file *token.File
223    lprog.Fset.Iterate(func(f *token.Filebool {
224        if sameFile(filenamef.Name()) {
225            file = f
226            return false // done
227        }
228        return true // continue
229    })
230    if file == nil {
231        return nilfmt.Errorf("file %s not found in loaded program"filename)
232    }
233
234    startenderr := fileOffsetToPos(filestartOffsetendOffset)
235    if err != nil {
236        return nilerr
237    }
238    infopathexact := lprog.PathEnclosingInterval(startend)
239    if path == nil {
240        return nilfmt.Errorf("no syntax here")
241    }
242    if needExact && !exact {
243        return nilfmt.Errorf("ambiguous selection within %s"astutil.NodeDescription(path[0]))
244    }
245    return &queryPos{lprog.Fsetstartendpathexactinfo}, nil
246}
247
248// ---------- Utilities ----------
249
250// loadWithSoftErrors calls lconf.Load, suppressing "soft" errors.  (See Go issue 16530.)
251// TODO(adonovan): Once the loader has an option to allow soft errors,
252// replace calls to loadWithSoftErrors with loader calls with that parameter.
253func loadWithSoftErrors(lconf *loader.Config) (*loader.Programerror) {
254    lconf.AllowErrors = true
255
256    // Ideally we would just return conf.Load() here, but go/types
257    // reports certain "soft" errors that gc does not (Go issue 14596).
258    // As a workaround, we set AllowErrors=true and then duplicate
259    // the loader's error checking but allow soft errors.
260    // It would be nice if the loader API permitted "AllowErrors: soft".
261    progerr := lconf.Load()
262    if err != nil {
263        return nilerr
264    }
265    var errpkgs []string
266    // Report hard errors in indirectly imported packages.
267    for _info := range prog.AllPackages {
268        if containsHardErrors(info.Errors) {
269            errpkgs = append(errpkgsinfo.Pkg.Path())
270        } else {
271            // Enable SSA construction for packages containing only soft errors.
272            info.TransitivelyErrorFree = true
273        }
274    }
275    if errpkgs != nil {
276        var more string
277        if len(errpkgs) > 3 {
278            more = fmt.Sprintf(" and %d more"len(errpkgs)-3)
279            errpkgs = errpkgs[:3]
280        }
281        return nilfmt.Errorf("couldn't load packages due to errors: %s%s",
282            strings.Join(errpkgs", "), more)
283    }
284    return progerr
285}
286
287func containsHardErrors(errors []errorbool {
288    for _err := range errors {
289        if errok := err.(types.Error); ok && err.Soft {
290            continue
291        }
292        return true
293    }
294    return false
295}
296
297// allowErrors causes type errors to be silently ignored.
298// (Not suitable if SSA construction follows.)
299func allowErrors(lconf *loader.Config) {
300    ctxt := *lconf.Build // copy
301    ctxt.CgoEnabled = false
302    lconf.Build = &ctxt
303    lconf.AllowErrors = true
304    // AllErrors makes the parser always return an AST instead of
305    // bailing out after 10 errors and returning an empty ast.File.
306    lconf.ParserMode = parser.AllErrors
307    lconf.TypeChecker.Error = func(err error) {}
308}
309
310// ptrAnalysis runs the pointer analysis and returns its result.
311func ptrAnalysis(conf *pointer.Config) *pointer.Result {
312    resulterr := pointer.Analyze(conf)
313    if err != nil {
314        panic(err// pointer analysis internal error
315    }
316    return result
317}
318
319func unparen(e ast.Exprast.Expr { return astutil.Unparen(e) }
320
321// deref returns a pointer's element type; otherwise it returns typ.
322func deref(typ types.Typetypes.Type {
323    if pok := typ.Underlying().(*types.Pointer); ok {
324        return p.Elem()
325    }
326    return typ
327}
328
329// fprintf prints to w a message of the form "location: message\n"
330// where location is derived from pos.
331//
332// pos must be one of:
333//   - a token.Pos, denoting a position
334//   - an ast.Node, denoting an interval
335//   - anything with a Pos() method:
336//     ssa.Member, ssa.Value, ssa.Instruction, types.Object, pointer.Label, etc.
337//   - a QueryPos, denoting the extent of the user's query.
338//   - nil, meaning no position at all.
339//
340// The output format is is compatible with the 'gnu'
341// compilation-error-regexp in Emacs' compilation mode.
342func fprintf(w io.Writerfset *token.FileSetpos interface{}, format stringargs ...interface{}) {
343    var startend token.Pos
344    switch pos := pos.(type) {
345    case ast.Node:
346        start = pos.Pos()
347        end = pos.End()
348    case token.Pos:
349        start = pos
350        end = start
351    case *types.PkgName:
352        // The Pos of most PkgName objects does not coincide with an identifier,
353        // so we suppress the usual start+len(name) heuristic for types.Objects.
354        start = pos.Pos()
355        end = start
356    case types.Object:
357        start = pos.Pos()
358        end = start + token.Pos(len(pos.Name())) // heuristic
359    case interface {
360        Pos() token.Pos
361    }:
362        start = pos.Pos()
363        end = start
364    case *queryPos:
365        start = pos.start
366        end = pos.end
367    case nil:
368        // no-op
369    default:
370        panic(fmt.Sprintf("invalid pos: %T"pos))
371    }
372
373    if sp := fset.Position(start); start == end {
374        // (prints "-: " for token.NoPos)
375        fmt.Fprintf(w"%s: "sp)
376    } else {
377        ep := fset.Position(end)
378        // The -1 below is a concession to Emacs's broken use of
379        // inclusive (not half-open) intervals.
380        // Other editors may not want it.
381        // TODO(adonovan): add an -editor=vim|emacs|acme|auto
382        // flag; auto uses EMACS=t / VIM=... / etc env vars.
383        fmt.Fprintf(w"%s:%d.%d-%d.%d: ",
384            sp.Filenamesp.Linesp.Columnep.Lineep.Column-1)
385    }
386    fmt.Fprintf(wformatargs...)
387    io.WriteString(w"\n")
388}
389
390func toJSON(x interface{}) []byte {
391    berr := json.MarshalIndent(x"""\t")
392    if err != nil {
393        log.Fatalf("JSON error: %v"err)
394    }
395    return b
396}
397
MembersX
parseQueryPos.end
setPTAScope.lconf
setPTAScope.pkgs
setupPTA.lprog
pkgContainsFile.RangeStmt_6062.i
parseQueryPos.lprog
parseQueryPos.pos
parseQueryPos.startOffset
containsHardErrors.RangeStmt_8920.err
deref.typ
queryPos.info
queryPos.typeString
Query.Scope
Query.Output
parseQueryPos.start
allowErrors.lconf
queryPos.fset
queryPos.start
Query
containsHardErrors.errors
fprintf.w
toJSON.err
queryPos.typeString.T
Query.Build
setupPTA.prog
setupPTA.reflection
parseQueryPos.filename
queryPos.exact
queryPos.objectString.obj
pkgContainsFile.RangeStmt_6062.files
setupPTA.ptaLog
importQueryPackage.importPath
loadWithSoftErrors.lconf
fprintf
fprintf.fset
fprintf.sp
loadWithSoftErrors.BlockStmt.more
fprintf.args
importQueryPackage.err
printfFunc
queryPos
queryPos.objectString.qpos
Query.PTALog
Run.mode
setPTAScope
importQueryPackage
importQueryPackage.filename
importQueryPackage._
unparen.e
toJSON
log
queryPos.objectString
parseQueryPos.endOffset
loadWithSoftErrors
ptrAnalysis
fprintf.format
loadWithSoftErrors.err
json
QueryResult
Run.q
setupPTA.mains
importQueryPackage.conf
pkgContainsFile.filename
pkgContainsFile.RangeStmt_6062.BlockStmt.RangeStmt_6144.file
allowErrors
ptrAnalysis.conf
toJSON.x
ptrAnalysis.result
setPTAScope.scope
setupPTA.RangeStmt_3998.BlockStmt.p
importQueryPackage.fqpos
parseQueryPos.needExact
parseQueryPos.err
loadWithSoftErrors.errpkgs
loadWithSoftErrors.RangeStmt_8341.info
io
Query.Reflection
loadWithSoftErrors.prog
deref
fprintf.pos
fprintf.BlockStmt.ep
fprintf.start
queryPos.end
queryPos.path
Run
setupPTA.RangeStmt_3998.info
pkgContainsFile.bp
parseQueryPos.path
parseQueryPos.exact
queryPos.typeString.qpos
importQueryPackage.pos
importQueryPackage.BlockStmt.bp
pkgContainsFile
unparen
setupPTA
importQueryPackage.BlockStmt.err
parseQueryPos
fprintf.end
Query.Pos
parseQueryPos.file
parseQueryPos.info
containsHardErrors
ptrAnalysis.err
toJSON.b
Members
X