diff --git a/build/lint-locale-usage/lint-locale-usage.go b/build/lint-locale-usage/lint-locale-usage.go index f42bc59cbb..e1d04da301 100644 --- a/build/lint-locale-usage/lint-locale-usage.go +++ b/build/lint-locale-usage/lint-locale-usage.go @@ -26,8 +26,6 @@ import ( // this works by first gathering all valid source string IDs from `en-US` reference files // and then checking if all used source strings are actually defined -type OnMsgidHandler func(fset *token.FileSet, pos token.Pos, msgid string) - type LocatedError struct { Location string Kind string @@ -49,8 +47,24 @@ func (e LocatedError) Error() string { return sb.String() } -func isLocaleTrFunction(funcname string) bool { - return funcname == "Tr" || funcname == "TrN" +func InitLocaleTrFunctions() map[string][]uint { + ret := make(map[string][]uint) + + f0 := []uint{0} + ret["Tr"] = f0 + ret["TrString"] = f0 + ret["TrHTML"] = f0 + + ret["TrPluralString"] = []uint{1} + ret["TrN"] = []uint{1, 2} + + return ret +} + +type Handler struct { + OnMsgid func(fset *token.FileSet, pos token.Pos, msgid string) + OnUnexpectedInvoke func(fset *token.FileSet, pos token.Pos, funcname string, argc int) + LocaleTrFunctions map[string][]uint } // the `Handle*File` functions follow the following calling convention: @@ -58,7 +72,7 @@ func isLocaleTrFunction(funcname string) bool { // * `src` is either `nil` (then the function invokes `ReadFile` to read the file) // or the contents of the file as {`[]byte`, or a `string`} -func (omh OnMsgidHandler) HandleGoFile(fname string, src any) error { +func (handler Handler) HandleGoFile(fname string, src any) error { fset := token.NewFileSet() node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution) if err != nil { @@ -70,31 +84,47 @@ func (omh OnMsgidHandler) HandleGoFile(fname string, src any) error { } ast.Inspect(node, func(n ast.Node) bool { - // search for function calls of the form `anything.Tr(any-string-lit)` + // search for function calls of the form `anything.Tr(any-string-lit, ...)` call, ok := n.(*ast.CallExpr) - if !ok || len(call.Args) != 1 { + if !ok || len(call.Args) < 1 { return true } funSel, ok := call.Fun.(*ast.SelectorExpr) - if (!ok) || !isLocaleTrFunction(funSel.Sel.Name) { + if !ok { return true } - argLit, ok := call.Args[0].(*ast.BasicLit) - if (!ok) || argLit.Kind != token.STRING { + ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name] + if !ok { return true } - // extract string content - arg, err := strconv.Unquote(argLit.Value) - if err != nil { - return true + var gotUnexpectedInvoke *int + + for _, argNum := range ltf { + if len(call.Args) >= int(argNum+1) { + argLit, ok := call.Args[int(argNum)].(*ast.BasicLit) + if !ok || argLit.Kind != token.STRING { + continue + } + + // extract string content + arg, err := strconv.Unquote(argLit.Value) + if err == nil { + // found interesting strings + handler.OnMsgid(fset, argLit.ValuePos, arg) + } + } else { + argc := len(call.Args) + gotUnexpectedInvoke = &argc + } } - // found interesting string - omh(fset, argLit.ValuePos, arg) + if gotUnexpectedInvoke != nil { + handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke) + } return true }) @@ -103,33 +133,33 @@ func (omh OnMsgidHandler) HandleGoFile(fname string, src any) error { } // derived from source: modules/templates/scopedtmpl/scopedtmpl.go, L169-L213 -func (omh OnMsgidHandler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) { +func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) { switch node.Type() { case tmplParser.NodeAction: - omh.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe) + handler.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe) case tmplParser.NodeList: nodeList := node.(*tmplParser.ListNode) - omh.handleTemplateFileNodes(fset, nodeList.Nodes) + handler.handleTemplateFileNodes(fset, nodeList.Nodes) case tmplParser.NodePipe: - omh.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode)) + handler.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode)) case tmplParser.NodeTemplate: - omh.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe) + handler.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe) case tmplParser.NodeIf: nodeIf := node.(*tmplParser.IfNode) - omh.handleTemplateBranchNode(fset, nodeIf.BranchNode) + handler.handleTemplateBranchNode(fset, nodeIf.BranchNode) case tmplParser.NodeRange: nodeRange := node.(*tmplParser.RangeNode) - omh.handleTemplateBranchNode(fset, nodeRange.BranchNode) + handler.handleTemplateBranchNode(fset, nodeRange.BranchNode) case tmplParser.NodeWith: nodeWith := node.(*tmplParser.WithNode) - omh.handleTemplateBranchNode(fset, nodeWith.BranchNode) + handler.handleTemplateBranchNode(fset, nodeWith.BranchNode) case tmplParser.NodeCommand: nodeCommand := node.(*tmplParser.CommandNode) - omh.handleTemplateFileNodes(fset, nodeCommand.Args) + handler.handleTemplateFileNodes(fset, nodeCommand.Args) - if len(nodeCommand.Args) != 2 { + if len(nodeCommand.Args) < 2 { return } @@ -138,54 +168,66 @@ func (omh OnMsgidHandler) handleTemplateNode(fset *token.FileSet, node tmplParse return } - nodeString, ok := nodeCommand.Args[1].(*tmplParser.StringNode) + nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode) + if !ok || nodeIdent.Ident != "ctx" || len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" { + return + } + + ltf, ok := handler.LocaleTrFunctions[nodeChain.Field[1]] if !ok { return } - nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode) - if !ok || nodeIdent.Ident != "ctx" { - return + var gotUnexpectedInvoke *int + + for _, argNum := range ltf { + if len(nodeCommand.Args) >= int(argNum+2) { + nodeString, ok := nodeCommand.Args[int(argNum+1)].(*tmplParser.StringNode) + if ok { + // found interesting strings + // the column numbers are a bit "off", but much better than nothing + handler.OnMsgid(fset, token.Pos(nodeString.Pos), nodeString.Text) + } + } else { + argc := len(nodeCommand.Args) - 1 + gotUnexpectedInvoke = &argc + } } - if len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" || !isLocaleTrFunction(nodeChain.Field[1]) { - return + if gotUnexpectedInvoke != nil { + handler.OnUnexpectedInvoke(fset, token.Pos(nodeChain.Pos), nodeChain.Field[1], *gotUnexpectedInvoke) } - // found interesting string - // the column numbers are a bit "off", but much better than nothing - omh(fset, token.Pos(nodeString.Pos), nodeString.Text) - default: } } -func (omh OnMsgidHandler) handleTemplatePipeNode(fset *token.FileSet, pipeNode *tmplParser.PipeNode) { +func (handler Handler) handleTemplatePipeNode(fset *token.FileSet, pipeNode *tmplParser.PipeNode) { if pipeNode == nil { return } // NOTE: we can't pass `pipeNode.Cmds` to handleTemplateFileNodes due to incompatible argument types for _, node := range pipeNode.Cmds { - omh.handleTemplateNode(fset, node) + handler.handleTemplateNode(fset, node) } } -func (omh OnMsgidHandler) handleTemplateBranchNode(fset *token.FileSet, branchNode tmplParser.BranchNode) { - omh.handleTemplatePipeNode(fset, branchNode.Pipe) - omh.handleTemplateFileNodes(fset, branchNode.List.Nodes) +func (handler Handler) handleTemplateBranchNode(fset *token.FileSet, branchNode tmplParser.BranchNode) { + handler.handleTemplatePipeNode(fset, branchNode.Pipe) + handler.handleTemplateFileNodes(fset, branchNode.List.Nodes) if branchNode.ElseList != nil { - omh.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes) + handler.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes) } } -func (omh OnMsgidHandler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) { +func (handler Handler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) { for _, node := range nodes { - omh.handleTemplateNode(fset, node) + handler.handleTemplateNode(fset, node) } } -func (omh OnMsgidHandler) HandleTemplateFile(fname string, src any) error { +func (handler Handler) HandleTemplateFile(fname string, src any) error { var tmplContent []byte switch src2 := src.(type) { case nil: @@ -222,7 +264,7 @@ func (omh OnMsgidHandler) HandleTemplateFile(fname string, src any) error { Err: err, } } - omh.handleTemplateFileNodes(fset, tmplParsed.Tree.Root.Nodes) + handler.handleTemplateFileNodes(fset, tmplParsed.Tree.Root.Nodes) return nil } @@ -289,12 +331,19 @@ func main() { gotAnyMsgidError := false - omh := OnMsgidHandler(func(fset *token.FileSet, pos token.Pos, msgid string) { - if !msgids.Contains(msgid) { + handler := Handler{ + OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string) { + if !msgids.Contains(msgid) { + gotAnyMsgidError = true + fmt.Printf("%s:\tmissing msgid: %s\n", fset.Position(pos).String(), msgid) + } + }, + OnUnexpectedInvoke: func(fset *token.FileSet, pos token.Pos, funcname string, argc int) { gotAnyMsgidError = true - fmt.Printf("%s:\tmissing msgid: %s\n", fset.Position(pos).String(), msgid) - } - }) + fmt.Printf("%s:\tunexpected invocation of %s with %d arguments\n", fset.Position(pos).String(), funcname, argc) + }, + LocaleTrFunctions: InitLocaleTrFunctions(), + } if err := filepath.WalkDir(".", func(fpath string, d fs.DirEntry, err error) error { if err != nil { @@ -308,15 +357,15 @@ func main() { if name == "docker" || name == ".git" || name == "node_modules" { return fs.SkipDir } - } else if name == "bindata.go" { + } else if name == "bindata.go" || fpath == "modules/translation/i18n/i18n_test.go" { // skip false positives } else if strings.HasSuffix(name, ".go") { - onError(omh.HandleGoFile(fpath, nil)) + onError(handler.HandleGoFile(fpath, nil)) } else if strings.HasSuffix(name, ".tmpl") { if strings.HasPrefix(fpath, "tests") && strings.HasSuffix(name, ".ini.tmpl") { // skip false positives } else { - onError(omh.HandleTemplateFile(fpath, nil)) + onError(handler.HandleTemplateFile(fpath, nil)) } } return nil diff --git a/build/lint-locale-usage/lint-locale-usage_test.go b/build/lint-locale-usage/lint-locale-usage_test.go index 3b3b746053..e573fb196e 100644 --- a/build/lint-locale-usage/lint-locale-usage_test.go +++ b/build/lint-locale-usage/lint-locale-usage_test.go @@ -11,21 +11,27 @@ import ( "github.com/stretchr/testify/require" ) +func buildHandler(ret *[]string) Handler { + return Handler{ + OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string) { + *ret = append(*ret, msgid) + }, + OnUnexpectedInvoke: func(fset *token.FileSet, pos token.Pos, funcname string, argc int) {}, + LocaleTrFunctions: InitLocaleTrFunctions(), + } +} + func HandleGoFileWrapped(t *testing.T, fname, src string) []string { var ret []string - omh := OnMsgidHandler(func(fset *token.FileSet, pos token.Pos, msgid string) { - ret = append(ret, msgid) - }) - require.NoError(t, omh.HandleGoFile(fname, src)) + handler := buildHandler(&ret) + require.NoError(t, handler.HandleGoFile(fname, src)) return ret } func HandleTemplateFileWrapped(t *testing.T, fname, src string) []string { var ret []string - omh := OnMsgidHandler(func(fset *token.FileSet, pos token.Pos, msgid string) { - ret = append(ret, msgid) - }) - require.NoError(t, omh.HandleTemplateFile(fname, src)) + handler := buildHandler(&ret) + require.NoError(t, handler.HandleTemplateFile(fname, src)) return ret } diff --git a/modules/translation/translation.go b/modules/translation/translation.go index 7be77536ca..3ecf8715a3 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -27,6 +27,10 @@ type contextKey struct{} var ContextKey any = &contextKey{} // Locale represents an interface to translation +// +// If this gets modified, remember to also adjust +// build/lint-locale-usage/lint-locale-usage.go's InitLocaleTrFunctions(), +// which requires to know in what argument positions `trKey`'s are given. type Locale interface { Language() string TrString(string, ...any) string