gitee.com/lonely0422/gometalinter.git@v3.0.1-0.20190307123442-32416ab75314+incompatible/_linters/src/github.com/mdempsky/unconvert/unconvert.go (about) 1 // Copyright 2015 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 // Unconvert removes redundant type conversions from Go packages. 6 package main 7 8 import ( 9 "bytes" 10 "flag" 11 "fmt" 12 "go/ast" 13 "go/build" 14 "go/format" 15 "go/parser" 16 "go/token" 17 "go/types" 18 "io/ioutil" 19 "log" 20 "os" 21 "reflect" 22 "runtime/pprof" 23 "sort" 24 "sync" 25 "unicode" 26 27 "github.com/kisielk/gotool" 28 "golang.org/x/text/width" 29 "golang.org/x/tools/go/loader" 30 ) 31 32 // Unnecessary conversions are identified by the position 33 // of their left parenthesis within a source file. 34 35 type editSet map[token.Position]struct{} 36 37 type fileToEditSet map[string]editSet 38 39 func apply(file string, edits editSet) { 40 if len(edits) == 0 { 41 return 42 } 43 44 fset := token.NewFileSet() 45 f, err := parser.ParseFile(fset, file, nil, parser.ParseComments) 46 if err != nil { 47 log.Fatal(err) 48 } 49 50 // Note: We modify edits during the walk. 51 v := editor{edits: edits, file: fset.File(f.Package)} 52 ast.Walk(&v, f) 53 if len(edits) != 0 { 54 log.Printf("%s: missing edits %s", file, edits) 55 } 56 57 // TODO(mdempsky): Write to temporary file and rename. 58 var buf bytes.Buffer 59 err = format.Node(&buf, fset, f) 60 if err != nil { 61 log.Fatal(err) 62 } 63 64 err = ioutil.WriteFile(file, buf.Bytes(), 0) 65 if err != nil { 66 log.Fatal(err) 67 } 68 } 69 70 type editor struct { 71 edits editSet 72 file *token.File 73 } 74 75 func (e *editor) Visit(n ast.Node) ast.Visitor { 76 if n == nil { 77 return nil 78 } 79 v := reflect.ValueOf(n).Elem() 80 for i, n := 0, v.NumField(); i < n; i++ { 81 switch f := v.Field(i).Addr().Interface().(type) { 82 case *ast.Expr: 83 e.rewrite(f) 84 case *[]ast.Expr: 85 for i := range *f { 86 e.rewrite(&(*f)[i]) 87 } 88 } 89 } 90 return e 91 } 92 93 func (e *editor) rewrite(f *ast.Expr) { 94 call, ok := (*f).(*ast.CallExpr) 95 if !ok { 96 return 97 } 98 99 pos := e.file.Position(call.Lparen) 100 if _, ok := e.edits[pos]; !ok { 101 return 102 } 103 *f = call.Args[0] 104 delete(e.edits, pos) 105 } 106 107 var ( 108 cr = []byte{'\r'} 109 nl = []byte{'\n'} 110 ) 111 112 func print(conversions []token.Position) { 113 var file string 114 var lines [][]byte 115 116 for _, pos := range conversions { 117 fmt.Printf("%s:%d:%d: unnecessary conversion\n", pos.Filename, pos.Line, pos.Column) 118 if *flagV { 119 if pos.Filename != file { 120 buf, err := ioutil.ReadFile(pos.Filename) 121 if err != nil { 122 log.Fatal(err) 123 } 124 file = pos.Filename 125 lines = bytes.Split(buf, nl) 126 } 127 128 line := bytes.TrimSuffix(lines[pos.Line-1], cr) 129 fmt.Printf("%s\n", line) 130 131 // For files processed by cgo, Column is the 132 // column location after cgo processing, which 133 // may be different than the source column 134 // that we want here. In lieu of a better 135 // heuristic for detecting this case, at least 136 // avoid panicking if column is out of bounds. 137 if pos.Column <= len(line) { 138 fmt.Printf("%s^\n", rub(line[:pos.Column-1])) 139 } 140 } 141 } 142 } 143 144 // Rub returns a copy of buf with all non-whitespace characters replaced 145 // by spaces (like rubbing them out with white out). 146 func rub(buf []byte) []byte { 147 // TODO(mdempsky): Handle combining characters? 148 var res bytes.Buffer 149 for _, r := range string(buf) { 150 if unicode.IsSpace(r) { 151 res.WriteRune(r) 152 continue 153 } 154 switch width.LookupRune(r).Kind() { 155 case width.EastAsianWide, width.EastAsianFullwidth: 156 res.WriteString(" ") 157 default: 158 res.WriteByte(' ') 159 } 160 } 161 return res.Bytes() 162 } 163 164 var ( 165 flagAll = flag.Bool("all", false, "type check all GOOS and GOARCH combinations") 166 flagApply = flag.Bool("apply", false, "apply edits to source files") 167 flagCPUProfile = flag.String("cpuprofile", "", "write CPU profile to file") 168 // TODO(mdempsky): Better description and maybe flag name. 169 flagSafe = flag.Bool("safe", false, "be more conservative (experimental)") 170 flagV = flag.Bool("v", false, "verbose output") 171 ) 172 173 func usage() { 174 fmt.Fprintf(os.Stderr, "usage: unconvert [flags] [package ...]\n") 175 flag.PrintDefaults() 176 } 177 178 func main() { 179 flag.Usage = usage 180 flag.Parse() 181 182 if *flagCPUProfile != "" { 183 f, err := os.Create(*flagCPUProfile) 184 if err != nil { 185 log.Fatal(err) 186 } 187 pprof.StartCPUProfile(f) 188 defer pprof.StopCPUProfile() 189 } 190 191 importPaths := gotool.ImportPaths(flag.Args()) 192 if len(importPaths) == 0 { 193 return 194 } 195 196 var m fileToEditSet 197 if *flagAll { 198 m = mergeEdits(importPaths) 199 } else { 200 m = computeEdits(importPaths, build.Default.GOOS, build.Default.GOARCH, build.Default.CgoEnabled) 201 } 202 203 if *flagApply { 204 var wg sync.WaitGroup 205 for f, e := range m { 206 wg.Add(1) 207 f, e := f, e 208 go func() { 209 defer wg.Done() 210 apply(f, e) 211 }() 212 } 213 wg.Wait() 214 } else { 215 var conversions []token.Position 216 for _, positions := range m { 217 for pos := range positions { 218 conversions = append(conversions, pos) 219 } 220 } 221 sort.Sort(byPosition(conversions)) 222 print(conversions) 223 if len(conversions) > 0 { 224 os.Exit(1) 225 } 226 } 227 } 228 229 var plats = [...]struct { 230 goos, goarch string 231 }{ 232 // TODO(mdempsky): buildall.bash also builds linux-386-387 and linux-arm-arm5. 233 {"android", "386"}, 234 {"android", "amd64"}, 235 {"android", "arm"}, 236 {"android", "arm64"}, 237 {"darwin", "386"}, 238 {"darwin", "amd64"}, 239 {"darwin", "arm"}, 240 {"darwin", "arm64"}, 241 {"dragonfly", "amd64"}, 242 {"freebsd", "386"}, 243 {"freebsd", "amd64"}, 244 {"freebsd", "arm"}, 245 {"linux", "386"}, 246 {"linux", "amd64"}, 247 {"linux", "arm"}, 248 {"linux", "arm64"}, 249 {"linux", "mips64"}, 250 {"linux", "mips64le"}, 251 {"linux", "ppc64"}, 252 {"linux", "ppc64le"}, 253 {"linux", "s390x"}, 254 {"nacl", "386"}, 255 {"nacl", "amd64p32"}, 256 {"nacl", "arm"}, 257 {"netbsd", "386"}, 258 {"netbsd", "amd64"}, 259 {"netbsd", "arm"}, 260 {"openbsd", "386"}, 261 {"openbsd", "amd64"}, 262 {"openbsd", "arm"}, 263 {"plan9", "386"}, 264 {"plan9", "amd64"}, 265 {"plan9", "arm"}, 266 {"solaris", "amd64"}, 267 {"windows", "386"}, 268 {"windows", "amd64"}, 269 } 270 271 func mergeEdits(importPaths []string) fileToEditSet { 272 m := make(fileToEditSet) 273 for _, plat := range plats { 274 for f, e := range computeEdits(importPaths, plat.goos, plat.goarch, false) { 275 if e0, ok := m[f]; ok { 276 for k := range e0 { 277 if _, ok := e[k]; !ok { 278 delete(e0, k) 279 } 280 } 281 } else { 282 m[f] = e 283 } 284 } 285 } 286 return m 287 } 288 289 type noImporter struct{} 290 291 func (noImporter) Import(path string) (*types.Package, error) { 292 panic("golang.org/x/tools/go/loader said this wouldn't be called") 293 } 294 295 func computeEdits(importPaths []string, os, arch string, cgoEnabled bool) fileToEditSet { 296 ctxt := build.Default 297 ctxt.GOOS = os 298 ctxt.GOARCH = arch 299 ctxt.CgoEnabled = cgoEnabled 300 301 var conf loader.Config 302 conf.Build = &ctxt 303 conf.TypeChecker.Importer = noImporter{} 304 for _, importPath := range importPaths { 305 conf.Import(importPath) 306 } 307 prog, err := conf.Load() 308 if err != nil { 309 log.Fatal(err) 310 } 311 312 type res struct { 313 file string 314 edits editSet 315 } 316 ch := make(chan res) 317 var wg sync.WaitGroup 318 for _, pkg := range prog.InitialPackages() { 319 for _, file := range pkg.Files { 320 pkg, file := pkg, file 321 wg.Add(1) 322 go func() { 323 defer wg.Done() 324 v := visitor{pkg: pkg, file: conf.Fset.File(file.Package), edits: make(editSet)} 325 ast.Walk(&v, file) 326 ch <- res{v.file.Name(), v.edits} 327 }() 328 } 329 } 330 go func() { 331 wg.Wait() 332 close(ch) 333 }() 334 335 m := make(fileToEditSet) 336 for r := range ch { 337 m[r.file] = r.edits 338 } 339 return m 340 } 341 342 type step struct { 343 n ast.Node 344 i int 345 } 346 347 type visitor struct { 348 pkg *loader.PackageInfo 349 file *token.File 350 edits editSet 351 path []step 352 } 353 354 func (v *visitor) Visit(node ast.Node) ast.Visitor { 355 if node != nil { 356 v.path = append(v.path, step{n: node}) 357 } else { 358 n := len(v.path) 359 v.path = v.path[:n-1] 360 if n >= 2 { 361 v.path[n-2].i++ 362 } 363 } 364 365 if call, ok := node.(*ast.CallExpr); ok { 366 v.unconvert(call) 367 } 368 return v 369 } 370 371 func (v *visitor) unconvert(call *ast.CallExpr) { 372 // TODO(mdempsky): Handle useless multi-conversions. 373 374 // Conversions have exactly one argument. 375 if len(call.Args) != 1 || call.Ellipsis != token.NoPos { 376 return 377 } 378 ft, ok := v.pkg.Types[call.Fun] 379 if !ok { 380 fmt.Println("Missing type for function") 381 return 382 } 383 if !ft.IsType() { 384 // Function call; not a conversion. 385 return 386 } 387 at, ok := v.pkg.Types[call.Args[0]] 388 if !ok { 389 fmt.Println("Missing type for argument") 390 return 391 } 392 if !types.Identical(ft.Type, at.Type) { 393 // A real conversion. 394 return 395 } 396 if isUntypedValue(call.Args[0], &v.pkg.Info) { 397 // Workaround golang.org/issue/13061. 398 return 399 } 400 if *flagSafe && !v.isSafeContext(at.Type) { 401 // TODO(mdempsky): Remove this message. 402 fmt.Println("Skipped a possible type conversion because of -safe at", v.file.Position(call.Pos())) 403 return 404 } 405 if v.isCgoCheckPointerContext() { 406 // cmd/cgo generates explicit type conversions that 407 // are often redundant when introducing 408 // _cgoCheckPointer calls (issue #16). Users can't do 409 // anything about these, so skip over them. 410 return 411 } 412 413 v.edits[v.file.Position(call.Lparen)] = struct{}{} 414 } 415 416 func (v *visitor) isCgoCheckPointerContext() bool { 417 ctxt := &v.path[len(v.path)-2] 418 if ctxt.i != 1 { 419 return false 420 } 421 call, ok := ctxt.n.(*ast.CallExpr) 422 if !ok { 423 return false 424 } 425 ident, ok := call.Fun.(*ast.Ident) 426 if !ok { 427 return false 428 } 429 return ident.Name == "_cgoCheckPointer" 430 } 431 432 // isSafeContext reports whether the current context requires 433 // an expression of type t. 434 // 435 // TODO(mdempsky): That's a bad explanation. 436 func (v *visitor) isSafeContext(t types.Type) bool { 437 ctxt := &v.path[len(v.path)-2] 438 switch n := ctxt.n.(type) { 439 case *ast.AssignStmt: 440 pos := ctxt.i - len(n.Lhs) 441 if pos < 0 { 442 fmt.Println("Type conversion on LHS of assignment?") 443 return false 444 } 445 if n.Tok == token.DEFINE { 446 // Skip := assignments. 447 return true 448 } 449 // We're a conversion in the pos'th element of n.Rhs. 450 // Check that the corresponding element of n.Lhs is of type t. 451 lt, ok := v.pkg.Types[n.Lhs[pos]] 452 if !ok { 453 fmt.Println("Missing type for LHS expression") 454 return false 455 } 456 return types.Identical(t, lt.Type) 457 case *ast.BinaryExpr: 458 if n.Op == token.SHL || n.Op == token.SHR { 459 if ctxt.i == 1 { 460 // RHS of a shift is always safe. 461 return true 462 } 463 // For the LHS, we should inspect up another level. 464 fmt.Println("TODO(mdempsky): Handle LHS of shift expressions") 465 return true 466 } 467 var other ast.Expr 468 if ctxt.i == 0 { 469 other = n.Y 470 } else { 471 other = n.X 472 } 473 ot, ok := v.pkg.Types[other] 474 if !ok { 475 fmt.Println("Missing type for other binop subexpr") 476 return false 477 } 478 return types.Identical(t, ot.Type) 479 case *ast.CallExpr: 480 pos := ctxt.i - 1 481 if pos < 0 { 482 // Type conversion in the function subexpr is okay. 483 return true 484 } 485 ft, ok := v.pkg.Types[n.Fun] 486 if !ok { 487 fmt.Println("Missing type for function expression") 488 return false 489 } 490 sig, ok := ft.Type.(*types.Signature) 491 if !ok { 492 // "Function" is either a type conversion (ok) or a builtin (ok?). 493 return true 494 } 495 params := sig.Params() 496 var pt types.Type 497 if sig.Variadic() && n.Ellipsis == token.NoPos && pos >= params.Len()-1 { 498 pt = params.At(params.Len() - 1).Type().(*types.Slice).Elem() 499 } else { 500 pt = params.At(pos).Type() 501 } 502 return types.Identical(t, pt) 503 case *ast.CompositeLit, *ast.KeyValueExpr: 504 fmt.Println("TODO(mdempsky): Compare against value type of composite literal type at", v.file.Position(n.Pos())) 505 return true 506 case *ast.ReturnStmt: 507 // TODO(mdempsky): Is there a better way to get the corresponding 508 // return parameter type? 509 var funcType *ast.FuncType 510 for i := len(v.path) - 1; funcType == nil && i >= 0; i-- { 511 switch f := v.path[i].n.(type) { 512 case *ast.FuncDecl: 513 funcType = f.Type 514 case *ast.FuncLit: 515 funcType = f.Type 516 } 517 } 518 var typeExpr ast.Expr 519 for i, j := ctxt.i, 0; j < len(funcType.Results.List); j++ { 520 f := funcType.Results.List[j] 521 if len(f.Names) == 0 { 522 if i >= 1 { 523 i-- 524 continue 525 } 526 } else { 527 if i >= len(f.Names) { 528 i -= len(f.Names) 529 continue 530 } 531 } 532 typeExpr = f.Type 533 break 534 } 535 if typeExpr == nil { 536 fmt.Println(ctxt) 537 } 538 pt, ok := v.pkg.Types[typeExpr] 539 if !ok { 540 fmt.Println("Missing type for return parameter at", v.file.Position(n.Pos())) 541 return false 542 } 543 return types.Identical(t, pt.Type) 544 case *ast.StarExpr, *ast.UnaryExpr: 545 // TODO(mdempsky): I think these are always safe. 546 return true 547 case *ast.SwitchStmt: 548 // TODO(mdempsky): I think this is always safe? 549 return true 550 default: 551 // TODO(mdempsky): When can this happen? 552 fmt.Printf("... huh, %T at %v\n", n, v.file.Position(n.Pos())) 553 return true 554 } 555 } 556 557 func isUntypedValue(n ast.Expr, info *types.Info) (res bool) { 558 switch n := n.(type) { 559 case *ast.BinaryExpr: 560 switch n.Op { 561 case token.SHL, token.SHR: 562 // Shifts yield an untyped value if their LHS is untyped. 563 return isUntypedValue(n.X, info) 564 case token.EQL, token.NEQ, token.LSS, token.GTR, token.LEQ, token.GEQ: 565 // Comparisons yield an untyped boolean value. 566 return true 567 case token.ADD, token.SUB, token.MUL, token.QUO, token.REM, 568 token.AND, token.OR, token.XOR, token.AND_NOT, 569 token.LAND, token.LOR: 570 return isUntypedValue(n.X, info) && isUntypedValue(n.Y, info) 571 } 572 case *ast.UnaryExpr: 573 switch n.Op { 574 case token.ADD, token.SUB, token.NOT, token.XOR: 575 return isUntypedValue(n.X, info) 576 } 577 case *ast.BasicLit: 578 // Basic literals are always untyped. 579 return true 580 case *ast.ParenExpr: 581 return isUntypedValue(n.X, info) 582 case *ast.SelectorExpr: 583 return isUntypedValue(n.Sel, info) 584 case *ast.Ident: 585 if obj, ok := info.Uses[n]; ok { 586 if obj.Pkg() == nil && obj.Name() == "nil" { 587 // The universal untyped zero value. 588 return true 589 } 590 if b, ok := obj.Type().(*types.Basic); ok && b.Info()&types.IsUntyped != 0 { 591 // Reference to an untyped constant. 592 return true 593 } 594 } 595 case *ast.CallExpr: 596 if b, ok := asBuiltin(n.Fun, info); ok { 597 switch b.Name() { 598 case "real", "imag": 599 return isUntypedValue(n.Args[0], info) 600 case "complex": 601 return isUntypedValue(n.Args[0], info) && isUntypedValue(n.Args[1], info) 602 } 603 } 604 } 605 606 return false 607 } 608 609 func asBuiltin(n ast.Expr, info *types.Info) (*types.Builtin, bool) { 610 for { 611 paren, ok := n.(*ast.ParenExpr) 612 if !ok { 613 break 614 } 615 n = paren.X 616 } 617 618 ident, ok := n.(*ast.Ident) 619 if !ok { 620 return nil, false 621 } 622 623 obj, ok := info.Uses[ident] 624 if !ok { 625 return nil, false 626 } 627 628 b, ok := obj.(*types.Builtin) 629 return b, ok 630 } 631 632 type byPosition []token.Position 633 634 func (p byPosition) Len() int { 635 return len(p) 636 } 637 638 func (p byPosition) Less(i, j int) bool { 639 if p[i].Filename != p[j].Filename { 640 return p[i].Filename < p[j].Filename 641 } 642 if p[i].Line != p[j].Line { 643 return p[i].Line < p[j].Line 644 } 645 return p[i].Column < p[j].Column 646 } 647 648 func (p byPosition) Swap(i, j int) { 649 p[i], p[j] = p[j], p[i] 650 }