mirror of
https://codeberg.org/forgejo/forgejo
synced 2025-10-21 07:10:39 +02:00
- Move a file around to avoid a circular dependency. - Make lint-locale-usage aware of `base.Messenger`, form struct tags and `$.locale.Tr`. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9095 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Ellen Εμιλία Άννα Zscheile <fogti+devel@ytrizja.de> Co-committed-by: Ellen Εμιλία Άννα Zscheile <fogti+devel@ytrizja.de>
177 lines
4.9 KiB
Go
177 lines
4.9 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"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
llu "forgejo.org/build/lint-locale-usage"
|
|
lluUnit "forgejo.org/models/unit/lint-locale-usage"
|
|
lluMigrate "forgejo.org/services/migrations/lint-locale-usage"
|
|
)
|
|
|
|
// 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 HandleGoFile(handler llu.Handler, fname string, src any) error {
|
|
fset := token.NewFileSet()
|
|
node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution|goParser.ParseComments)
|
|
if err != nil {
|
|
return llu.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, ...)`
|
|
|
|
switch n2 := n.(type) {
|
|
case *ast.CallExpr:
|
|
if len(n2.Args) == 0 {
|
|
return true
|
|
}
|
|
funSel, ok := n2.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(n2.Args) <= int(argNum) {
|
|
argc := len(n2.Args)
|
|
gotUnexpectedInvoke = &argc
|
|
} else {
|
|
handler.HandleGoTrArgument(fset, n2.Args[int(argNum)], "")
|
|
}
|
|
}
|
|
|
|
if gotUnexpectedInvoke != nil {
|
|
handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke)
|
|
}
|
|
|
|
case *ast.CompositeLit:
|
|
if strings.HasSuffix(fname, "models/unit/unit.go") {
|
|
lluUnit.HandleCompositeUnit(handler, fset, n2)
|
|
}
|
|
|
|
case *ast.FuncDecl:
|
|
matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKey")
|
|
if matchInsPrefix != nil {
|
|
results := n2.Type.Results.List
|
|
if len(results) != 1 {
|
|
handler.OnWarning(fset, n2.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", n2.Name.Name))
|
|
return true
|
|
}
|
|
|
|
ast.Inspect(n2.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, *matchInsPrefix)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
if strings.HasSuffix(fname, "services/migrations/migrate.go") {
|
|
lluMigrate.HandleMessengerInFunc(handler, fset, n2)
|
|
}
|
|
return true
|
|
case *ast.GenDecl:
|
|
switch n2.Tok {
|
|
case token.CONST, token.VAR:
|
|
matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, " llu:TrKeys")
|
|
if matchInsPrefix == nil {
|
|
return true
|
|
}
|
|
for _, spec := range n2.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, *matchInsPrefix)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
case token.TYPE:
|
|
// modules/web/middleware/binding.go:Validate uses the convention that structs
|
|
// entries can have tags.
|
|
// In particular, `locale:$msgid` should be handled; any fields with `form:-` shouldn't.
|
|
// Problem: we don't know which structs are forms, actually.
|
|
|
|
for _, spec := range n2.Specs {
|
|
tspec := spec.(*ast.TypeSpec)
|
|
structNode, ok := tspec.Type.(*ast.StructType)
|
|
if !ok || !(strings.HasSuffix(tspec.Name.Name, "Form") ||
|
|
(tspec.Doc != nil &&
|
|
slices.ContainsFunc(tspec.Doc.List, func(c *ast.Comment) bool {
|
|
return c.Text == "// swagger:model"
|
|
}))) {
|
|
continue
|
|
}
|
|
for _, field := range structNode.Fields.List {
|
|
if field.Names == nil {
|
|
continue
|
|
}
|
|
if len(field.Names) != 1 {
|
|
handler.OnWarning(fset, field.Type.Pos(), "unsupported multiple field names")
|
|
continue
|
|
}
|
|
msgidPos := field.Names[0].NamePos
|
|
msgid := "form." + field.Names[0].Name
|
|
if field.Tag != nil && field.Tag.Kind == token.STRING {
|
|
rawTag, err := strconv.Unquote(field.Tag.Value)
|
|
if err != nil {
|
|
handler.OnWarning(fset, field.Tag.ValuePos, "invalid tag value encountered")
|
|
continue
|
|
}
|
|
tag := reflect.StructTag(rawTag)
|
|
if tag.Get("form") == "-" {
|
|
continue
|
|
}
|
|
tmp := tag.Get("locale")
|
|
if len(tmp) != 0 {
|
|
msgidPos = field.Tag.ValuePos
|
|
msgid = tmp
|
|
}
|
|
}
|
|
handler.OnMsgid(fset, msgidPos, msgid, true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return nil
|
|
}
|