github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/oracle/describe15.go (about) 1 // Copyright 2013 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // +build go1.5,!go1.6 6 7 package oracle 8 9 import ( 10 "bytes" 11 "fmt" 12 "go/ast" 13 exact "go/constant" 14 "go/token" 15 "go/types" 16 "log" 17 "os" 18 "strings" 19 20 "golang.org/x/tools/go/ast/astutil" 21 "golang.org/x/tools/go/loader" 22 "golang.org/x/tools/go/types/typeutil" 23 "golang.org/x/tools/oracle/serial" 24 ) 25 26 // describe describes the syntax node denoted by the query position, 27 // including: 28 // - its syntactic category 29 // - the definition of its referent (for identifiers) [now redundant] 30 // - its type and method set (for an expression or type expression) 31 // 32 func describe(q *Query) error { 33 lconf := loader.Config{Build: q.Build} 34 allowErrors(&lconf) 35 36 if _, err := importQueryPackage(q.Pos, &lconf); err != nil { 37 return err 38 } 39 40 // Load/parse/type-check the program. 41 lprog, err := lconf.Load() 42 if err != nil { 43 return err 44 } 45 q.Fset = lprog.Fset 46 47 qpos, err := parseQueryPos(lprog, q.Pos, true) // (need exact pos) 48 if err != nil { 49 return err 50 } 51 52 if false { // debugging 53 fprintf(os.Stderr, lprog.Fset, qpos.path[0], "you selected: %s %s", 54 astutil.NodeDescription(qpos.path[0]), pathToString(qpos.path)) 55 } 56 57 path, action := findInterestingNode(qpos.info, qpos.path) 58 switch action { 59 case actionExpr: 60 q.result, err = describeValue(qpos, path) 61 62 case actionType: 63 q.result, err = describeType(qpos, path) 64 65 case actionPackage: 66 q.result, err = describePackage(qpos, path) 67 68 case actionStmt: 69 q.result, err = describeStmt(qpos, path) 70 71 case actionUnknown: 72 q.result = &describeUnknownResult{path[0]} 73 74 default: 75 panic(action) // unreachable 76 } 77 return err 78 } 79 80 type describeUnknownResult struct { 81 node ast.Node 82 } 83 84 func (r *describeUnknownResult) display(printf printfFunc) { 85 // Nothing much to say about misc syntax. 86 printf(r.node, "%s", astutil.NodeDescription(r.node)) 87 } 88 89 func (r *describeUnknownResult) toSerial(res *serial.Result, fset *token.FileSet) { 90 res.Describe = &serial.Describe{ 91 Desc: astutil.NodeDescription(r.node), 92 Pos: fset.Position(r.node.Pos()).String(), 93 } 94 } 95 96 type action int 97 98 const ( 99 actionUnknown action = iota // None of the below 100 actionExpr // FuncDecl, true Expr or Ident(types.{Const,Var}) 101 actionType // type Expr or Ident(types.TypeName). 102 actionStmt // Stmt or Ident(types.Label) 103 actionPackage // Ident(types.Package) or ImportSpec 104 ) 105 106 // findInterestingNode classifies the syntax node denoted by path as one of: 107 // - an expression, part of an expression or a reference to a constant 108 // or variable; 109 // - a type, part of a type, or a reference to a named type; 110 // - a statement, part of a statement, or a label referring to a statement; 111 // - part of a package declaration or import spec. 112 // - none of the above. 113 // and returns the most "interesting" associated node, which may be 114 // the same node, an ancestor or a descendent. 115 // 116 func findInterestingNode(pkginfo *loader.PackageInfo, path []ast.Node) ([]ast.Node, action) { 117 // TODO(adonovan): integrate with go/types/stdlib_test.go and 118 // apply this to every AST node we can find to make sure it 119 // doesn't crash. 120 121 // TODO(adonovan): audit for ParenExpr safety, esp. since we 122 // traverse up and down. 123 124 // TODO(adonovan): if the users selects the "." in 125 // "fmt.Fprintf()", they'll get an ambiguous selection error; 126 // we won't even reach here. Can we do better? 127 128 // TODO(adonovan): describing a field within 'type T struct {...}' 129 // describes the (anonymous) struct type and concludes "no methods". 130 // We should ascend to the enclosing type decl, if any. 131 132 for len(path) > 0 { 133 switch n := path[0].(type) { 134 case *ast.GenDecl: 135 if len(n.Specs) == 1 { 136 // Descend to sole {Import,Type,Value}Spec child. 137 path = append([]ast.Node{n.Specs[0]}, path...) 138 continue 139 } 140 return path, actionUnknown // uninteresting 141 142 case *ast.FuncDecl: 143 // Descend to function name. 144 path = append([]ast.Node{n.Name}, path...) 145 continue 146 147 case *ast.ImportSpec: 148 return path, actionPackage 149 150 case *ast.ValueSpec: 151 if len(n.Names) == 1 { 152 // Descend to sole Ident child. 153 path = append([]ast.Node{n.Names[0]}, path...) 154 continue 155 } 156 return path, actionUnknown // uninteresting 157 158 case *ast.TypeSpec: 159 // Descend to type name. 160 path = append([]ast.Node{n.Name}, path...) 161 continue 162 163 case ast.Stmt: 164 return path, actionStmt 165 166 case *ast.ArrayType, 167 *ast.StructType, 168 *ast.FuncType, 169 *ast.InterfaceType, 170 *ast.MapType, 171 *ast.ChanType: 172 return path, actionType 173 174 case *ast.Comment, *ast.CommentGroup, *ast.File, *ast.KeyValueExpr, *ast.CommClause: 175 return path, actionUnknown // uninteresting 176 177 case *ast.Ellipsis: 178 // Continue to enclosing node. 179 // e.g. [...]T in ArrayType 180 // f(x...) in CallExpr 181 // f(x...T) in FuncType 182 183 case *ast.Field: 184 // TODO(adonovan): this needs more thought, 185 // since fields can be so many things. 186 if len(n.Names) == 1 { 187 // Descend to sole Ident child. 188 path = append([]ast.Node{n.Names[0]}, path...) 189 continue 190 } 191 // Zero names (e.g. anon field in struct) 192 // or multiple field or param names: 193 // continue to enclosing field list. 194 195 case *ast.FieldList: 196 // Continue to enclosing node: 197 // {Struct,Func,Interface}Type or FuncDecl. 198 199 case *ast.BasicLit: 200 if _, ok := path[1].(*ast.ImportSpec); ok { 201 return path[1:], actionPackage 202 } 203 return path, actionExpr 204 205 case *ast.SelectorExpr: 206 // TODO(adonovan): use Selections info directly. 207 if pkginfo.Uses[n.Sel] == nil { 208 // TODO(adonovan): is this reachable? 209 return path, actionUnknown 210 } 211 // Descend to .Sel child. 212 path = append([]ast.Node{n.Sel}, path...) 213 continue 214 215 case *ast.Ident: 216 switch pkginfo.ObjectOf(n).(type) { 217 case *types.PkgName: 218 return path, actionPackage 219 220 case *types.Const: 221 return path, actionExpr 222 223 case *types.Label: 224 return path, actionStmt 225 226 case *types.TypeName: 227 return path, actionType 228 229 case *types.Var: 230 // For x in 'struct {x T}', return struct type, for now. 231 if _, ok := path[1].(*ast.Field); ok { 232 _ = path[2].(*ast.FieldList) // assertion 233 if _, ok := path[3].(*ast.StructType); ok { 234 return path[3:], actionType 235 } 236 } 237 return path, actionExpr 238 239 case *types.Func: 240 return path, actionExpr 241 242 case *types.Builtin: 243 // For reference to built-in function, return enclosing call. 244 path = path[1:] // ascend to enclosing function call 245 continue 246 247 case *types.Nil: 248 return path, actionExpr 249 } 250 251 // No object. 252 switch path[1].(type) { 253 case *ast.SelectorExpr: 254 // Return enclosing selector expression. 255 return path[1:], actionExpr 256 257 case *ast.Field: 258 // TODO(adonovan): test this. 259 // e.g. all f in: 260 // struct { f, g int } 261 // interface { f() } 262 // func (f T) method(f, g int) (f, g bool) 263 // 264 // switch path[3].(type) { 265 // case *ast.FuncDecl: 266 // case *ast.StructType: 267 // case *ast.InterfaceType: 268 // } 269 // 270 // return path[1:], actionExpr 271 // 272 // Unclear what to do with these. 273 // Struct.Fields -- field 274 // Interface.Methods -- field 275 // FuncType.{Params.Results} -- actionExpr 276 // FuncDecl.Recv -- actionExpr 277 278 case *ast.File: 279 // 'package foo' 280 return path, actionPackage 281 282 case *ast.ImportSpec: 283 // TODO(adonovan): fix: why no package object? go/types bug? 284 return path[1:], actionPackage 285 286 default: 287 // e.g. blank identifier 288 // or y in "switch y := x.(type)" 289 // or code in a _test.go file that's not part of the package. 290 log.Printf("unknown reference %s in %T\n", n, path[1]) 291 return path, actionUnknown 292 } 293 294 case *ast.StarExpr: 295 if pkginfo.Types[n].IsType() { 296 return path, actionType 297 } 298 return path, actionExpr 299 300 case ast.Expr: 301 // All Expr but {BasicLit,Ident,StarExpr} are 302 // "true" expressions that evaluate to a value. 303 return path, actionExpr 304 } 305 306 // Ascend to parent. 307 path = path[1:] 308 } 309 310 return nil, actionUnknown // unreachable 311 } 312 313 func describeValue(qpos *queryPos, path []ast.Node) (*describeValueResult, error) { 314 var expr ast.Expr 315 var obj types.Object 316 switch n := path[0].(type) { 317 case *ast.ValueSpec: 318 // ambiguous ValueSpec containing multiple names 319 return nil, fmt.Errorf("multiple value specification") 320 case *ast.Ident: 321 obj = qpos.info.ObjectOf(n) 322 expr = n 323 case ast.Expr: 324 expr = n 325 default: 326 // TODO(adonovan): is this reachable? 327 return nil, fmt.Errorf("unexpected AST for expr: %T", n) 328 } 329 330 typ := qpos.info.TypeOf(expr) 331 constVal := qpos.info.Types[expr].Value 332 333 return &describeValueResult{ 334 qpos: qpos, 335 expr: expr, 336 typ: typ, 337 constVal: constVal, 338 obj: obj, 339 }, nil 340 } 341 342 type describeValueResult struct { 343 qpos *queryPos 344 expr ast.Expr // query node 345 typ types.Type // type of expression 346 constVal exact.Value // value of expression, if constant 347 obj types.Object // var/func/const object, if expr was Ident 348 } 349 350 func (r *describeValueResult) display(printf printfFunc) { 351 var prefix, suffix string 352 if r.constVal != nil { 353 suffix = fmt.Sprintf(" of constant value %s", constValString(r.constVal)) 354 } 355 switch obj := r.obj.(type) { 356 case *types.Func: 357 if recv := obj.Type().(*types.Signature).Recv(); recv != nil { 358 if _, ok := recv.Type().Underlying().(*types.Interface); ok { 359 prefix = "interface method " 360 } else { 361 prefix = "method " 362 } 363 } 364 } 365 366 // Describe the expression. 367 if r.obj != nil { 368 if r.obj.Pos() == r.expr.Pos() { 369 // defining ident 370 printf(r.expr, "definition of %s%s%s", prefix, r.qpos.objectString(r.obj), suffix) 371 } else { 372 // referring ident 373 printf(r.expr, "reference to %s%s%s", prefix, r.qpos.objectString(r.obj), suffix) 374 if def := r.obj.Pos(); def != token.NoPos { 375 printf(def, "defined here") 376 } 377 } 378 } else { 379 desc := astutil.NodeDescription(r.expr) 380 if suffix != "" { 381 // constant expression 382 printf(r.expr, "%s%s", desc, suffix) 383 } else { 384 // non-constant expression 385 printf(r.expr, "%s of type %s", desc, r.qpos.typeString(r.typ)) 386 } 387 } 388 } 389 390 func (r *describeValueResult) toSerial(res *serial.Result, fset *token.FileSet) { 391 var value, objpos string 392 if r.constVal != nil { 393 value = r.constVal.String() 394 } 395 if r.obj != nil { 396 objpos = fset.Position(r.obj.Pos()).String() 397 } 398 399 res.Describe = &serial.Describe{ 400 Desc: astutil.NodeDescription(r.expr), 401 Pos: fset.Position(r.expr.Pos()).String(), 402 Detail: "value", 403 Value: &serial.DescribeValue{ 404 Type: r.qpos.typeString(r.typ), 405 Value: value, 406 ObjPos: objpos, 407 }, 408 } 409 } 410 411 // ---- TYPE ------------------------------------------------------------ 412 413 func describeType(qpos *queryPos, path []ast.Node) (*describeTypeResult, error) { 414 var description string 415 var t types.Type 416 switch n := path[0].(type) { 417 case *ast.Ident: 418 t = qpos.info.TypeOf(n) 419 switch t := t.(type) { 420 case *types.Basic: 421 description = "reference to built-in " 422 423 case *types.Named: 424 isDef := t.Obj().Pos() == n.Pos() // see caveats at isDef above 425 if isDef { 426 description = "definition of " 427 } else { 428 description = "reference to " 429 } 430 } 431 432 case ast.Expr: 433 t = qpos.info.TypeOf(n) 434 435 default: 436 // Unreachable? 437 return nil, fmt.Errorf("unexpected AST for type: %T", n) 438 } 439 440 description = description + "type " + qpos.typeString(t) 441 442 // Show sizes for structs and named types (it's fairly obvious for others). 443 switch t.(type) { 444 case *types.Named, *types.Struct: 445 szs := types.StdSizes{8, 8} // assume amd64 446 description = fmt.Sprintf("%s (size %d, align %d)", description, 447 szs.Sizeof(t), szs.Alignof(t)) 448 } 449 450 return &describeTypeResult{ 451 qpos: qpos, 452 node: path[0], 453 description: description, 454 typ: t, 455 methods: accessibleMethods(t, qpos.info.Pkg), 456 }, nil 457 } 458 459 type describeTypeResult struct { 460 qpos *queryPos 461 node ast.Node 462 description string 463 typ types.Type 464 methods []*types.Selection 465 } 466 467 func (r *describeTypeResult) display(printf printfFunc) { 468 printf(r.node, "%s", r.description) 469 470 // Show the underlying type for a reference to a named type. 471 if nt, ok := r.typ.(*types.Named); ok && r.node.Pos() != nt.Obj().Pos() { 472 printf(nt.Obj(), "defined as %s", r.qpos.typeString(nt.Underlying())) 473 } 474 475 // Print the method set, if the type kind is capable of bearing methods. 476 switch r.typ.(type) { 477 case *types.Interface, *types.Struct, *types.Named: 478 if len(r.methods) > 0 { 479 printf(r.node, "Method set:") 480 for _, meth := range r.methods { 481 // TODO(adonovan): print these relative 482 // to the owning package, not the 483 // query package. 484 printf(meth.Obj(), "\t%s", r.qpos.selectionString(meth)) 485 } 486 } else { 487 printf(r.node, "No methods.") 488 } 489 } 490 } 491 492 func (r *describeTypeResult) toSerial(res *serial.Result, fset *token.FileSet) { 493 var namePos, nameDef string 494 if nt, ok := r.typ.(*types.Named); ok { 495 namePos = fset.Position(nt.Obj().Pos()).String() 496 nameDef = nt.Underlying().String() 497 } 498 res.Describe = &serial.Describe{ 499 Desc: r.description, 500 Pos: fset.Position(r.node.Pos()).String(), 501 Detail: "type", 502 Type: &serial.DescribeType{ 503 Type: r.qpos.typeString(r.typ), 504 NamePos: namePos, 505 NameDef: nameDef, 506 Methods: methodsToSerial(r.qpos.info.Pkg, r.methods, fset), 507 }, 508 } 509 } 510 511 // ---- PACKAGE ------------------------------------------------------------ 512 513 func describePackage(qpos *queryPos, path []ast.Node) (*describePackageResult, error) { 514 var description string 515 var pkg *types.Package 516 switch n := path[0].(type) { 517 case *ast.ImportSpec: 518 var obj types.Object 519 if n.Name != nil { 520 obj = qpos.info.Defs[n.Name] 521 } else { 522 obj = qpos.info.Implicits[n] 523 } 524 pkgname, _ := obj.(*types.PkgName) 525 if pkgname == nil { 526 return nil, fmt.Errorf("can't import package %s", n.Path.Value) 527 } 528 pkg = pkgname.Imported() 529 description = fmt.Sprintf("import of package %q", pkg.Path()) 530 531 case *ast.Ident: 532 if _, isDef := path[1].(*ast.File); isDef { 533 // e.g. package id 534 pkg = qpos.info.Pkg 535 description = fmt.Sprintf("definition of package %q", pkg.Path()) 536 } else { 537 // e.g. import id "..." 538 // or id.F() 539 pkg = qpos.info.ObjectOf(n).(*types.PkgName).Imported() 540 description = fmt.Sprintf("reference to package %q", pkg.Path()) 541 } 542 543 default: 544 // Unreachable? 545 return nil, fmt.Errorf("unexpected AST for package: %T", n) 546 } 547 548 var members []*describeMember 549 // NB: "unsafe" has no types.Package 550 if pkg != nil { 551 // Enumerate the accessible package members 552 // in lexicographic order. 553 for _, name := range pkg.Scope().Names() { 554 if pkg == qpos.info.Pkg || ast.IsExported(name) { 555 mem := pkg.Scope().Lookup(name) 556 var methods []*types.Selection 557 if mem, ok := mem.(*types.TypeName); ok { 558 methods = accessibleMethods(mem.Type(), qpos.info.Pkg) 559 } 560 members = append(members, &describeMember{ 561 mem, 562 methods, 563 }) 564 565 } 566 } 567 } 568 569 return &describePackageResult{qpos.fset, path[0], description, pkg, members}, nil 570 } 571 572 type describePackageResult struct { 573 fset *token.FileSet 574 node ast.Node 575 description string 576 pkg *types.Package 577 members []*describeMember // in lexicographic name order 578 } 579 580 type describeMember struct { 581 obj types.Object 582 methods []*types.Selection // in types.MethodSet order 583 } 584 585 func (r *describePackageResult) display(printf printfFunc) { 586 printf(r.node, "%s", r.description) 587 588 // Compute max width of name "column". 589 maxname := 0 590 for _, mem := range r.members { 591 if l := len(mem.obj.Name()); l > maxname { 592 maxname = l 593 } 594 } 595 596 for _, mem := range r.members { 597 printf(mem.obj, "\t%s", formatMember(mem.obj, maxname)) 598 for _, meth := range mem.methods { 599 printf(meth.Obj(), "\t\t%s", types.SelectionString(meth, types.RelativeTo(r.pkg))) 600 } 601 } 602 } 603 604 func formatMember(obj types.Object, maxname int) string { 605 qualifier := types.RelativeTo(obj.Pkg()) 606 var buf bytes.Buffer 607 fmt.Fprintf(&buf, "%-5s %-*s", tokenOf(obj), maxname, obj.Name()) 608 switch obj := obj.(type) { 609 case *types.Const: 610 fmt.Fprintf(&buf, " %s = %s", types.TypeString(obj.Type(), qualifier), constValString(obj.Val())) 611 612 case *types.Func: 613 fmt.Fprintf(&buf, " %s", types.TypeString(obj.Type(), qualifier)) 614 615 case *types.TypeName: 616 // Abbreviate long aggregate type names. 617 var abbrev string 618 switch t := obj.Type().Underlying().(type) { 619 case *types.Interface: 620 if t.NumMethods() > 1 { 621 abbrev = "interface{...}" 622 } 623 case *types.Struct: 624 if t.NumFields() > 1 { 625 abbrev = "struct{...}" 626 } 627 } 628 if abbrev == "" { 629 fmt.Fprintf(&buf, " %s", types.TypeString(obj.Type().Underlying(), qualifier)) 630 } else { 631 fmt.Fprintf(&buf, " %s", abbrev) 632 } 633 634 case *types.Var: 635 fmt.Fprintf(&buf, " %s", types.TypeString(obj.Type(), qualifier)) 636 } 637 return buf.String() 638 } 639 640 func (r *describePackageResult) toSerial(res *serial.Result, fset *token.FileSet) { 641 var members []*serial.DescribeMember 642 for _, mem := range r.members { 643 typ := mem.obj.Type() 644 var val string 645 switch mem := mem.obj.(type) { 646 case *types.Const: 647 val = constValString(mem.Val()) 648 case *types.TypeName: 649 typ = typ.Underlying() 650 } 651 members = append(members, &serial.DescribeMember{ 652 Name: mem.obj.Name(), 653 Type: typ.String(), 654 Value: val, 655 Pos: fset.Position(mem.obj.Pos()).String(), 656 Kind: tokenOf(mem.obj), 657 Methods: methodsToSerial(r.pkg, mem.methods, fset), 658 }) 659 } 660 res.Describe = &serial.Describe{ 661 Desc: r.description, 662 Pos: fset.Position(r.node.Pos()).String(), 663 Detail: "package", 664 Package: &serial.DescribePackage{ 665 Path: r.pkg.Path(), 666 Members: members, 667 }, 668 } 669 } 670 671 func tokenOf(o types.Object) string { 672 switch o.(type) { 673 case *types.Func: 674 return "func" 675 case *types.Var: 676 return "var" 677 case *types.TypeName: 678 return "type" 679 case *types.Const: 680 return "const" 681 case *types.PkgName: 682 return "package" 683 } 684 panic(o) 685 } 686 687 // ---- STATEMENT ------------------------------------------------------------ 688 689 func describeStmt(qpos *queryPos, path []ast.Node) (*describeStmtResult, error) { 690 var description string 691 switch n := path[0].(type) { 692 case *ast.Ident: 693 if qpos.info.Defs[n] != nil { 694 description = "labelled statement" 695 } else { 696 description = "reference to labelled statement" 697 } 698 699 default: 700 // Nothing much to say about statements. 701 description = astutil.NodeDescription(n) 702 } 703 return &describeStmtResult{qpos.fset, path[0], description}, nil 704 } 705 706 type describeStmtResult struct { 707 fset *token.FileSet 708 node ast.Node 709 description string 710 } 711 712 func (r *describeStmtResult) display(printf printfFunc) { 713 printf(r.node, "%s", r.description) 714 } 715 716 func (r *describeStmtResult) toSerial(res *serial.Result, fset *token.FileSet) { 717 res.Describe = &serial.Describe{ 718 Desc: r.description, 719 Pos: fset.Position(r.node.Pos()).String(), 720 Detail: "unknown", 721 } 722 } 723 724 // ------------------- Utilities ------------------- 725 726 // pathToString returns a string containing the concrete types of the 727 // nodes in path. 728 func pathToString(path []ast.Node) string { 729 var buf bytes.Buffer 730 fmt.Fprint(&buf, "[") 731 for i, n := range path { 732 if i > 0 { 733 fmt.Fprint(&buf, " ") 734 } 735 fmt.Fprint(&buf, strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.")) 736 } 737 fmt.Fprint(&buf, "]") 738 return buf.String() 739 } 740 741 func accessibleMethods(t types.Type, from *types.Package) []*types.Selection { 742 var methods []*types.Selection 743 for _, meth := range typeutil.IntuitiveMethodSet(t, nil) { 744 if isAccessibleFrom(meth.Obj(), from) { 745 methods = append(methods, meth) 746 } 747 } 748 return methods 749 } 750 751 func isAccessibleFrom(obj types.Object, pkg *types.Package) bool { 752 return ast.IsExported(obj.Name()) || obj.Pkg() == pkg 753 } 754 755 func methodsToSerial(this *types.Package, methods []*types.Selection, fset *token.FileSet) []serial.DescribeMethod { 756 qualifier := types.RelativeTo(this) 757 var jmethods []serial.DescribeMethod 758 for _, meth := range methods { 759 var ser serial.DescribeMethod 760 if meth != nil { // may contain nils when called by implements (on a method) 761 ser = serial.DescribeMethod{ 762 Name: types.SelectionString(meth, qualifier), 763 Pos: fset.Position(meth.Obj().Pos()).String(), 764 } 765 } 766 jmethods = append(jmethods, ser) 767 } 768 return jmethods 769 } 770 771 // constValString emulates Go 1.6's go/constant.ExactString well enough 772 // to make the tests pass. This is just a stopgap until we throw away 773 // all the *15.go files. 774 func constValString(v exact.Value) string { 775 if v.Kind() == exact.Float { 776 f, _ := exact.Float64Val(v) 777 return fmt.Sprintf("%g", f) 778 } 779 return v.String() 780 }