435c0d9fc8
This PR switches the Nomad repository from using govendor to Go modules for managing dependencies. Aspects of the Nomad workflow remain pretty much the same. The usual Makefile targets should continue to work as they always did. The API submodule simply defers to the parent Nomad version on the repository, keeping the semantics of API versioning that currently exists.
1979 lines
54 KiB
Go
1979 lines
54 KiB
Go
package unused
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"go/types"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"honnef.co/go/tools/code"
|
|
"honnef.co/go/tools/go/types/typeutil"
|
|
"honnef.co/go/tools/internal/passes/buildir"
|
|
"honnef.co/go/tools/ir"
|
|
"honnef.co/go/tools/lint"
|
|
)
|
|
|
|
// The graph we construct omits nodes along a path that do not
|
|
// contribute any new information to the solution. For example, the
|
|
// full graph for a function with a receiver would be Func ->
|
|
// Signature -> Var -> Type. However, since signatures cannot be
|
|
// unused, and receivers are always considered used, we can compact
|
|
// the graph down to Func -> Type. This makes the graph smaller, but
|
|
// harder to debug.
|
|
|
|
// TODO(dh): conversions between structs mark fields as used, but the
|
|
// conversion itself isn't part of that subgraph. even if the function
|
|
// containing the conversion is unused, the fields will be marked as
|
|
// used.
|
|
|
|
// TODO(dh): we cannot observe function calls in assembly files.
|
|
|
|
/*
|
|
|
|
- packages use:
|
|
- (1.1) exported named types (unless in package main)
|
|
- (1.2) exported functions (unless in package main)
|
|
- (1.3) exported variables (unless in package main)
|
|
- (1.4) exported constants (unless in package main)
|
|
- (1.5) init functions
|
|
- (1.6) functions exported to cgo
|
|
- (1.7) the main function iff in the main package
|
|
- (1.8) symbols linked via go:linkname
|
|
|
|
- named types use:
|
|
- (2.1) exported methods
|
|
- (2.2) the type they're based on
|
|
- (2.3) all their aliases. we can't easily track uses of aliases
|
|
because go/types turns them into uses of the aliased types. assume
|
|
that if a type is used, so are all of its aliases.
|
|
- (2.4) the pointer type. this aids with eagerly implementing
|
|
interfaces. if a method that implements an interface is defined on
|
|
a pointer receiver, and the pointer type is never used, but the
|
|
named type is, then we still want to mark the method as used.
|
|
|
|
- variables and constants use:
|
|
- their types
|
|
|
|
- functions use:
|
|
- (4.1) all their arguments, return parameters and receivers
|
|
- (4.2) anonymous functions defined beneath them
|
|
- (4.3) closures and bound methods.
|
|
this implements a simplified model where a function is used merely by being referenced, even if it is never called.
|
|
that way we don't have to keep track of closures escaping functions.
|
|
- (4.4) functions they return. we assume that someone else will call the returned function
|
|
- (4.5) functions/interface methods they call
|
|
- types they instantiate or convert to
|
|
- (4.7) fields they access
|
|
- (4.8) types of all instructions
|
|
- (4.9) package-level variables they assign to iff in tests (sinks for benchmarks)
|
|
|
|
- conversions use:
|
|
- (5.1) when converting between two equivalent structs, the fields in
|
|
either struct use each other. the fields are relevant for the
|
|
conversion, but only if the fields are also accessed outside the
|
|
conversion.
|
|
- (5.2) when converting to or from unsafe.Pointer, mark all fields as used.
|
|
|
|
- structs use:
|
|
- (6.1) fields of type NoCopy sentinel
|
|
- (6.2) exported fields
|
|
- (6.3) embedded fields that help implement interfaces (either fully implements it, or contributes required methods) (recursively)
|
|
- (6.4) embedded fields that have exported methods (recursively)
|
|
- (6.5) embedded structs that have exported fields (recursively)
|
|
|
|
- (7.1) field accesses use fields
|
|
- (7.2) fields use their types
|
|
|
|
- (8.0) How we handle interfaces:
|
|
- (8.1) We do not technically care about interfaces that only consist of
|
|
exported methods. Exported methods on concrete types are always
|
|
marked as used.
|
|
- Any concrete type implements all known interfaces. Even if it isn't
|
|
assigned to any interfaces in our code, the user may receive a value
|
|
of the type and expect to pass it back to us through an interface.
|
|
|
|
Concrete types use their methods that implement interfaces. If the
|
|
type is used, it uses those methods. Otherwise, it doesn't. This
|
|
way, types aren't incorrectly marked reachable through the edge
|
|
from method to type.
|
|
|
|
- (8.3) All interface methods are marked as used, even if they never get
|
|
called. This is to accommodate sum types (unexported interface
|
|
method that must exist but never gets called.)
|
|
|
|
- (8.4) All embedded interfaces are marked as used. This is an
|
|
extension of 8.3, but we have to explicitly track embedded
|
|
interfaces because in a chain C->B->A, B wouldn't be marked as
|
|
used by 8.3 just because it contributes A's methods to C.
|
|
|
|
- Inherent uses:
|
|
- thunks and other generated wrappers call the real function
|
|
- (9.2) variables use their types
|
|
- (9.3) types use their underlying and element types
|
|
- (9.4) conversions use the type they convert to
|
|
- (9.5) instructions use their operands
|
|
- (9.6) instructions use their operands' types
|
|
- (9.7) variable _reads_ use variables, writes do not, except in tests
|
|
- (9.8) runtime functions that may be called from user code via the compiler
|
|
|
|
|
|
- const groups:
|
|
(10.1) if one constant out of a block of constants is used, mark all
|
|
of them used. a lot of the time, unused constants exist for the sake
|
|
of completeness. See also
|
|
https://github.com/dominikh/go-tools/issues/365
|
|
|
|
|
|
- (11.1) anonymous struct types use all their fields. we cannot
|
|
deduplicate struct types, as that leads to order-dependent
|
|
reportings. we can't not deduplicate struct types while still
|
|
tracking fields, because then each instance of the unnamed type in
|
|
the data flow chain will get its own fields, causing false
|
|
positives. Thus, we only accurately track fields of named struct
|
|
types, and assume that unnamed struct types use all their fields.
|
|
|
|
|
|
- Differences in whole program mode:
|
|
- (e2) types aim to implement all exported interfaces from all packages
|
|
- (e3) exported identifiers aren't automatically used. for fields and
|
|
methods this poses extra issues due to reflection. We assume
|
|
that all exported fields are used. We also maintain a list of
|
|
known reflection-based method callers.
|
|
|
|
*/
|
|
|
|
func assert(b bool) {
|
|
if !b {
|
|
panic("failed assertion")
|
|
}
|
|
}
|
|
|
|
func typString(obj types.Object) string {
|
|
switch obj := obj.(type) {
|
|
case *types.Func:
|
|
return "func"
|
|
case *types.Var:
|
|
if obj.IsField() {
|
|
return "field"
|
|
}
|
|
return "var"
|
|
case *types.Const:
|
|
return "const"
|
|
case *types.TypeName:
|
|
return "type"
|
|
default:
|
|
return "identifier"
|
|
}
|
|
}
|
|
|
|
// /usr/lib/go/src/runtime/proc.go:433:6: func badmorestackg0 is unused (U1000)
|
|
|
|
// Functions defined in the Go runtime that may be called through
|
|
// compiler magic or via assembly.
|
|
var runtimeFuncs = map[string]bool{
|
|
// The first part of the list is copied from
|
|
// cmd/compile/internal/gc/builtin.go, var runtimeDecls
|
|
"newobject": true,
|
|
"panicindex": true,
|
|
"panicslice": true,
|
|
"panicdivide": true,
|
|
"panicmakeslicelen": true,
|
|
"throwinit": true,
|
|
"panicwrap": true,
|
|
"gopanic": true,
|
|
"gorecover": true,
|
|
"goschedguarded": true,
|
|
"printbool": true,
|
|
"printfloat": true,
|
|
"printint": true,
|
|
"printhex": true,
|
|
"printuint": true,
|
|
"printcomplex": true,
|
|
"printstring": true,
|
|
"printpointer": true,
|
|
"printiface": true,
|
|
"printeface": true,
|
|
"printslice": true,
|
|
"printnl": true,
|
|
"printsp": true,
|
|
"printlock": true,
|
|
"printunlock": true,
|
|
"concatstring2": true,
|
|
"concatstring3": true,
|
|
"concatstring4": true,
|
|
"concatstring5": true,
|
|
"concatstrings": true,
|
|
"cmpstring": true,
|
|
"intstring": true,
|
|
"slicebytetostring": true,
|
|
"slicebytetostringtmp": true,
|
|
"slicerunetostring": true,
|
|
"stringtoslicebyte": true,
|
|
"stringtoslicerune": true,
|
|
"slicecopy": true,
|
|
"slicestringcopy": true,
|
|
"decoderune": true,
|
|
"countrunes": true,
|
|
"convI2I": true,
|
|
"convT16": true,
|
|
"convT32": true,
|
|
"convT64": true,
|
|
"convTstring": true,
|
|
"convTslice": true,
|
|
"convT2E": true,
|
|
"convT2Enoptr": true,
|
|
"convT2I": true,
|
|
"convT2Inoptr": true,
|
|
"assertE2I": true,
|
|
"assertE2I2": true,
|
|
"assertI2I": true,
|
|
"assertI2I2": true,
|
|
"panicdottypeE": true,
|
|
"panicdottypeI": true,
|
|
"panicnildottype": true,
|
|
"ifaceeq": true,
|
|
"efaceeq": true,
|
|
"fastrand": true,
|
|
"makemap64": true,
|
|
"makemap": true,
|
|
"makemap_small": true,
|
|
"mapaccess1": true,
|
|
"mapaccess1_fast32": true,
|
|
"mapaccess1_fast64": true,
|
|
"mapaccess1_faststr": true,
|
|
"mapaccess1_fat": true,
|
|
"mapaccess2": true,
|
|
"mapaccess2_fast32": true,
|
|
"mapaccess2_fast64": true,
|
|
"mapaccess2_faststr": true,
|
|
"mapaccess2_fat": true,
|
|
"mapassign": true,
|
|
"mapassign_fast32": true,
|
|
"mapassign_fast32ptr": true,
|
|
"mapassign_fast64": true,
|
|
"mapassign_fast64ptr": true,
|
|
"mapassign_faststr": true,
|
|
"mapiterinit": true,
|
|
"mapdelete": true,
|
|
"mapdelete_fast32": true,
|
|
"mapdelete_fast64": true,
|
|
"mapdelete_faststr": true,
|
|
"mapiternext": true,
|
|
"mapclear": true,
|
|
"makechan64": true,
|
|
"makechan": true,
|
|
"chanrecv1": true,
|
|
"chanrecv2": true,
|
|
"chansend1": true,
|
|
"closechan": true,
|
|
"writeBarrier": true,
|
|
"typedmemmove": true,
|
|
"typedmemclr": true,
|
|
"typedslicecopy": true,
|
|
"selectnbsend": true,
|
|
"selectnbrecv": true,
|
|
"selectnbrecv2": true,
|
|
"selectsetpc": true,
|
|
"selectgo": true,
|
|
"block": true,
|
|
"makeslice": true,
|
|
"makeslice64": true,
|
|
"growslice": true,
|
|
"memmove": true,
|
|
"memclrNoHeapPointers": true,
|
|
"memclrHasPointers": true,
|
|
"memequal": true,
|
|
"memequal8": true,
|
|
"memequal16": true,
|
|
"memequal32": true,
|
|
"memequal64": true,
|
|
"memequal128": true,
|
|
"int64div": true,
|
|
"uint64div": true,
|
|
"int64mod": true,
|
|
"uint64mod": true,
|
|
"float64toint64": true,
|
|
"float64touint64": true,
|
|
"float64touint32": true,
|
|
"int64tofloat64": true,
|
|
"uint64tofloat64": true,
|
|
"uint32tofloat64": true,
|
|
"complex128div": true,
|
|
"racefuncenter": true,
|
|
"racefuncenterfp": true,
|
|
"racefuncexit": true,
|
|
"raceread": true,
|
|
"racewrite": true,
|
|
"racereadrange": true,
|
|
"racewriterange": true,
|
|
"msanread": true,
|
|
"msanwrite": true,
|
|
"x86HasPOPCNT": true,
|
|
"x86HasSSE41": true,
|
|
"arm64HasATOMICS": true,
|
|
|
|
// The second part of the list is extracted from assembly code in
|
|
// the standard library, with the exception of the runtime package itself
|
|
"abort": true,
|
|
"aeshashbody": true,
|
|
"args": true,
|
|
"asminit": true,
|
|
"badctxt": true,
|
|
"badmcall2": true,
|
|
"badmcall": true,
|
|
"badmorestackg0": true,
|
|
"badmorestackgsignal": true,
|
|
"badsignal2": true,
|
|
"callbackasm1": true,
|
|
"callCfunction": true,
|
|
"cgocallback_gofunc": true,
|
|
"cgocallbackg": true,
|
|
"checkgoarm": true,
|
|
"check": true,
|
|
"debugCallCheck": true,
|
|
"debugCallWrap": true,
|
|
"emptyfunc": true,
|
|
"entersyscall": true,
|
|
"exit": true,
|
|
"exits": true,
|
|
"exitsyscall": true,
|
|
"externalthreadhandler": true,
|
|
"findnull": true,
|
|
"goexit1": true,
|
|
"gostring": true,
|
|
"i386_set_ldt": true,
|
|
"_initcgo": true,
|
|
"init_thread_tls": true,
|
|
"ldt0setup": true,
|
|
"libpreinit": true,
|
|
"load_g": true,
|
|
"morestack": true,
|
|
"mstart": true,
|
|
"nacl_sysinfo": true,
|
|
"nanotimeQPC": true,
|
|
"nanotime": true,
|
|
"newosproc0": true,
|
|
"newproc": true,
|
|
"newstack": true,
|
|
"noted": true,
|
|
"nowQPC": true,
|
|
"osinit": true,
|
|
"printf": true,
|
|
"racecallback": true,
|
|
"reflectcallmove": true,
|
|
"reginit": true,
|
|
"rt0_go": true,
|
|
"save_g": true,
|
|
"schedinit": true,
|
|
"setldt": true,
|
|
"settls": true,
|
|
"sighandler": true,
|
|
"sigprofNonGo": true,
|
|
"sigtrampgo": true,
|
|
"_sigtramp": true,
|
|
"sigtramp": true,
|
|
"stackcheck": true,
|
|
"syscall_chdir": true,
|
|
"syscall_chroot": true,
|
|
"syscall_close": true,
|
|
"syscall_dup2": true,
|
|
"syscall_execve": true,
|
|
"syscall_exit": true,
|
|
"syscall_fcntl": true,
|
|
"syscall_forkx": true,
|
|
"syscall_gethostname": true,
|
|
"syscall_getpid": true,
|
|
"syscall_ioctl": true,
|
|
"syscall_pipe": true,
|
|
"syscall_rawsyscall6": true,
|
|
"syscall_rawSyscall6": true,
|
|
"syscall_rawsyscall": true,
|
|
"syscall_RawSyscall": true,
|
|
"syscall_rawsysvicall6": true,
|
|
"syscall_setgid": true,
|
|
"syscall_setgroups": true,
|
|
"syscall_setpgid": true,
|
|
"syscall_setsid": true,
|
|
"syscall_setuid": true,
|
|
"syscall_syscall6": true,
|
|
"syscall_syscall": true,
|
|
"syscall_Syscall": true,
|
|
"syscall_sysvicall6": true,
|
|
"syscall_wait4": true,
|
|
"syscall_write": true,
|
|
"traceback": true,
|
|
"tstart": true,
|
|
"usplitR0": true,
|
|
"wbBufFlush": true,
|
|
"write": true,
|
|
}
|
|
|
|
type pkg struct {
|
|
Fset *token.FileSet
|
|
Files []*ast.File
|
|
Pkg *types.Package
|
|
TypesInfo *types.Info
|
|
TypesSizes types.Sizes
|
|
IR *ir.Package
|
|
SrcFuncs []*ir.Function
|
|
}
|
|
|
|
type Checker struct {
|
|
WholeProgram bool
|
|
Debug io.Writer
|
|
|
|
mu sync.Mutex
|
|
initialPackages map[*types.Package]struct{}
|
|
allPackages map[*types.Package]struct{}
|
|
graph *Graph
|
|
}
|
|
|
|
func NewChecker(wholeProgram bool) *Checker {
|
|
return &Checker{
|
|
initialPackages: map[*types.Package]struct{}{},
|
|
allPackages: map[*types.Package]struct{}{},
|
|
WholeProgram: wholeProgram,
|
|
}
|
|
}
|
|
|
|
func (c *Checker) Analyzer() *analysis.Analyzer {
|
|
name := "U1000"
|
|
if c.WholeProgram {
|
|
name = "U1001"
|
|
}
|
|
return &analysis.Analyzer{
|
|
Name: name,
|
|
Doc: "Unused code",
|
|
Run: c.Run,
|
|
Requires: []*analysis.Analyzer{buildir.Analyzer},
|
|
}
|
|
}
|
|
|
|
func (c *Checker) Run(pass *analysis.Pass) (interface{}, error) {
|
|
c.mu.Lock()
|
|
if c.graph == nil {
|
|
c.graph = NewGraph()
|
|
c.graph.wholeProgram = c.WholeProgram
|
|
c.graph.fset = pass.Fset
|
|
}
|
|
|
|
var visit func(pkg *types.Package)
|
|
visit = func(pkg *types.Package) {
|
|
if _, ok := c.allPackages[pkg]; ok {
|
|
return
|
|
}
|
|
c.allPackages[pkg] = struct{}{}
|
|
for _, imp := range pkg.Imports() {
|
|
visit(imp)
|
|
}
|
|
}
|
|
visit(pass.Pkg)
|
|
|
|
c.initialPackages[pass.Pkg] = struct{}{}
|
|
c.mu.Unlock()
|
|
|
|
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR)
|
|
pkg := &pkg{
|
|
Fset: pass.Fset,
|
|
Files: pass.Files,
|
|
Pkg: pass.Pkg,
|
|
TypesInfo: pass.TypesInfo,
|
|
TypesSizes: pass.TypesSizes,
|
|
IR: irpkg.Pkg,
|
|
SrcFuncs: irpkg.SrcFuncs,
|
|
}
|
|
|
|
c.processPkg(c.graph, pkg)
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (c *Checker) ProblemObject(fset *token.FileSet, obj types.Object) lint.Problem {
|
|
name := obj.Name()
|
|
if sig, ok := obj.Type().(*types.Signature); ok && sig.Recv() != nil {
|
|
switch sig.Recv().Type().(type) {
|
|
case *types.Named, *types.Pointer:
|
|
typ := types.TypeString(sig.Recv().Type(), func(*types.Package) string { return "" })
|
|
if len(typ) > 0 && typ[0] == '*' {
|
|
name = fmt.Sprintf("(%s).%s", typ, obj.Name())
|
|
} else if len(typ) > 0 {
|
|
name = fmt.Sprintf("%s.%s", typ, obj.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
checkName := "U1000"
|
|
if c.WholeProgram {
|
|
checkName = "U1001"
|
|
}
|
|
return lint.Problem{
|
|
Pos: lint.DisplayPosition(fset, obj.Pos()),
|
|
Message: fmt.Sprintf("%s %s is unused", typString(obj), name),
|
|
Check: checkName,
|
|
}
|
|
}
|
|
|
|
func (c *Checker) Result() []types.Object {
|
|
out := c.results()
|
|
|
|
out2 := make([]types.Object, 0, len(out))
|
|
for _, v := range out {
|
|
if _, ok := c.initialPackages[v.Pkg()]; !ok {
|
|
continue
|
|
}
|
|
out2 = append(out2, v)
|
|
}
|
|
|
|
return out2
|
|
}
|
|
|
|
func (c *Checker) debugf(f string, v ...interface{}) {
|
|
if c.Debug != nil {
|
|
fmt.Fprintf(c.Debug, f, v...)
|
|
}
|
|
}
|
|
|
|
func (graph *Graph) quieten(node *Node) {
|
|
if node.seen {
|
|
return
|
|
}
|
|
switch obj := node.obj.(type) {
|
|
case *types.Named:
|
|
for i := 0; i < obj.NumMethods(); i++ {
|
|
m := obj.Method(i)
|
|
if node, ok := graph.nodeMaybe(m); ok {
|
|
node.quiet = true
|
|
}
|
|
}
|
|
case *types.Struct:
|
|
for i := 0; i < obj.NumFields(); i++ {
|
|
if node, ok := graph.nodeMaybe(obj.Field(i)); ok {
|
|
node.quiet = true
|
|
}
|
|
}
|
|
case *types.Interface:
|
|
for i := 0; i < obj.NumExplicitMethods(); i++ {
|
|
m := obj.ExplicitMethod(i)
|
|
if node, ok := graph.nodeMaybe(m); ok {
|
|
node.quiet = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Checker) results() []types.Object {
|
|
if c.graph == nil {
|
|
// We never analyzed any packages
|
|
return nil
|
|
}
|
|
|
|
var out []types.Object
|
|
|
|
if c.WholeProgram {
|
|
var ifaces []*types.Interface
|
|
var notIfaces []types.Type
|
|
|
|
// implement as many interfaces as possible
|
|
c.graph.seenTypes.Iterate(func(t types.Type, _ interface{}) {
|
|
switch t := t.(type) {
|
|
case *types.Interface:
|
|
if t.NumMethods() > 0 {
|
|
ifaces = append(ifaces, t)
|
|
}
|
|
default:
|
|
if _, ok := t.Underlying().(*types.Interface); !ok {
|
|
notIfaces = append(notIfaces, t)
|
|
}
|
|
}
|
|
})
|
|
|
|
for pkg := range c.allPackages {
|
|
for _, iface := range interfacesFromExportData(pkg) {
|
|
if iface.NumMethods() > 0 {
|
|
ifaces = append(ifaces, iface)
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx := &context{
|
|
g: c.graph,
|
|
seenTypes: &c.graph.seenTypes,
|
|
}
|
|
// (8.0) handle interfaces
|
|
// (e2) types aim to implement all exported interfaces from all packages
|
|
for _, t := range notIfaces {
|
|
// OPT(dh): it is unfortunate that we do not have access
|
|
// to a populated method set at this point.
|
|
ms := types.NewMethodSet(t)
|
|
for _, iface := range ifaces {
|
|
if sels, ok := c.graph.implements(t, iface, ms); ok {
|
|
for _, sel := range sels {
|
|
c.graph.useMethod(ctx, t, sel, t, edgeImplements)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if c.Debug != nil {
|
|
debugNode := func(node *Node) {
|
|
if node.obj == nil {
|
|
c.debugf("n%d [label=\"Root\"];\n", node.id)
|
|
} else {
|
|
c.debugf("n%d [label=%q];\n", node.id, fmt.Sprintf("(%T) %s", node.obj, node.obj))
|
|
}
|
|
for _, e := range node.used {
|
|
for i := edgeKind(1); i < 64; i++ {
|
|
if e.kind.is(1 << i) {
|
|
c.debugf("n%d -> n%d [label=%q];\n", node.id, e.node.id, edgeKind(1<<i))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
c.debugf("digraph{\n")
|
|
debugNode(c.graph.Root)
|
|
for _, v := range c.graph.Nodes {
|
|
debugNode(v)
|
|
}
|
|
c.graph.TypeNodes.Iterate(func(key types.Type, value interface{}) {
|
|
debugNode(value.(*Node))
|
|
})
|
|
|
|
c.debugf("}\n")
|
|
}
|
|
|
|
c.graph.color(c.graph.Root)
|
|
// if a node is unused, don't report any of the node's
|
|
// children as unused. for example, if a function is unused,
|
|
// don't flag its receiver. if a named type is unused, don't
|
|
// flag its methods.
|
|
|
|
for _, v := range c.graph.Nodes {
|
|
c.graph.quieten(v)
|
|
}
|
|
c.graph.TypeNodes.Iterate(func(_ types.Type, value interface{}) {
|
|
c.graph.quieten(value.(*Node))
|
|
})
|
|
|
|
report := func(node *Node) {
|
|
if node.seen {
|
|
return
|
|
}
|
|
if node.quiet {
|
|
c.debugf("n%d [color=purple];\n", node.id)
|
|
return
|
|
}
|
|
|
|
c.debugf("n%d [color=red];\n", node.id)
|
|
switch obj := node.obj.(type) {
|
|
case *types.Var:
|
|
// don't report unnamed variables (interface embedding)
|
|
if obj.Name() != "" || obj.IsField() {
|
|
out = append(out, obj)
|
|
}
|
|
return
|
|
case types.Object:
|
|
if obj.Name() != "_" {
|
|
out = append(out, obj)
|
|
}
|
|
return
|
|
}
|
|
c.debugf("n%d [color=gray];\n", node.id)
|
|
}
|
|
for _, v := range c.graph.Nodes {
|
|
report(v)
|
|
}
|
|
c.graph.TypeNodes.Iterate(func(_ types.Type, value interface{}) {
|
|
report(value.(*Node))
|
|
})
|
|
|
|
return out
|
|
}
|
|
|
|
func (c *Checker) processPkg(graph *Graph, pkg *pkg) {
|
|
if pkg.Pkg.Path() == "unsafe" {
|
|
return
|
|
}
|
|
graph.entry(pkg)
|
|
}
|
|
|
|
func objNodeKeyFor(fset *token.FileSet, obj types.Object) objNodeKey {
|
|
var kind objType
|
|
switch obj.(type) {
|
|
case *types.PkgName:
|
|
kind = otPkgName
|
|
case *types.Const:
|
|
kind = otConst
|
|
case *types.TypeName:
|
|
kind = otTypeName
|
|
case *types.Var:
|
|
kind = otVar
|
|
case *types.Func:
|
|
kind = otFunc
|
|
case *types.Label:
|
|
kind = otLabel
|
|
case *types.Builtin:
|
|
kind = otBuiltin
|
|
case *types.Nil:
|
|
kind = otNil
|
|
default:
|
|
panic(fmt.Sprintf("unreachable: %T", obj))
|
|
}
|
|
|
|
position := fset.PositionFor(obj.Pos(), false)
|
|
position.Column = 0
|
|
position.Offset = 0
|
|
return objNodeKey{
|
|
position: position,
|
|
kind: kind,
|
|
name: obj.Name(),
|
|
}
|
|
}
|
|
|
|
type objType uint8
|
|
|
|
const (
|
|
otPkgName objType = iota
|
|
otConst
|
|
otTypeName
|
|
otVar
|
|
otFunc
|
|
otLabel
|
|
otBuiltin
|
|
otNil
|
|
)
|
|
|
|
// An objNodeKey describes a types.Object node in the graph.
|
|
//
|
|
// Due to test variants we may end up with multiple instances of the
|
|
// same object, which is why we have to deduplicate based on their
|
|
// source position. And because export data lacks column information,
|
|
// we also have to incorporate the object's string representation in
|
|
// the key.
|
|
//
|
|
// Previously we used the object's full string representation
|
|
// (types.ObjectString), but that causes a significant amount of
|
|
// allocations. Currently we're using the object's type and name, in
|
|
// the hope that it is impossible for two objects to have the same
|
|
// type, name and file position.
|
|
type objNodeKey struct {
|
|
position token.Position
|
|
kind objType
|
|
name string
|
|
}
|
|
|
|
type Graph struct {
|
|
// accessed atomically
|
|
nodeOffset uint64
|
|
|
|
// Safe for concurrent use
|
|
fset *token.FileSet
|
|
Root *Node
|
|
seenTypes typeutil.Map
|
|
|
|
// read-only
|
|
wholeProgram bool
|
|
|
|
// need synchronisation
|
|
mu sync.Mutex
|
|
TypeNodes typeutil.Map
|
|
Nodes map[interface{}]*Node
|
|
objNodes map[objNodeKey]*Node
|
|
}
|
|
|
|
type context struct {
|
|
g *Graph
|
|
pkg *pkg
|
|
seenFns map[string]struct{}
|
|
seenTypes *typeutil.Map
|
|
nodeCounter uint64
|
|
}
|
|
|
|
func NewGraph() *Graph {
|
|
g := &Graph{
|
|
Nodes: map[interface{}]*Node{},
|
|
objNodes: map[objNodeKey]*Node{},
|
|
}
|
|
g.Root = g.newNode(&context{}, nil)
|
|
return g
|
|
}
|
|
|
|
func (g *Graph) color(root *Node) {
|
|
if root.seen {
|
|
return
|
|
}
|
|
root.seen = true
|
|
for _, e := range root.used {
|
|
g.color(e.node)
|
|
}
|
|
}
|
|
|
|
type ConstGroup struct {
|
|
// give the struct a size to get unique pointers
|
|
_ byte
|
|
}
|
|
|
|
func (ConstGroup) String() string { return "const group" }
|
|
|
|
type edge struct {
|
|
node *Node
|
|
kind edgeKind
|
|
}
|
|
|
|
type Node struct {
|
|
obj interface{}
|
|
id uint64
|
|
|
|
mu sync.Mutex
|
|
used []edge
|
|
|
|
// set during final graph walk if node is reachable
|
|
seen bool
|
|
// a parent node (e.g. the struct type containing a field) is
|
|
// already unused, don't report children
|
|
quiet bool
|
|
}
|
|
|
|
func (g *Graph) nodeMaybe(obj types.Object) (*Node, bool) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
if node, ok := g.Nodes[obj]; ok {
|
|
return node, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (g *Graph) node(ctx *context, obj interface{}) (node *Node, new bool) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
switch obj := obj.(type) {
|
|
case types.Type:
|
|
if v := g.TypeNodes.At(obj); v != nil {
|
|
return v.(*Node), false
|
|
}
|
|
node := g.newNode(ctx, obj)
|
|
g.TypeNodes.Set(obj, node)
|
|
return node, true
|
|
case types.Object:
|
|
if node, ok := g.Nodes[obj]; ok {
|
|
return node, false
|
|
}
|
|
|
|
key := objNodeKeyFor(g.fset, obj)
|
|
if onode, ok := g.objNodes[key]; ok {
|
|
return onode, false
|
|
}
|
|
|
|
node = g.newNode(ctx, obj)
|
|
g.Nodes[obj] = node
|
|
g.objNodes[key] = node
|
|
return node, true
|
|
default:
|
|
if node, ok := g.Nodes[obj]; ok {
|
|
return node, false
|
|
}
|
|
|
|
node = g.newNode(ctx, obj)
|
|
g.Nodes[obj] = node
|
|
return node, true
|
|
}
|
|
}
|
|
|
|
func (g *Graph) newNode(ctx *context, obj interface{}) *Node {
|
|
ctx.nodeCounter++
|
|
return &Node{
|
|
obj: obj,
|
|
id: ctx.nodeCounter,
|
|
}
|
|
}
|
|
|
|
func (n *Node) use(node *Node, kind edgeKind) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
assert(node != nil)
|
|
n.used = append(n.used, edge{node: node, kind: kind})
|
|
}
|
|
|
|
// isIrrelevant reports whether an object's presence in the graph is
|
|
// of any relevance. A lot of objects will never have outgoing edges,
|
|
// nor meaningful incoming ones. Examples are basic types and empty
|
|
// signatures, among many others.
|
|
//
|
|
// Dropping these objects should have no effect on correctness, but
|
|
// may improve performance. It also helps with debugging, as it
|
|
// greatly reduces the size of the graph.
|
|
func isIrrelevant(obj interface{}) bool {
|
|
if obj, ok := obj.(types.Object); ok {
|
|
switch obj := obj.(type) {
|
|
case *types.Var:
|
|
if obj.IsField() {
|
|
// We need to track package fields
|
|
return false
|
|
}
|
|
if obj.Pkg() != nil && obj.Parent() == obj.Pkg().Scope() {
|
|
// We need to track package-level variables
|
|
return false
|
|
}
|
|
return isIrrelevant(obj.Type())
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
if T, ok := obj.(types.Type); ok {
|
|
switch T := T.(type) {
|
|
case *types.Array:
|
|
return isIrrelevant(T.Elem())
|
|
case *types.Slice:
|
|
return isIrrelevant(T.Elem())
|
|
case *types.Basic:
|
|
return true
|
|
case *types.Tuple:
|
|
for i := 0; i < T.Len(); i++ {
|
|
if !isIrrelevant(T.At(i).Type()) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case *types.Signature:
|
|
if T.Recv() != nil {
|
|
return false
|
|
}
|
|
for i := 0; i < T.Params().Len(); i++ {
|
|
if !isIrrelevant(T.Params().At(i)) {
|
|
return false
|
|
}
|
|
}
|
|
for i := 0; i < T.Results().Len(); i++ {
|
|
if !isIrrelevant(T.Results().At(i)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case *types.Interface:
|
|
return T.NumMethods() == 0 && T.NumEmbeddeds() == 0
|
|
case *types.Pointer:
|
|
return isIrrelevant(T.Elem())
|
|
case *types.Map:
|
|
return isIrrelevant(T.Key()) && isIrrelevant(T.Elem())
|
|
case *types.Struct:
|
|
return T.NumFields() == 0
|
|
case *types.Chan:
|
|
return isIrrelevant(T.Elem())
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (ctx *context) see(obj interface{}) *Node {
|
|
if isIrrelevant(obj) {
|
|
return nil
|
|
}
|
|
|
|
assert(obj != nil)
|
|
// add new node to graph
|
|
node, _ := ctx.g.node(ctx, obj)
|
|
return node
|
|
}
|
|
|
|
func (ctx *context) use(used, by interface{}, kind edgeKind) {
|
|
if isIrrelevant(used) {
|
|
return
|
|
}
|
|
|
|
assert(used != nil)
|
|
if obj, ok := by.(types.Object); ok && obj.Pkg() != nil {
|
|
if !ctx.g.wholeProgram && obj.Pkg() != ctx.pkg.Pkg {
|
|
return
|
|
}
|
|
}
|
|
usedNode, new := ctx.g.node(ctx, used)
|
|
assert(!new)
|
|
if by == nil {
|
|
ctx.g.Root.use(usedNode, kind)
|
|
} else {
|
|
byNode, new := ctx.g.node(ctx, by)
|
|
assert(!new)
|
|
byNode.use(usedNode, kind)
|
|
}
|
|
}
|
|
|
|
func (ctx *context) seeAndUse(used, by interface{}, kind edgeKind) *Node {
|
|
node := ctx.see(used)
|
|
ctx.use(used, by, kind)
|
|
return node
|
|
}
|
|
|
|
// trackExportedIdentifier reports whether obj should be considered
|
|
// used due to being exported, checking various conditions that affect
|
|
// the decision.
|
|
func (g *Graph) trackExportedIdentifier(ctx *context, obj types.Object) bool {
|
|
if !obj.Exported() {
|
|
// object isn't exported, the question is moot
|
|
return false
|
|
}
|
|
path := g.fset.Position(obj.Pos()).Filename
|
|
if g.wholeProgram {
|
|
// Example functions without "Output:" comments aren't being
|
|
// run and thus don't show up in the graph.
|
|
if strings.HasSuffix(path, "_test.go") && strings.HasPrefix(obj.Name(), "Example") {
|
|
return true
|
|
}
|
|
// whole program mode tracks exported identifiers accurately
|
|
return false
|
|
}
|
|
|
|
if ctx.pkg.Pkg.Name() == "main" && !strings.HasSuffix(path, "_test.go") {
|
|
// exported identifiers in package main can't be imported.
|
|
// However, test functions can be called, and xtest packages
|
|
// even have access to exported identifiers.
|
|
return false
|
|
}
|
|
|
|
if strings.HasSuffix(path, "_test.go") {
|
|
if strings.HasPrefix(obj.Name(), "Test") ||
|
|
strings.HasPrefix(obj.Name(), "Benchmark") ||
|
|
strings.HasPrefix(obj.Name(), "Example") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (g *Graph) entry(pkg *pkg) {
|
|
no := atomic.AddUint64(&g.nodeOffset, 1)
|
|
ctx := &context{
|
|
g: g,
|
|
pkg: pkg,
|
|
nodeCounter: no * 1e9,
|
|
seenFns: map[string]struct{}{},
|
|
}
|
|
if g.wholeProgram {
|
|
ctx.seenTypes = &g.seenTypes
|
|
} else {
|
|
ctx.seenTypes = &typeutil.Map{}
|
|
}
|
|
|
|
scopes := map[*types.Scope]*ir.Function{}
|
|
for _, fn := range pkg.SrcFuncs {
|
|
if fn.Object() != nil {
|
|
scope := fn.Object().(*types.Func).Scope()
|
|
scopes[scope] = fn
|
|
}
|
|
}
|
|
|
|
for _, f := range pkg.Files {
|
|
for _, cg := range f.Comments {
|
|
for _, c := range cg.List {
|
|
if strings.HasPrefix(c.Text, "//go:linkname ") {
|
|
// FIXME(dh): we're looking at all comments. The
|
|
// compiler only looks at comments in the
|
|
// left-most column. The intention probably is to
|
|
// only look at top-level comments.
|
|
|
|
// (1.8) packages use symbols linked via go:linkname
|
|
fields := strings.Fields(c.Text)
|
|
if len(fields) == 3 {
|
|
if m, ok := pkg.IR.Members[fields[1]]; ok {
|
|
var obj types.Object
|
|
switch m := m.(type) {
|
|
case *ir.Global:
|
|
obj = m.Object()
|
|
case *ir.Function:
|
|
obj = m.Object()
|
|
default:
|
|
panic(fmt.Sprintf("unhandled type: %T", m))
|
|
}
|
|
assert(obj != nil)
|
|
ctx.seeAndUse(obj, nil, edgeLinkname)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
surroundingFunc := func(obj types.Object) *ir.Function {
|
|
scope := obj.Parent()
|
|
for scope != nil {
|
|
if fn := scopes[scope]; fn != nil {
|
|
return fn
|
|
}
|
|
scope = scope.Parent()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IR form won't tell us about locally scoped types that aren't
|
|
// being used. Walk the list of Defs to get all named types.
|
|
//
|
|
// IR form also won't tell us about constants; use Defs and Uses
|
|
// to determine which constants exist and which are being used.
|
|
for _, obj := range pkg.TypesInfo.Defs {
|
|
switch obj := obj.(type) {
|
|
case *types.TypeName:
|
|
// types are being handled by walking the AST
|
|
case *types.Const:
|
|
ctx.see(obj)
|
|
fn := surroundingFunc(obj)
|
|
if fn == nil && g.trackExportedIdentifier(ctx, obj) {
|
|
// (1.4) packages use exported constants (unless in package main)
|
|
ctx.use(obj, nil, edgeExportedConstant)
|
|
}
|
|
g.typ(ctx, obj.Type(), nil)
|
|
ctx.seeAndUse(obj.Type(), obj, edgeType)
|
|
}
|
|
}
|
|
|
|
// Find constants being used inside functions, find sinks in tests
|
|
for _, fn := range pkg.SrcFuncs {
|
|
if fn.Object() != nil {
|
|
ctx.see(fn.Object())
|
|
}
|
|
node := fn.Source()
|
|
if node == nil {
|
|
continue
|
|
}
|
|
ast.Inspect(node, func(node ast.Node) bool {
|
|
switch node := node.(type) {
|
|
case *ast.Ident:
|
|
obj, ok := pkg.TypesInfo.Uses[node]
|
|
if !ok {
|
|
return true
|
|
}
|
|
switch obj := obj.(type) {
|
|
case *types.Const:
|
|
ctx.seeAndUse(obj, owningObject(fn), edgeUsedConstant)
|
|
}
|
|
case *ast.AssignStmt:
|
|
for _, expr := range node.Lhs {
|
|
ident, ok := expr.(*ast.Ident)
|
|
if !ok {
|
|
continue
|
|
}
|
|
obj := pkg.TypesInfo.ObjectOf(ident)
|
|
if obj == nil {
|
|
continue
|
|
}
|
|
path := g.fset.File(obj.Pos()).Name()
|
|
if strings.HasSuffix(path, "_test.go") {
|
|
if obj.Parent() != nil && obj.Parent().Parent() != nil && obj.Parent().Parent().Parent() == nil {
|
|
// object's scope is the package, whose
|
|
// parent is the file, whose parent is nil
|
|
|
|
// (4.9) functions use package-level variables they assign to iff in tests (sinks for benchmarks)
|
|
// (9.7) variable _reads_ use variables, writes do not, except in tests
|
|
ctx.seeAndUse(obj, owningObject(fn), edgeTestSink)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
// Find constants being used in non-function contexts
|
|
for _, obj := range pkg.TypesInfo.Uses {
|
|
_, ok := obj.(*types.Const)
|
|
if !ok {
|
|
continue
|
|
}
|
|
ctx.seeAndUse(obj, nil, edgeUsedConstant)
|
|
}
|
|
|
|
var fns []*types.Func
|
|
var fn *types.Func
|
|
var stack []ast.Node
|
|
for _, f := range pkg.Files {
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
if n == nil {
|
|
pop := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
if _, ok := pop.(*ast.FuncDecl); ok {
|
|
fns = fns[:len(fns)-1]
|
|
if len(fns) == 0 {
|
|
fn = nil
|
|
} else {
|
|
fn = fns[len(fns)-1]
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
stack = append(stack, n)
|
|
switch n := n.(type) {
|
|
case *ast.FuncDecl:
|
|
fn = pkg.TypesInfo.ObjectOf(n.Name).(*types.Func)
|
|
fns = append(fns, fn)
|
|
ctx.see(fn)
|
|
case *ast.GenDecl:
|
|
switch n.Tok {
|
|
case token.CONST:
|
|
groups := code.GroupSpecs(pkg.Fset, n.Specs)
|
|
for _, specs := range groups {
|
|
if len(specs) > 1 {
|
|
cg := &ConstGroup{}
|
|
ctx.see(cg)
|
|
for _, spec := range specs {
|
|
for _, name := range spec.(*ast.ValueSpec).Names {
|
|
obj := pkg.TypesInfo.ObjectOf(name)
|
|
// (10.1) const groups
|
|
ctx.seeAndUse(obj, cg, edgeConstGroup)
|
|
ctx.use(cg, obj, edgeConstGroup)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case token.VAR:
|
|
for _, spec := range n.Specs {
|
|
v := spec.(*ast.ValueSpec)
|
|
for _, name := range v.Names {
|
|
T := pkg.TypesInfo.TypeOf(name)
|
|
if fn != nil {
|
|
ctx.seeAndUse(T, fn, edgeVarDecl)
|
|
} else {
|
|
// TODO(dh): we likely want to make
|
|
// the type used by the variable, not
|
|
// the package containing the
|
|
// variable. But then we have to take
|
|
// special care of blank identifiers.
|
|
ctx.seeAndUse(T, nil, edgeVarDecl)
|
|
}
|
|
g.typ(ctx, T, nil)
|
|
}
|
|
}
|
|
case token.TYPE:
|
|
for _, spec := range n.Specs {
|
|
// go/types doesn't provide a way to go from a
|
|
// types.Named to the named type it was based on
|
|
// (the t1 in type t2 t1). Therefore we walk the
|
|
// AST and process GenDecls.
|
|
//
|
|
// (2.2) named types use the type they're based on
|
|
v := spec.(*ast.TypeSpec)
|
|
T := pkg.TypesInfo.TypeOf(v.Type)
|
|
obj := pkg.TypesInfo.ObjectOf(v.Name)
|
|
ctx.see(obj)
|
|
ctx.see(T)
|
|
ctx.use(T, obj, edgeType)
|
|
g.typ(ctx, obj.Type(), nil)
|
|
g.typ(ctx, T, nil)
|
|
|
|
if v.Assign != 0 {
|
|
aliasFor := obj.(*types.TypeName).Type()
|
|
// (2.3) named types use all their aliases. we can't easily track uses of aliases
|
|
if isIrrelevant(aliasFor) {
|
|
// We do not track the type this is an
|
|
// alias for (for example builtins), so
|
|
// just mark the alias used.
|
|
//
|
|
// FIXME(dh): what about aliases declared inside functions?
|
|
ctx.use(obj, nil, edgeAlias)
|
|
} else {
|
|
ctx.see(aliasFor)
|
|
ctx.seeAndUse(obj, aliasFor, edgeAlias)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
for _, m := range pkg.IR.Members {
|
|
switch m := m.(type) {
|
|
case *ir.NamedConst:
|
|
// nothing to do, we collect all constants from Defs
|
|
case *ir.Global:
|
|
if m.Object() != nil {
|
|
ctx.see(m.Object())
|
|
if g.trackExportedIdentifier(ctx, m.Object()) {
|
|
// (1.3) packages use exported variables (unless in package main)
|
|
ctx.use(m.Object(), nil, edgeExportedVariable)
|
|
}
|
|
}
|
|
case *ir.Function:
|
|
mObj := owningObject(m)
|
|
if mObj != nil {
|
|
ctx.see(mObj)
|
|
}
|
|
//lint:ignore SA9003 handled implicitly
|
|
if m.Name() == "init" {
|
|
// (1.5) packages use init functions
|
|
//
|
|
// This is handled implicitly. The generated init
|
|
// function has no object, thus everything in it will
|
|
// be owned by the package.
|
|
}
|
|
// This branch catches top-level functions, not methods.
|
|
if m.Object() != nil && g.trackExportedIdentifier(ctx, m.Object()) {
|
|
// (1.2) packages use exported functions (unless in package main)
|
|
ctx.use(mObj, nil, edgeExportedFunction)
|
|
}
|
|
if m.Name() == "main" && pkg.Pkg.Name() == "main" {
|
|
// (1.7) packages use the main function iff in the main package
|
|
ctx.use(mObj, nil, edgeMainFunction)
|
|
}
|
|
if pkg.Pkg.Path() == "runtime" && runtimeFuncs[m.Name()] {
|
|
// (9.8) runtime functions that may be called from user code via the compiler
|
|
ctx.use(mObj, nil, edgeRuntimeFunction)
|
|
}
|
|
if m.Source() != nil {
|
|
doc := m.Source().(*ast.FuncDecl).Doc
|
|
if doc != nil {
|
|
for _, cmt := range doc.List {
|
|
if strings.HasPrefix(cmt.Text, "//go:cgo_export_") {
|
|
// (1.6) packages use functions exported to cgo
|
|
ctx.use(mObj, nil, edgeCgoExported)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
g.function(ctx, m)
|
|
case *ir.Type:
|
|
if m.Object() != nil {
|
|
ctx.see(m.Object())
|
|
if g.trackExportedIdentifier(ctx, m.Object()) {
|
|
// (1.1) packages use exported named types (unless in package main)
|
|
ctx.use(m.Object(), nil, edgeExportedType)
|
|
}
|
|
}
|
|
g.typ(ctx, m.Type(), nil)
|
|
default:
|
|
panic(fmt.Sprintf("unreachable: %T", m))
|
|
}
|
|
}
|
|
|
|
if !g.wholeProgram {
|
|
// When not in whole program mode we reset seenTypes after each package,
|
|
// which means g.seenTypes only contains types of
|
|
// interest to us. In whole program mode, we're better off
|
|
// processing all interfaces at once, globally, both for
|
|
// performance reasons and because in whole program mode we
|
|
// actually care about all interfaces, not just the subset
|
|
// that has unexported methods.
|
|
|
|
var ifaces []*types.Interface
|
|
var notIfaces []types.Type
|
|
|
|
ctx.seenTypes.Iterate(func(t types.Type, _ interface{}) {
|
|
switch t := t.(type) {
|
|
case *types.Interface:
|
|
// OPT(dh): (8.1) we only need interfaces that have unexported methods
|
|
ifaces = append(ifaces, t)
|
|
default:
|
|
if _, ok := t.Underlying().(*types.Interface); !ok {
|
|
notIfaces = append(notIfaces, t)
|
|
}
|
|
}
|
|
})
|
|
|
|
// (8.0) handle interfaces
|
|
for _, t := range notIfaces {
|
|
ms := pkg.IR.Prog.MethodSets.MethodSet(t)
|
|
for _, iface := range ifaces {
|
|
if sels, ok := g.implements(t, iface, ms); ok {
|
|
for _, sel := range sels {
|
|
g.useMethod(ctx, t, sel, t, edgeImplements)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *Graph) useMethod(ctx *context, t types.Type, sel *types.Selection, by interface{}, kind edgeKind) {
|
|
obj := sel.Obj()
|
|
path := sel.Index()
|
|
assert(obj != nil)
|
|
if len(path) > 1 {
|
|
base := code.Dereference(t).Underlying().(*types.Struct)
|
|
for _, idx := range path[:len(path)-1] {
|
|
next := base.Field(idx)
|
|
// (6.3) structs use embedded fields that help implement interfaces
|
|
ctx.see(base)
|
|
ctx.seeAndUse(next, base, edgeProvidesMethod)
|
|
base, _ = code.Dereference(next.Type()).Underlying().(*types.Struct)
|
|
}
|
|
}
|
|
ctx.seeAndUse(obj, by, kind)
|
|
}
|
|
|
|
func owningObject(fn *ir.Function) types.Object {
|
|
if fn.Object() != nil {
|
|
return fn.Object()
|
|
}
|
|
if fn.Parent() != nil {
|
|
return owningObject(fn.Parent())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *Graph) function(ctx *context, fn *ir.Function) {
|
|
if fn.Package() != nil && fn.Package() != ctx.pkg.IR {
|
|
return
|
|
}
|
|
|
|
name := fn.RelString(nil)
|
|
if _, ok := ctx.seenFns[name]; ok {
|
|
return
|
|
}
|
|
ctx.seenFns[name] = struct{}{}
|
|
|
|
// (4.1) functions use all their arguments, return parameters and receivers
|
|
g.signature(ctx, fn.Signature, owningObject(fn))
|
|
g.instructions(ctx, fn)
|
|
for _, anon := range fn.AnonFuncs {
|
|
// (4.2) functions use anonymous functions defined beneath them
|
|
//
|
|
// This fact is expressed implicitly. Anonymous functions have
|
|
// no types.Object, so their owner is the surrounding
|
|
// function.
|
|
g.function(ctx, anon)
|
|
}
|
|
}
|
|
|
|
func (g *Graph) typ(ctx *context, t types.Type, parent types.Type) {
|
|
if g.wholeProgram {
|
|
g.mu.Lock()
|
|
}
|
|
if ctx.seenTypes.At(t) != nil {
|
|
if g.wholeProgram {
|
|
g.mu.Unlock()
|
|
}
|
|
return
|
|
}
|
|
if g.wholeProgram {
|
|
g.mu.Unlock()
|
|
}
|
|
if t, ok := t.(*types.Named); ok && t.Obj().Pkg() != nil {
|
|
if t.Obj().Pkg() != ctx.pkg.Pkg {
|
|
return
|
|
}
|
|
}
|
|
|
|
if g.wholeProgram {
|
|
g.mu.Lock()
|
|
}
|
|
ctx.seenTypes.Set(t, struct{}{})
|
|
if g.wholeProgram {
|
|
g.mu.Unlock()
|
|
}
|
|
if isIrrelevant(t) {
|
|
return
|
|
}
|
|
|
|
ctx.see(t)
|
|
switch t := t.(type) {
|
|
case *types.Struct:
|
|
for i := 0; i < t.NumFields(); i++ {
|
|
ctx.see(t.Field(i))
|
|
if t.Field(i).Exported() {
|
|
// (6.2) structs use exported fields
|
|
ctx.use(t.Field(i), t, edgeExportedField)
|
|
} else if t.Field(i).Name() == "_" {
|
|
ctx.use(t.Field(i), t, edgeBlankField)
|
|
} else if isNoCopyType(t.Field(i).Type()) {
|
|
// (6.1) structs use fields of type NoCopy sentinel
|
|
ctx.use(t.Field(i), t, edgeNoCopySentinel)
|
|
} else if parent == nil {
|
|
// (11.1) anonymous struct types use all their fields.
|
|
ctx.use(t.Field(i), t, edgeAnonymousStruct)
|
|
}
|
|
if t.Field(i).Anonymous() {
|
|
// (e3) exported identifiers aren't automatically used.
|
|
if !g.wholeProgram {
|
|
// does the embedded field contribute exported methods to the method set?
|
|
T := t.Field(i).Type()
|
|
if _, ok := T.Underlying().(*types.Pointer); !ok {
|
|
// An embedded field is addressable, so check
|
|
// the pointer type to get the full method set
|
|
T = types.NewPointer(T)
|
|
}
|
|
ms := ctx.pkg.IR.Prog.MethodSets.MethodSet(T)
|
|
for j := 0; j < ms.Len(); j++ {
|
|
if ms.At(j).Obj().Exported() {
|
|
// (6.4) structs use embedded fields that have exported methods (recursively)
|
|
ctx.use(t.Field(i), t, edgeExtendsExportedMethodSet)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
seen := map[*types.Struct]struct{}{}
|
|
var hasExportedField func(t types.Type) bool
|
|
hasExportedField = func(T types.Type) bool {
|
|
t, ok := code.Dereference(T).Underlying().(*types.Struct)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if _, ok := seen[t]; ok {
|
|
return false
|
|
}
|
|
seen[t] = struct{}{}
|
|
for i := 0; i < t.NumFields(); i++ {
|
|
field := t.Field(i)
|
|
if field.Exported() {
|
|
return true
|
|
}
|
|
if field.Embedded() && hasExportedField(field.Type()) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
// does the embedded field contribute exported fields?
|
|
if hasExportedField(t.Field(i).Type()) {
|
|
// (6.5) structs use embedded structs that have exported fields (recursively)
|
|
ctx.use(t.Field(i), t, edgeExtendsExportedFields)
|
|
}
|
|
|
|
}
|
|
g.variable(ctx, t.Field(i))
|
|
}
|
|
case *types.Basic:
|
|
// Nothing to do
|
|
case *types.Named:
|
|
// (9.3) types use their underlying and element types
|
|
ctx.seeAndUse(t.Underlying(), t, edgeUnderlyingType)
|
|
ctx.seeAndUse(t.Obj(), t, edgeTypeName)
|
|
ctx.seeAndUse(t, t.Obj(), edgeNamedType)
|
|
|
|
// (2.4) named types use the pointer type
|
|
if _, ok := t.Underlying().(*types.Interface); !ok && t.NumMethods() > 0 {
|
|
ctx.seeAndUse(types.NewPointer(t), t, edgePointerType)
|
|
}
|
|
|
|
for i := 0; i < t.NumMethods(); i++ {
|
|
ctx.see(t.Method(i))
|
|
// don't use trackExportedIdentifier here, we care about
|
|
// all exported methods, even in package main or in tests.
|
|
if t.Method(i).Exported() && !g.wholeProgram {
|
|
// (2.1) named types use exported methods
|
|
ctx.use(t.Method(i), t, edgeExportedMethod)
|
|
}
|
|
g.function(ctx, ctx.pkg.IR.Prog.FuncValue(t.Method(i)))
|
|
}
|
|
|
|
g.typ(ctx, t.Underlying(), t)
|
|
case *types.Slice:
|
|
// (9.3) types use their underlying and element types
|
|
ctx.seeAndUse(t.Elem(), t, edgeElementType)
|
|
g.typ(ctx, t.Elem(), nil)
|
|
case *types.Map:
|
|
// (9.3) types use their underlying and element types
|
|
ctx.seeAndUse(t.Elem(), t, edgeElementType)
|
|
// (9.3) types use their underlying and element types
|
|
ctx.seeAndUse(t.Key(), t, edgeKeyType)
|
|
g.typ(ctx, t.Elem(), nil)
|
|
g.typ(ctx, t.Key(), nil)
|
|
case *types.Signature:
|
|
g.signature(ctx, t, nil)
|
|
case *types.Interface:
|
|
for i := 0; i < t.NumMethods(); i++ {
|
|
m := t.Method(i)
|
|
// (8.3) All interface methods are marked as used
|
|
ctx.seeAndUse(m, t, edgeInterfaceMethod)
|
|
ctx.seeAndUse(m.Type().(*types.Signature), m, edgeSignature)
|
|
g.signature(ctx, m.Type().(*types.Signature), nil)
|
|
}
|
|
for i := 0; i < t.NumEmbeddeds(); i++ {
|
|
tt := t.EmbeddedType(i)
|
|
// (8.4) All embedded interfaces are marked as used
|
|
ctx.seeAndUse(tt, t, edgeEmbeddedInterface)
|
|
}
|
|
case *types.Array:
|
|
// (9.3) types use their underlying and element types
|
|
ctx.seeAndUse(t.Elem(), t, edgeElementType)
|
|
g.typ(ctx, t.Elem(), nil)
|
|
case *types.Pointer:
|
|
// (9.3) types use their underlying and element types
|
|
ctx.seeAndUse(t.Elem(), t, edgeElementType)
|
|
g.typ(ctx, t.Elem(), nil)
|
|
case *types.Chan:
|
|
// (9.3) types use their underlying and element types
|
|
ctx.seeAndUse(t.Elem(), t, edgeElementType)
|
|
g.typ(ctx, t.Elem(), nil)
|
|
case *types.Tuple:
|
|
for i := 0; i < t.Len(); i++ {
|
|
// (9.3) types use their underlying and element types
|
|
ctx.seeAndUse(t.At(i).Type(), t, edgeTupleElement|edgeType)
|
|
g.typ(ctx, t.At(i).Type(), nil)
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("unreachable: %T", t))
|
|
}
|
|
}
|
|
|
|
func (g *Graph) variable(ctx *context, v *types.Var) {
|
|
// (9.2) variables use their types
|
|
ctx.seeAndUse(v.Type(), v, edgeType)
|
|
g.typ(ctx, v.Type(), nil)
|
|
}
|
|
|
|
func (g *Graph) signature(ctx *context, sig *types.Signature, fn types.Object) {
|
|
var user interface{} = fn
|
|
if fn == nil {
|
|
user = sig
|
|
ctx.see(sig)
|
|
}
|
|
if sig.Recv() != nil {
|
|
ctx.seeAndUse(sig.Recv().Type(), user, edgeReceiver|edgeType)
|
|
g.typ(ctx, sig.Recv().Type(), nil)
|
|
}
|
|
for i := 0; i < sig.Params().Len(); i++ {
|
|
param := sig.Params().At(i)
|
|
ctx.seeAndUse(param.Type(), user, edgeFunctionArgument|edgeType)
|
|
g.typ(ctx, param.Type(), nil)
|
|
}
|
|
for i := 0; i < sig.Results().Len(); i++ {
|
|
param := sig.Results().At(i)
|
|
ctx.seeAndUse(param.Type(), user, edgeFunctionResult|edgeType)
|
|
g.typ(ctx, param.Type(), nil)
|
|
}
|
|
}
|
|
|
|
func (g *Graph) instructions(ctx *context, fn *ir.Function) {
|
|
fnObj := owningObject(fn)
|
|
for _, b := range fn.Blocks {
|
|
for _, instr := range b.Instrs {
|
|
ops := instr.Operands(nil)
|
|
switch instr.(type) {
|
|
case *ir.Store:
|
|
// (9.7) variable _reads_ use variables, writes do not
|
|
ops = ops[1:]
|
|
case *ir.DebugRef:
|
|
ops = nil
|
|
}
|
|
for _, arg := range ops {
|
|
walkPhi(*arg, func(v ir.Value) {
|
|
switch v := v.(type) {
|
|
case *ir.Function:
|
|
// (4.3) functions use closures and bound methods.
|
|
// (4.5) functions use functions they call
|
|
// (9.5) instructions use their operands
|
|
// (4.4) functions use functions they return. we assume that someone else will call the returned function
|
|
if owningObject(v) != nil {
|
|
ctx.seeAndUse(owningObject(v), fnObj, edgeInstructionOperand)
|
|
}
|
|
g.function(ctx, v)
|
|
case *ir.Const:
|
|
// (9.6) instructions use their operands' types
|
|
ctx.seeAndUse(v.Type(), fnObj, edgeType)
|
|
g.typ(ctx, v.Type(), nil)
|
|
case *ir.Global:
|
|
if v.Object() != nil {
|
|
// (9.5) instructions use their operands
|
|
ctx.seeAndUse(v.Object(), fnObj, edgeInstructionOperand)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
if v, ok := instr.(ir.Value); ok {
|
|
if _, ok := v.(*ir.Range); !ok {
|
|
// See https://github.com/golang/go/issues/19670
|
|
|
|
// (4.8) instructions use their types
|
|
// (9.4) conversions use the type they convert to
|
|
ctx.seeAndUse(v.Type(), fnObj, edgeType)
|
|
g.typ(ctx, v.Type(), nil)
|
|
}
|
|
}
|
|
switch instr := instr.(type) {
|
|
case *ir.Field:
|
|
st := instr.X.Type().Underlying().(*types.Struct)
|
|
field := st.Field(instr.Field)
|
|
// (4.7) functions use fields they access
|
|
ctx.seeAndUse(field, fnObj, edgeFieldAccess)
|
|
case *ir.FieldAddr:
|
|
st := code.Dereference(instr.X.Type()).Underlying().(*types.Struct)
|
|
field := st.Field(instr.Field)
|
|
// (4.7) functions use fields they access
|
|
ctx.seeAndUse(field, fnObj, edgeFieldAccess)
|
|
case *ir.Store:
|
|
// nothing to do, handled generically by operands
|
|
case *ir.Call:
|
|
c := instr.Common()
|
|
if !c.IsInvoke() {
|
|
// handled generically as an instruction operand
|
|
|
|
if g.wholeProgram {
|
|
// (e3) special case known reflection-based method callers
|
|
switch code.CallName(c) {
|
|
case "net/rpc.Register", "net/rpc.RegisterName", "(*net/rpc.Server).Register", "(*net/rpc.Server).RegisterName":
|
|
var arg ir.Value
|
|
switch code.CallName(c) {
|
|
case "net/rpc.Register":
|
|
arg = c.Args[0]
|
|
case "net/rpc.RegisterName":
|
|
arg = c.Args[1]
|
|
case "(*net/rpc.Server).Register":
|
|
arg = c.Args[1]
|
|
case "(*net/rpc.Server).RegisterName":
|
|
arg = c.Args[2]
|
|
}
|
|
walkPhi(arg, func(v ir.Value) {
|
|
if v, ok := v.(*ir.MakeInterface); ok {
|
|
walkPhi(v.X, func(vv ir.Value) {
|
|
ms := ctx.pkg.IR.Prog.MethodSets.MethodSet(vv.Type())
|
|
for i := 0; i < ms.Len(); i++ {
|
|
if ms.At(i).Obj().Exported() {
|
|
g.useMethod(ctx, vv.Type(), ms.At(i), fnObj, edgeNetRPCRegister)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
// (4.5) functions use functions/interface methods they call
|
|
ctx.seeAndUse(c.Method, fnObj, edgeInterfaceCall)
|
|
}
|
|
case *ir.Return:
|
|
// nothing to do, handled generically by operands
|
|
case *ir.ChangeType:
|
|
// conversion type handled generically
|
|
|
|
s1, ok1 := code.Dereference(instr.Type()).Underlying().(*types.Struct)
|
|
s2, ok2 := code.Dereference(instr.X.Type()).Underlying().(*types.Struct)
|
|
if ok1 && ok2 {
|
|
// Converting between two structs. The fields are
|
|
// relevant for the conversion, but only if the
|
|
// fields are also used outside of the conversion.
|
|
// Mark fields as used by each other.
|
|
|
|
assert(s1.NumFields() == s2.NumFields())
|
|
for i := 0; i < s1.NumFields(); i++ {
|
|
ctx.see(s1.Field(i))
|
|
ctx.see(s2.Field(i))
|
|
// (5.1) when converting between two equivalent structs, the fields in
|
|
// either struct use each other. the fields are relevant for the
|
|
// conversion, but only if the fields are also accessed outside the
|
|
// conversion.
|
|
ctx.seeAndUse(s1.Field(i), s2.Field(i), edgeStructConversion)
|
|
ctx.seeAndUse(s2.Field(i), s1.Field(i), edgeStructConversion)
|
|
}
|
|
}
|
|
case *ir.MakeInterface:
|
|
// nothing to do, handled generically by operands
|
|
case *ir.Slice:
|
|
// nothing to do, handled generically by operands
|
|
case *ir.RunDefers:
|
|
// nothing to do, the deferred functions are already marked use by defering them.
|
|
case *ir.Convert:
|
|
// to unsafe.Pointer
|
|
if typ, ok := instr.Type().(*types.Basic); ok && typ.Kind() == types.UnsafePointer {
|
|
if ptr, ok := instr.X.Type().Underlying().(*types.Pointer); ok {
|
|
if st, ok := ptr.Elem().Underlying().(*types.Struct); ok {
|
|
for i := 0; i < st.NumFields(); i++ {
|
|
// (5.2) when converting to or from unsafe.Pointer, mark all fields as used.
|
|
ctx.seeAndUse(st.Field(i), fnObj, edgeUnsafeConversion)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// from unsafe.Pointer
|
|
if typ, ok := instr.X.Type().(*types.Basic); ok && typ.Kind() == types.UnsafePointer {
|
|
if ptr, ok := instr.Type().Underlying().(*types.Pointer); ok {
|
|
if st, ok := ptr.Elem().Underlying().(*types.Struct); ok {
|
|
for i := 0; i < st.NumFields(); i++ {
|
|
// (5.2) when converting to or from unsafe.Pointer, mark all fields as used.
|
|
ctx.seeAndUse(st.Field(i), fnObj, edgeUnsafeConversion)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case *ir.TypeAssert:
|
|
// nothing to do, handled generically by instruction
|
|
// type (possibly a tuple, which contains the asserted
|
|
// to type). redundantly handled by the type of
|
|
// ir.Extract, too
|
|
case *ir.MakeClosure:
|
|
// nothing to do, handled generically by operands
|
|
case *ir.Alloc:
|
|
// nothing to do
|
|
case *ir.UnOp:
|
|
// nothing to do
|
|
case *ir.BinOp:
|
|
// nothing to do
|
|
case *ir.If:
|
|
// nothing to do
|
|
case *ir.Jump:
|
|
// nothing to do
|
|
case *ir.Unreachable:
|
|
// nothing to do
|
|
case *ir.IndexAddr:
|
|
// nothing to do
|
|
case *ir.Extract:
|
|
// nothing to do
|
|
case *ir.Panic:
|
|
// nothing to do
|
|
case *ir.DebugRef:
|
|
// nothing to do
|
|
case *ir.BlankStore:
|
|
// nothing to do
|
|
case *ir.Phi:
|
|
// nothing to do
|
|
case *ir.Sigma:
|
|
// nothing to do
|
|
case *ir.MakeMap:
|
|
// nothing to do
|
|
case *ir.MapUpdate:
|
|
// nothing to do
|
|
case *ir.MapLookup:
|
|
// nothing to do
|
|
case *ir.StringLookup:
|
|
// nothing to do
|
|
case *ir.MakeSlice:
|
|
// nothing to do
|
|
case *ir.Send:
|
|
// nothing to do
|
|
case *ir.MakeChan:
|
|
// nothing to do
|
|
case *ir.Range:
|
|
// nothing to do
|
|
case *ir.Next:
|
|
// nothing to do
|
|
case *ir.Index:
|
|
// nothing to do
|
|
case *ir.Select:
|
|
// nothing to do
|
|
case *ir.ChangeInterface:
|
|
// nothing to do
|
|
case *ir.Load:
|
|
// nothing to do
|
|
case *ir.Go:
|
|
// nothing to do
|
|
case *ir.Defer:
|
|
// nothing to do
|
|
case *ir.Parameter:
|
|
// nothing to do
|
|
case *ir.Const:
|
|
// nothing to do
|
|
case *ir.Recv:
|
|
// nothing to do
|
|
case *ir.TypeSwitch:
|
|
// nothing to do
|
|
case *ir.ConstantSwitch:
|
|
// nothing to do
|
|
default:
|
|
panic(fmt.Sprintf("unreachable: %T", instr))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// isNoCopyType reports whether a type represents the NoCopy sentinel
|
|
// type. The NoCopy type is a named struct with no fields and exactly
|
|
// one method `func Lock()` that is empty.
|
|
//
|
|
// FIXME(dh): currently we're not checking that the function body is
|
|
// empty.
|
|
func isNoCopyType(typ types.Type) bool {
|
|
st, ok := typ.Underlying().(*types.Struct)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if st.NumFields() != 0 {
|
|
return false
|
|
}
|
|
|
|
named, ok := typ.(*types.Named)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if named.NumMethods() != 1 {
|
|
return false
|
|
}
|
|
meth := named.Method(0)
|
|
if meth.Name() != "Lock" {
|
|
return false
|
|
}
|
|
sig := meth.Type().(*types.Signature)
|
|
if sig.Params().Len() != 0 || sig.Results().Len() != 0 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func walkPhi(v ir.Value, fn func(v ir.Value)) {
|
|
phi, ok := v.(*ir.Phi)
|
|
if !ok {
|
|
fn(v)
|
|
return
|
|
}
|
|
|
|
seen := map[ir.Value]struct{}{}
|
|
var impl func(v *ir.Phi)
|
|
impl = func(v *ir.Phi) {
|
|
if _, ok := seen[v]; ok {
|
|
return
|
|
}
|
|
seen[v] = struct{}{}
|
|
for _, e := range v.Edges {
|
|
if ev, ok := e.(*ir.Phi); ok {
|
|
impl(ev)
|
|
} else {
|
|
fn(e)
|
|
}
|
|
}
|
|
}
|
|
impl(phi)
|
|
}
|
|
|
|
func interfacesFromExportData(pkg *types.Package) []*types.Interface {
|
|
var out []*types.Interface
|
|
scope := pkg.Scope()
|
|
for _, name := range scope.Names() {
|
|
obj := scope.Lookup(name)
|
|
out = append(out, interfacesFromObject(obj)...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func interfacesFromObject(obj types.Object) []*types.Interface {
|
|
var out []*types.Interface
|
|
switch obj := obj.(type) {
|
|
case *types.Func:
|
|
sig := obj.Type().(*types.Signature)
|
|
for i := 0; i < sig.Results().Len(); i++ {
|
|
out = append(out, interfacesFromObject(sig.Results().At(i))...)
|
|
}
|
|
for i := 0; i < sig.Params().Len(); i++ {
|
|
out = append(out, interfacesFromObject(sig.Params().At(i))...)
|
|
}
|
|
case *types.TypeName:
|
|
if named, ok := obj.Type().(*types.Named); ok {
|
|
for i := 0; i < named.NumMethods(); i++ {
|
|
out = append(out, interfacesFromObject(named.Method(i))...)
|
|
}
|
|
|
|
if iface, ok := named.Underlying().(*types.Interface); ok {
|
|
out = append(out, iface)
|
|
}
|
|
}
|
|
case *types.Var:
|
|
// No call to Underlying here. We want unnamed interfaces
|
|
// only. Named interfaces are gotten directly from the
|
|
// package's scope.
|
|
if iface, ok := obj.Type().(*types.Interface); ok {
|
|
out = append(out, iface)
|
|
}
|
|
case *types.Const:
|
|
case *types.Builtin:
|
|
default:
|
|
panic(fmt.Sprintf("unhandled type: %T", obj))
|
|
}
|
|
return out
|
|
}
|