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>
This commit is contained in:
Marcel van Lohuizen 2017-11-22 18:34:41 -08:00
Родитель 76c26bac1a
Коммит 747f3eb757
5 изменённых файлов: 371 добавлений и 0 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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")
}

Просмотреть файл

@ -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)

Просмотреть файл

@ -87,6 +87,7 @@ func (c *Command) Runnable() bool {
// 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

304
cmd/gotext/rewrite.go Normal file
Просмотреть файл

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