Go fix tool for client API changes.

This commit is contained in:
Alex Bramley 2013-04-04 18:49:56 +01:00
parent b402a6792b
commit 920d395bbb
3 changed files with 236 additions and 1 deletions

View File

@ -11,7 +11,8 @@ There is some example code that demonstrates usage of the library in `client.go`
Please note: this branch has been feature-frozen for a while. Check out master Please note: this branch has been feature-frozen for a while. Check out master
for some interesting (but not backwards-compatible) changes that will become for some interesting (but not backwards-compatible) changes that will become
the new go1 branch when they're stable. the new go1 branch when they're stable. See `fix/goirc.go` and the README there
for a quick way to migrate.
### Using the framework ### Using the framework

35
fix/README.md Normal file
View File

@ -0,0 +1,35 @@
## client api changes
`goirc.go` is a fix compatible with `go tool fix` that attempts to
programaticallly change old client code to work with the newer, shinier
API. It might even work, depending on the complexity of your code. You'll
need to have a Go source tree in the path specified by GOROOT. GOOS and
GOARCH may not be set in your environment; use your head.
### To install:
cd $GOROOT/src/cmd/fix
ln -s $GOPATH/src/github.com/fluffle/goirc/fix/goirc.go
go build
mv fix $GOROOT/pkg/tool/$GOOS_$GOARCH
### To fix:
go tool fix -r goirc -diff /path/to/code
<check diffs>
go tool fix -r goirc /path/to/code
### Things that aren't fixed by this
This fix doesn't take care of some bad design decisions I made:
- conn.State is left as-is. If you're using this you'll need to rewrite
things to get scope into your handlers in a different fashion.
- conn.ER and conn.ED are left as-is. These should be completely removed
(and probably shouldn't have been left accessible in the first place).
It's also quite likely that this won't produce the nicest "fixed" code. In
particular, if you're seeing lots of lines like `conn.Config().XXX = "foo"`,
you probably want to consider creating a `Config` struct and then passing
it to `client.Client()`.

199
fix/goirc.go Normal file
View File

@ -0,0 +1,199 @@
package main
import (
"fmt"
"go/ast"
"go/token"
"strings"
)
// DISCLAIMER: will probably not fix everything. Notable omissions:
// - conn.State will not be removed, if you were using it.
// - conn.ER/ED will not be removed; exposing them was a Bad Idea, and
// the required changes are quite a challenge to express programatically.
func init() {
register(goircFix)
}
var goircFix = fix{
"goirc",
"2013-03-24",
goircNewApi,
`Update code that uses goirc/client to new API.`,
}
var goircConstants = map[string]string{
`"REGISTER"`: "REGISTER",
`"CONNECTED"`: "CONNECTED",
`"DISCONNECTED"`: "DISCONNECTED",
`"ACTION"`: "ACTION",
`"AWAY"`: "AWAY",
`"CTCP"`: "CTCP",
`"CTCPREPLY"`: "CTCPREPLY",
`"INVITE"`: "INVITE",
`"JOIN"`: "JOIN",
`"KICK"`: "KICK",
`"MODE"`: "MODE",
`"NICK"`: "NICK",
`"NOTICE"`: "NOTICE",
`"OPER"`: "OPER",
`"PART"`: "PART",
`"PASS"`: "PASS",
`"PING"`: "PING",
`"PONG"`: "PONG",
`"PRIVMSG"`: "PRIVMSG",
`"QUIT"`: "QUIT",
`"TOPIC"`: "TOPIC",
`"USER"`: "USER",
`"VERSION"`: "VERSION",
`"VHOST"`: "VHOST",
`"WHO"`: "WHO",
`"WHOIS"`: "WHOIS",
}
var goircStructToConfig = map[string]string{
"Host": "Server",
"Network": "Server",
"NewNick": "NewNick",
"SSL": "SSL",
"SSLConfig": "SSLConfig",
"PingFreq": "PingFreq",
"Flood": "Flood",
}
var goircStructToMethod = map[string]string{
"Me": "Me",
"ST": "StateTracker",
"Connected": "Connected",
}
var goircMethodRename = map[string]string{
"AddHandler": "HandleFunc",
"Connect": "ConnectTo",
}
func addCall(t ast.Expr, method string) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: t,
Sel: ast.NewIdent(method),
},
}
}
func goircNewApi(f *ast.File) bool {
state := stateApi(f)
client := clientApi(f)
return client || state
}
func stateApi(f *ast.File) bool {
spec := importSpec(f, "github.com/fluffle/goirc/state")
if spec == nil {
return false
}
stateImport := "state"
if spec.Name != nil {
stateImport = spec.Name.Name
}
return renameFixTab(f, []rename{
{"github.com/fluffle/goirc/state", "",
stateImport + ".StateTracker", stateImport + ".Tracker"},
{"github.com/fluffle/goirc/state", "",
stateImport + ".MockStateTracker", stateImport + ".MockTracker"},
})
}
func clientApi(f *ast.File) bool {
spec := importSpec(f, "github.com/fluffle/goirc/client")
if spec == nil {
return false
}
clientImport := "client"
if spec.Name != nil {
clientImport = spec.Name.Name
}
fixed := false
maybeReplaceBasicLit := func (expr *ast.Expr) {
str, ok := (*expr).(*ast.BasicLit)
if !ok || str == nil || str.Kind != token.STRING { return }
if repl, ok := goircConstants[strings.ToUpper(str.Value)]; ok {
*expr = &ast.SelectorExpr{
ast.NewIdent(clientImport),
ast.NewIdent(repl),
}
fixed = true
}
}
maybeReplaceConnSelectors := func (expr *ast.Expr) {
sel, ok := (*expr).(*ast.SelectorExpr)
if !ok || !isClientConn(sel.X, clientImport) { return }
name := sel.Sel.String()
if rep, ok := goircStructToConfig[name]; ok {
sel.X = addCall(sel.X, "Config")
sel.Sel = ast.NewIdent(rep)
fixed = true
} else if meth, ok := goircStructToMethod[name]; ok {
*expr = addCall(sel.X, meth)
fixed = true
} else if meth, ok := goircMethodRename[name]; ok {
sel.Sel = ast.NewIdent(meth)
fixed = true
}
}
walk(f, func(n interface{}) {
if expr, ok := n.(*ast.Expr); ok {
maybeReplaceBasicLit(expr)
maybeReplaceConnSelectors(expr)
}
if expr, ok := n.(*ast.CallExpr); ok {
if sel, ok := n.(*ast.SelectorExpr); ok &&
isPkgDot(sel, clientImport, "Client") {
// s/Client/SimpleClient/
sel.Sel = ast.NewIdent("SimpleClient")
// and delete the last arg from args
expr.Args = expr.Args[:3]
fixed = true
}
}
})
return fixed
}
func isClientConn(t ast.Expr, pkg string) bool {
id, ok := t.(*ast.Ident)
if !ok || id.Obj == nil { return false }
switch dec := id.Obj.Decl.(type) {
case *ast.ValueSpec:
// Declared with var X Type
return dec.Type != nil && isPtrPkgDot(dec.Type, pkg, "Conn")
case *ast.AssignStmt:
// Declared with X := Expr producing Type
// NOTE: not taking care of multiple-assignment case atm!
switch rhs := dec.Rhs[0].(type) {
case *ast.CallExpr:
// X := client.Client() or client.SimpleClient()
return isPkgDot(rhs.Fun, pkg, "Client") ||
isPkgDot(rhs.Fun, pkg, "SimpleClient")
case *ast.UnaryExpr:
// X := &client.Conn{}
lit, ok := rhs.X.(*ast.CompositeLit)
return ok && isPkgDot(lit.Type, pkg, "Conn")
case *ast.CompositeLit:
// X := client.Conn{}
return isPkgDot(lit.Type, pkg, "Conn")
default:
fmt.Printf("rhs: %#v\n", rhs)
}
case *ast.Field:
// Declared with func f(X Type)
return isPkgDot(dec.Type, pkg, "Conn")
default:
fmt.Printf("dec: %#v\n", dec)
}
return false
}