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>
This commit is contained in:
parent
619f9bd639
commit
d114055e4e
|
@ -30,13 +30,12 @@ import (
|
|||
"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 @@ const header = `// Copyright %v The Tint Authors.
|
|||
`
|
||||
|
||||
type generator struct {
|
||||
template *template.Template
|
||||
cache *genCache
|
||||
writeFile WriteFile
|
||||
rnd *rand.Rand
|
||||
|
@ -342,32 +340,13 @@ type WriteFile func(relPath, content string) error
|
|||
// 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 @@ func (g *generator) bindAndParse(t *template.Template, text string) error {
|
|||
"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 @@ func (g *generator) scramble(str string, avoid container.Set[string]) (string, e
|
|||
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 @@ func isLastIn(v, slice interface{}) bool {
|
|||
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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue