From 734737930440fc305a816e577cab457fbbc807c1 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 11 Feb 2016 17:57:17 -0500 Subject: [PATCH] cmd/guru: rename oracle to guru, copying it to a single package See plans at: https://docs.google.com/document/d/1UErU12vR7jTedYvKHVNRzGPmXqdMASZ6PfE7B-p6sIg/edit#heading=h.t5rbcuy7di7j Change-Id: I369747e0ce8cd42a2975eb0f651a2e5fba47cf9e Reviewed-on: https://go-review.googlesource.com/19475 Reviewed-by: Alan Donovan --- cmd/guru/callees.go | 258 ++++++ cmd/guru/callers.go | 115 +++ cmd/guru/callstack.go | 126 +++ cmd/guru/definition.go | 76 ++ cmd/guru/describe.go | 773 ++++++++++++++++++ cmd/guru/emacs-test.bash | 50 ++ cmd/guru/freevars.go | 222 +++++ cmd/guru/guru.el | 230 ++++++ cmd/guru/guru.go | 356 ++++++++ cmd/guru/guru.vim | 107 +++ cmd/guru/guru_test.go | 272 ++++++ cmd/guru/implements.go | 352 ++++++++ cmd/guru/main.go | 204 +++++ cmd/guru/peers.go | 252 ++++++ cmd/guru/pointsto.go | 291 +++++++ cmd/guru/pos.go | 147 ++++ cmd/guru/referrers.go | 241 ++++++ cmd/guru/serial/serial.go | 258 ++++++ cmd/guru/testdata/src/calls-json/main.go | 16 + cmd/guru/testdata/src/calls-json/main.golden | 34 + cmd/guru/testdata/src/calls/main.go | 129 +++ cmd/guru/testdata/src/calls/main.golden | 125 +++ cmd/guru/testdata/src/describe-json/main.go | 29 + .../testdata/src/describe-json/main.golden | 111 +++ cmd/guru/testdata/src/describe/main.go | 96 +++ cmd/guru/testdata/src/describe/main.golden | 184 +++++ cmd/guru/testdata/src/freevars/main.go | 41 + cmd/guru/testdata/src/freevars/main.golden | 18 + cmd/guru/testdata/src/implements-json/main.go | 27 + .../testdata/src/implements-json/main.golden | 159 ++++ .../src/implements-methods-json/main.go | 37 + .../src/implements-methods-json/main.golden | 312 +++++++ .../testdata/src/implements-methods/main.go | 37 + .../src/implements-methods/main.golden | 39 + cmd/guru/testdata/src/implements/main.go | 39 + cmd/guru/testdata/src/implements/main.golden | 46 ++ cmd/guru/testdata/src/imports/main.go | 29 + cmd/guru/testdata/src/imports/main.golden | 57 ++ cmd/guru/testdata/src/lib/lib.go | 20 + cmd/guru/testdata/src/main/multi.go | 13 + cmd/guru/testdata/src/peers-json/main.go | 13 + cmd/guru/testdata/src/peers-json/main.golden | 15 + cmd/guru/testdata/src/peers/main.go | 52 ++ cmd/guru/testdata/src/peers/main.golden | 100 +++ cmd/guru/testdata/src/pointsto-json/main.go | 27 + .../testdata/src/pointsto-json/main.golden | 35 + cmd/guru/testdata/src/pointsto/main.go | 75 ++ cmd/guru/testdata/src/pointsto/main.golden | 96 +++ cmd/guru/testdata/src/referrers-json/main.go | 24 + .../testdata/src/referrers-json/main.golden | 59 ++ cmd/guru/testdata/src/referrers/ext_test.go | 12 + cmd/guru/testdata/src/referrers/int_test.go | 8 + cmd/guru/testdata/src/referrers/main.go | 34 + cmd/guru/testdata/src/referrers/main.golden | 42 + cmd/guru/testdata/src/reflection/main.go | 30 + cmd/guru/testdata/src/reflection/main.golden | 34 + cmd/guru/testdata/src/what-json/main.go | 9 + cmd/guru/testdata/src/what-json/main.golden | 51 ++ cmd/guru/testdata/src/what/main.go | 11 + cmd/guru/testdata/src/what/main.golden | 39 + cmd/guru/testdata/src/whicherrs/main.go | 27 + cmd/guru/testdata/src/whicherrs/main.golden | 8 + cmd/guru/what.go | 210 +++++ cmd/guru/whicherrs.go | 326 ++++++++ 64 files changed, 7265 insertions(+) create mode 100644 cmd/guru/callees.go create mode 100644 cmd/guru/callers.go create mode 100644 cmd/guru/callstack.go create mode 100644 cmd/guru/definition.go create mode 100644 cmd/guru/describe.go create mode 100755 cmd/guru/emacs-test.bash create mode 100644 cmd/guru/freevars.go create mode 100644 cmd/guru/guru.el create mode 100644 cmd/guru/guru.go create mode 100644 cmd/guru/guru.vim create mode 100644 cmd/guru/guru_test.go create mode 100644 cmd/guru/implements.go create mode 100644 cmd/guru/main.go create mode 100644 cmd/guru/peers.go create mode 100644 cmd/guru/pointsto.go create mode 100644 cmd/guru/pos.go create mode 100644 cmd/guru/referrers.go create mode 100644 cmd/guru/serial/serial.go create mode 100644 cmd/guru/testdata/src/calls-json/main.go create mode 100644 cmd/guru/testdata/src/calls-json/main.golden create mode 100644 cmd/guru/testdata/src/calls/main.go create mode 100644 cmd/guru/testdata/src/calls/main.golden create mode 100644 cmd/guru/testdata/src/describe-json/main.go create mode 100644 cmd/guru/testdata/src/describe-json/main.golden create mode 100644 cmd/guru/testdata/src/describe/main.go create mode 100644 cmd/guru/testdata/src/describe/main.golden create mode 100644 cmd/guru/testdata/src/freevars/main.go create mode 100644 cmd/guru/testdata/src/freevars/main.golden create mode 100644 cmd/guru/testdata/src/implements-json/main.go create mode 100644 cmd/guru/testdata/src/implements-json/main.golden create mode 100644 cmd/guru/testdata/src/implements-methods-json/main.go create mode 100644 cmd/guru/testdata/src/implements-methods-json/main.golden create mode 100644 cmd/guru/testdata/src/implements-methods/main.go create mode 100644 cmd/guru/testdata/src/implements-methods/main.golden create mode 100644 cmd/guru/testdata/src/implements/main.go create mode 100644 cmd/guru/testdata/src/implements/main.golden create mode 100644 cmd/guru/testdata/src/imports/main.go create mode 100644 cmd/guru/testdata/src/imports/main.golden create mode 100644 cmd/guru/testdata/src/lib/lib.go create mode 100644 cmd/guru/testdata/src/main/multi.go create mode 100644 cmd/guru/testdata/src/peers-json/main.go create mode 100644 cmd/guru/testdata/src/peers-json/main.golden create mode 100644 cmd/guru/testdata/src/peers/main.go create mode 100644 cmd/guru/testdata/src/peers/main.golden create mode 100644 cmd/guru/testdata/src/pointsto-json/main.go create mode 100644 cmd/guru/testdata/src/pointsto-json/main.golden create mode 100644 cmd/guru/testdata/src/pointsto/main.go create mode 100644 cmd/guru/testdata/src/pointsto/main.golden create mode 100644 cmd/guru/testdata/src/referrers-json/main.go create mode 100644 cmd/guru/testdata/src/referrers-json/main.golden create mode 100644 cmd/guru/testdata/src/referrers/ext_test.go create mode 100644 cmd/guru/testdata/src/referrers/int_test.go create mode 100644 cmd/guru/testdata/src/referrers/main.go create mode 100644 cmd/guru/testdata/src/referrers/main.golden create mode 100644 cmd/guru/testdata/src/reflection/main.go create mode 100644 cmd/guru/testdata/src/reflection/main.golden create mode 100644 cmd/guru/testdata/src/what-json/main.go create mode 100644 cmd/guru/testdata/src/what-json/main.golden create mode 100644 cmd/guru/testdata/src/what/main.go create mode 100644 cmd/guru/testdata/src/what/main.golden create mode 100644 cmd/guru/testdata/src/whicherrs/main.go create mode 100644 cmd/guru/testdata/src/whicherrs/main.golden create mode 100644 cmd/guru/what.go create mode 100644 cmd/guru/whicherrs.go diff --git a/cmd/guru/callees.go b/cmd/guru/callees.go new file mode 100644 index 00000000..1fcd193c --- /dev/null +++ b/cmd/guru/callees.go @@ -0,0 +1,258 @@ +// Copyright 2013 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 main + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "sort" + + "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" + "golang.org/x/tools/cmd/guru/serial" +) + +// Callees reports the possible callees of the function call site +// identified by the specified source location. +func callees(q *Query) error { + lconf := loader.Config{Build: q.Build} + + if err := setPTAScope(&lconf, q.Scope); err != nil { + return err + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err := parseQueryPos(lprog, q.Pos, true) // needs exact pos + if err != nil { + return err + } + + // Determine the enclosing call for the specified position. + var e *ast.CallExpr + for _, n := range qpos.path { + if e, _ = n.(*ast.CallExpr); e != nil { + break + } + } + if e == nil { + return fmt.Errorf("there is no function call here") + } + // TODO(adonovan): issue an error if the call is "too far + // away" from the current selection, as this most likely is + // not what the user intended. + + // Reject type conversions. + if qpos.info.Types[e.Fun].IsType() { + return fmt.Errorf("this is a type conversion, not a function call") + } + + // Deal with obviously static calls before constructing SSA form. + // Some static calls may yet require SSA construction, + // e.g. f := func(){}; f(). + switch funexpr := unparen(e.Fun).(type) { + case *ast.Ident: + switch obj := qpos.info.Uses[funexpr].(type) { + case *types.Builtin: + // Reject calls to built-ins. + return fmt.Errorf("this is a call to the built-in '%s' operator", obj.Name()) + case *types.Func: + // This is a static function call + q.result = &calleesTypesResult{ + site: e, + callee: obj, + } + return nil + } + case *ast.SelectorExpr: + sel := qpos.info.Selections[funexpr] + if sel == nil { + // qualified identifier. + // May refer to top level function variable + // or to top level function. + callee := qpos.info.Uses[funexpr.Sel] + if obj, ok := callee.(*types.Func); ok { + q.result = &calleesTypesResult{ + site: e, + callee: obj, + } + return nil + } + } else if sel.Kind() == types.MethodVal { + // Inspect the receiver type of the selected method. + // If it is concrete, the call is statically dispatched. + // (Due to implicit field selections, it is not enough to look + // at sel.Recv(), the type of the actual receiver expression.) + method := sel.Obj().(*types.Func) + recvtype := method.Type().(*types.Signature).Recv().Type() + if !types.IsInterface(recvtype) { + // static method call + q.result = &calleesTypesResult{ + site: e, + callee: method, + } + return nil + } + } + } + + prog := ssautil.CreateProgram(lprog, ssa.GlobalDebug) + + ptaConfig, err := setupPTA(prog, lprog, q.PTALog, q.Reflection) + if err != nil { + return err + } + + pkg := prog.Package(qpos.info.Pkg) + if pkg == nil { + return fmt.Errorf("no SSA package") + } + + // Defer SSA construction till after errors are reported. + prog.Build() + + // Ascertain calling function and call site. + callerFn := ssa.EnclosingFunction(pkg, qpos.path) + if callerFn == nil { + return fmt.Errorf("no SSA function built for this location (dead code?)") + } + + // Find the call site. + site, err := findCallSite(callerFn, e) + if err != nil { + return err + } + + funcs, err := findCallees(ptaConfig, site) + if err != nil { + return err + } + + q.result = &calleesSSAResult{ + site: site, + funcs: funcs, + } + return nil +} + +func findCallSite(fn *ssa.Function, call *ast.CallExpr) (ssa.CallInstruction, error) { + instr, _ := fn.ValueForExpr(call) + callInstr, _ := instr.(ssa.CallInstruction) + if instr == nil { + return nil, fmt.Errorf("this call site is unreachable in this analysis") + } + return callInstr, nil +} + +func findCallees(conf *pointer.Config, site ssa.CallInstruction) ([]*ssa.Function, error) { + // Avoid running the pointer analysis for static calls. + if callee := site.Common().StaticCallee(); callee != nil { + switch callee.String() { + case "runtime.SetFinalizer", "(reflect.Value).Call": + // The PTA treats calls to these intrinsics as dynamic. + // TODO(adonovan): avoid reliance on PTA internals. + + default: + return []*ssa.Function{callee}, nil // singleton + } + } + + // Dynamic call: use pointer analysis. + conf.BuildCallGraph = true + cg := ptrAnalysis(conf).CallGraph + cg.DeleteSyntheticNodes() + + // Find all call edges from the site. + n := cg.Nodes[site.Parent()] + if n == nil { + return nil, fmt.Errorf("this call site is unreachable in this analysis") + } + calleesMap := make(map[*ssa.Function]bool) + for _, edge := range n.Out { + if edge.Site == site { + calleesMap[edge.Callee.Func] = true + } + } + + // De-duplicate and sort. + funcs := make([]*ssa.Function, 0, len(calleesMap)) + for f := range calleesMap { + funcs = append(funcs, f) + } + sort.Sort(byFuncPos(funcs)) + return funcs, nil +} + +type calleesSSAResult struct { + site ssa.CallInstruction + funcs []*ssa.Function +} + +type calleesTypesResult struct { + site *ast.CallExpr + callee *types.Func +} + +func (r *calleesSSAResult) display(printf printfFunc) { + if len(r.funcs) == 0 { + // dynamic call on a provably nil func/interface + printf(r.site, "%s on nil value", r.site.Common().Description()) + } else { + printf(r.site, "this %s dispatches to:", r.site.Common().Description()) + for _, callee := range r.funcs { + printf(callee, "\t%s", callee) + } + } +} + +func (r *calleesSSAResult) toSerial(res *serial.Result, fset *token.FileSet) { + j := &serial.Callees{ + Pos: fset.Position(r.site.Pos()).String(), + Desc: r.site.Common().Description(), + } + for _, callee := range r.funcs { + j.Callees = append(j.Callees, &serial.CalleesItem{ + Name: callee.String(), + Pos: fset.Position(callee.Pos()).String(), + }) + } + res.Callees = j +} + +func (r *calleesTypesResult) display(printf printfFunc) { + printf(r.site, "this static function call dispatches to:") + printf(r.callee, "\t%s", r.callee.FullName()) +} + +func (r *calleesTypesResult) toSerial(res *serial.Result, fset *token.FileSet) { + j := &serial.Callees{ + Pos: fset.Position(r.site.Pos()).String(), + Desc: "static function call", + } + j.Callees = []*serial.CalleesItem{ + &serial.CalleesItem{ + Name: r.callee.FullName(), + Pos: fset.Position(r.callee.Pos()).String(), + }, + } + res.Callees = j +} + +// NB: byFuncPos is not deterministic across packages since it depends on load order. +// Use lessPos if the tests need it. +type byFuncPos []*ssa.Function + +func (a byFuncPos) Len() int { return len(a) } +func (a byFuncPos) Less(i, j int) bool { return a[i].Pos() < a[j].Pos() } +func (a byFuncPos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/cmd/guru/callers.go b/cmd/guru/callers.go new file mode 100644 index 00000000..928555b8 --- /dev/null +++ b/cmd/guru/callers.go @@ -0,0 +1,115 @@ +// Copyright 2013 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 main + +import ( + "fmt" + "go/token" + + "golang.org/x/tools/go/callgraph" + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/cmd/guru/serial" +) + +// Callers reports the possible callers of the function +// immediately enclosing the specified source location. +// +func callers(q *Query) error { + lconf := loader.Config{Build: q.Build} + + if err := setPTAScope(&lconf, q.Scope); err != nil { + return err + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err := parseQueryPos(lprog, q.Pos, false) + if err != nil { + return err + } + + prog := ssautil.CreateProgram(lprog, 0) + + ptaConfig, err := setupPTA(prog, lprog, q.PTALog, q.Reflection) + if err != nil { + return err + } + + pkg := prog.Package(qpos.info.Pkg) + if pkg == nil { + return fmt.Errorf("no SSA package") + } + if !ssa.HasEnclosingFunction(pkg, qpos.path) { + return fmt.Errorf("this position is not inside a function") + } + + // Defer SSA construction till after errors are reported. + prog.Build() + + target := ssa.EnclosingFunction(pkg, qpos.path) + if target == nil { + return fmt.Errorf("no SSA function built for this location (dead code?)") + } + + // TODO(adonovan): opt: if function is never address-taken, skip + // the pointer analysis. Just look for direct calls. This can + // be done in a single pass over the SSA. + + // Run the pointer analysis, recording each + // call found to originate from target. + ptaConfig.BuildCallGraph = true + cg := ptrAnalysis(ptaConfig).CallGraph + cg.DeleteSyntheticNodes() + edges := cg.CreateNode(target).In + // TODO(adonovan): sort + dedup calls to ensure test determinism. + + q.result = &callersResult{ + target: target, + callgraph: cg, + edges: edges, + } + return nil +} + +type callersResult struct { + target *ssa.Function + callgraph *callgraph.Graph + edges []*callgraph.Edge +} + +func (r *callersResult) display(printf printfFunc) { + root := r.callgraph.Root + if r.edges == nil { + printf(r.target, "%s is not reachable in this program.", r.target) + } else { + printf(r.target, "%s is called from these %d sites:", r.target, len(r.edges)) + for _, edge := range r.edges { + if edge.Caller == root { + printf(r.target, "the root of the call graph") + } else { + printf(edge, "\t%s from %s", edge.Description(), edge.Caller.Func) + } + } + } +} + +func (r *callersResult) toSerial(res *serial.Result, fset *token.FileSet) { + var callers []serial.Caller + for _, edge := range r.edges { + callers = append(callers, serial.Caller{ + Caller: edge.Caller.Func.String(), + Pos: fset.Position(edge.Pos()).String(), + Desc: edge.Description(), + }) + } + res.Callers = callers +} diff --git a/cmd/guru/callstack.go b/cmd/guru/callstack.go new file mode 100644 index 00000000..b9894164 --- /dev/null +++ b/cmd/guru/callstack.go @@ -0,0 +1,126 @@ +// Copyright 2013 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 main + +import ( + "fmt" + "go/token" + + "golang.org/x/tools/go/callgraph" + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/cmd/guru/serial" +) + +// Callstack displays an arbitrary path from a root of the callgraph +// to the function at the current position. +// +// The information may be misleading in a context-insensitive +// analysis. e.g. the call path X->Y->Z might be infeasible if Y never +// calls Z when it is called from X. TODO(adonovan): think about UI. +// +// TODO(adonovan): permit user to specify a starting point other than +// the analysis root. +// +func callstack(q *Query) error { + fset := token.NewFileSet() + lconf := loader.Config{Fset: fset, Build: q.Build} + + if err := setPTAScope(&lconf, q.Scope); err != nil { + return err + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + + qpos, err := parseQueryPos(lprog, q.Pos, false) + if err != nil { + return err + } + + prog := ssautil.CreateProgram(lprog, 0) + + ptaConfig, err := setupPTA(prog, lprog, q.PTALog, q.Reflection) + if err != nil { + return err + } + + pkg := prog.Package(qpos.info.Pkg) + if pkg == nil { + return fmt.Errorf("no SSA package") + } + + if !ssa.HasEnclosingFunction(pkg, qpos.path) { + return fmt.Errorf("this position is not inside a function") + } + + // Defer SSA construction till after errors are reported. + prog.Build() + + target := ssa.EnclosingFunction(pkg, qpos.path) + if target == nil { + return fmt.Errorf("no SSA function built for this location (dead code?)") + } + + // Run the pointer analysis and build the complete call graph. + ptaConfig.BuildCallGraph = true + cg := ptrAnalysis(ptaConfig).CallGraph + cg.DeleteSyntheticNodes() + + // Search for an arbitrary path from a root to the target function. + isEnd := func(n *callgraph.Node) bool { return n.Func == target } + callpath := callgraph.PathSearch(cg.Root, isEnd) + if callpath != nil { + callpath = callpath[1:] // remove synthetic edge from + } + + q.Fset = fset + q.result = &callstackResult{ + qpos: qpos, + target: target, + callpath: callpath, + } + return nil +} + +type callstackResult struct { + qpos *queryPos + target *ssa.Function + callpath []*callgraph.Edge +} + +func (r *callstackResult) display(printf printfFunc) { + if r.callpath != nil { + printf(r.qpos, "Found a call path from root to %s", r.target) + printf(r.target, "%s", r.target) + for i := len(r.callpath) - 1; i >= 0; i-- { + edge := r.callpath[i] + printf(edge, "%s from %s", edge.Description(), edge.Caller.Func) + } + } else { + printf(r.target, "%s is unreachable in this analysis scope", r.target) + } +} + +func (r *callstackResult) toSerial(res *serial.Result, fset *token.FileSet) { + var callers []serial.Caller + for i := len(r.callpath) - 1; i >= 0; i-- { // (innermost first) + edge := r.callpath[i] + callers = append(callers, serial.Caller{ + Pos: fset.Position(edge.Pos()).String(), + Caller: edge.Caller.Func.String(), + Desc: edge.Description(), + }) + } + res.Callstack = &serial.CallStack{ + Pos: fset.Position(r.target.Pos()).String(), + Target: r.target.String(), + Callers: callers, + } +} diff --git a/cmd/guru/definition.go b/cmd/guru/definition.go new file mode 100644 index 00000000..99f54435 --- /dev/null +++ b/cmd/guru/definition.go @@ -0,0 +1,76 @@ +// Copyright 2013 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 main + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/loader" + "golang.org/x/tools/cmd/guru/serial" +) + +// definition reports the location of the definition of an identifier. +// +// TODO(adonovan): opt: for intra-file references, the parser's +// resolution might be enough; we should start with that. +// +func definition(q *Query) error { + lconf := loader.Config{Build: q.Build} + allowErrors(&lconf) + + if _, err := importQueryPackage(q.Pos, &lconf); err != nil { + return err + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err := parseQueryPos(lprog, q.Pos, false) + if err != nil { + return err + } + + id, _ := qpos.path[0].(*ast.Ident) + if id == nil { + return fmt.Errorf("no identifier here") + } + + obj := qpos.info.ObjectOf(id) + if obj == nil { + // Happens for y in "switch y := x.(type)", + // and the package declaration, + // but I think that's all. + return fmt.Errorf("no object for identifier") + } + + q.result = &definitionResult{qpos, obj} + return nil +} + +type definitionResult struct { + qpos *queryPos + obj types.Object // object it denotes +} + +func (r *definitionResult) display(printf printfFunc) { + printf(r.obj, "defined here as %s", r.qpos.objectString(r.obj)) +} + +func (r *definitionResult) toSerial(res *serial.Result, fset *token.FileSet) { + definition := &serial.Definition{ + Desc: r.obj.String(), + } + if pos := r.obj.Pos(); pos != token.NoPos { // Package objects have no Pos() + definition.ObjPos = fset.Position(pos).String() + } + res.Definition = definition +} diff --git a/cmd/guru/describe.go b/cmd/guru/describe.go new file mode 100644 index 00000000..d3f47317 --- /dev/null +++ b/cmd/guru/describe.go @@ -0,0 +1,773 @@ +// Copyright 2013 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 main + +import ( + "bytes" + "fmt" + "go/ast" + exact "go/constant" + "go/token" + "go/types" + "log" + "os" + "strings" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/cmd/guru/serial" +) + +// describe describes the syntax node denoted by the query position, +// including: +// - its syntactic category +// - the definition of its referent (for identifiers) [now redundant] +// - its type and method set (for an expression or type expression) +// +func describe(q *Query) error { + lconf := loader.Config{Build: q.Build} + allowErrors(&lconf) + + if _, err := importQueryPackage(q.Pos, &lconf); err != nil { + return err + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err := parseQueryPos(lprog, q.Pos, true) // (need exact pos) + if err != nil { + return err + } + + if false { // debugging + fprintf(os.Stderr, lprog.Fset, qpos.path[0], "you selected: %s %s", + astutil.NodeDescription(qpos.path[0]), pathToString(qpos.path)) + } + + path, action := findInterestingNode(qpos.info, qpos.path) + switch action { + case actionExpr: + q.result, err = describeValue(qpos, path) + + case actionType: + q.result, err = describeType(qpos, path) + + case actionPackage: + q.result, err = describePackage(qpos, path) + + case actionStmt: + q.result, err = describeStmt(qpos, path) + + case actionUnknown: + q.result = &describeUnknownResult{path[0]} + + default: + panic(action) // unreachable + } + return err +} + +type describeUnknownResult struct { + node ast.Node +} + +func (r *describeUnknownResult) display(printf printfFunc) { + // Nothing much to say about misc syntax. + printf(r.node, "%s", astutil.NodeDescription(r.node)) +} + +func (r *describeUnknownResult) toSerial(res *serial.Result, fset *token.FileSet) { + res.Describe = &serial.Describe{ + Desc: astutil.NodeDescription(r.node), + Pos: fset.Position(r.node.Pos()).String(), + } +} + +type action int + +const ( + actionUnknown action = iota // None of the below + actionExpr // FuncDecl, true Expr or Ident(types.{Const,Var}) + actionType // type Expr or Ident(types.TypeName). + actionStmt // Stmt or Ident(types.Label) + actionPackage // Ident(types.Package) or ImportSpec +) + +// findInterestingNode classifies the syntax node denoted by path as one of: +// - an expression, part of an expression or a reference to a constant +// or variable; +// - a type, part of a type, or a reference to a named type; +// - a statement, part of a statement, or a label referring to a statement; +// - part of a package declaration or import spec. +// - none of the above. +// and returns the most "interesting" associated node, which may be +// the same node, an ancestor or a descendent. +// +func findInterestingNode(pkginfo *loader.PackageInfo, path []ast.Node) ([]ast.Node, action) { + // TODO(adonovan): integrate with go/types/stdlib_test.go and + // apply this to every AST node we can find to make sure it + // doesn't crash. + + // TODO(adonovan): audit for ParenExpr safety, esp. since we + // traverse up and down. + + // TODO(adonovan): if the users selects the "." in + // "fmt.Fprintf()", they'll get an ambiguous selection error; + // we won't even reach here. Can we do better? + + // TODO(adonovan): describing a field within 'type T struct {...}' + // describes the (anonymous) struct type and concludes "no methods". + // We should ascend to the enclosing type decl, if any. + + for len(path) > 0 { + switch n := path[0].(type) { + case *ast.GenDecl: + if len(n.Specs) == 1 { + // Descend to sole {Import,Type,Value}Spec child. + path = append([]ast.Node{n.Specs[0]}, path...) + continue + } + return path, actionUnknown // uninteresting + + case *ast.FuncDecl: + // Descend to function name. + path = append([]ast.Node{n.Name}, path...) + continue + + case *ast.ImportSpec: + return path, actionPackage + + case *ast.ValueSpec: + if len(n.Names) == 1 { + // Descend to sole Ident child. + path = append([]ast.Node{n.Names[0]}, path...) + continue + } + return path, actionUnknown // uninteresting + + case *ast.TypeSpec: + // Descend to type name. + path = append([]ast.Node{n.Name}, path...) + continue + + case ast.Stmt: + return path, actionStmt + + case *ast.ArrayType, + *ast.StructType, + *ast.FuncType, + *ast.InterfaceType, + *ast.MapType, + *ast.ChanType: + return path, actionType + + case *ast.Comment, *ast.CommentGroup, *ast.File, *ast.KeyValueExpr, *ast.CommClause: + return path, actionUnknown // uninteresting + + case *ast.Ellipsis: + // Continue to enclosing node. + // e.g. [...]T in ArrayType + // f(x...) in CallExpr + // f(x...T) in FuncType + + case *ast.Field: + // TODO(adonovan): this needs more thought, + // since fields can be so many things. + if len(n.Names) == 1 { + // Descend to sole Ident child. + path = append([]ast.Node{n.Names[0]}, path...) + continue + } + // Zero names (e.g. anon field in struct) + // or multiple field or param names: + // continue to enclosing field list. + + case *ast.FieldList: + // Continue to enclosing node: + // {Struct,Func,Interface}Type or FuncDecl. + + case *ast.BasicLit: + if _, ok := path[1].(*ast.ImportSpec); ok { + return path[1:], actionPackage + } + return path, actionExpr + + case *ast.SelectorExpr: + // TODO(adonovan): use Selections info directly. + if pkginfo.Uses[n.Sel] == nil { + // TODO(adonovan): is this reachable? + return path, actionUnknown + } + // Descend to .Sel child. + path = append([]ast.Node{n.Sel}, path...) + continue + + case *ast.Ident: + switch pkginfo.ObjectOf(n).(type) { + case *types.PkgName: + return path, actionPackage + + case *types.Const: + return path, actionExpr + + case *types.Label: + return path, actionStmt + + case *types.TypeName: + return path, actionType + + case *types.Var: + // For x in 'struct {x T}', return struct type, for now. + if _, ok := path[1].(*ast.Field); ok { + _ = path[2].(*ast.FieldList) // assertion + if _, ok := path[3].(*ast.StructType); ok { + return path[3:], actionType + } + } + return path, actionExpr + + case *types.Func: + return path, actionExpr + + case *types.Builtin: + // For reference to built-in function, return enclosing call. + path = path[1:] // ascend to enclosing function call + continue + + case *types.Nil: + return path, actionExpr + } + + // No object. + switch path[1].(type) { + case *ast.SelectorExpr: + // Return enclosing selector expression. + return path[1:], actionExpr + + case *ast.Field: + // TODO(adonovan): test this. + // e.g. all f in: + // struct { f, g int } + // interface { f() } + // func (f T) method(f, g int) (f, g bool) + // + // switch path[3].(type) { + // case *ast.FuncDecl: + // case *ast.StructType: + // case *ast.InterfaceType: + // } + // + // return path[1:], actionExpr + // + // Unclear what to do with these. + // Struct.Fields -- field + // Interface.Methods -- field + // FuncType.{Params.Results} -- actionExpr + // FuncDecl.Recv -- actionExpr + + case *ast.File: + // 'package foo' + return path, actionPackage + + case *ast.ImportSpec: + // TODO(adonovan): fix: why no package object? go/types bug? + return path[1:], actionPackage + + default: + // e.g. blank identifier + // or y in "switch y := x.(type)" + // or code in a _test.go file that's not part of the package. + log.Printf("unknown reference %s in %T\n", n, path[1]) + return path, actionUnknown + } + + case *ast.StarExpr: + if pkginfo.Types[n].IsType() { + return path, actionType + } + return path, actionExpr + + case ast.Expr: + // All Expr but {BasicLit,Ident,StarExpr} are + // "true" expressions that evaluate to a value. + return path, actionExpr + } + + // Ascend to parent. + path = path[1:] + } + + return nil, actionUnknown // unreachable +} + +func describeValue(qpos *queryPos, path []ast.Node) (*describeValueResult, error) { + var expr ast.Expr + var obj types.Object + switch n := path[0].(type) { + case *ast.ValueSpec: + // ambiguous ValueSpec containing multiple names + return nil, fmt.Errorf("multiple value specification") + case *ast.Ident: + obj = qpos.info.ObjectOf(n) + expr = n + case ast.Expr: + expr = n + default: + // TODO(adonovan): is this reachable? + return nil, fmt.Errorf("unexpected AST for expr: %T", n) + } + + typ := qpos.info.TypeOf(expr) + constVal := qpos.info.Types[expr].Value + + return &describeValueResult{ + qpos: qpos, + expr: expr, + typ: typ, + constVal: constVal, + obj: obj, + }, nil +} + +type describeValueResult struct { + qpos *queryPos + expr ast.Expr // query node + typ types.Type // type of expression + constVal exact.Value // value of expression, if constant + obj types.Object // var/func/const object, if expr was Ident +} + +func (r *describeValueResult) display(printf printfFunc) { + var prefix, suffix string + if r.constVal != nil { + suffix = fmt.Sprintf(" of constant value %s", r.constVal) + } + switch obj := r.obj.(type) { + case *types.Func: + if recv := obj.Type().(*types.Signature).Recv(); recv != nil { + if _, ok := recv.Type().Underlying().(*types.Interface); ok { + prefix = "interface method " + } else { + prefix = "method " + } + } + } + + // Describe the expression. + if r.obj != nil { + if r.obj.Pos() == r.expr.Pos() { + // defining ident + printf(r.expr, "definition of %s%s%s", prefix, r.qpos.objectString(r.obj), suffix) + } else { + // referring ident + printf(r.expr, "reference to %s%s%s", prefix, r.qpos.objectString(r.obj), suffix) + if def := r.obj.Pos(); def != token.NoPos { + printf(def, "defined here") + } + } + } else { + desc := astutil.NodeDescription(r.expr) + if suffix != "" { + // constant expression + printf(r.expr, "%s%s", desc, suffix) + } else { + // non-constant expression + printf(r.expr, "%s of type %s", desc, r.qpos.typeString(r.typ)) + } + } +} + +func (r *describeValueResult) toSerial(res *serial.Result, fset *token.FileSet) { + var value, objpos string + if r.constVal != nil { + value = r.constVal.String() + } + if r.obj != nil { + objpos = fset.Position(r.obj.Pos()).String() + } + + res.Describe = &serial.Describe{ + Desc: astutil.NodeDescription(r.expr), + Pos: fset.Position(r.expr.Pos()).String(), + Detail: "value", + Value: &serial.DescribeValue{ + Type: r.qpos.typeString(r.typ), + Value: value, + ObjPos: objpos, + }, + } +} + +// ---- TYPE ------------------------------------------------------------ + +func describeType(qpos *queryPos, path []ast.Node) (*describeTypeResult, error) { + var description string + var t types.Type + switch n := path[0].(type) { + case *ast.Ident: + t = qpos.info.TypeOf(n) + switch t := t.(type) { + case *types.Basic: + description = "reference to built-in " + + case *types.Named: + isDef := t.Obj().Pos() == n.Pos() // see caveats at isDef above + if isDef { + description = "definition of " + } else { + description = "reference to " + } + } + + case ast.Expr: + t = qpos.info.TypeOf(n) + + default: + // Unreachable? + return nil, fmt.Errorf("unexpected AST for type: %T", n) + } + + description = description + "type " + qpos.typeString(t) + + // Show sizes for structs and named types (it's fairly obvious for others). + switch t.(type) { + case *types.Named, *types.Struct: + szs := types.StdSizes{8, 8} // assume amd64 + description = fmt.Sprintf("%s (size %d, align %d)", description, + szs.Sizeof(t), szs.Alignof(t)) + } + + return &describeTypeResult{ + qpos: qpos, + node: path[0], + description: description, + typ: t, + methods: accessibleMethods(t, qpos.info.Pkg), + }, nil +} + +type describeTypeResult struct { + qpos *queryPos + node ast.Node + description string + typ types.Type + methods []*types.Selection +} + +func (r *describeTypeResult) display(printf printfFunc) { + printf(r.node, "%s", r.description) + + // Show the underlying type for a reference to a named type. + if nt, ok := r.typ.(*types.Named); ok && r.node.Pos() != nt.Obj().Pos() { + printf(nt.Obj(), "defined as %s", r.qpos.typeString(nt.Underlying())) + } + + // Print the method set, if the type kind is capable of bearing methods. + switch r.typ.(type) { + case *types.Interface, *types.Struct, *types.Named: + if len(r.methods) > 0 { + printf(r.node, "Method set:") + for _, meth := range r.methods { + // TODO(adonovan): print these relative + // to the owning package, not the + // query package. + printf(meth.Obj(), "\t%s", r.qpos.selectionString(meth)) + } + } else { + printf(r.node, "No methods.") + } + } +} + +func (r *describeTypeResult) toSerial(res *serial.Result, fset *token.FileSet) { + var namePos, nameDef string + if nt, ok := r.typ.(*types.Named); ok { + namePos = fset.Position(nt.Obj().Pos()).String() + nameDef = nt.Underlying().String() + } + res.Describe = &serial.Describe{ + Desc: r.description, + Pos: fset.Position(r.node.Pos()).String(), + Detail: "type", + Type: &serial.DescribeType{ + Type: r.qpos.typeString(r.typ), + NamePos: namePos, + NameDef: nameDef, + Methods: methodsToSerial(r.qpos.info.Pkg, r.methods, fset), + }, + } +} + +// ---- PACKAGE ------------------------------------------------------------ + +func describePackage(qpos *queryPos, path []ast.Node) (*describePackageResult, error) { + var description string + var pkg *types.Package + switch n := path[0].(type) { + case *ast.ImportSpec: + var obj types.Object + if n.Name != nil { + obj = qpos.info.Defs[n.Name] + } else { + obj = qpos.info.Implicits[n] + } + pkgname, _ := obj.(*types.PkgName) + if pkgname == nil { + return nil, fmt.Errorf("can't import package %s", n.Path.Value) + } + pkg = pkgname.Imported() + description = fmt.Sprintf("import of package %q", pkg.Path()) + + case *ast.Ident: + if _, isDef := path[1].(*ast.File); isDef { + // e.g. package id + pkg = qpos.info.Pkg + description = fmt.Sprintf("definition of package %q", pkg.Path()) + } else { + // e.g. import id "..." + // or id.F() + pkg = qpos.info.ObjectOf(n).(*types.PkgName).Imported() + description = fmt.Sprintf("reference to package %q", pkg.Path()) + } + + default: + // Unreachable? + return nil, fmt.Errorf("unexpected AST for package: %T", n) + } + + var members []*describeMember + // NB: "unsafe" has no types.Package + if pkg != nil { + // Enumerate the accessible package members + // in lexicographic order. + for _, name := range pkg.Scope().Names() { + if pkg == qpos.info.Pkg || ast.IsExported(name) { + mem := pkg.Scope().Lookup(name) + var methods []*types.Selection + if mem, ok := mem.(*types.TypeName); ok { + methods = accessibleMethods(mem.Type(), qpos.info.Pkg) + } + members = append(members, &describeMember{ + mem, + methods, + }) + + } + } + } + + return &describePackageResult{qpos.fset, path[0], description, pkg, members}, nil +} + +type describePackageResult struct { + fset *token.FileSet + node ast.Node + description string + pkg *types.Package + members []*describeMember // in lexicographic name order +} + +type describeMember struct { + obj types.Object + methods []*types.Selection // in types.MethodSet order +} + +func (r *describePackageResult) display(printf printfFunc) { + printf(r.node, "%s", r.description) + + // Compute max width of name "column". + maxname := 0 + for _, mem := range r.members { + if l := len(mem.obj.Name()); l > maxname { + maxname = l + } + } + + for _, mem := range r.members { + printf(mem.obj, "\t%s", formatMember(mem.obj, maxname)) + for _, meth := range mem.methods { + printf(meth.Obj(), "\t\t%s", types.SelectionString(meth, types.RelativeTo(r.pkg))) + } + } +} + +func formatMember(obj types.Object, maxname int) string { + qualifier := types.RelativeTo(obj.Pkg()) + var buf bytes.Buffer + fmt.Fprintf(&buf, "%-5s %-*s", tokenOf(obj), maxname, obj.Name()) + switch obj := obj.(type) { + case *types.Const: + fmt.Fprintf(&buf, " %s = %s", types.TypeString(obj.Type(), qualifier), obj.Val().String()) + + case *types.Func: + fmt.Fprintf(&buf, " %s", types.TypeString(obj.Type(), qualifier)) + + case *types.TypeName: + // Abbreviate long aggregate type names. + var abbrev string + switch t := obj.Type().Underlying().(type) { + case *types.Interface: + if t.NumMethods() > 1 { + abbrev = "interface{...}" + } + case *types.Struct: + if t.NumFields() > 1 { + abbrev = "struct{...}" + } + } + if abbrev == "" { + fmt.Fprintf(&buf, " %s", types.TypeString(obj.Type().Underlying(), qualifier)) + } else { + fmt.Fprintf(&buf, " %s", abbrev) + } + + case *types.Var: + fmt.Fprintf(&buf, " %s", types.TypeString(obj.Type(), qualifier)) + } + return buf.String() +} + +func (r *describePackageResult) toSerial(res *serial.Result, fset *token.FileSet) { + var members []*serial.DescribeMember + for _, mem := range r.members { + typ := mem.obj.Type() + var val string + switch mem := mem.obj.(type) { + case *types.Const: + val = mem.Val().String() + case *types.TypeName: + typ = typ.Underlying() + } + members = append(members, &serial.DescribeMember{ + Name: mem.obj.Name(), + Type: typ.String(), + Value: val, + Pos: fset.Position(mem.obj.Pos()).String(), + Kind: tokenOf(mem.obj), + Methods: methodsToSerial(r.pkg, mem.methods, fset), + }) + } + res.Describe = &serial.Describe{ + Desc: r.description, + Pos: fset.Position(r.node.Pos()).String(), + Detail: "package", + Package: &serial.DescribePackage{ + Path: r.pkg.Path(), + Members: members, + }, + } +} + +func tokenOf(o types.Object) string { + switch o.(type) { + case *types.Func: + return "func" + case *types.Var: + return "var" + case *types.TypeName: + return "type" + case *types.Const: + return "const" + case *types.PkgName: + return "package" + case *types.Builtin: + return "builtin" // e.g. when describing package "unsafe" + case *types.Nil: + return "nil" + case *types.Label: + return "label" + } + panic(o) +} + +// ---- STATEMENT ------------------------------------------------------------ + +func describeStmt(qpos *queryPos, path []ast.Node) (*describeStmtResult, error) { + var description string + switch n := path[0].(type) { + case *ast.Ident: + if qpos.info.Defs[n] != nil { + description = "labelled statement" + } else { + description = "reference to labelled statement" + } + + default: + // Nothing much to say about statements. + description = astutil.NodeDescription(n) + } + return &describeStmtResult{qpos.fset, path[0], description}, nil +} + +type describeStmtResult struct { + fset *token.FileSet + node ast.Node + description string +} + +func (r *describeStmtResult) display(printf printfFunc) { + printf(r.node, "%s", r.description) +} + +func (r *describeStmtResult) toSerial(res *serial.Result, fset *token.FileSet) { + res.Describe = &serial.Describe{ + Desc: r.description, + Pos: fset.Position(r.node.Pos()).String(), + Detail: "unknown", + } +} + +// ------------------- Utilities ------------------- + +// pathToString returns a string containing the concrete types of the +// nodes in path. +func pathToString(path []ast.Node) string { + var buf bytes.Buffer + fmt.Fprint(&buf, "[") + for i, n := range path { + if i > 0 { + fmt.Fprint(&buf, " ") + } + fmt.Fprint(&buf, strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.")) + } + fmt.Fprint(&buf, "]") + return buf.String() +} + +func accessibleMethods(t types.Type, from *types.Package) []*types.Selection { + var methods []*types.Selection + for _, meth := range typeutil.IntuitiveMethodSet(t, nil) { + if isAccessibleFrom(meth.Obj(), from) { + methods = append(methods, meth) + } + } + return methods +} + +func isAccessibleFrom(obj types.Object, pkg *types.Package) bool { + return ast.IsExported(obj.Name()) || obj.Pkg() == pkg +} + +func methodsToSerial(this *types.Package, methods []*types.Selection, fset *token.FileSet) []serial.DescribeMethod { + qualifier := types.RelativeTo(this) + var jmethods []serial.DescribeMethod + for _, meth := range methods { + var ser serial.DescribeMethod + if meth != nil { // may contain nils when called by implements (on a method) + ser = serial.DescribeMethod{ + Name: types.SelectionString(meth, qualifier), + Pos: fset.Position(meth.Obj().Pos()).String(), + } + } + jmethods = append(jmethods, ser) + } + return jmethods +} diff --git a/cmd/guru/emacs-test.bash b/cmd/guru/emacs-test.bash new file mode 100755 index 00000000..154a1f7c --- /dev/null +++ b/cmd/guru/emacs-test.bash @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Simple test of Go guru/Emacs integration. +# Requires that GOROOT and GOPATH are set. +# Side effect: builds and installs guru in $GOROOT. + +set -eu + +[ -z "$GOROOT" ] && { echo "Error: GOROOT is unset." >&2; exit 1; } +[ -z "$GOPATH" ] && { echo "Error: GOPATH is unset." >&2; exit 1; } + +log=/tmp/$(basename $0)-$$.log +thisdir=$(dirname $0) + +function die() { + echo "Error: $@." + cat $log + exit 1 +} >&2 + +trap "rm -f $log" EXIT + +# Build and install guru. +go get golang.org/x/tools/cmd/guru || die "'go get' failed" +mv -f $GOPATH/bin/guru $GOROOT/bin/ +$GOROOT/bin/guru >$log 2>&1 || true # (prints usage and exits 1) +grep -q "Run.*help" $log || die "$GOROOT/bin/guru not installed" + + +# Run Emacs, set the scope to the guru tool itself, +# load ./main.go, and describe the "fmt" import. +emacs --batch --no-splash --no-window-system --no-init \ + --load $GOROOT/misc/emacs/go-mode.el \ + --load $thisdir/guru.el \ + --eval ' +(progn + (setq go-guru-scope "golang.org/x/tools/cmd/guru") + (find-file "'$thisdir'/main.go") + (search-forward "\"fmt\"") + (backward-char) + (go-guru-describe) + (princ (with-current-buffer "*go-guru*" + (buffer-substring-no-properties (point-min) (point-max)))) + (kill-emacs 0)) +' main.go >$log 2>&1 || die "emacs command failed" + +# Check that Println is mentioned. +grep -q "fmt/print.go.*func Println" $log || die "didn't find expected lines in log; got:" + +echo "PASS" diff --git a/cmd/guru/freevars.go b/cmd/guru/freevars.go new file mode 100644 index 00000000..60f4db9b --- /dev/null +++ b/cmd/guru/freevars.go @@ -0,0 +1,222 @@ +// Copyright 2013 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 main + +import ( + "bytes" + "go/ast" + "go/printer" + "go/token" + "go/types" + "sort" + + "golang.org/x/tools/go/loader" + "golang.org/x/tools/cmd/guru/serial" +) + +// freevars displays the lexical (not package-level) free variables of +// the selection. +// +// It treats A.B.C as a separate variable from A to reveal the parts +// of an aggregate type that are actually needed. +// This aids refactoring. +// +// TODO(adonovan): optionally display the free references to +// file/package scope objects, and to objects from other packages. +// Depending on where the resulting function abstraction will go, +// these might be interesting. Perhaps group the results into three +// bands. +// +func freevars(q *Query) error { + lconf := loader.Config{Build: q.Build} + allowErrors(&lconf) + + if _, err := importQueryPackage(q.Pos, &lconf); err != nil { + return err + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err := parseQueryPos(lprog, q.Pos, false) + if err != nil { + return err + } + + file := qpos.path[len(qpos.path)-1] // the enclosing file + fileScope := qpos.info.Scopes[file] + pkgScope := fileScope.Parent() + + // The id and sel functions return non-nil if they denote an + // object o or selection o.x.y that is referenced by the + // selection but defined neither within the selection nor at + // file scope, i.e. it is in the lexical environment. + var id func(n *ast.Ident) types.Object + var sel func(n *ast.SelectorExpr) types.Object + + sel = func(n *ast.SelectorExpr) types.Object { + switch x := unparen(n.X).(type) { + case *ast.SelectorExpr: + return sel(x) + case *ast.Ident: + return id(x) + } + return nil + } + + id = func(n *ast.Ident) types.Object { + obj := qpos.info.Uses[n] + if obj == nil { + return nil // not a reference + } + if _, ok := obj.(*types.PkgName); ok { + return nil // imported package + } + if !(file.Pos() <= obj.Pos() && obj.Pos() <= file.End()) { + return nil // not defined in this file + } + scope := obj.Parent() + if scope == nil { + return nil // e.g. interface method, struct field + } + if scope == fileScope || scope == pkgScope { + return nil // defined at file or package scope + } + if qpos.start <= obj.Pos() && obj.Pos() <= qpos.end { + return nil // defined within selection => not free + } + return obj + } + + // Maps each reference that is free in the selection + // to the object it refers to. + // The map de-duplicates repeated references. + refsMap := make(map[string]freevarsRef) + + // Visit all the identifiers in the selected ASTs. + ast.Inspect(qpos.path[0], func(n ast.Node) bool { + if n == nil { + return true // popping DFS stack + } + + // Is this node contained within the selection? + // (freevars permits inexact selections, + // like two stmts in a block.) + if qpos.start <= n.Pos() && n.End() <= qpos.end { + var obj types.Object + var prune bool + switch n := n.(type) { + case *ast.Ident: + obj = id(n) + + case *ast.SelectorExpr: + obj = sel(n) + prune = true + } + + if obj != nil { + var kind string + switch obj.(type) { + case *types.Var: + kind = "var" + case *types.Func: + kind = "func" + case *types.TypeName: + kind = "type" + case *types.Const: + kind = "const" + case *types.Label: + kind = "label" + default: + panic(obj) + } + + typ := qpos.info.TypeOf(n.(ast.Expr)) + ref := freevarsRef{kind, printNode(lprog.Fset, n), typ, obj} + refsMap[ref.ref] = ref + + if prune { + return false // don't descend + } + } + } + + return true // descend + }) + + refs := make([]freevarsRef, 0, len(refsMap)) + for _, ref := range refsMap { + refs = append(refs, ref) + } + sort.Sort(byRef(refs)) + + q.result = &freevarsResult{ + qpos: qpos, + refs: refs, + } + return nil +} + +type freevarsResult struct { + qpos *queryPos + refs []freevarsRef +} + +type freevarsRef struct { + kind string + ref string + typ types.Type + obj types.Object +} + +func (r *freevarsResult) display(printf printfFunc) { + if len(r.refs) == 0 { + printf(r.qpos, "No free identifiers.") + } else { + printf(r.qpos, "Free identifiers:") + qualifier := types.RelativeTo(r.qpos.info.Pkg) + for _, ref := range r.refs { + // Avoid printing "type T T". + var typstr string + if ref.kind != "type" { + typstr = " " + types.TypeString(ref.typ, qualifier) + } + printf(ref.obj, "%s %s%s", ref.kind, ref.ref, typstr) + } + } +} + +func (r *freevarsResult) toSerial(res *serial.Result, fset *token.FileSet) { + var refs []*serial.FreeVar + for _, ref := range r.refs { + refs = append(refs, + &serial.FreeVar{ + Pos: fset.Position(ref.obj.Pos()).String(), + Kind: ref.kind, + Ref: ref.ref, + Type: ref.typ.String(), + }) + } + res.Freevars = refs +} + +// -------- utils -------- + +type byRef []freevarsRef + +func (p byRef) Len() int { return len(p) } +func (p byRef) Less(i, j int) bool { return p[i].ref < p[j].ref } +func (p byRef) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +// printNode returns the pretty-printed syntax of n. +func printNode(fset *token.FileSet, n ast.Node) string { + var buf bytes.Buffer + printer.Fprint(&buf, fset, n) + return buf.String() +} diff --git a/cmd/guru/guru.el b/cmd/guru/guru.el new file mode 100644 index 00000000..e33c30c7 --- /dev/null +++ b/cmd/guru/guru.el @@ -0,0 +1,230 @@ +;;; +;;; Integration of the Go 'guru' analysis tool into Emacs. +;;; +;;; To install the Go guru, run: +;;; % export GOROOT=... GOPATH=... +;;; % go get golang.org/x/tools/cmd/guru +;;; % mv $GOPATH/bin/guru $GOROOT/bin/ +;;; +;;; Load this file into Emacs and set go-guru-scope to your +;;; configuration. Then, find a file of Go source code, enable +;;; go-guru-mode, select an expression of interest, and press `C-c C-o d' +;;; (for "describe") or run one of the other go-guru-xxx commands. +;;; +;;; TODO(adonovan): simplify installation and configuration by making +;;; guru a subcommand of 'go tool'. + +(require 'compile) +(require 'go-mode) +(require 'cl) + +(defgroup go-guru nil + "Options specific to the Go guru." + :group 'go) + +(defcustom go-guru-command "guru" + "The Go guru command." + :type 'string + :group 'go-guru) + +(defcustom go-guru-scope "" + "The scope of the analysis. See `go-guru-set-scope'." + :type 'string + :group 'go-guru) + +(defvar go-guru--scope-history + nil + "History of values supplied to `go-guru-set-scope'.") + +;; Extend go-mode-map. +(let ((m go-mode-map)) + (define-key m (kbd "C-c C-o t") #'go-guru-describe) ; t for type + (define-key m (kbd "C-c C-o f") #'go-guru-freevars) + (define-key m (kbd "C-c C-o g") #'go-guru-callgraph) + (define-key m (kbd "C-c C-o i") #'go-guru-implements) + (define-key m (kbd "C-c C-o c") #'go-guru-peers) ; c for channel + (define-key m (kbd "C-c C-o r") #'go-guru-referrers) + (define-key m (kbd "C-c C-o d") #'go-guru-definition) + (define-key m (kbd "C-c C-o p") #'go-guru-pointsto) + (define-key m (kbd "C-c C-o s") #'go-guru-callstack) + (define-key m (kbd "C-c C-o <") #'go-guru-callers) + (define-key m (kbd "C-c C-o >") #'go-guru-callees) + (define-key m (kbd "") #'go-guru-describe) + (define-key m (kbd "") #'go-guru-referrers)) + +;; TODO(dominikh): Rethink set-scope some. Setting it to a file is +;; painful because it doesn't use find-file, and variables/~ aren't +;; expanded. Setting it to an import path is somewhat painful because +;; it doesn't make use of go-mode's import path completion. One option +;; would be having two different functions, but then we can't +;; automatically call it when no scope has been set. Also it wouldn't +;; easily allow specifying more than one file/package. +(defun go-guru-set-scope () + "Set the scope for the Go guru, prompting the user to edit the +previous scope. + +The scope specifies a set of arguments, separated by spaces. +It may be: +1) a set of packages whose main() functions will be analyzed. +2) a list of *.go filenames; they will treated like as a single + package (see #3). +3) a single package whose main() function and/or Test* functions + will be analyzed. + +In the common case, this is similar to the argument(s) you would +specify to 'go build'." + (interactive) + (let ((scope (read-from-minibuffer "Go guru scope: " + go-guru-scope + nil + nil + 'go-guru--scope-history))) + (if (string-equal "" scope) + (error "You must specify a non-empty scope for the Go guru")) + (setq go-guru-scope scope))) + +(defun go-guru--run (mode &optional need-scope) + "Run the Go guru in the specified MODE, passing it the +selected region of the current buffer. If NEED-SCOPE, prompt for +a scope if not already set. Process the output to replace each +file name with a small hyperlink. Display the result." + (if (not buffer-file-name) + (error "Cannot use guru on a buffer without a file name")) + ;; It's not sufficient to save a modified buffer since if + ;; gofmt-before-save is on the before-save-hook, saving will + ;; disturb the selected region. + (if (buffer-modified-p) + (error "Please save the buffer before invoking go-guru")) + (and need-scope + (string-equal "" go-guru-scope) + (go-guru-set-scope)) + (let* ((filename (file-truename buffer-file-name)) + (posflag (if (use-region-p) + (format "-pos=%s:#%d,#%d" + filename + (1- (go--position-bytes (region-beginning))) + (1- (go--position-bytes (region-end)))) + (format "-pos=%s:#%d" + filename + (1- (position-bytes (point)))))) + (env-vars (go-root-and-paths)) + (goroot-env (concat "GOROOT=" (car env-vars))) + (gopath-env (concat "GOPATH=" (mapconcat #'identity (cdr env-vars) ":")))) + (with-current-buffer (get-buffer-create "*go-guru*") + (setq buffer-read-only nil) + (erase-buffer) + (insert "Go Guru\n") + (let ((args (append (list go-guru-command nil t nil posflag mode) + (split-string go-guru-scope " " t)))) + ;; Log the command to *Messages*, for debugging. + (message "Command: %s:" args) + (message nil) ; clears/shrinks minibuffer + + (message "Running guru...") + ;; Use dynamic binding to modify/restore the environment + (let ((process-environment (list* goroot-env gopath-env process-environment))) + (apply #'call-process args))) + (insert "\n") + (compilation-mode) + (setq compilation-error-screen-columns nil) + + ;; Hide the file/line info to save space. + ;; Replace each with a little widget. + ;; compilation-mode + this loop = slooow. + ;; TODO(adonovan): have guru give us JSON + ;; and we'll do the markup directly. + (let ((buffer-read-only nil) + (p 1)) + (while (not (null p)) + (let ((np (compilation-next-single-property-change p 'compilation-message))) + (if np + (when (equal (line-number-at-pos p) (line-number-at-pos np)) + ;; Using a fixed width greatly improves readability, so + ;; if the filename is longer than 20, show ".../last/17chars.go". + ;; This usually includes the last segment of the package name. + ;; Don't show the line or column number. + (let* ((loc (buffer-substring p np)) ; "/home/foo/go/pkg/file.go:1:2-3:4" + (i (search ":" loc))) + (setq loc (cond + ((null i) "...") + ((>= i 17) (concat "..." (substring loc (- i 17) i))) + (t (substring loc 0 i)))) + ;; np is (typically) the space following ":"; consume it too. + (put-text-property p np 'display (concat loc ":"))) + (goto-char np) + (insert " ") + (incf np))) ; so we don't get stuck (e.g. on a panic stack dump) + (setq p np))) + (message nil)) + + (let ((w (display-buffer (current-buffer)))) + (balance-windows) + (shrink-window-if-larger-than-buffer w) + (set-window-point w (point-min)))))) + +(defun go-guru-callees () + "Show possible callees of the function call at the current point." + (interactive) + (go-guru--run "callees" t)) + +(defun go-guru-callers () + "Show the set of callers of the function containing the current point." + (interactive) + (go-guru--run "callers" t)) + +(defun go-guru-callgraph () + "Show the callgraph of the current program." + (interactive) + (go-guru--run "callgraph" t)) + +(defun go-guru-callstack () + "Show an arbitrary path from a root of the call graph to the +function containing the current point." + (interactive) + (go-guru--run "callstack" t)) + +(defun go-guru-definition () + "Show the definition of the selected identifier." + (interactive) + (go-guru--run "definition")) + +(defun go-guru-describe () + "Describe the selected syntax, its kind, type and methods." + (interactive) + (go-guru--run "describe")) + +(defun go-guru-pointsto () + "Show what the selected expression points to." + (interactive) + (go-guru--run "pointsto" t)) + +(defun go-guru-implements () + "Describe the 'implements' relation for types in the package +containing the current point." + (interactive) + (go-guru--run "implements")) + +(defun go-guru-freevars () + "Enumerate the free variables of the current selection." + (interactive) + (go-guru--run "freevars")) + +(defun go-guru-peers () + "Enumerate the set of possible corresponding sends/receives for +this channel receive/send operation." + (interactive) + (go-guru--run "peers" t)) + +(defun go-guru-referrers () + "Enumerate all references to the object denoted by the selected +identifier." + (interactive) + (go-guru--run "referrers")) + +(defun go-guru-whicherrs () + "Show globals, constants and types to which the selected +expression (of type 'error') may refer." + (interactive) + (go-guru--run "whicherrs" t)) + +(provide 'go-guru) diff --git a/cmd/guru/guru.go b/cmd/guru/guru.go new file mode 100644 index 00000000..69becfe0 --- /dev/null +++ b/cmd/guru/guru.go @@ -0,0 +1,356 @@ +// 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 main + +// TODO(adonovan): new queries +// - show all statements that may update the selected lvalue +// (local, global, field, etc). +// - show all places where an object of type T is created +// (&T{}, var t T, new(T), new(struct{array [3]T}), etc. + +import ( + "fmt" + "go/ast" + "go/build" + "go/parser" + "go/token" + "go/types" + "io" + "path/filepath" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/pointer" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/cmd/guru/serial" +) + +type printfFunc func(pos interface{}, format string, args ...interface{}) + +// queryResult is the interface of each query-specific result type. +type queryResult interface { + toSerial(res *serial.Result, fset *token.FileSet) + display(printf printfFunc) +} + +// A QueryPos represents the position provided as input to a query: +// a textual extent in the program's source code, the AST node it +// corresponds to, and the package to which it belongs. +// Instances are created by parseQueryPos. +type queryPos struct { + fset *token.FileSet + start, end token.Pos // source extent of query + path []ast.Node // AST path from query node to root of ast.File + exact bool // 2nd result of PathEnclosingInterval + info *loader.PackageInfo // type info for the queried package (nil for fastQueryPos) +} + +// TypeString prints type T relative to the query position. +func (qpos *queryPos) typeString(T types.Type) string { + return types.TypeString(T, types.RelativeTo(qpos.info.Pkg)) +} + +// ObjectString prints object obj relative to the query position. +func (qpos *queryPos) objectString(obj types.Object) string { + return types.ObjectString(obj, types.RelativeTo(qpos.info.Pkg)) +} + +// SelectionString prints selection sel relative to the query position. +func (qpos *queryPos) selectionString(sel *types.Selection) string { + return types.SelectionString(sel, types.RelativeTo(qpos.info.Pkg)) +} + +// A Query specifies a single guru query. +type Query struct { + Mode string // query mode ("callers", etc) + Pos string // query position + Build *build.Context // package loading configuration + + // pointer analysis options + Scope []string // main packages in (*loader.Config).FromArgs syntax + PTALog io.Writer // (optional) pointer-analysis log file + Reflection bool // model reflection soundly (currently slow). + + // Populated during Run() + Fset *token.FileSet + result queryResult +} + +// Serial returns an instance of serial.Result, which implements the +// {xml,json}.Marshaler interfaces so that query results can be +// serialized as JSON or XML. +// +func (q *Query) Serial() *serial.Result { + resj := &serial.Result{Mode: q.Mode} + q.result.toSerial(resj, q.Fset) + return resj +} + +// WriteTo writes the guru query result res to out in a compiler diagnostic format. +func (q *Query) WriteTo(out io.Writer) { + printf := func(pos interface{}, format string, args ...interface{}) { + fprintf(out, q.Fset, pos, format, args...) + } + q.result.display(printf) +} + +// Run runs an guru query and populates its Fset and Result. +func Run(q *Query) error { + switch q.Mode { + case "callees": + return callees(q) + case "callers": + return callers(q) + case "callstack": + return callstack(q) + case "peers": + return peers(q) + case "pointsto": + return pointsto(q) + case "whicherrs": + return whicherrs(q) + case "definition": + return definition(q) + case "describe": + return describe(q) + case "freevars": + return freevars(q) + case "implements": + return implements(q) + case "referrers": + return referrers(q) + case "what": + return what(q) + default: + return fmt.Errorf("invalid mode: %q", q.Mode) + } +} + +func setPTAScope(lconf *loader.Config, scope []string) error { + if len(scope) == 0 { + return fmt.Errorf("no packages specified for pointer analysis scope") + } + + // Determine initial packages for PTA. + args, err := lconf.FromArgs(scope, true) + if err != nil { + return err + } + if len(args) > 0 { + return fmt.Errorf("surplus arguments: %q", args) + } + return nil +} + +// Create a pointer.Config whose scope is the initial packages of lprog +// and their dependencies. +func setupPTA(prog *ssa.Program, lprog *loader.Program, ptaLog io.Writer, reflection bool) (*pointer.Config, error) { + // TODO(adonovan): the body of this function is essentially + // duplicated in all go/pointer clients. Refactor. + + // For each initial package (specified on the command line), + // if it has a main function, analyze that, + // otherwise analyze its tests, if any. + var testPkgs, mains []*ssa.Package + for _, info := range lprog.InitialPackages() { + initialPkg := prog.Package(info.Pkg) + + // Add package to the pointer analysis scope. + if initialPkg.Func("main") != nil { + mains = append(mains, initialPkg) + } else { + testPkgs = append(testPkgs, initialPkg) + } + } + if testPkgs != nil { + if p := prog.CreateTestMainPackage(testPkgs...); p != nil { + mains = append(mains, p) + } + } + if mains == nil { + return nil, fmt.Errorf("analysis scope has no main and no tests") + } + return &pointer.Config{ + Log: ptaLog, + Reflection: reflection, + Mains: mains, + }, nil +} + +// importQueryPackage finds the package P containing the +// query position and tells conf to import it. +// It returns the package's path. +func importQueryPackage(pos string, conf *loader.Config) (string, error) { + fqpos, err := fastQueryPos(pos) + if err != nil { + return "", err // bad query + } + filename := fqpos.fset.File(fqpos.start).Name() + + // This will not work for ad-hoc packages + // such as $GOROOT/src/net/http/triv.go. + // TODO(adonovan): ensure we report a clear error. + _, importPath, err := guessImportPath(filename, conf.Build) + if err != nil { + return "", err // can't find GOPATH dir + } + if importPath == "" { + return "", fmt.Errorf("can't guess import path from %s", filename) + } + + // Check that it's possible to load the queried package. + // (e.g. guru tests contain different 'package' decls in same dir.) + // Keep consistent with logic in loader/util.go! + cfg2 := *conf.Build + cfg2.CgoEnabled = false + bp, err := cfg2.Import(importPath, "", 0) + if err != nil { + return "", err // no files for package + } + + switch pkgContainsFile(bp, filename) { + case 'T': + conf.ImportWithTests(importPath) + case 'X': + conf.ImportWithTests(importPath) + importPath += "_test" // for TypeCheckFuncBodies + case 'G': + conf.Import(importPath) + default: + return "", fmt.Errorf("package %q doesn't contain file %s", + importPath, filename) + } + + conf.TypeCheckFuncBodies = func(p string) bool { return p == importPath } + + return importPath, nil +} + +// pkgContainsFile reports whether file was among the packages Go +// files, Test files, eXternal test files, or not found. +func pkgContainsFile(bp *build.Package, filename string) byte { + for i, files := range [][]string{bp.GoFiles, bp.TestGoFiles, bp.XTestGoFiles} { + for _, file := range files { + if sameFile(filepath.Join(bp.Dir, file), filename) { + return "GTX"[i] + } + } + } + return 0 // not found +} + +// ParseQueryPos parses the source query position pos and returns the +// AST node of the loaded program lprog that it identifies. +// If needExact, it must identify a single AST subtree; +// this is appropriate for queries that allow fairly arbitrary syntax, +// e.g. "describe". +// +func parseQueryPos(lprog *loader.Program, posFlag string, needExact bool) (*queryPos, error) { + filename, startOffset, endOffset, err := parsePosFlag(posFlag) + if err != nil { + return nil, err + } + start, end, err := findQueryPos(lprog.Fset, filename, startOffset, endOffset) + if err != nil { + return nil, err + } + info, path, exact := lprog.PathEnclosingInterval(start, end) + if path == nil { + return nil, fmt.Errorf("no syntax here") + } + if needExact && !exact { + return nil, fmt.Errorf("ambiguous selection within %s", astutil.NodeDescription(path[0])) + } + return &queryPos{lprog.Fset, start, end, path, exact, info}, nil +} + +// ---------- Utilities ---------- + +// allowErrors causes type errors to be silently ignored. +// (Not suitable if SSA construction follows.) +func allowErrors(lconf *loader.Config) { + ctxt := *lconf.Build // copy + ctxt.CgoEnabled = false + lconf.Build = &ctxt + lconf.AllowErrors = true + // AllErrors makes the parser always return an AST instead of + // bailing out after 10 errors and returning an empty ast.File. + lconf.ParserMode = parser.AllErrors + lconf.TypeChecker.Error = func(err error) {} +} + +// ptrAnalysis runs the pointer analysis and returns its result. +func ptrAnalysis(conf *pointer.Config) *pointer.Result { + result, err := pointer.Analyze(conf) + if err != nil { + panic(err) // pointer analysis internal error + } + return result +} + +func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } + +// deref returns a pointer's element type; otherwise it returns typ. +func deref(typ types.Type) types.Type { + if p, ok := typ.Underlying().(*types.Pointer); ok { + return p.Elem() + } + return typ +} + +// fprintf prints to w a message of the form "location: message\n" +// where location is derived from pos. +// +// pos must be one of: +// - a token.Pos, denoting a position +// - an ast.Node, denoting an interval +// - anything with a Pos() method: +// ssa.Member, ssa.Value, ssa.Instruction, types.Object, pointer.Label, etc. +// - a QueryPos, denoting the extent of the user's query. +// - nil, meaning no position at all. +// +// The output format is is compatible with the 'gnu' +// compilation-error-regexp in Emacs' compilation mode. +// TODO(adonovan): support other editors. +// +func fprintf(w io.Writer, fset *token.FileSet, pos interface{}, format string, args ...interface{}) { + var start, end token.Pos + switch pos := pos.(type) { + case ast.Node: + start = pos.Pos() + end = pos.End() + case token.Pos: + start = pos + end = start + case interface { + Pos() token.Pos + }: + start = pos.Pos() + end = start + case *queryPos: + start = pos.start + end = pos.end + case nil: + // no-op + default: + panic(fmt.Sprintf("invalid pos: %T", pos)) + } + + if sp := fset.Position(start); start == end { + // (prints "-: " for token.NoPos) + fmt.Fprintf(w, "%s: ", sp) + } else { + ep := fset.Position(end) + // The -1 below is a concession to Emacs's broken use of + // inclusive (not half-open) intervals. + // Other editors may not want it. + // TODO(adonovan): add an -editor=vim|emacs|acme|auto + // flag; auto uses EMACS=t / VIM=... / etc env vars. + fmt.Fprintf(w, "%s:%d.%d-%d.%d: ", + sp.Filename, sp.Line, sp.Column, ep.Line, ep.Column-1) + } + fmt.Fprintf(w, format, args...) + io.WriteString(w, "\n") +} diff --git a/cmd/guru/guru.vim b/cmd/guru/guru.vim new file mode 100644 index 00000000..ab96fef1 --- /dev/null +++ b/cmd/guru/guru.vim @@ -0,0 +1,107 @@ +" -*- text -*- +" guru.vim -- Vim integration for the Go guru. +" +" Load with (e.g.) :source guru.vim +" Call with (e.g.) :GoGuruDescribe +" while cursor or selection is over syntax of interest. +" Run :copen to show the quick-fix file. +" +" This is an absolutely rudimentary integration of the Go Guru into +" Vim's quickfix mechanism and it needs a number of usability +" improvements before it can be practically useful to Vim users. +" Voluntary contributions welcomed! +" +" TODO(adonovan): +" - reject buffers with no filename. +" - hide all filenames in quickfix buffer. + +" Get the path to the Go guru executable. +func! s:go_guru_bin() + let [ext, sep] = (has('win32') || has('win64') ? ['.exe', ';'] : ['', ':']) + let go_guru = globpath(join(split($GOPATH, sep), ','), '/bin/guru' . ext) + if go_guru == '' + let go_guru = globpath($GOROOT, '/bin/guru' . ext) + endif + return go_guru +endfunction + +let s:go_guru = s:go_guru_bin() + +func! s:qflist(output) + let qflist = [] + " Parse GNU-style 'file:line.col-line.col: message' format. + let mx = '^\(\a:[\\/][^:]\+\|[^:]\+\):\(\d\+\):\(\d\+\):\(.*\)$' + for line in split(a:output, "\n") + let ml = matchlist(line, mx) + " Ignore non-match lines or warnings + if ml == [] || ml[4] =~ '^ warning:' + continue + endif + let item = { + \ 'filename': ml[1], + \ 'text': ml[4], + \ 'lnum': ml[2], + \ 'col': ml[3], + \} + let bnr = bufnr(fnameescape(ml[1])) + if bnr != -1 + let item['bufnr'] = bnr + endif + call add(qflist, item) + endfor + call setqflist(qflist) + cwindow +endfun + +func! s:getpos(l, c) + if &encoding != 'utf-8' + let buf = a:l == 1 ? '' : (join(getline(1, a:l-1), "\n") . "\n") + let buf .= a:c == 1 ? '' : getline('.')[:a:c-2] + return len(iconv(buf, &encoding, 'utf-8')) + endif + return line2byte(a:l) + (a:c-2) +endfun + +func! s:RunGuru(mode, selected) range abort + let fname = expand('%:p') + let sname = get(g:, 'go_guru_scope_file', fname) + if a:selected != -1 + let pos1 = s:getpos(line("'<"), col("'<")) + let pos2 = s:getpos(line("'>"), col("'>")) + let cmd = printf('%s -pos=%s:#%d,#%d %s %s', + \ s:go_guru, + \ shellescape(fname), pos1, pos2, a:mode, shellescape(sname)) + else + let pos = s:getpos(line('.'), col('.')) + let cmd = printf('%s -pos=%s:#%d %s %s', + \ s:go_guru, + \ shellescape(fname), pos, a:mode, shellescape(sname)) + endif + call s:qflist(system(cmd)) +endfun + +" Describe the expression at the current point. +command! -range=% GoGuruDescribe + \ call s:RunGuru('describe', ) + +" Show possible callees of the function call at the current point. +command! -range=% GoGuruCallees + \ call s:RunGuru('callees', ) + +" Show the set of callers of the function containing the current point. +command! -range=% GoGuruCallers + \ call s:RunGuru('callers', ) + +" Show the callgraph of the current program. +command! -range=% GoGuruCallgraph + \ call s:RunGuru('callgraph', ) + +" Describe the 'implements' relation for types in the +" package containing the current point. +command! -range=% GoGuruImplements + \ call s:RunGuru('implements', ) + +" Enumerate the set of possible corresponding sends/receives for +" this channel receive/send operation. +command! -range=% GoGuruChannelPeers + \ call s:RunGuru('peers', ) diff --git a/cmd/guru/guru_test.go b/cmd/guru/guru_test.go new file mode 100644 index 00000000..a3ee4b5c --- /dev/null +++ b/cmd/guru/guru_test.go @@ -0,0 +1,272 @@ +// Copyright 2013 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 main_test + +// This file defines a test framework for guru queries. +// +// The files beneath testdata/src/main contain Go programs containing +// query annotations of the form: +// +// @verb id "select" +// +// where verb is the query mode (e.g. "callers"), id is a unique name +// for this query, and "select" is a regular expression matching the +// substring of the current line that is the query's input selection. +// +// The expected output for each query is provided in the accompanying +// .golden file. +// +// (Location information is not included because it's too fragile to +// display as text. TODO(adonovan): think about how we can test its +// correctness, since it is critical information.) +// +// Run this test with: +// % go test golang.org/x/tools/cmd/guru -update +// to update the golden files. + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "go/build" + "go/parser" + "go/token" + "io" + "io/ioutil" + "os" + "os/exec" + "regexp" + "runtime" + "strconv" + "strings" + "testing" + + guru "golang.org/x/tools/cmd/guru" +) + +var updateFlag = flag.Bool("update", false, "Update the golden files.") + +type query struct { + id string // unique id + verb string // query mode, e.g. "callees" + posn token.Position // position of of query + filename string + queryPos string // value of -pos flag +} + +func parseRegexp(text string) (*regexp.Regexp, error) { + pattern, err := strconv.Unquote(text) + if err != nil { + return nil, fmt.Errorf("can't unquote %s", text) + } + return regexp.Compile(pattern) +} + +// parseQueries parses and returns the queries in the named file. +func parseQueries(t *testing.T, filename string) []*query { + filedata, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + + // Parse the file once to discover the test queries. + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filename, filedata, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + + lines := bytes.Split(filedata, []byte("\n")) + + var queries []*query + queriesById := make(map[string]*query) + + // Find all annotations of these forms: + expectRe := regexp.MustCompile(`@([a-z]+)\s+(\S+)\s+(\".*)$`) // @verb id "regexp" + for _, c := range f.Comments { + text := strings.TrimSpace(c.Text()) + if text == "" || text[0] != '@' { + continue + } + posn := fset.Position(c.Pos()) + + // @verb id "regexp" + match := expectRe.FindStringSubmatch(text) + if match == nil { + t.Errorf("%s: ill-formed query: %s", posn, text) + continue + } + + id := match[2] + if prev, ok := queriesById[id]; ok { + t.Errorf("%s: duplicate id %s", posn, id) + t.Errorf("%s: previously used here", prev.posn) + continue + } + + q := &query{ + id: id, + verb: match[1], + filename: filename, + posn: posn, + } + + if match[3] != `"nopos"` { + selectRe, err := parseRegexp(match[3]) + if err != nil { + t.Errorf("%s: %s", posn, err) + continue + } + + // Find text of the current line, sans query. + // (Queries must be // not /**/ comments.) + line := lines[posn.Line-1][:posn.Column-1] + + // Apply regexp to current line to find input selection. + loc := selectRe.FindIndex(line) + if loc == nil { + t.Errorf("%s: selection pattern %s doesn't match line %q", + posn, match[3], string(line)) + continue + } + + // Assumes ASCII. TODO(adonovan): test on UTF-8. + linestart := posn.Offset - (posn.Column - 1) + + // Compute the file offsets. + q.queryPos = fmt.Sprintf("%s:#%d,#%d", + filename, linestart+loc[0], linestart+loc[1]) + } + + queries = append(queries, q) + queriesById[id] = q + } + + // Return the slice, not map, for deterministic iteration. + return queries +} + +// WriteResult writes res (-format=plain) to w, stripping file locations. +func WriteResult(w io.Writer, q *guru.Query) { + capture := new(bytes.Buffer) // capture standard output + q.WriteTo(capture) + for _, line := range strings.Split(capture.String(), "\n") { + // Remove a "file:line: " prefix. + if i := strings.Index(line, ": "); i >= 0 { + line = line[i+2:] + } + fmt.Fprintf(w, "%s\n", line) + } +} + +// doQuery poses query q to the guru and writes its response and +// error (if any) to out. +func doQuery(out io.Writer, q *query, useJson bool) { + fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id) + + var buildContext = build.Default + buildContext.GOPATH = "testdata" + query := guru.Query{ + Mode: q.verb, + Pos: q.queryPos, + Build: &buildContext, + Scope: []string{q.filename}, + Reflection: true, + } + if err := guru.Run(&query); err != nil { + fmt.Fprintf(out, "\nError: %s\n", err) + return + } + + if useJson { + // JSON output + b, err := json.MarshalIndent(query.Serial(), "", "\t") + if err != nil { + fmt.Fprintf(out, "JSON error: %s\n", err.Error()) + return + } + out.Write(b) + fmt.Fprintln(out) + } else { + // "plain" (compiler diagnostic format) output + WriteResult(out, &query) + } +} + +func TestGuru(t *testing.T) { + switch runtime.GOOS { + case "android": + t.Skipf("skipping test on %q (no testdata dir)", runtime.GOOS) + case "windows": + t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS) + } + + for _, filename := range []string{ + "testdata/src/calls/main.go", + "testdata/src/describe/main.go", + "testdata/src/freevars/main.go", + "testdata/src/implements/main.go", + "testdata/src/implements-methods/main.go", + "testdata/src/imports/main.go", + "testdata/src/peers/main.go", + "testdata/src/pointsto/main.go", + "testdata/src/referrers/main.go", + "testdata/src/reflection/main.go", + "testdata/src/what/main.go", + "testdata/src/whicherrs/main.go", + // JSON: + // TODO(adonovan): most of these are very similar; combine them. + "testdata/src/calls-json/main.go", + "testdata/src/peers-json/main.go", + "testdata/src/describe-json/main.go", + "testdata/src/implements-json/main.go", + "testdata/src/implements-methods-json/main.go", + "testdata/src/pointsto-json/main.go", + "testdata/src/referrers-json/main.go", + "testdata/src/what-json/main.go", + } { + useJson := strings.Contains(filename, "-json/") + queries := parseQueries(t, filename) + golden := filename + "lden" + got := filename + "t" + gotfh, err := os.Create(got) + if err != nil { + t.Errorf("Create(%s) failed: %s", got, err) + continue + } + defer gotfh.Close() + defer os.Remove(got) + + // Run the guru on each query, redirecting its output + // and error (if any) to the foo.got file. + for _, q := range queries { + doQuery(gotfh, q, useJson) + } + + // Compare foo.got with foo.golden. + var cmd *exec.Cmd + switch runtime.GOOS { + case "plan9": + cmd = exec.Command("/bin/diff", "-c", golden, got) + default: + cmd = exec.Command("/usr/bin/diff", "-u", golden, got) + } + buf := new(bytes.Buffer) + cmd.Stdout = buf + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + t.Errorf("Guru tests for %s failed: %s.\n%s\n", + filename, err, buf) + + if *updateFlag { + t.Logf("Updating %s...", golden) + if err := exec.Command("/bin/cp", got, golden).Run(); err != nil { + t.Errorf("Update failed: %s", err) + } + } + } + } +} diff --git a/cmd/guru/implements.go b/cmd/guru/implements.go new file mode 100644 index 00000000..d2e75ea8 --- /dev/null +++ b/cmd/guru/implements.go @@ -0,0 +1,352 @@ +// Copyright 2013 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 main + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "reflect" + "sort" + "strings" + + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/cmd/guru/serial" + "golang.org/x/tools/refactor/importgraph" +) + +// Implements displays the "implements" relation as it pertains to the +// selected type. +// If the selection is a method, 'implements' displays +// the corresponding methods of the types that would have been reported +// by an implements query on the receiver type. +// +func implements(q *Query) error { + lconf := loader.Config{Build: q.Build} + allowErrors(&lconf) + + qpkg, err := importQueryPackage(q.Pos, &lconf) + if err != nil { + return err + } + + // Set the packages to search. + if len(q.Scope) > 0 { + // Inspect all packages in the analysis scope, if specified. + if err := setPTAScope(&lconf, q.Scope); err != nil { + return err + } + } else { + // Otherwise inspect the forward and reverse + // transitive closure of the selected package. + // (In theory even this is incomplete.) + _, rev, _ := importgraph.Build(q.Build) + for path := range rev.Search(qpkg) { + lconf.ImportWithTests(path) + } + + // TODO(adonovan): for completeness, we should also + // type-check and inspect function bodies in all + // imported packages. This would be expensive, but we + // could optimize by skipping functions that do not + // contain type declarations. This would require + // changing the loader's TypeCheckFuncBodies hook to + // provide the []*ast.File. + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err := parseQueryPos(lprog, q.Pos, false) + if err != nil { + return err + } + + // Find the selected type. + path, action := findInterestingNode(qpos.info, qpos.path) + + var method *types.Func + var T types.Type // selected type (receiver if method != nil) + + switch action { + case actionExpr: + // method? + if id, ok := path[0].(*ast.Ident); ok { + if obj, ok := qpos.info.ObjectOf(id).(*types.Func); ok { + recv := obj.Type().(*types.Signature).Recv() + if recv == nil { + return fmt.Errorf("this function is not a method") + } + method = obj + T = recv.Type() + } + } + case actionType: + T = qpos.info.TypeOf(path[0].(ast.Expr)) + } + if T == nil { + return fmt.Errorf("no type or method here") + } + + // Find all named types, even local types (which can have + // methods via promotion) and the built-in "error". + var allNamed []types.Type + for _, info := range lprog.AllPackages { + for _, obj := range info.Defs { + if obj, ok := obj.(*types.TypeName); ok { + allNamed = append(allNamed, obj.Type()) + } + } + } + allNamed = append(allNamed, types.Universe.Lookup("error").Type()) + + var msets typeutil.MethodSetCache + + // Test each named type. + var to, from, fromPtr []types.Type + for _, U := range allNamed { + if isInterface(T) { + if msets.MethodSet(T).Len() == 0 { + continue // empty interface + } + if isInterface(U) { + if msets.MethodSet(U).Len() == 0 { + continue // empty interface + } + + // T interface, U interface + if !types.Identical(T, U) { + if types.AssignableTo(U, T) { + to = append(to, U) + } + if types.AssignableTo(T, U) { + from = append(from, U) + } + } + } else { + // T interface, U concrete + if types.AssignableTo(U, T) { + to = append(to, U) + } else if pU := types.NewPointer(U); types.AssignableTo(pU, T) { + to = append(to, pU) + } + } + } else if isInterface(U) { + if msets.MethodSet(U).Len() == 0 { + continue // empty interface + } + + // T concrete, U interface + if types.AssignableTo(T, U) { + from = append(from, U) + } else if pT := types.NewPointer(T); types.AssignableTo(pT, U) { + fromPtr = append(fromPtr, U) + } + } + } + + var pos interface{} = qpos + if nt, ok := deref(T).(*types.Named); ok { + pos = nt.Obj() + } + + // Sort types (arbitrarily) to ensure test determinism. + sort.Sort(typesByString(to)) + sort.Sort(typesByString(from)) + sort.Sort(typesByString(fromPtr)) + + var toMethod, fromMethod, fromPtrMethod []*types.Selection // contain nils + if method != nil { + for _, t := range to { + toMethod = append(toMethod, + types.NewMethodSet(t).Lookup(method.Pkg(), method.Name())) + } + for _, t := range from { + fromMethod = append(fromMethod, + types.NewMethodSet(t).Lookup(method.Pkg(), method.Name())) + } + for _, t := range fromPtr { + fromPtrMethod = append(fromPtrMethod, + types.NewMethodSet(t).Lookup(method.Pkg(), method.Name())) + } + } + + q.result = &implementsResult{ + qpos, T, pos, to, from, fromPtr, method, toMethod, fromMethod, fromPtrMethod, + } + return nil +} + +type implementsResult struct { + qpos *queryPos + + t types.Type // queried type (not necessarily named) + pos interface{} // pos of t (*types.Name or *QueryPos) + to []types.Type // named or ptr-to-named types assignable to interface T + from []types.Type // named interfaces assignable from T + fromPtr []types.Type // named interfaces assignable only from *T + + // if a method was queried: + method *types.Func // queried method + toMethod []*types.Selection // method of type to[i], if any + fromMethod []*types.Selection // method of type from[i], if any + fromPtrMethod []*types.Selection // method of type fromPtrMethod[i], if any +} + +func (r *implementsResult) display(printf printfFunc) { + relation := "is implemented by" + + meth := func(sel *types.Selection) { + if sel != nil { + printf(sel.Obj(), "\t%s method (%s).%s", + relation, r.qpos.typeString(sel.Recv()), sel.Obj().Name()) + } + } + + if isInterface(r.t) { + if types.NewMethodSet(r.t).Len() == 0 { // TODO(adonovan): cache mset + printf(r.pos, "empty interface type %s", r.qpos.typeString(r.t)) + return + } + + if r.method == nil { + printf(r.pos, "interface type %s", r.qpos.typeString(r.t)) + } else { + printf(r.method, "abstract method %s", r.qpos.objectString(r.method)) + } + + // Show concrete types (or methods) first; use two passes. + for i, sub := range r.to { + if !isInterface(sub) { + if r.method == nil { + printf(deref(sub).(*types.Named).Obj(), "\t%s %s type %s", + relation, typeKind(sub), r.qpos.typeString(sub)) + } else { + meth(r.toMethod[i]) + } + } + } + for i, sub := range r.to { + if isInterface(sub) { + if r.method == nil { + printf(sub.(*types.Named).Obj(), "\t%s %s type %s", + relation, typeKind(sub), r.qpos.typeString(sub)) + } else { + meth(r.toMethod[i]) + } + } + } + + relation = "implements" + for i, super := range r.from { + if r.method == nil { + printf(super.(*types.Named).Obj(), "\t%s %s", + relation, r.qpos.typeString(super)) + } else { + meth(r.fromMethod[i]) + } + } + } else { + relation = "implements" + + if r.from != nil { + if r.method == nil { + printf(r.pos, "%s type %s", + typeKind(r.t), r.qpos.typeString(r.t)) + } else { + printf(r.method, "concrete method %s", + r.qpos.objectString(r.method)) + } + for i, super := range r.from { + if r.method == nil { + printf(super.(*types.Named).Obj(), "\t%s %s", + relation, r.qpos.typeString(super)) + } else { + meth(r.fromMethod[i]) + } + } + } + if r.fromPtr != nil { + if r.method == nil { + printf(r.pos, "pointer type *%s", r.qpos.typeString(r.t)) + } else { + // TODO(adonovan): de-dup (C).f and (*C).f implementing (I).f. + printf(r.method, "concrete method %s", + r.qpos.objectString(r.method)) + } + + for i, psuper := range r.fromPtr { + if r.method == nil { + printf(psuper.(*types.Named).Obj(), "\t%s %s", + relation, r.qpos.typeString(psuper)) + } else { + meth(r.fromPtrMethod[i]) + } + } + } else if r.from == nil { + printf(r.pos, "%s type %s implements only interface{}", + typeKind(r.t), r.qpos.typeString(r.t)) + } + } +} + +func (r *implementsResult) toSerial(res *serial.Result, fset *token.FileSet) { + res.Implements = &serial.Implements{ + T: makeImplementsType(r.t, fset), + AssignableTo: makeImplementsTypes(r.to, fset), + AssignableFrom: makeImplementsTypes(r.from, fset), + AssignableFromPtr: makeImplementsTypes(r.fromPtr, fset), + AssignableToMethod: methodsToSerial(r.qpos.info.Pkg, r.toMethod, fset), + AssignableFromMethod: methodsToSerial(r.qpos.info.Pkg, r.fromMethod, fset), + AssignableFromPtrMethod: methodsToSerial(r.qpos.info.Pkg, r.fromPtrMethod, fset), + } + if r.method != nil { + res.Implements.Method = &serial.DescribeMethod{ + Name: r.qpos.objectString(r.method), + Pos: fset.Position(r.method.Pos()).String(), + } + } +} + +func makeImplementsTypes(tt []types.Type, fset *token.FileSet) []serial.ImplementsType { + var r []serial.ImplementsType + for _, t := range tt { + r = append(r, makeImplementsType(t, fset)) + } + return r +} + +func makeImplementsType(T types.Type, fset *token.FileSet) serial.ImplementsType { + var pos token.Pos + if nt, ok := deref(T).(*types.Named); ok { // implementsResult.t may be non-named + pos = nt.Obj().Pos() + } + return serial.ImplementsType{ + Name: T.String(), + Pos: fset.Position(pos).String(), + Kind: typeKind(T), + } +} + +// typeKind returns a string describing the underlying kind of type, +// e.g. "slice", "array", "struct". +func typeKind(T types.Type) string { + s := reflect.TypeOf(T.Underlying()).String() + return strings.ToLower(strings.TrimPrefix(s, "*types.")) +} + +func isInterface(T types.Type) bool { return types.IsInterface(T) } + +type typesByString []types.Type + +func (p typesByString) Len() int { return len(p) } +func (p typesByString) Less(i, j int) bool { return p[i].String() < p[j].String() } +func (p typesByString) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/cmd/guru/main.go b/cmd/guru/main.go new file mode 100644 index 00000000..ed338d1b --- /dev/null +++ b/cmd/guru/main.go @@ -0,0 +1,204 @@ +// Copyright 2013 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. + +// guru: a tool for answering questions about Go source code. +// +// http://golang.org/s/oracle-design +// http://golang.org/s/oracle-user-manual +// +// Run with -help flag or help subcommand for usage information. +// +package main // import "golang.org/x/tools/cmd/guru" + +import ( + "bufio" + "encoding/json" + "encoding/xml" + "flag" + "fmt" + "go/build" + "io" + "log" + "os" + "runtime" + "runtime/pprof" + + "golang.org/x/tools/go/buildutil" + "golang.org/x/tools/go/loader" +) + +var posFlag = flag.String("pos", "", + "Filename and byte offset or extent of a syntax element about which to query, "+ + "e.g. foo.go:#123,#456, bar.go:#123.") + +var ptalogFlag = flag.String("ptalog", "", + "Location of the points-to analysis log file, or empty to disable logging.") + +var formatFlag = flag.String("format", "plain", "Output format. One of {plain,json,xml}.") + +var reflectFlag = flag.Bool("reflect", false, "Analyze reflection soundly (slow).") + +func init() { + flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) +} + +const useHelp = "Run 'guru -help' for more information.\n" + +const helpMessage = `Go source code guru. +Usage: guru [ ...] ... + +The -format flag controls the output format: + plain an editor-friendly format in which every line of output + is of the form "pos: text", where pos is "-" if unknown. + json structured data in JSON syntax. + xml structured data in XML syntax. + +The -pos flag is required in all modes. + +The mode argument determines the query to perform: + + callees show possible targets of selected function call + callers show possible callers of selected function + callstack show path from callgraph root to selected function + definition show declaration of selected identifier + describe describe selected syntax: definition, methods, etc + freevars show free variables of selection + implements show 'implements' relation for selected type or method + peers show send/receive corresponding to selected channel op + pointsto show variables to which the selected pointer may point + referrers show all refs to entity denoted by selected identifier + what show basic information about the selected syntax node + whicherrs show possible values of the selected error variable + +The user manual is available here: http://golang.org/s/guru-user-manual + +Examples: + +Describe the syntax at offset 530 in this file (an import spec): +% guru -pos=src/golang.org/x/tools/cmd/guru/main.go:#530 describe \ + golang.org/x/tools/cmd/guru + +` + loader.FromArgsUsage + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") + +func init() { + // If $GOMAXPROCS isn't set, use the full capacity of the machine. + // For small machines, use at least 4 threads. + if os.Getenv("GOMAXPROCS") == "" { + n := runtime.NumCPU() + if n < 4 { + n = 4 + } + runtime.GOMAXPROCS(n) + } +} + +func printHelp() { + fmt.Fprintln(os.Stderr, helpMessage) + fmt.Fprintln(os.Stderr, "Flags:") + flag.PrintDefaults() +} + +func main() { + // Don't print full help unless -help was requested. + // Just gently remind users that it's there. + flag.Usage = func() { fmt.Fprint(os.Stderr, useHelp) } + flag.CommandLine.Init(os.Args[0], flag.ContinueOnError) // hack + if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + // (err has already been printed) + if err == flag.ErrHelp { + printHelp() + } + os.Exit(2) + } + + args := flag.Args() + if len(args) == 0 || args[0] == "" { + fmt.Fprint(os.Stderr, "guru: a mode argument is required.\n"+useHelp) + os.Exit(2) + } + + mode := args[0] + args = args[1:] + if mode == "help" { + printHelp() + os.Exit(2) + } + + // Set up points-to analysis log file. + var ptalog io.Writer + if *ptalogFlag != "" { + if f, err := os.Create(*ptalogFlag); err != nil { + log.Fatalf("Failed to create PTA log file: %s", err) + } else { + buf := bufio.NewWriter(f) + ptalog = buf + defer func() { + if err := buf.Flush(); err != nil { + log.Printf("flush: %s", err) + } + if err := f.Close(); err != nil { + log.Printf("close: %s", err) + } + }() + } + } + + // Profiling support. + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal(err) + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + + // -format flag + switch *formatFlag { + case "json", "plain", "xml": + // ok + default: + fmt.Fprintf(os.Stderr, "guru: illegal -format value: %q.\n"+useHelp, *formatFlag) + os.Exit(2) + } + + // Ask the guru. + query := Query{ + Mode: mode, + Pos: *posFlag, + Build: &build.Default, + Scope: args, + PTALog: ptalog, + Reflection: *reflectFlag, + } + + if err := Run(&query); err != nil { + fmt.Fprintf(os.Stderr, "guru: %s\n", err) + os.Exit(1) + } + + // Print the result. + switch *formatFlag { + case "json": + b, err := json.MarshalIndent(query.Serial(), "", "\t") + if err != nil { + fmt.Fprintf(os.Stderr, "guru: JSON error: %s\n", err) + os.Exit(1) + } + os.Stdout.Write(b) + + case "xml": + b, err := xml.MarshalIndent(query.Serial(), "", "\t") + if err != nil { + fmt.Fprintf(os.Stderr, "guru: XML error: %s\n", err) + os.Exit(1) + } + os.Stdout.Write(b) + + case "plain": + query.WriteTo(os.Stdout) + } +} diff --git a/cmd/guru/peers.go b/cmd/guru/peers.go new file mode 100644 index 00000000..eda36107 --- /dev/null +++ b/cmd/guru/peers.go @@ -0,0 +1,252 @@ +// Copyright 2013 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 main + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "sort" + + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/cmd/guru/serial" +) + +// peers enumerates, for a given channel send (or receive) operation, +// the set of possible receives (or sends) that correspond to it. +// +// TODO(adonovan): support reflect.{Select,Recv,Send,Close}. +// TODO(adonovan): permit the user to query based on a MakeChan (not send/recv), +// or the implicit receive in "for v := range ch". +func peers(q *Query) error { + lconf := loader.Config{Build: q.Build} + + if err := setPTAScope(&lconf, q.Scope); err != nil { + return err + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err := parseQueryPos(lprog, q.Pos, false) + if err != nil { + return err + } + + prog := ssautil.CreateProgram(lprog, ssa.GlobalDebug) + + ptaConfig, err := setupPTA(prog, lprog, q.PTALog, q.Reflection) + if err != nil { + return err + } + + opPos := findOp(qpos) + if opPos == token.NoPos { + return fmt.Errorf("there is no channel operation here") + } + + // Defer SSA construction till after errors are reported. + prog.Build() + + var queryOp chanOp // the originating send or receive operation + var ops []chanOp // all sends/receives of opposite direction + + // Look at all channel operations in the whole ssa.Program. + // Build a list of those of same type as the query. + allFuncs := ssautil.AllFunctions(prog) + for fn := range allFuncs { + for _, b := range fn.Blocks { + for _, instr := range b.Instrs { + for _, op := range chanOps(instr) { + ops = append(ops, op) + if op.pos == opPos { + queryOp = op // we found the query op + } + } + } + } + } + if queryOp.ch == nil { + return fmt.Errorf("ssa.Instruction for send/receive not found") + } + + // Discard operations of wrong channel element type. + // Build set of channel ssa.Values as query to pointer analysis. + // We compare channels by element types, not channel types, to + // ignore both directionality and type names. + queryType := queryOp.ch.Type() + queryElemType := queryType.Underlying().(*types.Chan).Elem() + ptaConfig.AddQuery(queryOp.ch) + i := 0 + for _, op := range ops { + if types.Identical(op.ch.Type().Underlying().(*types.Chan).Elem(), queryElemType) { + ptaConfig.AddQuery(op.ch) + ops[i] = op + i++ + } + } + ops = ops[:i] + + // Run the pointer analysis. + ptares := ptrAnalysis(ptaConfig) + + // Find the points-to set. + queryChanPtr := ptares.Queries[queryOp.ch] + + // Ascertain which make(chan) labels the query's channel can alias. + var makes []token.Pos + for _, label := range queryChanPtr.PointsTo().Labels() { + makes = append(makes, label.Pos()) + } + sort.Sort(byPos(makes)) + + // Ascertain which channel operations can alias the same make(chan) labels. + var sends, receives, closes []token.Pos + for _, op := range ops { + if ptr, ok := ptares.Queries[op.ch]; ok && ptr.MayAlias(queryChanPtr) { + switch op.dir { + case types.SendOnly: + sends = append(sends, op.pos) + case types.RecvOnly: + receives = append(receives, op.pos) + case types.SendRecv: + closes = append(closes, op.pos) + } + } + } + sort.Sort(byPos(sends)) + sort.Sort(byPos(receives)) + sort.Sort(byPos(closes)) + + q.result = &peersResult{ + queryPos: opPos, + queryType: queryType, + makes: makes, + sends: sends, + receives: receives, + closes: closes, + } + return nil +} + +// findOp returns the position of the enclosing send/receive/close op. +// For send and receive operations, this is the position of the <- token; +// for close operations, it's the Lparen of the function call. +// +// TODO(adonovan): handle implicit receive operations from 'for...range chan' statements. +func findOp(qpos *queryPos) token.Pos { + for _, n := range qpos.path { + switch n := n.(type) { + case *ast.UnaryExpr: + if n.Op == token.ARROW { + return n.OpPos + } + case *ast.SendStmt: + return n.Arrow + case *ast.CallExpr: + // close function call can only exist as a direct identifier + if close, ok := unparen(n.Fun).(*ast.Ident); ok { + if b, ok := qpos.info.Info.Uses[close].(*types.Builtin); ok && b.Name() == "close" { + return n.Lparen + } + } + } + } + return token.NoPos +} + +// chanOp abstracts an ssa.Send, ssa.Unop(ARROW), or a SelectState. +type chanOp struct { + ch ssa.Value + dir types.ChanDir // SendOnly=send, RecvOnly=recv, SendRecv=close + pos token.Pos +} + +// chanOps returns a slice of all the channel operations in the instruction. +func chanOps(instr ssa.Instruction) []chanOp { + // TODO(adonovan): handle calls to reflect.{Select,Recv,Send,Close} too. + var ops []chanOp + switch instr := instr.(type) { + case *ssa.UnOp: + if instr.Op == token.ARROW { + ops = append(ops, chanOp{instr.X, types.RecvOnly, instr.Pos()}) + } + case *ssa.Send: + ops = append(ops, chanOp{instr.Chan, types.SendOnly, instr.Pos()}) + case *ssa.Select: + for _, st := range instr.States { + ops = append(ops, chanOp{st.Chan, st.Dir, st.Pos}) + } + case ssa.CallInstruction: + cc := instr.Common() + if b, ok := cc.Value.(*ssa.Builtin); ok && b.Name() == "close" { + ops = append(ops, chanOp{cc.Args[0], types.SendRecv, cc.Pos()}) + } + } + return ops +} + +type peersResult struct { + queryPos token.Pos // of queried channel op + queryType types.Type // type of queried channel + makes, sends, receives, closes []token.Pos // positions of aliased makechan/send/receive/close instrs +} + +func (r *peersResult) display(printf printfFunc) { + if len(r.makes) == 0 { + printf(r.queryPos, "This channel can't point to anything.") + return + } + printf(r.queryPos, "This channel of type %s may be:", r.queryType) + for _, alloc := range r.makes { + printf(alloc, "\tallocated here") + } + for _, send := range r.sends { + printf(send, "\tsent to, here") + } + for _, receive := range r.receives { + printf(receive, "\treceived from, here") + } + for _, clos := range r.closes { + printf(clos, "\tclosed, here") + } +} + +func (r *peersResult) toSerial(res *serial.Result, fset *token.FileSet) { + peers := &serial.Peers{ + Pos: fset.Position(r.queryPos).String(), + Type: r.queryType.String(), + } + for _, alloc := range r.makes { + peers.Allocs = append(peers.Allocs, fset.Position(alloc).String()) + } + for _, send := range r.sends { + peers.Sends = append(peers.Sends, fset.Position(send).String()) + } + for _, receive := range r.receives { + peers.Receives = append(peers.Receives, fset.Position(receive).String()) + } + for _, clos := range r.closes { + peers.Closes = append(peers.Closes, fset.Position(clos).String()) + } + res.Peers = peers +} + +// -------- utils -------- + +// NB: byPos is not deterministic across packages since it depends on load order. +// Use lessPos if the tests need it. +type byPos []token.Pos + +func (p byPos) Len() int { return len(p) } +func (p byPos) Less(i, j int) bool { return p[i] < p[j] } +func (p byPos) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/cmd/guru/pointsto.go b/cmd/guru/pointsto.go new file mode 100644 index 00000000..758ba028 --- /dev/null +++ b/cmd/guru/pointsto.go @@ -0,0 +1,291 @@ +// Copyright 2013 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 main + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "sort" + + "golang.org/x/tools/go/ast/astutil" + "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" + "golang.org/x/tools/oracle/serial" +) + +// pointsto runs the pointer analysis on the selected expression, +// and reports its points-to set (for a pointer-like expression) +// or its dynamic types (for an interface, reflect.Value, or +// reflect.Type expression) and their points-to sets. +// +// All printed sets are sorted to ensure determinism. +// +func pointsto(q *Query) error { + lconf := loader.Config{Build: q.Build} + + if err := setPTAScope(&lconf, q.Scope); err != nil { + return err + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err := parseQueryPos(lprog, q.Pos, true) // needs exact pos + if err != nil { + return err + } + + prog := ssautil.CreateProgram(lprog, ssa.GlobalDebug) + + ptaConfig, err := setupPTA(prog, lprog, q.PTALog, q.Reflection) + if err != nil { + return err + } + + path, action := findInterestingNode(qpos.info, qpos.path) + if action != actionExpr { + return fmt.Errorf("pointer analysis wants an expression; got %s", + astutil.NodeDescription(qpos.path[0])) + } + + var expr ast.Expr + var obj types.Object + switch n := path[0].(type) { + case *ast.ValueSpec: + // ambiguous ValueSpec containing multiple names + return fmt.Errorf("multiple value specification") + case *ast.Ident: + obj = qpos.info.ObjectOf(n) + expr = n + case ast.Expr: + expr = n + default: + // TODO(adonovan): is this reachable? + return fmt.Errorf("unexpected AST for expr: %T", n) + } + + // Reject non-pointerlike types (includes all constants---except nil). + // TODO(adonovan): reject nil too. + typ := qpos.info.TypeOf(expr) + if !pointer.CanPoint(typ) { + return fmt.Errorf("pointer analysis wants an expression of reference type; got %s", typ) + } + + // Determine the ssa.Value for the expression. + var value ssa.Value + var isAddr bool + if obj != nil { + // def/ref of func/var object + value, isAddr, err = ssaValueForIdent(prog, qpos.info, obj, path) + } else { + value, isAddr, err = ssaValueForExpr(prog, qpos.info, path) + } + if err != nil { + return err // e.g. trivially dead code + } + + // Defer SSA construction till after errors are reported. + prog.Build() + + // Run the pointer analysis. + ptrs, err := runPTA(ptaConfig, value, isAddr) + if err != nil { + return err // e.g. analytically unreachable + } + + q.result = &pointstoResult{ + qpos: qpos, + typ: typ, + ptrs: ptrs, + } + return nil +} + +// ssaValueForIdent returns the ssa.Value for the ast.Ident whose path +// to the root of the AST is path. isAddr reports whether the +// ssa.Value is the address denoted by the ast.Ident, not its value. +// +func ssaValueForIdent(prog *ssa.Program, qinfo *loader.PackageInfo, obj types.Object, path []ast.Node) (value ssa.Value, isAddr bool, err error) { + switch obj := obj.(type) { + case *types.Var: + pkg := prog.Package(qinfo.Pkg) + pkg.Build() + if v, addr := prog.VarValue(obj, pkg, path); v != nil { + return v, addr, nil + } + return nil, false, fmt.Errorf("can't locate SSA Value for var %s", obj.Name()) + + case *types.Func: + fn := prog.FuncValue(obj) + if fn == nil { + return nil, false, fmt.Errorf("%s is an interface method", obj) + } + // TODO(adonovan): there's no point running PTA on a *Func ident. + // Eliminate this feature. + return fn, false, nil + } + panic(obj) +} + +// ssaValueForExpr returns the ssa.Value of the non-ast.Ident +// expression whose path to the root of the AST is path. +// +func ssaValueForExpr(prog *ssa.Program, qinfo *loader.PackageInfo, path []ast.Node) (value ssa.Value, isAddr bool, err error) { + pkg := prog.Package(qinfo.Pkg) + pkg.SetDebugMode(true) + pkg.Build() + + fn := ssa.EnclosingFunction(pkg, path) + if fn == nil { + return nil, false, fmt.Errorf("no SSA function built for this location (dead code?)") + } + + if v, addr := fn.ValueForExpr(path[0].(ast.Expr)); v != nil { + return v, addr, nil + } + + return nil, false, fmt.Errorf("can't locate SSA Value for expression in %s", fn) +} + +// runPTA runs the pointer analysis of the selected SSA value or address. +func runPTA(conf *pointer.Config, v ssa.Value, isAddr bool) (ptrs []pointerResult, err error) { + T := v.Type() + if isAddr { + conf.AddIndirectQuery(v) + T = deref(T) + } else { + conf.AddQuery(v) + } + ptares := ptrAnalysis(conf) + + var ptr pointer.Pointer + if isAddr { + ptr = ptares.IndirectQueries[v] + } else { + ptr = ptares.Queries[v] + } + if ptr == (pointer.Pointer{}) { + return nil, fmt.Errorf("pointer analysis did not find expression (dead code?)") + } + pts := ptr.PointsTo() + + if pointer.CanHaveDynamicTypes(T) { + // Show concrete types for interface/reflect.Value expression. + if concs := pts.DynamicTypes(); concs.Len() > 0 { + concs.Iterate(func(conc types.Type, pta interface{}) { + labels := pta.(pointer.PointsToSet).Labels() + sort.Sort(byPosAndString(labels)) // to ensure determinism + ptrs = append(ptrs, pointerResult{conc, labels}) + }) + } + } else { + // Show labels for other expressions. + labels := pts.Labels() + sort.Sort(byPosAndString(labels)) // to ensure determinism + ptrs = append(ptrs, pointerResult{T, labels}) + } + sort.Sort(byTypeString(ptrs)) // to ensure determinism + return ptrs, nil +} + +type pointerResult struct { + typ types.Type // type of the pointer (always concrete) + labels []*pointer.Label // set of labels +} + +type pointstoResult struct { + qpos *queryPos + typ types.Type // type of expression + ptrs []pointerResult // pointer info (typ is concrete => len==1) +} + +func (r *pointstoResult) display(printf printfFunc) { + if pointer.CanHaveDynamicTypes(r.typ) { + // Show concrete types for interface, reflect.Type or + // reflect.Value expression. + + if len(r.ptrs) > 0 { + printf(r.qpos, "this %s may contain these dynamic types:", r.qpos.typeString(r.typ)) + for _, ptr := range r.ptrs { + var obj types.Object + if nt, ok := deref(ptr.typ).(*types.Named); ok { + obj = nt.Obj() + } + if len(ptr.labels) > 0 { + printf(obj, "\t%s, may point to:", r.qpos.typeString(ptr.typ)) + printLabels(printf, ptr.labels, "\t\t") + } else { + printf(obj, "\t%s", r.qpos.typeString(ptr.typ)) + } + } + } else { + printf(r.qpos, "this %s cannot contain any dynamic types.", r.typ) + } + } else { + // Show labels for other expressions. + if ptr := r.ptrs[0]; len(ptr.labels) > 0 { + printf(r.qpos, "this %s may point to these objects:", + r.qpos.typeString(r.typ)) + printLabels(printf, ptr.labels, "\t") + } else { + printf(r.qpos, "this %s may not point to anything.", + r.qpos.typeString(r.typ)) + } + } +} + +func (r *pointstoResult) toSerial(res *serial.Result, fset *token.FileSet) { + var pts []serial.PointsTo + for _, ptr := range r.ptrs { + var namePos string + if nt, ok := deref(ptr.typ).(*types.Named); ok { + namePos = fset.Position(nt.Obj().Pos()).String() + } + var labels []serial.PointsToLabel + for _, l := range ptr.labels { + labels = append(labels, serial.PointsToLabel{ + Pos: fset.Position(l.Pos()).String(), + Desc: l.String(), + }) + } + pts = append(pts, serial.PointsTo{ + Type: r.qpos.typeString(ptr.typ), + NamePos: namePos, + Labels: labels, + }) + } + res.PointsTo = pts +} + +type byTypeString []pointerResult + +func (a byTypeString) Len() int { return len(a) } +func (a byTypeString) Less(i, j int) bool { return a[i].typ.String() < a[j].typ.String() } +func (a byTypeString) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type byPosAndString []*pointer.Label + +func (a byPosAndString) Len() int { return len(a) } +func (a byPosAndString) Less(i, j int) bool { + cmp := a[i].Pos() - a[j].Pos() + return cmp < 0 || (cmp == 0 && a[i].String() < a[j].String()) +} +func (a byPosAndString) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func printLabels(printf printfFunc, labels []*pointer.Label, prefix string) { + // TODO(adonovan): due to context-sensitivity, many of these + // labels may differ only by context, which isn't apparent. + for _, label := range labels { + printf(label, "%s%s", prefix, label) + } +} diff --git a/cmd/guru/pos.go b/cmd/guru/pos.go new file mode 100644 index 00000000..9b086c07 --- /dev/null +++ b/cmd/guru/pos.go @@ -0,0 +1,147 @@ +// Copyright 2013 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 main + +// This file defines utilities for working with file positions. + +import ( + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/tools/go/ast/astutil" +) + +// parseOctothorpDecimal returns the numeric value if s matches "#%d", +// otherwise -1. +func parseOctothorpDecimal(s string) int { + if s != "" && s[0] == '#' { + if s, err := strconv.ParseInt(s[1:], 10, 32); err == nil { + return int(s) + } + } + return -1 +} + +// parsePosFlag parses a string of the form "file:pos" or +// file:start,end" where pos, start, end match #%d and represent byte +// offsets, and returns its components. +// +// (Numbers without a '#' prefix are reserved for future use, +// e.g. to indicate line/column positions.) +// +func parsePosFlag(posFlag string) (filename string, startOffset, endOffset int, err error) { + if posFlag == "" { + err = fmt.Errorf("no source position specified (-pos flag)") + return + } + + colon := strings.LastIndex(posFlag, ":") + if colon < 0 { + err = fmt.Errorf("invalid source position -pos=%q", posFlag) + return + } + filename, offset := posFlag[:colon], posFlag[colon+1:] + startOffset = -1 + endOffset = -1 + if hyphen := strings.Index(offset, ","); hyphen < 0 { + // e.g. "foo.go:#123" + startOffset = parseOctothorpDecimal(offset) + endOffset = startOffset + } else { + // e.g. "foo.go:#123,#456" + startOffset = parseOctothorpDecimal(offset[:hyphen]) + endOffset = parseOctothorpDecimal(offset[hyphen+1:]) + } + if startOffset < 0 || endOffset < 0 { + err = fmt.Errorf("invalid -pos offset %q", offset) + return + } + return +} + +// findQueryPos searches fset for filename and translates the +// specified file-relative byte offsets into token.Pos form. It +// returns an error if the file was not found or the offsets were out +// of bounds. +// +func findQueryPos(fset *token.FileSet, filename string, startOffset, endOffset int) (start, end token.Pos, err error) { + var file *token.File + fset.Iterate(func(f *token.File) bool { + if sameFile(filename, f.Name()) { + // (f.Name() is absolute) + file = f + return false // done + } + return true // continue + }) + if file == nil { + err = fmt.Errorf("couldn't find file containing position") + return + } + + // Range check [start..end], inclusive of both end-points. + + if 0 <= startOffset && startOffset <= file.Size() { + start = file.Pos(int(startOffset)) + } else { + err = fmt.Errorf("start position is beyond end of file") + return + } + + if 0 <= endOffset && endOffset <= file.Size() { + end = file.Pos(int(endOffset)) + } else { + err = fmt.Errorf("end position is beyond end of file") + return + } + + return +} + +// sameFile returns true if x and y have the same basename and denote +// the same file. +// +func sameFile(x, y string) bool { + if filepath.Base(x) == filepath.Base(y) { // (optimisation) + if xi, err := os.Stat(x); err == nil { + if yi, err := os.Stat(y); err == nil { + return os.SameFile(xi, yi) + } + } + } + return false +} + +// fastQueryPos parses the -pos flag and returns a QueryPos. +// It parses only a single file, and does not run the type checker. +func fastQueryPos(posFlag string) (*queryPos, error) { + filename, startOffset, endOffset, err := parsePosFlag(posFlag) + if err != nil { + return nil, err + } + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filename, nil, 0) + if err != nil { + return nil, err + } + + start, end, err := findQueryPos(fset, filename, startOffset, endOffset) + if err != nil { + return nil, err + } + + path, exact := astutil.PathEnclosingInterval(f, start, end) + if path == nil { + return nil, fmt.Errorf("no syntax here") + } + + return &queryPos{fset, start, end, path, exact, nil}, nil +} diff --git a/cmd/guru/referrers.go b/cmd/guru/referrers.go new file mode 100644 index 00000000..eaf5f1d2 --- /dev/null +++ b/cmd/guru/referrers.go @@ -0,0 +1,241 @@ +// Copyright 2013 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 main + +import ( + "bytes" + "fmt" + "go/ast" + "go/token" + "go/types" + "io/ioutil" + "sort" + + "golang.org/x/tools/go/loader" + "golang.org/x/tools/cmd/guru/serial" + "golang.org/x/tools/refactor/importgraph" +) + +// Referrers reports all identifiers that resolve to the same object +// as the queried identifier, within any package in the analysis scope. +func referrers(q *Query) error { + lconf := loader.Config{Build: q.Build} + allowErrors(&lconf) + + if _, err := importQueryPackage(q.Pos, &lconf); err != nil { + return err + } + + var id *ast.Ident + var obj types.Object + var lprog *loader.Program + var pass2 bool + var qpos *queryPos + for { + // Load/parse/type-check the program. + var err error + lprog, err = lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err = parseQueryPos(lprog, q.Pos, false) + if err != nil { + return err + } + + id, _ = qpos.path[0].(*ast.Ident) + if id == nil { + return fmt.Errorf("no identifier here") + } + + obj = qpos.info.ObjectOf(id) + if obj == nil { + // Happens for y in "switch y := x.(type)", + // the package declaration, + // and unresolved identifiers. + if _, ok := qpos.path[1].(*ast.File); ok { // package decl? + pkg := qpos.info.Pkg + obj = types.NewPkgName(id.Pos(), pkg, pkg.Name(), pkg) + } else { + return fmt.Errorf("no object for identifier: %T", qpos.path[1]) + } + } + + if pass2 { + break + } + + // If the identifier is exported, we must load all packages that + // depend transitively upon the package that defines it. + // Treat PkgNames as exported, even though they're lowercase. + if _, isPkg := obj.(*types.PkgName); !(isPkg || obj.Exported()) { + break // not exported + } + + // Scan the workspace and build the import graph. + // Ignore broken packages. + _, rev, _ := importgraph.Build(q.Build) + + // Re-load the larger program. + // Create a new file set so that ... + // External test packages are never imported, + // so they will never appear in the graph. + // (We must reset the Config here, not just reset the Fset field.) + lconf = loader.Config{ + Fset: token.NewFileSet(), + Build: q.Build, + } + allowErrors(&lconf) + for path := range rev.Search(obj.Pkg().Path()) { + lconf.ImportWithTests(path) + } + pass2 = true + } + + // Iterate over all go/types' Uses facts for the entire program. + var refs []*ast.Ident + for _, info := range lprog.AllPackages { + for id2, obj2 := range info.Uses { + if sameObj(obj, obj2) { + refs = append(refs, id2) + } + } + } + sort.Sort(byNamePos{q.Fset, refs}) + + q.result = &referrersResult{ + qpos: qpos, + query: id, + obj: obj, + refs: refs, + } + return nil +} + +// same reports whether x and y are identical, or both are PkgNames +// that import the same Package. +// +func sameObj(x, y types.Object) bool { + if x == y { + return true + } + if x, ok := x.(*types.PkgName); ok { + if y, ok := y.(*types.PkgName); ok { + return x.Imported() == y.Imported() + } + } + return false +} + +// -------- utils -------- + +// An deterministic ordering for token.Pos that doesn't +// depend on the order in which packages were loaded. +func lessPos(fset *token.FileSet, x, y token.Pos) bool { + fx := fset.File(x) + fy := fset.File(y) + if fx != fy { + return fx.Name() < fy.Name() + } + return x < y +} + +type byNamePos struct { + fset *token.FileSet + ids []*ast.Ident +} + +func (p byNamePos) Len() int { return len(p.ids) } +func (p byNamePos) Swap(i, j int) { p.ids[i], p.ids[j] = p.ids[j], p.ids[i] } +func (p byNamePos) Less(i, j int) bool { + return lessPos(p.fset, p.ids[i].NamePos, p.ids[j].NamePos) +} + +type referrersResult struct { + qpos *queryPos + query *ast.Ident // identifier of query + obj types.Object // object it denotes + refs []*ast.Ident // set of all other references to it +} + +func (r *referrersResult) display(printf printfFunc) { + printf(r.obj, "%d references to %s", len(r.refs), r.qpos.objectString(r.obj)) + + // Show referring lines, like grep. + type fileinfo struct { + refs []*ast.Ident + linenums []int // line number of refs[i] + data chan interface{} // file contents or error + } + var fileinfos []*fileinfo + fileinfosByName := make(map[string]*fileinfo) + + // First pass: start the file reads concurrently. + sema := make(chan struct{}, 20) // counting semaphore to limit I/O concurrency + for _, ref := range r.refs { + posn := r.qpos.fset.Position(ref.Pos()) + fi := fileinfosByName[posn.Filename] + if fi == nil { + fi = &fileinfo{data: make(chan interface{})} + fileinfosByName[posn.Filename] = fi + fileinfos = append(fileinfos, fi) + + // First request for this file: + // start asynchronous read. + go func() { + sema <- struct{}{} // acquire token + content, err := ioutil.ReadFile(posn.Filename) + <-sema // release token + if err != nil { + fi.data <- err + } else { + fi.data <- content + } + }() + } + fi.refs = append(fi.refs, ref) + fi.linenums = append(fi.linenums, posn.Line) + } + + // Second pass: print refs in original order. + // One line may have several refs at different columns. + for _, fi := range fileinfos { + v := <-fi.data // wait for I/O completion + + // Print one item for all refs in a file that could not + // be loaded (perhaps due to //line directives). + if err, ok := v.(error); ok { + var suffix string + if more := len(fi.refs) - 1; more > 0 { + suffix = fmt.Sprintf(" (+ %d more refs in this file)", more) + } + printf(fi.refs[0], "%v%s", err, suffix) + continue + } + + lines := bytes.Split(v.([]byte), []byte("\n")) + for i, ref := range fi.refs { + printf(ref, "%s", lines[fi.linenums[i]-1]) + } + } +} + +// TODO(adonovan): encode extent, not just Pos info, in Serial form. + +func (r *referrersResult) toSerial(res *serial.Result, fset *token.FileSet) { + referrers := &serial.Referrers{ + Pos: fset.Position(r.query.Pos()).String(), + Desc: r.obj.String(), + } + if pos := r.obj.Pos(); pos != token.NoPos { // Package objects have no Pos() + referrers.ObjPos = fset.Position(pos).String() + } + for _, ref := range r.refs { + referrers.Refs = append(referrers.Refs, fset.Position(ref.NamePos).String()) + } + res.Referrers = referrers +} diff --git a/cmd/guru/serial/serial.go b/cmd/guru/serial/serial.go new file mode 100644 index 00000000..0110c064 --- /dev/null +++ b/cmd/guru/serial/serial.go @@ -0,0 +1,258 @@ +// Copyright 2013 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 serial defines the guru's schema for structured data +// serialization using JSON, XML, etc. +package serial + +// All 'pos' strings are of the form "file:line:col". +// TODO(adonovan): improve performance by sharing filename strings. +// TODO(adonovan): improve precision by providing the start/end +// interval when available. +// +// TODO(adonovan): consider richer encodings of types, functions, +// methods, etc. + +// A Peers is the result of a 'peers' query. +// If Allocs is empty, the selected channel can't point to anything. +type Peers struct { + Pos string `json:"pos"` // location of the selected channel op (<-) + Type string `json:"type"` // type of the selected channel + Allocs []string `json:"allocs,omitempty"` // locations of aliased make(chan) ops + Sends []string `json:"sends,omitempty"` // locations of aliased ch<-x ops + Receives []string `json:"receives,omitempty"` // locations of aliased <-ch ops + Closes []string `json:"closes,omitempty"` // locations of aliased close(ch) ops +} + +// A Referrers is the result of a 'referrers' query. +type Referrers struct { + Pos string `json:"pos"` // location of the query reference + ObjPos string `json:"objpos,omitempty"` // location of the definition + Desc string `json:"desc"` // description of the denoted object + Refs []string `json:"refs,omitempty"` // locations of all references +} + +// A Definition is the result of a 'definition' query. +type Definition struct { + ObjPos string `json:"objpos,omitempty"` // location of the definition + Desc string `json:"desc"` // description of the denoted object +} + +type CalleesItem struct { + Name string `json:"name"` // full name of called function + Pos string `json:"pos"` // location of called function +} + +// A Callees is the result of a 'callees' query. +// +// Callees is nonempty unless the call was a dynamic call on a +// provably nil func or interface value. +type Callees struct { + Pos string `json:"pos"` // location of selected call site + Desc string `json:"desc"` // description of call site + Callees []*CalleesItem `json:"callees,omitempty"` // set of possible call targets +} + +// A Caller is one element of the slice returned by a 'callers' query. +// (Callstack also contains a similar slice.) +// +// The root of the callgraph has an unspecified "Caller" string. +type Caller struct { + Pos string `json:"pos,omitempty"` // location of the calling function + Desc string `json:"desc"` // description of call site + Caller string `json:"caller"` // full name of calling function +} + +// A CallStack is the result of a 'callstack' query. +// It indicates an arbitrary path from the root of the callgraph to +// the query function. +// +// If the Callers slice is empty, the function was unreachable in this +// analysis scope. +type CallStack struct { + Pos string `json:"pos"` // location of the selected function + Target string `json:"target"` // the selected function + Callers []Caller `json:"callers"` // enclosing calls, innermost first. +} + +// A FreeVar is one element of the slice returned by a 'freevars' +// query. Each one identifies an expression referencing a local +// identifier defined outside the selected region. +type FreeVar struct { + Pos string `json:"pos"` // location of the identifier's definition + Kind string `json:"kind"` // one of {var,func,type,const,label} + Ref string `json:"ref"` // referring expression (e.g. "x" or "x.y.z") + Type string `json:"type"` // type of the expression +} + +// An Implements contains the result of an 'implements' query. +// It describes the queried type, the set of named non-empty interface +// types to which it is assignable, and the set of named/*named types +// (concrete or non-empty interface) which may be assigned to it. +// +type Implements struct { + T ImplementsType `json:"type,omitempty"` // the queried type + AssignableTo []ImplementsType `json:"to,omitempty"` // types assignable to T + AssignableFrom []ImplementsType `json:"from,omitempty"` // interface types assignable from T + AssignableFromPtr []ImplementsType `json:"fromptr,omitempty"` // interface types assignable only from *T + + // The following fields are set only if the query was a method. + // Assignable{To,From,FromPtr}Method[i] is the corresponding + // method of type Assignable{To,From,FromPtr}[i], or blank + // {"",""} if that type lacks the method. + Method *DescribeMethod `json:"method,omitempty"` // the queried method + AssignableToMethod []DescribeMethod `json:"to_method,omitempty"` + AssignableFromMethod []DescribeMethod `json:"from_method,omitempty"` + AssignableFromPtrMethod []DescribeMethod `json:"fromptr_method,omitempty"` +} + +// An ImplementsType describes a single type as part of an 'implements' query. +type ImplementsType struct { + Name string `json:"name"` // full name of the type + Pos string `json:"pos"` // location of its definition + Kind string `json:"kind"` // "basic", "array", etc +} + +// A SyntaxNode is one element of a stack of enclosing syntax nodes in +// a "what" query. +type SyntaxNode struct { + Description string `json:"desc"` // description of syntax tree + Start int `json:"start"` // start byte offset, 0-based + End int `json:"end"` // end byte offset +} + +// A What is the result of the "what" query, which quickly identifies +// the selection, parsing only a single file. It is intended for use +// in low-latency GUIs. +type What struct { + Enclosing []SyntaxNode `json:"enclosing"` // enclosing nodes of syntax tree + Modes []string `json:"modes"` // query modes enabled for this selection. + SrcDir string `json:"srcdir,omitempty"` // $GOROOT src directory containing queried package + ImportPath string `json:"importpath,omitempty"` // import path of queried package +} + +// A PointsToLabel describes a pointer analysis label. +// +// A "label" is an object that may be pointed to by a pointer, map, +// channel, 'func', slice or interface. Labels include: +// - functions +// - globals +// - arrays created by literals (e.g. []byte("foo")) and conversions ([]byte(s)) +// - stack- and heap-allocated variables (including composite literals) +// - arrays allocated by append() +// - channels, maps and arrays created by make() +// - and their subelements, e.g. "alloc.y[*].z" +// +type PointsToLabel struct { + Pos string `json:"pos"` // location of syntax that allocated the object + Desc string `json:"desc"` // description of the label +} + +// A PointsTo is one element of the result of a 'pointsto' query on an +// expression. It describes a single pointer: its type and the set of +// "labels" it points to. +// +// If the pointer is of interface type, it will have one PTS entry +// describing each concrete type that it may contain. For each +// concrete type that is a pointer, the PTS entry describes the labels +// it may point to. The same is true for reflect.Values, except the +// dynamic types needn't be concrete. +// +type PointsTo struct { + Type string `json:"type"` // (concrete) type of the pointer + NamePos string `json:"namepos,omitempty"` // location of type defn, if Named + Labels []PointsToLabel `json:"labels,omitempty"` // pointed-to objects +} + +// A DescribeValue is the additional result of a 'describe' query +// if the selection indicates a value or expression. +type DescribeValue struct { + Type string `json:"type"` // type of the expression + Value string `json:"value,omitempty"` // value of the expression, if constant + ObjPos string `json:"objpos,omitempty"` // location of the definition, if an Ident +} + +type DescribeMethod struct { + Name string `json:"name"` // method name, as defined by types.Selection.String() + Pos string `json:"pos"` // location of the method's definition +} + +// A DescribeType is the additional result of a 'describe' query +// if the selection indicates a type. +type DescribeType struct { + Type string `json:"type"` // the string form of the type + NamePos string `json:"namepos,omitempty"` // location of definition of type, if named + NameDef string `json:"namedef,omitempty"` // underlying definition of type, if named + Methods []DescribeMethod `json:"methods,omitempty"` // methods of the type +} + +type DescribeMember struct { + Name string `json:"name"` // name of member + Type string `json:"type,omitempty"` // type of member (underlying, if 'type') + Value string `json:"value,omitempty"` // value of member (if 'const') + Pos string `json:"pos"` // location of definition of member + Kind string `json:"kind"` // one of {var,const,func,type} + Methods []DescribeMethod `json:"methods,omitempty"` // methods (if member is a type) +} + +// A DescribePackage is the additional result of a 'describe' if +// the selection indicates a package. +type DescribePackage struct { + Path string `json:"path"` // import path of the package + Members []*DescribeMember `json:"members,omitempty"` // accessible members of the package +} + +// A Describe is the result of a 'describe' query. +// It may contain an element describing the selected semantic entity +// in detail. +type Describe struct { + Desc string `json:"desc"` // description of the selected syntax node + Pos string `json:"pos"` // location of the selected syntax node + Detail string `json:"detail,omitempty"` // one of {package, type, value}, or "". + + // At most one of the following fields is populated: + // the one specified by 'detail'. + Package *DescribePackage `json:"package,omitempty"` + Type *DescribeType `json:"type,omitempty"` + Value *DescribeValue `json:"value,omitempty"` +} + +// A WhichErrs is the result of a 'whicherrs' query. +// It contains the position of the queried error and the possible globals, +// constants, and types it may point to. +type WhichErrs struct { + ErrPos string `json:"errpos,omitempty"` // location of queried error + Globals []string `json:"globals,omitempty"` // locations of globals + Constants []string `json:"constants,omitempty"` // locations of constants + Types []WhichErrsType `json:"types,omitempty"` // Types +} + +type WhichErrsType struct { + Type string `json:"type,omitempty"` + Position string `json:"position,omitempty"` +} + +// A Result is the common result of any guru query. +// It contains a query-specific result element. +// +// TODO(adonovan): perhaps include other info such as: analysis scope, +// raw query position, stack of ast nodes, query package, etc. +type Result struct { + Mode string `json:"mode"` // mode of the query + + // Exactly one of the following fields is populated: + // the one specified by 'mode'. + Callees *Callees `json:"callees,omitempty"` + Callers []Caller `json:"callers,omitempty"` + Callstack *CallStack `json:"callstack,omitempty"` + Definition *Definition `json:"definition,omitempty"` + Describe *Describe `json:"describe,omitempty"` + Freevars []*FreeVar `json:"freevars,omitempty"` + Implements *Implements `json:"implements,omitempty"` + Peers *Peers `json:"peers,omitempty"` + PointsTo []PointsTo `json:"pointsto,omitempty"` + Referrers *Referrers `json:"referrers,omitempty"` + What *What `json:"what,omitempty"` + WhichErrs *WhichErrs `json:"whicherrs,omitempty"` +} diff --git a/cmd/guru/testdata/src/calls-json/main.go b/cmd/guru/testdata/src/calls-json/main.go new file mode 100644 index 00000000..9d58ed1e --- /dev/null +++ b/cmd/guru/testdata/src/calls-json/main.go @@ -0,0 +1,16 @@ +package main + +// Tests of call-graph queries, -format=json. +// See go.tools/guru/guru_test.go for explanation. +// See calls-json.golden for expected query results. + +func call(f func()) { + f() // @callees @callees-f "f" +} + +func main() { + call(func() { + // @callers callers-main.anon "^" + // @callstack callstack-main.anon "^" + }) +} diff --git a/cmd/guru/testdata/src/calls-json/main.golden b/cmd/guru/testdata/src/calls-json/main.golden new file mode 100644 index 00000000..f5eced6b --- /dev/null +++ b/cmd/guru/testdata/src/calls-json/main.golden @@ -0,0 +1,34 @@ +-------- @callees @callees-f -------- +{ + "mode": "callees", + "callees": { + "pos": "testdata/src/calls-json/main.go:8:3", + "desc": "dynamic function call", + "callees": [ + { + "name": "main.main$1", + "pos": "testdata/src/calls-json/main.go:12:7" + } + ] + } +} +-------- @callstack callstack-main.anon -------- +{ + "mode": "callstack", + "callstack": { + "pos": "testdata/src/calls-json/main.go:12:7", + "target": "main.main$1", + "callers": [ + { + "pos": "testdata/src/calls-json/main.go:8:3", + "desc": "dynamic function call", + "caller": "main.call" + }, + { + "pos": "testdata/src/calls-json/main.go:12:6", + "desc": "static function call", + "caller": "main.main" + } + ] + } +} diff --git a/cmd/guru/testdata/src/calls/main.go b/cmd/guru/testdata/src/calls/main.go new file mode 100644 index 00000000..a2089140 --- /dev/null +++ b/cmd/guru/testdata/src/calls/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" +) + +// Tests of call-graph queries. +// See go.tools/guru/guru_test.go for explanation. +// See calls.golden for expected query results. + +func A(x *int) { // @pointsto pointsto-A-x "x" + // @callers callers-A "^" + // @callstack callstack-A "^" +} + +func B(x *int) { // @pointsto pointsto-B-x "x" + // @callers callers-B "^" +} + +func foo() { +} + +// apply is not (yet) treated context-sensitively. +func apply(f func(x *int), x *int) { + f(x) // @callees callees-apply "f" + // @callers callers-apply "^" +} + +// store *is* treated context-sensitively, +// so the points-to sets for pc, pd are precise. +func store(ptr **int, value *int) { + *ptr = value + // @callers callers-store "^" +} + +func call(f func() *int) { + // Result points to anon function. + f() // @pointsto pointsto-result-f "f" + + // Target of call is anon function. + f() // @callees callees-main.call-f "f" + + // @callers callers-main.call "^" +} + +func main() { + var a, b int + go apply(A, &a) // @callees callees-main-apply1 "app" + defer apply(B, &b) + + var c, d int + var pc, pd *int // @pointsto pointsto-pc "pc" + store(&pc, &c) + store(&pd, &d) + _ = pd // @pointsto pointsto-pd "pd" + + call(func() *int { + // We are called twice from main.call + // @callers callers-main.anon "^" + return &a + }) + + // Errors + _ = "no function call here" // @callees callees-err-no-call "no" + print("builtin") // @callees callees-err-builtin "builtin" + _ = string("type conversion") // @callees callees-err-conversion "str" + call(nil) // @callees callees-err-bad-selection "call\\(nil" + if false { + main() // @callees callees-err-deadcode1 "main" + } + var nilFunc func() + nilFunc() // @callees callees-err-nil-func "nilFunc" + var i interface { + f() + } + i.f() // @callees callees-err-nil-interface "i.f" + + i = new(myint) + i.f() // @callees callees-not-a-wrapper "f" + + // statically dispatched calls. Handled specially by callees, so test that they work. + foo() // @callees callees-static-call "foo" + fmt.Println() // @callees callees-qualified-call "Println" + m := new(method) + m.f() // @callees callees-static-method-call "f" + g := new(embeddedIface) + g.iface = m + g.f() // @callees callees-implicit-selection-method-call "f" +} + +type myint int + +func (myint) f() { + // @callers callers-not-a-wrapper "^" +} + +type method int + +func (method) f() { +} + +type embeddedIface struct { + iface +} + +type iface interface { + f() +} + +var dynamic = func() {} + +func deadcode() { + main() // @callees callees-err-deadcode2 "main" + // @callers callers-err-deadcode "^" + // @callstack callstack-err-deadcode "^" + + // Within dead code, dynamic calls have no callees. + dynamic() // @callees callees-err-deadcode3 "dynamic" +} + +// This code belongs to init. +var global = 123 // @callers callers-global "global" + +// The package initializer may be called by other packages' inits, or +// in this case, the root of the callgraph. The source-level init functions +// are in turn called by it. +func init() { + // @callstack callstack-init "^" +} diff --git a/cmd/guru/testdata/src/calls/main.golden b/cmd/guru/testdata/src/calls/main.golden new file mode 100644 index 00000000..9159cd60 --- /dev/null +++ b/cmd/guru/testdata/src/calls/main.golden @@ -0,0 +1,125 @@ +-------- @pointsto pointsto-A-x -------- +this *int may point to these objects: + a + b + +-------- @callstack callstack-A -------- +Found a call path from root to main.A +main.A +dynamic function call from main.apply +concurrent static function call from main.main + +-------- @pointsto pointsto-B-x -------- +this *int may point to these objects: + a + b + +-------- @callers callers-B -------- +main.B is called from these 1 sites: + dynamic function call from main.apply + +-------- @callees callees-apply -------- +this dynamic function call dispatches to: + main.A + main.B + +-------- @callers callers-apply -------- +main.apply is called from these 2 sites: + concurrent static function call from main.main + deferred static function call from main.main + +-------- @callers callers-store -------- +main.store is called from these 2 sites: + static function call from main.main + static function call from main.main + +-------- @pointsto pointsto-result-f -------- +this func() *int may point to these objects: + main.main$1 + +-------- @callees callees-main.call-f -------- +this dynamic function call dispatches to: + main.main$1 + +-------- @callers callers-main.call -------- +main.call is called from these 2 sites: + static function call from main.main + static function call from main.main + +-------- @callees callees-main-apply1 -------- +this static function call dispatches to: + main.apply + +-------- @pointsto pointsto-pc -------- +this *int may point to these objects: + c + +-------- @pointsto pointsto-pd -------- +this *int may point to these objects: + d + +-------- @callees callees-err-no-call -------- + +Error: there is no function call here +-------- @callees callees-err-builtin -------- + +Error: this is a call to the built-in 'print' operator +-------- @callees callees-err-conversion -------- + +Error: this is a type conversion, not a function call +-------- @callees callees-err-bad-selection -------- + +Error: ambiguous selection within function call (or conversion) +-------- @callees callees-err-deadcode1 -------- +this static function call dispatches to: + main.main + +-------- @callees callees-err-nil-func -------- +dynamic function call on nil value + +-------- @callees callees-err-nil-interface -------- +dynamic method call on nil value + +-------- @callees callees-not-a-wrapper -------- +this dynamic method call dispatches to: + (main.myint).f + +-------- @callees callees-static-call -------- +this static function call dispatches to: + main.foo + +-------- @callees callees-qualified-call -------- +this static function call dispatches to: + fmt.Println + +-------- @callees callees-static-method-call -------- +this static function call dispatches to: + (main.method).f + +-------- @callees callees-implicit-selection-method-call -------- +this dynamic method call dispatches to: + (main.method).f + +-------- @callers callers-not-a-wrapper -------- +(main.myint).f is called from these 1 sites: + dynamic method call from main.main + +-------- @callees callees-err-deadcode2 -------- +this static function call dispatches to: + main.main + +-------- @callstack callstack-err-deadcode -------- +main.deadcode is unreachable in this analysis scope + +-------- @callees callees-err-deadcode3 -------- + +Error: this call site is unreachable in this analysis +-------- @callers callers-global -------- +main.init is called from these 1 sites: +the root of the call graph + +-------- @callstack callstack-init -------- +Found a call path from root to main.init#1 +main.init#1 +static function call from main.init + diff --git a/cmd/guru/testdata/src/describe-json/main.go b/cmd/guru/testdata/src/describe-json/main.go new file mode 100644 index 00000000..549dd8aa --- /dev/null +++ b/cmd/guru/testdata/src/describe-json/main.go @@ -0,0 +1,29 @@ +package describe // @describe pkgdecl "describe" + +// Tests of 'describe' query, -format=json. +// See go.tools/guru/guru_test.go for explanation. +// See describe-json.golden for expected query results. + +func main() { + var s struct{ x [3]int } + p := &s.x[0] // @describe desc-val-p "p" + _ = p + + var i I = C(0) + if i == nil { + i = new(D) + } + print(i) // @describe desc-val-i "\\bi\\b" + + go main() // @describe desc-stmt "go" +} + +type I interface { + f() +} + +type C int // @describe desc-type-C "C" +type D struct{} + +func (c C) f() {} +func (d *D) f() {} diff --git a/cmd/guru/testdata/src/describe-json/main.golden b/cmd/guru/testdata/src/describe-json/main.golden new file mode 100644 index 00000000..9d03661a --- /dev/null +++ b/cmd/guru/testdata/src/describe-json/main.golden @@ -0,0 +1,111 @@ +-------- @describe pkgdecl -------- +{ + "mode": "describe", + "describe": { + "desc": "definition of package \"describe-json\"", + "pos": "testdata/src/describe-json/main.go:1:9", + "detail": "package", + "package": { + "path": "describe-json", + "members": [ + { + "name": "C", + "type": "int", + "pos": "testdata/src/describe-json/main.go:25:6", + "kind": "type", + "methods": [ + { + "name": "method (C) f()", + "pos": "testdata/src/describe-json/main.go:28:12" + } + ] + }, + { + "name": "D", + "type": "struct{}", + "pos": "testdata/src/describe-json/main.go:26:6", + "kind": "type", + "methods": [ + { + "name": "method (*D) f()", + "pos": "testdata/src/describe-json/main.go:29:13" + } + ] + }, + { + "name": "I", + "type": "interface{f()}", + "pos": "testdata/src/describe-json/main.go:21:6", + "kind": "type", + "methods": [ + { + "name": "method (I) f()", + "pos": "testdata/src/describe-json/main.go:22:2" + } + ] + }, + { + "name": "main", + "type": "func()", + "pos": "testdata/src/describe-json/main.go:7:6", + "kind": "func" + } + ] + } + } +} +-------- @describe desc-val-p -------- +{ + "mode": "describe", + "describe": { + "desc": "identifier", + "pos": "testdata/src/describe-json/main.go:9:2", + "detail": "value", + "value": { + "type": "*int", + "objpos": "testdata/src/describe-json/main.go:9:2" + } + } +} +-------- @describe desc-val-i -------- +{ + "mode": "describe", + "describe": { + "desc": "identifier", + "pos": "testdata/src/describe-json/main.go:16:8", + "detail": "value", + "value": { + "type": "I", + "objpos": "testdata/src/describe-json/main.go:12:6" + } + } +} +-------- @describe desc-stmt -------- +{ + "mode": "describe", + "describe": { + "desc": "go statement", + "pos": "testdata/src/describe-json/main.go:18:2", + "detail": "unknown" + } +} +-------- @describe desc-type-C -------- +{ + "mode": "describe", + "describe": { + "desc": "definition of type C (size 8, align 8)", + "pos": "testdata/src/describe-json/main.go:25:6", + "detail": "type", + "type": { + "type": "C", + "namepos": "testdata/src/describe-json/main.go:25:6", + "namedef": "int", + "methods": [ + { + "name": "method (C) f()", + "pos": "testdata/src/describe-json/main.go:28:12" + } + ] + } + } +} diff --git a/cmd/guru/testdata/src/describe/main.go b/cmd/guru/testdata/src/describe/main.go new file mode 100644 index 00000000..5f01bb31 --- /dev/null +++ b/cmd/guru/testdata/src/describe/main.go @@ -0,0 +1,96 @@ +package describe // @describe pkgdecl "describe" + +// Tests of 'describe' query. +// See go.tools/guru/guru_test.go for explanation. +// See describe.golden for expected query results. + +// TODO(adonovan): more coverage of the (extensive) logic. + +import ( + "nosuchpkg" // @describe badimport1 "nosuchpkg" + nosuchpkg2 "nosuchpkg" // @describe badimport2 "nosuchpkg2" + _ "unsafe" // @describe unsafe "unsafe" +) + +var _ nosuchpkg.T +var _ nosuchpkg2.T + +type cake float64 // @describe type-ref-builtin "float64" + +const c = iota // @describe const-ref-iota "iota" + +const pi = 3.141 // @describe const-def-pi "pi" +const pie = cake(pi) // @describe const-def-pie "pie" +const _ = pi // @describe const-ref-pi "pi" + +var global = new(string) // NB: ssa.Global is indirect, i.e. **string + +func main() { // @describe func-def-main "main" + // func objects + _ = main // @describe func-ref-main "main" + _ = (*C).f // @describe func-ref-*C.f "..C..f" + _ = D.f // @describe func-ref-D.f "D.f" + _ = I.f // @describe func-ref-I.f "I.f" + var d D // @describe type-D "D" + var i I // @describe type-I "I" + _ = d.f // @describe func-ref-d.f "d.f" + _ = i.f // @describe func-ref-i.f "i.f" + + // var objects + anon := func() { + _ = d // @describe ref-lexical-d "d" + } + _ = anon // @describe ref-anon "anon" + _ = global // @describe ref-global "global" + + // SSA affords some local flow sensitivity. + var a, b int + var x = &a // @describe var-def-x-1 "x" + _ = x // @describe var-ref-x-1 "x" + x = &b // @describe var-def-x-2 "x" + _ = x // @describe var-ref-x-2 "x" + + i = new(C) // @describe var-ref-i-C "i" + if i != nil { + i = D{} // @describe var-ref-i-D "i" + } + print(i) // @describe var-ref-i "\\bi\\b" + + // const objects + const localpi = 3.141 // @describe const-local-pi "localpi" + const localpie = cake(pi) // @describe const-local-pie "localpie" + const _ = localpi // @describe const-ref-localpi "localpi" + + // type objects + type T int // @describe type-def-T "T" + var three T = 3 // @describe type-ref-T "T" + _ = three + + print(1 + 2*3) // @describe const-expr " 2.3" + print(real(1+2i) - 3) // @describe const-expr2 "real.*3" + + m := map[string]*int{"a": &a} + mapval, _ := m["a"] // @describe map-lookup,ok "m..a.." + _ = mapval // @describe mapval "mapval" + _ = m // @describe m "m" + + defer main() // @describe defer-stmt "defer" + go main() // @describe go-stmt "go" + + panic(3) // @describe builtin-ref-panic "panic" + + var a2 int // @describe var-decl-stmt "var a2 int" + _ = a2 + var _ int // @describe var-decl-stmt2 "var _ int" + var _ int // @describe var-def-blank "_" +} + +type I interface { // @describe def-iface-I "I" + f() // @describe def-imethod-I.f "f" +} + +type C int +type D struct{} + +func (c *C) f() {} +func (d D) f() {} diff --git a/cmd/guru/testdata/src/describe/main.golden b/cmd/guru/testdata/src/describe/main.golden new file mode 100644 index 00000000..bc6d9f70 --- /dev/null +++ b/cmd/guru/testdata/src/describe/main.golden @@ -0,0 +1,184 @@ +-------- @describe pkgdecl -------- +definition of package "describe" + type C int + method (*C) f() + type D struct{} + method (D) f() + type I interface{f()} + method (I) f() + const c untyped int = 0 + type cake float64 + var global *string + func main func() + const pi untyped float = 3.141 + const pie cake = 3.141 + +-------- @describe badimport1 -------- + +Error: can't import package "nosuchpkg" +-------- @describe badimport2 -------- + +Error: can't import package "nosuchpkg" +-------- @describe unsafe -------- +import of package "unsafe" + builtin Alignof + builtin Offsetof + type Pointer unsafe.Pointer + builtin Sizeof + +-------- @describe type-ref-builtin -------- +reference to built-in type float64 + +-------- @describe const-ref-iota -------- +reference to const iota untyped int of constant value 0 + +-------- @describe const-def-pi -------- +definition of const pi untyped float + +-------- @describe const-def-pie -------- +definition of const pie cake + +-------- @describe const-ref-pi -------- +reference to const pi untyped float of constant value 3.141 +defined here + +-------- @describe func-def-main -------- +definition of func main() + +-------- @describe func-ref-main -------- +reference to func main() +defined here + +-------- @describe func-ref-*C.f -------- +reference to method func (*C).f() +defined here + +-------- @describe func-ref-D.f -------- +reference to method func (D).f() +defined here + +-------- @describe func-ref-I.f -------- +reference to interface method func (I).f() +defined here + +-------- @describe type-D -------- +reference to type D (size 0, align 1) +defined as struct{} +Method set: + method (D) f() + +-------- @describe type-I -------- +reference to type I (size 16, align 8) +defined as interface{f()} +Method set: + method (I) f() + +-------- @describe func-ref-d.f -------- +reference to method func (D).f() +defined here + +-------- @describe func-ref-i.f -------- +reference to interface method func (I).f() +defined here + +-------- @describe ref-lexical-d -------- +reference to var d D +defined here + +-------- @describe ref-anon -------- +reference to var anon func() +defined here + +-------- @describe ref-global -------- +reference to var global *string +defined here + +-------- @describe var-def-x-1 -------- +definition of var x *int + +-------- @describe var-ref-x-1 -------- +reference to var x *int +defined here + +-------- @describe var-def-x-2 -------- +reference to var x *int +defined here + +-------- @describe var-ref-x-2 -------- +reference to var x *int +defined here + +-------- @describe var-ref-i-C -------- +reference to var i I +defined here + +-------- @describe var-ref-i-D -------- +reference to var i I +defined here + +-------- @describe var-ref-i -------- +reference to var i I +defined here + +-------- @describe const-local-pi -------- +definition of const localpi untyped float + +-------- @describe const-local-pie -------- +definition of const localpie cake + +-------- @describe const-ref-localpi -------- +reference to const localpi untyped float of constant value 3.141 +defined here + +-------- @describe type-def-T -------- +definition of type T (size 8, align 8) +No methods. + +-------- @describe type-ref-T -------- +reference to type T (size 8, align 8) +defined as int +No methods. + +-------- @describe const-expr -------- +binary * operation of constant value 6 + +-------- @describe const-expr2 -------- +binary - operation of constant value -2 + +-------- @describe map-lookup,ok -------- +index expression of type (*int, bool) + +-------- @describe mapval -------- +reference to var mapval *int +defined here + +-------- @describe m -------- +reference to var m map[string]*int +defined here + +-------- @describe defer-stmt -------- +defer statement + +-------- @describe go-stmt -------- +go statement + +-------- @describe builtin-ref-panic -------- +function call (or conversion) of type () + +-------- @describe var-decl-stmt -------- +definition of var a2 int + +-------- @describe var-decl-stmt2 -------- +definition of var _ int + +-------- @describe var-def-blank -------- +definition of var _ int + +-------- @describe def-iface-I -------- +definition of type I (size 16, align 8) +Method set: + method (I) f() + +-------- @describe def-imethod-I.f -------- +definition of interface method func (I).f() + diff --git a/cmd/guru/testdata/src/freevars/main.go b/cmd/guru/testdata/src/freevars/main.go new file mode 100644 index 00000000..5820a02c --- /dev/null +++ b/cmd/guru/testdata/src/freevars/main.go @@ -0,0 +1,41 @@ +package main + +// Tests of 'freevars' query. +// See go.tools/guru/guru_test.go for explanation. +// See freevars.golden for expected query results. + +// TODO(adonovan): it's hard to test this query in a single line of gofmt'd code. + +type T struct { + a, b int +} + +type S struct { + x int + t T +} + +func f(int) {} + +func main() { + type C int + x := 1 + const exp = 6 + if y := 2; x+y+int(C(3)) != exp { // @freevars fv1 "if.*{" + panic("expected 6") + } + + var s S + + for x, y := range "foo" { + println(s.x + s.t.a + s.t.b + x + int(y)) // @freevars fv2 "print.*y." + } + + f(x) // @freevars fv3 "f.x." + + // TODO(adonovan): enable when go/types supports labels. +loop: // #@freevars fv-def-label "loop:" + for { + break loop // #@freevars fv-ref-label "break loop" + } +} diff --git a/cmd/guru/testdata/src/freevars/main.golden b/cmd/guru/testdata/src/freevars/main.golden new file mode 100644 index 00000000..b9eeab21 --- /dev/null +++ b/cmd/guru/testdata/src/freevars/main.golden @@ -0,0 +1,18 @@ +-------- @freevars fv1 -------- +Free identifiers: +type C +const exp int +var x int + +-------- @freevars fv2 -------- +Free identifiers: +var s.t.a int +var s.t.b int +var s.x int +var x int +var y rune + +-------- @freevars fv3 -------- +Free identifiers: +var x int + diff --git a/cmd/guru/testdata/src/implements-json/main.go b/cmd/guru/testdata/src/implements-json/main.go new file mode 100644 index 00000000..e18a373a --- /dev/null +++ b/cmd/guru/testdata/src/implements-json/main.go @@ -0,0 +1,27 @@ +package main + +// Tests of 'implements' query, -output=json. +// See go.tools/guru/guru_test.go for explanation. +// See implements.golden for expected query results. + +func main() { +} + +type E interface{} // @implements E "E" + +type F interface { // @implements F "F" + f() +} + +type FG interface { // @implements FG "FG" + f() + g() []int // @implements slice "..int" +} + +type C int // @implements C "C" +type D struct{} + +func (c *C) f() {} // @implements starC ".C" +func (d D) f() {} // @implements D "D" + +func (d *D) g() []int { return nil } // @implements starD ".D" diff --git a/cmd/guru/testdata/src/implements-json/main.golden b/cmd/guru/testdata/src/implements-json/main.golden new file mode 100644 index 00000000..7e37f9e0 --- /dev/null +++ b/cmd/guru/testdata/src/implements-json/main.golden @@ -0,0 +1,159 @@ +-------- @implements E -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-json.E", + "pos": "testdata/src/implements-json/main.go:10:6", + "kind": "interface" + } + } +} +-------- @implements F -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-json.F", + "pos": "testdata/src/implements-json/main.go:12:6", + "kind": "interface" + }, + "to": [ + { + "name": "*implements-json.C", + "pos": "testdata/src/implements-json/main.go:21:6", + "kind": "pointer" + }, + { + "name": "implements-json.D", + "pos": "testdata/src/implements-json/main.go:22:6", + "kind": "struct" + }, + { + "name": "implements-json.FG", + "pos": "testdata/src/implements-json/main.go:16:6", + "kind": "interface" + } + ] + } +} +-------- @implements FG -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-json.FG", + "pos": "testdata/src/implements-json/main.go:16:6", + "kind": "interface" + }, + "to": [ + { + "name": "*implements-json.D", + "pos": "testdata/src/implements-json/main.go:22:6", + "kind": "pointer" + } + ], + "from": [ + { + "name": "implements-json.F", + "pos": "testdata/src/implements-json/main.go:12:6", + "kind": "interface" + } + ] + } +} +-------- @implements slice -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "[]int", + "pos": "-", + "kind": "slice" + } + } +} +-------- @implements C -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-json.C", + "pos": "testdata/src/implements-json/main.go:21:6", + "kind": "basic" + }, + "fromptr": [ + { + "name": "implements-json.F", + "pos": "testdata/src/implements-json/main.go:12:6", + "kind": "interface" + } + ] + } +} +-------- @implements starC -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "*implements-json.C", + "pos": "testdata/src/implements-json/main.go:21:6", + "kind": "pointer" + }, + "from": [ + { + "name": "implements-json.F", + "pos": "testdata/src/implements-json/main.go:12:6", + "kind": "interface" + } + ] + } +} +-------- @implements D -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-json.D", + "pos": "testdata/src/implements-json/main.go:22:6", + "kind": "struct" + }, + "from": [ + { + "name": "implements-json.F", + "pos": "testdata/src/implements-json/main.go:12:6", + "kind": "interface" + } + ], + "fromptr": [ + { + "name": "implements-json.FG", + "pos": "testdata/src/implements-json/main.go:16:6", + "kind": "interface" + } + ] + } +} +-------- @implements starD -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "*implements-json.D", + "pos": "testdata/src/implements-json/main.go:22:6", + "kind": "pointer" + }, + "from": [ + { + "name": "implements-json.F", + "pos": "testdata/src/implements-json/main.go:12:6", + "kind": "interface" + }, + { + "name": "implements-json.FG", + "pos": "testdata/src/implements-json/main.go:16:6", + "kind": "interface" + } + ] + } +} diff --git a/cmd/guru/testdata/src/implements-methods-json/main.go b/cmd/guru/testdata/src/implements-methods-json/main.go new file mode 100644 index 00000000..646276d5 --- /dev/null +++ b/cmd/guru/testdata/src/implements-methods-json/main.go @@ -0,0 +1,37 @@ +package main + +// Tests of 'implements' query applied to methods, -output=json. +// See go.tools/guru/guru_test.go for explanation. +// See implements-methods.golden for expected query results. + +import _ "lib" + +func main() { +} + +type F interface { + f() // @implements F.f "f" +} + +type FG interface { + f() // @implements FG.f "f" + g() []int // @implements FG.g "g" +} + +type C int +type D struct{} + +func (c *C) f() {} // @implements *C.f "f" +func (d D) f() {} // @implements D.f "f" + +func (d *D) g() []int { return nil } // @implements *D.g "g" + +type sorter []int + +func (sorter) Len() int { return 0 } // @implements Len "Len" +func (sorter) Less(i, j int) bool { return false } +func (sorter) Swap(i, j int) {} + +type I interface { + Method(*int) *int // @implements I.Method "Method" +} diff --git a/cmd/guru/testdata/src/implements-methods-json/main.golden b/cmd/guru/testdata/src/implements-methods-json/main.golden new file mode 100644 index 00000000..831900a5 --- /dev/null +++ b/cmd/guru/testdata/src/implements-methods-json/main.golden @@ -0,0 +1,312 @@ +-------- @implements F.f -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-methods-json.F", + "pos": "testdata/src/implements-methods-json/main.go:12:6", + "kind": "interface" + }, + "to": [ + { + "name": "*implements-methods-json.C", + "pos": "testdata/src/implements-methods-json/main.go:21:6", + "kind": "pointer" + }, + { + "name": "implements-methods-json.D", + "pos": "testdata/src/implements-methods-json/main.go:22:6", + "kind": "struct" + }, + { + "name": "implements-methods-json.FG", + "pos": "testdata/src/implements-methods-json/main.go:16:6", + "kind": "interface" + } + ], + "method": { + "name": "func (F).f()", + "pos": "testdata/src/implements-methods-json/main.go:13:2" + }, + "to_method": [ + { + "name": "method (*C) f()", + "pos": "testdata/src/implements-methods-json/main.go:24:13" + }, + { + "name": "method (D) f()", + "pos": "testdata/src/implements-methods-json/main.go:25:12" + }, + { + "name": "method (FG) f()", + "pos": "testdata/src/implements-methods-json/main.go:17:2" + } + ] + } +} +-------- @implements FG.f -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-methods-json.FG", + "pos": "testdata/src/implements-methods-json/main.go:16:6", + "kind": "interface" + }, + "to": [ + { + "name": "*implements-methods-json.D", + "pos": "testdata/src/implements-methods-json/main.go:22:6", + "kind": "pointer" + } + ], + "from": [ + { + "name": "implements-methods-json.F", + "pos": "testdata/src/implements-methods-json/main.go:12:6", + "kind": "interface" + } + ], + "method": { + "name": "func (FG).f()", + "pos": "testdata/src/implements-methods-json/main.go:17:2" + }, + "to_method": [ + { + "name": "method (*D) f()", + "pos": "testdata/src/implements-methods-json/main.go:25:12" + } + ], + "from_method": [ + { + "name": "method (F) f()", + "pos": "testdata/src/implements-methods-json/main.go:13:2" + } + ] + } +} +-------- @implements FG.g -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-methods-json.FG", + "pos": "testdata/src/implements-methods-json/main.go:16:6", + "kind": "interface" + }, + "to": [ + { + "name": "*implements-methods-json.D", + "pos": "testdata/src/implements-methods-json/main.go:22:6", + "kind": "pointer" + } + ], + "from": [ + { + "name": "implements-methods-json.F", + "pos": "testdata/src/implements-methods-json/main.go:12:6", + "kind": "interface" + } + ], + "method": { + "name": "func (FG).g() []int", + "pos": "testdata/src/implements-methods-json/main.go:18:2" + }, + "to_method": [ + { + "name": "method (*D) g() []int", + "pos": "testdata/src/implements-methods-json/main.go:27:13" + } + ], + "from_method": [ + { + "name": "", + "pos": "" + } + ] + } +} +-------- @implements *C.f -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "*implements-methods-json.C", + "pos": "testdata/src/implements-methods-json/main.go:21:6", + "kind": "pointer" + }, + "from": [ + { + "name": "implements-methods-json.F", + "pos": "testdata/src/implements-methods-json/main.go:12:6", + "kind": "interface" + } + ], + "method": { + "name": "func (*C).f()", + "pos": "testdata/src/implements-methods-json/main.go:24:13" + }, + "from_method": [ + { + "name": "method (F) f()", + "pos": "testdata/src/implements-methods-json/main.go:13:2" + } + ] + } +} +-------- @implements D.f -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-methods-json.D", + "pos": "testdata/src/implements-methods-json/main.go:22:6", + "kind": "struct" + }, + "from": [ + { + "name": "implements-methods-json.F", + "pos": "testdata/src/implements-methods-json/main.go:12:6", + "kind": "interface" + } + ], + "fromptr": [ + { + "name": "implements-methods-json.FG", + "pos": "testdata/src/implements-methods-json/main.go:16:6", + "kind": "interface" + } + ], + "method": { + "name": "func (D).f()", + "pos": "testdata/src/implements-methods-json/main.go:25:12" + }, + "from_method": [ + { + "name": "method (F) f()", + "pos": "testdata/src/implements-methods-json/main.go:13:2" + } + ], + "fromptr_method": [ + { + "name": "method (FG) f()", + "pos": "testdata/src/implements-methods-json/main.go:17:2" + } + ] + } +} +-------- @implements *D.g -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "*implements-methods-json.D", + "pos": "testdata/src/implements-methods-json/main.go:22:6", + "kind": "pointer" + }, + "from": [ + { + "name": "implements-methods-json.F", + "pos": "testdata/src/implements-methods-json/main.go:12:6", + "kind": "interface" + }, + { + "name": "implements-methods-json.FG", + "pos": "testdata/src/implements-methods-json/main.go:16:6", + "kind": "interface" + } + ], + "method": { + "name": "func (*D).g() []int", + "pos": "testdata/src/implements-methods-json/main.go:27:13" + }, + "from_method": [ + { + "name": "", + "pos": "" + }, + { + "name": "method (FG) g() []int", + "pos": "testdata/src/implements-methods-json/main.go:18:2" + } + ] + } +} +-------- @implements Len -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-methods-json.sorter", + "pos": "testdata/src/implements-methods-json/main.go:29:6", + "kind": "slice" + }, + "from": [ + { + "name": "lib.Sorter", + "pos": "testdata/src/lib/lib.go:16:6", + "kind": "interface" + } + ], + "method": { + "name": "func (sorter).Len() int", + "pos": "testdata/src/implements-methods-json/main.go:31:15" + }, + "from_method": [ + { + "name": "method (lib.Sorter) Len() int", + "pos": "testdata/src/lib/lib.go:17:2" + } + ] + } +} +-------- @implements I.Method -------- +{ + "mode": "implements", + "implements": { + "type": { + "name": "implements-methods-json.I", + "pos": "testdata/src/implements-methods-json/main.go:35:6", + "kind": "interface" + }, + "to": [ + { + "name": "lib.Type", + "pos": "testdata/src/lib/lib.go:3:6", + "kind": "basic" + }, + { + "name": "main.I", + "pos": "testdata/src/implements-methods-json/main.go:35:6", + "kind": "interface" + } + ], + "from": [ + { + "name": "main.I", + "pos": "testdata/src/implements-methods-json/main.go:35:6", + "kind": "interface" + } + ], + "method": { + "name": "func (I).Method(*int) *int", + "pos": "testdata/src/implements-methods-json/main.go:36:2" + }, + "to_method": [ + { + "name": "method (lib.Type) Method(x *int) *int", + "pos": "testdata/src/lib/lib.go:5:13" + }, + { + "name": "method (main.I) Method(*int) *int", + "pos": "testdata/src/implements-methods-json/main.go:36:2" + } + ], + "from_method": [ + { + "name": "method (main.I) Method(*int) *int", + "pos": "testdata/src/implements-methods-json/main.go:36:2" + } + ] + } +} diff --git a/cmd/guru/testdata/src/implements-methods/main.go b/cmd/guru/testdata/src/implements-methods/main.go new file mode 100644 index 00000000..757be44a --- /dev/null +++ b/cmd/guru/testdata/src/implements-methods/main.go @@ -0,0 +1,37 @@ +package main + +// Tests of 'implements' query applied to methods. +// See go.tools/guru/guru_test.go for explanation. +// See implements-methods.golden for expected query results. + +import _ "lib" + +func main() { +} + +type F interface { + f() // @implements F.f "f" +} + +type FG interface { + f() // @implements FG.f "f" + g() []int // @implements FG.g "g" +} + +type C int +type D struct{} + +func (c *C) f() {} // @implements *C.f "f" +func (d D) f() {} // @implements D.f "f" + +func (d *D) g() []int { return nil } // @implements *D.g "g" + +type sorter []int + +func (sorter) Len() int { return 0 } // @implements Len "Len" +func (sorter) Less(i, j int) bool { return false } +func (sorter) Swap(i, j int) {} + +type I interface { + Method(*int) *int // @implements I.Method "Method" +} diff --git a/cmd/guru/testdata/src/implements-methods/main.golden b/cmd/guru/testdata/src/implements-methods/main.golden new file mode 100644 index 00000000..227d305c --- /dev/null +++ b/cmd/guru/testdata/src/implements-methods/main.golden @@ -0,0 +1,39 @@ +-------- @implements F.f -------- +abstract method func (F).f() + is implemented by method (*C).f + is implemented by method (D).f + is implemented by method (FG).f + +-------- @implements FG.f -------- +abstract method func (FG).f() + is implemented by method (*D).f + implements method (F).f + +-------- @implements FG.g -------- +abstract method func (FG).g() []int + is implemented by method (*D).g + +-------- @implements *C.f -------- +concrete method func (*C).f() + implements method (F).f + +-------- @implements D.f -------- +concrete method func (D).f() + implements method (F).f +concrete method func (D).f() + implements method (FG).f + +-------- @implements *D.g -------- +concrete method func (*D).g() []int + implements method (FG).g + +-------- @implements Len -------- +concrete method func (sorter).Len() int + implements method (lib.Sorter).Len + +-------- @implements I.Method -------- +abstract method func (I).Method(*int) *int + is implemented by method (lib.Type).Method + is implemented by method (main.I).Method + implements method (main.I).Method + diff --git a/cmd/guru/testdata/src/implements/main.go b/cmd/guru/testdata/src/implements/main.go new file mode 100644 index 00000000..22457d99 --- /dev/null +++ b/cmd/guru/testdata/src/implements/main.go @@ -0,0 +1,39 @@ +package main + +// Tests of 'implements' query. +// See go.tools/guru/guru_test.go for explanation. +// See implements.golden for expected query results. + +import _ "lib" + +func main() { +} + +type E interface{} // @implements E "E" + +type F interface { // @implements F "F" + f() +} + +type FG interface { // @implements FG "FG" + f() + g() []int // @implements slice "..int" +} + +type C int // @implements C "C" +type D struct{} + +func (c *C) f() {} // @implements starC ".C" +func (d D) f() {} // @implements D "D" + +func (d *D) g() []int { return nil } // @implements starD ".D" + +type sorter []int // @implements sorter "sorter" + +func (sorter) Len() int { return 0 } +func (sorter) Less(i, j int) bool { return false } +func (sorter) Swap(i, j int) {} + +type I interface { // @implements I "I" + Method(*int) *int +} diff --git a/cmd/guru/testdata/src/implements/main.golden b/cmd/guru/testdata/src/implements/main.golden new file mode 100644 index 00000000..cb2f2ac5 --- /dev/null +++ b/cmd/guru/testdata/src/implements/main.golden @@ -0,0 +1,46 @@ +-------- @implements E -------- +empty interface type E + +-------- @implements F -------- +interface type F + is implemented by pointer type *C + is implemented by struct type D + is implemented by interface type FG + +-------- @implements FG -------- +interface type FG + is implemented by pointer type *D + implements F + +-------- @implements slice -------- +slice type []int implements only interface{} + +-------- @implements C -------- +pointer type *C + implements F + +-------- @implements starC -------- +pointer type *C + implements F + +-------- @implements D -------- +struct type D + implements F +pointer type *D + implements FG + +-------- @implements starD -------- +pointer type *D + implements F + implements FG + +-------- @implements sorter -------- +slice type sorter + implements lib.Sorter + +-------- @implements I -------- +interface type I + is implemented by basic type lib.Type + is implemented by interface type main.I + implements main.I + diff --git a/cmd/guru/testdata/src/imports/main.go b/cmd/guru/testdata/src/imports/main.go new file mode 100644 index 00000000..3bab36a9 --- /dev/null +++ b/cmd/guru/testdata/src/imports/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "hash/fnv" // @describe ref-pkg-import2 "fnv" + "lib" // @describe ref-pkg-import "lib" +) + +// Tests that import another package. (To make the tests run quickly, +// we avoid using imports in all the other tests. Remember, each +// query causes parsing and typechecking of the whole program.) +// +// See go.tools/guru/guru_test.go for explanation. +// See imports.golden for expected query results. + +var a int + +func main() { + const c = lib.Const // @describe ref-const "Const" + lib.Func() // @describe ref-func "Func" + lib.Var++ // @describe ref-var "Var" + var t lib.Type // @describe ref-type "Type" + p := t.Method(&a) // @describe ref-method "Method" + + print(*p + 1) // @pointsto p "p " + + var _ lib.Type // @describe ref-pkg "lib" + + fnv.New32() +} diff --git a/cmd/guru/testdata/src/imports/main.golden b/cmd/guru/testdata/src/imports/main.golden new file mode 100644 index 00000000..91442106 --- /dev/null +++ b/cmd/guru/testdata/src/imports/main.golden @@ -0,0 +1,57 @@ +-------- @describe ref-pkg-import2 -------- +import of package "hash/fnv" + func New32 func() hash.Hash32 + func New32a func() hash.Hash32 + func New64 func() hash.Hash64 + func New64a func() hash.Hash64 + +-------- @describe ref-pkg-import -------- +import of package "lib" + const Const untyped int = 3 + func Func func() + type Sorter interface{...} + method (Sorter) Len() int + method (Sorter) Less(i int, j int) bool + method (Sorter) Swap(i int, j int) + type Type int + method (Type) Method(x *int) *int + var Var int + +-------- @describe ref-const -------- +reference to const lib.Const untyped int +defined here + +-------- @describe ref-func -------- +reference to func lib.Func() +defined here + +-------- @describe ref-var -------- +reference to var lib.Var int +defined here + +-------- @describe ref-type -------- +reference to type lib.Type (size 8, align 8) +defined as int +Method set: + method (lib.Type) Method(x *int) *int + +-------- @describe ref-method -------- +reference to method func (lib.Type).Method(x *int) *int +defined here + +-------- @pointsto p -------- +this *int may point to these objects: + main.a + +-------- @describe ref-pkg -------- +reference to package "lib" + const Const untyped int = 3 + func Func func() + type Sorter interface{...} + method (Sorter) Len() int + method (Sorter) Less(i int, j int) bool + method (Sorter) Swap(i int, j int) + type Type int + method (Type) Method(x *int) *int + var Var int + diff --git a/cmd/guru/testdata/src/lib/lib.go b/cmd/guru/testdata/src/lib/lib.go new file mode 100644 index 00000000..9131c274 --- /dev/null +++ b/cmd/guru/testdata/src/lib/lib.go @@ -0,0 +1,20 @@ +package lib + +type Type int + +func (Type) Method(x *int) *int { + return x +} + +func Func() { +} + +const Const = 3 + +var Var = 0 + +type Sorter interface { + Len() int + Less(i, j int) bool + Swap(i, j int) +} diff --git a/cmd/guru/testdata/src/main/multi.go b/cmd/guru/testdata/src/main/multi.go new file mode 100644 index 00000000..8c650cd2 --- /dev/null +++ b/cmd/guru/testdata/src/main/multi.go @@ -0,0 +1,13 @@ +package main + +func g(x int) { +} + +func f() { + x := 1 + g(x) // "g(x)" is the selection for multiple queries +} + +func main() { + f() +} diff --git a/cmd/guru/testdata/src/peers-json/main.go b/cmd/guru/testdata/src/peers-json/main.go new file mode 100644 index 00000000..ef63992b --- /dev/null +++ b/cmd/guru/testdata/src/peers-json/main.go @@ -0,0 +1,13 @@ +package main + +// Tests of channel 'peers' query, -format=json. +// See go.tools/guru/guru_test.go for explanation. +// See peers-json.golden for expected query results. + +func main() { + chA := make(chan *int) + <-chA + select { + case <-chA: // @peers peer-recv-chA "<-" + } +} diff --git a/cmd/guru/testdata/src/peers-json/main.golden b/cmd/guru/testdata/src/peers-json/main.golden new file mode 100644 index 00000000..8c2d06c7 --- /dev/null +++ b/cmd/guru/testdata/src/peers-json/main.golden @@ -0,0 +1,15 @@ +-------- @peers peer-recv-chA -------- +{ + "mode": "peers", + "peers": { + "pos": "testdata/src/peers-json/main.go:11:7", + "type": "chan *int", + "allocs": [ + "testdata/src/peers-json/main.go:8:13" + ], + "receives": [ + "testdata/src/peers-json/main.go:9:2", + "testdata/src/peers-json/main.go:11:7" + ] + } +} diff --git a/cmd/guru/testdata/src/peers/main.go b/cmd/guru/testdata/src/peers/main.go new file mode 100644 index 00000000..fa533f64 --- /dev/null +++ b/cmd/guru/testdata/src/peers/main.go @@ -0,0 +1,52 @@ +package main + +// Tests of channel 'peers' query. +// See go.tools/guru/guru_test.go for explanation. +// See peers.golden for expected query results. + +var a2 int + +func main() { + chA := make(chan *int) + a1 := 1 + chA <- &a1 + + chA2 := make(chan *int, 2) + if a2 == 0 { + chA = chA2 + } + + chB := make(chan *int) + b := 3 + chB <- &b + + <-chA // @pointsto pointsto-chA "chA" + <-chA2 // @pointsto pointsto-chA2 "chA2" + <-chB // @pointsto pointsto-chB "chB" + + select { + case rA := <-chA: // @peers peer-recv-chA "<-" + _ = rA // @pointsto pointsto-rA "rA" + case rB := <-chB: // @peers peer-recv-chB "<-" + _ = rB // @pointsto pointsto-rB "rB" + + case <-chA: // @peers peer-recv-chA' "<-" + + case chA2 <- &a2: // @peers peer-send-chA' "<-" + } + + for _ = range chA { + } + + close(chA) // @peers peer-close-chA "chA" + + chC := make(chan *int) + (close)(chC) // @peers peer-close-chC "chC" + + close := func(ch chan *int) chan *int { + return ch + } + + close(chC) <- &b // @peers peer-send-chC "chC" + <-close(chC) // @peers peer-recv-chC "chC" +} diff --git a/cmd/guru/testdata/src/peers/main.golden b/cmd/guru/testdata/src/peers/main.golden new file mode 100644 index 00000000..597a3c6c --- /dev/null +++ b/cmd/guru/testdata/src/peers/main.golden @@ -0,0 +1,100 @@ +-------- @pointsto pointsto-chA -------- +this chan *int may point to these objects: + makechan + makechan + +-------- @pointsto pointsto-chA2 -------- +this chan *int may point to these objects: + makechan + +-------- @pointsto pointsto-chB -------- +this chan *int may point to these objects: + makechan + +-------- @peers peer-recv-chA -------- +This channel of type chan *int may be: + allocated here + allocated here + sent to, here + sent to, here + received from, here + received from, here + received from, here + received from, here + received from, here + closed, here + +-------- @pointsto pointsto-rA -------- +this *int may point to these objects: + main.a2 + a1 + +-------- @peers peer-recv-chB -------- +This channel of type chan *int may be: + allocated here + sent to, here + received from, here + received from, here + +-------- @pointsto pointsto-rB -------- +this *int may point to these objects: + b + +-------- @peers peer-recv-chA' -------- +This channel of type chan *int may be: + allocated here + allocated here + sent to, here + sent to, here + received from, here + received from, here + received from, here + received from, here + received from, here + closed, here + +-------- @peers peer-send-chA' -------- +This channel of type chan *int may be: + allocated here + sent to, here + received from, here + received from, here + received from, here + received from, here + received from, here + closed, here + +-------- @peers peer-close-chA -------- +This channel of type chan *int may be: + allocated here + allocated here + sent to, here + sent to, here + received from, here + received from, here + received from, here + received from, here + received from, here + closed, here + +-------- @peers peer-close-chC -------- +This channel of type chan *int may be: + allocated here + sent to, here + received from, here + closed, here + +-------- @peers peer-send-chC -------- +This channel of type chan *int may be: + allocated here + sent to, here + received from, here + closed, here + +-------- @peers peer-recv-chC -------- +This channel of type chan *int may be: + allocated here + sent to, here + received from, here + closed, here + diff --git a/cmd/guru/testdata/src/pointsto-json/main.go b/cmd/guru/testdata/src/pointsto-json/main.go new file mode 100644 index 00000000..0a9f3186 --- /dev/null +++ b/cmd/guru/testdata/src/pointsto-json/main.go @@ -0,0 +1,27 @@ +package main + +// Tests of 'pointsto' queries, -format=json. +// See go.tools/guru/guru_test.go for explanation. +// See pointsto-json.golden for expected query results. + +func main() { // + var s struct{ x [3]int } + p := &s.x[0] // @pointsto val-p "p" + _ = p + + var i I = C(0) + if i == nil { + i = new(D) + } + print(i) // @pointsto val-i "\\bi\\b" +} + +type I interface { + f() +} + +type C int +type D struct{} + +func (c C) f() {} +func (d *D) f() {} diff --git a/cmd/guru/testdata/src/pointsto-json/main.golden b/cmd/guru/testdata/src/pointsto-json/main.golden new file mode 100644 index 00000000..13ac1df9 --- /dev/null +++ b/cmd/guru/testdata/src/pointsto-json/main.golden @@ -0,0 +1,35 @@ +-------- @pointsto val-p -------- +{ + "mode": "pointsto", + "pointsto": [ + { + "type": "*int", + "labels": [ + { + "pos": "testdata/src/pointsto-json/main.go:8:6", + "desc": "s.x[*]" + } + ] + } + ] +} +-------- @pointsto val-i -------- +{ + "mode": "pointsto", + "pointsto": [ + { + "type": "*D", + "namepos": "testdata/src/pointsto-json/main.go:24:6", + "labels": [ + { + "pos": "testdata/src/pointsto-json/main.go:14:10", + "desc": "new" + } + ] + }, + { + "type": "C", + "namepos": "testdata/src/pointsto-json/main.go:23:6" + } + ] +} diff --git a/cmd/guru/testdata/src/pointsto/main.go b/cmd/guru/testdata/src/pointsto/main.go new file mode 100644 index 00000000..c4ba2e25 --- /dev/null +++ b/cmd/guru/testdata/src/pointsto/main.go @@ -0,0 +1,75 @@ +package main + +// Tests of 'pointsto' query. +// See go.tools/guru/guru_test.go for explanation. +// See pointsto.golden for expected query results. + +const pi = 3.141 // @pointsto const "pi" + +var global = new(string) // NB: ssa.Global is indirect, i.e. **string + +func main() { + livecode() + + // func objects + _ = main // @pointsto func-ref-main "main" + _ = (*C).f // @pointsto func-ref-*C.f "..C..f" + _ = D.f // @pointsto func-ref-D.f "D.f" + _ = I.f // @pointsto func-ref-I.f "I.f" + var d D + var i I + _ = d.f // @pointsto func-ref-d.f "d.f" + _ = i.f // @pointsto func-ref-i.f "i.f" + + // var objects + anon := func() { + _ = d.f // @pointsto ref-lexical-d.f "d.f" + } + _ = anon // @pointsto ref-anon "anon" + _ = global // @pointsto ref-global "global" + + // SSA affords some local flow sensitivity. + var a, b int + var x = &a // @pointsto var-def-x-1 "x" + _ = x // @pointsto var-ref-x-1 "x" + x = &b // @pointsto var-def-x-2 "x" + _ = x // @pointsto var-ref-x-2 "x" + + i = new(C) // @pointsto var-ref-i-C "i" + if i != nil { + i = D{} // @pointsto var-ref-i-D "i" + } + print(i) // @pointsto var-ref-i "\\bi\\b" + + m := map[string]*int{"a": &a} + mapval, _ := m["a"] // @pointsto map-lookup,ok "m..a.." + _ = mapval // @pointsto mapval "mapval" + _ = m // @pointsto m "m" + + if false { + panic(3) // @pointsto builtin-panic "panic" + } + + // NB: s.f is addressable per (*ssa.Program).VarValue, + // but our query concerns the object, not its address. + s := struct{ f interface{} }{f: make(chan bool)} + print(s.f) // @pointsto var-ref-s-f "s.f" +} + +func livecode() {} // @pointsto func-live "livecode" + +func deadcode() { // @pointsto func-dead "deadcode" + // Pointer analysis can't run on dead code. + var b = new(int) // @pointsto b "b" + _ = b +} + +type I interface { + f() +} + +type C int +type D struct{} + +func (c *C) f() {} +func (d D) f() {} diff --git a/cmd/guru/testdata/src/pointsto/main.golden b/cmd/guru/testdata/src/pointsto/main.golden new file mode 100644 index 00000000..fd68bda6 --- /dev/null +++ b/cmd/guru/testdata/src/pointsto/main.golden @@ -0,0 +1,96 @@ +-------- @pointsto const -------- + +Error: pointer analysis wants an expression of reference type; got untyped float +-------- @pointsto func-ref-main -------- +this func() may point to these objects: + main.main + +-------- @pointsto func-ref-*C.f -------- +this func() may point to these objects: + (*main.C).f + +-------- @pointsto func-ref-D.f -------- +this func() may point to these objects: + (main.D).f + +-------- @pointsto func-ref-I.f -------- + +Error: func (main.I).f() is an interface method +-------- @pointsto func-ref-d.f -------- +this func() may point to these objects: + (main.D).f + +-------- @pointsto func-ref-i.f -------- + +Error: func (main.I).f() is an interface method +-------- @pointsto ref-lexical-d.f -------- +this func() may point to these objects: + (main.D).f + +-------- @pointsto ref-anon -------- +this func() may point to these objects: + main.main$1 + +-------- @pointsto ref-global -------- +this *string may point to these objects: + new + +-------- @pointsto var-def-x-1 -------- +this *int may point to these objects: + a + +-------- @pointsto var-ref-x-1 -------- +this *int may point to these objects: + a + +-------- @pointsto var-def-x-2 -------- +this *int may point to these objects: + b + +-------- @pointsto var-ref-x-2 -------- +this *int may point to these objects: + b + +-------- @pointsto var-ref-i-C -------- +this I may contain these dynamic types: + *C, may point to: + new + +-------- @pointsto var-ref-i-D -------- +this I may contain these dynamic types: + D + +-------- @pointsto var-ref-i -------- +this I may contain these dynamic types: + *C, may point to: + new + D + +-------- @pointsto map-lookup,ok -------- + +Error: pointer analysis wants an expression of reference type; got (*int, bool) +-------- @pointsto mapval -------- +this *int may point to these objects: + a + +-------- @pointsto m -------- +this map[string]*int may point to these objects: + makemap + +-------- @pointsto builtin-panic -------- + +Error: pointer analysis wants an expression of reference type; got () +-------- @pointsto var-ref-s-f -------- +this interface{} may contain these dynamic types: + chan bool, may point to: + makechan + +-------- @pointsto func-live -------- + +Error: pointer analysis did not find expression (dead code?) +-------- @pointsto func-dead -------- + +Error: pointer analysis did not find expression (dead code?) +-------- @pointsto b -------- + +Error: pointer analysis did not find expression (dead code?) diff --git a/cmd/guru/testdata/src/referrers-json/main.go b/cmd/guru/testdata/src/referrers-json/main.go new file mode 100644 index 00000000..0fd23425 --- /dev/null +++ b/cmd/guru/testdata/src/referrers-json/main.go @@ -0,0 +1,24 @@ +package main + +// Tests of 'referrers' query. +// See go.tools/guru/guru_test.go for explanation. +// See referrers.golden for expected query results. + +import "lib" + +type s struct { + f int +} + +func main() { + var v lib.Type = lib.Const // @referrers ref-package "lib" + _ = v.Method // @referrers ref-method "Method" + _ = v.Method + v++ //@referrers ref-local "v" + v++ + + _ = s{}.f // @referrers ref-field "f" + + var s2 s + s2.f = 1 +} diff --git a/cmd/guru/testdata/src/referrers-json/main.golden b/cmd/guru/testdata/src/referrers-json/main.golden new file mode 100644 index 00000000..47a2d01b --- /dev/null +++ b/cmd/guru/testdata/src/referrers-json/main.golden @@ -0,0 +1,59 @@ +-------- @referrers ref-package -------- +{ + "mode": "referrers", + "referrers": { + "pos": "testdata/src/referrers-json/main.go:14:8", + "objpos": "testdata/src/referrers-json/main.go:7:8", + "desc": "package lib", + "refs": [ + "testdata/src/referrers-json/main.go:14:8", + "testdata/src/referrers-json/main.go:14:19" + ] + } +} +-------- @referrers ref-method -------- +{ + "mode": "referrers", + "referrers": { + "pos": "testdata/src/referrers-json/main.go:15:8", + "objpos": "testdata/src/lib/lib.go:5:13", + "desc": "func (lib.Type).Method(x *int) *int", + "refs": [ + "testdata/src/imports/main.go:22:9", + "testdata/src/referrers-json/main.go:15:8", + "testdata/src/referrers-json/main.go:16:8", + "testdata/src/referrers/ext_test.go:10:17", + "testdata/src/referrers/int_test.go:7:17", + "testdata/src/referrers/main.go:17:8", + "testdata/src/referrers/main.go:18:8" + ] + } +} +-------- @referrers ref-local -------- +{ + "mode": "referrers", + "referrers": { + "pos": "testdata/src/referrers-json/main.go:17:2", + "objpos": "testdata/src/referrers-json/main.go:14:6", + "desc": "var v lib.Type", + "refs": [ + "testdata/src/referrers-json/main.go:15:6", + "testdata/src/referrers-json/main.go:16:6", + "testdata/src/referrers-json/main.go:17:2", + "testdata/src/referrers-json/main.go:18:2" + ] + } +} +-------- @referrers ref-field -------- +{ + "mode": "referrers", + "referrers": { + "pos": "testdata/src/referrers-json/main.go:20:10", + "objpos": "testdata/src/referrers-json/main.go:10:2", + "desc": "field f int", + "refs": [ + "testdata/src/referrers-json/main.go:20:10", + "testdata/src/referrers-json/main.go:23:5" + ] + } +} diff --git a/cmd/guru/testdata/src/referrers/ext_test.go b/cmd/guru/testdata/src/referrers/ext_test.go new file mode 100644 index 00000000..35e3199a --- /dev/null +++ b/cmd/guru/testdata/src/referrers/ext_test.go @@ -0,0 +1,12 @@ +package main_test + +import ( + "lib" + renamed "referrers" // package has name "main", path "referrers", local name "renamed" +) + +func _() { + // This reference should be found by the ref-method query. + _ = (lib.Type).Method // ref from external test package + var _ renamed.T +} diff --git a/cmd/guru/testdata/src/referrers/int_test.go b/cmd/guru/testdata/src/referrers/int_test.go new file mode 100644 index 00000000..9102cd6f --- /dev/null +++ b/cmd/guru/testdata/src/referrers/int_test.go @@ -0,0 +1,8 @@ +package main + +import "lib" + +func _() { + // This reference should be found by the ref-method query. + _ = (lib.Type).Method // ref from internal test package +} diff --git a/cmd/guru/testdata/src/referrers/main.go b/cmd/guru/testdata/src/referrers/main.go new file mode 100644 index 00000000..1fc80ea2 --- /dev/null +++ b/cmd/guru/testdata/src/referrers/main.go @@ -0,0 +1,34 @@ +package main // @referrers package-decl "main" + +// Tests of 'referrers' query. +// See go.tools/guru/guru_test.go for explanation. +// See referrers.golden for expected query results. + +import "lib" + +type s struct { // @referrers type " s " + f int +} + +type T int + +func main() { + var v lib.Type = lib.Const // @referrers ref-package "lib" + _ = v.Method // @referrers ref-method "Method" + _ = v.Method + v++ //@referrers ref-local "v" + v++ + + _ = s{}.f // @referrers ref-field "f" + + var s2 s + s2.f = 1 +} + +// Test //line directives: + +type U int // @referrers ref-type-U "U" + +//line nosuchfile.y:123 +var u1 U +var u2 U diff --git a/cmd/guru/testdata/src/referrers/main.golden b/cmd/guru/testdata/src/referrers/main.golden new file mode 100644 index 00000000..d98d5109 --- /dev/null +++ b/cmd/guru/testdata/src/referrers/main.golden @@ -0,0 +1,42 @@ +-------- @referrers package-decl -------- +1 references to package main ("referrers") + var _ renamed.T + +-------- @referrers type -------- +2 references to type s struct{f int} + _ = s{}.f // @referrers ref-field "f" + var s2 s + +-------- @referrers ref-package -------- +4 references to package lib + _ = (lib.Type).Method // ref from external test package + _ = (lib.Type).Method // ref from internal test package + var v lib.Type = lib.Const // @referrers ref-package "lib" + var v lib.Type = lib.Const // @referrers ref-package "lib" + +-------- @referrers ref-method -------- +7 references to func (lib.Type).Method(x *int) *int + p := t.Method(&a) // @describe ref-method "Method" + _ = v.Method // @referrers ref-method "Method" + _ = v.Method + _ = (lib.Type).Method // ref from external test package + _ = (lib.Type).Method // ref from internal test package + _ = v.Method // @referrers ref-method "Method" + _ = v.Method + +-------- @referrers ref-local -------- +4 references to var v lib.Type + _ = v.Method // @referrers ref-method "Method" + _ = v.Method + v++ //@referrers ref-local "v" + v++ + +-------- @referrers ref-field -------- +2 references to field f int + _ = s{}.f // @referrers ref-field "f" + s2.f = 1 + +-------- @referrers ref-type-U -------- +2 references to type U int +open testdata/src/referrers/nosuchfile.y: no such file or directory (+ 1 more refs in this file) + diff --git a/cmd/guru/testdata/src/reflection/main.go b/cmd/guru/testdata/src/reflection/main.go new file mode 100644 index 00000000..392643ba --- /dev/null +++ b/cmd/guru/testdata/src/reflection/main.go @@ -0,0 +1,30 @@ +package main + +// This is a test of 'pointsto', but we split it into a separate file +// so that pointsto.go doesn't have to import "reflect" each time. + +import "reflect" + +var a int +var b bool + +func main() { + m := make(map[*int]*bool) + m[&a] = &b + + mrv := reflect.ValueOf(m) + if a > 0 { + mrv = reflect.ValueOf(&b) + } + if a > 0 { + mrv = reflect.ValueOf(&a) + } + + _ = mrv // @pointsto mrv "mrv" + p1 := mrv.Interface() // @pointsto p1 "p1" + p2 := mrv.MapKeys() // @pointsto p2 "p2" + p3 := p2[0] // @pointsto p3 "p3" + p4 := reflect.TypeOf(p1) // @pointsto p4 "p4" + + _, _, _, _ = p1, p2, p3, p4 +} diff --git a/cmd/guru/testdata/src/reflection/main.golden b/cmd/guru/testdata/src/reflection/main.golden new file mode 100644 index 00000000..6190c065 --- /dev/null +++ b/cmd/guru/testdata/src/reflection/main.golden @@ -0,0 +1,34 @@ +-------- @pointsto mrv -------- +this reflect.Value may contain these dynamic types: + *bool, may point to: + main.b + *int, may point to: + main.a + map[*int]*bool, may point to: + makemap + +-------- @pointsto p1 -------- +this interface{} may contain these dynamic types: + *bool, may point to: + main.b + *int, may point to: + main.a + map[*int]*bool, may point to: + makemap + +-------- @pointsto p2 -------- +this []reflect.Value may point to these objects: + + +-------- @pointsto p3 -------- +this reflect.Value may contain these dynamic types: + *int, may point to: + main.a + +-------- @pointsto p4 -------- +this reflect.Type may contain these dynamic types: + *reflect.rtype, may point to: + *bool + *int + map[*int]*bool + diff --git a/cmd/guru/testdata/src/what-json/main.go b/cmd/guru/testdata/src/what-json/main.go new file mode 100644 index 00000000..49fd0a84 --- /dev/null +++ b/cmd/guru/testdata/src/what-json/main.go @@ -0,0 +1,9 @@ +package main + +// Tests of 'what' queries, -format=json. +// See go.tools/guru/guru_test.go for explanation. +// See what-json.golden for expected query results. + +func main() { + f() // @what call "f" +} diff --git a/cmd/guru/testdata/src/what-json/main.golden b/cmd/guru/testdata/src/what-json/main.golden new file mode 100644 index 00000000..ee1e3c7c --- /dev/null +++ b/cmd/guru/testdata/src/what-json/main.golden @@ -0,0 +1,51 @@ +-------- @what call -------- +{ + "mode": "what", + "what": { + "enclosing": [ + { + "desc": "identifier", + "start": 175, + "end": 176 + }, + { + "desc": "function call (or conversion)", + "start": 175, + "end": 178 + }, + { + "desc": "expression statement", + "start": 175, + "end": 178 + }, + { + "desc": "block", + "start": 172, + "end": 198 + }, + { + "desc": "function declaration", + "start": 160, + "end": 198 + }, + { + "desc": "source file", + "start": 0, + "end": 198 + } + ], + "modes": [ + "callees", + "callers", + "callstack", + "definition", + "describe", + "freevars", + "implements", + "pointsto", + "referrers" + ], + "srcdir": "testdata/src", + "importpath": "what-json" + } +} diff --git a/cmd/guru/testdata/src/what/main.go b/cmd/guru/testdata/src/what/main.go new file mode 100644 index 00000000..9e6a8b92 --- /dev/null +++ b/cmd/guru/testdata/src/what/main.go @@ -0,0 +1,11 @@ +package main // @what pkgdecl "main" + +// Tests of 'what' queries. +// See go.tools/guru/guru_test.go for explanation. +// See what.golden for expected query results. + +func main() { + f() // @what call "f" + var ch chan int // @what var "var" + <-ch // @what recv "ch" +} diff --git a/cmd/guru/testdata/src/what/main.golden b/cmd/guru/testdata/src/what/main.golden new file mode 100644 index 00000000..56b97dde --- /dev/null +++ b/cmd/guru/testdata/src/what/main.golden @@ -0,0 +1,39 @@ +-------- @what pkgdecl -------- +identifier +source file +modes: [definition describe freevars implements pointsto referrers] +srcdir: testdata/src +import path: what + +-------- @what call -------- +identifier +function call (or conversion) +expression statement +block +function declaration +source file +modes: [callees callers callstack definition describe freevars implements pointsto referrers] +srcdir: testdata/src +import path: what + +-------- @what var -------- +variable declaration +variable declaration statement +block +function declaration +source file +modes: [callers callstack describe freevars pointsto] +srcdir: testdata/src +import path: what + +-------- @what recv -------- +identifier +unary <- operation +expression statement +block +function declaration +source file +modes: [callers callstack definition describe freevars implements peers pointsto referrers] +srcdir: testdata/src +import path: what + diff --git a/cmd/guru/testdata/src/whicherrs/main.go b/cmd/guru/testdata/src/whicherrs/main.go new file mode 100644 index 00000000..27fe6b56 --- /dev/null +++ b/cmd/guru/testdata/src/whicherrs/main.go @@ -0,0 +1,27 @@ +package main + +type errType string + +const constErr errType = "blah" + +func (et errType) Error() string { + return string(et) +} + +var errVar error = errType("foo") + +func genErr(i int) error { + switch i { + case 0: + return constErr + case 1: + return errVar + default: + return nil + } +} + +func main() { + err := genErr(0) // @whicherrs localerrs "err" + _ = err +} diff --git a/cmd/guru/testdata/src/whicherrs/main.golden b/cmd/guru/testdata/src/whicherrs/main.golden new file mode 100644 index 00000000..1118e0a8 --- /dev/null +++ b/cmd/guru/testdata/src/whicherrs/main.golden @@ -0,0 +1,8 @@ +-------- @whicherrs localerrs -------- +this error may point to these globals: + errVar +this error may contain these constants: + constErr +this error may contain these dynamic types: + errType + diff --git a/cmd/guru/what.go b/cmd/guru/what.go new file mode 100644 index 00000000..1d6ddee8 --- /dev/null +++ b/cmd/guru/what.go @@ -0,0 +1,210 @@ +// Copyright 2013 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 main + +import ( + "fmt" + "go/ast" + "go/build" + "go/token" + "os" + "path/filepath" + "sort" + "strings" + + "golang.org/x/tools/cmd/guru/serial" + "golang.org/x/tools/go/ast/astutil" +) + +// what reports all the information about the query selection that can be +// obtained from parsing only its containing source file. +// It is intended to be a very low-latency query callable from GUI +// tools, e.g. to populate a menu of options of slower queries about +// the selected location. +// +func what(q *Query) error { + qpos, err := fastQueryPos(q.Pos) + if err != nil { + return err + } + q.Fset = qpos.fset + + // (ignore errors) + srcdir, importPath, _ := guessImportPath(q.Fset.File(qpos.start).Name(), q.Build) + + // Determine which query modes are applicable to the selection. + enable := map[string]bool{ + "describe": true, // any syntax; always enabled + } + + if qpos.end > qpos.start { + enable["freevars"] = true // nonempty selection? + } + + for _, n := range qpos.path { + switch n := n.(type) { + case *ast.Ident: + enable["definition"] = true + enable["referrers"] = true + enable["implements"] = true + case *ast.CallExpr: + enable["callees"] = true + case *ast.FuncDecl: + enable["callers"] = true + enable["callstack"] = true + case *ast.SendStmt: + enable["peers"] = true + case *ast.UnaryExpr: + if n.Op == token.ARROW { + enable["peers"] = true + } + } + + // For implements, we approximate findInterestingNode. + if _, ok := enable["implements"]; !ok { + switch n.(type) { + case *ast.ArrayType, + *ast.StructType, + *ast.FuncType, + *ast.InterfaceType, + *ast.MapType, + *ast.ChanType: + enable["implements"] = true + } + } + + // For pointsto, we approximate findInterestingNode. + if _, ok := enable["pointsto"]; !ok { + switch n.(type) { + case ast.Stmt, + *ast.ArrayType, + *ast.StructType, + *ast.FuncType, + *ast.InterfaceType, + *ast.MapType, + *ast.ChanType: + enable["pointsto"] = false // not an expr + + case ast.Expr, ast.Decl, *ast.ValueSpec: + enable["pointsto"] = true // an expr, maybe + + default: + // Comment, Field, KeyValueExpr, etc: ascend. + } + } + } + + // If we don't have an exact selection, disable modes that need one. + if !qpos.exact { + enable["callees"] = false + enable["pointsto"] = false + enable["whicherrs"] = false + enable["describe"] = false + } + + var modes []string + for mode := range enable { + modes = append(modes, mode) + } + sort.Strings(modes) + + q.result = &whatResult{ + path: qpos.path, + srcdir: srcdir, + importPath: importPath, + modes: modes, + } + return nil +} + +// guessImportPath finds the package containing filename, and returns +// its source directory (an element of $GOPATH) and its import path +// relative to it. +// +// TODO(adonovan): what about _test.go files that are not part of the +// package? +// +func guessImportPath(filename string, buildContext *build.Context) (srcdir, importPath string, err error) { + absFile, err := filepath.Abs(filename) + if err != nil { + err = fmt.Errorf("can't form absolute path of %s", filename) + return + } + absFileDir := segments(filepath.Dir(absFile)) + + // Find the innermost directory in $GOPATH that encloses filename. + minD := 1024 + for _, gopathDir := range buildContext.SrcDirs() { + absDir, err := filepath.Abs(gopathDir) + if err != nil { + continue // e.g. non-existent dir on $GOPATH + } + d := prefixLen(segments(absDir), absFileDir) + // If there are multiple matches, + // prefer the innermost enclosing directory + // (smallest d). + if d >= 0 && d < minD { + minD = d + srcdir = gopathDir + importPath = strings.Join(absFileDir[len(absFileDir)-minD:], string(os.PathSeparator)) + } + } + if srcdir == "" { + err = fmt.Errorf("directory %s is not beneath any of these GOROOT/GOPATH directories: %s", + filepath.Dir(absFile), strings.Join(buildContext.SrcDirs(), ", ")) + } + return +} + +func segments(path string) []string { + return strings.Split(path, string(os.PathSeparator)) +} + +// prefixLen returns the length of the remainder of y if x is a prefix +// of y, a negative number otherwise. +func prefixLen(x, y []string) int { + d := len(y) - len(x) + if d >= 0 { + for i := range x { + if y[i] != x[i] { + return -1 // not a prefix + } + } + } + return d +} + +type whatResult struct { + path []ast.Node + modes []string + srcdir string + importPath string +} + +func (r *whatResult) display(printf printfFunc) { + for _, n := range r.path { + printf(n, "%s", astutil.NodeDescription(n)) + } + printf(nil, "modes: %s", r.modes) + printf(nil, "srcdir: %s", r.srcdir) + printf(nil, "import path: %s", r.importPath) +} + +func (r *whatResult) toSerial(res *serial.Result, fset *token.FileSet) { + var enclosing []serial.SyntaxNode + for _, n := range r.path { + enclosing = append(enclosing, serial.SyntaxNode{ + Description: astutil.NodeDescription(n), + Start: fset.Position(n.Pos()).Offset, + End: fset.Position(n.End()).Offset, + }) + } + res.What = &serial.What{ + Modes: r.modes, + SrcDir: r.srcdir, + ImportPath: r.importPath, + Enclosing: enclosing, + } +} diff --git a/cmd/guru/whicherrs.go b/cmd/guru/whicherrs.go new file mode 100644 index 00000000..1baf2a47 --- /dev/null +++ b/cmd/guru/whicherrs.go @@ -0,0 +1,326 @@ +// 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 main + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "sort" + + "golang.org/x/tools/cmd/guru/serial" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +var builtinErrorType = types.Universe.Lookup("error").Type() + +// whicherrs takes an position to an error and tries to find all types, constants +// and global value which a given error can point to and which can be checked from the +// scope where the error lives. +// In short, it returns a list of things that can be checked against in order to handle +// an error properly. +// +// TODO(dmorsing): figure out if fields in errors like *os.PathError.Err +// can be queried recursively somehow. +func whicherrs(q *Query) error { + lconf := loader.Config{Build: q.Build} + + if err := setPTAScope(&lconf, q.Scope); err != nil { + return err + } + + // Load/parse/type-check the program. + lprog, err := lconf.Load() + if err != nil { + return err + } + q.Fset = lprog.Fset + + qpos, err := parseQueryPos(lprog, q.Pos, true) // needs exact pos + if err != nil { + return err + } + + prog := ssautil.CreateProgram(lprog, ssa.GlobalDebug) + + ptaConfig, err := setupPTA(prog, lprog, q.PTALog, q.Reflection) + if err != nil { + return err + } + + path, action := findInterestingNode(qpos.info, qpos.path) + if action != actionExpr { + return fmt.Errorf("whicherrs wants an expression; got %s", + astutil.NodeDescription(qpos.path[0])) + } + var expr ast.Expr + var obj types.Object + switch n := path[0].(type) { + case *ast.ValueSpec: + // ambiguous ValueSpec containing multiple names + return fmt.Errorf("multiple value specification") + case *ast.Ident: + obj = qpos.info.ObjectOf(n) + expr = n + case ast.Expr: + expr = n + default: + return fmt.Errorf("unexpected AST for expr: %T", n) + } + + typ := qpos.info.TypeOf(expr) + if !types.Identical(typ, builtinErrorType) { + return fmt.Errorf("selection is not an expression of type 'error'") + } + // Determine the ssa.Value for the expression. + var value ssa.Value + if obj != nil { + // def/ref of func/var object + value, _, err = ssaValueForIdent(prog, qpos.info, obj, path) + } else { + value, _, err = ssaValueForExpr(prog, qpos.info, path) + } + if err != nil { + return err // e.g. trivially dead code + } + + // Defer SSA construction till after errors are reported. + prog.Build() + + globals := findVisibleErrs(prog, qpos) + constants := findVisibleConsts(prog, qpos) + + res := &whicherrsResult{ + qpos: qpos, + errpos: expr.Pos(), + } + + // TODO(adonovan): the following code is heavily duplicated + // w.r.t. "pointsto". Refactor? + + // Find the instruction which initialized the + // global error. If more than one instruction has stored to the global + // remove the global from the set of values that we want to query. + allFuncs := ssautil.AllFunctions(prog) + for fn := range allFuncs { + for _, b := range fn.Blocks { + for _, instr := range b.Instrs { + store, ok := instr.(*ssa.Store) + if !ok { + continue + } + gval, ok := store.Addr.(*ssa.Global) + if !ok { + continue + } + gbl, ok := globals[gval] + if !ok { + continue + } + // we already found a store to this global + // The normal error define is just one store in the init + // so we just remove this global from the set we want to query + if gbl != nil { + delete(globals, gval) + } + globals[gval] = store.Val + } + } + } + + ptaConfig.AddQuery(value) + for _, v := range globals { + ptaConfig.AddQuery(v) + } + + ptares := ptrAnalysis(ptaConfig) + valueptr := ptares.Queries[value] + for g, v := range globals { + ptr, ok := ptares.Queries[v] + if !ok { + continue + } + if !ptr.MayAlias(valueptr) { + continue + } + res.globals = append(res.globals, g) + } + pts := valueptr.PointsTo() + dedup := make(map[*ssa.NamedConst]bool) + for _, label := range pts.Labels() { + // These values are either MakeInterfaces or reflect + // generated interfaces. For the purposes of this + // analysis, we don't care about reflect generated ones + makeiface, ok := label.Value().(*ssa.MakeInterface) + if !ok { + continue + } + constval, ok := makeiface.X.(*ssa.Const) + if !ok { + continue + } + c := constants[*constval] + if c != nil && !dedup[c] { + dedup[c] = true + res.consts = append(res.consts, c) + } + } + concs := pts.DynamicTypes() + concs.Iterate(func(conc types.Type, _ interface{}) { + // go/types is a bit annoying here. + // We want to find all the types that we can + // typeswitch or assert to. This means finding out + // if the type pointed to can be seen by us. + // + // For the purposes of this analysis, the type is always + // either a Named type or a pointer to one. + // There are cases where error can be implemented + // by unnamed types, but in that case, we can't assert to + // it, so we don't care about it for this analysis. + var name *types.TypeName + switch t := conc.(type) { + case *types.Pointer: + named, ok := t.Elem().(*types.Named) + if !ok { + return + } + name = named.Obj() + case *types.Named: + name = t.Obj() + default: + return + } + if !isAccessibleFrom(name, qpos.info.Pkg) { + return + } + res.types = append(res.types, &errorType{conc, name}) + }) + sort.Sort(membersByPosAndString(res.globals)) + sort.Sort(membersByPosAndString(res.consts)) + sort.Sort(sorterrorType(res.types)) + + q.result = res + return nil +} + +// findVisibleErrs returns a mapping from each package-level variable of type "error" to nil. +func findVisibleErrs(prog *ssa.Program, qpos *queryPos) map[*ssa.Global]ssa.Value { + globals := make(map[*ssa.Global]ssa.Value) + for _, pkg := range prog.AllPackages() { + for _, mem := range pkg.Members { + gbl, ok := mem.(*ssa.Global) + if !ok { + continue + } + gbltype := gbl.Type() + // globals are always pointers + if !types.Identical(deref(gbltype), builtinErrorType) { + continue + } + if !isAccessibleFrom(gbl.Object(), qpos.info.Pkg) { + continue + } + globals[gbl] = nil + } + } + return globals +} + +// findVisibleConsts returns a mapping from each package-level constant assignable to type "error", to nil. +func findVisibleConsts(prog *ssa.Program, qpos *queryPos) map[ssa.Const]*ssa.NamedConst { + constants := make(map[ssa.Const]*ssa.NamedConst) + for _, pkg := range prog.AllPackages() { + for _, mem := range pkg.Members { + obj, ok := mem.(*ssa.NamedConst) + if !ok { + continue + } + consttype := obj.Type() + if !types.AssignableTo(consttype, builtinErrorType) { + continue + } + if !isAccessibleFrom(obj.Object(), qpos.info.Pkg) { + continue + } + constants[*obj.Value] = obj + } + } + + return constants +} + +type membersByPosAndString []ssa.Member + +func (a membersByPosAndString) Len() int { return len(a) } +func (a membersByPosAndString) Less(i, j int) bool { + cmp := a[i].Pos() - a[j].Pos() + return cmp < 0 || cmp == 0 && a[i].String() < a[j].String() +} +func (a membersByPosAndString) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type sorterrorType []*errorType + +func (a sorterrorType) Len() int { return len(a) } +func (a sorterrorType) Less(i, j int) bool { + cmp := a[i].obj.Pos() - a[j].obj.Pos() + return cmp < 0 || cmp == 0 && a[i].typ.String() < a[j].typ.String() +} +func (a sorterrorType) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type errorType struct { + typ types.Type // concrete type N or *N that implements error + obj *types.TypeName // the named type N +} + +type whicherrsResult struct { + qpos *queryPos + errpos token.Pos + globals []ssa.Member + consts []ssa.Member + types []*errorType +} + +func (r *whicherrsResult) display(printf printfFunc) { + if len(r.globals) > 0 { + printf(r.qpos, "this error may point to these globals:") + for _, g := range r.globals { + printf(g.Pos(), "\t%s", g.RelString(r.qpos.info.Pkg)) + } + } + if len(r.consts) > 0 { + printf(r.qpos, "this error may contain these constants:") + for _, c := range r.consts { + printf(c.Pos(), "\t%s", c.RelString(r.qpos.info.Pkg)) + } + } + if len(r.types) > 0 { + printf(r.qpos, "this error may contain these dynamic types:") + for _, t := range r.types { + printf(t.obj.Pos(), "\t%s", r.qpos.typeString(t.typ)) + } + } +} + +func (r *whicherrsResult) toSerial(res *serial.Result, fset *token.FileSet) { + we := &serial.WhichErrs{} + we.ErrPos = fset.Position(r.errpos).String() + for _, g := range r.globals { + we.Globals = append(we.Globals, fset.Position(g.Pos()).String()) + } + for _, c := range r.consts { + we.Constants = append(we.Constants, fset.Position(c.Pos()).String()) + } + for _, t := range r.types { + var et serial.WhichErrsType + et.Type = r.qpos.typeString(t.typ) + et.Position = fset.Position(t.obj.Pos()).String() + we.Types = append(we.Types, et) + } + res.WhichErrs = we +}