610 lines
14 KiB
Go
610 lines
14 KiB
Go
/*
|
|
Copyright 2019 The Kubernetes Authors All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package extract
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/golang-collections/collections/stack"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// exclude is a list of strings to explicitly omit from translation files.
|
|
var exclude = []string{
|
|
"{{.error}}",
|
|
"{{.url}}",
|
|
" {{.url}}",
|
|
"{{.msg}}: {{.err}}",
|
|
"{{.key}}={{.value}}",
|
|
"opt {{.docker_option}}",
|
|
"kube-system",
|
|
"env {{.docker_env}}",
|
|
"\\n",
|
|
"==\u003e {{.name}} \u003c==",
|
|
"- {{.profile}}",
|
|
" - {{.profile}}",
|
|
"test/integration",
|
|
"pkg/minikube/reason/exitcodes.go",
|
|
"{{.err}}",
|
|
"{{.extra_option_component_name}}.{{.key}}={{.value}}",
|
|
"{{ .name }}: {{ .rejection }}",
|
|
"127.0.0.1",
|
|
"- {{.logPath}}",
|
|
}
|
|
|
|
// ErrMapFile is a constant to refer to the err_map file, which contains the Advice strings.
|
|
const ErrMapFile string = "pkg/minikube/reason/known_issues.go"
|
|
|
|
// state is a struct that represent the current state of the extraction process
|
|
type state struct {
|
|
// The list of functions to check for
|
|
funcs map[funcType]struct{}
|
|
|
|
// A stack representation of funcs for easy iteration
|
|
fs *stack.Stack
|
|
|
|
// The list of translatable strings, in map form for easy json marhsalling
|
|
translations map[string]interface{}
|
|
|
|
// The function call we're currently checking for
|
|
currentFunc funcType
|
|
|
|
// The function we're currently parsing
|
|
parentFunc funcType
|
|
|
|
// The file we're currently checking
|
|
filename string
|
|
|
|
// The package we're currently in
|
|
currentPackage string
|
|
}
|
|
|
|
type funcType struct {
|
|
pack string // The package the function is in
|
|
name string // The name of the function
|
|
}
|
|
|
|
// newExtractor initializes state for extraction
|
|
func newExtractor(functionsToCheck []string) (*state, error) {
|
|
funcs := make(map[funcType]struct{})
|
|
fs := stack.New()
|
|
|
|
for _, t := range functionsToCheck {
|
|
// Functions must be of the form "package.function"
|
|
t2 := strings.Split(t, ".")
|
|
if len(t2) < 2 {
|
|
return nil, errors.Errorf("invalid function string %s. Needs package name as well", t)
|
|
}
|
|
f := funcType{
|
|
pack: t2[0],
|
|
name: t2[1],
|
|
}
|
|
funcs[f] = struct{}{}
|
|
fs.Push(f)
|
|
}
|
|
|
|
return &state{
|
|
funcs: funcs,
|
|
fs: fs,
|
|
translations: make(map[string]interface{}),
|
|
}, nil
|
|
}
|
|
|
|
// SetParentFunc Sets the current parent function, along with package information
|
|
func setParentFunc(e *state, f string) {
|
|
e.parentFunc = funcType{
|
|
pack: e.currentPackage,
|
|
name: f,
|
|
}
|
|
}
|
|
|
|
// TranslatableStrings finds all strings to that need to be translated in paths and prints them out to all json files in output
|
|
func TranslatableStrings(paths []string, functions []string, output string) error {
|
|
e, err := newExtractor(functions)
|
|
|
|
if err != nil {
|
|
return errors.Wrap(err, "Initializing")
|
|
}
|
|
|
|
fmt.Println("Compiling translation strings...")
|
|
for e.fs.Len() > 0 {
|
|
f := e.fs.Pop().(funcType)
|
|
e.currentFunc = f
|
|
for _, root := range paths {
|
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if shouldCheckFile(path) {
|
|
e.filename = path
|
|
return inspectFile(e)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return errors.Wrap(err, "Extracting strings")
|
|
}
|
|
}
|
|
}
|
|
|
|
err = writeStringsToFiles(e, output)
|
|
|
|
if err != nil {
|
|
return errors.Wrap(err, "Writing translation files")
|
|
}
|
|
|
|
fmt.Println("Done!")
|
|
return nil
|
|
}
|
|
|
|
func shouldCheckFile(path string) bool {
|
|
return strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go")
|
|
}
|
|
|
|
// inspectFile goes through the given file line by line looking for translatable strings
|
|
func inspectFile(e *state) error {
|
|
fset := token.NewFileSet()
|
|
r, err := os.ReadFile(e.filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file, err := parser.ParseFile(fset, "", r, parser.ParseComments)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if e.filename == ErrMapFile {
|
|
return extractAdvice(file, e)
|
|
}
|
|
|
|
ast.Inspect(file, func(x ast.Node) bool {
|
|
if fi, ok := x.(*ast.File); ok {
|
|
e.currentPackage = fi.Name.String()
|
|
return true
|
|
}
|
|
|
|
if fd, ok := x.(*ast.FuncDecl); ok {
|
|
setParentFunc(e, fd.Name.String())
|
|
return true
|
|
}
|
|
|
|
checkNode(x, e)
|
|
return true
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkNode checks each node to see if it's a function call
|
|
func checkNode(stmt ast.Node, e *state) {
|
|
// This line is a function call, that's what we care about
|
|
if expr, ok := stmt.(*ast.CallExpr); ok {
|
|
checkCallExpression(expr, e)
|
|
}
|
|
|
|
// Check all key value pairs for possible help text
|
|
if kvp, ok := stmt.(*ast.KeyValueExpr); ok {
|
|
checkKeyValueExpression(kvp, e)
|
|
}
|
|
}
|
|
|
|
// checkCallExpression takes a function call, and checks its arguments for strings
|
|
func checkCallExpression(s *ast.CallExpr, e *state) {
|
|
for _, arg := range s.Args {
|
|
// This argument is a function literal, check its body.
|
|
if fl, ok := arg.(*ast.FuncLit); ok {
|
|
for _, stmt := range fl.Body.List {
|
|
checkNode(stmt, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
var functionName string
|
|
var packageName string
|
|
|
|
// SelectorExpr is a function call to a separate package
|
|
sf, ok := s.Fun.(*ast.SelectorExpr)
|
|
if ok {
|
|
// Parse out the package of the call
|
|
sfi, ok := sf.X.(*ast.Ident)
|
|
if !ok {
|
|
if sfc, ok := sf.X.(*ast.CallExpr); ok {
|
|
extractFlagHelpText(s, sfc, e)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
packageName = sfi.Name
|
|
functionName = sf.Sel.Name
|
|
}
|
|
|
|
// Ident is an identifier, in this case it's a function call in the same package
|
|
id, ok := s.Fun.(*ast.Ident)
|
|
if ok {
|
|
functionName = id.Name
|
|
packageName = e.currentPackage
|
|
}
|
|
|
|
// This is not a function call.
|
|
if len(functionName) == 0 {
|
|
return
|
|
}
|
|
|
|
// This is not the correct function call, or it was called with no arguments.
|
|
if e.currentFunc.name != functionName || e.currentFunc.pack != packageName || len(s.Args) == 0 {
|
|
return
|
|
}
|
|
|
|
checkArguments(s, e)
|
|
}
|
|
|
|
// checkArguments checks the arguments of a function call for strings
|
|
func checkArguments(s *ast.CallExpr, e *state) {
|
|
matched := false
|
|
for _, arg := range s.Args {
|
|
// This argument is an identifier.
|
|
if i, ok := arg.(*ast.Ident); ok {
|
|
if s := checkIdentForStringValue(i); s != "" {
|
|
e.translations[s] = ""
|
|
matched = true
|
|
}
|
|
}
|
|
|
|
// This argument is a string.
|
|
if argString, ok := arg.(*ast.BasicLit); ok {
|
|
if s := checkString(argString.Value); s != "" {
|
|
e.translations[s] = ""
|
|
matched = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// No string arguments were found, check everything that calls this function for strings
|
|
if !matched {
|
|
addParentFuncToList(e)
|
|
}
|
|
|
|
}
|
|
|
|
// checkIdentForStringValue takes a identifier and sees if it's a variable assigned to a string
|
|
func checkIdentForStringValue(i *ast.Ident) string {
|
|
// This identifier is nil
|
|
if i.Obj == nil {
|
|
return ""
|
|
}
|
|
|
|
var s string
|
|
|
|
// This identifier was directly assigned a value
|
|
if as, ok := i.Obj.Decl.(*ast.AssignStmt); ok {
|
|
if rhs, ok := as.Rhs[0].(*ast.BasicLit); ok {
|
|
s = rhs.Value
|
|
}
|
|
}
|
|
|
|
// This Identifier is part of the const or var declaration
|
|
if vs, ok := i.Obj.Decl.(*ast.ValueSpec); ok {
|
|
for j, n := range vs.Names {
|
|
if n.Name == i.Name {
|
|
if len(vs.Values) < j+1 {
|
|
// There's no way anything was assigned here, abort
|
|
return ""
|
|
}
|
|
if v, ok := vs.Values[j].(*ast.BasicLit); ok {
|
|
s = v.Value
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return checkString(s)
|
|
|
|
}
|
|
|
|
// checkString checks if a string is meant to be translated
|
|
func checkString(s string) string {
|
|
// Empty strings don't need translating
|
|
if len(s) <= 2 {
|
|
return ""
|
|
}
|
|
|
|
// Parse out quote marks
|
|
stringToTranslate := s[1 : len(s)-1]
|
|
|
|
// Don't translate integers
|
|
if _, err := strconv.Atoi(stringToTranslate); err == nil {
|
|
return ""
|
|
}
|
|
|
|
// Don't translate URLs
|
|
if u, err := url.Parse(stringToTranslate); err == nil && u.Scheme != "" && u.Host != "" {
|
|
return ""
|
|
}
|
|
|
|
// Don't translate commands
|
|
if strings.HasPrefix(stringToTranslate, "sudo ") {
|
|
return ""
|
|
}
|
|
|
|
// Don't translate excluded strings
|
|
for _, e := range exclude {
|
|
if e == stringToTranslate {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Remove unnecessary backslashes
|
|
if s[0] == '"' {
|
|
r := strings.NewReplacer(`\\`, "\\", `\a`, "\a", `\b`, "\b", `\f`, "\f", `\n`, "\n", `\r`, "\r", `\t`, "\t", `\v`, "\v", `\"`, "\"")
|
|
stringToTranslate = r.Replace(stringToTranslate)
|
|
}
|
|
|
|
// Hooray, we can translate the string!
|
|
return stringToTranslate
|
|
}
|
|
|
|
// checkKeyValueExpression checks all kvps for help text
|
|
func checkKeyValueExpression(kvp *ast.KeyValueExpr, e *state) {
|
|
// The key must be an identifier
|
|
i, ok := kvp.Key.(*ast.Ident)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Specifically, it needs to be "Short" or "Long"
|
|
if i.Name == "Short" || i.Name == "Long" {
|
|
// The help text is directly a string, the most common case
|
|
if help, ok := kvp.Value.(*ast.BasicLit); ok {
|
|
s := checkString(help.Value)
|
|
if s != "" {
|
|
e.translations[s] = ""
|
|
}
|
|
}
|
|
|
|
// The help text is assigned to a variable, only happens if it's very long
|
|
if help, ok := kvp.Value.(*ast.Ident); ok {
|
|
s := checkIdentForStringValue(help)
|
|
if s != "" {
|
|
e.translations[s] = ""
|
|
}
|
|
}
|
|
|
|
// Ok now this is just a mess
|
|
if help, ok := kvp.Value.(*ast.BinaryExpr); ok {
|
|
s := checkBinaryExpression(help)
|
|
if s != "" {
|
|
e.translations[s] = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkBinaryExpression checks binary expressions, stuff of the form x + y, for strings and concats them
|
|
func checkBinaryExpression(b *ast.BinaryExpr) string {
|
|
// Check the left side
|
|
var s string
|
|
if l, ok := b.X.(*ast.BasicLit); ok {
|
|
if x := checkString(l.Value); x != "" {
|
|
s += x
|
|
}
|
|
}
|
|
|
|
if i, ok := b.X.(*ast.Ident); ok {
|
|
if x := checkIdentForStringValue(i); x != "" {
|
|
s += x
|
|
}
|
|
}
|
|
|
|
if b1, ok := b.X.(*ast.BinaryExpr); ok {
|
|
if x := checkBinaryExpression(b1); x != "" {
|
|
s += x
|
|
}
|
|
}
|
|
|
|
// Check the right side
|
|
if l, ok := b.Y.(*ast.BasicLit); ok {
|
|
if x := checkString(l.Value); x != "" {
|
|
s += x
|
|
}
|
|
}
|
|
|
|
if i, ok := b.Y.(*ast.Ident); ok {
|
|
if x := checkIdentForStringValue(i); x != "" {
|
|
s += x
|
|
}
|
|
}
|
|
|
|
if b1, ok := b.Y.(*ast.BinaryExpr); ok {
|
|
if x := checkBinaryExpression(b1); x != "" {
|
|
s += x
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// writeStringsToFiles writes translations to all translation files in output
|
|
func writeStringsToFiles(e *state, output string) error {
|
|
err := filepath.Walk(output, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return errors.Wrap(err, "accessing path")
|
|
}
|
|
if info.Mode().IsDir() {
|
|
return nil
|
|
}
|
|
if !strings.HasSuffix(path, ".json") {
|
|
return nil
|
|
}
|
|
fmt.Printf("Writing to %s", filepath.Base(path))
|
|
currentTranslations := make(map[string]interface{})
|
|
f, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading translation file")
|
|
}
|
|
// Unmarhsal nonempty files
|
|
if len(f) > 0 {
|
|
err = json.Unmarshal(f, ¤tTranslations)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unmarshalling current translations")
|
|
}
|
|
}
|
|
|
|
// Make sure to not overwrite already translated strings
|
|
for k := range e.translations {
|
|
if _, ok := currentTranslations[k]; !ok {
|
|
currentTranslations[k] = ""
|
|
}
|
|
}
|
|
|
|
// Remove translations from the file that are empty and were not extracted
|
|
for k, v := range currentTranslations {
|
|
if _, ok := e.translations[k]; !ok && len(v.(string)) == 0 {
|
|
delete(currentTranslations, k)
|
|
}
|
|
}
|
|
|
|
t := 0 // translated
|
|
u := 0 // untranslated
|
|
for k := range e.translations {
|
|
if currentTranslations[k] != "" {
|
|
t++
|
|
} else {
|
|
u++
|
|
}
|
|
}
|
|
|
|
c, err := json.MarshalIndent(currentTranslations, "", "\t")
|
|
if err != nil {
|
|
return errors.Wrap(err, "marshalling translations")
|
|
}
|
|
err = os.WriteFile(path, c, info.Mode())
|
|
if err != nil {
|
|
return errors.Wrap(err, "writing translation file")
|
|
}
|
|
|
|
fmt.Printf(" (%d translated, %d untranslated)\n", t, u)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c, err := json.MarshalIndent(e.translations, "", "\t")
|
|
if err != nil {
|
|
return errors.Wrap(err, "marshalling translations")
|
|
}
|
|
path := filepath.Join(output, "strings.txt")
|
|
err = os.WriteFile(path, c, 0644)
|
|
if err != nil {
|
|
return errors.Wrap(err, "writing translation file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addParentFuncToList adds the current parent function to the list of functions to inspect more closely.
|
|
func addParentFuncToList(e *state) {
|
|
if _, ok := e.funcs[e.parentFunc]; !ok {
|
|
e.funcs[e.parentFunc] = struct{}{}
|
|
e.fs.Push(e.parentFunc)
|
|
}
|
|
}
|
|
|
|
// extractAdvice specifically extracts Advice strings in err_map.go, since they don't conform to our normal translatable string format.
|
|
func extractAdvice(f ast.Node, e *state) error {
|
|
ast.Inspect(f, func(x ast.Node) bool {
|
|
// We want the "Advice: <advice string>" key-value pair
|
|
// First make sure we're looking at a kvp
|
|
kvp, ok := x.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// Now make sure we're looking at an Advice kvp
|
|
i, ok := kvp.Key.(*ast.Ident)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
if i.Name == "Advice" {
|
|
// At this point we know the value in the kvp is guaranteed to be a string
|
|
advice, ok := kvp.Value.(*ast.BasicLit)
|
|
if !ok {
|
|
// Maybe the advice is a function call, like fmt.Sprintf
|
|
ad, ok := kvp.Value.(*ast.CallExpr)
|
|
if !ok {
|
|
// Ok, we tried. Abort.
|
|
return true
|
|
}
|
|
// Let's support fmt.Sprintf with a string argument only
|
|
for _, arg := range ad.Args {
|
|
adArg, ok := arg.(*ast.BasicLit)
|
|
if !ok {
|
|
continue
|
|
}
|
|
s := checkString(adArg.Value)
|
|
if s != "" {
|
|
e.translations[s] = ""
|
|
}
|
|
}
|
|
return true
|
|
|
|
}
|
|
s := checkString(advice.Value)
|
|
if s != "" {
|
|
e.translations[s] = ""
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractFlagHelpText finds usage text for all command flags and adds them to the list to translate
|
|
func extractFlagHelpText(c *ast.CallExpr, sfc *ast.CallExpr, e *state) {
|
|
// We're looking for calls of the form cmd.Flags().VarP()
|
|
flags, ok := sfc.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if flags.Sel.Name != "Flags" || len(c.Args) == 1 {
|
|
return
|
|
}
|
|
|
|
// The usage text for flags is always the final argument in the Flags() call
|
|
usage, ok := c.Args[len(c.Args)-1].(*ast.BasicLit)
|
|
if !ok {
|
|
// Something has gone wrong, abort
|
|
return
|
|
}
|
|
s := checkString(usage.Value)
|
|
if s != "" {
|
|
e.translations[s] = ""
|
|
}
|
|
}
|