mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-29 21:31:02 +00:00
c28f7cb29f
Initial part of #435
665 lines
15 KiB
Go
665 lines
15 KiB
Go
// Copyright 2015 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.
|
|
|
|
// Unconvert removes redundant type conversions from Go packages.
|
|
package unconvert
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/types"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"reflect"
|
|
"runtime/pprof"
|
|
"sort"
|
|
"sync"
|
|
"unicode"
|
|
|
|
"github.com/kisielk/gotool"
|
|
"golang.org/x/text/width"
|
|
"golang.org/x/tools/go/loader"
|
|
)
|
|
|
|
// Unnecessary conversions are identified by the position
|
|
// of their left parenthesis within a source file.
|
|
|
|
type editSet map[token.Position]struct{}
|
|
|
|
type fileToEditSet map[string]editSet
|
|
|
|
func apply(file string, edits editSet) {
|
|
if len(edits) == 0 {
|
|
return
|
|
}
|
|
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Note: We modify edits during the walk.
|
|
v := editor{edits: edits, file: fset.File(f.Package)}
|
|
ast.Walk(&v, f)
|
|
if len(edits) != 0 {
|
|
log.Printf("%s: missing edits %s", file, edits)
|
|
}
|
|
|
|
// TODO(mdempsky): Write to temporary file and rename.
|
|
var buf bytes.Buffer
|
|
err = format.Node(&buf, fset, f)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
err = ioutil.WriteFile(file, buf.Bytes(), 0)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
type editor struct {
|
|
edits editSet
|
|
file *token.File
|
|
}
|
|
|
|
func (e *editor) Visit(n ast.Node) ast.Visitor {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
v := reflect.ValueOf(n).Elem()
|
|
for i, n := 0, v.NumField(); i < n; i++ {
|
|
switch f := v.Field(i).Addr().Interface().(type) {
|
|
case *ast.Expr:
|
|
e.rewrite(f)
|
|
case *[]ast.Expr:
|
|
for i := range *f {
|
|
e.rewrite(&(*f)[i])
|
|
}
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
|
|
func (e *editor) rewrite(f *ast.Expr) {
|
|
call, ok := (*f).(*ast.CallExpr)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
pos := e.file.Position(call.Lparen)
|
|
if _, ok := e.edits[pos]; !ok {
|
|
return
|
|
}
|
|
*f = call.Args[0]
|
|
delete(e.edits, pos)
|
|
}
|
|
|
|
var (
|
|
cr = []byte{'\r'}
|
|
nl = []byte{'\n'}
|
|
)
|
|
|
|
func print(conversions []token.Position) {
|
|
var file string
|
|
var lines [][]byte
|
|
|
|
for _, pos := range conversions {
|
|
fmt.Printf("%s:%d:%d: unnecessary conversion\n", pos.Filename, pos.Line, pos.Column)
|
|
if *flagV {
|
|
if pos.Filename != file {
|
|
buf, err := ioutil.ReadFile(pos.Filename)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
file = pos.Filename
|
|
lines = bytes.Split(buf, nl)
|
|
}
|
|
|
|
line := bytes.TrimSuffix(lines[pos.Line-1], cr)
|
|
fmt.Printf("%s\n", line)
|
|
|
|
// For files processed by cgo, Column is the
|
|
// column location after cgo processing, which
|
|
// may be different than the source column
|
|
// that we want here. In lieu of a better
|
|
// heuristic for detecting this case, at least
|
|
// avoid panicking if column is out of bounds.
|
|
if pos.Column <= len(line) {
|
|
fmt.Printf("%s^\n", rub(line[:pos.Column-1]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rub returns a copy of buf with all non-whitespace characters replaced
|
|
// by spaces (like rubbing them out with white out).
|
|
func rub(buf []byte) []byte {
|
|
// TODO(mdempsky): Handle combining characters?
|
|
var res bytes.Buffer
|
|
for _, r := range string(buf) {
|
|
if unicode.IsSpace(r) {
|
|
res.WriteRune(r)
|
|
continue
|
|
}
|
|
switch width.LookupRune(r).Kind() {
|
|
case width.EastAsianWide, width.EastAsianFullwidth:
|
|
res.WriteString(" ")
|
|
default:
|
|
res.WriteByte(' ')
|
|
}
|
|
}
|
|
return res.Bytes()
|
|
}
|
|
|
|
var (
|
|
flagAll = flag.Bool("unconvert.all", false, "type check all GOOS and GOARCH combinations")
|
|
flagApply = flag.Bool("unconvert.apply", false, "apply edits to source files")
|
|
flagCPUProfile = flag.String("unconvert.cpuprofile", "", "write CPU profile to file")
|
|
// TODO(mdempsky): Better description and maybe flag name.
|
|
flagSafe = flag.Bool("unconvert.safe", false, "be more conservative (experimental)")
|
|
flagV = flag.Bool("unconvert.v", false, "verbose output")
|
|
)
|
|
|
|
func usage() {
|
|
fmt.Fprintf(os.Stderr, "usage: unconvert [flags] [package ...]\n")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
func nomain() {
|
|
flag.Usage = usage
|
|
flag.Parse()
|
|
|
|
if *flagCPUProfile != "" {
|
|
f, err := os.Create(*flagCPUProfile)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
pprof.StartCPUProfile(f)
|
|
defer pprof.StopCPUProfile()
|
|
}
|
|
|
|
importPaths := gotool.ImportPaths(flag.Args())
|
|
if len(importPaths) == 0 {
|
|
return
|
|
}
|
|
|
|
var m fileToEditSet
|
|
if *flagAll {
|
|
m = mergeEdits(importPaths)
|
|
} else {
|
|
m = computeEdits(importPaths, build.Default.GOOS, build.Default.GOARCH, build.Default.CgoEnabled)
|
|
}
|
|
|
|
if *flagApply {
|
|
var wg sync.WaitGroup
|
|
for f, e := range m {
|
|
wg.Add(1)
|
|
f, e := f, e
|
|
go func() {
|
|
defer wg.Done()
|
|
apply(f, e)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
} else {
|
|
var conversions []token.Position
|
|
for _, positions := range m {
|
|
for pos := range positions {
|
|
conversions = append(conversions, pos)
|
|
}
|
|
}
|
|
sort.Sort(byPosition(conversions))
|
|
print(conversions)
|
|
if len(conversions) > 0 {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Run(prog *loader.Program) []token.Position {
|
|
m := computeEditsFromProg(prog)
|
|
var conversions []token.Position
|
|
for _, positions := range m {
|
|
for pos := range positions {
|
|
conversions = append(conversions, pos)
|
|
}
|
|
}
|
|
return conversions
|
|
}
|
|
|
|
var plats = [...]struct {
|
|
goos, goarch string
|
|
}{
|
|
// TODO(mdempsky): buildall.bash also builds linux-386-387 and linux-arm-arm5.
|
|
{"android", "386"},
|
|
{"android", "amd64"},
|
|
{"android", "arm"},
|
|
{"android", "arm64"},
|
|
{"darwin", "386"},
|
|
{"darwin", "amd64"},
|
|
{"darwin", "arm"},
|
|
{"darwin", "arm64"},
|
|
{"dragonfly", "amd64"},
|
|
{"freebsd", "386"},
|
|
{"freebsd", "amd64"},
|
|
{"freebsd", "arm"},
|
|
{"linux", "386"},
|
|
{"linux", "amd64"},
|
|
{"linux", "arm"},
|
|
{"linux", "arm64"},
|
|
{"linux", "mips64"},
|
|
{"linux", "mips64le"},
|
|
{"linux", "ppc64"},
|
|
{"linux", "ppc64le"},
|
|
{"linux", "s390x"},
|
|
{"nacl", "386"},
|
|
{"nacl", "amd64p32"},
|
|
{"nacl", "arm"},
|
|
{"netbsd", "386"},
|
|
{"netbsd", "amd64"},
|
|
{"netbsd", "arm"},
|
|
{"openbsd", "386"},
|
|
{"openbsd", "amd64"},
|
|
{"openbsd", "arm"},
|
|
{"plan9", "386"},
|
|
{"plan9", "amd64"},
|
|
{"plan9", "arm"},
|
|
{"solaris", "amd64"},
|
|
{"windows", "386"},
|
|
{"windows", "amd64"},
|
|
}
|
|
|
|
func mergeEdits(importPaths []string) fileToEditSet {
|
|
m := make(fileToEditSet)
|
|
for _, plat := range plats {
|
|
for f, e := range computeEdits(importPaths, plat.goos, plat.goarch, false) {
|
|
if e0, ok := m[f]; ok {
|
|
for k := range e0 {
|
|
if _, ok := e[k]; !ok {
|
|
delete(e0, k)
|
|
}
|
|
}
|
|
} else {
|
|
m[f] = e
|
|
}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
type noImporter struct{}
|
|
|
|
func (noImporter) Import(path string) (*types.Package, error) {
|
|
panic("golang.org/x/tools/go/loader said this wouldn't be called")
|
|
}
|
|
|
|
func computeEdits(importPaths []string, os, arch string, cgoEnabled bool) fileToEditSet {
|
|
ctxt := build.Default
|
|
ctxt.GOOS = os
|
|
ctxt.GOARCH = arch
|
|
ctxt.CgoEnabled = cgoEnabled
|
|
|
|
var conf loader.Config
|
|
conf.Build = &ctxt
|
|
conf.TypeChecker.Importer = noImporter{}
|
|
for _, importPath := range importPaths {
|
|
conf.Import(importPath)
|
|
}
|
|
prog, err := conf.Load()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
return computeEditsFromProg(prog)
|
|
}
|
|
|
|
func computeEditsFromProg(prog *loader.Program) fileToEditSet {
|
|
type res struct {
|
|
file string
|
|
edits editSet
|
|
}
|
|
ch := make(chan res)
|
|
var wg sync.WaitGroup
|
|
for _, pkg := range prog.InitialPackages() {
|
|
for _, file := range pkg.Files {
|
|
pkg, file := pkg, file
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
v := visitor{pkg: pkg, file: prog.Fset.File(file.Package), edits: make(editSet)}
|
|
ast.Walk(&v, file)
|
|
ch <- res{v.file.Name(), v.edits}
|
|
}()
|
|
}
|
|
}
|
|
go func() {
|
|
wg.Wait()
|
|
close(ch)
|
|
}()
|
|
|
|
m := make(fileToEditSet)
|
|
for r := range ch {
|
|
m[r.file] = r.edits
|
|
}
|
|
return m
|
|
}
|
|
|
|
type step struct {
|
|
n ast.Node
|
|
i int
|
|
}
|
|
|
|
type visitor struct {
|
|
pkg *loader.PackageInfo
|
|
file *token.File
|
|
edits editSet
|
|
path []step
|
|
}
|
|
|
|
func (v *visitor) Visit(node ast.Node) ast.Visitor {
|
|
if node != nil {
|
|
v.path = append(v.path, step{n: node})
|
|
} else {
|
|
n := len(v.path)
|
|
v.path = v.path[:n-1]
|
|
if n >= 2 {
|
|
v.path[n-2].i++
|
|
}
|
|
}
|
|
|
|
if call, ok := node.(*ast.CallExpr); ok {
|
|
v.unconvert(call)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (v *visitor) unconvert(call *ast.CallExpr) {
|
|
// TODO(mdempsky): Handle useless multi-conversions.
|
|
|
|
// Conversions have exactly one argument.
|
|
if len(call.Args) != 1 || call.Ellipsis != token.NoPos {
|
|
return
|
|
}
|
|
ft, ok := v.pkg.Types[call.Fun]
|
|
if !ok {
|
|
fmt.Println("Missing type for function")
|
|
return
|
|
}
|
|
if !ft.IsType() {
|
|
// Function call; not a conversion.
|
|
return
|
|
}
|
|
at, ok := v.pkg.Types[call.Args[0]]
|
|
if !ok {
|
|
fmt.Println("Missing type for argument")
|
|
return
|
|
}
|
|
if !types.Identical(ft.Type, at.Type) {
|
|
// A real conversion.
|
|
return
|
|
}
|
|
if isUntypedValue(call.Args[0], &v.pkg.Info) {
|
|
// Workaround golang.org/issue/13061.
|
|
return
|
|
}
|
|
if *flagSafe && !v.isSafeContext(at.Type) {
|
|
// TODO(mdempsky): Remove this message.
|
|
fmt.Println("Skipped a possible type conversion because of -safe at", v.file.Position(call.Pos()))
|
|
return
|
|
}
|
|
if v.isCgoCheckPointerContext() {
|
|
// cmd/cgo generates explicit type conversions that
|
|
// are often redundant when introducing
|
|
// _cgoCheckPointer calls (issue #16). Users can't do
|
|
// anything about these, so skip over them.
|
|
return
|
|
}
|
|
|
|
v.edits[v.file.Position(call.Lparen)] = struct{}{}
|
|
}
|
|
|
|
func (v *visitor) isCgoCheckPointerContext() bool {
|
|
ctxt := &v.path[len(v.path)-2]
|
|
if ctxt.i != 1 {
|
|
return false
|
|
}
|
|
call, ok := ctxt.n.(*ast.CallExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
ident, ok := call.Fun.(*ast.Ident)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return ident.Name == "_cgoCheckPointer"
|
|
}
|
|
|
|
// isSafeContext reports whether the current context requires
|
|
// an expression of type t.
|
|
//
|
|
// TODO(mdempsky): That's a bad explanation.
|
|
func (v *visitor) isSafeContext(t types.Type) bool {
|
|
ctxt := &v.path[len(v.path)-2]
|
|
switch n := ctxt.n.(type) {
|
|
case *ast.AssignStmt:
|
|
pos := ctxt.i - len(n.Lhs)
|
|
if pos < 0 {
|
|
fmt.Println("Type conversion on LHS of assignment?")
|
|
return false
|
|
}
|
|
if n.Tok == token.DEFINE {
|
|
// Skip := assignments.
|
|
return true
|
|
}
|
|
// We're a conversion in the pos'th element of n.Rhs.
|
|
// Check that the corresponding element of n.Lhs is of type t.
|
|
lt, ok := v.pkg.Types[n.Lhs[pos]]
|
|
if !ok {
|
|
fmt.Println("Missing type for LHS expression")
|
|
return false
|
|
}
|
|
return types.Identical(t, lt.Type)
|
|
case *ast.BinaryExpr:
|
|
if n.Op == token.SHL || n.Op == token.SHR {
|
|
if ctxt.i == 1 {
|
|
// RHS of a shift is always safe.
|
|
return true
|
|
}
|
|
// For the LHS, we should inspect up another level.
|
|
fmt.Println("TODO(mdempsky): Handle LHS of shift expressions")
|
|
return true
|
|
}
|
|
var other ast.Expr
|
|
if ctxt.i == 0 {
|
|
other = n.Y
|
|
} else {
|
|
other = n.X
|
|
}
|
|
ot, ok := v.pkg.Types[other]
|
|
if !ok {
|
|
fmt.Println("Missing type for other binop subexpr")
|
|
return false
|
|
}
|
|
return types.Identical(t, ot.Type)
|
|
case *ast.CallExpr:
|
|
pos := ctxt.i - 1
|
|
if pos < 0 {
|
|
// Type conversion in the function subexpr is okay.
|
|
return true
|
|
}
|
|
ft, ok := v.pkg.Types[n.Fun]
|
|
if !ok {
|
|
fmt.Println("Missing type for function expression")
|
|
return false
|
|
}
|
|
sig, ok := ft.Type.(*types.Signature)
|
|
if !ok {
|
|
// "Function" is either a type conversion (ok) or a builtin (ok?).
|
|
return true
|
|
}
|
|
params := sig.Params()
|
|
var pt types.Type
|
|
if sig.Variadic() && n.Ellipsis == token.NoPos && pos >= params.Len()-1 {
|
|
pt = params.At(params.Len() - 1).Type().(*types.Slice).Elem()
|
|
} else {
|
|
pt = params.At(pos).Type()
|
|
}
|
|
return types.Identical(t, pt)
|
|
case *ast.CompositeLit, *ast.KeyValueExpr:
|
|
fmt.Println("TODO(mdempsky): Compare against value type of composite literal type at", v.file.Position(n.Pos()))
|
|
return true
|
|
case *ast.ReturnStmt:
|
|
// TODO(mdempsky): Is there a better way to get the corresponding
|
|
// return parameter type?
|
|
var funcType *ast.FuncType
|
|
for i := len(v.path) - 1; funcType == nil && i >= 0; i-- {
|
|
switch f := v.path[i].n.(type) {
|
|
case *ast.FuncDecl:
|
|
funcType = f.Type
|
|
case *ast.FuncLit:
|
|
funcType = f.Type
|
|
}
|
|
}
|
|
var typeExpr ast.Expr
|
|
for i, j := ctxt.i, 0; j < len(funcType.Results.List); j++ {
|
|
f := funcType.Results.List[j]
|
|
if len(f.Names) == 0 {
|
|
if i >= 1 {
|
|
i--
|
|
continue
|
|
}
|
|
} else {
|
|
if i >= len(f.Names) {
|
|
i -= len(f.Names)
|
|
continue
|
|
}
|
|
}
|
|
typeExpr = f.Type
|
|
break
|
|
}
|
|
if typeExpr == nil {
|
|
fmt.Println(ctxt)
|
|
}
|
|
pt, ok := v.pkg.Types[typeExpr]
|
|
if !ok {
|
|
fmt.Println("Missing type for return parameter at", v.file.Position(n.Pos()))
|
|
return false
|
|
}
|
|
return types.Identical(t, pt.Type)
|
|
case *ast.StarExpr, *ast.UnaryExpr:
|
|
// TODO(mdempsky): I think these are always safe.
|
|
return true
|
|
case *ast.SwitchStmt:
|
|
// TODO(mdempsky): I think this is always safe?
|
|
return true
|
|
default:
|
|
// TODO(mdempsky): When can this happen?
|
|
fmt.Printf("... huh, %T at %v\n", n, v.file.Position(n.Pos()))
|
|
return true
|
|
}
|
|
}
|
|
|
|
func isUntypedValue(n ast.Expr, info *types.Info) (res bool) {
|
|
switch n := n.(type) {
|
|
case *ast.BinaryExpr:
|
|
switch n.Op {
|
|
case token.SHL, token.SHR:
|
|
// Shifts yield an untyped value if their LHS is untyped.
|
|
return isUntypedValue(n.X, info)
|
|
case token.EQL, token.NEQ, token.LSS, token.GTR, token.LEQ, token.GEQ:
|
|
// Comparisons yield an untyped boolean value.
|
|
return true
|
|
case token.ADD, token.SUB, token.MUL, token.QUO, token.REM,
|
|
token.AND, token.OR, token.XOR, token.AND_NOT,
|
|
token.LAND, token.LOR:
|
|
return isUntypedValue(n.X, info) && isUntypedValue(n.Y, info)
|
|
}
|
|
case *ast.UnaryExpr:
|
|
switch n.Op {
|
|
case token.ADD, token.SUB, token.NOT, token.XOR:
|
|
return isUntypedValue(n.X, info)
|
|
}
|
|
case *ast.BasicLit:
|
|
// Basic literals are always untyped.
|
|
return true
|
|
case *ast.ParenExpr:
|
|
return isUntypedValue(n.X, info)
|
|
case *ast.SelectorExpr:
|
|
return isUntypedValue(n.Sel, info)
|
|
case *ast.Ident:
|
|
if obj, ok := info.Uses[n]; ok {
|
|
if obj.Pkg() == nil && obj.Name() == "nil" {
|
|
// The universal untyped zero value.
|
|
return true
|
|
}
|
|
if b, ok := obj.Type().(*types.Basic); ok && b.Info()&types.IsUntyped != 0 {
|
|
// Reference to an untyped constant.
|
|
return true
|
|
}
|
|
}
|
|
case *ast.CallExpr:
|
|
if b, ok := asBuiltin(n.Fun, info); ok {
|
|
switch b.Name() {
|
|
case "real", "imag":
|
|
return isUntypedValue(n.Args[0], info)
|
|
case "complex":
|
|
return isUntypedValue(n.Args[0], info) && isUntypedValue(n.Args[1], info)
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func asBuiltin(n ast.Expr, info *types.Info) (*types.Builtin, bool) {
|
|
for {
|
|
paren, ok := n.(*ast.ParenExpr)
|
|
if !ok {
|
|
break
|
|
}
|
|
n = paren.X
|
|
}
|
|
|
|
ident, ok := n.(*ast.Ident)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
obj, ok := info.Uses[ident]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
b, ok := obj.(*types.Builtin)
|
|
return b, ok
|
|
}
|
|
|
|
type byPosition []token.Position
|
|
|
|
func (p byPosition) Len() int {
|
|
return len(p)
|
|
}
|
|
|
|
func (p byPosition) Less(i, j int) bool {
|
|
if p[i].Filename != p[j].Filename {
|
|
return p[i].Filename < p[j].Filename
|
|
}
|
|
if p[i].Line != p[j].Line {
|
|
return p[i].Line < p[j].Line
|
|
}
|
|
return p[i].Column < p[j].Column
|
|
}
|
|
|
|
func (p byPosition) Swap(i, j int) {
|
|
p[i], p[j] = p[j], p[i]
|
|
}
|