tools/src: Move template logic of 'gen' to package And add tests. This is useful for other tooling. Change-Id: Ia399071baf6d4bb617f3c73e4ccd4ed72d522c2e Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/111020 Commit-Queue: Ben Clayton <bclayton@google.com> Kokoro: Kokoro <noreply+kokoro@google.com> Reviewed-by: Antonio Maiorano <amaiorano@google.com>
diff --git a/tools/src/cmd/gen/main.go b/tools/src/cmd/gen/main.go index 653adbb..cdf91da 100644 --- a/tools/src/cmd/gen/main.go +++ b/tools/src/cmd/gen/main.go
@@ -30,13 +30,12 @@ "runtime" "strconv" "strings" - "text/template" "time" - "unicode" "dawn.googlesource.com/dawn/tools/src/container" "dawn.googlesource.com/dawn/tools/src/fileutils" "dawn.googlesource.com/dawn/tools/src/glob" + "dawn.googlesource.com/dawn/tools/src/template" "dawn.googlesource.com/dawn/tools/src/tint/intrinsic/gen" "dawn.googlesource.com/dawn/tools/src/tint/intrinsic/parser" "dawn.googlesource.com/dawn/tools/src/tint/intrinsic/resolver" @@ -325,7 +324,6 @@ ` type generator struct { - template *template.Template cache *genCache writeFile WriteFile rnd *rand.Rand @@ -342,32 +340,13 @@ // syntax. func generate(tmpl string, cache *genCache, w io.Writer, writeFile WriteFile) error { g := generator{ - template: template.New("<template>"), cache: cache, writeFile: writeFile, rnd: rand.New(rand.NewSource(4561123)), } - if err := g.bindAndParse(g.template, tmpl); err != nil { - return err - } - return g.template.Execute(w, nil) -} -func (g *generator) bindAndParse(t *template.Template, text string) error { - _, err := t.Funcs(map[string]interface{}{ - "Map": newMap, - "Iterate": iterate, - "Title": strings.Title, - "PascalCase": pascalCase, + funcs := map[string]interface{}{ "SplitDisplayName": gen.SplitDisplayName, - "Contains": strings.Contains, - "HasPrefix": strings.HasPrefix, - "HasSuffix": strings.HasSuffix, - "TrimPrefix": strings.TrimPrefix, - "TrimSuffix": strings.TrimSuffix, - "TrimLeft": strings.TrimLeft, - "TrimRight": strings.TrimRight, - "Split": strings.Split, "Scramble": g.scramble, "IsEnumEntry": is(sem.EnumEntry{}), "IsEnumMatcher": is(sem.EnumMatcher{}), @@ -387,63 +366,9 @@ "Sem": g.cache.sem, "IntrinsicTable": g.cache.intrinsicTable, "Permute": g.cache.permute, - "Eval": g.eval, - "Import": g.importTmpl, "WriteFile": func(relPath, content string) (string, error) { return "", g.writeFile(relPath, content) }, - }).Option("missingkey=error").Parse(text) - return err -} - -// eval executes the sub-template with the given name and argument, returning -// the generated output -func (g *generator) eval(template string, args ...interface{}) (string, error) { - target := g.template.Lookup(template) - if target == nil { - return "", fmt.Errorf("template '%v' not found", template) } - sb := strings.Builder{} - - var err error - if len(args) == 1 { - err = target.Execute(&sb, args[0]) - } else { - m := newMap() - if len(args)%2 != 0 { - return "", fmt.Errorf("Eval expects a single argument or list name-value pairs") - } - for i := 0; i < len(args); i += 2 { - name, ok := args[i].(string) - if !ok { - return "", fmt.Errorf("Eval argument %v is not a string", i) - } - m.Put(name, args[i+1]) - } - err = target.Execute(&sb, m) - } - - if err != nil { - return "", fmt.Errorf("while evaluating '%v': %v", template, err) - } - return sb.String(), nil -} - -// importTmpl parses the template at the given project-relative path, merging -// the template definitions into the global namespace. -// Note: The body of the template is not executed. -func (g *generator) importTmpl(path string) (string, error) { - if strings.Contains(path, "..") { - return "", fmt.Errorf("import path must not contain '..'") - } - path = filepath.Join(fileutils.DawnRoot(), path) - data, err := ioutil.ReadFile(path) - if err != nil { - return "", fmt.Errorf("failed to open '%v': %w", path, err) - } - t := g.template.New("") - if err := g.bindAndParse(t, string(data)); err != nil { - return "", fmt.Errorf("failed to parse '%v': %w", path, err) - } - return "", nil + return template.Run(tmpl, w, funcs) } // scramble randomly modifies the input string so that it is no longer equal to @@ -477,24 +402,6 @@ return string(bytes), nil } -// Map is a simple generic key-value map, which can be used in the template -type Map map[interface{}]interface{} - -func newMap() Map { return Map{} } - -// Put adds the key-value pair into the map. -// Put always returns an empty string so nothing is printed in the template. -func (m Map) Put(key, value interface{}) string { - m[key] = value - return "" -} - -// Get looks up and returns the value with the given key. If the map does not -// contain the given key, then nil is returned. -func (m Map) Get(key interface{}) interface{} { - return m[key] -} - // is returns a function that returns true if the value passed to the function // matches the type of 'ty'. func is(ty interface{}) func(interface{}) bool { @@ -525,43 +432,6 @@ return s.Index(count-1).Interface() == v } -// iterate returns a slice of length 'n', with each element equal to its index. -// Useful for: {{- range Iterate $n -}}<this will be looped $n times>{{end}} -func iterate(n int) []int { - out := make([]int, n) - for i := range out { - out[i] = i - } - return out -} - -// pascalCase returns the snake-case string s transformed into 'PascalCase', -// Rules: -// * The first letter of the string is capitalized -// * Characters following an underscore or number are capitalized -// * Underscores are removed from the returned string -// See: https://en.wikipedia.org/wiki/Camel_case -func pascalCase(s string) string { - b := strings.Builder{} - upper := true - for _, r := range s { - if r == '_' { - upper = true - continue - } - if upper { - b.WriteRune(unicode.ToUpper(r)) - upper = false - } else { - b.WriteRune(r) - } - if unicode.IsNumber(r) { - upper = true - } - } - return b.String() -} - // Invokes the clang-format executable at 'exe' to format the file content 'in'. // Returns the formatted file. func clangFormat(in, exe string) (string, error) {
diff --git a/tools/src/template/template.go b/tools/src/template/template.go new file mode 100644 index 0000000..d70635b --- /dev/null +++ b/tools/src/template/template.go
@@ -0,0 +1,190 @@ +// Copyright 2022 The Tint Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// package template wraps the golang "text/template" package to provide an +// enhanced template generator. +package template + +import ( + "fmt" + "io" + "io/ioutil" + "path/filepath" + "strings" + "text/template" + "unicode" + + "dawn.googlesource.com/dawn/tools/src/fileutils" +) + +// The template function binding table +type Functions map[string]interface{} + +// Run executes the template tmpl, writing the output to w. +// funcs are the functions provided to the template. +// See https://golang.org/pkg/text/template/ for documentation on the template +// syntax. +func Run(tmpl string, w io.Writer, funcs Functions) error { + g := generator{ + template: template.New("<template>"), + } + + // Add a bunch of generic useful functions + g.funcs = Functions{ + "Contains": strings.Contains, + "Eval": g.eval, + "HasPrefix": strings.HasPrefix, + "HasSuffix": strings.HasSuffix, + "Import": g.importTmpl, + "Iterate": iterate, + "Map": newMap, + "PascalCase": pascalCase, + "Split": strings.Split, + "Title": strings.Title, + "TrimLeft": strings.TrimLeft, + "TrimPrefix": strings.TrimPrefix, + "TrimRight": strings.TrimRight, + "TrimSuffix": strings.TrimSuffix, + } + + // Append custom functions + for name, fn := range funcs { + g.funcs[name] = fn + } + + if err := g.bindAndParse(g.template, tmpl); err != nil { + return err + } + + return g.template.Execute(w, nil) +} + +type generator struct { + template *template.Template + funcs Functions +} + +func (g *generator) bindAndParse(t *template.Template, tmpl string) error { + _, err := t. + Funcs(map[string]interface{}(g.funcs)). + Option("missingkey=error"). + Parse(tmpl) + return err +} + +// eval executes the sub-template with the given name and argument, returning +// the generated output +func (g *generator) eval(template string, args ...interface{}) (string, error) { + target := g.template.Lookup(template) + if target == nil { + return "", fmt.Errorf("template '%v' not found", template) + } + sb := strings.Builder{} + + var err error + if len(args) == 1 { + err = target.Execute(&sb, args[0]) + } else { + m := newMap() + if len(args)%2 != 0 { + return "", fmt.Errorf("Eval expects a single argument or list name-value pairs") + } + for i := 0; i < len(args); i += 2 { + name, ok := args[i].(string) + if !ok { + return "", fmt.Errorf("Eval argument %v is not a string", i) + } + m.Put(name, args[i+1]) + } + err = target.Execute(&sb, m) + } + + if err != nil { + return "", fmt.Errorf("while evaluating '%v': %v", template, err) + } + return sb.String(), nil +} + +// importTmpl parses the template at the given project-relative path, merging +// the template definitions into the global namespace. +// Note: The body of the template is not executed. +func (g *generator) importTmpl(path string) (string, error) { + if strings.Contains(path, "..") { + return "", fmt.Errorf("import path must not contain '..'") + } + path = filepath.Join(fileutils.DawnRoot(), path) + data, err := ioutil.ReadFile(path) + if err != nil { + return "", fmt.Errorf("failed to open '%v': %w", path, err) + } + if err := g.bindAndParse(g.template.New(""), string(data)); err != nil { + return "", fmt.Errorf("failed to parse '%v': %w", path, err) + } + return "", nil +} + +// Map is a simple generic key-value map, which can be used in the template +type Map map[interface{}]interface{} + +func newMap() Map { return Map{} } + +// Put adds the key-value pair into the map. +// Put always returns an empty string so nothing is printed in the template. +func (m Map) Put(key, value interface{}) string { + m[key] = value + return "" +} + +// Get looks up and returns the value with the given key. If the map does not +// contain the given key, then nil is returned. +func (m Map) Get(key interface{}) interface{} { + return m[key] +} + +// iterate returns a slice of length 'n', with each element equal to its index. +// Useful for: {{- range Iterate $n -}}<this will be looped $n times>{{end}} +func iterate(n int) []int { + out := make([]int, n) + for i := range out { + out[i] = i + } + return out +} + +// pascalCase returns the snake-case string s transformed into 'PascalCase', +// Rules: +// * The first letter of the string is capitalized +// * Characters following an underscore or number are capitalized +// * Underscores are removed from the returned string +// See: https://en.wikipedia.org/wiki/Camel_case +func pascalCase(s string) string { + b := strings.Builder{} + upper := true + for _, r := range s { + if r == '_' || r == ' ' { + upper = true + continue + } + if upper { + b.WriteRune(unicode.ToUpper(r)) + upper = false + } else { + b.WriteRune(r) + } + if unicode.IsNumber(r) { + upper = true + } + } + return b.String() +}
diff --git a/tools/src/template/template_test.go b/tools/src/template/template_test.go new file mode 100644 index 0000000..f18e727 --- /dev/null +++ b/tools/src/template/template_test.go
@@ -0,0 +1,225 @@ +// Copyright 2022 The Tint Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template_test + +import ( + "bytes" + "testing" + + "dawn.googlesource.com/dawn/tools/src/template" + "github.com/google/go-cmp/cmp" +) + +func check(t *testing.T, tmpl, expected string, fns template.Functions) { + t.Helper() + w := &bytes.Buffer{} + err := template.Run(tmpl, w, fns) + if err != nil { + t.Errorf("template.Run() failed with %v", err) + return + } + got := w.String() + if diff := cmp.Diff(expected, got); diff != "" { + t.Errorf("output was not as expected. Diff:\n%v", diff) + } +} + +func TestContains(t *testing.T) { + tmpl := ` +{{ Contains "hello world" "hello"}} +{{ Contains "hello world" "fish"}} +` + expected := ` +true +false +` + check(t, tmpl, expected, nil) +} + +func TestEvalSingleParameter(t *testing.T) { + tmpl := ` +pre-eval +{{ Eval "T" 123 }} +{{ Eval "T" "cat" }} +post-eval + +pre-define +{{- define "T"}} + . is {{.}} +{{- end }} +post-define +` + expected := ` +pre-eval + + . is 123 + + . is cat +post-eval + +pre-define +post-define +` + check(t, tmpl, expected, nil) +} + +func TestEvalParameterPairs(t *testing.T) { + tmpl := ` +pre-eval +{{ Eval "T" "number" 123 "animal" "cat" }} +post-eval + +pre-define +{{- define "T"}} + .number is {{.number}} + .animal is {{.animal}} +{{- end }} +post-define +` + expected := ` +pre-eval + + .number is 123 + .animal is cat +post-eval + +pre-define +post-define +` + check(t, tmpl, expected, nil) +} + +func TestHasPrefix(t *testing.T) { + tmpl := ` +{{ HasPrefix "hello world" "hello"}} +{{ HasPrefix "hello world" "world"}} +` + expected := ` +true +false +` + check(t, tmpl, expected, nil) +} + +func TestIterate(t *testing.T) { + tmpl := ` +{{- range $i := Iterate 5}} + {{$i}} +{{- end}} +` + expected := ` + 0 + 1 + 2 + 3 + 4 +` + check(t, tmpl, expected, nil) +} + +func TestMap(t *testing.T) { + tmpl := ` + {{- $m := Map }} + {{- $m.Put "one" 1 }} + {{- $m.Put "two" 2 }} + one: {{ $m.Get "one" }} + two: {{ $m.Get "two" }} +` + expected := ` + one: 1 + two: 2 +` + check(t, tmpl, expected, nil) +} + +func TestPascalCase(t *testing.T) { + tmpl := ` +{{ PascalCase "hello world" }} +{{ PascalCase "hello_world" }} +` + expected := ` +HelloWorld +HelloWorld +` + check(t, tmpl, expected, nil) +} + +func TestSplit(t *testing.T) { + tmpl := ` +{{- range $i, $s := Split "cat_says_meow" "_" }} + {{$i}}: '{{$s}}' +{{- end }} +` + expected := ` + 0: 'cat' + 1: 'says' + 2: 'meow' +` + check(t, tmpl, expected, nil) +} + +func TestTitle(t *testing.T) { + tmpl := ` +{{Title "hello world"}} +` + expected := ` +Hello World +` + check(t, tmpl, expected, nil) +} + +func TrimLeft(t *testing.T) { + tmpl := ` +'{{TrimLeft "hello world", "hel"}}' +` + expected := ` +'o world' +` + check(t, tmpl, expected, nil) +} + +func TrimPrefix(t *testing.T) { + tmpl := ` +'{{TrimLeft "hello world", "hel"}}' +'{{TrimLeft "hello world", "heo"}}' +` + expected := ` +'o world' +'hello world' +` + check(t, tmpl, expected, nil) +} + +func TrimRight(t *testing.T) { + tmpl := ` +'{{TrimRight "hello world", "wld"}}' +` + expected := ` +'hello wor' +` + check(t, tmpl, expected, nil) +} + +func TrimSuffix(t *testing.T) { + tmpl := ` +'{{TrimRight "hello world", "rld"}}' +'{{TrimRight "hello world", "wld"}}' +` + expected := ` +'hello wo' +'hello world' +` + check(t, tmpl, expected, nil) +}