mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-26 12:13:57 +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
|
@ -7,22 +7,15 @@ package main
|
|||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
goParser "go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
tmplParser "text/template/parse"
|
||||
|
||||
"forgejo.org/modules/container"
|
||||
fjTemplates "forgejo.org/modules/templates"
|
||||
"forgejo.org/modules/translation/localeiter"
|
||||
"forgejo.org/modules/util"
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
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
|
||||
//
|
||||
// Possible command line flags:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue