From e8a557c427df614fa98727b8dea7a9e406105860 Mon Sep 17 00:00:00 2001 From: pkong-ds Date: Mon, 26 Aug 2024 20:25:33 +0800 Subject: [PATCH] Copy traverse code from pkg/util/template/validation.go --- devtools/gotemplatetranslationlinter/func.go | 159 ++++++++++++++++++ .../translation_key_rule.go | 140 ++++++++++++++- 2 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 devtools/gotemplatetranslationlinter/func.go diff --git a/devtools/gotemplatetranslationlinter/func.go b/devtools/gotemplatetranslationlinter/func.go new file mode 100644 index 00000000000..659516c4e06 --- /dev/null +++ b/devtools/gotemplatetranslationlinter/func.go @@ -0,0 +1,159 @@ +package main + +import ( + "bytes" + "fmt" + "html/template" + "io" + "reflect" + "strconv" + "strings" + "time" + + "github.com/Masterminds/sprig" +) + +const ( + templateTranslationMessageTemplateName = "__translation_message.html" +) + +type tpl interface { + ExecuteTemplate(wr io.Writer, name string, data any) error +} + +func MakeTemplateFuncMap(t tpl) map[string]interface{} { + var templateFuncMap = sprig.HermeticHtmlFuncMap() + // templateFuncMap[messageformat.TemplateRuntimeFuncName] = messageformat.TemplateRuntimeFunc + templateFuncMap["rfc3339"] = RFC3339 + templateFuncMap["ensureTime"] = EnsureTime + templateFuncMap["isNil"] = IsNil + templateFuncMap["showAttributeValue"] = ShowAttributeValue + templateFuncMap["htmlattr"] = HTMLAttr + templateFuncMap["include"] = makeInclude(t) + templateFuncMap["translate"] = makeTranslate(t) + templateFuncMap["trimHTML"] = trimHTML + return templateFuncMap +} + +func RFC3339(date interface{}) interface{} { + switch date := date.(type) { + case *time.Time: + return date.UTC().Format(time.RFC3339) + case time.Time: + return date.UTC().Format(time.RFC3339) + default: + return "INVALID_DATE" + } +} + +func EnsureTime(anyValue interface{}) interface{} { + switch anyValue := anyValue.(type) { + case *time.Time: + return anyValue + case time.Time: + return anyValue + case string: + t, err := time.Parse(time.RFC3339, anyValue) + if err != nil { + panic(err) + } + return t + case *string: + t, err := time.Parse(time.RFC3339, *anyValue) + if err != nil { + panic(err) + } + return t + default: + return anyValue + } +} + +func IsNil(v interface{}) bool { + return v == nil || + (reflect.ValueOf(v).Kind() == reflect.Ptr && reflect.ValueOf(v).IsNil()) +} + +func ShowAttributeValue(v interface{}) string { + value := reflect.ValueOf(v) + if value.Kind() == reflect.Ptr { + if !value.IsNil() { + return ShowAttributeValue(reflect.ValueOf(v).Elem().Interface()) + } + return "" + } + + switch v := v.(type) { + case string: + return v + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case nil: + return "" + default: + return fmt.Sprintf("%v", v) + + } +} + +func HTMLAttr(v string) template.HTMLAttr { + // Ignore gosec error because the app developer can actually write any template + // But we should be careful that do not pass any user input to this function + return template.HTMLAttr(v) // nolint:gosec +} + +func makeInclude(t tpl) func(tplName string, data any) (template.HTML, error) { + return func( + tplName string, + data any, + ) (template.HTML, error) { + buf := &bytes.Buffer{} + err := t.ExecuteTemplate(buf, tplName, data) + // Ignore gosec error because the app developer can actually write any template + // But we should be careful that do not pass any user input to this function + html := template.HTML(buf.String()) // nolint:gosec + return html, err + } +} + +// `translate` is intended for `include` a translation message but wrapped it +// in a span and set its translation key with data attribute +// In theory it can be used with resources other than translation, but take your +// own risks +func makeTranslate(t tpl) func(tranlsationKey string, data any) (template.HTML, error) { + include := makeInclude(t) + return func( + tranlsationKey string, + data any, + ) (template.HTML, error) { + included, err := include(tranlsationKey, data) + if err != nil { + return template.HTML(""), err + } + buf := &bytes.Buffer{} + d := make(map[string]interface{}) + d["Key"] = tranlsationKey + d["Value"] = included + err = t.ExecuteTemplate(buf, templateTranslationMessageTemplateName, d) + // Ignore gosec error because the app developer can actually write any template + // But we should be careful that do not pass any user input to this function + html := template.HTML(buf.String()) // nolint:gosec + return html, err + } +} + +func trimHTML(input interface{}) interface{} { + switch input := input.(type) { + case string: + return strings.TrimSpace(input) + case template.HTML: + // `Masterminds/sprig`'s `trimAll` cannot handle html type, so we need to convert it to string first + // Ignore gosec error because this function is intended to be used with trusted input (__alert_message.html) + // and the output is intended to be used as HTML + return template.HTML(strings.TrimSpace(string(input))) // nolint:gosec + default: + return "" + } +} diff --git a/devtools/gotemplatetranslationlinter/translation_key_rule.go b/devtools/gotemplatetranslationlinter/translation_key_rule.go index ed3c347f2ab..9c86a366e04 100644 --- a/devtools/gotemplatetranslationlinter/translation_key_rule.go +++ b/devtools/gotemplatetranslationlinter/translation_key_rule.go @@ -1,9 +1,147 @@ package main +import ( + "fmt" + htmltemplate "html/template" + "sort" + "text/template/parse" +) + type TranslationKeyRule struct{} func (r TranslationKeyRule) Check(content string, path string) LintViolations { + return r.check(content, path) +} + +func (r TranslationKeyRule) check(content string, path string) LintViolations { var violations LintViolations - // TODO: implement linting logic here + t := r.makeTemplate(content) + + r.validateHTMLTemplate(t) + + // TODO: add violations return violations } + +func (r TranslationKeyRule) makeTemplate(content string) *htmltemplate.Template { + t := htmltemplate.New("") + funcMap := MakeTemplateFuncMap(t) + t.Funcs(funcMap) + parsed := htmltemplate.Must(t.Parse(content)) + + return parsed +} + +func (r TranslationKeyRule) validateHTMLTemplate(template *htmltemplate.Template) error { + tpls := template.Templates() + + sort.Slice(tpls, func(i, j int) bool { + return tpls[i].Name() < tpls[j].Name() + }) + + for _, tpl := range tpls { + if tpl.Tree == nil { + fmt.Printf("tpl.Tree == nil\n") + continue + } + // fmt.Printf("tpl.Tree != nil\n") + if err := r.validateTree(tpl.Tree); err != nil { + return err + } + } + return nil +} + +func (r TranslationKeyRule) validateTree(tree *parse.Tree) (err error) { + validateFn := func(n parse.Node, depth int) (cont bool) { + // fmt.Printf("inside validateFn\n") + switch n := n.(type) { + case *parse.CommandNode: + fmt.Printf("case *parse.CommandNode:\n") + for _, arg := range n.Args { + if ident, ok := arg.(*parse.IdentifierNode); ok && ident.String() == "include" { + // TODO: handle include fn + fmt.Printf("pkong# xdd %s\n", ident) + fmt.Printf("pkong# include n: %s\n", n) + } + } + case *parse.TemplateNode: + fmt.Printf("case *parse.TemplateNode:\n") + // TODO: handle template node + fmt.Printf("pkong# template n: %s\n", n) + // default: + // fmt.Printf("pkong# node n: %s\n", n) + // fmt.Printf("pkong# node n.(type): %t\n", n) + } + + return err == nil + } + + r.traverseTree(tree, validateFn) + return +} + +func (r TranslationKeyRule) traverseTree(tree *parse.Tree, fn func(n parse.Node, depth int) (cont bool)) { + r.traverseTreeVisit(tree.Root, 0, fn) +} + +func (r TranslationKeyRule) traverseTreeVisitBranch(n *parse.BranchNode, depth int, fn func(n parse.Node, depth int) (cont bool)) (cont bool) { + if cont = r.traverseTreeVisit(n.Pipe, depth, fn); !cont { + return + } + if cont = r.traverseTreeVisit(n.List, depth, fn); !cont { + return + } + if n.ElseList != nil { + if cont = r.traverseTreeVisit(n.ElseList, depth, fn); !cont { + return false + } + } + return +} + +func (r TranslationKeyRule) traverseTreeVisit(n parse.Node, depth int, fn func(n parse.Node, depth int) (cont bool)) (cont bool) { + // fmt.Printf("traverseTreeVisit\n") + cont = fn(n, depth) + if !cont { + return + } + + // fmt.Printf("n: %s\n", n) + switch n := n.(type) { + case *parse.PipeNode: + fmt.Printf("case *parse.PipeNode:\n") + for _, cmd := range n.Cmds { + if cont = r.traverseTreeVisit(cmd, depth, fn); !cont { + break + } + } + case *parse.CommandNode: + fmt.Printf("case *parse.CommandNode:\n") + for _, arg := range n.Args { + if pipe, ok := arg.(*parse.PipeNode); ok { + if cont = r.traverseTreeVisit(pipe, depth+1, fn); !cont { + break + } + } + } + case *parse.ActionNode: + cont = r.traverseTreeVisit(n.Pipe, depth, fn) + case *parse.TemplateNode, *parse.TextNode: + break + case *parse.IfNode: + cont = r.traverseTreeVisitBranch(&n.BranchNode, depth, fn) + case *parse.RangeNode: + cont = r.traverseTreeVisitBranch(&n.BranchNode, depth, fn) + case *parse.WithNode: + cont = r.traverseTreeVisitBranch(&n.BranchNode, depth, fn) + case *parse.ListNode: + for _, n := range n.Nodes { + if cont = r.traverseTreeVisit(n, depth+1, fn); !cont { + break + } + } + } + + return +}