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
+}