mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-30 06:03:58 +00:00
refactor(build): split lint-locale-usage into multiple files
This commit is contained in:
parent
31190e385e
commit
2533ddfc18
3 changed files with 340 additions and 313 deletions
180
build/lint-locale-usage/handle-go.go
Normal file
180
build/lint-locale-usage/handle-go.go
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
goParser "go/parser"
|
||||||
|
"go/token"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (handler Handler) handleGoTrBasicLit(fset *token.FileSet, argLit *ast.BasicLit) {
|
||||||
|
if argLit.Kind == token.STRING {
|
||||||
|
// extract string content
|
||||||
|
arg, err := strconv.Unquote(argLit.Value)
|
||||||
|
if err == nil {
|
||||||
|
// found interesting strings
|
||||||
|
if strings.HasSuffix(arg, ".") || strings.HasSuffix(arg, "_") {
|
||||||
|
handler.OnMsgidPrefix(fset, argLit.ValuePos, arg)
|
||||||
|
} else {
|
||||||
|
handler.OnMsgid(fset, argLit.ValuePos, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler Handler) handleGoTrArgument(fset *token.FileSet, n ast.Expr) {
|
||||||
|
if argLit, ok := n.(*ast.BasicLit); ok {
|
||||||
|
handler.handleGoTrBasicLit(fset, argLit)
|
||||||
|
} else if argBinExpr, ok := n.(*ast.BinaryExpr); ok {
|
||||||
|
if argBinExpr.Op != token.ADD {
|
||||||
|
// pass
|
||||||
|
} else if argLit, ok := argBinExpr.X.(*ast.BasicLit); ok && argLit.Kind == token.STRING {
|
||||||
|
// extract string content
|
||||||
|
arg, err := strconv.Unquote(argLit.Value)
|
||||||
|
if err == nil {
|
||||||
|
// found interesting strings
|
||||||
|
handler.OnMsgidPrefix(fset, argLit.ValuePos, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the `Handle*File` functions follow the following calling convention:
|
||||||
|
// * `fname` is the name of the input file
|
||||||
|
// * `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 (handler Handler) HandleGoFile(fname string, src any) error {
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution|goParser.ParseComments)
|
||||||
|
if err != nil {
|
||||||
|
return LocatedError{
|
||||||
|
Location: fname,
|
||||||
|
Kind: "Go parser",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.Inspect(node, func(n ast.Node) bool {
|
||||||
|
// search for function calls of the form `anything.Tr(any-string-lit, ...)`
|
||||||
|
|
||||||
|
if call, ok := n.(*ast.CallExpr); ok && len(call.Args) >= 1 {
|
||||||
|
funSel, ok := call.Fun.(*ast.SelectorExpr)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name]
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotUnexpectedInvoke *int
|
||||||
|
|
||||||
|
for _, argNum := range ltf {
|
||||||
|
if len(call.Args) < int(argNum+1) {
|
||||||
|
argc := len(call.Args)
|
||||||
|
gotUnexpectedInvoke = &argc
|
||||||
|
} else {
|
||||||
|
handler.handleGoTrArgument(fset, call.Args[int(argNum)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotUnexpectedInvoke != nil {
|
||||||
|
handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke)
|
||||||
|
}
|
||||||
|
} else if composite, ok := n.(*ast.CompositeLit); ok {
|
||||||
|
ident, ok := composite.Type.(*ast.Ident)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// special case: models/unit/unit.go
|
||||||
|
if strings.HasSuffix(fname, "unit.go") && ident.Name == "Unit" && len(composite.Elts) == 6 {
|
||||||
|
// NameKey has index 2
|
||||||
|
// invoked like '{{ctx.Locale.Tr $unit.NameKey}}'
|
||||||
|
nameKey, ok := composite.Elts[2].(*ast.BasicLit)
|
||||||
|
if !ok || nameKey.Kind != token.STRING {
|
||||||
|
handler.OnWarning(fset, composite.Elts[2].Pos(), "unexpected initialization of 'Unit'")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract string content
|
||||||
|
arg, err := strconv.Unquote(nameKey.Value)
|
||||||
|
if err == nil {
|
||||||
|
// found interesting strings
|
||||||
|
handler.OnMsgid(fset, nameKey.ValuePos, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if function, ok := n.(*ast.FuncDecl); ok {
|
||||||
|
matches := false
|
||||||
|
if function.Doc != nil {
|
||||||
|
for _, comment := range function.Doc.List {
|
||||||
|
if strings.TrimSpace(comment.Text) == "//llu:returnsTrKey" {
|
||||||
|
matches = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matches {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
results := function.Type.Results.List
|
||||||
|
if len(results) != 1 {
|
||||||
|
handler.OnWarning(fset, function.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", function.Name.Name))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.Inspect(function.Body, func(n ast.Node) bool {
|
||||||
|
// search for return stmts
|
||||||
|
// TODO: what about nested functions?
|
||||||
|
if ret, ok := n.(*ast.ReturnStmt); ok {
|
||||||
|
for _, res := range ret.Results {
|
||||||
|
ast.Inspect(res, func(n ast.Node) bool {
|
||||||
|
if expr, ok := n.(ast.Expr); ok {
|
||||||
|
handler.handleGoTrArgument(fset, expr)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} else if decl, ok := n.(*ast.GenDecl); ok && (decl.Tok == token.CONST || decl.Tok == token.VAR) {
|
||||||
|
matches := false
|
||||||
|
if decl.Doc != nil {
|
||||||
|
for _, comment := range decl.Doc.List {
|
||||||
|
if strings.TrimSpace(comment.Text) == "// llu:TrKeys" {
|
||||||
|
matches = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matches {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, spec := range decl.Specs {
|
||||||
|
// interpret all contained strings as message IDs
|
||||||
|
ast.Inspect(spec, func(n ast.Node) bool {
|
||||||
|
if argLit, ok := n.(*ast.BasicLit); ok {
|
||||||
|
handler.handleGoTrBasicLit(fset, argLit)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
160
build/lint-locale-usage/handle-tmpl.go
Normal file
160
build/lint-locale-usage/handle-tmpl.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/token"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
tmplParser "text/template/parse"
|
||||||
|
|
||||||
|
fjTemplates "forgejo.org/modules/templates"
|
||||||
|
"forgejo.org/modules/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// derived from source: modules/templates/scopedtmpl/scopedtmpl.go, L169-L213
|
||||||
|
func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) {
|
||||||
|
switch node.Type() {
|
||||||
|
case tmplParser.NodeAction:
|
||||||
|
handler.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe)
|
||||||
|
case tmplParser.NodeList:
|
||||||
|
nodeList := node.(*tmplParser.ListNode)
|
||||||
|
handler.handleTemplateFileNodes(fset, nodeList.Nodes)
|
||||||
|
case tmplParser.NodePipe:
|
||||||
|
handler.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode))
|
||||||
|
case tmplParser.NodeTemplate:
|
||||||
|
handler.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe)
|
||||||
|
case tmplParser.NodeIf:
|
||||||
|
nodeIf := node.(*tmplParser.IfNode)
|
||||||
|
handler.handleTemplateBranchNode(fset, nodeIf.BranchNode)
|
||||||
|
case tmplParser.NodeRange:
|
||||||
|
nodeRange := node.(*tmplParser.RangeNode)
|
||||||
|
handler.handleTemplateBranchNode(fset, nodeRange.BranchNode)
|
||||||
|
case tmplParser.NodeWith:
|
||||||
|
nodeWith := node.(*tmplParser.WithNode)
|
||||||
|
handler.handleTemplateBranchNode(fset, nodeWith.BranchNode)
|
||||||
|
|
||||||
|
case tmplParser.NodeCommand:
|
||||||
|
nodeCommand := node.(*tmplParser.CommandNode)
|
||||||
|
|
||||||
|
handler.handleTemplateFileNodes(fset, nodeCommand.Args)
|
||||||
|
|
||||||
|
if len(nodeCommand.Args) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funcname := ""
|
||||||
|
if nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode); ok {
|
||||||
|
if nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode); ok {
|
||||||
|
if nodeIdent.Ident != "ctx" || len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
funcname = nodeChain.Field[1]
|
||||||
|
}
|
||||||
|
} else if nodeField, ok := nodeCommand.Args[0].(*tmplParser.FieldNode); ok {
|
||||||
|
if len(nodeField.Ident) != 2 || !(nodeField.Ident[0] == "locale" || nodeField.Ident[0] == "Locale") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
funcname = nodeField.Ident[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotUnexpectedInvoke *int
|
||||||
|
ltf, ok := handler.LocaleTrFunctions[funcname]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 gotUnexpectedInvoke != nil {
|
||||||
|
handler.OnUnexpectedInvoke(fset, token.Pos(nodeCommand.Pos), funcname, *gotUnexpectedInvoke)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
handler.handleTemplateNode(fset, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
handler.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler Handler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) {
|
||||||
|
for _, node := range nodes {
|
||||||
|
handler.handleTemplateNode(fset, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the `Handle*File` functions follow the following calling convention:
|
||||||
|
// * `fname` is the name of the input file
|
||||||
|
// * `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 (handler Handler) HandleTemplateFile(fname string, src any) error {
|
||||||
|
var tmplContent []byte
|
||||||
|
switch src2 := src.(type) {
|
||||||
|
case nil:
|
||||||
|
var err error
|
||||||
|
tmplContent, err = os.ReadFile(fname)
|
||||||
|
if err != nil {
|
||||||
|
return LocatedError{
|
||||||
|
Location: fname,
|
||||||
|
Kind: "ReadFile",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
tmplContent = src2
|
||||||
|
case string:
|
||||||
|
// SAFETY: we do not modify tmplContent below
|
||||||
|
tmplContent = util.UnsafeStringToBytes(src2)
|
||||||
|
default:
|
||||||
|
panic("invalid type for 'src'")
|
||||||
|
}
|
||||||
|
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
fset.AddFile(fname, 1, len(tmplContent)).SetLinesForContent(tmplContent)
|
||||||
|
// SAFETY: we do not modify tmplContent2 below
|
||||||
|
tmplContent2 := util.UnsafeBytesToString(tmplContent)
|
||||||
|
|
||||||
|
tmpl := template.New(fname)
|
||||||
|
tmpl.Funcs(fjTemplates.NewFuncMap())
|
||||||
|
tmplParsed, err := tmpl.Parse(tmplContent2)
|
||||||
|
if err != nil {
|
||||||
|
return LocatedError{
|
||||||
|
Location: fname,
|
||||||
|
Kind: "Template parser",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.handleTemplateFileNodes(fset, tmplParsed.Root.Nodes)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -7,22 +7,15 @@ package main
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
|
||||||
goParser "go/parser"
|
|
||||||
"go/token"
|
"go/token"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
|
||||||
tmplParser "text/template/parse"
|
|
||||||
|
|
||||||
"forgejo.org/modules/container"
|
"forgejo.org/modules/container"
|
||||||
fjTemplates "forgejo.org/modules/templates"
|
|
||||||
"forgejo.org/modules/translation/localeiter"
|
"forgejo.org/modules/translation/localeiter"
|
||||||
"forgejo.org/modules/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// this works by first gathering all valid source string IDs from `en-US` reference files
|
// this works by first gathering all valid source string IDs from `en-US` reference files
|
||||||
|
@ -147,312 +140,6 @@ func ParseAllowedMaskedUsages(fname string, usedMsgids *container.Set[string], a
|
||||||
return scanner.Err()
|
return scanner.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler Handler) HandleGoTrBasicLit(fset *token.FileSet, argLit *ast.BasicLit) {
|
|
||||||
if argLit.Kind == token.STRING {
|
|
||||||
// extract string content
|
|
||||||
arg, err := strconv.Unquote(argLit.Value)
|
|
||||||
if err == nil {
|
|
||||||
// found interesting strings
|
|
||||||
if strings.HasSuffix(arg, ".") || strings.HasSuffix(arg, "_") {
|
|
||||||
handler.OnMsgidPrefix(fset, argLit.ValuePos, arg)
|
|
||||||
} else {
|
|
||||||
handler.OnMsgid(fset, argLit.ValuePos, arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler Handler) HandleGoTrArgument(fset *token.FileSet, n ast.Expr) {
|
|
||||||
if argLit, ok := n.(*ast.BasicLit); ok {
|
|
||||||
handler.HandleGoTrBasicLit(fset, argLit)
|
|
||||||
} else if argBinExpr, ok := n.(*ast.BinaryExpr); ok {
|
|
||||||
if argBinExpr.Op != token.ADD {
|
|
||||||
// pass
|
|
||||||
} else if argLit, ok := argBinExpr.X.(*ast.BasicLit); ok && argLit.Kind == token.STRING {
|
|
||||||
// extract string content
|
|
||||||
arg, err := strconv.Unquote(argLit.Value)
|
|
||||||
if err == nil {
|
|
||||||
// found interesting strings
|
|
||||||
handler.OnMsgidPrefix(fset, argLit.ValuePos, arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the `Handle*File` functions follow the following calling convention:
|
|
||||||
// * `fname` is the name of the input file
|
|
||||||
// * `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 (handler Handler) HandleGoFile(fname string, src any) error {
|
|
||||||
fset := token.NewFileSet()
|
|
||||||
node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution|goParser.ParseComments)
|
|
||||||
if err != nil {
|
|
||||||
return LocatedError{
|
|
||||||
Location: fname,
|
|
||||||
Kind: "Go parser",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ast.Inspect(node, func(n ast.Node) bool {
|
|
||||||
// search for function calls of the form `anything.Tr(any-string-lit, ...)`
|
|
||||||
|
|
||||||
if call, ok := n.(*ast.CallExpr); ok && len(call.Args) >= 1 {
|
|
||||||
funSel, ok := call.Fun.(*ast.SelectorExpr)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name]
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var gotUnexpectedInvoke *int
|
|
||||||
|
|
||||||
for _, argNum := range ltf {
|
|
||||||
if len(call.Args) < int(argNum+1) {
|
|
||||||
argc := len(call.Args)
|
|
||||||
gotUnexpectedInvoke = &argc
|
|
||||||
} else {
|
|
||||||
handler.HandleGoTrArgument(fset, call.Args[int(argNum)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if gotUnexpectedInvoke != nil {
|
|
||||||
handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke)
|
|
||||||
}
|
|
||||||
} else if composite, ok := n.(*ast.CompositeLit); ok {
|
|
||||||
ident, ok := composite.Type.(*ast.Ident)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// special case: models/unit/unit.go
|
|
||||||
if strings.HasSuffix(fname, "unit.go") && ident.Name == "Unit" && len(composite.Elts) == 6 {
|
|
||||||
// NameKey has index 2
|
|
||||||
// invoked like '{{ctx.Locale.Tr $unit.NameKey}}'
|
|
||||||
nameKey, ok := composite.Elts[2].(*ast.BasicLit)
|
|
||||||
if !ok || nameKey.Kind != token.STRING {
|
|
||||||
handler.OnWarning(fset, composite.Elts[2].Pos(), "unexpected initialization of 'Unit'")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract string content
|
|
||||||
arg, err := strconv.Unquote(nameKey.Value)
|
|
||||||
if err == nil {
|
|
||||||
// found interesting strings
|
|
||||||
handler.OnMsgid(fset, nameKey.ValuePos, arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if function, ok := n.(*ast.FuncDecl); ok {
|
|
||||||
matches := false
|
|
||||||
if function.Doc != nil {
|
|
||||||
for _, comment := range function.Doc.List {
|
|
||||||
if strings.TrimSpace(comment.Text) == "//llu:returnsTrKey" {
|
|
||||||
matches = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matches {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
results := function.Type.Results.List
|
|
||||||
if len(results) != 1 {
|
|
||||||
handler.OnWarning(fset, function.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", function.Name.Name))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
ast.Inspect(function.Body, func(n ast.Node) bool {
|
|
||||||
// search for return stmts
|
|
||||||
// TODO: what about nested functions?
|
|
||||||
if ret, ok := n.(*ast.ReturnStmt); ok {
|
|
||||||
for _, res := range ret.Results {
|
|
||||||
ast.Inspect(res, func(n ast.Node) bool {
|
|
||||||
if expr, ok := n.(ast.Expr); ok {
|
|
||||||
handler.HandleGoTrArgument(fset, expr)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
} else if decl, ok := n.(*ast.GenDecl); ok && (decl.Tok == token.CONST || decl.Tok == token.VAR) {
|
|
||||||
matches := false
|
|
||||||
if decl.Doc != nil {
|
|
||||||
for _, comment := range decl.Doc.List {
|
|
||||||
if strings.TrimSpace(comment.Text) == "// llu:TrKeys" {
|
|
||||||
matches = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matches {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, spec := range decl.Specs {
|
|
||||||
// interpret all contained strings as message IDs
|
|
||||||
ast.Inspect(spec, func(n ast.Node) bool {
|
|
||||||
if argLit, ok := n.(*ast.BasicLit); ok {
|
|
||||||
handler.HandleGoTrBasicLit(fset, argLit)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// derived from source: modules/templates/scopedtmpl/scopedtmpl.go, L169-L213
|
|
||||||
func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) {
|
|
||||||
switch node.Type() {
|
|
||||||
case tmplParser.NodeAction:
|
|
||||||
handler.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe)
|
|
||||||
case tmplParser.NodeList:
|
|
||||||
nodeList := node.(*tmplParser.ListNode)
|
|
||||||
handler.handleTemplateFileNodes(fset, nodeList.Nodes)
|
|
||||||
case tmplParser.NodePipe:
|
|
||||||
handler.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode))
|
|
||||||
case tmplParser.NodeTemplate:
|
|
||||||
handler.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe)
|
|
||||||
case tmplParser.NodeIf:
|
|
||||||
nodeIf := node.(*tmplParser.IfNode)
|
|
||||||
handler.handleTemplateBranchNode(fset, nodeIf.BranchNode)
|
|
||||||
case tmplParser.NodeRange:
|
|
||||||
nodeRange := node.(*tmplParser.RangeNode)
|
|
||||||
handler.handleTemplateBranchNode(fset, nodeRange.BranchNode)
|
|
||||||
case tmplParser.NodeWith:
|
|
||||||
nodeWith := node.(*tmplParser.WithNode)
|
|
||||||
handler.handleTemplateBranchNode(fset, nodeWith.BranchNode)
|
|
||||||
|
|
||||||
case tmplParser.NodeCommand:
|
|
||||||
nodeCommand := node.(*tmplParser.CommandNode)
|
|
||||||
|
|
||||||
handler.handleTemplateFileNodes(fset, nodeCommand.Args)
|
|
||||||
|
|
||||||
if len(nodeCommand.Args) < 2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
funcname := ""
|
|
||||||
if nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode); ok {
|
|
||||||
if nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode); ok {
|
|
||||||
if nodeIdent.Ident != "ctx" || len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
funcname = nodeChain.Field[1]
|
|
||||||
}
|
|
||||||
} else if nodeField, ok := nodeCommand.Args[0].(*tmplParser.FieldNode); ok {
|
|
||||||
if len(nodeField.Ident) != 2 || !(nodeField.Ident[0] == "locale" || nodeField.Ident[0] == "Locale") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
funcname = nodeField.Ident[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
var gotUnexpectedInvoke *int
|
|
||||||
ltf, ok := handler.LocaleTrFunctions[funcname]
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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 gotUnexpectedInvoke != nil {
|
|
||||||
handler.OnUnexpectedInvoke(fset, token.Pos(nodeCommand.Pos), funcname, *gotUnexpectedInvoke)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
handler.handleTemplateNode(fset, node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
handler.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler Handler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) {
|
|
||||||
for _, node := range nodes {
|
|
||||||
handler.handleTemplateNode(fset, node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler Handler) HandleTemplateFile(fname string, src any) error {
|
|
||||||
var tmplContent []byte
|
|
||||||
switch src2 := src.(type) {
|
|
||||||
case nil:
|
|
||||||
var err error
|
|
||||||
tmplContent, err = os.ReadFile(fname)
|
|
||||||
if err != nil {
|
|
||||||
return LocatedError{
|
|
||||||
Location: fname,
|
|
||||||
Kind: "ReadFile",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []byte:
|
|
||||||
tmplContent = src2
|
|
||||||
case string:
|
|
||||||
// SAFETY: we do not modify tmplContent below
|
|
||||||
tmplContent = util.UnsafeStringToBytes(src2)
|
|
||||||
default:
|
|
||||||
panic("invalid type for 'src'")
|
|
||||||
}
|
|
||||||
|
|
||||||
fset := token.NewFileSet()
|
|
||||||
fset.AddFile(fname, 1, len(tmplContent)).SetLinesForContent(tmplContent)
|
|
||||||
// SAFETY: we do not modify tmplContent2 below
|
|
||||||
tmplContent2 := util.UnsafeBytesToString(tmplContent)
|
|
||||||
|
|
||||||
tmpl := template.New(fname)
|
|
||||||
tmpl.Funcs(fjTemplates.NewFuncMap())
|
|
||||||
tmplParsed, err := tmpl.Parse(tmplContent2)
|
|
||||||
if err != nil {
|
|
||||||
return LocatedError{
|
|
||||||
Location: fname,
|
|
||||||
Kind: "Template parser",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handler.handleTemplateFileNodes(fset, tmplParsed.Root.Nodes)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This command assumes that we get started from the project root directory
|
// This command assumes that we get started from the project root directory
|
||||||
//
|
//
|
||||||
// Possible command line flags:
|
// Possible command line flags:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue