mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-26 04:03:48 +00:00
188 lines
5.2 KiB
Go
188 lines
5.2 KiB
Go
// 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, "_") {
|
|
prep, trunc := PrepareMsgidPrefix(arg)
|
|
if trunc {
|
|
handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf("needed to truncate message id prefix: %s", arg))
|
|
}
|
|
handler.OnMsgidPrefix(fset, argLit.ValuePos, prep)
|
|
} 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
|
|
prep, trunc := PrepareMsgidPrefix(arg)
|
|
if trunc {
|
|
handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf("needed to truncate message id prefix: %s", arg))
|
|
}
|
|
handler.OnMsgidPrefix(fset, argLit.ValuePos, prep)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|