cmd/gotext: add rewrite command
Change-Id: Ibc6a957773086df50fb37634e1e79beb361e2914
Reviewed-on: https://go-review.googlesource.com/79578
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/cmd/gotext/doc.go b/cmd/gotext/doc.go
index 54eb485..f78912f 100644
--- a/cmd/gotext/doc.go
+++ b/cmd/gotext/doc.go
@@ -14,6 +14,7 @@
// The commands are:
//
// extract extract strings to be translated from code
+// rewrite rewrite rewrites fmt functions to use a message Printer
//
// Use "go help [command]" for more information about a command.
//
@@ -32,4 +33,16 @@
//
//
//
+// Rewrite rewrites fmt functions to use a message Printer
+//
+// Usage:
+//
+// go rewrite <package>*
+//
+// rewrite is typically done once for a project. It rewrites all usages of
+// fmt to use x/text's message package whenever a message.Printer is in scope.
+// It rewrites Print and Println calls with constant strings to the equivalent
+// using Printf to allow translators to reorder arguments.
+//
+//
package main
diff --git a/cmd/gotext/examples/rewrite/main.go b/cmd/gotext/examples/rewrite/main.go
new file mode 100644
index 0000000..2fada45
--- /dev/null
+++ b/cmd/gotext/examples/rewrite/main.go
@@ -0,0 +1,37 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+ "fmt"
+
+ "golang.org/x/text/language"
+ "golang.org/x/text/message"
+)
+
+func main() {
+ var nPizzas = 4
+ // The following call gets replaced by a call to the globally
+ // defined printer.
+ fmt.Println("We ate", nPizzas, "pizzas.")
+
+ p := message.NewPrinter(language.English)
+
+ // Prevent build failure, although it is okay for gotext.
+ p.Println(1024)
+
+ // Replaced by a call to p.
+ fmt.Println("Example punctuation:", "$%^&!")
+
+ {
+ q := message.NewPrinter(language.French)
+
+ const leaveAnIdentBe = "Don't expand me."
+ fmt.Print(leaveAnIdentBe)
+ q.Println() // Prevent build failure, although it is okay for gotext.
+ }
+
+ fmt.Printf("Hello %s\n", "City")
+}
diff --git a/cmd/gotext/examples/rewrite/printer.go b/cmd/gotext/examples/rewrite/printer.go
new file mode 100644
index 0000000..9ed0556
--- /dev/null
+++ b/cmd/gotext/examples/rewrite/printer.go
@@ -0,0 +1,16 @@
+// Copyright 2017 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.
+
+// +build ignore
+
+package main
+
+import (
+ "golang.org/x/text/language"
+ "golang.org/x/text/message"
+)
+
+// The printer defined here will be picked up by the first print statement
+// in main.go.
+var printer = message.NewPrinter(language.English)
diff --git a/cmd/gotext/main.go b/cmd/gotext/main.go
index b03eb55..de2e0ff 100644
--- a/cmd/gotext/main.go
+++ b/cmd/gotext/main.go
@@ -87,6 +87,7 @@
// The order here is the order in which they are printed by 'go help'.
var commands = []*Command{
cmdExtract,
+ cmdRewrite,
// TODO:
// - generate code from translations.
// - update: full-cycle update of extraction, sending, and integration
diff --git a/cmd/gotext/rewrite.go b/cmd/gotext/rewrite.go
new file mode 100644
index 0000000..af97d41
--- /dev/null
+++ b/cmd/gotext/rewrite.go
@@ -0,0 +1,304 @@
+// Copyright 2017 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.
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/build"
+ "go/constant"
+ "go/format"
+ "go/parser"
+ "go/token"
+ "log"
+ "os"
+ "strings"
+
+ "golang.org/x/tools/go/loader"
+)
+
+const printerType = "golang.org/x/text/message.Printer"
+
+// TODO:
+// - merge information into existing files
+// - handle different file formats (PO, XLIFF)
+// - handle features (gender, plural)
+// - message rewriting
+
+func init() {
+ overwrite = cmdRewrite.Flag.Bool("w", false, "write files in place")
+}
+
+var (
+ overwrite *bool
+)
+
+var cmdRewrite = &Command{
+ Run: runRewrite,
+ UsageLine: "rewrite <package>*",
+ Short: "rewrite rewrites fmt functions to use a message Printer",
+ Long: `
+rewrite is typically done once for a project. It rewrites all usages of
+fmt to use x/text's message package whenever a message.Printer is in scope.
+It rewrites Print and Println calls with constant strings to the equivalent
+using Printf to allow translators to reorder arguments.
+`,
+}
+
+func runRewrite(cmd *Command, args []string) error {
+ if len(args) == 0 {
+ args = []string{"."}
+ }
+
+ conf := loader.Config{
+ Build: &build.Default,
+ ParserMode: parser.ParseComments,
+ AllowErrors: true, // Allow unused instances of message.Printer.
+ }
+
+ // Use the initial packages from the command line.
+ args, err := conf.FromArgs(args, false)
+ if err != nil {
+ return err
+ }
+
+ // Load, parse and type-check the whole program.
+ iprog, err := conf.Load()
+ if err != nil {
+ return err
+ }
+
+ for _, info := range iprog.InitialPackages() {
+ for _, f := range info.Files {
+ // Associate comments with nodes.
+
+ // Pick up initialized Printers at the package level.
+ r := rewriter{info: info, conf: &conf}
+ for _, n := range info.InitOrder {
+ if t := r.info.Types[n.Rhs].Type.String(); strings.HasSuffix(t, printerType) {
+ r.printerVar = n.Lhs[0].Name()
+ }
+ }
+
+ ast.Walk(&r, f)
+
+ w := os.Stdout
+ if *overwrite {
+ var err error
+ if w, err = os.Create(conf.Fset.File(f.Pos()).Name()); err != nil {
+ log.Fatalf("Could not open file: %v", err)
+ }
+ }
+
+ if err := format.Node(w, conf.Fset, f); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+type rewriter struct {
+ info *loader.PackageInfo
+ conf *loader.Config
+ printerVar string
+}
+
+// print returns Go syntax for the specified node.
+func (r *rewriter) print(n ast.Node) string {
+ var buf bytes.Buffer
+ format.Node(&buf, r.conf.Fset, n)
+ return buf.String()
+}
+
+func (r *rewriter) Visit(n ast.Node) ast.Visitor {
+ // Save the state by scope.
+ if _, ok := n.(*ast.BlockStmt); ok {
+ r := *r
+ return &r
+ }
+ // Find Printers created by assignment.
+ stmt, ok := n.(*ast.AssignStmt)
+ if ok {
+ for _, v := range stmt.Lhs {
+ if r.printerVar == r.print(v) {
+ r.printerVar = ""
+ }
+ }
+ for i, v := range stmt.Rhs {
+ if t := r.info.Types[v].Type.String(); strings.HasSuffix(t, printerType) {
+ r.printerVar = r.print(stmt.Lhs[i])
+ return r
+ }
+ }
+ }
+ // Find Printers created by variable declaration.
+ spec, ok := n.(*ast.ValueSpec)
+ if ok {
+ for _, v := range spec.Names {
+ if r.printerVar == r.print(v) {
+ r.printerVar = ""
+ }
+ }
+ for i, v := range spec.Values {
+ if t := r.info.Types[v].Type.String(); strings.HasSuffix(t, printerType) {
+ r.printerVar = r.print(spec.Names[i])
+ return r
+ }
+ }
+ }
+ if r.printerVar == "" {
+ return r
+ }
+ call, ok := n.(*ast.CallExpr)
+ if !ok {
+ return r
+ }
+
+ // TODO: Handle literal values?
+ sel, ok := call.Fun.(*ast.SelectorExpr)
+ if !ok {
+ return r
+ }
+ meth := r.info.Selections[sel]
+
+ source := r.print(sel.X)
+ fun := r.print(sel.Sel)
+ if meth != nil {
+ source = meth.Recv().String()
+ fun = meth.Obj().Name()
+ }
+
+ // TODO: remove cheap hack and check if the type either
+ // implements some interface or is specifically of type
+ // "golang.org/x/text/message".Printer.
+ m, ok := rewriteFuncs[source]
+ if !ok {
+ return r
+ }
+
+ rewriteType, ok := m[fun]
+ if !ok {
+ return r
+ }
+ ident := ast.NewIdent(r.printerVar)
+ ident.NamePos = sel.X.Pos()
+ sel.X = ident
+ if rewriteType.method != "" {
+ sel.Sel.Name = rewriteType.method
+ }
+
+ // Analyze arguments.
+ argn := rewriteType.arg
+ if rewriteType.format || argn >= len(call.Args) {
+ return r
+ }
+ hasConst := false
+ for _, a := range call.Args[argn:] {
+ if v := r.info.Types[a].Value; v != nil && v.Kind() == constant.String {
+ hasConst = true
+ break
+ }
+ }
+ if !hasConst {
+ return r
+ }
+ sel.Sel.Name = rewriteType.methodf
+
+ // We are done if there is only a single string that does not need to be
+ // escaped.
+ if len(call.Args) == 1 {
+ s, ok := constStr(r.info, call.Args[0])
+ if ok && !strings.Contains(s, "%") && !rewriteType.newLine {
+ return r
+ }
+ }
+
+ // Rewrite arguments as format string.
+ expr := &ast.BasicLit{
+ ValuePos: call.Lparen,
+ Kind: token.STRING,
+ }
+ newArgs := append(call.Args[:argn:argn], expr)
+ newStr := []string{}
+ for i, a := range call.Args[argn:] {
+ if s, ok := constStr(r.info, a); ok {
+ newStr = append(newStr, strings.Replace(s, "%", "%%", -1))
+ } else {
+ newStr = append(newStr, "%v")
+ newArgs = append(newArgs, call.Args[argn+i])
+ }
+ }
+ s := strings.Join(newStr, rewriteType.sep)
+ if rewriteType.newLine {
+ s += "\n"
+ }
+ expr.Value = fmt.Sprintf("%q", s)
+
+ call.Args = newArgs
+
+ // TODO: consider creating an expression instead of a constant string and
+ // then wrapping it in an escape function or so:
+ // call.Args[argn+i] = &ast.CallExpr{
+ // Fun: &ast.SelectorExpr{
+ // X: ast.NewIdent("message"),
+ // Sel: ast.NewIdent("Lookup"),
+ // },
+ // Args: []ast.Expr{a},
+ // }
+ // }
+
+ return r
+}
+
+type rewriteType struct {
+ // method is the name of the equivalent method on a printer, or "" if it is
+ // the same.
+ method string
+
+ // methodf is the method to use if the arguments can be rewritten as a
+ // arguments to a printf-style call.
+ methodf string
+
+ // format is true if the method takes a formatting string followed by
+ // substitution arguments.
+ format bool
+
+ // arg indicates the position of the argument to extract. If all is
+ // positive, all arguments from this argument onwards needs to be extracted.
+ arg int
+
+ sep string
+ newLine bool
+}
+
+// rewriteFuncs list functions that can be directly mapped to the printer
+// functions of the message package.
+var rewriteFuncs = map[string]map[string]rewriteType{
+ // TODO: Printer -> *golang.org/x/text/message.Printer
+ "fmt": {
+ "Print": rewriteType{methodf: "Printf"},
+ "Sprint": rewriteType{methodf: "Sprintf"},
+ "Fprint": rewriteType{methodf: "Fprintf"},
+
+ "Println": rewriteType{methodf: "Printf", sep: " ", newLine: true},
+ "Sprintln": rewriteType{methodf: "Sprintf", sep: " ", newLine: true},
+ "Fprintln": rewriteType{methodf: "Fprintf", sep: " ", newLine: true},
+
+ "Printf": rewriteType{method: "Printf", format: true},
+ "Sprintf": rewriteType{method: "Sprintf", format: true},
+ "Fprintf": rewriteType{method: "Fprintf", format: true},
+ },
+}
+
+func constStr(info *loader.PackageInfo, e ast.Expr) (s string, ok bool) {
+ v := info.Types[e].Value
+ if v == nil || v.Kind() != constant.String {
+ return "", false
+ }
+ return constant.StringVal(v), true
+}