forgejo/build/lint-locale-usage/handle-tmpl.go
2025-08-19 16:34:20 +02:00

160 lines
4.8 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 (
"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
}