// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bytes" "fmt" "go/ast" "go/constant" "go/token" "go/types" "os" "strings" "unicode/utf8" "golang.org/x/tools/cmd/guru/serial" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/types/typeutil" ) // describe describes the syntax node denoted by the query position, // including: // - its syntactic category // - the definition of its referent (for identifiers) [now redundant] // - its type, fields, and methods (for an expression or type expression) // func describe(q *Query) error { lconf := loader.Config{Build: q.Build} allowErrors(&lconf) if _, err := importQueryPackage(q.Pos, &lconf); err != nil { return err } // Load/parse/type-check the program. lprog, err := lconf.Load() if err != nil { return err } qpos, err := parseQueryPos(lprog, q.Pos, true) // (need exact pos) if err != nil { return err } if false { // debugging fprintf(os.Stderr, lprog.Fset, qpos.path[0], "you selected: %s %s", astutil.NodeDescription(qpos.path[0]), pathToString(qpos.path)) } var qr QueryResult path, action := findInterestingNode(qpos.info, qpos.path) switch action { case actionExpr: qr, err = describeValue(qpos, path) case actionType: qr, err = describeType(qpos, path) case actionPackage: qr, err = describePackage(qpos, path) case actionStmt: qr, err = describeStmt(qpos, path) case actionUnknown: qr = &describeUnknownResult{path[0]} default: panic(action) // unreachable } if err != nil { return err } q.Output(lprog.Fset, qr) return nil } type describeUnknownResult struct { node ast.Node } func (r *describeUnknownResult) PrintPlain(printf printfFunc) { // Nothing much to say about misc syntax. printf(r.node, "%s", astutil.NodeDescription(r.node)) } func (r *describeUnknownResult) JSON(fset *token.FileSet) []byte { return toJSON(&serial.Describe{ Desc: astutil.NodeDescription(r.node), Pos: fset.Position(r.node.Pos()).String(), }) } type action int const ( actionUnknown action = iota // None of the below actionExpr // FuncDecl, true Expr or Ident(types.{Const,Var}) actionType // type Expr or Ident(types.TypeName). actionStmt // Stmt or Ident(types.Label) actionPackage // Ident(types.Package) or ImportSpec ) // findInterestingNode classifies the syntax node denoted by path as one of: // - an expression, part of an expression or a reference to a constant // or variable; // - a type, part of a type, or a reference to a named type; // - a statement, part of a statement, or a label referring to a statement; // - part of a package declaration or import spec. // - none of the above. // and returns the most "interesting" associated node, which may be // the same node, an ancestor or a descendent. // func findInterestingNode(pkginfo *loader.PackageInfo, path []ast.Node) ([]ast.Node, action) { // TODO(adonovan): integrate with go/types/stdlib_test.go and // apply this to every AST node we can find to make sure it // doesn't crash. // TODO(adonovan): audit for ParenExpr safety, esp. since we // traverse up and down. // TODO(adonovan): if the users selects the "." in // "fmt.Fprintf()", they'll get an ambiguous selection error; // we won't even reach here. Can we do better? // TODO(adonovan): describing a field within 'type T struct {...}' // describes the (anonymous) struct type and concludes "no methods". // We should ascend to the enclosing type decl, if any. for len(path) > 0 { switch n := path[0].(type) { case *ast.GenDecl: if len(n.Specs) == 1 { // Descend to sole {Import,Type,Value}Spec child. path = append([]ast.Node{n.Specs[0]}, path...) continue } return path, actionUnknown // uninteresting case *ast.FuncDecl: // Descend to function name. path = append([]ast.Node{n.Name}, path...) continue case *ast.ImportSpec: return path, actionPackage case *ast.ValueSpec: if len(n.Names) == 1 { // Descend to sole Ident child. path = append([]ast.Node{n.Names[0]}, path...) continue } return path, actionUnknown // uninteresting case *ast.TypeSpec: // Descend to type name. path = append([]ast.Node{n.Name}, path...) continue case *ast.Comment, *ast.CommentGroup, *ast.File, *ast.KeyValueExpr, *ast.CommClause: return path, actionUnknown // uninteresting case ast.Stmt: return path, actionStmt case *ast.ArrayType, *ast.StructType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.ChanType: return path, actionType case *ast.Ellipsis: // Continue to enclosing node. // e.g. [...]T in ArrayType // f(x...) in CallExpr // f(x...T) in FuncType case *ast.Field: // TODO(adonovan): this needs more thought, // since fields can be so many things. if len(n.Names) == 1 { // Descend to sole Ident child. path = append([]ast.Node{n.Names[0]}, path...) continue } // Zero names (e.g. anon field in struct) // or multiple field or param names: // continue to enclosing field list. case *ast.FieldList: // Continue to enclosing node: // {Struct,Func,Interface}Type or FuncDecl. case *ast.BasicLit: if _, ok := path[1].(*ast.ImportSpec); ok { return path[1:], actionPackage } return path, actionExpr case *ast.SelectorExpr: // TODO(adonovan): use Selections info directly. if pkginfo.Uses[n.Sel] == nil { // TODO(adonovan): is this reachable? return path, actionUnknown } // Descend to .Sel child. path = append([]ast.Node{n.Sel}, path...) continue case *ast.Ident: switch pkginfo.ObjectOf(n).(type) { case *types.PkgName: return path, actionPackage case *types.Const: return path, actionExpr case *types.Label: return path, actionStmt case *types.TypeName: return path, actionType case *types.Var: // For x in 'struct {x T}', return struct type, for now. if _, ok := path[1].(*ast.Field); ok { _ = path[2].(*ast.FieldList) // assertion if _, ok := path[3].(*ast.StructType); ok { return path[3:], actionType } } return path, actionExpr case *types.Func: return path, actionExpr case *types.Builtin: // For reference to built-in function, return enclosing call. path = path[1:] // ascend to enclosing function call continue case *types.Nil: return path, actionExpr } // No object. switch path[1].(type) { case *ast.SelectorExpr: // Return enclosing selector expression. return path[1:], actionExpr case *ast.Field: // TODO(adonovan): test this. // e.g. all f in: // struct { f, g int } // interface { f() } // func (f T) method(f, g int) (f, g bool) // // switch path[3].(type) { // case *ast.FuncDecl: // case *ast.StructType: // case *ast.InterfaceType: // } // // return path[1:], actionExpr // // Unclear what to do with these. // Struct.Fields -- field // Interface.Methods -- field // FuncType.{Params.Results} -- actionExpr // FuncDecl.Recv -- actionExpr case *ast.File: // 'package foo' return path, actionPackage case *ast.ImportSpec: return path[1:], actionPackage default: // e.g. blank identifier // or y in "switch y := x.(type)" // or code in a _test.go file that's not part of the package. return path, actionUnknown } case *ast.StarExpr: if pkginfo.Types[n].IsType() { return path, actionType } return path, actionExpr case ast.Expr: // All Expr but {BasicLit,Ident,StarExpr} are // "true" expressions that evaluate to a value. return path, actionExpr } // Ascend to parent. path = path[1:] } return nil, actionUnknown // unreachable } func describeValue(qpos *queryPos, path []ast.Node) (*describeValueResult, error) { var expr ast.Expr var obj types.Object switch n := path[0].(type) { case *ast.ValueSpec: // ambiguous ValueSpec containing multiple names return nil, fmt.Errorf("multiple value specification") case *ast.Ident: obj = qpos.info.ObjectOf(n) expr = n case ast.Expr: expr = n default: // TODO(adonovan): is this reachable? return nil, fmt.Errorf("unexpected AST for expr: %T", n) } typ := qpos.info.TypeOf(expr) if typ == nil { typ = types.Typ[types.Invalid] } constVal := qpos.info.Types[expr].Value if c, ok := obj.(*types.Const); ok { constVal = c.Val() } return &describeValueResult{ qpos: qpos, expr: expr, typ: typ, names: appendNames(nil, typ), constVal: constVal, obj: obj, methods: accessibleMethods(typ, qpos.info.Pkg), fields: accessibleFields(typ, qpos.info.Pkg), }, nil } // appendNames returns named types found within the Type by // removing map, pointer, channel, slice, and array constructors. // It does not descend into structs or interfaces. func appendNames(names []*types.Named, typ types.Type) []*types.Named { // elemType specifies type that has some element in it // such as array, slice, chan, pointer type elemType interface { Elem() types.Type } switch t := typ.(type) { case *types.Named: names = append(names, t) case *types.Map: names = appendNames(names, t.Key()) names = appendNames(names, t.Elem()) case elemType: names = appendNames(names, t.Elem()) } return names } type describeValueResult struct { qpos *queryPos expr ast.Expr // query node typ types.Type // type of expression names []*types.Named // named types within typ constVal constant.Value // value of expression, if constant obj types.Object // var/func/const object, if expr was Ident methods []*types.Selection fields []describeField } func (r *describeValueResult) PrintPlain(printf printfFunc) { var prefix, suffix string if r.constVal != nil { suffix = fmt.Sprintf(" of value %s", r.constVal) } switch obj := r.obj.(type) { case *types.Func: if recv := obj.Type().(*types.Signature).Recv(); recv != nil { if _, ok := recv.Type().Underlying().(*types.Interface); ok { prefix = "interface method " } else { prefix = "method " } } } // Describe the expression. if r.obj != nil { if r.obj.Pos() == r.expr.Pos() { // defining ident printf(r.expr, "definition of %s%s%s", prefix, r.qpos.objectString(r.obj), suffix) } else { // referring ident printf(r.expr, "reference to %s%s%s", prefix, r.qpos.objectString(r.obj), suffix) if def := r.obj.Pos(); def != token.NoPos { printf(def, "defined here") } } } else { desc := astutil.NodeDescription(r.expr) if suffix != "" { // constant expression printf(r.expr, "%s%s", desc, suffix) } else { // non-constant expression printf(r.expr, "%s of type %s", desc, r.qpos.typeString(r.typ)) } } printMethods(printf, r.expr, r.methods) printFields(printf, r.expr, r.fields) printNamedTypes(printf, r.expr, r.names) } func (r *describeValueResult) JSON(fset *token.FileSet) []byte { var value, objpos string if r.constVal != nil { value = r.constVal.String() } if r.obj != nil { objpos = fset.Position(r.obj.Pos()).String() } typesPos := make([]serial.Definition, len(r.names)) for i, t := range r.names { typesPos[i] = serial.Definition{ ObjPos: fset.Position(t.Obj().Pos()).String(), Desc: r.qpos.typeString(t), } } return toJSON(&serial.Describe{ Desc: astutil.NodeDescription(r.expr), Pos: fset.Position(r.expr.Pos()).String(), Detail: "value", Value: &serial.DescribeValue{ Type: r.qpos.typeString(r.typ), TypesPos: typesPos, Value: value, ObjPos: objpos, }, }) } // ---- TYPE ------------------------------------------------------------ func describeType(qpos *queryPos, path []ast.Node) (*describeTypeResult, error) { var description string var typ types.Type switch n := path[0].(type) { case *ast.Ident: obj := qpos.info.ObjectOf(n).(*types.TypeName) typ = obj.Type() if isAlias(obj) { description = "alias of " } else if obj.Pos() == n.Pos() { description = "definition of " // (Named type) } else if _, ok := typ.(*types.Basic); ok { description = "reference to built-in " } else { description = "reference to " // (Named type) } case ast.Expr: typ = qpos.info.TypeOf(n) default: // Unreachable? return nil, fmt.Errorf("unexpected AST for type: %T", n) } description = description + "type " + qpos.typeString(typ) // Show sizes for structs and named types (it's fairly obvious for others). switch typ.(type) { case *types.Named, *types.Struct: szs := types.StdSizes{WordSize: 8, MaxAlign: 8} // assume amd64 description = fmt.Sprintf("%s (size %d, align %d)", description, szs.Sizeof(typ), szs.Alignof(typ)) } return &describeTypeResult{ qpos: qpos, node: path[0], description: description, typ: typ, methods: accessibleMethods(typ, qpos.info.Pkg), fields: accessibleFields(typ, qpos.info.Pkg), }, nil } type describeTypeResult struct { qpos *queryPos node ast.Node description string typ types.Type methods []*types.Selection fields []describeField } type describeField struct { implicits []*types.Named field *types.Var } func printMethods(printf printfFunc, node ast.Node, methods []*types.Selection) { if len(methods) > 0 { printf(node, "Methods:") } for _, meth := range methods { // Print the method type relative to the package // in which it was defined, not the query package, printf(meth.Obj(), "\t%s", types.SelectionString(meth, types.RelativeTo(meth.Obj().Pkg()))) } } func printFields(printf printfFunc, node ast.Node, fields []describeField) { if len(fields) > 0 { printf(node, "Fields:") } // Align the names and the types (requires two passes). var width int var names []string for _, f := range fields { var buf bytes.Buffer for _, fld := range f.implicits { buf.WriteString(fld.Obj().Name()) buf.WriteByte('.') } buf.WriteString(f.field.Name()) name := buf.String() if n := utf8.RuneCountInString(name); n > width { width = n } names = append(names, name) } for i, f := range fields { // Print the field type relative to the package // in which it was defined, not the query package, printf(f.field, "\t%*s %s", -width, names[i], types.TypeString(f.field.Type(), types.RelativeTo(f.field.Pkg()))) } } func printNamedTypes(printf printfFunc, node ast.Node, names []*types.Named) { if len(names) > 0 { printf(node, "Named types:") } for _, t := range names { // Print the type relative to the package // in which it was defined, not the query package, printf(t.Obj(), "\ttype %s defined here", types.TypeString(t.Obj().Type(), types.RelativeTo(t.Obj().Pkg()))) } } func (r *describeTypeResult) PrintPlain(printf printfFunc) { printf(r.node, "%s", r.description) // Show the underlying type for a reference to a named type. if nt, ok := r.typ.(*types.Named); ok && r.node.Pos() != nt.Obj().Pos() { // TODO(adonovan): improve display of complex struct/interface types. printf(nt.Obj(), "defined as %s", r.qpos.typeString(nt.Underlying())) } printMethods(printf, r.node, r.methods) if len(r.methods) == 0 { // Only report null result for type kinds // capable of bearing methods. switch r.typ.(type) { case *types.Interface, *types.Struct, *types.Named: printf(r.node, "No methods.") } } printFields(printf, r.node, r.fields) } func (r *describeTypeResult) JSON(fset *token.FileSet) []byte { var namePos, nameDef string if nt, ok := r.typ.(*types.Named); ok { namePos = fset.Position(nt.Obj().Pos()).String() nameDef = nt.Underlying().String() } return toJSON(&serial.Describe{ Desc: r.description, Pos: fset.Position(r.node.Pos()).String(), Detail: "type", Type: &serial.DescribeType{ Type: r.qpos.typeString(r.typ), NamePos: namePos, NameDef: nameDef, Methods: methodsToSerial(r.qpos.info.Pkg, r.methods, fset), }, }) } // ---- PACKAGE ------------------------------------------------------------ func describePackage(qpos *queryPos, path []ast.Node) (*describePackageResult, error) { var description string var pkg *types.Package switch n := path[0].(type) { case *ast.ImportSpec: var obj types.Object if n.Name != nil { obj = qpos.info.Defs[n.Name] } else { obj = qpos.info.Implicits[n] } pkgname, _ := obj.(*types.PkgName) if pkgname == nil { return nil, fmt.Errorf("can't import package %s", n.Path.Value) } pkg = pkgname.Imported() description = fmt.Sprintf("import of package %q", pkg.Path()) case *ast.Ident: if _, isDef := path[1].(*ast.File); isDef { // e.g. package id pkg = qpos.info.Pkg description = fmt.Sprintf("definition of package %q", pkg.Path()) } else { // e.g. import id "..." // or id.F() pkg = qpos.info.ObjectOf(n).(*types.PkgName).Imported() description = fmt.Sprintf("reference to package %q", pkg.Path()) } default: // Unreachable? return nil, fmt.Errorf("unexpected AST for package: %T", n) } var members []*describeMember // NB: "unsafe" has no types.Package if pkg != nil { // Enumerate the accessible package members // in lexicographic order. for _, name := range pkg.Scope().Names() { if pkg == qpos.info.Pkg || ast.IsExported(name) { mem := pkg.Scope().Lookup(name) var methods []*types.Selection if mem, ok := mem.(*types.TypeName); ok { methods = accessibleMethods(mem.Type(), qpos.info.Pkg) } members = append(members, &describeMember{ mem, methods, }) } } } return &describePackageResult{qpos.fset, path[0], description, pkg, members}, nil } type describePackageResult struct { fset *token.FileSet node ast.Node description string pkg *types.Package members []*describeMember // in lexicographic name order } type describeMember struct { obj types.Object methods []*types.Selection // in types.MethodSet order } func (r *describePackageResult) PrintPlain(printf printfFunc) { printf(r.node, "%s", r.description) // Compute max width of name "column". maxname := 0 for _, mem := range r.members { if l := len(mem.obj.Name()); l > maxname { maxname = l } } for _, mem := range r.members { printf(mem.obj, "\t%s", formatMember(mem.obj, maxname)) for _, meth := range mem.methods { printf(meth.Obj(), "\t\t%s", types.SelectionString(meth, types.RelativeTo(r.pkg))) } } } func formatMember(obj types.Object, maxname int) string { qualifier := types.RelativeTo(obj.Pkg()) var buf bytes.Buffer fmt.Fprintf(&buf, "%-5s %-*s", tokenOf(obj), maxname, obj.Name()) switch obj := obj.(type) { case *types.Const: fmt.Fprintf(&buf, " %s = %s", types.TypeString(obj.Type(), qualifier), obj.Val()) case *types.Func: fmt.Fprintf(&buf, " %s", types.TypeString(obj.Type(), qualifier)) case *types.TypeName: typ := obj.Type() if isAlias(obj) { buf.WriteString(" = ") } else { buf.WriteByte(' ') typ = typ.Underlying() } var typestr string // Abbreviate long aggregate type names. switch typ := typ.(type) { case *types.Interface: if typ.NumMethods() > 1 { typestr = "interface{...}" } case *types.Struct: if typ.NumFields() > 1 { typestr = "struct{...}" } } if typestr == "" { typestr = types.TypeString(typ, qualifier) } buf.WriteString(typestr) case *types.Var: fmt.Fprintf(&buf, " %s", types.TypeString(obj.Type(), qualifier)) } return buf.String() } func (r *describePackageResult) JSON(fset *token.FileSet) []byte { var members []*serial.DescribeMember for _, mem := range r.members { obj := mem.obj typ := obj.Type() var val string var alias string switch obj := obj.(type) { case *types.Const: val = obj.Val().String() case *types.TypeName: if isAlias(obj) { alias = "= " // kludgy } else { typ = typ.Underlying() } } members = append(members, &serial.DescribeMember{ Name: obj.Name(), Type: alias + typ.String(), Value: val, Pos: fset.Position(obj.Pos()).String(), Kind: tokenOf(obj), Methods: methodsToSerial(r.pkg, mem.methods, fset), }) } return toJSON(&serial.Describe{ Desc: r.description, Pos: fset.Position(r.node.Pos()).String(), Detail: "package", Package: &serial.DescribePackage{ Path: r.pkg.Path(), Members: members, }, }) } func tokenOf(o types.Object) string { switch o.(type) { case *types.Func: return "func" case *types.Var: return "var" case *types.TypeName: return "type" case *types.Const: return "const" case *types.PkgName: return "package" case *types.Builtin: return "builtin" // e.g. when describing package "unsafe" case *types.Nil: return "nil" case *types.Label: return "label" } panic(o) } // ---- STATEMENT ------------------------------------------------------------ func describeStmt(qpos *queryPos, path []ast.Node) (*describeStmtResult, error) { var description string switch n := path[0].(type) { case *ast.Ident: if qpos.info.Defs[n] != nil { description = "labelled statement" } else { description = "reference to labelled statement" } default: // Nothing much to say about statements. description = astutil.NodeDescription(n) } return &describeStmtResult{qpos.fset, path[0], description}, nil } type describeStmtResult struct { fset *token.FileSet node ast.Node description string } func (r *describeStmtResult) PrintPlain(printf printfFunc) { printf(r.node, "%s", r.description) } func (r *describeStmtResult) JSON(fset *token.FileSet) []byte { return toJSON(&serial.Describe{ Desc: r.description, Pos: fset.Position(r.node.Pos()).String(), Detail: "unknown", }) } // ------------------- Utilities ------------------- // pathToString returns a string containing the concrete types of the // nodes in path. func pathToString(path []ast.Node) string { var buf bytes.Buffer fmt.Fprint(&buf, "[") for i, n := range path { if i > 0 { fmt.Fprint(&buf, " ") } fmt.Fprint(&buf, strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.")) } fmt.Fprint(&buf, "]") return buf.String() } func accessibleMethods(t types.Type, from *types.Package) []*types.Selection { var methods []*types.Selection for _, meth := range typeutil.IntuitiveMethodSet(t, nil) { if isAccessibleFrom(meth.Obj(), from) { methods = append(methods, meth) } } return methods } // accessibleFields returns the set of accessible // field selections on a value of type recv. func accessibleFields(recv types.Type, from *types.Package) []describeField { wantField := func(f *types.Var) bool { if !isAccessibleFrom(f, from) { return false } // Check that the field is not shadowed. obj, _, _ := types.LookupFieldOrMethod(recv, true, f.Pkg(), f.Name()) return obj == f } var fields []describeField var visit func(t types.Type, stack []*types.Named) visit = func(t types.Type, stack []*types.Named) { tStruct, ok := deref(t).Underlying().(*types.Struct) if !ok { return } fieldloop: for i := 0; i < tStruct.NumFields(); i++ { f := tStruct.Field(i) // Handle recursion through anonymous fields. if f.Anonymous() { tf := f.Type() if ptr, ok := tf.(*types.Pointer); ok { tf = ptr.Elem() } if named, ok := tf.(*types.Named); ok { // (be defensive) // If we've already visited this named type // on this path, break the cycle. for _, x := range stack { if x == named { continue fieldloop } } visit(f.Type(), append(stack, named)) } } // Save accessible fields. if wantField(f) { fields = append(fields, describeField{ implicits: append([]*types.Named(nil), stack...), field: f, }) } } } visit(recv, nil) return fields } func isAccessibleFrom(obj types.Object, pkg *types.Package) bool { return ast.IsExported(obj.Name()) || obj.Pkg() == pkg } func methodsToSerial(this *types.Package, methods []*types.Selection, fset *token.FileSet) []serial.DescribeMethod { qualifier := types.RelativeTo(this) var jmethods []serial.DescribeMethod for _, meth := range methods { var ser serial.DescribeMethod if meth != nil { // may contain nils when called by implements (on a method) ser = serial.DescribeMethod{ Name: types.SelectionString(meth, qualifier), Pos: fset.Position(meth.Obj().Pos()).String(), } } jmethods = append(jmethods, ser) } return jmethods }