github.com/ltltlt/go-source-code@v0.0.0-20190830023027-95be009773aa/cmd/fix/typecheck.go (about) 1 // Copyright 2011 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 package main 6 7 import ( 8 "fmt" 9 "go/ast" 10 "go/parser" 11 "go/token" 12 "io/ioutil" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "reflect" 17 "runtime" 18 "strings" 19 ) 20 21 // Partial type checker. 22 // 23 // The fact that it is partial is very important: the input is 24 // an AST and a description of some type information to 25 // assume about one or more packages, but not all the 26 // packages that the program imports. The checker is 27 // expected to do as much as it can with what it has been 28 // given. There is not enough information supplied to do 29 // a full type check, but the type checker is expected to 30 // apply information that can be derived from variable 31 // declarations, function and method returns, and type switches 32 // as far as it can, so that the caller can still tell the types 33 // of expression relevant to a particular fix. 34 // 35 // TODO(rsc,gri): Replace with go/typechecker. 36 // Doing that could be an interesting test case for go/typechecker: 37 // the constraints about working with partial information will 38 // likely exercise it in interesting ways. The ideal interface would 39 // be to pass typecheck a map from importpath to package API text 40 // (Go source code), but for now we use data structures (TypeConfig, Type). 41 // 42 // The strings mostly use gofmt form. 43 // 44 // A Field or FieldList has as its type a comma-separated list 45 // of the types of the fields. For example, the field list 46 // x, y, z int 47 // has type "int, int, int". 48 49 // The prefix "type " is the type of a type. 50 // For example, given 51 // var x int 52 // type T int 53 // x's type is "int" but T's type is "type int". 54 // mkType inserts the "type " prefix. 55 // getType removes it. 56 // isType tests for it. 57 58 func mkType(t string) string { 59 return "type " + t 60 } 61 62 func getType(t string) string { 63 if !isType(t) { 64 return "" 65 } 66 return t[len("type "):] 67 } 68 69 func isType(t string) bool { 70 return strings.HasPrefix(t, "type ") 71 } 72 73 // TypeConfig describes the universe of relevant types. 74 // For ease of creation, the types are all referred to by string 75 // name (e.g., "reflect.Value"). TypeByName is the only place 76 // where the strings are resolved. 77 78 type TypeConfig struct { 79 Type map[string]*Type 80 Var map[string]string 81 Func map[string]string 82 83 // External maps from a name to its type. 84 // It provides additional typings not present in the Go source itself. 85 // For now, the only additional typings are those generated by cgo. 86 External map[string]string 87 } 88 89 // typeof returns the type of the given name, which may be of 90 // the form "x" or "p.X". 91 func (cfg *TypeConfig) typeof(name string) string { 92 if cfg.Var != nil { 93 if t := cfg.Var[name]; t != "" { 94 return t 95 } 96 } 97 if cfg.Func != nil { 98 if t := cfg.Func[name]; t != "" { 99 return "func()" + t 100 } 101 } 102 return "" 103 } 104 105 // Type describes the Fields and Methods of a type. 106 // If the field or method cannot be found there, it is next 107 // looked for in the Embed list. 108 type Type struct { 109 Field map[string]string // map field name to type 110 Method map[string]string // map method name to comma-separated return types (should start with "func ") 111 Embed []string // list of types this type embeds (for extra methods) 112 Def string // definition of named type 113 } 114 115 // dot returns the type of "typ.name", making its decision 116 // using the type information in cfg. 117 func (typ *Type) dot(cfg *TypeConfig, name string) string { 118 if typ.Field != nil { 119 if t := typ.Field[name]; t != "" { 120 return t 121 } 122 } 123 if typ.Method != nil { 124 if t := typ.Method[name]; t != "" { 125 return t 126 } 127 } 128 129 for _, e := range typ.Embed { 130 etyp := cfg.Type[e] 131 if etyp != nil { 132 if t := etyp.dot(cfg, name); t != "" { 133 return t 134 } 135 } 136 } 137 138 return "" 139 } 140 141 // typecheck type checks the AST f assuming the information in cfg. 142 // It returns two maps with type information: 143 // typeof maps AST nodes to type information in gofmt string form. 144 // assign maps type strings to lists of expressions that were assigned 145 // to values of another type that were assigned to that type. 146 func typecheck(cfg *TypeConfig, f *ast.File) (typeof map[interface{}]string, assign map[string][]interface{}) { 147 typeof = make(map[interface{}]string) 148 assign = make(map[string][]interface{}) 149 cfg1 := &TypeConfig{} 150 *cfg1 = *cfg // make copy so we can add locally 151 copied := false 152 153 // If we import "C", add types of cgo objects. 154 cfg.External = map[string]string{} 155 cfg1.External = cfg.External 156 if imports(f, "C") { 157 // Run cgo on gofmtFile(f) 158 // Parse, extract decls from _cgo_gotypes.go 159 // Map _Ctype_* types to C.* types. 160 err := func() error { 161 txt, err := gofmtFile(f) 162 if err != nil { 163 return err 164 } 165 dir, err := ioutil.TempDir(os.TempDir(), "fix_cgo_typecheck") 166 if err != nil { 167 return err 168 } 169 defer os.RemoveAll(dir) 170 err = ioutil.WriteFile(filepath.Join(dir, "in.go"), txt, 0600) 171 if err != nil { 172 return err 173 } 174 cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), "tool", "cgo", "-objdir", dir, "-srcdir", dir, "in.go") 175 err = cmd.Run() 176 if err != nil { 177 return err 178 } 179 out, err := ioutil.ReadFile(filepath.Join(dir, "_cgo_gotypes.go")) 180 if err != nil { 181 return err 182 } 183 cgo, err := parser.ParseFile(token.NewFileSet(), "cgo.go", out, 0) 184 if err != nil { 185 return err 186 } 187 for _, decl := range cgo.Decls { 188 fn, ok := decl.(*ast.FuncDecl) 189 if !ok { 190 continue 191 } 192 if strings.HasPrefix(fn.Name.Name, "_Cfunc_") { 193 var params, results []string 194 for _, p := range fn.Type.Params.List { 195 t := gofmt(p.Type) 196 t = strings.Replace(t, "_Ctype_", "C.", -1) 197 params = append(params, t) 198 } 199 for _, r := range fn.Type.Results.List { 200 t := gofmt(r.Type) 201 t = strings.Replace(t, "_Ctype_", "C.", -1) 202 results = append(results, t) 203 } 204 cfg.External["C."+fn.Name.Name[7:]] = joinFunc(params, results) 205 } 206 } 207 return nil 208 }() 209 if err != nil { 210 fmt.Printf("warning: no cgo types: %s\n", err) 211 } 212 } 213 214 // gather function declarations 215 for _, decl := range f.Decls { 216 fn, ok := decl.(*ast.FuncDecl) 217 if !ok { 218 continue 219 } 220 typecheck1(cfg, fn.Type, typeof, assign) 221 t := typeof[fn.Type] 222 if fn.Recv != nil { 223 // The receiver must be a type. 224 rcvr := typeof[fn.Recv] 225 if !isType(rcvr) { 226 if len(fn.Recv.List) != 1 { 227 continue 228 } 229 rcvr = mkType(gofmt(fn.Recv.List[0].Type)) 230 typeof[fn.Recv.List[0].Type] = rcvr 231 } 232 rcvr = getType(rcvr) 233 if rcvr != "" && rcvr[0] == '*' { 234 rcvr = rcvr[1:] 235 } 236 typeof[rcvr+"."+fn.Name.Name] = t 237 } else { 238 if isType(t) { 239 t = getType(t) 240 } else { 241 t = gofmt(fn.Type) 242 } 243 typeof[fn.Name] = t 244 245 // Record typeof[fn.Name.Obj] for future references to fn.Name. 246 typeof[fn.Name.Obj] = t 247 } 248 } 249 250 // gather struct declarations 251 for _, decl := range f.Decls { 252 d, ok := decl.(*ast.GenDecl) 253 if ok { 254 for _, s := range d.Specs { 255 switch s := s.(type) { 256 case *ast.TypeSpec: 257 if cfg1.Type[s.Name.Name] != nil { 258 break 259 } 260 if !copied { 261 copied = true 262 // Copy map lazily: it's time. 263 cfg1.Type = make(map[string]*Type) 264 for k, v := range cfg.Type { 265 cfg1.Type[k] = v 266 } 267 } 268 t := &Type{Field: map[string]string{}} 269 cfg1.Type[s.Name.Name] = t 270 switch st := s.Type.(type) { 271 case *ast.StructType: 272 for _, f := range st.Fields.List { 273 for _, n := range f.Names { 274 t.Field[n.Name] = gofmt(f.Type) 275 } 276 } 277 case *ast.ArrayType, *ast.StarExpr, *ast.MapType: 278 t.Def = gofmt(st) 279 } 280 } 281 } 282 } 283 } 284 285 typecheck1(cfg1, f, typeof, assign) 286 return typeof, assign 287 } 288 289 func makeExprList(a []*ast.Ident) []ast.Expr { 290 var b []ast.Expr 291 for _, x := range a { 292 b = append(b, x) 293 } 294 return b 295 } 296 297 // Typecheck1 is the recursive form of typecheck. 298 // It is like typecheck but adds to the information in typeof 299 // instead of allocating a new map. 300 func typecheck1(cfg *TypeConfig, f interface{}, typeof map[interface{}]string, assign map[string][]interface{}) { 301 // set sets the type of n to typ. 302 // If isDecl is true, n is being declared. 303 set := func(n ast.Expr, typ string, isDecl bool) { 304 if typeof[n] != "" || typ == "" { 305 if typeof[n] != typ { 306 assign[typ] = append(assign[typ], n) 307 } 308 return 309 } 310 typeof[n] = typ 311 312 // If we obtained typ from the declaration of x 313 // propagate the type to all the uses. 314 // The !isDecl case is a cheat here, but it makes 315 // up in some cases for not paying attention to 316 // struct fields. The real type checker will be 317 // more accurate so we won't need the cheat. 318 if id, ok := n.(*ast.Ident); ok && id.Obj != nil && (isDecl || typeof[id.Obj] == "") { 319 typeof[id.Obj] = typ 320 } 321 } 322 323 // Type-check an assignment lhs = rhs. 324 // If isDecl is true, this is := so we can update 325 // the types of the objects that lhs refers to. 326 typecheckAssign := func(lhs, rhs []ast.Expr, isDecl bool) { 327 if len(lhs) > 1 && len(rhs) == 1 { 328 if _, ok := rhs[0].(*ast.CallExpr); ok { 329 t := split(typeof[rhs[0]]) 330 // Lists should have same length but may not; pair what can be paired. 331 for i := 0; i < len(lhs) && i < len(t); i++ { 332 set(lhs[i], t[i], isDecl) 333 } 334 return 335 } 336 } 337 if len(lhs) == 1 && len(rhs) == 2 { 338 // x = y, ok 339 rhs = rhs[:1] 340 } else if len(lhs) == 2 && len(rhs) == 1 { 341 // x, ok = y 342 lhs = lhs[:1] 343 } 344 345 // Match as much as we can. 346 for i := 0; i < len(lhs) && i < len(rhs); i++ { 347 x, y := lhs[i], rhs[i] 348 if typeof[y] != "" { 349 set(x, typeof[y], isDecl) 350 } else { 351 set(y, typeof[x], false) 352 } 353 } 354 } 355 356 expand := func(s string) string { 357 typ := cfg.Type[s] 358 if typ != nil && typ.Def != "" { 359 return typ.Def 360 } 361 return s 362 } 363 364 // The main type check is a recursive algorithm implemented 365 // by walkBeforeAfter(n, before, after). 366 // Most of it is bottom-up, but in a few places we need 367 // to know the type of the function we are checking. 368 // The before function records that information on 369 // the curfn stack. 370 var curfn []*ast.FuncType 371 372 before := func(n interface{}) { 373 // push function type on stack 374 switch n := n.(type) { 375 case *ast.FuncDecl: 376 curfn = append(curfn, n.Type) 377 case *ast.FuncLit: 378 curfn = append(curfn, n.Type) 379 } 380 } 381 382 // After is the real type checker. 383 after := func(n interface{}) { 384 if n == nil { 385 return 386 } 387 if false && reflect.TypeOf(n).Kind() == reflect.Ptr { // debugging trace 388 defer func() { 389 if t := typeof[n]; t != "" { 390 pos := fset.Position(n.(ast.Node).Pos()) 391 fmt.Fprintf(os.Stderr, "%s: typeof[%s] = %s\n", pos, gofmt(n), t) 392 } 393 }() 394 } 395 396 switch n := n.(type) { 397 case *ast.FuncDecl, *ast.FuncLit: 398 // pop function type off stack 399 curfn = curfn[:len(curfn)-1] 400 401 case *ast.FuncType: 402 typeof[n] = mkType(joinFunc(split(typeof[n.Params]), split(typeof[n.Results]))) 403 404 case *ast.FieldList: 405 // Field list is concatenation of sub-lists. 406 t := "" 407 for _, field := range n.List { 408 if t != "" { 409 t += ", " 410 } 411 t += typeof[field] 412 } 413 typeof[n] = t 414 415 case *ast.Field: 416 // Field is one instance of the type per name. 417 all := "" 418 t := typeof[n.Type] 419 if !isType(t) { 420 // Create a type, because it is typically *T or *p.T 421 // and we might care about that type. 422 t = mkType(gofmt(n.Type)) 423 typeof[n.Type] = t 424 } 425 t = getType(t) 426 if len(n.Names) == 0 { 427 all = t 428 } else { 429 for _, id := range n.Names { 430 if all != "" { 431 all += ", " 432 } 433 all += t 434 typeof[id.Obj] = t 435 typeof[id] = t 436 } 437 } 438 typeof[n] = all 439 440 case *ast.ValueSpec: 441 // var declaration. Use type if present. 442 if n.Type != nil { 443 t := typeof[n.Type] 444 if !isType(t) { 445 t = mkType(gofmt(n.Type)) 446 typeof[n.Type] = t 447 } 448 t = getType(t) 449 for _, id := range n.Names { 450 set(id, t, true) 451 } 452 } 453 // Now treat same as assignment. 454 typecheckAssign(makeExprList(n.Names), n.Values, true) 455 456 case *ast.AssignStmt: 457 typecheckAssign(n.Lhs, n.Rhs, n.Tok == token.DEFINE) 458 459 case *ast.Ident: 460 // Identifier can take its type from underlying object. 461 if t := typeof[n.Obj]; t != "" { 462 typeof[n] = t 463 } 464 465 case *ast.SelectorExpr: 466 // Field or method. 467 name := n.Sel.Name 468 if t := typeof[n.X]; t != "" { 469 t = strings.TrimPrefix(t, "*") // implicit * 470 if typ := cfg.Type[t]; typ != nil { 471 if t := typ.dot(cfg, name); t != "" { 472 typeof[n] = t 473 return 474 } 475 } 476 tt := typeof[t+"."+name] 477 if isType(tt) { 478 typeof[n] = getType(tt) 479 return 480 } 481 } 482 // Package selector. 483 if x, ok := n.X.(*ast.Ident); ok && x.Obj == nil { 484 str := x.Name + "." + name 485 if cfg.Type[str] != nil { 486 typeof[n] = mkType(str) 487 return 488 } 489 if t := cfg.typeof(x.Name + "." + name); t != "" { 490 typeof[n] = t 491 return 492 } 493 } 494 495 case *ast.CallExpr: 496 // make(T) has type T. 497 if isTopName(n.Fun, "make") && len(n.Args) >= 1 { 498 typeof[n] = gofmt(n.Args[0]) 499 return 500 } 501 // new(T) has type *T 502 if isTopName(n.Fun, "new") && len(n.Args) == 1 { 503 typeof[n] = "*" + gofmt(n.Args[0]) 504 return 505 } 506 // Otherwise, use type of function to determine arguments. 507 t := typeof[n.Fun] 508 if t == "" { 509 t = cfg.External[gofmt(n.Fun)] 510 } 511 in, out := splitFunc(t) 512 if in == nil && out == nil { 513 return 514 } 515 typeof[n] = join(out) 516 for i, arg := range n.Args { 517 if i >= len(in) { 518 break 519 } 520 if typeof[arg] == "" { 521 typeof[arg] = in[i] 522 } 523 } 524 525 case *ast.TypeAssertExpr: 526 // x.(type) has type of x. 527 if n.Type == nil { 528 typeof[n] = typeof[n.X] 529 return 530 } 531 // x.(T) has type T. 532 if t := typeof[n.Type]; isType(t) { 533 typeof[n] = getType(t) 534 } else { 535 typeof[n] = gofmt(n.Type) 536 } 537 538 case *ast.SliceExpr: 539 // x[i:j] has type of x. 540 typeof[n] = typeof[n.X] 541 542 case *ast.IndexExpr: 543 // x[i] has key type of x's type. 544 t := expand(typeof[n.X]) 545 if strings.HasPrefix(t, "[") || strings.HasPrefix(t, "map[") { 546 // Lazy: assume there are no nested [] in the array 547 // length or map key type. 548 if i := strings.Index(t, "]"); i >= 0 { 549 typeof[n] = t[i+1:] 550 } 551 } 552 553 case *ast.StarExpr: 554 // *x for x of type *T has type T when x is an expr. 555 // We don't use the result when *x is a type, but 556 // compute it anyway. 557 t := expand(typeof[n.X]) 558 if isType(t) { 559 typeof[n] = "type *" + getType(t) 560 } else if strings.HasPrefix(t, "*") { 561 typeof[n] = t[len("*"):] 562 } 563 564 case *ast.UnaryExpr: 565 // &x for x of type T has type *T. 566 t := typeof[n.X] 567 if t != "" && n.Op == token.AND { 568 typeof[n] = "*" + t 569 } 570 571 case *ast.CompositeLit: 572 // T{...} has type T. 573 typeof[n] = gofmt(n.Type) 574 575 // Propagate types down to values used in the composite literal. 576 t := expand(typeof[n]) 577 if strings.HasPrefix(t, "[") { // array or slice 578 // Lazy: assume there are no nested [] in the array length. 579 if i := strings.Index(t, "]"); i >= 0 { 580 et := t[i+1:] 581 for _, e := range n.Elts { 582 if kv, ok := e.(*ast.KeyValueExpr); ok { 583 e = kv.Value 584 } 585 if typeof[e] == "" { 586 typeof[e] = et 587 } 588 } 589 } 590 } 591 if strings.HasPrefix(t, "map[") { // map 592 // Lazy: assume there are no nested [] in the map key type. 593 if i := strings.Index(t, "]"); i >= 0 { 594 kt, vt := t[4:i], t[i+1:] 595 for _, e := range n.Elts { 596 if kv, ok := e.(*ast.KeyValueExpr); ok { 597 if typeof[kv.Key] == "" { 598 typeof[kv.Key] = kt 599 } 600 if typeof[kv.Value] == "" { 601 typeof[kv.Value] = vt 602 } 603 } 604 } 605 } 606 } 607 if typ := cfg.Type[t]; typ != nil && len(typ.Field) > 0 { // struct 608 for _, e := range n.Elts { 609 if kv, ok := e.(*ast.KeyValueExpr); ok { 610 if ft := typ.Field[fmt.Sprintf("%s", kv.Key)]; ft != "" { 611 if typeof[kv.Value] == "" { 612 typeof[kv.Value] = ft 613 } 614 } 615 } 616 } 617 } 618 619 case *ast.ParenExpr: 620 // (x) has type of x. 621 typeof[n] = typeof[n.X] 622 623 case *ast.RangeStmt: 624 t := expand(typeof[n.X]) 625 if t == "" { 626 return 627 } 628 var key, value string 629 if t == "string" { 630 key, value = "int", "rune" 631 } else if strings.HasPrefix(t, "[") { 632 key = "int" 633 if i := strings.Index(t, "]"); i >= 0 { 634 value = t[i+1:] 635 } 636 } else if strings.HasPrefix(t, "map[") { 637 if i := strings.Index(t, "]"); i >= 0 { 638 key, value = t[4:i], t[i+1:] 639 } 640 } 641 changed := false 642 if n.Key != nil && key != "" { 643 changed = true 644 set(n.Key, key, n.Tok == token.DEFINE) 645 } 646 if n.Value != nil && value != "" { 647 changed = true 648 set(n.Value, value, n.Tok == token.DEFINE) 649 } 650 // Ugly failure of vision: already type-checked body. 651 // Do it again now that we have that type info. 652 if changed { 653 typecheck1(cfg, n.Body, typeof, assign) 654 } 655 656 case *ast.TypeSwitchStmt: 657 // Type of variable changes for each case in type switch, 658 // but go/parser generates just one variable. 659 // Repeat type check for each case with more precise 660 // type information. 661 as, ok := n.Assign.(*ast.AssignStmt) 662 if !ok { 663 return 664 } 665 varx, ok := as.Lhs[0].(*ast.Ident) 666 if !ok { 667 return 668 } 669 t := typeof[varx] 670 for _, cas := range n.Body.List { 671 cas := cas.(*ast.CaseClause) 672 if len(cas.List) == 1 { 673 // Variable has specific type only when there is 674 // exactly one type in the case list. 675 if tt := typeof[cas.List[0]]; isType(tt) { 676 tt = getType(tt) 677 typeof[varx] = tt 678 typeof[varx.Obj] = tt 679 typecheck1(cfg, cas.Body, typeof, assign) 680 } 681 } 682 } 683 // Restore t. 684 typeof[varx] = t 685 typeof[varx.Obj] = t 686 687 case *ast.ReturnStmt: 688 if len(curfn) == 0 { 689 // Probably can't happen. 690 return 691 } 692 f := curfn[len(curfn)-1] 693 res := n.Results 694 if f.Results != nil { 695 t := split(typeof[f.Results]) 696 for i := 0; i < len(res) && i < len(t); i++ { 697 set(res[i], t[i], false) 698 } 699 } 700 701 case *ast.BinaryExpr: 702 // Propagate types across binary ops that require two args of the same type. 703 switch n.Op { 704 case token.EQL, token.NEQ: // TODO: more cases. This is enough for the cftype fix. 705 if typeof[n.X] != "" && typeof[n.Y] == "" { 706 typeof[n.Y] = typeof[n.X] 707 } 708 if typeof[n.X] == "" && typeof[n.Y] != "" { 709 typeof[n.X] = typeof[n.Y] 710 } 711 } 712 } 713 } 714 walkBeforeAfter(f, before, after) 715 } 716 717 // Convert between function type strings and lists of types. 718 // Using strings makes this a little harder, but it makes 719 // a lot of the rest of the code easier. This will all go away 720 // when we can use go/typechecker directly. 721 722 // splitFunc splits "func(x,y,z) (a,b,c)" into ["x", "y", "z"] and ["a", "b", "c"]. 723 func splitFunc(s string) (in, out []string) { 724 if !strings.HasPrefix(s, "func(") { 725 return nil, nil 726 } 727 728 i := len("func(") // index of beginning of 'in' arguments 729 nparen := 0 730 for j := i; j < len(s); j++ { 731 switch s[j] { 732 case '(': 733 nparen++ 734 case ')': 735 nparen-- 736 if nparen < 0 { 737 // found end of parameter list 738 out := strings.TrimSpace(s[j+1:]) 739 if len(out) >= 2 && out[0] == '(' && out[len(out)-1] == ')' { 740 out = out[1 : len(out)-1] 741 } 742 return split(s[i:j]), split(out) 743 } 744 } 745 } 746 return nil, nil 747 } 748 749 // joinFunc is the inverse of splitFunc. 750 func joinFunc(in, out []string) string { 751 outs := "" 752 if len(out) == 1 { 753 outs = " " + out[0] 754 } else if len(out) > 1 { 755 outs = " (" + join(out) + ")" 756 } 757 return "func(" + join(in) + ")" + outs 758 } 759 760 // split splits "int, float" into ["int", "float"] and splits "" into []. 761 func split(s string) []string { 762 out := []string{} 763 i := 0 // current type being scanned is s[i:j]. 764 nparen := 0 765 for j := 0; j < len(s); j++ { 766 switch s[j] { 767 case ' ': 768 if i == j { 769 i++ 770 } 771 case '(': 772 nparen++ 773 case ')': 774 nparen-- 775 if nparen < 0 { 776 // probably can't happen 777 return nil 778 } 779 case ',': 780 if nparen == 0 { 781 if i < j { 782 out = append(out, s[i:j]) 783 } 784 i = j + 1 785 } 786 } 787 } 788 if nparen != 0 { 789 // probably can't happen 790 return nil 791 } 792 if i < len(s) { 793 out = append(out, s[i:]) 794 } 795 return out 796 } 797 798 // join is the inverse of split. 799 func join(x []string) string { 800 return strings.Join(x, ", ") 801 }