mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-10-19 02:43:18 +00:00
feat(build): add support for the base.Messenger, $.locale.Tr, Form structs to lint-locale-usage (#9095)
- 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>
This commit is contained in:
parent
710600f459
commit
5687a8ef65
11 changed files with 375 additions and 202 deletions
|
@ -38,6 +38,10 @@ routers/.* @gusted
|
|||
options/locale/.* @0ko
|
||||
options/locale_next/.* @0ko
|
||||
|
||||
# Personal interest
|
||||
# lint-locale-usage
|
||||
build/lint-locale-usage/.* @fogti
|
||||
models/unit/.* @fogti
|
||||
services/migrations/lint-locale-usage/.* @fogti
|
||||
|
||||
# Personal interest
|
||||
.*/webhook.* @oliverpool
|
||||
|
|
2
Makefile
2
Makefile
|
@ -475,7 +475,7 @@ lint-locale:
|
|||
|
||||
.PHONY: lint-locale-usage
|
||||
lint-locale-usage:
|
||||
$(GO) run ./build/lint-locale-usage --allow-masked-usages-from=build/lint-locale-usage/allowed-masked-usage.txt
|
||||
$(GO) run ./build/lint-locale-usage/bin --allow-masked-usages-from=build/lint-locale-usage/allowed-masked-usage.txt
|
||||
|
||||
.PHONY: lint-md
|
||||
lint-md: node_modules
|
||||
|
|
|
@ -23,12 +23,6 @@ themes.names.
|
|||
# services/context/context.go
|
||||
relativetime.
|
||||
|
||||
# templates/mail/issue/default.tmpl: $.locale.Tr
|
||||
mail.issue.in_tree_path
|
||||
|
||||
# templates/package/metadata/arch.tmpl: $.locale.Tr
|
||||
packages.details.license
|
||||
|
||||
# templates/repo/issue/view_content.tmpl: indirection via $closeTranslationKey
|
||||
repo.issues.close
|
||||
repo.pulls.close
|
||||
|
@ -48,7 +42,3 @@ projects.type-3.display_name
|
|||
# templates/repo/settings/webhook/link_menu.tmpl, templates/webhook/new.tmpl: repo.settings.web_hook_name_
|
||||
# tests/integration/repo_archive_text_test.go
|
||||
repo.settings.
|
||||
|
||||
# services/migrations/migrate.go: messenger calls
|
||||
# ToDo: give them a unique prefix
|
||||
repo.migrate.
|
||||
|
|
177
build/lint-locale-usage/bin/handle-go.go
Normal file
177
build/lint-locale-usage/bin/handle-go.go
Normal file
|
@ -0,0 +1,177 @@
|
|||
// 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
|
||||
}
|
|
@ -16,6 +16,7 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
llu "forgejo.org/build/lint-locale-usage"
|
||||
"forgejo.org/modules/container"
|
||||
"forgejo.org/modules/translation/localeiter"
|
||||
)
|
||||
|
@ -23,27 +24,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 LocatedError struct {
|
||||
Location string
|
||||
Kind string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e LocatedError) Error() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(e.Location)
|
||||
sb.WriteString(":\t")
|
||||
if e.Kind != "" {
|
||||
sb.WriteString(e.Kind)
|
||||
sb.WriteString(": ")
|
||||
}
|
||||
sb.WriteString("ERROR: ")
|
||||
sb.WriteString(e.Err.Error())
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func InitLocaleTrFunctions() map[string][]uint {
|
||||
ret := make(map[string][]uint)
|
||||
|
||||
|
@ -58,14 +38,6 @@ func InitLocaleTrFunctions() map[string][]uint {
|
|||
return ret
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
OnMsgid func(fset *token.FileSet, pos token.Pos, msgid string)
|
||||
OnMsgidPrefix func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool)
|
||||
OnUnexpectedInvoke func(fset *token.FileSet, pos token.Pos, funcname string, argc int)
|
||||
OnWarning func(fset *token.FileSet, pos token.Pos, msg string)
|
||||
LocaleTrFunctions map[string][]uint
|
||||
}
|
||||
|
||||
type StringTrie interface {
|
||||
Matches(key []string) bool
|
||||
}
|
||||
|
@ -113,7 +85,7 @@ func (m StringTrieMap) Insert(key []string) {
|
|||
func ParseAllowedMaskedUsages(fname string, usedMsgids container.Set[string], allowedMaskedPrefixes StringTrieMap, chkMsgid func(msgid string) bool) error {
|
||||
file, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return LocatedError{
|
||||
return llu.LocatedError{
|
||||
Location: fname,
|
||||
Kind: "Open",
|
||||
Err: err,
|
||||
|
@ -133,7 +105,7 @@ func ParseAllowedMaskedUsages(fname string, usedMsgids container.Set[string], al
|
|||
allowedMaskedPrefixes.Insert(strings.Split(linePrefix, "."))
|
||||
} else {
|
||||
if !chkMsgid(line) {
|
||||
return LocatedError{
|
||||
return llu.LocatedError{
|
||||
Location: fmt.Sprintf("%s: line %d", fname, lno),
|
||||
Kind: "undefined msgid",
|
||||
Err: errors.New(line),
|
||||
|
@ -143,7 +115,7 @@ func ParseAllowedMaskedUsages(fname string, usedMsgids container.Set[string], al
|
|||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return LocatedError{
|
||||
return llu.LocatedError{
|
||||
Location: fname,
|
||||
Kind: "Scanner",
|
||||
Err: err,
|
||||
|
@ -152,15 +124,6 @@ func ParseAllowedMaskedUsages(fname string, usedMsgids container.Set[string], al
|
|||
return nil
|
||||
}
|
||||
|
||||
// Truncating a message id prefix to the last dot
|
||||
func PrepareMsgidPrefix(s string) (string, bool) {
|
||||
index := strings.LastIndexByte(s, 0x2e)
|
||||
if index == -1 {
|
||||
return "", true
|
||||
}
|
||||
return s[:index], index != len(s)-1
|
||||
}
|
||||
|
||||
func Usage() {
|
||||
outp := flag.CommandLine.Output()
|
||||
fmt.Fprintf(outp, "Usage of %s:\n", os.Args[0])
|
||||
|
@ -210,6 +173,7 @@ func Usage() {
|
|||
func main() {
|
||||
allowMissingMsgids := false
|
||||
allowUnusedMsgids := false
|
||||
allowWeakMissingMsgids := true
|
||||
usedMsgids := make(container.Set[string])
|
||||
allowedMaskedPrefixes := make(StringTrieMap)
|
||||
|
||||
|
@ -228,6 +192,12 @@ func main() {
|
|||
false,
|
||||
"don't return an error code if missing message IDs are found",
|
||||
)
|
||||
flag.BoolVar(
|
||||
&allowWeakMissingMsgids,
|
||||
"allow-weak-missing-msgids",
|
||||
true,
|
||||
"Don't return an error code if missing 'weak' (e.g. \"form.$msgid\") message IDs are found",
|
||||
)
|
||||
flag.BoolVar(
|
||||
&allowUnusedMsgids,
|
||||
"allow-unused-msgids",
|
||||
|
@ -289,7 +259,7 @@ func main() {
|
|||
os.Exit(3)
|
||||
}
|
||||
|
||||
handler := Handler{
|
||||
handler := llu.Handler{
|
||||
OnMsgidPrefix: func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool) {
|
||||
msgidPrefixSplit := strings.Split(msgidPrefix, ".")
|
||||
if !truncated {
|
||||
|
@ -299,8 +269,11 @@ func main() {
|
|||
fmt.Printf("%s:\tmissing msgid prefix: %s\n", fset.Position(pos).String(), msgidPrefix)
|
||||
}
|
||||
},
|
||||
OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string) {
|
||||
OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string, weak bool) {
|
||||
if !msgids.Contains(msgid) {
|
||||
if weak && allowWeakMissingMsgids {
|
||||
return
|
||||
}
|
||||
gotAnyMsgidError = true
|
||||
fmt.Printf("%s:\tmissing msgid: %s\n", fset.Position(pos).String(), msgid)
|
||||
} else {
|
||||
|
@ -314,7 +287,7 @@ func main() {
|
|||
OnWarning: func(fset *token.FileSet, pos token.Pos, msg string) {
|
||||
fmt.Printf("%s:\tWARNING: %s\n", fset.Position(pos).String(), msg)
|
||||
},
|
||||
LocaleTrFunctions: InitLocaleTrFunctions(),
|
||||
LocaleTrFunctions: llu.InitLocaleTrFunctions(),
|
||||
}
|
||||
|
||||
if err := filepath.WalkDir(".", func(fpath string, d fs.DirEntry, err error) error {
|
||||
|
@ -332,7 +305,7 @@ func main() {
|
|||
} else if name == "bindata.go" || fpath == "modules/translation/i18n/i18n_test.go" {
|
||||
// skip false positives
|
||||
} else if strings.HasSuffix(name, ".go") {
|
||||
onError(handler.HandleGoFile(fpath, nil))
|
||||
onError(HandleGoFile(handler, fpath, nil))
|
||||
} else if strings.HasSuffix(name, ".tmpl") {
|
||||
if strings.HasPrefix(fpath, "tests") && strings.HasSuffix(name, ".ini.tmpl") {
|
||||
// skip false positives
|
|
@ -7,24 +7,26 @@ import (
|
|||
"go/token"
|
||||
"testing"
|
||||
|
||||
llu "forgejo.org/build/lint-locale-usage"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func buildHandler(ret *[]string) Handler {
|
||||
return Handler{
|
||||
OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string) {
|
||||
func buildHandler(ret *[]string) llu.Handler {
|
||||
return llu.Handler{
|
||||
OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string, weak bool) {
|
||||
*ret = append(*ret, msgid)
|
||||
},
|
||||
OnUnexpectedInvoke: func(fset *token.FileSet, pos token.Pos, funcname string, argc int) {},
|
||||
LocaleTrFunctions: InitLocaleTrFunctions(),
|
||||
LocaleTrFunctions: llu.InitLocaleTrFunctions(),
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGoFileWrapped(t *testing.T, fname, src string) []string {
|
||||
var ret []string
|
||||
handler := buildHandler(&ret)
|
||||
require.NoError(t, handler.HandleGoFile(fname, src))
|
||||
require.NoError(t, HandleGoFile(handler, fname, src))
|
||||
return ret
|
||||
}
|
||||
|
|
@ -2,18 +2,17 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
package lintLocaleUsage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
goParser "go/parser"
|
||||
"go/token"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (handler Handler) handleGoTrBasicLit(fset *token.FileSet, argLit *ast.BasicLit, prefix string) {
|
||||
func (handler Handler) HandleGoTrBasicLit(fset *token.FileSet, argLit *ast.BasicLit, prefix string) {
|
||||
if argLit.Kind == token.STRING {
|
||||
// extract string content
|
||||
arg, err := strconv.Unquote(argLit.Value)
|
||||
|
@ -29,14 +28,14 @@ func (handler Handler) handleGoTrBasicLit(fset *token.FileSet, argLit *ast.Basic
|
|||
}
|
||||
handler.OnMsgidPrefix(fset, argLit.ValuePos, prep, trunc)
|
||||
} else {
|
||||
handler.OnMsgid(fset, argLit.ValuePos, arg)
|
||||
handler.OnMsgid(fset, argLit.ValuePos, arg, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (handler Handler) handleGoTrArgument(fset *token.FileSet, n ast.Expr, prefix string) {
|
||||
func (handler Handler) HandleGoTrArgument(fset *token.FileSet, n ast.Expr, prefix string) {
|
||||
if argLit, ok := n.(*ast.BasicLit); ok {
|
||||
handler.handleGoTrBasicLit(fset, argLit, prefix)
|
||||
handler.HandleGoTrBasicLit(fset, argLit, prefix)
|
||||
} else if argBinExpr, ok := n.(*ast.BinaryExpr); ok {
|
||||
if argBinExpr.Op != token.ADD {
|
||||
// pass
|
||||
|
@ -57,7 +56,7 @@ func (handler Handler) handleGoTrArgument(fset *token.FileSet, n ast.Expr, prefi
|
|||
}
|
||||
}
|
||||
|
||||
func (handler Handler) handleGoCommentGroup(fset *token.FileSet, cg *ast.CommentGroup, commentPrefix string) *string {
|
||||
func (handler Handler) HandleGoCommentGroup(fset *token.FileSet, cg *ast.CommentGroup, commentPrefix string) *string {
|
||||
if cg == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -87,131 +86,3 @@ func (handler Handler) handleGoCommentGroup(fset *token.FileSet, cg *ast.Comment
|
|||
return &matchInsPrefix
|
||||
}
|
||||
}
|
||||
|
||||
// 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, ...)`
|
||||
|
||||
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:
|
||||
ident, ok := n2.Type.(*ast.Ident)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// special case: models/unit/unit.go
|
||||
if strings.HasSuffix(fname, "unit.go") && ident.Name == "Unit" {
|
||||
if len(n2.Elts) != 6 {
|
||||
handler.OnWarning(fset, n2.Pos(), "unexpected initialization of 'Unit' (unexpected number of arguments)")
|
||||
}
|
||||
// NameKey has index 2
|
||||
// invoked like '{{ctx.Locale.Tr $unit.NameKey}}'
|
||||
nameKey, ok := n2.Elts[2].(*ast.BasicLit)
|
||||
if !ok || nameKey.Kind != token.STRING {
|
||||
handler.OnWarning(fset, n2.Elts[2].Pos(), "unexpected initialization of 'Unit' (expected string literal as NameKey)")
|
||||
return true
|
||||
}
|
||||
|
||||
// extract string content
|
||||
arg, err := strconv.Unquote(nameKey.Value)
|
||||
if err == nil {
|
||||
// found interesting strings
|
||||
handler.OnMsgid(fset, nameKey.ValuePos, arg)
|
||||
}
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
matchInsPrefix := handler.handleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKey")
|
||||
if matchInsPrefix == nil {
|
||||
return true
|
||||
}
|
||||
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
|
||||
})
|
||||
return true
|
||||
case *ast.GenDecl:
|
||||
if !(n2.Tok == token.CONST || n2.Tok == token.VAR) {
|
||||
return true
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
package lintLocaleUsage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -48,18 +48,29 @@ func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.N
|
|||
}
|
||||
|
||||
funcname := ""
|
||||
if nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode); ok {
|
||||
switch nodeCommand.Args[0].Type() {
|
||||
case tmplParser.NodeChain:
|
||||
nodeChain := nodeCommand.Args[0].(*tmplParser.ChainNode)
|
||||
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 {
|
||||
|
||||
case tmplParser.NodeField:
|
||||
nodeField := nodeCommand.Args[0].(*tmplParser.FieldNode)
|
||||
if len(nodeField.Ident) != 2 || !(nodeField.Ident[0] == "locale" || nodeField.Ident[0] == "Locale") {
|
||||
return
|
||||
}
|
||||
funcname = nodeField.Ident[1]
|
||||
|
||||
case tmplParser.NodeVariable:
|
||||
nodeVar := nodeCommand.Args[0].(*tmplParser.VariableNode)
|
||||
if len(nodeVar.Ident) != 3 || !(nodeVar.Ident[0] == "$" && nodeVar.Ident[1] == "locale") {
|
||||
return
|
||||
}
|
||||
funcname = nodeVar.Ident[2]
|
||||
}
|
||||
|
||||
var gotUnexpectedInvoke *int
|
||||
|
@ -93,7 +104,7 @@ func (handler Handler) handleTemplateMsgid(fset *token.FileSet, node tmplParser.
|
|||
case tmplParser.NodeString:
|
||||
nodeString := node.(*tmplParser.StringNode)
|
||||
// found interesting strings
|
||||
handler.OnMsgid(fset, pos, nodeString.Text)
|
||||
handler.OnMsgid(fset, pos, nodeString.Text, false)
|
||||
|
||||
case tmplParser.NodePipe:
|
||||
nodePipe := node.(*tmplParser.PipeNode)
|
||||
|
@ -132,7 +143,7 @@ func (handler Handler) handleTemplateMsgid(fset *token.FileSet, node tmplParser.
|
|||
|
||||
if len(nodeCommand.Args) == 2 {
|
||||
// found interesting strings
|
||||
handler.OnMsgid(fset, stringPos, msgidPrefix)
|
||||
handler.OnMsgid(fset, stringPos, msgidPrefix, false)
|
||||
} else {
|
||||
if nodeIdent.Ident == "printf" {
|
||||
parts := strings.SplitN(msgidPrefix, "%", 2)
|
||||
|
|
62
build/lint-locale-usage/handler.go
Normal file
62
build/lint-locale-usage/handler.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lintLocaleUsage
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LocatedError struct {
|
||||
Location string
|
||||
Kind string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e LocatedError) Error() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(e.Location)
|
||||
sb.WriteString(":\t")
|
||||
if e.Kind != "" {
|
||||
sb.WriteString(e.Kind)
|
||||
sb.WriteString(": ")
|
||||
}
|
||||
sb.WriteString("ERROR: ")
|
||||
sb.WriteString(e.Err.Error())
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
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, weak bool)
|
||||
OnMsgidPrefix func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool)
|
||||
OnUnexpectedInvoke func(fset *token.FileSet, pos token.Pos, funcname string, argc int)
|
||||
OnWarning func(fset *token.FileSet, pos token.Pos, msg string)
|
||||
LocaleTrFunctions map[string][]uint
|
||||
}
|
||||
|
||||
// Truncating a message id prefix to the last dot
|
||||
func PrepareMsgidPrefix(s string) (string, bool) {
|
||||
index := strings.LastIndexByte(s, 0x2e)
|
||||
if index == -1 {
|
||||
return "", true
|
||||
}
|
||||
return s[:index], index != len(s)-1
|
||||
}
|
38
models/unit/lint-locale-usage/llu.go
Normal file
38
models/unit/lint-locale-usage/llu.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lintLocaleUsage
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"strconv"
|
||||
|
||||
llu "forgejo.org/build/lint-locale-usage"
|
||||
)
|
||||
|
||||
func HandleCompositeUnit(handler llu.Handler, fset *token.FileSet, n *ast.CompositeLit) {
|
||||
ident, ok := n.Type.(*ast.Ident)
|
||||
if !ok || ident.Name != "Unit" {
|
||||
return
|
||||
}
|
||||
|
||||
if len(n.Elts) != 6 {
|
||||
handler.OnWarning(fset, n.Pos(), "unexpected initialization of 'Unit' (unexpected number of arguments)")
|
||||
return
|
||||
}
|
||||
// NameKey has index 2
|
||||
// invoked like '{{ctx.Locale.Tr $unit.NameKey}}'
|
||||
nameKey, ok := n.Elts[2].(*ast.BasicLit)
|
||||
if !ok || nameKey.Kind != token.STRING {
|
||||
handler.OnWarning(fset, n.Elts[2].Pos(), "unexpected initialization of 'Unit' (expected string literal as NameKey)")
|
||||
return
|
||||
}
|
||||
|
||||
// extract string content
|
||||
arg, err := strconv.Unquote(nameKey.Value)
|
||||
if err == nil {
|
||||
// found interesting strings
|
||||
handler.OnMsgid(fset, nameKey.ValuePos, arg, false)
|
||||
}
|
||||
}
|
45
services/migrations/lint-locale-usage/llu.go
Normal file
45
services/migrations/lint-locale-usage/llu.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lintLocaleUsage
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
llu "forgejo.org/build/lint-locale-usage"
|
||||
"forgejo.org/modules/container"
|
||||
)
|
||||
|
||||
// special case: services/migrations/migrate.go
|
||||
func HandleMessengerInFunc(handler llu.Handler, fset *token.FileSet, n2 *ast.FuncDecl) {
|
||||
messenger := make(container.Set[string])
|
||||
for _, i := range n2.Type.Params.List {
|
||||
if ret, ok := i.Type.(*ast.SelectorExpr); ok && ret.Sel.Name == "Messenger" {
|
||||
if ret, ok := ret.X.(*ast.Ident); ok && ret.Name == "base" {
|
||||
for _, j := range i.Names {
|
||||
messenger.Add(j.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(messenger) == 0 {
|
||||
return
|
||||
}
|
||||
ast.Inspect(n2.Body, func(n ast.Node) bool {
|
||||
// search for "messenger" function calls
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if ret, ok := call.Fun.(*ast.Ident); !(ok && messenger.Contains(ret.Name)) {
|
||||
return true
|
||||
}
|
||||
if len(call.Args) != 1 {
|
||||
handler.OnWarning(fset, call.Lparen, "unexpected invocation of base.Messenger (expected exactly 1 argument)")
|
||||
return true
|
||||
}
|
||||
handler.HandleGoTrArgument(fset, call.Args[0], "")
|
||||
return true
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue