mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
1978 lines
54 KiB
Go
1978 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
|
|
}
|