diff --git a/cmd/d8-lint/main.go b/cmd/d8-lint/main.go index 8f3d28d..d304acd 100644 --- a/cmd/d8-lint/main.go +++ b/cmd/d8-lint/main.go @@ -4,10 +4,10 @@ import ( "fmt" "os" + "github.com/deckhouse/d8-lint/internal/flags" + "github.com/deckhouse/d8-lint/internal/logger" + "github.com/deckhouse/d8-lint/internal/manager" "github.com/deckhouse/d8-lint/pkg/config" - "github.com/deckhouse/d8-lint/pkg/flags" - "github.com/deckhouse/d8-lint/pkg/logger" - "github.com/deckhouse/d8-lint/pkg/manager" ) func main() { diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 0000000..c219b8c --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1,442 @@ +/* +Copyright The Helm 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 engine + +import ( + "fmt" + "log" + "path" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/pkg/errors" + "k8s.io/client-go/rest" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + + "github.com/deckhouse/d8-lint/internal/template" +) + +// Engine is an implementation of the Helm rendering implementation for templates. +type Engine struct { + // If strict is enabled, template rendering will fail if a template references + // a value that was not passed in. + Strict bool + // In LintMode, some 'required' template values may be missing, so don't fail + LintMode bool + // optional provider of clients to talk to the Kubernetes API + clientProvider *ClientProvider + // EnableDNS tells the engine to allow DNS lookups when rendering templates + EnableDNS bool +} + +// New creates a new instance of Engine using the passed in rest config. +func New(config *rest.Config) Engine { + var clientProvider ClientProvider = clientProviderFromConfig{config} + return Engine{ + clientProvider: &clientProvider, + } +} + +// Render takes a chart, optional values, and value overrides, and attempts to render the Go templates. +// +// Render can be called repeatedly on the same engine. +// +// This will look in the chart's 'templates' data (e.g. the 'templates/' directory) +// and attempt to render the templates there using the values passed in. +// +// Values are scoped to their templates. A dependency template will not have +// access to the values set for its parent. If chart "foo" includes chart "bar", +// "bar" will not have access to the values for "foo". +// +// Values should be prepared with something like `chartutils.ReadValues`. +// +// Values are passed through the templates according to scope. If the top layer +// chart includes the chart foo, which includes the chart bar, the values map +// will be examined for a table called "foo". If "foo" is found in vals, +// that section of the values will be passed into the "foo" chart. And if that +// section contains a value named "bar", that value will be passed on to the +// bar chart during render time. +func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { + tmap := allTemplates(chrt, values) + return e.render(tmap) +} + +// Render takes a chart, optional values, and value overrides, and attempts to +// render the Go templates using the default options. +func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { + return new(Engine).Render(chrt, values) +} + +// RenderWithClient takes a chart, optional values, and value overrides, and attempts to +// render the Go templates using the default options. This engine is client aware and so can have template +// functions that interact with the client. +func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) { + var clientProvider ClientProvider = clientProviderFromConfig{config} + return Engine{ + clientProvider: &clientProvider, + }.Render(chrt, values) +} + +// RenderWithClientProvider takes a chart, optional values, and value overrides, and attempts to +// render the Go templates using the default options. This engine is client aware and so can have template +// functions that interact with the client. +// This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed. +func RenderWithClientProvider(chrt *chart.Chart, values chartutil.Values, clientProvider ClientProvider) (map[string]string, error) { + return Engine{ + clientProvider: &clientProvider, + }.Render(chrt, values) +} + +// renderable is an object that can be rendered. +type renderable struct { + // tpl is the current template. + tpl string + // vals are the values to be supplied to the template. + vals chartutil.Values + // namespace prefix to the templates of the current chart + basePath string +} + +const warnStartDelim = "HELM_ERR_START" +const warnEndDelim = "HELM_ERR_END" +const recursionMaxNums = 1000 + +var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim) + +func warnWrap(warn string) string { + return warnStartDelim + warn + warnEndDelim +} + +// 'include' needs to be defined in the scope of a 'tpl' template as +// well as regular file-loaded templates. +func includeFun(t *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) { + return func(name string, data interface{}) (string, error) { + var buf strings.Builder + if v, ok := includedNames[name]; ok { + if v > recursionMaxNums { + return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name) + } + includedNames[name]++ + } else { + includedNames[name] = 1 + } + err := t.ExecuteTemplate(&buf, name, data) + includedNames[name]-- + return buf.String(), err + } +} + +// As does 'tpl', so that nested calls to 'tpl' see the templates +// defined by their enclosing contexts. +func tplFun(parent *template.Template, includedNames map[string]int, strict bool) func(string, interface{}) (string, error) { + return func(tpl string, vals interface{}) (string, error) { + t, err := parent.Clone() + if err != nil { + return "", errors.Wrapf(err, "cannot clone template") + } + + // Re-inject the missingkey option, see text/template issue https://github.com/golang/go/issues/43022 + // We have to go by strict from our engine configuration, as the option fields are private in Template. + // TODO: Remove workaround (and the strict parameter) once we build only with golang versions with a fix. + if strict { + t.Option("missingkey=error") + } else { + t.Option("missingkey=zero") + } + + // Re-inject 'include' so that it can close over our clone of t; + // this lets any 'define's inside tpl be 'include'd. + t.Funcs(template.FuncMap{ + "include": includeFun(t, includedNames), + "tpl": tplFun(t, includedNames, strict), + }) + + // We need a .New template, as template text which is just blanks + // or comments after parsing out defines just adds new named + // template definitions without changing the main template. + // https://pkg.go.dev/text/template#Template.Parse + // Use the parent's name for lack of a better way to identify the tpl + // text string. (Maybe we could use a hash appended to the name?) + t, err = t.New(parent.Name()).Parse(tpl) + if err != nil { + return "", errors.Wrapf(err, "cannot parse template %q", tpl) + } + + var buf strings.Builder + if err := t.Execute(&buf, vals); err != nil { + return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl) + } + + // See comment in renderWithReferences explaining the hack. + return strings.ReplaceAll(buf.String(), "", ""), nil + } +} + +// initFunMap creates the Engine's FuncMap and adds context-specific functions. +func (e Engine) initFunMap(t *template.Template) { + funcMap := funcMap() + includedNames := make(map[string]int) + + // Add the template-rendering functions here so we can close over t. + funcMap["include"] = includeFun(t, includedNames) + funcMap["tpl"] = tplFun(t, includedNames, e.Strict) + + // Add the `required` function here so we can use lintMode + funcMap["required"] = func(warn string, val interface{}) (interface{}, error) { + if val == nil { + if e.LintMode { + // Don't fail on missing required values when linting + log.Printf("[INFO] Missing required value: %s", warn) + return "", nil + } + return val, errors.Errorf(warnWrap(warn)) + } else if _, ok := val.(string); ok { + if val == "" { + if e.LintMode { + // Don't fail on missing required values when linting + log.Printf("[INFO] Missing required value: %s", warn) + return "", nil + } + return val, errors.Errorf(warnWrap(warn)) + } + } + return val, nil + } + + // Override sprig fail function for linting and wrapping message + funcMap["fail"] = func(msg string) (string, error) { + if e.LintMode { + // Don't fail when linting + log.Printf("[INFO] Fail: %s", msg) + return "", nil + } + return "", errors.New(warnWrap(msg)) + } + + // If we are not linting and have a cluster connection, provide a Kubernetes-backed + // implementation. + if !e.LintMode && e.clientProvider != nil { + funcMap["lookup"] = newLookupFunction(*e.clientProvider) + } + + // When DNS lookups are not enabled override the sprig function and return + // an empty string. + if !e.EnableDNS { + funcMap["getHostByName"] = func(_ string) string { + return "" + } + } + + t.Funcs(template.FuncMap(funcMap)) +} + +// render takes a map of templates/values and renders them. +func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) { + // Basically, what we do here is start with an empty parent template and then + // build up a list of templates -- one for each file. Once all of the templates + // have been parsed, we loop through again and execute every template. + // + // The idea with this process is to make it possible for more complex templates + // to share common blocks, but to make the entire thing feel like a file-based + // template engine. + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("rendering template failed: %v", r) + } + }() + t := template.New("gotpl") + if e.Strict { + t.Option("missingkey=error") + } else { + // Not that zero will attempt to add default values for types it knows, + // but will still emit for others. We mitigate that later. + t.Option("missingkey=zero") + } + + e.initFunMap(t) + + // We want to parse the templates in a predictable order. The order favors + // higher-level (in file system) templates over deeply nested templates. + keys := sortTemplates(tpls) + + for _, filename := range keys { + r := tpls[filename] + if _, err := t.New(filename).Parse(r.tpl); err != nil { + return map[string]string{}, cleanupParseError(filename, err) + } + } + + rendered = make(map[string]string, len(keys)) + for _, filename := range keys { + // Don't render partials. We don't care out the direct output of partials. + // They are only included from other templates. + if strings.HasPrefix(path.Base(filename), "_") { + continue + } + // At render time, add information about the template that is being rendered. + vals := tpls[filename].vals + vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath} + var buf strings.Builder + if err := t.ExecuteTemplate(&buf, filename, vals); err != nil { + return map[string]string{}, cleanupExecError(filename, err) + } + + // Work around the issue where Go will emit "" even if Options(missing=zero) + // is set. Since missing=error will never get here, we do not need to handle + // the Strict case. + rendered[filename] = strings.ReplaceAll(buf.String(), "", "") + } + + return rendered, nil +} + +func cleanupParseError(filename string, err error) error { + tokens := strings.Split(err.Error(), ": ") + if len(tokens) == 1 { + // This might happen if a non-templating error occurs + return fmt.Errorf("parse error in (%s): %s", filename, err) + } + // The first token is "template" + // The second token is either "filename:lineno" or "filename:lineNo:columnNo" + location := tokens[1] + // The remaining tokens make up a stacktrace-like chain, ending with the relevant error + errMsg := tokens[len(tokens)-1] + return fmt.Errorf("parse error at (%s): %s", string(location), errMsg) +} + +func cleanupExecError(filename string, err error) error { + if _, isExecError := err.(template.ExecError); !isExecError { + return err + } + + tokens := strings.SplitN(err.Error(), ": ", 3) + if len(tokens) != 3 { + // This might happen if a non-templating error occurs + return fmt.Errorf("execution error in (%s): %s", filename, err) + } + + // The first token is "template" + // The second token is either "filename:lineno" or "filename:lineNo:columnNo" + location := tokens[1] + + parts := warnRegex.FindStringSubmatch(tokens[2]) + if len(parts) >= 2 { + return fmt.Errorf("execution error at (%s): %s", string(location), parts[1]) + } + + return err +} + +func sortTemplates(tpls map[string]renderable) []string { + keys := make([]string, len(tpls)) + i := 0 + for key := range tpls { + keys[i] = key + i++ + } + sort.Sort(sort.Reverse(byPathLen(keys))) + return keys +} + +type byPathLen []string + +func (p byPathLen) Len() int { return len(p) } +func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] } +func (p byPathLen) Less(i, j int) bool { + a, b := p[i], p[j] + ca, cb := strings.Count(a, "/"), strings.Count(b, "/") + if ca == cb { + return strings.Compare(a, b) == -1 + } + return ca < cb +} + +// allTemplates returns all templates for a chart and its dependencies. +// +// As it goes, it also prepares the values in a scope-sensitive manner. +func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { + templates := make(map[string]renderable) + recAllTpls(c, templates, vals) + return templates +} + +// recAllTpls recurses through the templates in a chart. +// +// As it recurses, it also sets the values to be appropriate for the template +// scope. +func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} { + subCharts := make(map[string]interface{}) + chartMetaData := struct { + chart.Metadata + IsRoot bool + }{*c.Metadata, c.IsRoot()} + + next := map[string]interface{}{ + "Chart": chartMetaData, + "Files": newFiles(c.Files), + "Release": vals["Release"], + "Capabilities": vals["Capabilities"], + "Values": make(chartutil.Values), + "Subcharts": subCharts, + } + + // If there is a {{.Values.ThisChart}} in the parent metadata, + // copy that into the {{.Values}} for this template. + if c.IsRoot() { + next["Values"] = vals["Values"] + } else if vs, err := vals.Table("Values." + c.Name()); err == nil { + next["Values"] = vs + } + + for _, child := range c.Dependencies() { + subCharts[child.Name()] = recAllTpls(child, templates, next) + } + + newParentID := c.ChartFullPath() + for _, t := range c.Templates { + if t == nil { + continue + } + if !isTemplateValid(c, t.Name) { + continue + } + templates[path.Join(newParentID, t.Name)] = renderable{ + tpl: string(t.Data), + vals: next, + basePath: path.Join(newParentID, "templates"), + } + } + + return next +} + +// isTemplateValid returns true if the template is valid for the chart type +func isTemplateValid(ch *chart.Chart, templateName string) bool { + if isLibraryChart(ch) { + return strings.HasPrefix(filepath.Base(templateName), "_") + } + return true +} + +// isLibraryChart returns true if the chart is a library chart +func isLibraryChart(c *chart.Chart) bool { + return strings.EqualFold(c.Metadata.Type, "library") +} diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go new file mode 100644 index 0000000..de1896a --- /dev/null +++ b/internal/engine/engine_test.go @@ -0,0 +1,1302 @@ +/* +Copyright The Helm 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 engine + +import ( + "fmt" + "path" + "strings" + "sync" + "testing" + "text/template" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/fake" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" +) + +func TestSortTemplates(t *testing.T) { + tpls := map[string]renderable{ + "/mychart/templates/foo.tpl": {}, + "/mychart/templates/charts/foo/charts/bar/templates/foo.tpl": {}, + "/mychart/templates/bar.tpl": {}, + "/mychart/templates/charts/foo/templates/bar.tpl": {}, + "/mychart/templates/_foo.tpl": {}, + "/mychart/templates/charts/foo/templates/foo.tpl": {}, + "/mychart/templates/charts/bar/templates/foo.tpl": {}, + } + got := sortTemplates(tpls) + if len(got) != len(tpls) { + t.Fatal("Sorted results are missing templates") + } + + expect := []string{ + "/mychart/templates/charts/foo/charts/bar/templates/foo.tpl", + "/mychart/templates/charts/foo/templates/foo.tpl", + "/mychart/templates/charts/foo/templates/bar.tpl", + "/mychart/templates/charts/bar/templates/foo.tpl", + "/mychart/templates/foo.tpl", + "/mychart/templates/bar.tpl", + "/mychart/templates/_foo.tpl", + } + for i, e := range expect { + if got[i] != e { + t.Fatalf("\n\tExp:\n%s\n\tGot:\n%s", + strings.Join(expect, "\n"), + strings.Join(got, "\n"), + ) + } + } +} + +func TestFuncMap(t *testing.T) { + fns := funcMap() + forbidden := []string{"env", "expandenv"} + for _, f := range forbidden { + if _, ok := fns[f]; ok { + t.Errorf("Forbidden function %s exists in FuncMap.", f) + } + } + + // Test for Engine-specific template functions. + expect := []string{"include", "required", "tpl", "toYaml", "fromYaml", "toToml", "fromToml", "toJson", "fromJson", "lookup"} + for _, f := range expect { + if _, ok := fns[f]; !ok { + t.Errorf("Expected add-on function %q", f) + } + } +} + +func TestRender(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "moby", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, + {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, + {Name: "templates/test3", Data: []byte("{{.noValue}}")}, + {Name: "templates/test4", Data: []byte("{{toJson .Values}}")}, + {Name: "templates/test5", Data: []byte("{{getHostByName \"helm.sh\"}}")}, + }, + Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"}, + } + + vals := map[string]interface{}{ + "Values": map[string]interface{}{ + "outer": "spouter", + "inner": "inn", + "global": map[string]interface{}{ + "callme": "Ishmael", + }, + }, + } + + v, err := chartutil.CoalesceValues(c, vals) + if err != nil { + t.Fatalf("Failed to coalesce values: %s", err) + } + out, err := Render(c, v) + if err != nil { + t.Errorf("Failed to render templates: %s", err) + } + + expect := map[string]string{ + "moby/templates/test1": "Spouter Inn", + "moby/templates/test2": "ishmael", + "moby/templates/test3": "", + "moby/templates/test4": `{"global":{"callme":"Ishmael"},"inner":"inn","outer":"spouter"}`, + "moby/templates/test5": "", + } + + for name, data := range expect { + if out[name] != data { + t.Errorf("Expected %q, got %q", data, out[name]) + } + } +} + +func TestRenderRefsOrdering(t *testing.T) { + parentChart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "parent", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, + {Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, + }, + } + childChart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "child", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, + }, + } + parentChart.AddDependency(childChart) + + expect := map[string]string{ + "parent/templates/test.yaml": "parent value", + } + + for i := 0; i < 100; i++ { + out, err := Render(parentChart, chartutil.Values{}) + if err != nil { + t.Fatalf("Failed to render templates: %s", err) + } + + for name, data := range expect { + if out[name] != data { + t.Fatalf("Expected %q, got %q (iteration %d)", data, out[name], i+1) + } + } + } +} + +func TestRenderInternals(t *testing.T) { + // Test the internals of the rendering tool. + + vals := chartutil.Values{"Name": "one", "Value": "two"} + tpls := map[string]renderable{ + "one": {tpl: `Hello {{title .Name}}`, vals: vals}, + "two": {tpl: `Goodbye {{upper .Value}}`, vals: vals}, + // Test whether a template can reliably reference another template + // without regard for ordering. + "three": {tpl: `{{template "two" dict "Value" "three"}}`, vals: vals}, + } + + out, err := new(Engine).render(tpls) + if err != nil { + t.Fatalf("Failed template rendering: %s", err) + } + + if len(out) != 3 { + t.Fatalf("Expected 3 templates, got %d", len(out)) + } + + if out["one"] != "Hello One" { + t.Errorf("Expected 'Hello One', got %q", out["one"]) + } + + if out["two"] != "Goodbye TWO" { + t.Errorf("Expected 'Goodbye TWO'. got %q", out["two"]) + } + + if out["three"] != "Goodbye THREE" { + t.Errorf("Expected 'Goodbye THREE'. got %q", out["two"]) + } +} + +func TestRenderWithDNS(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "moby", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/test1", Data: []byte("{{getHostByName \"helm.sh\"}}")}, + }, + Values: map[string]interface{}{}, + } + + vals := map[string]interface{}{ + "Values": map[string]interface{}{}, + } + + v, err := chartutil.CoalesceValues(c, vals) + if err != nil { + t.Fatalf("Failed to coalesce values: %s", err) + } + + var e Engine + e.EnableDNS = true + out, err := e.Render(c, v) + if err != nil { + t.Errorf("Failed to render templates: %s", err) + } + + for _, val := range c.Templates { + fp := path.Join("moby", val.Name) + if out[fp] == "" { + t.Errorf("Expected IP address, got %q", out[fp]) + } + } +} + +type kindProps struct { + shouldErr error + gvr schema.GroupVersionResource + namespaced bool +} + +type testClientProvider struct { + t *testing.T + scheme map[string]kindProps + objects []runtime.Object +} + +func (p *testClientProvider) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) { + props := p.scheme[path.Join(apiVersion, kind)] + if props.shouldErr != nil { + return nil, false, props.shouldErr + } + return fake.NewSimpleDynamicClient(runtime.NewScheme(), p.objects...).Resource(props.gvr), props.namespaced, nil +} + +var _ ClientProvider = &testClientProvider{} + +// makeUnstructured is a convenience function for single-line creation of Unstructured objects. +func makeUnstructured(apiVersion, kind, name, namespace string) *unstructured.Unstructured { + ret := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": name, + }, + }} + if namespace != "" { + ret.Object["metadata"].(map[string]interface{})["namespace"] = namespace + } + return ret +} + +func TestRenderWithClientProvider(t *testing.T) { + provider := &testClientProvider{ + t: t, + scheme: map[string]kindProps{ + "v1/Namespace": { + gvr: schema.GroupVersionResource{ + Version: "v1", + Resource: "namespaces", + }, + }, + "v1/Pod": { + gvr: schema.GroupVersionResource{ + Version: "v1", + Resource: "pods", + }, + namespaced: true, + }, + }, + objects: []runtime.Object{ + makeUnstructured("v1", "Namespace", "default", ""), + makeUnstructured("v1", "Pod", "pod1", "default"), + makeUnstructured("v1", "Pod", "pod2", "ns1"), + makeUnstructured("v1", "Pod", "pod3", "ns1"), + }, + } + + type testCase struct { + template string + output string + } + cases := map[string]testCase{ + "ns-single": { + template: `{{ (lookup "v1" "Namespace" "" "default").metadata.name }}`, + output: "default", + }, + "ns-list": { + template: `{{ (lookup "v1" "Namespace" "" "").items | len }}`, + output: "1", + }, + "ns-missing": { + template: `{{ (lookup "v1" "Namespace" "" "absent") }}`, + output: "map[]", + }, + "pod-single": { + template: `{{ (lookup "v1" "Pod" "default" "pod1").metadata.name }}`, + output: "pod1", + }, + "pod-list": { + template: `{{ (lookup "v1" "Pod" "ns1" "").items | len }}`, + output: "2", + }, + "pod-all": { + template: `{{ (lookup "v1" "Pod" "" "").items | len }}`, + output: "3", + }, + "pod-missing": { + template: `{{ (lookup "v1" "Pod" "" "ns2") }}`, + output: "map[]", + }, + } + + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "moby", + Version: "1.2.3", + }, + Values: map[string]interface{}{}, + } + + for name, exp := range cases { + c.Templates = append(c.Templates, &chart.File{ + Name: path.Join("templates", name), + Data: []byte(exp.template), + }) + } + + vals := map[string]interface{}{ + "Values": map[string]interface{}{}, + } + + v, err := chartutil.CoalesceValues(c, vals) + if err != nil { + t.Fatalf("Failed to coalesce values: %s", err) + } + + out, err := RenderWithClientProvider(c, v, provider) + if err != nil { + t.Errorf("Failed to render templates: %s", err) + } + + for name, want := range cases { + t.Run(name, func(t *testing.T) { + key := path.Join("moby/templates", name) + if out[key] != want.output { + t.Errorf("Expected %q, got %q", want, out[key]) + } + }) + } +} + +func TestRenderWithClientProvider_error(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "moby", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/error", Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)}, + }, + Values: map[string]interface{}{}, + } + + vals := map[string]interface{}{ + "Values": map[string]interface{}{}, + } + + v, err := chartutil.CoalesceValues(c, vals) + if err != nil { + t.Fatalf("Failed to coalesce values: %s", err) + } + + provider := &testClientProvider{ + t: t, + scheme: map[string]kindProps{ + "v1/Error": { + shouldErr: fmt.Errorf("kaboom"), + }, + }, + } + _, err = RenderWithClientProvider(c, v, provider) + if err == nil || !strings.Contains(err.Error(), "kaboom") { + t.Errorf("Expected error from client provider when rendering, got %q", err) + } +} + +func TestParallelRenderInternals(t *testing.T) { + // Make sure that we can use one Engine to run parallel template renders. + e := new(Engine) + var wg sync.WaitGroup + for i := 0; i < 20; i++ { + wg.Add(1) + go func(i int) { + tt := fmt.Sprintf("expect-%d", i) + tpls := map[string]renderable{ + "t": { + tpl: `{{.val}}`, + vals: map[string]interface{}{"val": tt}, + }, + } + out, err := e.render(tpls) + if err != nil { + t.Errorf("Failed to render %s: %s", tt, err) + } + if out["t"] != tt { + t.Errorf("Expected %q, got %q", tt, out["t"]) + } + wg.Done() + }(i) + } + wg.Wait() +} + +func TestParseErrors(t *testing.T) { + vals := chartutil.Values{"Values": map[string]interface{}{}} + + tplsUndefinedFunction := map[string]renderable{ + "undefined_function": {tpl: `{{foo}}`, vals: vals}, + } + _, err := new(Engine).render(tplsUndefinedFunction) + if err == nil { + t.Fatalf("Expected failures while rendering: %s", err) + } + expected := `parse error at (undefined_function:1): function "foo" not defined` + if err.Error() != expected { + t.Errorf("Expected '%s', got %q", expected, err.Error()) + } +} + +func TestExecErrors(t *testing.T) { + vals := chartutil.Values{"Values": map[string]interface{}{}} + cases := []struct { + name string + tpls map[string]renderable + expected string + }{ + { + name: "MissingRequired", + tpls: map[string]renderable{ + "missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals}, + }, + expected: `execution error at (missing_required:1:2): foo is required`, + }, + { + name: "MissingRequiredWithColons", + tpls: map[string]renderable{ + "missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals}, + }, + expected: `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`, + }, + { + name: "Issue6044", + tpls: map[string]renderable{ + "issue6044": { + vals: vals, + tpl: `{{ $someEmptyValue := "" }} +{{ $myvar := "abc" }} +{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`, + }, + }, + expected: `execution error at (issue6044:3:4): abc: something is missing`, + }, + { + name: "MissingRequiredWithNewlines", + tpls: map[string]renderable{ + "issue9981": {tpl: `{{required "foo is required\nmore info after the break" .Values.foo}}`, vals: vals}, + }, + expected: `execution error at (issue9981:1:2): foo is required +more info after the break`, + }, + { + name: "FailWithNewlines", + tpls: map[string]renderable{ + "issue9981": {tpl: `{{fail "something is wrong\nlinebreak"}}`, vals: vals}, + }, + expected: `execution error at (issue9981:1:2): something is wrong +linebreak`, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + _, err := new(Engine).render(tt.tpls) + if err == nil { + t.Fatalf("Expected failures while rendering: %s", err) + } + if err.Error() != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, err.Error()) + } + }) + } +} + +func TestFailErrors(t *testing.T) { + vals := chartutil.Values{"Values": map[string]interface{}{}} + + failtpl := `All your base are belong to us{{ fail "This is an error" }}` + tplsFailed := map[string]renderable{ + "failtpl": {tpl: failtpl, vals: vals}, + } + _, err := new(Engine).render(tplsFailed) + if err == nil { + t.Fatalf("Expected failures while rendering: %s", err) + } + expected := `execution error at (failtpl:1:33): This is an error` + if err.Error() != expected { + t.Errorf("Expected '%s', got %q", expected, err.Error()) + } + + var e Engine + e.LintMode = true + out, err := e.render(tplsFailed) + if err != nil { + t.Fatal(err) + } + + expectStr := "All your base are belong to us" + if gotStr := out["failtpl"]; gotStr != expectStr { + t.Errorf("Expected %q, got %q (%v)", expectStr, gotStr, out) + } +} + +func TestAllTemplates(t *testing.T) { + ch1 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "ch1"}, + Templates: []*chart.File{ + {Name: "templates/foo", Data: []byte("foo")}, + {Name: "templates/bar", Data: []byte("bar")}, + }, + } + dep1 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "laboratory mice"}, + Templates: []*chart.File{ + {Name: "templates/pinky", Data: []byte("pinky")}, + {Name: "templates/brain", Data: []byte("brain")}, + }, + } + ch1.AddDependency(dep1) + + dep2 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "same thing we do every night"}, + Templates: []*chart.File{ + {Name: "templates/innermost", Data: []byte("innermost")}, + }, + } + dep1.AddDependency(dep2) + + tpls := allTemplates(ch1, chartutil.Values{}) + if len(tpls) != 5 { + t.Errorf("Expected 5 charts, got %d", len(tpls)) + } +} + +func TestChartValuesContainsIsRoot(t *testing.T) { + ch1 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "parent"}, + Templates: []*chart.File{ + {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + }, + } + dep1 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "child"}, + Templates: []*chart.File{ + {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + }, + } + ch1.AddDependency(dep1) + + out, err := Render(ch1, chartutil.Values{}) + if err != nil { + t.Fatalf("failed to render templates: %s", err) + } + expects := map[string]string{ + "parent/charts/child/templates/isroot": "false", + "parent/templates/isroot": "true", + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + +func TestRenderDependency(t *testing.T) { + deptpl := `{{define "myblock"}}World{{end}}` + toptpl := `Hello {{template "myblock"}}` + ch := &chart.Chart{ + Metadata: &chart.Metadata{Name: "outerchart"}, + Templates: []*chart.File{ + {Name: "templates/outer", Data: []byte(toptpl)}, + }, + } + ch.AddDependency(&chart.Chart{ + Metadata: &chart.Metadata{Name: "innerchart"}, + Templates: []*chart.File{ + {Name: "templates/inner", Data: []byte(deptpl)}, + }, + }) + + out, err := Render(ch, map[string]interface{}{}) + if err != nil { + t.Fatalf("failed to render chart: %s", err) + } + + if len(out) != 2 { + t.Errorf("Expected 2, got %d", len(out)) + } + + expect := "Hello World" + if out["outerchart/templates/outer"] != expect { + t.Errorf("Expected %q, got %q", expect, out["outer"]) + } + +} + +func TestRenderNestedValues(t *testing.T) { + innerpath := "templates/inner.tpl" + outerpath := "templates/outer.tpl" + // Ensure namespacing rules are working. + deepestpath := "templates/inner.tpl" + checkrelease := "templates/release.tpl" + // Ensure subcharts scopes are working. + subchartspath := "templates/subcharts.tpl" + + deepest := &chart.Chart{ + Metadata: &chart.Metadata{Name: "deepest"}, + Templates: []*chart.File{ + {Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, + {Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, + }, + Values: map[string]interface{}{"what": "milkshake", "where": "here"}, + } + + inner := &chart.Chart{ + Metadata: &chart.Metadata{Name: "herrick"}, + Templates: []*chart.File{ + {Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, + }, + Values: map[string]interface{}{"who": "Robert", "what": "glasses"}, + } + inner.AddDependency(deepest) + + outer := &chart.Chart{ + Metadata: &chart.Metadata{Name: "top"}, + Templates: []*chart.File{ + {Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, + {Name: subchartspath, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, + }, + Values: map[string]interface{}{ + "what": "stinkweed", + "who": "me", + "herrick": map[string]interface{}{ + "who": "time", + "what": "Sun", + }, + }, + } + outer.AddDependency(inner) + + injValues := map[string]interface{}{ + "what": "rosebuds", + "herrick": map[string]interface{}{ + "deepest": map[string]interface{}{ + "what": "flower", + "where": "Heaven", + }, + }, + "global": map[string]interface{}{ + "when": "to-day", + }, + } + + tmp, err := chartutil.CoalesceValues(outer, injValues) + if err != nil { + t.Fatalf("Failed to coalesce values: %s", err) + } + + inject := chartutil.Values{ + "Values": tmp, + "Chart": outer.Metadata, + "Release": chartutil.Values{ + "Name": "dyin", + }, + } + + t.Logf("Calculated values: %v", inject) + + out, err := Render(outer, inject) + if err != nil { + t.Fatalf("failed to render templates: %s", err) + } + + fullouterpath := "top/" + outerpath + if out[fullouterpath] != "Gather ye rosebuds while ye may" { + t.Errorf("Unexpected outer: %q", out[fullouterpath]) + } + + fullinnerpath := "top/charts/herrick/" + innerpath + if out[fullinnerpath] != "Old time is still a-flyin'" { + t.Errorf("Unexpected inner: %q", out[fullinnerpath]) + } + + fulldeepestpath := "top/charts/herrick/charts/deepest/" + deepestpath + if out[fulldeepestpath] != "And this same flower that smiles to-day" { + t.Errorf("Unexpected deepest: %q", out[fulldeepestpath]) + } + + fullcheckrelease := "top/charts/herrick/charts/deepest/" + checkrelease + if out[fullcheckrelease] != "Tomorrow will be dyin" { + t.Errorf("Unexpected release: %q", out[fullcheckrelease]) + } + + fullchecksubcharts := "top/" + subchartspath + if out[fullchecksubcharts] != "The glorious Lamp of Heaven, the Sun" { + t.Errorf("Unexpected subcharts: %q", out[fullchecksubcharts]) + } +} + +func TestRenderBuiltinValues(t *testing.T) { + inner := &chart.Chart{ + Metadata: &chart.Metadata{Name: "Latium"}, + Templates: []*chart.File{ + {Name: "templates/Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + {Name: "templates/From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, + }, + Files: []*chart.File{ + {Name: "author", Data: []byte("Virgil")}, + {Name: "book/title.txt", Data: []byte("Aeneid")}, + }, + } + + outer := &chart.Chart{ + Metadata: &chart.Metadata{Name: "Troy"}, + Templates: []*chart.File{ + {Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + {Name: "templates/Amata", Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, + }, + } + outer.AddDependency(inner) + + inject := chartutil.Values{ + "Values": "", + "Chart": outer.Metadata, + "Release": chartutil.Values{ + "Name": "Aeneid", + }, + } + + t.Logf("Calculated values: %v", outer) + + out, err := Render(outer, inject) + if err != nil { + t.Fatalf("failed to render templates: %s", err) + } + + expects := map[string]string{ + "Troy/charts/Latium/templates/Lavinia": "Troy/charts/Latium/templates/LaviniaLatiumAeneid", + "Troy/templates/Aeneas": "Troy/templates/AeneasTroyAeneid", + "Troy/templates/Amata": "Latium Virgil", + "Troy/charts/Latium/templates/From": "Virgil Aeneid", + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } + +} + +func TestAlterFuncMap_include(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "conrad"}, + Templates: []*chart.File{ + {Name: "templates/quote", Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, + {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + }, + } + + // Check nested reference in include FuncMap + d := &chart.Chart{ + Metadata: &chart.Metadata{Name: "nested"}, + Templates: []*chart.File{ + {Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, + {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + }, + } + + v := chartutil.Values{ + "Values": "", + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "Mistah Kurtz", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expect := " Mistah Kurtz - he dead." + if got := out["conrad/templates/quote"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) + } + + _, err = Render(d, v) + expectErrName := "nested/templates/quote" + if err == nil { + t.Errorf("Expected err of nested reference name: %v", expectErrName) + } +} + +func TestAlterFuncMap_require(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "conan"}, + Templates: []*chart.File{ + {Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, + {Name: "templates/bases", Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, + }, + } + + v := chartutil.Values{ + "Values": chartutil.Values{ + "who": "us", + "bases": 2, + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "That 90s meme", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expectStr := "All your base are belong to us" + if gotStr := out["conan/templates/quote"]; gotStr != expectStr { + t.Errorf("Expected %q, got %q (%v)", expectStr, gotStr, out) + } + expectNum := "All 2 of them!" + if gotNum := out["conan/templates/bases"]; gotNum != expectNum { + t.Errorf("Expected %q, got %q (%v)", expectNum, gotNum, out) + } + + // test required without passing in needed values with lint mode on + // verifies lint replaces required with an empty string (should not fail) + lintValues := chartutil.Values{ + "Values": chartutil.Values{ + "who": "us", + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "That 90s meme", + }, + } + var e Engine + e.LintMode = true + out, err = e.Render(c, lintValues) + if err != nil { + t.Fatal(err) + } + + expectStr = "All your base are belong to us" + if gotStr := out["conan/templates/quote"]; gotStr != expectStr { + t.Errorf("Expected %q, got %q (%v)", expectStr, gotStr, out) + } + expectNum = "All of them!" + if gotNum := out["conan/templates/bases"]; gotNum != expectNum { + t.Errorf("Expected %q, got %q (%v)", expectNum, gotNum, out) + } +} + +func TestAlterFuncMap_tpl(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplFunction"}, + Templates: []*chart.File{ + {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, + }, + } + + v := chartutil.Values{ + "Values": chartutil.Values{ + "value": "myvalue", + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expect := "Evaluate tpl Value: myvalue" + if got := out["TplFunction/templates/base"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) + } +} + +func TestAlterFuncMap_tplfunc(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplFunction"}, + Templates: []*chart.File{ + {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, + }, + } + + v := chartutil.Values{ + "Values": chartutil.Values{ + "value": "myvalue", + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expect := "Evaluate tpl Value: \"myvalue\"" + if got := out["TplFunction/templates/base"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) + } +} + +func TestAlterFuncMap_tplinclude(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplFunction"}, + Templates: []*chart.File{ + {Name: "templates/base", Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, + {Name: "templates/_partial", Data: []byte(`{{.Template.Name}}`)}, + }, + } + v := chartutil.Values{ + "Values": chartutil.Values{ + "value": "myvalue", + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expect := "\"TplFunction/templates/base\"" + if got := out["TplFunction/templates/base"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) + } + +} + +func TestRenderRecursionLimit(t *testing.T) { + // endless recursion should produce an error + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "bad"}, + Templates: []*chart.File{ + {Name: "templates/base", Data: []byte(`{{include "recursion" . }}`)}, + {Name: "templates/recursion", Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, + }, + } + v := chartutil.Values{ + "Values": "", + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + expectErr := "rendering template has a nested reference name: recursion: unable to execute template" + + _, err := Render(c, v) + if err == nil || !strings.HasSuffix(err.Error(), expectErr) { + t.Errorf("Expected err with suffix: %s", expectErr) + } + + // calling the same function many times is ok + times := 4000 + phrase := "All work and no play makes Jack a dull boy" + printFunc := `{{define "overlook"}}{{printf "` + phrase + `\n"}}{{end}}` + var repeatedIncl string + for i := 0; i < times; i++ { + repeatedIncl += `{{include "overlook" . }}` + } + + d := &chart.Chart{ + Metadata: &chart.Metadata{Name: "overlook"}, + Templates: []*chart.File{ + {Name: "templates/quote", Data: []byte(repeatedIncl)}, + {Name: "templates/_function", Data: []byte(printFunc)}, + }, + } + + out, err := Render(d, v) + if err != nil { + t.Fatal(err) + } + + var expect string + for i := 0; i < times; i++ { + expect += phrase + "\n" + } + if got := out["overlook/templates/quote"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) + } + +} + +func TestRenderLoadTemplateForTplFromFile(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplLoadFromFile"}, + Templates: []*chart.File{ + {Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, + {Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, + }, + Files: []*chart.File{ + {Name: "test", Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, + {Name: "test2", Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, + }, + } + + v := chartutil.Values{ + "Values": chartutil.Values{ + "filename": "test", + "filename2": "test2", + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expect := "test-function nested-define-content" + if got := out["TplLoadFromFile/templates/base"]; got != expect { + t.Fatalf("Expected %q, got %q", expect, got) + } +} + +func TestRenderTplEmpty(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplEmpty"}, + Templates: []*chart.File{ + {Name: "templates/empty-string", Data: []byte(`{{tpl "" .}}`)}, + {Name: "templates/empty-action", Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, + {Name: "templates/only-defines", Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, + }, + } + v := chartutil.Values{ + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expects := map[string]string{ + "TplEmpty/templates/empty-string": "", + "TplEmpty/templates/empty-action": "", + "TplEmpty/templates/only-defines": "", + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + +func TestRenderTplTemplateNames(t *testing.T) { + // .Template.BasePath and .Name make it through + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplTemplateNames"}, + Templates: []*chart.File{ + {Name: "templates/default-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, + {Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, + {Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, + {Name: "templates/modified-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)}, + {Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, + }, + } + v := chartutil.Values{ + "Values": chartutil.Values{ + "dot": chartutil.Values{ + "Template": chartutil.Values{ + "BasePath": "path/to/template", + "Name": "name-of-template", + "Field": "extra-field", + }, + }, + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expects := map[string]string{ + "TplTemplateNames/templates/default-basepath": "TplTemplateNames/templates", + "TplTemplateNames/templates/default-name": "TplTemplateNames/templates/default-name", + "TplTemplateNames/templates/modified-basepath": "path/to/template", + "TplTemplateNames/templates/modified-name": "name-of-template", + "TplTemplateNames/templates/modified-field": "extra-field", + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + +func TestRenderTplRedefines(t *testing.T) { + // Redefining a template inside 'tpl' does not affect the outer definition + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplRedefines"}, + Templates: []*chart.File{ + {Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, + {Name: "templates/partial", Data: []byte( + `before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`, + )}, + {Name: "templates/manifest", Data: []byte( + `{{define "manifest"}}original-in-manifest{{end}}` + + `before: {{include "manifest" .}}\n{{tpl .Values.manifestText .}}\nafter: {{include "manifest" .}}`, + )}, + {Name: "templates/manifest-only", Data: []byte( + `{{define "manifest-only"}}only-in-manifest{{end}}` + + `before: {{include "manifest-only" .}}\n{{tpl .Values.manifestOnlyText .}}\nafter: {{include "manifest-only" .}}`, + )}, + {Name: "templates/nested", Data: []byte( + `{{define "nested"}}original-in-manifest{{end}}` + + `{{define "nested-outer"}}original-outer-in-manifest{{end}}` + + `before: {{include "nested" .}} {{include "nested-outer" .}}\n` + + `{{tpl .Values.nestedText .}}\n` + + `after: {{include "nested" .}} {{include "nested-outer" .}}`, + )}, + }, + } + v := chartutil.Values{ + "Values": chartutil.Values{ + "partialText": `{{define "partial"}}redefined-in-tpl{{end}}tpl: {{include "partial" .}}`, + "manifestText": `{{define "manifest"}}redefined-in-tpl{{end}}tpl: {{include "manifest" .}}`, + "manifestOnlyText": `tpl: {{include "manifest-only" .}}`, + "nestedText": `{{define "nested"}}redefined-in-tpl{{end}}` + + `{{define "nested-outer"}}redefined-outer-in-tpl{{end}}` + + `before-inner-tpl: {{include "nested" .}} {{include "nested-outer" . }}\n` + + `{{tpl .Values.innerText .}}\n` + + `after-inner-tpl: {{include "nested" .}} {{include "nested-outer" . }}`, + "innerText": `{{define "nested"}}redefined-in-inner-tpl{{end}}inner-tpl: {{include "nested" .}} {{include "nested-outer" . }}`, + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expects := map[string]string{ + "TplRedefines/templates/partial": `before: original-in-partial\ntpl: redefined-in-tpl\nafter: original-in-partial`, + "TplRedefines/templates/manifest": `before: original-in-manifest\ntpl: redefined-in-tpl\nafter: original-in-manifest`, + "TplRedefines/templates/manifest-only": `before: only-in-manifest\ntpl: only-in-manifest\nafter: only-in-manifest`, + "TplRedefines/templates/nested": `before: original-in-manifest original-outer-in-manifest\n` + + `before-inner-tpl: redefined-in-tpl redefined-outer-in-tpl\n` + + `inner-tpl: redefined-in-inner-tpl redefined-outer-in-tpl\n` + + `after-inner-tpl: redefined-in-tpl redefined-outer-in-tpl\n` + + `after: original-in-manifest original-outer-in-manifest`, + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + +func TestRenderTplMissingKey(t *testing.T) { + // Rendering a missing key results in empty/zero output. + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplMissingKey"}, + Templates: []*chart.File{ + {Name: "templates/manifest", Data: []byte( + `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, + )}, + }, + } + v := chartutil.Values{ + "Values": chartutil.Values{}, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expects := map[string]string{ + "TplMissingKey/templates/manifest": `missingValue: `, + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + +func TestRenderTplMissingKeyString(t *testing.T) { + // Rendering a missing key results in error + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"}, + Templates: []*chart.File{ + {Name: "templates/manifest", Data: []byte( + `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, + )}, + }, + } + v := chartutil.Values{ + "Values": chartutil.Values{}, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + e := new(Engine) + e.Strict = true + + out, err := e.Render(c, v) + if err == nil { + t.Errorf("Expected error, got %v", out) + return + } + switch err.(type) { + case (template.ExecError): + errTxt := fmt.Sprint(err) + if !strings.Contains(errTxt, "noSuchKey") { + t.Errorf("Expected error to contain 'noSuchKey', got %s", errTxt) + } + default: + // Some unexpected error. + t.Fatal(err) + } +} diff --git a/internal/engine/files.go b/internal/engine/files.go new file mode 100644 index 0000000..f2cfdb3 --- /dev/null +++ b/internal/engine/files.go @@ -0,0 +1,165 @@ +/* +Copyright The Helm 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 engine + +import ( + "encoding/base64" + "path" + "strings" + + "github.com/gobwas/glob" + + "helm.sh/helm/v3/pkg/chart" +) + +// files is a map of files in a chart that can be accessed from a template. +type files map[string][]byte + +// NewFiles creates a new files from chart files. +// Given an []*chart.File (the format for files in a chart.Chart), extract a map of files. +func newFiles(from []*chart.File) files { + files := make(map[string][]byte) + for _, f := range from { + files[f.Name] = f.Data + } + return files +} + +// GetBytes gets a file by path. +// +// The returned data is raw. In a template context, this is identical to calling +// {{index .Files $path}}. +// +// This is intended to be accessed from within a template, so a missed key returns +// an empty []byte. +func (f files) GetBytes(name string) []byte { + if v, ok := f[name]; ok { + return v + } + return []byte{} +} + +// Get returns a string representation of the given file. +// +// Fetch the contents of a file as a string. It is designed to be called in a +// template. +// +// {{.Files.Get "foo"}} +func (f files) Get(name string) string { + return string(f.GetBytes(name)) +} + +// Glob takes a glob pattern and returns another files object only containing +// matched files. +// +// This is designed to be called from a template. +// +// {{ range $name, $content := .Files.Glob("foo/**") }} +// {{ $name }}: | +// {{ .Files.Get($name) | indent 4 }}{{ end }} +func (f files) Glob(pattern string) files { + g, err := glob.Compile(pattern, '/') + if err != nil { + g, _ = glob.Compile("**") + } + + nf := newFiles(nil) + for name, contents := range f { + if g.Match(name) { + nf[name] = contents + } + } + + return nf +} + +// AsConfig turns a Files group and flattens it to a YAML map suitable for +// including in the 'data' section of a Kubernetes ConfigMap definition. +// Duplicate keys will be overwritten, so be aware that your file names +// (regardless of path) should be unique. +// +// This is designed to be called from a template, and will return empty string +// (via toYAML function) if it cannot be serialized to YAML, or if the Files +// object is nil. +// +// The output will not be indented, so you will want to pipe this to the +// 'indent' template function. +// +// data: +// +// {{ .Files.Glob("config/**").AsConfig() | indent 4 }} +func (f files) AsConfig() string { + if f == nil { + return "" + } + + m := make(map[string]string) + + // Explicitly convert to strings, and file names + for k, v := range f { + m[path.Base(k)] = string(v) + } + + return toYAML(m) +} + +// AsSecrets returns the base64-encoded value of a Files object suitable for +// including in the 'data' section of a Kubernetes Secret definition. +// Duplicate keys will be overwritten, so be aware that your file names +// (regardless of path) should be unique. +// +// This is designed to be called from a template, and will return empty string +// (via toYAML function) if it cannot be serialized to YAML, or if the Files +// object is nil. +// +// The output will not be indented, so you will want to pipe this to the +// 'indent' template function. +// +// data: +// +// {{ .Files.Glob("secrets/*").AsSecrets() | indent 4 }} +func (f files) AsSecrets() string { + if f == nil { + return "" + } + + m := make(map[string]string) + + for k, v := range f { + m[path.Base(k)] = base64.StdEncoding.EncodeToString(v) + } + + return toYAML(m) +} + +// Lines returns each line of a named file (split by "\n") as a slice, so it can +// be ranged over in your templates. +// +// This is designed to be called from a template. +// +// {{ range .Files.Lines "foo/bar.html" }} +// {{ . }}{{ end }} +func (f files) Lines(path string) []string { + if f == nil || f[path] == nil { + return []string{} + } + s := string(f[path]) + if s[len(s)-1] == '\n' { + s = s[:len(s)-1] + } + return strings.Split(s, "\n") +} diff --git a/internal/engine/files_test.go b/internal/engine/files_test.go new file mode 100644 index 0000000..e53263c --- /dev/null +++ b/internal/engine/files_test.go @@ -0,0 +1,111 @@ +/* +Copyright The Helm 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 engine + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var cases = []struct { + path, data string +}{ + {"ship/captain.txt", "The Captain"}, + {"ship/stowaway.txt", "Legatt"}, + {"story/name.txt", "The Secret Sharer"}, + {"story/author.txt", "Joseph Conrad"}, + {"multiline/test.txt", "bar\nfoo\n"}, + {"multiline/test_with_blank_lines.txt", "bar\nfoo\n\n\n"}, +} + +func getTestFiles() files { + a := make(files, len(cases)) + for _, c := range cases { + a[c.path] = []byte(c.data) + } + return a +} + +func TestNewFiles(t *testing.T) { + files := getTestFiles() + if len(files) != len(cases) { + t.Errorf("Expected len() = %d, got %d", len(cases), len(files)) + } + + for i, f := range cases { + if got := string(files.GetBytes(f.path)); got != f.data { + t.Errorf("%d: expected %q, got %q", i, f.data, got) + } + if got := files.Get(f.path); got != f.data { + t.Errorf("%d: expected %q, got %q", i, f.data, got) + } + } +} + +func TestFileGlob(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + matched := f.Glob("story/**") + + as.Len(matched, 2, "Should be two files in glob story/**") + as.Equal("Joseph Conrad", matched.Get("story/author.txt")) +} + +func TestToConfig(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + out := f.Glob("**/captain.txt").AsConfig() + as.Equal("captain.txt: The Captain", out) + + out = f.Glob("ship/**").AsConfig() + as.Equal("captain.txt: The Captain\nstowaway.txt: Legatt", out) +} + +func TestToSecret(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + out := f.Glob("ship/**").AsSecrets() + as.Equal("captain.txt: VGhlIENhcHRhaW4=\nstowaway.txt: TGVnYXR0", out) +} + +func TestLines(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + out := f.Lines("multiline/test.txt") + as.Len(out, 2) + + as.Equal("bar", out[0]) +} + +func TestBlankLines(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + out := f.Lines("multiline/test_with_blank_lines.txt") + as.Len(out, 4) + + as.Equal("bar", out[0]) + as.Equal("", out[3]) +} diff --git a/internal/engine/funcs.go b/internal/engine/funcs.go new file mode 100644 index 0000000..d03a818 --- /dev/null +++ b/internal/engine/funcs.go @@ -0,0 +1,207 @@ +/* +Copyright The Helm 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 engine + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" + + "github.com/BurntSushi/toml" + "github.com/Masterminds/sprig/v3" + "sigs.k8s.io/yaml" + goYaml "sigs.k8s.io/yaml/goyaml.v3" +) + +// funcMap returns a mapping of all of the functions that Engine has. +// +// Because some functions are late-bound (e.g. contain context-sensitive +// data), the functions may not all perform identically outside of an Engine +// as they will inside of an Engine. +// +// Known late-bound functions: +// +// - "include" +// - "tpl" +// +// These are late-bound in Engine.Render(). The +// version included in the FuncMap is a placeholder. +func funcMap() template.FuncMap { + f := sprig.TxtFuncMap() + delete(f, "env") + delete(f, "expandenv") + + // Add some extra functionality + extra := template.FuncMap{ + "toToml": toTOML, + "fromToml": fromTOML, + "toYaml": toYAML, + "toYamlPretty": toYAMLPretty, + "fromYaml": fromYAML, + "fromYamlArray": fromYAMLArray, + "toJson": toJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, + + // This is a placeholder for the "include" function, which is + // late-bound to a template. By declaring it here, we preserve the + // integrity of the linter. + "include": func(string, interface{}) string { return "not implemented" }, + "tpl": func(string, interface{}) interface{} { return "not implemented" }, + "required": func(string, interface{}) (interface{}, error) { return "not implemented", nil }, + // Provide a placeholder for the "lookup" function, which requires a kubernetes + // connection. + "lookup": func(string, string, string, string) (map[string]interface{}, error) { + return map[string]interface{}{}, nil + }, + } + + for k, v := range extra { + f[k] = v + } + + return f +} + +// toYAML takes an interface, marshals it to yaml, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toYAML(v interface{}) string { + data, err := yaml.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return strings.TrimSuffix(string(data), "\n") +} + +func toYAMLPretty(v interface{}) string { + var data bytes.Buffer + encoder := goYaml.NewEncoder(&data) + encoder.SetIndent(2) + err := encoder.Encode(v) + + if err != nil { + // Swallow errors inside of a template. + return "" + } + return strings.TrimSuffix(data.String(), "\n") +} + +// fromYAML converts a YAML document into a map[string]interface{}. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromYAML(str string) map[string]interface{} { + m := map[string]interface{}{} + + if err := yaml.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// fromYAMLArray converts a YAML array into a []interface{}. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromYAMLArray(str string) []interface{} { + a := []interface{}{} + + if err := yaml.Unmarshal([]byte(str), &a); err != nil { + a = []interface{}{err.Error()} + } + return a +} + +// toTOML takes an interface, marshals it to toml, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toTOML(v interface{}) string { + b := bytes.NewBuffer(nil) + e := toml.NewEncoder(b) + err := e.Encode(v) + if err != nil { + return err.Error() + } + return b.String() +} + +// fromTOML converts a TOML document into a map[string]interface{}. +// +// This is not a general-purpose TOML parser, and will not parse all valid +// TOML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromTOML(str string) map[string]interface{} { + m := make(map[string]interface{}) + + if err := toml.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// toJSON takes an interface, marshals it to json, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toJSON(v interface{}) string { + data, err := json.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return string(data) +} + +// fromJSON converts a JSON document into a map[string]interface{}. +// +// This is not a general-purpose JSON parser, and will not parse all valid +// JSON documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromJSON(str string) map[string]interface{} { + m := make(map[string]interface{}) + + if err := json.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// fromJSONArray converts a JSON array into a []interface{}. +// +// This is not a general-purpose JSON parser, and will not parse all valid +// JSON documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromJSONArray(str string) []interface{} { + a := []interface{}{} + + if err := json.Unmarshal([]byte(str), &a); err != nil { + a = []interface{}{err.Error()} + } + return a +} diff --git a/internal/engine/funcs_test.go b/internal/engine/funcs_test.go new file mode 100644 index 0000000..a4f4d60 --- /dev/null +++ b/internal/engine/funcs_test.go @@ -0,0 +1,206 @@ +/* +Copyright The Helm 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 engine + +import ( + "strings" + "testing" + "text/template" + + "github.com/stretchr/testify/assert" +) + +func TestFuncs(t *testing.T) { + //TODO write tests for failure cases + tests := []struct { + tpl, expect string + vars interface{} + }{{ + tpl: `{{ toYaml . }}`, + expect: `foo: bar`, + vars: map[string]interface{}{"foo": "bar"}, + }, { + tpl: `{{ toYamlPretty . }}`, + expect: "baz:\n - 1\n - 2\n - 3", + vars: map[string]interface{}{"baz": []int{1, 2, 3}}, + }, { + tpl: `{{ toToml . }}`, + expect: "foo = \"bar\"\n", + vars: map[string]interface{}{"foo": "bar"}, + }, { + tpl: `{{ fromToml . }}`, + expect: "map[hello:world]", + vars: `hello = "world"`, + }, { + tpl: `{{ fromToml . }}`, + expect: "map[table:map[keyInTable:valueInTable subtable:map[keyInSubtable:valueInSubTable]]]", + vars: ` +[table] +keyInTable = "valueInTable" +[table.subtable] +keyInSubtable = "valueInSubTable"`, + }, { + tpl: `{{ fromToml . }}`, + expect: "map[tableArray:[map[keyInElement0:valueInElement0] map[keyInElement1:valueInElement1]]]", + vars: ` +[[tableArray]] +keyInElement0 = "valueInElement0" +[[tableArray]] +keyInElement1 = "valueInElement1"`, + }, { + tpl: `{{ fromToml . }}`, + expect: "map[Error:toml: line 0: unexpected EOF; expected key separator '=']", + vars: "one", + }, { + tpl: `{{ toJson . }}`, + expect: `{"foo":"bar"}`, + vars: map[string]interface{}{"foo": "bar"}, + }, { + tpl: `{{ fromYaml . }}`, + expect: "map[hello:world]", + vars: `hello: world`, + }, { + tpl: `{{ fromYamlArray . }}`, + expect: "[one 2 map[name:helm]]", + vars: "- one\n- 2\n- name: helm\n", + }, { + tpl: `{{ fromYamlArray . }}`, + expect: "[one 2 map[name:helm]]", + vars: `["one", 2, { "name": "helm" }]`, + }, { + // Regression for https://github.com/helm/helm/issues/2271 + tpl: `{{ toToml . }}`, + expect: "[mast]\n sail = \"white\"\n", + vars: map[string]map[string]string{"mast": {"sail": "white"}}, + }, { + tpl: `{{ fromYaml . }}`, + expect: "map[Error:error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type map[string]interface {}]", + vars: "- one\n- two\n", + }, { + tpl: `{{ fromJson .}}`, + expect: `map[hello:world]`, + vars: `{"hello":"world"}`, + }, { + tpl: `{{ fromJson . }}`, + expect: `map[Error:json: cannot unmarshal array into Go value of type map[string]interface {}]`, + vars: `["one", "two"]`, + }, { + tpl: `{{ fromJsonArray . }}`, + expect: `[one 2 map[name:helm]]`, + vars: `["one", 2, { "name": "helm" }]`, + }, { + tpl: `{{ fromJsonArray . }}`, + expect: `[json: cannot unmarshal object into Go value of type []interface {}]`, + vars: `{"hello": "world"}`, + }, { + tpl: `{{ merge .dict (fromYaml .yaml) }}`, + expect: `map[a:map[b:c]]`, + vars: map[string]interface{}{"dict": map[string]interface{}{"a": map[string]interface{}{"b": "c"}}, "yaml": `{"a":{"b":"d"}}`}, + }, { + tpl: `{{ merge (fromYaml .yaml) .dict }}`, + expect: `map[a:map[b:d]]`, + vars: map[string]interface{}{"dict": map[string]interface{}{"a": map[string]interface{}{"b": "c"}}, "yaml": `{"a":{"b":"d"}}`}, + }, { + tpl: `{{ fromYaml . }}`, + expect: `map[Error:error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type map[string]interface {}]`, + vars: `["one", "two"]`, + }, { + tpl: `{{ fromYamlArray . }}`, + expect: `[error unmarshaling JSON: while decoding JSON: json: cannot unmarshal object into Go value of type []interface {}]`, + vars: `hello: world`, + }, { + // This should never result in a network lookup. Regression for #7955 + tpl: `{{ lookup "v1" "Namespace" "" "unlikelynamespace99999999" }}`, + expect: `map[]`, + vars: `["one", "two"]`, + }} + + for _, tt := range tests { + var b strings.Builder + err := template.Must(template.New("test").Funcs(funcMap()).Parse(tt.tpl)).Execute(&b, tt.vars) + assert.NoError(t, err) + assert.Equal(t, tt.expect, b.String(), tt.tpl) + } +} + +// This test to check a function provided by sprig is due to a change in a +// dependency of sprig. mergo in v0.3.9 changed the way it merges and only does +// public fields (i.e. those starting with a capital letter). This test, from +// sprig, fails in the new version. This is a behavior change for mergo that +// impacts sprig and Helm users. This test will help us to not update to a +// version of mergo (even accidentally) that causes a breaking change. See +// sprig changelog and notes for more details. +// Note, Go modules assume semver is never broken. So, there is no way to tell +// the tooling to not update to a minor or patch version. `go install` could +// be used to accidentally update mergo. This test and message should catch +// the problem and explain why it's happening. +func TestMerge(t *testing.T) { + dict := map[string]interface{}{ + "src2": map[string]interface{}{ + "h": 10, + "i": "i", + "j": "j", + }, + "src1": map[string]interface{}{ + "a": 1, + "b": 2, + "d": map[string]interface{}{ + "e": "four", + }, + "g": []int{6, 7}, + "i": "aye", + "j": "jay", + "k": map[string]interface{}{ + "l": false, + }, + }, + "dst": map[string]interface{}{ + "a": "one", + "c": 3, + "d": map[string]interface{}{ + "f": 5, + }, + "g": []int{8, 9}, + "i": "eye", + "k": map[string]interface{}{ + "l": true, + }, + }, + } + tpl := `{{merge .dst .src1 .src2}}` + var b strings.Builder + err := template.Must(template.New("test").Funcs(funcMap()).Parse(tpl)).Execute(&b, dict) + assert.NoError(t, err) + + expected := map[string]interface{}{ + "a": "one", // key overridden + "b": 2, // merged from src1 + "c": 3, // merged from dst + "d": map[string]interface{}{ // deep merge + "e": "four", + "f": 5, + }, + "g": []int{8, 9}, // overridden - arrays are not merged + "h": 10, // merged from src2 + "i": "eye", // overridden twice + "j": "jay", // overridden and merged + "k": map[string]interface{}{ + "l": true, // overridden + }, + } + assert.Equal(t, expected, dict["dst"]) +} diff --git a/internal/engine/lookup_func.go b/internal/engine/lookup_func.go new file mode 100644 index 0000000..75e8509 --- /dev/null +++ b/internal/engine/lookup_func.go @@ -0,0 +1,143 @@ +/* +Copyright The Helm 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 engine + +import ( + "context" + "log" + "strings" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" +) + +type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) + +// NewLookupFunction returns a function for looking up objects in the cluster. +// +// If the resource does not exist, no error is raised. +// +// This function is considered deprecated, and will be renamed in Helm 4. It will no +// longer be a public function. +func NewLookupFunction(config *rest.Config) lookupFunc { + return newLookupFunction(clientProviderFromConfig{config: config}) +} + +type ClientProvider interface { + // GetClientFor returns a dynamic.NamespaceableResourceInterface suitable for interacting with resources + // corresponding to the provided apiVersion and kind, as well as a boolean indicating whether the resources + // are namespaced. + GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) +} + +type clientProviderFromConfig struct { + config *rest.Config +} + +func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) { + return getDynamicClientOnKind(apiVersion, kind, c.config) +} + +func newLookupFunction(clientProvider ClientProvider) lookupFunc { + return func(apiversion string, kind string, namespace string, name string) (map[string]interface{}, error) { + var client dynamic.ResourceInterface + c, namespaced, err := clientProvider.GetClientFor(apiversion, kind) + if err != nil { + return map[string]interface{}{}, err + } + if namespaced && namespace != "" { + client = c.Namespace(namespace) + } else { + client = c + } + if name != "" { + // this will return a single object + obj, err := client.Get(context.Background(), name, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + // Just return an empty interface when the object was not found. + // That way, users can use `if not (lookup ...)` in their templates. + return map[string]interface{}{}, nil + } + return map[string]interface{}{}, err + } + return obj.UnstructuredContent(), nil + } + // this will return a list + obj, err := client.List(context.Background(), metav1.ListOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + // Just return an empty interface when the object was not found. + // That way, users can use `if not (lookup ...)` in their templates. + return map[string]interface{}{}, nil + } + return map[string]interface{}{}, err + } + return obj.UnstructuredContent(), nil + } +} + +// getDynamicClientOnKind returns a dynamic client on an Unstructured type. This client can be further namespaced. +func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) { + gvk := schema.FromAPIVersionAndKind(apiversion, kind) + apiRes, err := getAPIResourceForGVK(gvk, config) + if err != nil { + log.Printf("[ERROR] unable to get apiresource from unstructured: %s , error %s", gvk.String(), err) + return nil, false, errors.Wrapf(err, "unable to get apiresource from unstructured: %s", gvk.String()) + } + gvr := schema.GroupVersionResource{ + Group: apiRes.Group, + Version: apiRes.Version, + Resource: apiRes.Name, + } + intf, err := dynamic.NewForConfig(config) + if err != nil { + log.Printf("[ERROR] unable to get dynamic client %s", err) + return nil, false, err + } + res := intf.Resource(gvr) + return res, apiRes.Namespaced, nil +} + +func getAPIResourceForGVK(gvk schema.GroupVersionKind, config *rest.Config) (metav1.APIResource, error) { + res := metav1.APIResource{} + discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { + log.Printf("[ERROR] unable to create discovery client %s", err) + return res, err + } + resList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) + if err != nil { + log.Printf("[ERROR] unable to retrieve resource list for: %s , error: %s", gvk.GroupVersion().String(), err) + return res, err + } + for _, resource := range resList.APIResources { + // if a resource contains a "/" it's referencing a subresource. we don't support subresource for now. + if resource.Kind == gvk.Kind && !strings.Contains(resource.Name, "/") { + res = resource + res.Group = gvk.Group + res.Version = gvk.Version + break + } + } + return res, nil +} diff --git a/pkg/flags/flags.go b/internal/flags/flags.go similarity index 100% rename from pkg/flags/flags.go rename to internal/flags/flags.go diff --git a/pkg/fsutils/fsutils.go b/internal/fsutils/fsutils.go similarity index 100% rename from pkg/fsutils/fsutils.go rename to internal/fsutils/fsutils.go diff --git a/pkg/fsutils/getfiles.go b/internal/fsutils/getfiles.go similarity index 100% rename from pkg/fsutils/getfiles.go rename to internal/fsutils/getfiles.go diff --git a/pkg/helm/render.go b/internal/helm/render.go similarity index 100% rename from pkg/helm/render.go rename to internal/helm/render.go diff --git a/pkg/k8s/camel.go b/internal/k8s/camel.go similarity index 100% rename from pkg/k8s/camel.go rename to internal/k8s/camel.go diff --git a/pkg/k8s/controller.go b/internal/k8s/controller.go similarity index 93% rename from pkg/k8s/controller.go rename to internal/k8s/controller.go index 10b14b9..7a7fb75 100644 --- a/pkg/k8s/controller.go +++ b/internal/k8s/controller.go @@ -11,9 +11,9 @@ import ( "gopkg.in/yaml.v3" "helm.sh/helm/v3/pkg/chartutil" - "github.com/deckhouse/d8-lint/pkg/helm" - "github.com/deckhouse/d8-lint/pkg/module" - "github.com/deckhouse/d8-lint/pkg/storage" + "github.com/deckhouse/d8-lint/internal/helm" + "github.com/deckhouse/d8-lint/internal/module" + "github.com/deckhouse/d8-lint/internal/storage" ) var ( diff --git a/pkg/k8s/images_tags_generated.go b/internal/k8s/images_tags_generated.go similarity index 100% rename from pkg/k8s/images_tags_generated.go rename to internal/k8s/images_tags_generated.go diff --git a/pkg/k8s/openapi.go b/internal/k8s/openapi.go similarity index 98% rename from pkg/k8s/openapi.go rename to internal/k8s/openapi.go index 2c6f906..7e1b7ad 100644 --- a/pkg/k8s/openapi.go +++ b/internal/k8s/openapi.go @@ -10,8 +10,8 @@ import ( "github.com/mohae/deepcopy" "helm.sh/helm/v3/pkg/chartutil" - "github.com/deckhouse/d8-lint/pkg/module" - "github.com/deckhouse/d8-lint/pkg/valuesvalidation" + "github.com/deckhouse/d8-lint/internal/module" + "github.com/deckhouse/d8-lint/internal/valuesvalidation" ) const ( diff --git a/pkg/logger/logger.go b/internal/logger/logger.go similarity index 95% rename from pkg/logger/logger.go rename to internal/logger/logger.go index a7f4703..75f61bf 100644 --- a/pkg/logger/logger.go +++ b/internal/logger/logger.go @@ -7,7 +7,7 @@ import ( "log/slog" "os" - "github.com/deckhouse/d8-lint/pkg/flags" + "github.com/deckhouse/d8-lint/internal/flags" ) var logger *slog.Logger diff --git a/pkg/manager/linter.go b/internal/manager/linter.go similarity index 81% rename from pkg/manager/linter.go rename to internal/manager/linter.go index ebe34ec..f0214f9 100644 --- a/pkg/manager/linter.go +++ b/internal/manager/linter.go @@ -1,8 +1,8 @@ package manager import ( + "github.com/deckhouse/d8-lint/internal/module" "github.com/deckhouse/d8-lint/pkg/errors" - "github.com/deckhouse/d8-lint/pkg/module" ) type Linter interface { diff --git a/pkg/manager/manager.go b/internal/manager/manager.go similarity index 94% rename from pkg/manager/manager.go rename to internal/manager/manager.go index c945ac6..883ed31 100644 --- a/pkg/manager/manager.go +++ b/internal/manager/manager.go @@ -9,15 +9,15 @@ import ( "github.com/mitchellh/go-homedir" "github.com/sourcegraph/conc/pool" + "github.com/deckhouse/d8-lint/internal/flags" + "github.com/deckhouse/d8-lint/internal/logger" + "github.com/deckhouse/d8-lint/internal/module" "github.com/deckhouse/d8-lint/pkg/config" "github.com/deckhouse/d8-lint/pkg/errors" - "github.com/deckhouse/d8-lint/pkg/flags" "github.com/deckhouse/d8-lint/pkg/linters/copyright" no_cyrillic "github.com/deckhouse/d8-lint/pkg/linters/no-cyrillic" "github.com/deckhouse/d8-lint/pkg/linters/openapi" "github.com/deckhouse/d8-lint/pkg/linters/probes" - "github.com/deckhouse/d8-lint/pkg/logger" - "github.com/deckhouse/d8-lint/pkg/module" ) const ( @@ -78,7 +78,8 @@ func NewManager(dirs []string, cfg *config.Config) *Manager { mdl, err := module.NewModule(paths[i]) if err != nil { // this error not critical, just notice what we have error on setting module chart - logger.ErrorF("Chart fill not success for module `%s`: %v", mdl.GetName(), err) + logger.ErrorF("Chart fill not success for path module `%s`: %v", paths[i], err) + continue } m.Modules = append(m.Modules, mdl) } diff --git a/pkg/module/module.go b/internal/module/module.go similarity index 100% rename from pkg/module/module.go rename to internal/module/module.go diff --git a/pkg/storage/storage.go b/internal/storage/storage.go similarity index 100% rename from pkg/storage/storage.go rename to internal/storage/storage.go diff --git a/internal/template/exec.go b/internal/template/exec.go new file mode 100644 index 0000000..edc8b51 --- /dev/null +++ b/internal/template/exec.go @@ -0,0 +1,1128 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package template + +import ( + "errors" + "fmt" + "io" + "reflect" + "runtime" + "strings" + "text/template/parse" + + "github.com/deckhouse/d8-lint/internal/template/internal/fmtsort" +) + +// maxExecDepth specifies the maximum stack depth of templates within +// templates. This limit is only practically reached by accidentally +// recursive template invocations. This limit allows us to return +// an error instead of triggering a stack overflow. +var maxExecDepth = initMaxExecDepth() + +func initMaxExecDepth() int { + if runtime.GOARCH == "wasm" { + return 1000 + } + return 100000 +} + +// state represents the state of an execution. It's not part of the +// template so that multiple executions of the same template +// can execute in parallel. +type state struct { + tmpl *Template + wr io.Writer + node parse.Node // current node, for errors + vars []variable // push-down stack of variable values. + depth int // the height of the stack of executing templates. +} + +// variable holds the dynamic value of a variable such as $, $x etc. +type variable struct { + name string + value reflect.Value +} + +// push pushes a new variable on the stack. +func (s *state) push(name string, value reflect.Value) { + s.vars = append(s.vars, variable{name, value}) +} + +// mark returns the length of the variable stack. +func (s *state) mark() int { + return len(s.vars) +} + +// pop pops the variable stack up to the mark. +func (s *state) pop(mark int) { + s.vars = s.vars[0:mark] +} + +// setVar overwrites the last declared variable with the given name. +// Used by variable assignments. +func (s *state) setVar(name string, value reflect.Value) { + for i := s.mark() - 1; i >= 0; i-- { + if s.vars[i].name == name { + s.vars[i].value = value + return + } + } + s.errorf("undefined variable: %s", name) +} + +// setTopVar overwrites the top-nth variable on the stack. Used by range iterations. +func (s *state) setTopVar(n int, value reflect.Value) { + s.vars[len(s.vars)-n].value = value +} + +// varValue returns the value of the named variable. +func (s *state) varValue(name string) reflect.Value { + for i := s.mark() - 1; i >= 0; i-- { + if s.vars[i].name == name { + return s.vars[i].value + } + } + s.errorf("undefined variable: %s", name) + return zero +} + +var zero reflect.Value + +type missingValType struct{} + +var missingVal = reflect.ValueOf(missingValType{}) + +var missingValReflectType = reflect.TypeFor[missingValType]() + +func isMissing(v reflect.Value) bool { + return v.IsValid() && v.Type() == missingValReflectType +} + +// at marks the state to be on node n, for error reporting. +func (s *state) at(node parse.Node) { + s.node = node +} + +// doublePercent returns the string with %'s replaced by %%, if necessary, +// so it can be used safely inside a Printf format string. +func doublePercent(str string) string { + return strings.ReplaceAll(str, "%", "%%") +} + +// TODO: It would be nice if ExecError was more broken down, but +// the way ErrorContext embeds the template name makes the +// processing too clumsy. + +// ExecError is the custom error type returned when Execute has an +// error evaluating its template. (If a write error occurs, the actual +// error is returned; it will not be of type ExecError.) +type ExecError struct { + Name string // Name of template. + Err error // Pre-formatted error. +} + +func (e ExecError) Error() string { + return e.Err.Error() +} + +func (e ExecError) Unwrap() error { + return e.Err +} + +// errorf records an ExecError and terminates processing. +func (s *state) errorf(format string, args ...any) { + name := doublePercent(s.tmpl.Name()) + if s.node == nil { + format = fmt.Sprintf("template: %s: %s", name, format) + } else { + location, context := s.tmpl.ErrorContext(s.node) + format = fmt.Sprintf("template: %s: executing %q at <%s>: %s", location, name, doublePercent(context), format) + } + panic(ExecError{ + Name: s.tmpl.Name(), + Err: fmt.Errorf(format, args...), + }) +} + +// writeError is the wrapper type used internally when Execute has an +// error writing to its output. We strip the wrapper in errRecover. +// Note that this is not an implementation of error, so it cannot escape +// from the package as an error value. +type writeError struct { + Err error // Original error. +} + +func (s *state) writeError(err error) { + panic(writeError{ + Err: err, + }) +} + +// errRecover is the handler that turns panics into returns from the top +// level of Parse. +func errRecover(errp *error) { + e := recover() + if e != nil { + switch err := e.(type) { + case runtime.Error: + panic(e) + case writeError: + *errp = err.Err // Strip the wrapper. + case ExecError: + *errp = err // Keep the wrapper. + default: + panic(e) + } + } +} + +// ExecuteTemplate applies the template associated with t that has the given name +// to the specified data object and writes the output to wr. +// If an error occurs executing the template or writing its output, +// execution stops, but partial results may already have been written to +// the output writer. +// A template may be executed safely in parallel, although if parallel +// executions share a Writer the output may be interleaved. +func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error { + tmpl := t.Lookup(name) + if tmpl == nil { + return fmt.Errorf("template: no template %q associated with template %q", name, t.name) + } + return tmpl.Execute(wr, data) +} + +// Execute applies a parsed template to the specified data object, +// and writes the output to wr. +// If an error occurs executing the template or writing its output, +// execution stops, but partial results may already have been written to +// the output writer. +// A template may be executed safely in parallel, although if parallel +// executions share a Writer the output may be interleaved. +// +// If data is a [reflect.Value], the template applies to the concrete +// value that the reflect.Value holds, as in [fmt.Print]. +func (t *Template) Execute(wr io.Writer, data any) error { + return t.execute(wr, data) +} + +func (t *Template) execute(wr io.Writer, data any) (err error) { + defer errRecover(&err) + value, ok := data.(reflect.Value) + if !ok { + value = reflect.ValueOf(data) + } + state := &state{ + tmpl: t, + wr: wr, + vars: []variable{{"$", value}}, + } + if t.Tree == nil || t.Root == nil { + state.errorf("%q is an incomplete or empty template", t.Name()) + } + state.walk(value, t.Root) + return +} + +// DefinedTemplates returns a string listing the defined templates, +// prefixed by the string "; defined templates are: ". If there are none, +// it returns the empty string. For generating an error message here +// and in [html/template]. +func (t *Template) DefinedTemplates() string { + if t.common == nil { + return "" + } + var b strings.Builder + t.muTmpl.RLock() + defer t.muTmpl.RUnlock() + for name, tmpl := range t.tmpl { + if tmpl.Tree == nil || tmpl.Root == nil { + continue + } + if b.Len() == 0 { + b.WriteString("; defined templates are: ") + } else { + b.WriteString(", ") + } + fmt.Fprintf(&b, "%q", name) + } + return b.String() +} + +// Sentinel errors for use with panic to signal early exits from range loops. +var ( + walkBreak = errors.New("break") + walkContinue = errors.New("continue") +) + +// Walk functions step through the major pieces of the template structure, +// generating output as they go. +func (s *state) walk(dot reflect.Value, node parse.Node) { + s.at(node) + switch node := node.(type) { + case *parse.ActionNode: + // Do not pop variables so they persist until next end. + // Also, if the action declares variables, don't print the result. + val := s.evalPipeline(dot, node.Pipe) + if len(node.Pipe.Decl) == 0 { + s.printValue(node, val) + } + case *parse.BreakNode: + panic(walkBreak) + case *parse.CommentNode: + case *parse.ContinueNode: + panic(walkContinue) + case *parse.IfNode: + s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList) + case *parse.ListNode: + for _, node := range node.Nodes { + s.walk(dot, node) + } + case *parse.RangeNode: + s.walkRange(dot, node) + case *parse.TemplateNode: + s.walkTemplate(dot, node) + case *parse.TextNode: + if _, err := s.wr.Write(node.Text); err != nil { + s.writeError(err) + } + case *parse.WithNode: + s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList) + default: + s.errorf("unknown node: %s", node) + } +} + +// walkIfOrWith walks an 'if' or 'with' node. The two control structures +// are identical in behavior except that 'with' sets dot. +func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse.PipeNode, list, elseList *parse.ListNode) { + defer s.pop(s.mark()) + val := s.evalPipeline(dot, pipe) + _, ok := isTrue(indirectInterface(val)) + if !ok { + s.errorf("if/with can't use %v", val) + } + if typ == parse.NodeWith { + s.walk(val, list) + } else { + s.walk(dot, list) + } + if elseList != nil { + s.walk(dot, elseList) + } +} + +// IsTrue reports whether the value is 'true', in the sense of not the zero of its type, +// and whether the value has a meaningful truth value. This is the definition of +// truth used by if and other such actions. +func IsTrue(val any) (truth, ok bool) { + return isTrue(reflect.ValueOf(val)) +} + +func isTrue(val reflect.Value) (truth, ok bool) { + if !val.IsValid() { + // Something like var x interface{}, never set. It's a form of nil. + return false, true + } + switch val.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + truth = val.Len() > 0 + case reflect.Bool: + truth = val.Bool() + case reflect.Complex64, reflect.Complex128: + truth = val.Complex() != 0 + case reflect.Chan, reflect.Func, reflect.Pointer, reflect.Interface: + truth = !val.IsNil() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + truth = val.Int() != 0 + case reflect.Float32, reflect.Float64: + truth = val.Float() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + truth = val.Uint() != 0 + case reflect.Struct: + truth = true // Struct values are always true. + default: + return + } + return truth, true +} + +func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) { + s.at(r) + defer func() { + if r := recover(); r != nil && r != walkBreak { + panic(r) + } + }() + defer s.pop(s.mark()) + val, _ := indirect(s.evalPipeline(dot, r.Pipe)) + // mark top of stack before any variables in the body are pushed. + mark := s.mark() + oneIteration := func(index, elem reflect.Value) { + if len(r.Pipe.Decl) > 0 { + if r.Pipe.IsAssign { + // With two variables, index comes first. + // With one, we use the element. + if len(r.Pipe.Decl) > 1 { + s.setVar(r.Pipe.Decl[0].Ident[0], index) + } else { + s.setVar(r.Pipe.Decl[0].Ident[0], elem) + } + } else { + // Set top var (lexically the second if there + // are two) to the element. + s.setTopVar(1, elem) + } + } + if len(r.Pipe.Decl) > 1 { + if r.Pipe.IsAssign { + s.setVar(r.Pipe.Decl[1].Ident[0], elem) + } else { + // Set next var (lexically the first if there + // are two) to the index. + s.setTopVar(2, index) + } + } + defer s.pop(mark) + defer func() { + // Consume panic(walkContinue) + if r := recover(); r != nil && r != walkContinue { + panic(r) + } + }() + s.walk(elem, r.List) + } + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if len(r.Pipe.Decl) > 1 { + s.errorf("can't use %v to iterate over more than one variable", val) + break + } + run := false + for v := range val.Seq() { + run = true + // Pass element as second value, as we do for channels. + oneIteration(reflect.Value{}, v) + } + if !run { + break + } + return + case reflect.Array, reflect.Slice: + if val.Len() == 0 { + break + } + for i := 0; i < val.Len(); i++ { + oneIteration(reflect.ValueOf(i), val.Index(i)) + } + return + case reflect.Map: + if val.Len() == 0 { + break + } + om := fmtsort.Sort(val) + for _, m := range om { + oneIteration(m.Key, m.Value) + } + return + case reflect.Chan: + if val.IsNil() { + break + } + if val.Type().ChanDir() == reflect.SendDir { + s.errorf("range over send-only channel %v", val) + break + } + i := 0 + for ; ; i++ { + elem, ok := val.Recv() + if !ok { + break + } + oneIteration(reflect.ValueOf(i), elem) + } + if i == 0 { + break + } + return + case reflect.Invalid: + break // An invalid value is likely a nil map, etc. and acts like an empty map. + case reflect.Func: + if val.Type().CanSeq() { + if len(r.Pipe.Decl) > 1 { + s.errorf("can't use %v iterate over more than one variable", val) + break + } + run := false + for v := range val.Seq() { + run = true + // Pass element as second value, + // as we do for channels. + oneIteration(reflect.Value{}, v) + } + if !run { + break + } + return + } + if val.Type().CanSeq2() { + run := false + for i, v := range val.Seq2() { + run = true + if len(r.Pipe.Decl) > 1 { + oneIteration(i, v) + } else { + // If there is only one range variable, + // oneIteration will use the + // second value. + oneIteration(reflect.Value{}, i) + } + } + if !run { + break + } + return + } + fallthrough + default: + s.errorf("range can't iterate over %v", val) + } + if r.ElseList != nil { + s.walk(dot, r.ElseList) + } +} + +func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) { + s.at(t) + tmpl := s.tmpl.Lookup(t.Name) + if tmpl == nil { + s.errorf("template %q not defined", t.Name) + } + if s.depth == maxExecDepth { + s.errorf("exceeded maximum template depth (%v)", maxExecDepth) + } + // Variables declared by the pipeline persist. + dot = s.evalPipeline(dot, t.Pipe) + newState := *s + newState.depth++ + newState.tmpl = tmpl + // No dynamic scoping: template invocations inherit no variables. + newState.vars = []variable{{"$", dot}} + newState.walk(dot, tmpl.Root) +} + +// Eval functions evaluate pipelines, commands, and their elements and extract +// values from the data structure by examining fields, calling methods, and so on. +// The printing of those values happens only through walk functions. + +// evalPipeline returns the value acquired by evaluating a pipeline. If the +// pipeline has a variable declaration, the variable will be pushed on the +// stack. Callers should therefore pop the stack after they are finished +// executing commands depending on the pipeline value. +func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value reflect.Value) { + if pipe == nil { + return + } + s.at(pipe) + value = missingVal + for _, cmd := range pipe.Cmds { + value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg. + // If the object has type interface{}, dig down one level to the thing inside. + if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 { + value = value.Elem() + } + } + for _, variable := range pipe.Decl { + if pipe.IsAssign { + s.setVar(variable.Ident[0], value) + } else { + s.push(variable.Ident[0], value) + } + } + return value +} + +func (s *state) notAFunction(args []parse.Node, final reflect.Value) { + if len(args) > 1 || !isMissing(final) { + s.errorf("can't give argument to non-function %s", args[0]) + } +} + +func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final reflect.Value) reflect.Value { + firstWord := cmd.Args[0] + switch n := firstWord.(type) { + case *parse.FieldNode: + return s.evalFieldNode(dot, n, cmd.Args, final) + case *parse.ChainNode: + return s.evalChainNode(dot, n, cmd.Args, final) + case *parse.IdentifierNode: + // Must be a function. + return s.evalFunction(dot, n, cmd, cmd.Args, final) + case *parse.PipeNode: + // Parenthesized pipeline. The arguments are all inside the pipeline; final must be absent. + s.notAFunction(cmd.Args, final) + return s.evalPipeline(dot, n) + case *parse.VariableNode: + return s.evalVariableNode(dot, n, cmd.Args, final) + } + s.at(firstWord) + s.notAFunction(cmd.Args, final) + switch word := firstWord.(type) { + case *parse.BoolNode: + return reflect.ValueOf(word.True) + case *parse.DotNode: + return dot + case *parse.NilNode: + s.errorf("nil is not a command") + case *parse.NumberNode: + return s.idealConstant(word) + case *parse.StringNode: + return reflect.ValueOf(word.Text) + } + s.errorf("can't evaluate command %q", firstWord) + panic("not reached") +} + +// idealConstant is called to return the value of a number in a context where +// we don't know the type. In that case, the syntax of the number tells us +// its type, and we use Go rules to resolve. Note there is no such thing as +// a uint ideal constant in this situation - the value must be of int type. +func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value { + // These are ideal constants but we don't know the type + // and we have no context. (If it was a method argument, + // we'd know what we need.) The syntax guides us to some extent. + s.at(constant) + switch { + case constant.IsComplex: + return reflect.ValueOf(constant.Complex128) // incontrovertible. + + case constant.IsFloat && + !isHexInt(constant.Text) && !isRuneInt(constant.Text) && + strings.ContainsAny(constant.Text, ".eEpP"): + return reflect.ValueOf(constant.Float64) + + case constant.IsInt: + n := int(constant.Int64) + if int64(n) != constant.Int64 { + s.errorf("%s overflows int", constant.Text) + } + return reflect.ValueOf(n) + + case constant.IsUint: + s.errorf("%s overflows int", constant.Text) + } + return zero +} + +func isRuneInt(s string) bool { + return len(s) > 0 && s[0] == '\'' +} + +func isHexInt(s string) bool { + return len(s) > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') && !strings.ContainsAny(s, "pP") +} + +func (s *state) evalFieldNode(dot reflect.Value, field *parse.FieldNode, args []parse.Node, final reflect.Value) reflect.Value { + s.at(field) + return s.evalFieldChain(dot, dot, field, field.Ident, args, final) +} + +func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []parse.Node, final reflect.Value) reflect.Value { + s.at(chain) + if len(chain.Field) == 0 { + s.errorf("internal error: no fields in evalChainNode") + } + if chain.Node.Type() == parse.NodeNil { + s.errorf("indirection through explicit nil in %s", chain) + } + // (pipe).Field1.Field2 has pipe as .Node, fields as .Field. Eval the pipeline, then the fields. + pipe := s.evalArg(dot, nil, chain.Node) + return s.evalFieldChain(dot, pipe, chain, chain.Field, args, final) +} + +func (s *state) evalVariableNode(dot reflect.Value, variable *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value { + // $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields. + s.at(variable) + value := s.varValue(variable.Ident[0]) + if len(variable.Ident) == 1 { + s.notAFunction(args, final) + return value + } + return s.evalFieldChain(dot, value, variable, variable.Ident[1:], args, final) +} + +// evalFieldChain evaluates .X.Y.Z possibly followed by arguments. +// dot is the environment in which to evaluate arguments, while +// receiver is the value being walked along the chain. +func (s *state) evalFieldChain(dot, receiver reflect.Value, node parse.Node, ident []string, args []parse.Node, final reflect.Value) reflect.Value { + n := len(ident) + for i := 0; i < n-1; i++ { + receiver = s.evalField(dot, ident[i], node, nil, missingVal, receiver) + } + // Now if it's a method, it gets the arguments. + return s.evalField(dot, ident[n-1], node, args, final, receiver) +} + +func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value { + s.at(node) + name := node.Ident + function, isBuiltin, ok := findFunction(name, s.tmpl) + if !ok { + s.errorf("%q is not a defined function", name) + } + return s.evalCall(dot, function, isBuiltin, cmd, name, args, final) +} + +// evalField evaluates an expression like (.Field) or (.Field arg1 arg2). +// The 'final' argument represents the return value from the preceding +// value of the pipeline, if any. +func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, args []parse.Node, final, receiver reflect.Value) reflect.Value { + if !receiver.IsValid() { + if s.tmpl.option.missingKey == mapError { // Treat invalid value as missing map key. + s.errorf("nil data; no entry for key %q", fieldName) + } + return zero + } + typ := receiver.Type() + receiver, isNil := indirect(receiver) + if receiver.Kind() == reflect.Interface && isNil { + // Calling a method on a nil interface can't work. The + // MethodByName method call below would panic. + s.errorf("nil pointer evaluating %s.%s", typ, fieldName) + return zero + } + + // Unless it's an interface, need to get to a value of type *T to guarantee + // we see all methods of T and *T. + ptr := receiver + if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Pointer && ptr.CanAddr() { + ptr = ptr.Addr() + } + if method := ptr.MethodByName(fieldName); method.IsValid() { + return s.evalCall(dot, method, false, node, fieldName, args, final) + } + hasArgs := len(args) > 1 || !isMissing(final) + // It's not a method; must be a field of a struct or an element of a map. + switch receiver.Kind() { + case reflect.Struct: + tField, ok := receiver.Type().FieldByName(fieldName) + if ok { + field, err := receiver.FieldByIndexErr(tField.Index) + if !tField.IsExported() { + s.errorf("%s is an unexported field of struct type %s", fieldName, typ) + } + if err != nil { + s.errorf("%v", err) + } + // If it's a function, we must call it. + if hasArgs { + s.errorf("%s has arguments but cannot be invoked as function", fieldName) + } + return field + } + case reflect.Map: + // If it's a map, attempt to use the field name as a key. + nameVal := reflect.ValueOf(fieldName) + if nameVal.Type().AssignableTo(receiver.Type().Key()) { + if hasArgs { + s.errorf("%s is not a method but has arguments", fieldName) + } + result := receiver.MapIndex(nameVal) + if !result.IsValid() { + switch s.tmpl.option.missingKey { + case mapInvalid: + // Just use the invalid value. + case mapZeroValue: + result = reflect.Zero(receiver.Type().Elem()) + case mapError: + s.errorf("map has no entry for key %q", fieldName) + } + } + return result + } + case reflect.Pointer: + etyp := receiver.Type().Elem() + if etyp.Kind() == reflect.Struct { + if _, ok := etyp.FieldByName(fieldName); !ok { + // If there's no such field, say "can't evaluate" + // instead of "nil pointer evaluating". + break + } + } + if isNil { + s.errorf("nil pointer evaluating %s.%s", typ, fieldName) + } + } + s.errorf("can't evaluate field %s in type %s", fieldName, typ) + panic("not reached") +} + +var ( + errorType = reflect.TypeFor[error]() + fmtStringerType = reflect.TypeFor[fmt.Stringer]() + reflectValueType = reflect.TypeFor[reflect.Value]() +) + +// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so +// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0] +// as the function itself. +func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value { + if args != nil { + args = args[1:] // Zeroth arg is function name/node; not passed to function. + } + typ := fun.Type() + numIn := len(args) + if !isMissing(final) { + numIn++ + } + numFixed := len(args) + if typ.IsVariadic() { + numFixed = typ.NumIn() - 1 // last arg is the variadic one. + if numIn < numFixed { + s.errorf("wrong number of args for %s: want at least %d got %d", name, typ.NumIn()-1, len(args)) + } + } else if numIn != typ.NumIn() { + s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn) + } + if err := goodFunc(name, typ); err != nil { + s.errorf("%v", err) + } + + unwrap := func(v reflect.Value) reflect.Value { + if v.Type() == reflectValueType { + v = v.Interface().(reflect.Value) + } + return v + } + + // Special case for builtin and/or, which short-circuit. + if isBuiltin && (name == "and" || name == "or") { + argType := typ.In(0) + var v reflect.Value + for _, arg := range args { + v = s.evalArg(dot, argType, arg).Interface().(reflect.Value) + if truth(v) == (name == "or") { + // This value was already unwrapped + // by the .Interface().(reflect.Value). + return v + } + } + if final != missingVal { + // The last argument to and/or is coming from + // the pipeline. We didn't short circuit on an earlier + // argument, so we are going to return this one. + // We don't have to evaluate final, but we do + // have to check its type. Then, since we are + // going to return it, we have to unwrap it. + v = unwrap(s.validateType(final, argType)) + } + return v + } + + // Build the arg list. + argv := make([]reflect.Value, numIn) + // Args must be evaluated. Fixed args first. + i := 0 + for ; i < numFixed && i < len(args); i++ { + argv[i] = s.evalArg(dot, typ.In(i), args[i]) + } + // Now the ... args. + if typ.IsVariadic() { + argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice. + for ; i < len(args); i++ { + argv[i] = s.evalArg(dot, argType, args[i]) + } + } + // Add final value if necessary. + if !isMissing(final) { + t := typ.In(typ.NumIn() - 1) + if typ.IsVariadic() { + if numIn-1 < numFixed { + // The added final argument corresponds to a fixed parameter of the function. + // Validate against the type of the actual parameter. + t = typ.In(numIn - 1) + } else { + // The added final argument corresponds to the variadic part. + // Validate against the type of the elements of the variadic slice. + t = t.Elem() + } + } + argv[i] = s.validateType(final, t) + } + + // Special case for the "call" builtin. + // Insert the name of the callee function as the first argument. + if isBuiltin && name == "call" { + calleeName := args[0].String() + argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...) + fun = reflect.ValueOf(call) + } + + v, err := safeCall(fun, argv) + // If we have an error that is not nil, stop execution and return that + // error to the caller. + if err != nil { + s.at(node) + s.errorf("error calling %s: %w", name, err) + } + return unwrap(v) +} + +// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. +func canBeNil(typ reflect.Type) bool { + switch typ.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return true + case reflect.Struct: + return typ == reflectValueType + } + return false +} + +// validateType guarantees that the value is valid and assignable to the type. +func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value { + if !value.IsValid() { + if typ == nil { + // An untyped nil interface{}. Accept as a proper nil value. + return reflect.ValueOf(nil) + } + if canBeNil(typ) { + // Like above, but use the zero value of the non-nil type. + return reflect.Zero(typ) + } + s.errorf("invalid value; expected %s", typ) + } + if typ == reflectValueType && value.Type() != typ { + return reflect.ValueOf(value) + } + if typ != nil && !value.Type().AssignableTo(typ) { + if value.Kind() == reflect.Interface && !value.IsNil() { + value = value.Elem() + if value.Type().AssignableTo(typ) { + return value + } + // fallthrough + } + // Does one dereference or indirection work? We could do more, as we + // do with method receivers, but that gets messy and method receivers + // are much more constrained, so it makes more sense there than here. + // Besides, one is almost always all you need. + switch { + case value.Kind() == reflect.Pointer && value.Type().Elem().AssignableTo(typ): + value = value.Elem() + if !value.IsValid() { + s.errorf("dereference of nil pointer of type %s", typ) + } + case reflect.PointerTo(value.Type()).AssignableTo(typ) && value.CanAddr(): + value = value.Addr() + default: + s.errorf("wrong type for value; expected %s; got %s", typ, value.Type()) + } + } + return value +} + +func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) reflect.Value { + s.at(n) + switch arg := n.(type) { + case *parse.DotNode: + return s.validateType(dot, typ) + case *parse.NilNode: + if canBeNil(typ) { + return reflect.Zero(typ) + } + s.errorf("cannot assign nil to %s", typ) + case *parse.FieldNode: + return s.validateType(s.evalFieldNode(dot, arg, []parse.Node{n}, missingVal), typ) + case *parse.VariableNode: + return s.validateType(s.evalVariableNode(dot, arg, nil, missingVal), typ) + case *parse.PipeNode: + return s.validateType(s.evalPipeline(dot, arg), typ) + case *parse.IdentifierNode: + return s.validateType(s.evalFunction(dot, arg, arg, nil, missingVal), typ) + case *parse.ChainNode: + return s.validateType(s.evalChainNode(dot, arg, nil, missingVal), typ) + } + switch typ.Kind() { + case reflect.Bool: + return s.evalBool(typ, n) + case reflect.Complex64, reflect.Complex128: + return s.evalComplex(typ, n) + case reflect.Float32, reflect.Float64: + return s.evalFloat(typ, n) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return s.evalInteger(typ, n) + case reflect.Interface: + if typ.NumMethod() == 0 { + return s.evalEmptyInterface(dot, n) + } + case reflect.Struct: + if typ == reflectValueType { + return reflect.ValueOf(s.evalEmptyInterface(dot, n)) + } + case reflect.String: + return s.evalString(typ, n) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return s.evalUnsignedInteger(typ, n) + } + s.errorf("can't handle %s for arg of type %s", n, typ) + panic("not reached") +} + +func (s *state) evalBool(typ reflect.Type, n parse.Node) reflect.Value { + s.at(n) + if n, ok := n.(*parse.BoolNode); ok { + value := reflect.New(typ).Elem() + value.SetBool(n.True) + return value + } + s.errorf("expected bool; found %s", n) + panic("not reached") +} + +func (s *state) evalString(typ reflect.Type, n parse.Node) reflect.Value { + s.at(n) + if n, ok := n.(*parse.StringNode); ok { + value := reflect.New(typ).Elem() + value.SetString(n.Text) + return value + } + s.errorf("expected string; found %s", n) + panic("not reached") +} + +func (s *state) evalInteger(typ reflect.Type, n parse.Node) reflect.Value { + s.at(n) + if n, ok := n.(*parse.NumberNode); ok && n.IsInt { + value := reflect.New(typ).Elem() + value.SetInt(n.Int64) + return value + } + s.errorf("expected integer; found %s", n) + panic("not reached") +} + +func (s *state) evalUnsignedInteger(typ reflect.Type, n parse.Node) reflect.Value { + s.at(n) + if n, ok := n.(*parse.NumberNode); ok && n.IsUint { + value := reflect.New(typ).Elem() + value.SetUint(n.Uint64) + return value + } + s.errorf("expected unsigned integer; found %s", n) + panic("not reached") +} + +func (s *state) evalFloat(typ reflect.Type, n parse.Node) reflect.Value { + s.at(n) + if n, ok := n.(*parse.NumberNode); ok && n.IsFloat { + value := reflect.New(typ).Elem() + value.SetFloat(n.Float64) + return value + } + s.errorf("expected float; found %s", n) + panic("not reached") +} + +func (s *state) evalComplex(typ reflect.Type, n parse.Node) reflect.Value { + if n, ok := n.(*parse.NumberNode); ok && n.IsComplex { + value := reflect.New(typ).Elem() + value.SetComplex(n.Complex128) + return value + } + s.errorf("expected complex; found %s", n) + panic("not reached") +} + +func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Value { + s.at(n) + switch n := n.(type) { + case *parse.BoolNode: + return reflect.ValueOf(n.True) + case *parse.DotNode: + return dot + case *parse.FieldNode: + return s.evalFieldNode(dot, n, nil, missingVal) + case *parse.IdentifierNode: + return s.evalFunction(dot, n, n, nil, missingVal) + case *parse.NilNode: + // NilNode is handled in evalArg, the only place that calls here. + s.errorf("evalEmptyInterface: nil (can't happen)") + case *parse.NumberNode: + return s.idealConstant(n) + case *parse.StringNode: + return reflect.ValueOf(n.Text) + case *parse.VariableNode: + return s.evalVariableNode(dot, n, nil, missingVal) + case *parse.PipeNode: + return s.evalPipeline(dot, n) + } + s.errorf("can't handle assignment of %s to empty interface argument", n) + panic("not reached") +} + +// indirect returns the item at the end of indirection, and a bool to indicate +// if it's nil. If the returned bool is true, the returned value's kind will be +// either a pointer or interface. +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + } + return v, false +} + +// indirectInterface returns the concrete value in an interface value, +// or else the zero reflect.Value. +// That is, if v represents the interface value x, the result is the same as reflect.ValueOf(x): +// the fact that x was an interface value is forgotten. +func indirectInterface(v reflect.Value) reflect.Value { + if v.Kind() != reflect.Interface { + return v + } + if v.IsNil() { + return reflect.Value{} + } + return v.Elem() +} + +// printValue writes the textual representation of the value to the output of +// the template. +func (s *state) printValue(n parse.Node, v reflect.Value) { + s.at(n) + iface, ok := printableValue(v) + if !ok { + s.errorf("can't print %s of type %s", n, v.Type()) + } + _, err := fmt.Fprint(s.wr, iface) + if err != nil { + s.writeError(err) + } +} + +// printableValue returns the, possibly indirected, interface value inside v that +// is best for a call to formatted printer. +func printableValue(v reflect.Value) (any, bool) { + if v.Kind() == reflect.Pointer { + v, _ = indirect(v) // fmt.Fprint handles nil. + } + if !v.IsValid() { + return "", true + } + + if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) { + if v.CanAddr() && (reflect.PointerTo(v.Type()).Implements(errorType) || reflect.PointerTo(v.Type()).Implements(fmtStringerType)) { + v = v.Addr() + } else { + switch v.Kind() { + case reflect.Chan, reflect.Func: + return nil, false + } + } + } + return v.Interface(), true +} diff --git a/internal/template/exec_test.go b/internal/template/exec_test.go new file mode 100644 index 0000000..8b40fe4 --- /dev/null +++ b/internal/template/exec_test.go @@ -0,0 +1,1959 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package template + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io" + "iter" + "reflect" + "strings" + "sync" + "testing" +) + +var debug = flag.Bool("debug", false, "show the errors produced by the tests") + +// T has lots of interesting pieces to use to test execution. +type T struct { + // Basics + True bool + I int + U16 uint16 + X, S string + FloatZero float64 + ComplexZero complex128 + // Nested structs. + U *U + // Struct with String method. + V0 V + V1, V2 *V + // Struct with Error method. + W0 W + W1, W2 *W + // Slices + SI []int + SICap []int + SIEmpty []int + SB []bool + // Arrays + AI [3]int + // Maps + MSI map[string]int + MSIone map[string]int // one element, for deterministic output + MSIEmpty map[string]int + MXI map[any]int + MII map[int]int + MI32S map[int32]string + MI64S map[int64]string + MUI32S map[uint32]string + MUI64S map[uint64]string + MI8S map[int8]string + MUI8S map[uint8]string + SMSI []map[string]int + // Empty interfaces; used to see if we can dig inside one. + Empty0 any // nil + Empty1 any + Empty2 any + Empty3 any + Empty4 any + // Non-empty interfaces. + NonEmptyInterface I + NonEmptyInterfacePtS *I + NonEmptyInterfaceNil I + NonEmptyInterfaceTypedNil I + // Stringer. + Str fmt.Stringer + Err error + // Pointers + PI *int + PS *string + PSI *[]int + NIL *int + // Function (not method) + BinaryFunc func(string, string) string + VariadicFunc func(...string) string + VariadicFuncInt func(int, ...string) string + NilOKFunc func(*int) bool + ErrFunc func() (string, error) + PanicFunc func() string + TooFewReturnCountFunc func() + TooManyReturnCountFunc func() (string, error, int) + InvalidReturnTypeFunc func() (string, bool) + // Template to test evaluation of templates. + Tmpl *Template + // Unexported field; cannot be accessed by template. + unexported int +} + +type S []string + +func (S) Method0() string { + return "M0" +} + +type U struct { + V string +} + +type V struct { + j int +} + +func (v *V) String() string { + if v == nil { + return "nilV" + } + return fmt.Sprintf("<%d>", v.j) +} + +type W struct { + k int +} + +func (w *W) Error() string { + if w == nil { + return "nilW" + } + return fmt.Sprintf("[%d]", w.k) +} + +var siVal = I(S{"a", "b"}) + +var tVal = &T{ + True: true, + I: 17, + U16: 16, + X: "x", + S: "xyz", + U: &U{"v"}, + V0: V{6666}, + V1: &V{7777}, // leave V2 as nil + W0: W{888}, + W1: &W{999}, // leave W2 as nil + SI: []int{3, 4, 5}, + SICap: make([]int, 5, 10), + AI: [3]int{3, 4, 5}, + SB: []bool{true, false}, + MSI: map[string]int{"one": 1, "two": 2, "three": 3}, + MSIone: map[string]int{"one": 1}, + MXI: map[any]int{"one": 1}, + MII: map[int]int{1: 1}, + MI32S: map[int32]string{1: "one", 2: "two"}, + MI64S: map[int64]string{2: "i642", 3: "i643"}, + MUI32S: map[uint32]string{2: "u322", 3: "u323"}, + MUI64S: map[uint64]string{2: "ui642", 3: "ui643"}, + MI8S: map[int8]string{2: "i82", 3: "i83"}, + MUI8S: map[uint8]string{2: "u82", 3: "u83"}, + SMSI: []map[string]int{ + {"one": 1, "two": 2}, + {"eleven": 11, "twelve": 12}, + }, + Empty1: 3, + Empty2: "empty2", + Empty3: []int{7, 8}, + Empty4: &U{"UinEmpty"}, + NonEmptyInterface: &T{X: "x"}, + NonEmptyInterfacePtS: &siVal, + NonEmptyInterfaceTypedNil: (*T)(nil), + Str: bytes.NewBuffer([]byte("foozle")), + Err: errors.New("erroozle"), + PI: newInt(23), + PS: newString("a string"), + PSI: newIntSlice(21, 22, 23), + BinaryFunc: func(a, b string) string { return fmt.Sprintf("[%s=%s]", a, b) }, + VariadicFunc: func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") }, + VariadicFuncInt: func(a int, s ...string) string { return fmt.Sprint(a, "=<", strings.Join(s, "+"), ">") }, + NilOKFunc: func(s *int) bool { return s == nil }, + ErrFunc: func() (string, error) { return "bla", nil }, + PanicFunc: func() string { panic("test panic") }, + TooFewReturnCountFunc: func() {}, + TooManyReturnCountFunc: func() (string, error, int) { return "", nil, 0 }, + InvalidReturnTypeFunc: func() (string, bool) { return "", false }, + Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X +} + +var tSliceOfNil = []*T{nil} + +// A non-empty interface. +type I interface { + Method0() string +} + +var iVal I = tVal + +// Helpers for creation. +func newInt(n int) *int { + return &n +} + +func newString(s string) *string { + return &s +} + +func newIntSlice(n ...int) *[]int { + p := new([]int) + *p = make([]int, len(n)) + copy(*p, n) + return p +} + +// Simple methods with and without arguments. +func (t *T) Method0() string { + return "M0" +} + +func (t *T) Method1(a int) int { + return a +} + +func (t *T) Method2(a uint16, b string) string { + return fmt.Sprintf("Method2: %d %s", a, b) +} + +func (t *T) Method3(v any) string { + return fmt.Sprintf("Method3: %v", v) +} + +func (t *T) Copy() *T { + n := new(T) + *n = *t + return n +} + +func (t *T) MAdd(a int, b []int) []int { + v := make([]int, len(b)) + for i, x := range b { + v[i] = x + a + } + return v +} + +var myError = errors.New("my error") + +// MyError returns a value and an error according to its argument. +func (t *T) MyError(error bool) (bool, error) { + if error { + return true, myError + } + return false, nil +} + +// A few methods to test chaining. +func (t *T) GetU() *U { + return t.U +} + +func (u *U) TrueFalse(b bool) string { + if b { + return "true" + } + return "" +} + +func typeOf(arg any) string { + return fmt.Sprintf("%T", arg) +} + +type execTest struct { + name string + input string + output string + data any + ok bool +} + +// bigInt and bigUint are hex string representing numbers either side +// of the max int boundary. +// We do it this way so the test doesn't depend on ints being 32 bits. +var ( + bigInt = fmt.Sprintf("0x%x", int(1<", tVal, true}, + {"map .one interface", "{{.MXI.one}}", "1", tVal, true}, + {"map .WRONG args", "{{.MSI.one 1}}", "", tVal, false}, + {"map .WRONG type", "{{.MII.one}}", "", tVal, false}, + + // Dots of all kinds to test basic evaluation. + {"dot int", "<{{.}}>", "<13>", 13, true}, + {"dot uint", "<{{.}}>", "<14>", uint(14), true}, + {"dot float", "<{{.}}>", "<15.1>", 15.1, true}, + {"dot bool", "<{{.}}>", "", true, true}, + {"dot complex", "<{{.}}>", "<(16.2-17i)>", 16.2 - 17i, true}, + {"dot string", "<{{.}}>", "", "hello", true}, + {"dot slice", "<{{.}}>", "<[-1 -2 -3]>", []int{-1, -2, -3}, true}, + {"dot map", "<{{.}}>", "", map[string]int{"two": 22}, true}, + {"dot struct", "<{{.}}>", "<{7 seven}>", struct { + a int + b string + }{7, "seven"}, true}, + + // Variables. + {"$ int", "{{$}}", "123", 123, true}, + {"$.I", "{{$.I}}", "17", tVal, true}, + {"$.U.V", "{{$.U.V}}", "v", tVal, true}, + {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, + {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true}, + {"nested assignment", + "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}", + "3", tVal, true}, + {"nested assignment changes the last declaration", + "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}", + "1", tVal, true}, + + // Type with String method. + {"V{6666}.String()", "-{{.V0}}-", "-<6666>-", tVal, true}, + {"&V{7777}.String()", "-{{.V1}}-", "-<7777>-", tVal, true}, + {"(*V)(nil).String()", "-{{.V2}}-", "-nilV-", tVal, true}, + + // Type with Error method. + {"W{888}.Error()", "-{{.W0}}-", "-[888]-", tVal, true}, + {"&W{999}.Error()", "-{{.W1}}-", "-[999]-", tVal, true}, + {"(*W)(nil).Error()", "-{{.W2}}-", "-nilW-", tVal, true}, + + // Pointers. + {"*int", "{{.PI}}", "23", tVal, true}, + {"*string", "{{.PS}}", "a string", tVal, true}, + {"*[]int", "{{.PSI}}", "[21 22 23]", tVal, true}, + {"*[]int[1]", "{{index .PSI 1}}", "22", tVal, true}, + {"NIL", "{{.NIL}}", "", tVal, true}, + + // Empty interfaces holding values. + {"empty nil", "{{.Empty0}}", "", tVal, true}, + {"empty with int", "{{.Empty1}}", "3", tVal, true}, + {"empty with string", "{{.Empty2}}", "empty2", tVal, true}, + {"empty with slice", "{{.Empty3}}", "[7 8]", tVal, true}, + {"empty with struct", "{{.Empty4}}", "{UinEmpty}", tVal, true}, + {"empty with struct, field", "{{.Empty4.V}}", "UinEmpty", tVal, true}, + + // Edge cases with with an interface value + {"field on interface", "{{.foo}}", "", nil, true}, + {"field on parenthesized interface", "{{(.).foo}}", "", nil, true}, + + // Issue 31810: Parenthesized first element of pipeline with arguments. + // See also TestIssue31810. + {"unparenthesized non-function", "{{1 2}}", "", nil, false}, + {"parenthesized non-function", "{{(1) 2}}", "", nil, false}, + {"parenthesized non-function with no args", "{{(1)}}", "1", nil, true}, // This is fine. + + // Method calls. + {".Method0", "-{{.Method0}}-", "-M0-", tVal, true}, + {".Method1(1234)", "-{{.Method1 1234}}-", "-1234-", tVal, true}, + {".Method1(.I)", "-{{.Method1 .I}}-", "-17-", tVal, true}, + {".Method2(3, .X)", "-{{.Method2 3 .X}}-", "-Method2: 3 x-", tVal, true}, + {".Method2(.U16, `str`)", "-{{.Method2 .U16 `str`}}-", "-Method2: 16 str-", tVal, true}, + {".Method2(.U16, $x)", "{{if $x := .X}}-{{.Method2 .U16 $x}}{{end}}-", "-Method2: 16 x-", tVal, true}, + {".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: -", tVal, true}, + {".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: -", tVal, true}, + {"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true}, + {"method on chained var", + "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", + "trueWRONG", tVal, true}, + {"chained method", + "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", + "trueWRONG", tVal, true}, + {"chained method on variable", + "{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}", + "true", tVal, true}, + {".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true}, + {".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true}, + {"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true}, + {"method on typed nil interface value", "{{.NonEmptyInterfaceTypedNil.Method0}}", "M0", tVal, true}, + + // Function call builtin. + {".BinaryFunc", "{{call .BinaryFunc `1` `2`}}", "[1=2]", tVal, true}, + {".VariadicFunc0", "{{call .VariadicFunc}}", "<>", tVal, true}, + {".VariadicFunc2", "{{call .VariadicFunc `he` `llo`}}", "", tVal, true}, + {".VariadicFuncInt", "{{call .VariadicFuncInt 33 `he` `llo`}}", "33=", tVal, true}, + {"if .BinaryFunc call", "{{ if .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{end}}", "[1=2]", tVal, true}, + {"if not .BinaryFunc call", "{{ if not .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{else}}No{{end}}", "[1=2]No", tVal, true}, + {"Interface Call", `{{stringer .S}}`, "foozle", map[string]any{"S": bytes.NewBufferString("foozle")}, true}, + {".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true}, + {"call nil", "{{call nil}}", "", tVal, false}, + + // Erroneous function calls (check args). + {".BinaryFuncTooFew", "{{call .BinaryFunc `1`}}", "", tVal, false}, + {".BinaryFuncTooMany", "{{call .BinaryFunc `1` `2` `3`}}", "", tVal, false}, + {".BinaryFuncBad0", "{{call .BinaryFunc 1 3}}", "", tVal, false}, + {".BinaryFuncBad1", "{{call .BinaryFunc `1` 3}}", "", tVal, false}, + {".VariadicFuncBad0", "{{call .VariadicFunc 3}}", "", tVal, false}, + {".VariadicFuncIntBad0", "{{call .VariadicFuncInt}}", "", tVal, false}, + {".VariadicFuncIntBad`", "{{call .VariadicFuncInt `x`}}", "", tVal, false}, + {".VariadicFuncNilBad", "{{call .VariadicFunc nil}}", "", tVal, false}, + + // Pipelines. + {"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 M0-", tVal, true}, + {"pipeline func", "-{{call .VariadicFunc `llo` | call .VariadicFunc `he` }}-", "->-", tVal, true}, + + // Nil values aren't missing arguments. + {"nil pipeline", "{{ .Empty0 | call .NilOKFunc }}", "true", tVal, true}, + {"nil call arg", "{{ call .NilOKFunc .Empty0 }}", "true", tVal, true}, + {"bad nil pipeline", "{{ .Empty0 | .VariadicFunc }}", "", tVal, false}, + + // Parenthesized expressions + {"parens in pipeline", "{{printf `%d %d %d` (1) (2 | add 3) (add 4 (add 5 6))}}", "1 5 15", tVal, true}, + + // Parenthesized expressions with field accesses + {"parens: $ in paren", "{{($).X}}", "x", tVal, true}, + {"parens: $.GetU in paren", "{{($.GetU).V}}", "v", tVal, true}, + {"parens: $ in paren in pipe", "{{($ | echo).X}}", "x", tVal, true}, + {"parens: spaces and args", `{{(makemap "up" "down" "left" "right").left}}`, "right", tVal, true}, + + // If. + {"if true", "{{if true}}TRUE{{end}}", "TRUE", tVal, true}, + {"if false", "{{if false}}TRUE{{else}}FALSE{{end}}", "TRUEFALSE", tVal, true}, + {"if nil", "{{if nil}}TRUE{{end}}", "", tVal, false}, + {"if on typed nil interface value", "{{if .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "TRUE", tVal, true}, + {"if 1", "{{if 1}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZEROZERO", tVal, true}, + {"if 0", "{{if 0}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZEROZERO", tVal, true}, + {"if 1.5", "{{if 1.5}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZEROZERO", tVal, true}, + {"if 0.0", "{{if .FloatZero}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZEROZERO", tVal, true}, + {"if 1.5i", "{{if 1.5i}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZEROZERO", tVal, true}, + {"if 0.0i", "{{if .ComplexZero}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZEROZERO", tVal, true}, + {"if emptystring", "{{if ``}}NON-EMPTY{{else}}EMPTY{{end}}", "NON-EMPTYEMPTY", tVal, true}, + {"if string", "{{if `notempty`}}NON-EMPTY{{else}}EMPTY{{end}}", "NON-EMPTYEMPTY", tVal, true}, + {"if emptyslice", "{{if .SIEmpty}}NON-EMPTY{{else}}EMPTY{{end}}", "NON-EMPTYEMPTY", tVal, true}, + {"if slice", "{{if .SI}}NON-EMPTY{{else}}EMPTY{{end}}", "NON-EMPTYEMPTY", tVal, true}, + {"if emptymap", "{{if .MSIEmpty}}NON-EMPTY{{else}}EMPTY{{end}}", "NON-EMPTYEMPTY", tVal, true}, + {"if map", "{{if .MSI}}NON-EMPTY{{else}}EMPTY{{end}}", "NON-EMPTYEMPTY", tVal, true}, + {"if map unset", "{{if .MXI.none}}NON-ZERO{{else}}ZERO{{end}}", "NON-ZEROZERO", tVal, true}, + {"if map not unset", "{{if not .MXI.none}}ZERO{{else}}NON-ZERO{{end}}", "ZERONON-ZERO", tVal, true}, + {"if $x with $y int", "{{if $x := true}}{{with $y := .I}}{{$x}},{{$y}}{{end}}{{end}}", "true,17", tVal, true}, + {"if $x with $x int", "{{if $x := true}}{{with $x := .I}}{{$x}},{{end}}{{$x}}{{end}}", "17,true", tVal, true}, + {"if else if", "{{if false}}FALSE{{else if true}}TRUE{{end}}", "FALSETRUE", tVal, true}, + //{"if else chain", "{{if eq 1 3}}1{{else if eq 2 3}}2{{else if eq 3 3}}3{{end}}", "123", tVal, true}, + + // Print etc. + {"print", `{{print "hello, print"}}`, "hello, print", tVal, true}, + {"print 123", `{{print 1 2 3}}`, "1 2 3", tVal, true}, + {"print nil", `{{print nil}}`, "", tVal, true}, + {"println", `{{println 1 2 3}}`, "1 2 3\n", tVal, true}, + {"printf int", `{{printf "%04x" 127}}`, "007f", tVal, true}, + {"printf float", `{{printf "%g" 3.5}}`, "3.5", tVal, true}, + {"printf complex", `{{printf "%g" 1+7i}}`, "(1+7i)", tVal, true}, + {"printf string", `{{printf "%s" "hello"}}`, "hello", tVal, true}, + {"printf function", `{{printf "%#q" zeroArgs}}`, "`zeroArgs`", tVal, true}, + {"printf field", `{{printf "%s" .U.V}}`, "v", tVal, true}, + {"printf method", `{{printf "%s" .Method0}}`, "M0", tVal, true}, + {"printf dot", `{{with .I}}{{printf "%d" .}}{{end}}`, "17", tVal, true}, + {"printf var", `{{with $x := .I}}{{printf "%d" $x}}{{end}}`, "17", tVal, true}, + {"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true}, + + // HTML. + {"html", `{{html ""}}`, + "<script>alert("XSS");</script>", nil, true}, + {"html pipeline", `{{printf "" | html}}`, + "<script>alert("XSS");</script>", nil, true}, + {"html", `{{html .PS}}`, "a string", tVal, true}, + {"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true}, + {"html untyped nil", `{{html .Empty0}}`, "<no value>", tVal, true}, + + // JavaScript. + {"js", `{{js .}}`, `It\'d be nice.`, `It'd be nice.`, true}, + + // URL query. + {"urlquery", `{{"http://www.example.org/"|urlquery}}`, "http%3A%2F%2Fwww.example.org%2F", nil, true}, + + // Booleans + {"not", "{{not true}} {{not false}}", "false true", nil, true}, + {"and", "{{and false 0}} {{and 1 0}} {{and 0 true}} {{and 1 1}}", "false 0 0 1", nil, true}, + {"or", "{{or 0 0}} {{or 1 0}} {{or 0 true}} {{or 1 1}}", "0 1 true 1", nil, true}, + {"or short-circuit", "{{or 0 1 (die)}}", "1", nil, true}, + {"and short-circuit", "{{and 1 0 (die)}}", "0", nil, true}, + {"or short-circuit2", "{{or 0 0 (die)}}", "", nil, false}, + {"and short-circuit2", "{{and 1 1 (die)}}", "", nil, false}, + {"and pipe-true", "{{1 | and 1}}", "1", nil, true}, + {"and pipe-false", "{{0 | and 1}}", "0", nil, true}, + {"or pipe-true", "{{1 | or 0}}", "1", nil, true}, + {"or pipe-false", "{{0 | or 0}}", "0", nil, true}, + {"and undef", "{{and 1 .Unknown}}", "", nil, true}, + {"or undef", "{{or 0 .Unknown}}", "", nil, true}, + {"boolean if", "{{if and true 1 `hi`}}TRUE{{else}}FALSE{{end}}", "TRUEFALSE", tVal, true}, + {"boolean if not", "{{if and true 1 `hi` | not}}TRUE{{else}}FALSE{{end}}", "TRUEFALSE", nil, true}, + {"boolean if pipe", "{{if true | not | and 1}}TRUE{{else}}FALSE{{end}}", "TRUEFALSE", nil, true}, + + // Indexing. + {"slice[0]", "{{index .SI 0}}", "3", tVal, true}, + {"slice[1]", "{{index .SI 1}}", "4", tVal, true}, + {"slice[HUGE]", "{{index .SI 10}}", "", tVal, false}, + {"slice[WRONG]", "{{index .SI `hello`}}", "", tVal, false}, + {"slice[nil]", "{{index .SI nil}}", "", tVal, false}, + {"map[one]", "{{index .MSI `one`}}", "1", tVal, true}, + {"map[two]", "{{index .MSI `two`}}", "2", tVal, true}, + {"map[NO]", "{{index .MSI `XXX`}}", "0", tVal, true}, + {"map[nil]", "{{index .MSI nil}}", "", tVal, false}, + {"map[``]", "{{index .MSI ``}}", "0", tVal, true}, + {"map[WRONG]", "{{index .MSI 10}}", "", tVal, false}, + {"double index", "{{index .SMSI 1 `eleven`}}", "11", tVal, true}, + {"nil[1]", "{{index nil 1}}", "", tVal, false}, + {"map MI64S", "{{index .MI64S 2}}", "i642", tVal, true}, + {"map MI32S", "{{index .MI32S 2}}", "two", tVal, true}, + {"map MUI64S", "{{index .MUI64S 3}}", "ui643", tVal, true}, + {"map MI8S", "{{index .MI8S 3}}", "i83", tVal, true}, + {"map MUI8S", "{{index .MUI8S 2}}", "u82", tVal, true}, + {"index of an interface field", "{{index .Empty3 0}}", "7", tVal, true}, + + // Slicing. + {"slice[:]", "{{slice .SI}}", "[3 4 5]", tVal, true}, + {"slice[1:]", "{{slice .SI 1}}", "[4 5]", tVal, true}, + {"slice[1:2]", "{{slice .SI 1 2}}", "[4]", tVal, true}, + {"slice[-1:]", "{{slice .SI -1}}", "", tVal, false}, + {"slice[1:-2]", "{{slice .SI 1 -2}}", "", tVal, false}, + {"slice[1:2:-1]", "{{slice .SI 1 2 -1}}", "", tVal, false}, + {"slice[2:1]", "{{slice .SI 2 1}}", "", tVal, false}, + {"slice[2:2:1]", "{{slice .SI 2 2 1}}", "", tVal, false}, + {"out of range", "{{slice .SI 4 5}}", "", tVal, false}, + {"out of range", "{{slice .SI 2 2 5}}", "", tVal, false}, + {"len(s) < indexes < cap(s)", "{{slice .SICap 6 10}}", "[0 0 0 0]", tVal, true}, + {"len(s) < indexes < cap(s)", "{{slice .SICap 6 10 10}}", "[0 0 0 0]", tVal, true}, + {"indexes > cap(s)", "{{slice .SICap 10 11}}", "", tVal, false}, + {"indexes > cap(s)", "{{slice .SICap 6 10 11}}", "", tVal, false}, + {"array[:]", "{{slice .AI}}", "[3 4 5]", tVal, true}, + {"array[1:]", "{{slice .AI 1}}", "[4 5]", tVal, true}, + {"array[1:2]", "{{slice .AI 1 2}}", "[4]", tVal, true}, + {"string[:]", "{{slice .S}}", "xyz", tVal, true}, + {"string[0:1]", "{{slice .S 0 1}}", "x", tVal, true}, + {"string[1:]", "{{slice .S 1}}", "yz", tVal, true}, + {"string[1:2]", "{{slice .S 1 2}}", "y", tVal, true}, + {"out of range", "{{slice .S 1 5}}", "", tVal, false}, + {"3-index slice of string", "{{slice .S 1 2 2}}", "", tVal, false}, + {"slice of an interface field", "{{slice .Empty3 0 1}}", "[7]", tVal, true}, + + // Len. + {"slice", "{{len .SI}}", "3", tVal, true}, + {"map", "{{len .MSI }}", "3", tVal, true}, + {"len of int", "{{len 3}}", "", tVal, false}, + {"len of nothing", "{{len .Empty0}}", "", tVal, false}, + {"len of an interface field", "{{len .Empty3}}", "2", tVal, true}, + + // With. + {"with true", "{{with true}}{{.}}{{end}}", "true", tVal, true}, + {"with false", "{{with false}}{{.}}{{else}}FALSE{{end}}", "falseFALSE", tVal, true}, + {"with 1", "{{with 1}}{{.}}{{else}}ZERO{{end}}", "1ZERO", tVal, true}, + {"with 0", "{{with 0}}{{.}}{{else}}ZERO{{end}}", "0ZERO", tVal, true}, + {"with 1.5", "{{with 1.5}}{{.}}{{else}}ZERO{{end}}", "1.5ZERO", tVal, true}, + {"with 0.0", "{{with .FloatZero}}{{.}}{{else}}ZERO{{end}}", "0ZERO", tVal, true}, + {"with 1.5i", "{{with 1.5i}}{{.}}{{else}}ZERO{{end}}", "(0+1.5i)ZERO", tVal, true}, + {"with 0.0i", "{{with .ComplexZero}}{{.}}{{else}}ZERO{{end}}", "(0+0i)ZERO", tVal, true}, + {"with emptystring", "{{with ``}}{{.}}{{else}}EMPTY{{end}}", "EMPTY", tVal, true}, + {"with string", "{{with `notempty`}}{{.}}{{else}}EMPTY{{end}}", "notemptyEMPTY", tVal, true}, + {"with emptyslice", "{{with .SIEmpty}}{{.}}{{else}}EMPTY{{end}}", "[]EMPTY", tVal, true}, + {"with slice", "{{with .SI}}{{.}}{{else}}EMPTY{{end}}", "[3 4 5]EMPTY", tVal, true}, + {"with emptymap", "{{with .MSIEmpty}}{{.}}{{else}}EMPTY{{end}}", "map[]EMPTY", tVal, true}, + {"with map", "{{with .MSIone}}{{.}}{{else}}EMPTY{{end}}", "map[one:1]EMPTY", tVal, true}, + {"with empty interface, struct field", "{{with .Empty4}}{{.V}}{{end}}", "UinEmpty", tVal, true}, + {"with $x int", "{{with $x := .I}}{{$x}}{{end}}", "17", tVal, true}, + {"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true}, + {"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true}, + {"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "TRUE", tVal, true}, + {"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "0true", tVal, true}, + {"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "0falsenotempty", tVal, true}, + + // Range. + {"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true}, + {"range empty no else", "{{range .SIEmpty}}-{{.}}-{{end}}", "", tVal, true}, + {"range []int else", "{{range .SI}}-{{.}}-{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true}, + {"range empty else", "{{range .SIEmpty}}-{{.}}-{{else}}EMPTY{{end}}", "EMPTY", tVal, true}, + {"range []int break else", "{{range .SI}}-{{.}}-{{break}}NOTREACHED{{else}}EMPTY{{end}}", "-3-", tVal, true}, + {"range []int continue else", "{{range .SI}}-{{.}}-{{continue}}NOTREACHED{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true}, + {"range []bool", "{{range .SB}}-{{.}}-{{end}}", "-true--false-", tVal, true}, + {"range []int method", "{{range .SI | .MAdd .I}}-{{.}}-{{end}}", "-20--21--22-", tVal, true}, + {"range map", "{{range .MSI}}-{{.}}-{{end}}", "-1--3--2-", tVal, true}, + {"range empty map no else", "{{range .MSIEmpty}}-{{.}}-{{end}}", "", tVal, true}, + {"range map else", "{{range .MSI}}-{{.}}-{{else}}EMPTY{{end}}", "-1--3--2-", tVal, true}, + {"range empty map else", "{{range .MSIEmpty}}-{{.}}-{{else}}EMPTY{{end}}", "EMPTY", tVal, true}, + {"range empty interface", "{{range .Empty3}}-{{.}}-{{else}}EMPTY{{end}}", "-7--8-", tVal, true}, + {"range empty nil", "{{range .Empty0}}-{{.}}-{{end}}", "", tVal, true}, + {"range $x SI", "{{range $x := .SI}}<{{$x}}>{{end}}", "<3><4><5>", tVal, true}, + {"range $x $y SI", "{{range $x, $y := .SI}}<{{$x}}={{$y}}>{{end}}", "<0=3><1=4><2=5>", tVal, true}, + {"range $x MSIone", "{{range $x := .MSIone}}<{{$x}}>{{end}}", "<1>", tVal, true}, + {"range $x $y MSIone", "{{range $x, $y := .MSIone}}<{{$x}}={{$y}}>{{end}}", "", tVal, true}, + {"range $x PSI", "{{range $x := .PSI}}<{{$x}}>{{end}}", "<21><22><23>", tVal, true}, + {"declare in range", "{{range $x := .PSI}}<{{$foo:=$x}}{{$x}}>{{end}}", "<21><22><23>", tVal, true}, + {"range count", `{{range $i, $x := count 5}}[{{$i}}]{{$x}}{{end}}`, "[0]a[1]b[2]c[3]d[4]e", tVal, true}, + {"range nil count", `{{range $i, $x := count 0}}{{else}}empty{{end}}`, "empty", tVal, true}, + {"range iter.Seq[int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal1(2), true}, + {"i = range iter.Seq[int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true}, + {"range iter.Seq[int] over two var", `{{range $i, $c := .}}{{$c}}{{end}}`, "", fVal1(2), false}, + {"i, c := range iter.Seq2[int,int]", `{{range $i, $c := .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true}, + {"i, c = range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true}, + {"i = range iter.Seq2[int,int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal2(2), true}, + {"i := range iter.Seq2[int,int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal2(2), true}, + {"i,c,x range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{$x := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true}, + {"i,x range iter.Seq[int]", `{{$i := 0}}{{$x := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true}, + {"range iter.Seq[int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal1(0), true}, + {"range iter.Seq2[int,int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal2(0), true}, + {"range int8", rangeTestInt, rangeTestData[int8](), int8(5), true}, + {"range int16", rangeTestInt, rangeTestData[int16](), int16(5), true}, + {"range int32", rangeTestInt, rangeTestData[int32](), int32(5), true}, + {"range int64", rangeTestInt, rangeTestData[int64](), int64(5), true}, + {"range int", rangeTestInt, rangeTestData[int](), int(5), true}, + {"range uint8", rangeTestInt, rangeTestData[uint8](), uint8(5), true}, + {"range uint16", rangeTestInt, rangeTestData[uint16](), uint16(5), true}, + {"range uint32", rangeTestInt, rangeTestData[uint32](), uint32(5), true}, + {"range uint64", rangeTestInt, rangeTestData[uint64](), uint64(5), true}, + {"range uint", rangeTestInt, rangeTestData[uint](), uint(5), true}, + {"range uintptr", rangeTestInt, rangeTestData[uintptr](), uintptr(5), true}, + {"range uintptr(0)", `{{range $v := .}}{{print $v}}{{else}}empty{{end}}`, "empty", uintptr(0), true}, + {"range 5", `{{range $v := 5}}{{printf "%T%d" $v $v}}{{end}}`, rangeTestData[int](), nil, true}, + + // Cute examples. + {"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true}, + {"or as if false", `{{or .SIEmpty "slice is empty"}}`, "slice is empty", tVal, true}, + + // Error handling. + {"error method, error", "{{.MyError true}}", "", tVal, false}, + {"error method, no error", "{{.MyError false}}", "false", tVal, true}, + + // Numbers + {"decimal", "{{print 1234}}", "1234", tVal, true}, + {"decimal _", "{{print 12_34}}", "1234", tVal, true}, + {"binary", "{{print 0b101}}", "5", tVal, true}, + {"binary _", "{{print 0b_1_0_1}}", "5", tVal, true}, + {"BINARY", "{{print 0B101}}", "5", tVal, true}, + {"octal0", "{{print 0377}}", "255", tVal, true}, + {"octal", "{{print 0o377}}", "255", tVal, true}, + {"octal _", "{{print 0o_3_7_7}}", "255", tVal, true}, + {"OCTAL", "{{print 0O377}}", "255", tVal, true}, + {"hex", "{{print 0x123}}", "291", tVal, true}, + {"hex _", "{{print 0x1_23}}", "291", tVal, true}, + {"HEX", "{{print 0X123ABC}}", "1194684", tVal, true}, + {"float", "{{print 123.4}}", "123.4", tVal, true}, + {"float _", "{{print 0_0_1_2_3.4}}", "123.4", tVal, true}, + {"hex float", "{{print +0x1.ep+2}}", "7.5", tVal, true}, + {"hex float _", "{{print +0x_1.e_0p+0_2}}", "7.5", tVal, true}, + {"HEX float", "{{print +0X1.EP+2}}", "7.5", tVal, true}, + {"print multi", "{{print 1_2_3_4 7.5_00_00_00}}", "1234 7.5", tVal, true}, + {"print multi2", "{{print 1234 0x0_1.e_0p+02}}", "1234 7.5", tVal, true}, + + // Fixed bugs. + // Must separate dot and receiver; otherwise args are evaluated with dot set to variable. + {"bug0", "{{range .MSIone}}{{if $.Method1 .}}X{{end}}{{end}}", "X", tVal, true}, + // Do not loop endlessly in indirect for non-empty interfaces. + // The bug appears with *interface only; looped forever. + {"bug1", "{{.Method0}}", "M0", &iVal, true}, + // Was taking address of interface field, so method set was empty. + {"bug2", "{{$.NonEmptyInterface.Method0}}", "M0", tVal, true}, + // Struct values were not legal in with - mere oversight. + {"bug3", "{{with $}}{{.Method0}}{{end}}", "M0", tVal, true}, + // Nil interface values in if. + {"bug4", "{{if .Empty0}}non-nil{{else}}nil{{end}}", "non-nilnil", tVal, true}, + // Stringer. + {"bug5", "{{.Str}}", "foozle", tVal, true}, + {"bug5a", "{{.Err}}", "erroozle", tVal, true}, + // Args need to be indirected and dereferenced sometimes. + {"bug6a", "{{vfunc .V0 .V1}}", "vfunc", tVal, true}, + {"bug6b", "{{vfunc .V0 .V0}}", "vfunc", tVal, true}, + {"bug6c", "{{vfunc .V1 .V0}}", "vfunc", tVal, true}, + {"bug6d", "{{vfunc .V1 .V1}}", "vfunc", tVal, true}, + // Legal parse but illegal execution: non-function should have no arguments. + {"bug7a", "{{3 2}}", "", tVal, false}, + {"bug7b", "{{$x := 1}}{{$x 2}}", "", tVal, false}, + {"bug7c", "{{$x := 1}}{{3 | $x}}", "", tVal, false}, + // Pipelined arg was not being type-checked. + {"bug8a", "{{3|oneArg}}", "", tVal, false}, + {"bug8b", "{{4|dddArg 3}}", "", tVal, false}, + // A bug was introduced that broke map lookups for lower-case names. + {"bug9", "{{.cause}}", "neglect", map[string]string{"cause": "neglect"}, true}, + // Field chain starting with function did not work. + {"bug10", "{{mapOfThree.three}}-{{(mapOfThree).three}}", "3-3", 0, true}, + // Dereferencing nil pointer while evaluating function arguments should not panic. Issue 7333. + {"bug11", "{{valueString .PS}}", "", T{}, false}, + // 0xef gave constant type float64. Issue 8622. + {"bug12xe", "{{printf `%T` 0xef}}", "int", T{}, true}, + {"bug12xE", "{{printf `%T` 0xEE}}", "int", T{}, true}, + {"bug12Xe", "{{printf `%T` 0Xef}}", "int", T{}, true}, + {"bug12XE", "{{printf `%T` 0XEE}}", "int", T{}, true}, + // Chained nodes did not work as arguments. Issue 8473. + {"bug13", "{{print (.Copy).I}}", "17", tVal, true}, + // Didn't protect against nil or literal values in field chains. + {"bug14a", "{{(nil).True}}", "", tVal, false}, + {"bug14b", "{{$x := nil}}{{$x.anything}}", "", tVal, false}, + {"bug14c", `{{$x := (1.0)}}{{$y := ("hello")}}{{$x.anything}}{{$y.true}}`, "", tVal, false}, + // Didn't call validateType on function results. Issue 10800. + {"bug15", "{{valueString returnInt}}", "", tVal, false}, + // Variadic function corner cases. Issue 10946. + {"bug16a", "{{true|printf}}", "", tVal, false}, + {"bug16b", "{{1|printf}}", "", tVal, false}, + {"bug16c", "{{1.1|printf}}", "", tVal, false}, + {"bug16d", "{{'x'|printf}}", "", tVal, false}, + {"bug16e", "{{0i|printf}}", "", tVal, false}, + {"bug16f", "{{true|twoArgs \"xxx\"}}", "", tVal, false}, + {"bug16g", "{{\"aaa\" |twoArgs \"bbb\"}}", "twoArgs=bbbaaa", tVal, true}, + {"bug16h", "{{1|oneArg}}", "", tVal, false}, + {"bug16i", "{{\"aaa\"|oneArg}}", "oneArg=aaa", tVal, true}, + {"bug16j", "{{1+2i|printf \"%v\"}}", "(1+2i)", tVal, true}, + {"bug16k", "{{\"aaa\"|printf }}", "aaa", tVal, true}, + {"bug17a", "{{.NonEmptyInterface.X}}", "x", tVal, true}, + {"bug17b", "-{{.NonEmptyInterface.Method1 1234}}-", "-1234-", tVal, true}, + {"bug17c", "{{len .NonEmptyInterfacePtS}}", "2", tVal, true}, + {"bug17d", "{{index .NonEmptyInterfacePtS 0}}", "a", tVal, true}, + {"bug17e", "{{range .NonEmptyInterfacePtS}}-{{.}}-{{end}}", "-a--b-", tVal, true}, + + // More variadic function corner cases. Some runes would get evaluated + // as constant floats instead of ints. Issue 34483. + {"bug18a", "{{eq . '.'}}", "true", '.', true}, + {"bug18b", "{{eq . 'e'}}", "true", 'e', true}, + {"bug18c", "{{eq . 'P'}}", "true", 'P', true}, + + {"issue56490", "{{$i := 0}}{{$x := 0}}{{range $i = .AI}}{{end}}{{$i}}", "5", tVal, true}, + {"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true}, +} + +func fVal1(i int) iter.Seq[int] { + return func(yield func(int) bool) { + for v := range i { + if !yield(v) { + break + } + } + } +} + +func fVal2(i int) iter.Seq2[int, int] { + return func(yield func(int, int) bool) { + for v := range i { + if !yield(v, v+1) { + break + } + } + } +} + +const rangeTestInt = `{{range $v := .}}{{printf "%T%d" $v $v}}{{end}}` + +func rangeTestData[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr]() string { + I := T(5) + var buf strings.Builder + for i := T(0); i < I; i++ { + fmt.Fprintf(&buf, "%T%d", i, i) + } + return buf.String() +} + +func zeroArgs() string { + return "zeroArgs" +} + +func oneArg(a string) string { + return "oneArg=" + a +} + +func twoArgs(a, b string) string { + return "twoArgs=" + a + b +} + +func dddArg(a int, b ...string) string { + return fmt.Sprintln(a, b) +} + +// count returns a channel that will deliver n sequential 1-letter strings starting at "a" +func count(n int) chan string { + if n == 0 { + return nil + } + c := make(chan string) + go func() { + for i := 0; i < n; i++ { + c <- "abcdefghijklmnop"[i : i+1] + } + close(c) + }() + return c +} + +// vfunc takes a *V and a V +func vfunc(V, *V) string { + return "vfunc" +} + +// valueString takes a string, not a pointer. +func valueString(v string) string { + return "value is ignored" +} + +// returnInt returns an int +func returnInt() int { + return 7 +} + +func add(args ...int) int { + sum := 0 + for _, x := range args { + sum += x + } + return sum +} + +func echo(arg any) any { + return arg +} + +func makemap(arg ...string) map[string]string { + if len(arg)%2 != 0 { + panic("bad makemap") + } + m := make(map[string]string) + for i := 0; i < len(arg); i += 2 { + m[arg[i]] = arg[i+1] + } + return m +} + +func stringer(s fmt.Stringer) string { + return s.String() +} + +func mapOfThree() any { + return map[string]int{"three": 3} +} + +func testExecute(execTests []execTest, template *Template, t *testing.T) { + b := new(strings.Builder) + funcs := FuncMap{ + "add": add, + "count": count, + "dddArg": dddArg, + "die": func() bool { panic("die") }, + "echo": echo, + "makemap": makemap, + "mapOfThree": mapOfThree, + "oneArg": oneArg, + "returnInt": returnInt, + "stringer": stringer, + "twoArgs": twoArgs, + "typeOf": typeOf, + "valueString": valueString, + "vfunc": vfunc, + "zeroArgs": zeroArgs, + } + for _, test := range execTests { + var tmpl *Template + var err error + if template == nil { + tmpl, err = New(test.name).Funcs(funcs).Parse(test.input) + } else { + tmpl, err = template.New(test.name).Funcs(funcs).Parse(test.input) + } + if err != nil { + t.Errorf("%s: parse error: %s", test.name, err) + continue + } + b.Reset() + err = tmpl.Execute(b, test.data) + switch { + case !test.ok && err == nil: + t.Errorf("%s: expected error; got none", test.name) + continue + case test.ok && err != nil: + t.Errorf("%s: unexpected execute error: %s", test.name, err) + continue + case !test.ok && err != nil: + // expected error, got one + if *debug { + fmt.Printf("%s: %s\n\t%s\n", test.name, test.input, err) + } + } + result := b.String() + if result != test.output { + t.Errorf("%s: expected\n\t%q\ngot\n\t%q", test.name, test.output, result) + } + } +} + +func TestExecute(t *testing.T) { + testExecute(execTests, nil, t) +} + +var delimPairs = []string{ + "", "", // default + "{{", "}}", // same as default + "<<", ">>", // distinct + "|", "|", // same + "(日)", "(本)", // peculiar +} + +func TestDelims(t *testing.T) { + const hello = "Hello, world" + var value = struct{ Str string }{hello} + for i := 0; i < len(delimPairs); i += 2 { + text := ".Str" + left := delimPairs[i+0] + trueLeft := left + right := delimPairs[i+1] + trueRight := right + if left == "" { // default case + trueLeft = "{{" + } + if right == "" { // default case + trueRight = "}}" + } + text = trueLeft + text + trueRight + // Now add a comment + text += trueLeft + "/*comment*/" + trueRight + // Now add an action containing a string. + text += trueLeft + `"` + trueLeft + `"` + trueRight + // At this point text looks like `{{.Str}}{{/*comment*/}}{{"{{"}}`. + tmpl, err := New("delims").Delims(left, right).Parse(text) + if err != nil { + t.Fatalf("delim %q text %q parse err %s", left, text, err) + } + var b = new(strings.Builder) + err = tmpl.Execute(b, value) + if err != nil { + t.Fatalf("delim %q exec err %s", left, err) + } + if b.String() != hello+trueLeft { + t.Errorf("expected %q got %q", hello+trueLeft, b.String()) + } + } +} + +// Check that an error from a method flows back to the top. +func TestExecuteError(t *testing.T) { + b := new(bytes.Buffer) + tmpl := New("error") + _, err := tmpl.Parse("{{.MyError true}}") + if err != nil { + t.Fatalf("parse error: %s", err) + } + err = tmpl.Execute(b, tVal) + if err == nil { + t.Errorf("expected error; got none") + } else if !strings.Contains(err.Error(), myError.Error()) { + if *debug { + fmt.Printf("test execute error: %s\n", err) + } + t.Errorf("expected myError; got %s", err) + } +} + +const execErrorText = `line 1 +line 2 +line 3 +{{template "one" .}} +{{define "one"}}{{template "two" .}}{{end}} +{{define "two"}}{{template "three" .}}{{end}} +{{define "three"}}{{index "hi" $}}{{end}}` + +// Check that an error from a nested template contains all the relevant information. +func TestExecError(t *testing.T) { + tmpl, err := New("top").Parse(execErrorText) + if err != nil { + t.Fatal("parse error:", err) + } + var b bytes.Buffer + err = tmpl.Execute(&b, 5) // 5 is out of range indexing "hi" + if err == nil { + t.Fatal("expected error") + } + const want = `template: top:7:20: executing "three" at : error calling index: index out of range: 5` + got := err.Error() + if got != want { + t.Errorf("expected\n%q\ngot\n%q", want, got) + } +} + +type CustomError struct{} + +func (*CustomError) Error() string { return "heyo !" } + +// Check that a custom error can be returned. +func TestExecError_CustomError(t *testing.T) { + failingFunc := func() (string, error) { + return "", &CustomError{} + } + tmpl := Must(New("top").Funcs(FuncMap{ + "err": failingFunc, + }).Parse("{{ err }}")) + + var b bytes.Buffer + err := tmpl.Execute(&b, nil) + + var e *CustomError + if !errors.As(err, &e) { + t.Fatalf("expected custom error; got %s", err) + } +} + +func TestJSEscaping(t *testing.T) { + testCases := []struct { + in, exp string + }{ + {`a`, `a`}, + {`'foo`, `\'foo`}, + {`Go "jump" \`, `Go \"jump\" \\`}, + {`Yukihiro says "今日は世界"`, `Yukihiro says \"今日は世界\"`}, + {"unprintable \uFFFE", `unprintable \uFFFE`}, + {``, `\u003Chtml\u003E`}, + {`no = in attributes`, `no \u003D in attributes`}, + {`' does not become HTML entity`, `\u0026#x27; does not become HTML entity`}, + } + for _, tc := range testCases { + s := JSEscapeString(tc.in) + if s != tc.exp { + t.Errorf("JS escaping [%s] got [%s] want [%s]", tc.in, s, tc.exp) + } + } +} + +// A nice example: walk a binary tree. + +type Tree struct { + Val int + Left, Right *Tree +} + +// Use different delimiters to test Set.Delims. +// Also test the trimming of leading and trailing spaces. +const treeTemplate = ` + (- define "tree" -) + [ + (- .Val -) + (- with .Left -) + (template "tree" . -) + (- end -) + (- with .Right -) + (- template "tree" . -) + (- end -) + ] + (- end -) +` + +func TestTree(t *testing.T) { + var tree = &Tree{ + 1, + &Tree{ + 2, &Tree{ + 3, + &Tree{ + 4, nil, nil, + }, + nil, + }, + &Tree{ + 5, + &Tree{ + 6, nil, nil, + }, + nil, + }, + }, + &Tree{ + 7, + &Tree{ + 8, + &Tree{ + 9, nil, nil, + }, + nil, + }, + &Tree{ + 10, + &Tree{ + 11, nil, nil, + }, + nil, + }, + }, + } + tmpl, err := New("root").Delims("(", ")").Parse(treeTemplate) + if err != nil { + t.Fatal("parse error:", err) + } + var b strings.Builder + const expect = "[1[2[3[4]][5[6]]][7[8[9]][10[11]]]]" + // First by looking up the template. + err = tmpl.Lookup("tree").Execute(&b, tree) + if err != nil { + t.Fatal("exec error:", err) + } + result := b.String() + if result != expect { + t.Errorf("expected %q got %q", expect, result) + } + // Then direct to execution. + b.Reset() + err = tmpl.ExecuteTemplate(&b, "tree", tree) + if err != nil { + t.Fatal("exec error:", err) + } + result = b.String() + if result != expect { + t.Errorf("expected %q got %q", expect, result) + } +} + +func TestExecuteOnNewTemplate(t *testing.T) { + // This is issue 3872. + New("Name").Templates() + // This is issue 11379. + new(Template).Templates() + new(Template).Parse("") + new(Template).New("abc").Parse("") + new(Template).Execute(nil, nil) // returns an error (but does not crash) + new(Template).ExecuteTemplate(nil, "XXX", nil) // returns an error (but does not crash) +} + +const testTemplates = `{{define "one"}}one{{end}}{{define "two"}}two{{end}}` + +func TestMessageForExecuteEmpty(t *testing.T) { + // Test a truly empty template. + tmpl := New("empty") + var b bytes.Buffer + err := tmpl.Execute(&b, 0) + if err == nil { + t.Fatal("expected initial error") + } + got := err.Error() + want := `template: empty: "empty" is an incomplete or empty template` + if got != want { + t.Errorf("expected error %s got %s", want, got) + } + // Add a non-empty template to check that the error is helpful. + tests, err := New("").Parse(testTemplates) + if err != nil { + t.Fatal(err) + } + tmpl.AddParseTree("secondary", tests.Tree) + err = tmpl.Execute(&b, 0) + if err == nil { + t.Fatal("expected second error") + } + got = err.Error() + want = `template: empty: "empty" is an incomplete or empty template` + if got != want { + t.Errorf("expected error %s got %s", want, got) + } + // Make sure we can execute the secondary. + err = tmpl.ExecuteTemplate(&b, "secondary", 0) + if err != nil { + t.Fatal(err) + } +} + +func TestFinalForPrintf(t *testing.T) { + tmpl, err := New("").Parse(`{{"x" | printf}}`) + if err != nil { + t.Fatal(err) + } + var b bytes.Buffer + err = tmpl.Execute(&b, 0) + if err != nil { + t.Fatal(err) + } +} + +//type cmpTest struct { +// expr string +// truth string +// ok bool +//} +// +//var cmpTests = []cmpTest{ +// {"eq true true", "true", true}, +// {"eq true false", "false", true}, +// {"eq 1+2i 1+2i", "true", true}, +// {"eq 1+2i 1+3i", "false", true}, +// {"eq 1.5 1.5", "true", true}, +// {"eq 1.5 2.5", "false", true}, +// {"eq 1 1", "true", true}, +// {"eq 1 2", "false", true}, +// {"eq `xy` `xy`", "true", true}, +// {"eq `xy` `xyz`", "false", true}, +// {"eq .Uthree .Uthree", "true", true}, +// {"eq .Uthree .Ufour", "false", true}, +// {"eq 3 4 5 6 3", "true", true}, +// {"eq 3 4 5 6 7", "false", true}, +// {"ne true true", "false", true}, +// {"ne true false", "true", true}, +// {"ne 1+2i 1+2i", "false", true}, +// {"ne 1+2i 1+3i", "true", true}, +// {"ne 1.5 1.5", "false", true}, +// {"ne 1.5 2.5", "true", true}, +// {"ne 1 1", "false", true}, +// {"ne 1 2", "true", true}, +// {"ne `xy` `xy`", "false", true}, +// {"ne `xy` `xyz`", "true", true}, +// {"ne .Uthree .Uthree", "false", true}, +// {"ne .Uthree .Ufour", "true", true}, +// {"lt 1.5 1.5", "false", true}, +// {"lt 1.5 2.5", "true", true}, +// {"lt 1 1", "false", true}, +// {"lt 1 2", "true", true}, +// {"lt `xy` `xy`", "false", true}, +// {"lt `xy` `xyz`", "true", true}, +// {"lt .Uthree .Uthree", "false", true}, +// {"lt .Uthree .Ufour", "true", true}, +// {"le 1.5 1.5", "true", true}, +// {"le 1.5 2.5", "true", true}, +// {"le 2.5 1.5", "false", true}, +// {"le 1 1", "true", true}, +// {"le 1 2", "true", true}, +// {"le 2 1", "false", true}, +// {"le `xy` `xy`", "true", true}, +// {"le `xy` `xyz`", "true", true}, +// {"le `xyz` `xy`", "false", true}, +// {"le .Uthree .Uthree", "true", true}, +// {"le .Uthree .Ufour", "true", true}, +// {"le .Ufour .Uthree", "false", true}, +// {"gt 1.5 1.5", "false", true}, +// {"gt 1.5 2.5", "false", true}, +// {"gt 1 1", "false", true}, +// {"gt 2 1", "true", true}, +// {"gt 1 2", "false", true}, +// {"gt `xy` `xy`", "false", true}, +// {"gt `xy` `xyz`", "false", true}, +// {"gt .Uthree .Uthree", "false", true}, +// {"gt .Uthree .Ufour", "false", true}, +// {"gt .Ufour .Uthree", "true", true}, +// {"ge 1.5 1.5", "true", true}, +// {"ge 1.5 2.5", "false", true}, +// {"ge 2.5 1.5", "true", true}, +// {"ge 1 1", "true", true}, +// {"ge 1 2", "false", true}, +// {"ge 2 1", "true", true}, +// {"ge `xy` `xy`", "true", true}, +// {"ge `xy` `xyz`", "false", true}, +// {"ge `xyz` `xy`", "true", true}, +// {"ge .Uthree .Uthree", "true", true}, +// {"ge .Uthree .Ufour", "false", true}, +// {"ge .Ufour .Uthree", "true", true}, +// // Mixing signed and unsigned integers. +// {"eq .Uthree .Three", "true", true}, +// {"eq .Three .Uthree", "true", true}, +// {"le .Uthree .Three", "true", true}, +// {"le .Three .Uthree", "true", true}, +// {"ge .Uthree .Three", "true", true}, +// {"ge .Three .Uthree", "true", true}, +// {"lt .Uthree .Three", "false", true}, +// {"lt .Three .Uthree", "false", true}, +// {"gt .Uthree .Three", "false", true}, +// {"gt .Three .Uthree", "false", true}, +// {"eq .Ufour .Three", "false", true}, +// {"lt .Ufour .Three", "false", true}, +// {"gt .Ufour .Three", "true", true}, +// {"eq .NegOne .Uthree", "false", true}, +// {"eq .Uthree .NegOne", "false", true}, +// {"ne .NegOne .Uthree", "true", true}, +// {"ne .Uthree .NegOne", "true", true}, +// {"lt .NegOne .Uthree", "true", true}, +// {"lt .Uthree .NegOne", "false", true}, +// {"le .NegOne .Uthree", "true", true}, +// {"le .Uthree .NegOne", "false", true}, +// {"gt .NegOne .Uthree", "false", true}, +// {"gt .Uthree .NegOne", "true", true}, +// {"ge .NegOne .Uthree", "false", true}, +// {"ge .Uthree .NegOne", "true", true}, +// {"eq (index `x` 0) 'x'", "true", true}, // The example that triggered this rule. +// {"eq (index `x` 0) 'y'", "false", true}, +// {"eq .V1 .V2", "true", true}, +// {"eq .Ptr .Ptr", "true", true}, +// {"eq .Ptr .NilPtr", "false", true}, +// {"eq .NilPtr .NilPtr", "true", true}, +// {"eq .Iface1 .Iface1", "true", true}, +// {"eq .Iface1 .NilIface", "false", true}, +// {"eq .NilIface .NilIface", "true", true}, +// {"eq .NilIface .Iface1", "false", true}, +// {"eq .NilIface 0", "false", true}, +// {"eq 0 .NilIface", "false", true}, +// {"eq .Map .Map", "true", true}, // Uncomparable types but nil is OK. +// {"eq .Map nil", "true", true}, // Uncomparable types but nil is OK. +// {"eq nil .Map", "true", true}, // Uncomparable types but nil is OK. +// {"eq .Map .NonNilMap", "false", true}, // Uncomparable types but nil is OK. +// // Errors +// {"eq `xy` 1", "", false}, // Different types. +// {"eq 2 2.0", "", false}, // Different types. +// {"lt true true", "", false}, // Unordered types. +// {"lt 1+0i 1+0i", "", false}, // Unordered types. +// {"eq .Ptr 1", "", false}, // Incompatible types. +// {"eq .Ptr .NegOne", "", false}, // Incompatible types. +// {"eq .Map .V1", "", false}, // Uncomparable types. +// {"eq .NonNilMap .NonNilMap", "", false}, // Uncomparable types. +//} +// +//func TestComparison(t *testing.T) { +// b := new(strings.Builder) +// var cmpStruct = struct { +// Uthree, Ufour uint +// NegOne, Three int +// Ptr, NilPtr *int +// NonNilMap map[int]int +// Map map[int]int +// V1, V2 V +// Iface1, NilIface fmt.Stringer +// }{ +// Uthree: 3, +// Ufour: 4, +// NegOne: -1, +// Three: 3, +// Ptr: new(int), +// NonNilMap: make(map[int]int), +// Iface1: b, +// } +// for _, test := range cmpTests { +// text := fmt.Sprintf("{{if %s}}true{{else}}false{{end}}", test.expr) +// tmpl, err := New("empty").Parse(text) +// if err != nil { +// t.Fatalf("%q: %s", test.expr, err) +// } +// b.Reset() +// err = tmpl.Execute(b, &cmpStruct) +// if test.ok && err != nil { +// t.Errorf("%s errored incorrectly: %s", test.expr, err) +// continue +// } +// if !test.ok && err == nil { +// t.Errorf("%s did not error", test.expr) +// continue +// } +// if b.String() != test.truth { +// t.Errorf("%s: want %s; got %s", test.expr, test.truth, b.String()) +// } +// } +//} + +func TestMissingMapKey(t *testing.T) { + data := map[string]int{ + "x": 99, + } + tmpl, err := New("t1").Parse("{{.x}} {{.y}}") + if err != nil { + t.Fatal(err) + } + var b strings.Builder + // By default, just get "" + err = tmpl.Execute(&b, data) + if err != nil { + t.Fatal(err) + } + want := "99 " + got := b.String() + if got != want { + t.Errorf("got %q; expected %q", got, want) + } + // Same if we set the option explicitly to the default. + tmpl.Option("missingkey=default") + b.Reset() + err = tmpl.Execute(&b, data) + if err != nil { + t.Fatal("default:", err) + } + want = "99 " + got = b.String() + if got != want { + t.Errorf("got %q; expected %q", got, want) + } + // Next we ask for a zero value + tmpl.Option("missingkey=zero") + b.Reset() + err = tmpl.Execute(&b, data) + if err != nil { + t.Fatal("zero:", err) + } + want = "99 0" + got = b.String() + if got != want { + t.Errorf("got %q; expected %q", got, want) + } + // Now we ask for an error. + tmpl.Option("missingkey=error") + err = tmpl.Execute(&b, data) + if err == nil { + t.Errorf("expected error; got none") + } + // same Option, but now a nil interface: ask for an error + err = tmpl.Execute(&b, nil) + t.Log(err) + if err == nil { + t.Errorf("expected error for nil-interface; got none") + } +} + +// Test that the error message for multiline unterminated string +// refers to the line number of the opening quote. +func TestUnterminatedStringError(t *testing.T) { + _, err := New("X").Parse("hello\n\n{{`unterminated\n\n\n\n}}\n some more\n\n") + if err == nil { + t.Fatal("expected error") + } + str := err.Error() + if !strings.Contains(str, "X:3: unterminated raw quoted string") { + t.Fatalf("unexpected error: %s", str) + } +} + +const alwaysErrorText = "always be failing" + +var alwaysError = errors.New(alwaysErrorText) + +type ErrorWriter int + +func (e ErrorWriter) Write(p []byte) (int, error) { + return 0, alwaysError +} + +func TestExecuteGivesExecError(t *testing.T) { + // First, a non-execution error shouldn't be an ExecError. + tmpl, err := New("X").Parse("hello") + if err != nil { + t.Fatal(err) + } + err = tmpl.Execute(ErrorWriter(0), 0) + if err == nil { + t.Fatal("expected error; got none") + } + if err.Error() != alwaysErrorText { + t.Errorf("expected %q error; got %q", alwaysErrorText, err) + } + // This one should be an ExecError. + tmpl, err = New("X").Parse("hello, {{.X.Y}}") + if err != nil { + t.Fatal(err) + } + err = tmpl.Execute(io.Discard, 0) + if err == nil { + t.Fatal("expected error; got none") + } + eerr, ok := err.(ExecError) + if !ok { + t.Fatalf("did not expect ExecError %s", eerr) + } + expect := "field X in type int" + if !strings.Contains(err.Error(), expect) { + t.Errorf("expected %q; got %q", expect, err) + } +} + +func funcNameTestFunc() int { + return 0 +} + +func TestGoodFuncNames(t *testing.T) { + names := []string{ + "_", + "a", + "a1", + "a1", + "Ӵ", + } + for _, name := range names { + tmpl := New("X").Funcs( + FuncMap{ + name: funcNameTestFunc, + }, + ) + if tmpl == nil { + t.Fatalf("nil result for %q", name) + } + } +} + +func TestBadFuncNames(t *testing.T) { + names := []string{ + "", + "2", + "a-b", + } + for _, name := range names { + testBadFuncName(name, t) + } +} + +func testBadFuncName(name string, t *testing.T) { + t.Helper() + defer func() { + recover() + }() + New("X").Funcs( + FuncMap{ + name: funcNameTestFunc, + }, + ) + // If we get here, the name did not cause a panic, which is how Funcs + // reports an error. + t.Errorf("%q succeeded incorrectly as function name", name) +} + +func TestBlock(t *testing.T) { + const ( + input = `a({{block "inner" .}}bar({{.}})baz{{end}})b` + want = `a(bar(hello)baz)b` + overlay = `{{define "inner"}}foo({{.}})bar{{end}}` + want2 = `a(foo(goodbye)bar)b` + ) + tmpl, err := New("outer").Parse(input) + if err != nil { + t.Fatal(err) + } + tmpl2, err := Must(tmpl.Clone()).Parse(overlay) + if err != nil { + t.Fatal(err) + } + + var buf strings.Builder + if err := tmpl.Execute(&buf, "hello"); err != nil { + t.Fatal(err) + } + if got := buf.String(); got != want { + t.Errorf("got %q, want %q", got, want) + } + + buf.Reset() + if err := tmpl2.Execute(&buf, "goodbye"); err != nil { + t.Fatal(err) + } + if got := buf.String(); got != want2 { + t.Errorf("got %q, want %q", got, want2) + } +} + +func TestEvalFieldErrors(t *testing.T) { + tests := []struct { + name, src string + value any + want string + }{ + { + // Check that calling an invalid field on nil pointer + // prints a field error instead of a distracting nil + // pointer error. https://golang.org/issue/15125 + "MissingFieldOnNil", + "{{.MissingField}}", + (*T)(nil), + "can't evaluate field MissingField in type *template.T", + }, + { + "MissingFieldOnNonNil", + "{{.MissingField}}", + &T{}, + "can't evaluate field MissingField in type *template.T", + }, + { + "ExistingFieldOnNil", + "{{.X}}", + (*T)(nil), + "nil pointer evaluating *template.T.X", + }, + { + "MissingKeyOnNilMap", + "{{.MissingKey}}", + (*map[string]string)(nil), + "nil pointer evaluating *map[string]string.MissingKey", + }, + { + "MissingKeyOnNilMapPtr", + "{{.MissingKey}}", + (*map[string]string)(nil), + "nil pointer evaluating *map[string]string.MissingKey", + }, + { + "MissingKeyOnMapPtrToNil", + "{{.MissingKey}}", + &map[string]string{}, + "", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tmpl := Must(New("tmpl").Parse(tc.src)) + err := tmpl.Execute(io.Discard, tc.value) + got := "" + if err != nil { + got = err.Error() + } + if !strings.HasSuffix(got, tc.want) { + t.Fatalf("got error %q, want %q", got, tc.want) + } + }) + } +} + +func TestMaxExecDepth(t *testing.T) { + if testing.Short() { + t.Skip("skipping in -short mode") + } + tmpl := Must(New("tmpl").Parse(`{{template "tmpl" .}}`)) + err := tmpl.Execute(io.Discard, nil) + got := "" + if err != nil { + got = err.Error() + } + const want = "exceeded maximum template depth" + if !strings.Contains(got, want) { + t.Errorf("got error %q; want %q", got, want) + } +} + +func TestAddrOfIndex(t *testing.T) { + // golang.org/issue/14916. + // Before index worked on reflect.Values, the .String could not be + // found on the (incorrectly unaddressable) V value, + // in contrast to range, which worked fine. + // Also testing that passing a reflect.Value to tmpl.Execute works. + texts := []string{ + `{{range .}}{{.String}}{{end}}`, + `{{with index . 0}}{{.String}}{{end}}`, + } + for _, text := range texts { + tmpl := Must(New("tmpl").Parse(text)) + var buf strings.Builder + err := tmpl.Execute(&buf, reflect.ValueOf([]V{{1}})) + if err != nil { + t.Fatalf("%s: Execute: %v", text, err) + } + if buf.String() != "<1>" { + t.Fatalf("%s: template output = %q, want %q", text, &buf, "<1>") + } + } +} + +func TestInterfaceValues(t *testing.T) { + // golang.org/issue/17714. + // Before index worked on reflect.Values, interface values + // were always implicitly promoted to the underlying value, + // except that nil interfaces were promoted to the zero reflect.Value. + // Eliminating a round trip to interface{} and back to reflect.Value + // eliminated this promotion, breaking these cases. + tests := []struct { + text string + out string + }{ + {`{{index .Nil 1}}`, "ERROR: index of untyped nil"}, + {`{{index .Slice 2}}`, "2"}, + {`{{index .Slice .Two}}`, "2"}, + {`{{call .Nil 1}}`, "ERROR: call of nil"}, + {`{{call .PlusOne 1}}`, "2"}, + {`{{call .PlusOne .One}}`, "2"}, + {`{{and (index .Slice 0) true}}`, "0"}, + {`{{and .Zero true}}`, "0"}, + {`{{and (index .Slice 1) false}}`, "false"}, + {`{{and .One false}}`, "false"}, + {`{{or (index .Slice 0) false}}`, "false"}, + {`{{or .Zero false}}`, "false"}, + {`{{or (index .Slice 1) true}}`, "1"}, + {`{{or .One true}}`, "1"}, + {`{{not (index .Slice 0)}}`, "true"}, + {`{{not .Zero}}`, "true"}, + {`{{not (index .Slice 1)}}`, "false"}, + {`{{not .One}}`, "false"}, + //{`{{eq (index .Slice 0) .Zero}}`, "true"}, + //{`{{eq (index .Slice 1) .One}}`, "true"}, + //{`{{ne (index .Slice 0) .Zero}}`, "false"}, + //{`{{ne (index .Slice 1) .One}}`, "false"}, + //{`{{ge (index .Slice 0) .One}}`, "false"}, + //{`{{ge (index .Slice 1) .Zero}}`, "true"}, + //{`{{gt (index .Slice 0) .One}}`, "false"}, + //{`{{gt (index .Slice 1) .Zero}}`, "true"}, + //{`{{le (index .Slice 0) .One}}`, "true"}, + //{`{{le (index .Slice 1) .Zero}}`, "false"}, + //{`{{lt (index .Slice 0) .One}}`, "true"}, + //{`{{lt (index .Slice 1) .Zero}}`, "false"}, + } + + for _, tt := range tests { + tmpl := Must(New("tmpl").Parse(tt.text)) + var buf strings.Builder + err := tmpl.Execute(&buf, map[string]any{ + "PlusOne": func(n int) int { + return n + 1 + }, + "Slice": []int{0, 1, 2, 3}, + "One": 1, + "Two": 2, + "Nil": nil, + "Zero": 0, + }) + if strings.HasPrefix(tt.out, "ERROR:") { + e := strings.TrimSpace(strings.TrimPrefix(tt.out, "ERROR:")) + if err == nil || !strings.Contains(err.Error(), e) { + t.Errorf("%s: Execute: %v, want error %q", tt.text, err, e) + } + continue + } + if err != nil { + t.Errorf("%s: Execute: %v", tt.text, err) + continue + } + if buf.String() != tt.out { + t.Errorf("%s: template output = %q, want %q", tt.text, &buf, tt.out) + } + } +} + +// Check that panics during calls are recovered and returned as errors. +func TestExecutePanicDuringCall(t *testing.T) { + funcs := map[string]any{ + "doPanic": func() string { + panic("custom panic string") + }, + } + tests := []struct { + name string + input string + data any + wantErr string + }{ + { + "direct func call panics", + "{{doPanic}}", (*T)(nil), + `template: t:1:2: executing "t" at : error calling doPanic: custom panic string`, + }, + { + "indirect func call panics", + "{{call doPanic}}", (*T)(nil), + `template: t:1:7: executing "t" at : error calling doPanic: custom panic string`, + }, + { + "direct method call panics", + "{{.GetU}}", (*T)(nil), + `template: t:1:2: executing "t" at <.GetU>: error calling GetU: runtime error: invalid memory address or nil pointer dereference`, + }, + { + "indirect method call panics", + "{{call .GetU}}", (*T)(nil), + `template: t:1:7: executing "t" at <.GetU>: error calling GetU: runtime error: invalid memory address or nil pointer dereference`, + }, + { + "func field call panics", + "{{call .PanicFunc}}", tVal, + `template: t:1:2: executing "t" at : error calling call: test panic`, + }, + { + "method call on nil interface", + "{{.NonEmptyInterfaceNil.Method0}}", tVal, + `template: t:1:23: executing "t" at <.NonEmptyInterfaceNil.Method0>: nil pointer evaluating template.I.Method0`, + }, + } + for _, tc := range tests { + b := new(bytes.Buffer) + tmpl, err := New("t").Funcs(funcs).Parse(tc.input) + if err != nil { + t.Fatalf("parse error: %s", err) + } + err = tmpl.Execute(b, tc.data) + if err == nil { + t.Errorf("%s: expected error; got none", tc.name) + } else if !strings.Contains(err.Error(), tc.wantErr) { + if *debug { + fmt.Printf("%s: test execute error: %s\n", tc.name, err) + } + t.Errorf("%s: expected error:\n%s\ngot:\n%s", tc.name, tc.wantErr, err) + } + } +} + +func TestFunctionCheckDuringCall(t *testing.T) { + tests := []struct { + name string + input string + data any + wantErr string + }{{ + name: "call nothing", + input: `{{call}}`, + data: tVal, + wantErr: "wrong number of args for call: want at least 1 got 0", + }, + { + name: "call non-function", + input: "{{call .True}}", + data: tVal, + wantErr: "error calling call: non-function .True of type bool", + }, + { + name: "call func with wrong argument", + input: "{{call .BinaryFunc 1}}", + data: tVal, + wantErr: "error calling call: wrong number of args for .BinaryFunc: got 1 want 2", + }, + { + name: "call variadic func with wrong argument", + input: `{{call .VariadicFuncInt}}`, + data: tVal, + wantErr: "error calling call: wrong number of args for .VariadicFuncInt: got 0 want at least 1", + }, + { + name: "call too few return number func", + input: `{{call .TooFewReturnCountFunc}}`, + data: tVal, + wantErr: "error calling call: function .TooFewReturnCountFunc has 0 return values; should be 1 or 2", + }, + { + name: "call too many return number func", + input: `{{call .TooManyReturnCountFunc}}`, + data: tVal, + wantErr: "error calling call: function .TooManyReturnCountFunc has 3 return values; should be 1 or 2", + }, + { + name: "call invalid return type func", + input: `{{call .InvalidReturnTypeFunc}}`, + data: tVal, + wantErr: "error calling call: invalid function signature for .InvalidReturnTypeFunc: second return value should be error; is bool", + }, + { + name: "call pipeline", + input: `{{call (len "test")}}`, + data: nil, + wantErr: "error calling call: non-function len \"test\" of type int", + }, + } + + for _, tc := range tests { + b := new(bytes.Buffer) + tmpl, err := New("t").Parse(tc.input) + if err != nil { + t.Fatalf("parse error: %s", err) + } + err = tmpl.Execute(b, tc.data) + if err == nil { + t.Errorf("%s: expected error; got none", tc.name) + } else if tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr) { + if *debug { + fmt.Printf("%s: test execute error: %s\n", tc.name, err) + } + t.Errorf("%s: expected error:\n%s\ngot:\n%s", tc.name, tc.wantErr, err) + } + } +} + +// Issue 31810. Check that a parenthesized first argument behaves properly. +func TestIssue31810(t *testing.T) { + // A simple value with no arguments is fine. + var b strings.Builder + const text = "{{ (.) }}" + tmpl, err := New("").Parse(text) + if err != nil { + t.Error(err) + } + err = tmpl.Execute(&b, "result") + if err != nil { + t.Error(err) + } + if b.String() != "result" { + t.Errorf("%s got %q, expected %q", text, b.String(), "result") + } + + // Even a plain function fails - need to use call. + f := func() string { return "result" } + b.Reset() + err = tmpl.Execute(&b, f) + if err == nil { + t.Error("expected error with no call, got none") + } + + // Works if the function is explicitly called. + const textCall = "{{ (call .) }}" + tmpl, err = New("").Parse(textCall) + b.Reset() + err = tmpl.Execute(&b, f) + if err != nil { + t.Error(err) + } + if b.String() != "result" { + t.Errorf("%s got %q, expected %q", textCall, b.String(), "result") + } +} + +// Issue 43065, range over send only channel +func TestIssue43065(t *testing.T) { + var b bytes.Buffer + tmp := Must(New("").Parse(`{{range .}}{{end}}`)) + ch := make(chan<- int) + err := tmp.Execute(&b, ch) + if err == nil { + t.Error("expected err got nil") + } else if !strings.Contains(err.Error(), "range over send-only channel") { + t.Errorf("%s", err) + } +} + +// Issue 39807: data race in html/template & text/template +func TestIssue39807(t *testing.T) { + var wg sync.WaitGroup + + tplFoo, err := New("foo").Parse(`{{ template "bar" . }}`) + if err != nil { + t.Error(err) + } + + tplBar, err := New("bar").Parse("bar") + if err != nil { + t.Error(err) + } + + gofuncs := 10 + numTemplates := 10 + + for i := 1; i <= gofuncs; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < numTemplates; j++ { + _, err := tplFoo.AddParseTree(tplBar.Name(), tplBar.Tree) + if err != nil { + t.Error(err) + } + err = tplFoo.Execute(io.Discard, nil) + if err != nil { + t.Error(err) + } + } + }() + } + + wg.Wait() +} + +// Issue 48215: embedded nil pointer causes panic. +// Fixed by adding FieldByIndexErr to the reflect package. +func TestIssue48215(t *testing.T) { + type A struct { + S string + } + type B struct { + *A + } + tmpl, err := New("").Parse(`{{ .S }}`) + if err != nil { + t.Fatal(err) + } + err = tmpl.Execute(io.Discard, B{}) + // We expect an error, not a panic. + if err == nil { + t.Fatal("did not get error for nil embedded struct") + } + if !strings.Contains(err.Error(), "reflect: indirection through nil pointer to embedded struct field A") { + t.Fatal(err) + } +} diff --git a/internal/template/funcs.go b/internal/template/funcs.go new file mode 100644 index 0000000..a3f1f1a --- /dev/null +++ b/internal/template/funcs.go @@ -0,0 +1,677 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package template + +import ( + "errors" + "fmt" + "io" + "net/url" + "reflect" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +// FuncMap is the type of the map defining the mapping from names to functions. +// Each function must have either a single return value, or two return values of +// which the second has type error. In that case, if the second (error) +// return value evaluates to non-nil during execution, execution terminates and +// Execute returns that error. +// +// Errors returned by Execute wrap the underlying error; call [errors.As] to +// unwrap them. +// +// When template execution invokes a function with an argument list, that list +// must be assignable to the function's parameter types. Functions meant to +// apply to arguments of arbitrary type can use parameters of type interface{} or +// of type [reflect.Value]. Similarly, functions meant to return a result of arbitrary +// type can return interface{} or [reflect.Value]. +type FuncMap map[string]any + +// builtins returns the FuncMap. +// It is not a global variable so the linker can dead code eliminate +// more when this isn't called. See golang.org/issue/36021. +// TODO: revert this back to a global map once golang.org/issue/2559 is fixed. +func builtins() FuncMap { + return FuncMap{ + "and": and, + "call": emptyCall, + "html": HTMLEscaper, + "index": index, + "slice": slice, + "js": JSEscaper, + "len": length, + "not": not, + "or": or, + "print": fmt.Sprint, + "printf": fmt.Sprintf, + "println": fmt.Sprintln, + "urlquery": URLQueryEscaper, + + // Comparisons + "eq": eq, // == + "ge": ge, // >= + "gt": gt, // > + "le": le, // <= + "lt": lt, // < + "ne": ne, // != + } +} + +var builtinFuncsOnce struct { + sync.Once + v map[string]reflect.Value +} + +// builtinFuncsOnce lazily computes & caches the builtinFuncs map. +// TODO: revert this back to a global map once golang.org/issue/2559 is fixed. +func builtinFuncs() map[string]reflect.Value { + builtinFuncsOnce.Do(func() { + builtinFuncsOnce.v = createValueFuncs(builtins()) + }) + return builtinFuncsOnce.v +} + +// createValueFuncs turns a FuncMap into a map[string]reflect.Value +func createValueFuncs(funcMap FuncMap) map[string]reflect.Value { + m := make(map[string]reflect.Value) + addValueFuncs(m, funcMap) + return m +} + +// addValueFuncs adds to values the functions in funcs, converting them to reflect.Values. +func addValueFuncs(out map[string]reflect.Value, in FuncMap) { + for name, fn := range in { + if !goodName(name) { + panic(fmt.Errorf("function name %q is not a valid identifier", name)) + } + v := reflect.ValueOf(fn) + if v.Kind() != reflect.Func { + panic("value for " + name + " not a function") + } + if err := goodFunc(name, v.Type()); err != nil { + panic(err) + } + out[name] = v + } +} + +// addFuncs adds to values the functions in funcs. It does no checking of the input - +// call addValueFuncs first. +func addFuncs(out, in FuncMap) { + for name, fn := range in { + out[name] = fn + } +} + +// goodFunc reports whether the function or method has the right result signature. +func goodFunc(name string, typ reflect.Type) error { + // We allow functions with 1 result or 2 results where the second is an error. + switch numOut := typ.NumOut(); { + case numOut == 1: + return nil + case numOut == 2 && typ.Out(1) == errorType: + return nil + case numOut == 2: + return fmt.Errorf("invalid function signature for %s: second return value should be error; is %s", name, typ.Out(1)) + default: + return fmt.Errorf("function %s has %d return values; should be 1 or 2", name, typ.NumOut()) + } +} + +// goodName reports whether the function name is a valid identifier. +func goodName(name string) bool { + if name == "" { + return false + } + for i, r := range name { + switch { + case r == '_': + case i == 0 && !unicode.IsLetter(r): + return false + case !unicode.IsLetter(r) && !unicode.IsDigit(r): + return false + } + } + return true +} + +// findFunction looks for a function in the template, and global map. +func findFunction(name string, tmpl *Template) (v reflect.Value, isBuiltin, ok bool) { + if tmpl != nil && tmpl.common != nil { + tmpl.muFuncs.RLock() + defer tmpl.muFuncs.RUnlock() + if fn := tmpl.execFuncs[name]; fn.IsValid() { + return fn, false, true + } + } + if fn := builtinFuncs()[name]; fn.IsValid() { + return fn, true, true + } + return reflect.Value{}, false, false +} + +// prepareArg checks if value can be used as an argument of type argType, and +// converts an invalid value to appropriate zero if possible. +func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) { + if !value.IsValid() { + if !canBeNil(argType) { + return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType) + } + value = reflect.Zero(argType) + } + if value.Type().AssignableTo(argType) { + return value, nil + } + if intLike(value.Kind()) && intLike(argType.Kind()) && value.Type().ConvertibleTo(argType) { + value = value.Convert(argType) + return value, nil + } + return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType) +} + +func intLike(typ reflect.Kind) bool { + switch typ { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return true + } + return false +} + +// indexArg checks if a reflect.Value can be used as an index, and converts it to int if possible. +func indexArg(index reflect.Value, cap int) (int, error) { + var x int64 + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x = index.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x = int64(index.Uint()) + case reflect.Invalid: + return 0, fmt.Errorf("cannot index slice/array with nil") + default: + return 0, fmt.Errorf("cannot index slice/array with type %s", index.Type()) + } + if x < 0 || int(x) < 0 || int(x) > cap { + return 0, fmt.Errorf("index out of range: %d", x) + } + return int(x), nil +} + +// Indexing. + +// index returns the result of indexing its first argument by the following +// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each +// indexed item must be a map, slice, or array. +func index(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) { + item = indirectInterface(item) + if !item.IsValid() { + return reflect.Value{}, fmt.Errorf("index of untyped nil") + } + for _, index := range indexes { + index = indirectInterface(index) + var isNil bool + if item, isNil = indirect(item); isNil { + return reflect.Value{}, fmt.Errorf("index of nil pointer") + } + switch item.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + x, err := indexArg(index, item.Len()) + if err != nil { + return reflect.Value{}, err + } + item = item.Index(x) + case reflect.Map: + index, err := prepareArg(index, item.Type().Key()) + if err != nil { + return reflect.Value{}, err + } + if x := item.MapIndex(index); x.IsValid() { + item = x + } else { + item = reflect.Zero(item.Type().Elem()) + } + case reflect.Invalid: + // the loop holds invariant: item.IsValid() + panic("unreachable") + default: + return reflect.Value{}, fmt.Errorf("can't index item of type %s", item.Type()) + } + } + return item, nil +} + +// Slicing. + +// slice returns the result of slicing its first argument by the remaining +// arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2], while "slice x" +// is x[:], "slice x 1" is x[1:], and "slice x 1 2 3" is x[1:2:3]. The first +// argument must be a string, slice, or array. +func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) { + item = indirectInterface(item) + if !item.IsValid() { + return reflect.Value{}, fmt.Errorf("slice of untyped nil") + } + if len(indexes) > 3 { + return reflect.Value{}, fmt.Errorf("too many slice indexes: %d", len(indexes)) + } + var cap int + switch item.Kind() { + case reflect.String: + if len(indexes) == 3 { + return reflect.Value{}, fmt.Errorf("cannot 3-index slice a string") + } + cap = item.Len() + case reflect.Array, reflect.Slice: + cap = item.Cap() + default: + return reflect.Value{}, fmt.Errorf("can't slice item of type %s", item.Type()) + } + // set default values for cases item[:], item[i:]. + idx := [3]int{0, item.Len()} + for i, index := range indexes { + x, err := indexArg(index, cap) + if err != nil { + return reflect.Value{}, err + } + idx[i] = x + } + // given item[i:j], make sure i <= j. + if idx[0] > idx[1] { + return reflect.Value{}, fmt.Errorf("invalid slice index: %d > %d", idx[0], idx[1]) + } + if len(indexes) < 3 { + return item.Slice(idx[0], idx[1]), nil + } + // given item[i:j:k], make sure i <= j <= k. + if idx[1] > idx[2] { + return reflect.Value{}, fmt.Errorf("invalid slice index: %d > %d", idx[1], idx[2]) + } + return item.Slice3(idx[0], idx[1], idx[2]), nil +} + +// Length + +// length returns the length of the item, with an error if it has no defined length. +func length(item reflect.Value) (int, error) { + item, isNil := indirect(item) + if isNil { + return 0, fmt.Errorf("len of nil pointer") + } + switch item.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: + return item.Len(), nil + } + return 0, fmt.Errorf("len of type %s", item.Type()) +} + +// Function invocation + +func emptyCall(fn reflect.Value, args ...reflect.Value) reflect.Value { + panic("unreachable") // implemented as a special case in evalCall +} + +// call returns the result of evaluating the first argument as a function. +// The function must return 1 result, or 2 results, the second of which is an error. +func call(name string, fn reflect.Value, args ...reflect.Value) (reflect.Value, error) { + fn = indirectInterface(fn) + if !fn.IsValid() { + return reflect.Value{}, fmt.Errorf("call of nil") + } + typ := fn.Type() + if typ.Kind() != reflect.Func { + return reflect.Value{}, fmt.Errorf("non-function %s of type %s", name, typ) + } + + if err := goodFunc(name, typ); err != nil { + return reflect.Value{}, err + } + numIn := typ.NumIn() + var dddType reflect.Type + if typ.IsVariadic() { + if len(args) < numIn-1 { + return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want at least %d", name, len(args), numIn-1) + } + dddType = typ.In(numIn - 1).Elem() + } else { + if len(args) != numIn { + return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want %d", name, len(args), numIn) + } + } + argv := make([]reflect.Value, len(args)) + for i, arg := range args { + arg = indirectInterface(arg) + // Compute the expected type. Clumsy because of variadics. + argType := dddType + if !typ.IsVariadic() || i < numIn-1 { + argType = typ.In(i) + } + + var err error + if argv[i], err = prepareArg(arg, argType); err != nil { + return reflect.Value{}, fmt.Errorf("arg %d: %w", i, err) + } + } + return safeCall(fn, argv) +} + +// safeCall runs fun.Call(args), and returns the resulting value and error, if +// any. If the call panics, the panic value is returned as an error. +func safeCall(fun reflect.Value, args []reflect.Value) (val reflect.Value, err error) { + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + err = e + } else { + err = fmt.Errorf("%v", r) + } + } + }() + ret := fun.Call(args) + if len(ret) == 2 && !ret[1].IsNil() { + return ret[0], ret[1].Interface().(error) + } + return ret[0], nil +} + +// Boolean logic. + +func truth(arg reflect.Value) bool { + t, _ := isTrue(indirectInterface(arg)) + return t +} + +// and computes the Boolean AND of its arguments, returning +// the first false argument it encounters, or the last argument. +func and(arg0 reflect.Value, args ...reflect.Value) reflect.Value { + panic("unreachable") // implemented as a special case in evalCall +} + +// or computes the Boolean OR of its arguments, returning +// the first true argument it encounters, or the last argument. +func or(arg0 reflect.Value, args ...reflect.Value) reflect.Value { + panic("unreachable") // implemented as a special case in evalCall +} + +// not returns the Boolean negation of its argument. +func not(arg reflect.Value) bool { + return !truth(arg) +} + +// Comparison. + +// TODO: Perhaps allow comparison between signed and unsigned integers. + +var ( + errBadComparisonType = errors.New("invalid type for comparison") + errBadComparison = errors.New("incompatible types for comparison") + errNoComparison = errors.New("missing argument for comparison") +) + +type kind int + +const ( + invalidKind kind = iota + boolKind + complexKind + intKind + floatKind + stringKind + uintKind +) + +func basicKind(v reflect.Value) (kind, error) { + switch v.Kind() { + case reflect.Bool: + return boolKind, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return intKind, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return uintKind, nil + case reflect.Float32, reflect.Float64: + return floatKind, nil + case reflect.Complex64, reflect.Complex128: + return complexKind, nil + case reflect.String: + return stringKind, nil + } + return invalidKind, errBadComparisonType +} + +// isNil returns true if v is the zero reflect.Value, or nil of its type. +func isNil(v reflect.Value) bool { + if !v.IsValid() { + return true + } + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return v.IsNil() + } + return false +} + +// canCompare reports whether v1 and v2 are both the same kind, or one is nil. +// Called only when dealing with nillable types, or there's about to be an error. +func canCompare(v1, v2 reflect.Value) bool { + k1 := v1.Kind() + k2 := v2.Kind() + if k1 == k2 { + return true + } + // We know the type can be compared to nil. + return k1 == reflect.Invalid || k2 == reflect.Invalid +} + +// eq evaluates the comparison a == b || a == c || ... +func eq(_ reflect.Value, _ ...reflect.Value) (bool, error) { + return true, nil +} + +// ne evaluates the comparison a != b. +func ne(_, _ reflect.Value) (bool, error) { + return true, nil +} + +// lt evaluates the comparison a < b. +func lt(_, _ reflect.Value) (bool, error) { + return true, nil +} + +// le evaluates the comparison <= b. +func le(_, _ reflect.Value) (bool, error) { + return true, nil +} + +// gt evaluates the comparison a > b. +func gt(_, _ reflect.Value) (bool, error) { + return true, nil +} + +// ge evaluates the comparison a >= b. +func ge(_, _ reflect.Value) (bool, error) { + return true, nil +} + +// HTML escaping. + +var ( + htmlQuot = []byte(""") // shorter than """ + htmlApos = []byte("'") // shorter than "'" and apos was not in HTML until HTML5 + htmlAmp = []byte("&") + htmlLt = []byte("<") + htmlGt = []byte(">") + htmlNull = []byte("\uFFFD") +) + +// HTMLEscape writes to w the escaped HTML equivalent of the plain text data b. +func HTMLEscape(w io.Writer, b []byte) { + last := 0 + for i, c := range b { + var html []byte + switch c { + case '\000': + html = htmlNull + case '"': + html = htmlQuot + case '\'': + html = htmlApos + case '&': + html = htmlAmp + case '<': + html = htmlLt + case '>': + html = htmlGt + default: + continue + } + w.Write(b[last:i]) + w.Write(html) + last = i + 1 + } + w.Write(b[last:]) +} + +// HTMLEscapeString returns the escaped HTML equivalent of the plain text data s. +func HTMLEscapeString(s string) string { + // Avoid allocation if we can. + if !strings.ContainsAny(s, "'\"&<>\000") { + return s + } + var b strings.Builder + HTMLEscape(&b, []byte(s)) + return b.String() +} + +// HTMLEscaper returns the escaped HTML equivalent of the textual +// representation of its arguments. +func HTMLEscaper(args ...any) string { + return HTMLEscapeString(evalArgs(args)) +} + +// JavaScript escaping. + +var ( + jsLowUni = []byte(`\u00`) + hex = []byte("0123456789ABCDEF") + + jsBackslash = []byte(`\\`) + jsApos = []byte(`\'`) + jsQuot = []byte(`\"`) + jsLt = []byte(`\u003C`) + jsGt = []byte(`\u003E`) + jsAmp = []byte(`\u0026`) + jsEq = []byte(`\u003D`) +) + +// JSEscape writes to w the escaped JavaScript equivalent of the plain text data b. +func JSEscape(w io.Writer, b []byte) { + last := 0 + for i := 0; i < len(b); i++ { + c := b[i] + + if !jsIsSpecial(rune(c)) { + // fast path: nothing to do + continue + } + w.Write(b[last:i]) + + if c < utf8.RuneSelf { + // Quotes, slashes and angle brackets get quoted. + // Control characters get written as \u00XX. + switch c { + case '\\': + w.Write(jsBackslash) + case '\'': + w.Write(jsApos) + case '"': + w.Write(jsQuot) + case '<': + w.Write(jsLt) + case '>': + w.Write(jsGt) + case '&': + w.Write(jsAmp) + case '=': + w.Write(jsEq) + default: + w.Write(jsLowUni) + t, b := c>>4, c&0x0f + w.Write(hex[t : t+1]) + w.Write(hex[b : b+1]) + } + } else { + // Unicode rune. + r, size := utf8.DecodeRune(b[i:]) + if unicode.IsPrint(r) { + w.Write(b[i : i+size]) + } else { + fmt.Fprintf(w, "\\u%04X", r) + } + i += size - 1 + } + last = i + 1 + } + w.Write(b[last:]) +} + +// JSEscapeString returns the escaped JavaScript equivalent of the plain text data s. +func JSEscapeString(s string) string { + // Avoid allocation if we can. + if strings.IndexFunc(s, jsIsSpecial) < 0 { + return s + } + var b strings.Builder + JSEscape(&b, []byte(s)) + return b.String() +} + +func jsIsSpecial(r rune) bool { + switch r { + case '\\', '\'', '"', '<', '>', '&', '=': + return true + } + return r < ' ' || utf8.RuneSelf <= r +} + +// JSEscaper returns the escaped JavaScript equivalent of the textual +// representation of its arguments. +func JSEscaper(args ...any) string { + return JSEscapeString(evalArgs(args)) +} + +// URLQueryEscaper returns the escaped value of the textual representation of +// its arguments in a form suitable for embedding in a URL query. +func URLQueryEscaper(args ...any) string { + return url.QueryEscape(evalArgs(args)) +} + +// evalArgs formats the list of arguments into a string. It is therefore equivalent to +// +// fmt.Sprint(args...) +// +// except that each argument is indirected (if a pointer), as required, +// using the same rules as the default string evaluation during template +// execution. +func evalArgs(args []any) string { + ok := false + var s string + // Fast path for simple common case. + if len(args) == 1 { + s, ok = args[0].(string) + } + if !ok { + for i, arg := range args { + a, ok := printableValue(reflect.ValueOf(arg)) + if ok { + args[i] = a + } // else let fmt do its thing + } + s = fmt.Sprint(args...) + } + return s +} diff --git a/internal/template/helper.go b/internal/template/helper.go new file mode 100644 index 0000000..81b5553 --- /dev/null +++ b/internal/template/helper.go @@ -0,0 +1,178 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Helper functions to make constructing templates easier. + +package template + +import ( + "fmt" + "io/fs" + "os" + "path" + "path/filepath" +) + +// Functions and methods to parse templates. + +// Must is a helper that wraps a call to a function returning ([*Template], error) +// and panics if the error is non-nil. It is intended for use in variable +// initializations such as +// +// var t = template.Must(template.New("name").Parse("text")) +func Must(t *Template, err error) *Template { + if err != nil { + panic(err) + } + return t +} + +// ParseFiles creates a new [Template] and parses the template definitions from +// the named files. The returned template's name will have the base name and +// parsed contents of the first file. There must be at least one file. +// If an error occurs, parsing stops and the returned *Template is nil. +// +// When parsing multiple files with the same name in different directories, +// the last one mentioned will be the one that results. +// For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template +// named "foo", while "a/foo" is unavailable. +func ParseFiles(filenames ...string) (*Template, error) { + return parseFiles(nil, readFileOS, filenames...) +} + +// ParseFiles parses the named files and associates the resulting templates with +// t. If an error occurs, parsing stops and the returned template is nil; +// otherwise it is t. There must be at least one file. +// Since the templates created by ParseFiles are named by the base +// (see [filepath.Base]) names of the argument files, t should usually have the +// name of one of the (base) names of the files. If it does not, depending on +// t's contents before calling ParseFiles, t.Execute may fail. In that +// case use t.ExecuteTemplate to execute a valid template. +// +// When parsing multiple files with the same name in different directories, +// the last one mentioned will be the one that results. +func (t *Template) ParseFiles(filenames ...string) (*Template, error) { + t.init() + return parseFiles(t, readFileOS, filenames...) +} + +// parseFiles is the helper for the method and function. If the argument +// template is nil, it is created from the first file. +func parseFiles(t *Template, readFile func(string) (string, []byte, error), filenames ...string) (*Template, error) { + if len(filenames) == 0 { + // Not really a problem, but be consistent. + return nil, fmt.Errorf("template: no files named in call to ParseFiles") + } + for _, filename := range filenames { + name, b, err := readFile(filename) + if err != nil { + return nil, err + } + s := string(b) + // First template becomes return value if not already defined, + // and we use that one for subsequent New calls to associate + // all the templates together. Also, if this file has the same name + // as t, this file becomes the contents of t, so + // t, err := New(name).Funcs(xxx).ParseFiles(name) + // works. Otherwise we create a new template associated with t. + var tmpl *Template + if t == nil { + t = New(name) + } + if name == t.Name() { + tmpl = t + } else { + tmpl = t.New(name) + } + _, err = tmpl.Parse(s) + if err != nil { + return nil, err + } + } + return t, nil +} + +// ParseGlob creates a new [Template] and parses the template definitions from +// the files identified by the pattern. The files are matched according to the +// semantics of [filepath.Match], and the pattern must match at least one file. +// The returned template will have the [filepath.Base] name and (parsed) +// contents of the first file matched by the pattern. ParseGlob is equivalent to +// calling [ParseFiles] with the list of files matched by the pattern. +// +// When parsing multiple files with the same name in different directories, +// the last one mentioned will be the one that results. +func ParseGlob(pattern string) (*Template, error) { + return parseGlob(nil, pattern) +} + +// ParseGlob parses the template definitions in the files identified by the +// pattern and associates the resulting templates with t. The files are matched +// according to the semantics of [filepath.Match], and the pattern must match at +// least one file. ParseGlob is equivalent to calling [Template.ParseFiles] with +// the list of files matched by the pattern. +// +// When parsing multiple files with the same name in different directories, +// the last one mentioned will be the one that results. +func (t *Template) ParseGlob(pattern string) (*Template, error) { + t.init() + return parseGlob(t, pattern) +} + +// parseGlob is the implementation of the function and method ParseGlob. +func parseGlob(t *Template, pattern string) (*Template, error) { + filenames, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + if len(filenames) == 0 { + return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern) + } + return parseFiles(t, readFileOS, filenames...) +} + +// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys +// instead of the host operating system's file system. +// It accepts a list of glob patterns (see [path.Match]). +// (Note that most file names serve as glob patterns matching only themselves.) +func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) { + return parseFS(nil, fsys, patterns) +} + +// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys +// instead of the host operating system's file system. +// It accepts a list of glob patterns (see [path.Match]). +// (Note that most file names serve as glob patterns matching only themselves.) +func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) { + t.init() + return parseFS(t, fsys, patterns) +} + +func parseFS(t *Template, fsys fs.FS, patterns []string) (*Template, error) { + var filenames []string + for _, pattern := range patterns { + list, err := fs.Glob(fsys, pattern) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern) + } + filenames = append(filenames, list...) + } + return parseFiles(t, readFileFS(fsys), filenames...) +} + +func readFileOS(file string) (name string, b []byte, err error) { + name = filepath.Base(file) + b, err = os.ReadFile(file) + return +} + +func readFileFS(fsys fs.FS) func(string) (string, []byte, error) { + return func(file string) (name string, b []byte, err error) { + name = path.Base(file) + b, err = fs.ReadFile(fsys, file) + return + } +} diff --git a/internal/template/internal/fmtsort/sort.go b/internal/template/internal/fmtsort/sort.go new file mode 100644 index 0000000..f51cdc7 --- /dev/null +++ b/internal/template/internal/fmtsort/sort.go @@ -0,0 +1,154 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package fmtsort provides a general stable ordering mechanism +// for maps, on behalf of the fmt and text/template packages. +// It is not guaranteed to be efficient and works only for types +// that are valid map keys. +package fmtsort + +import ( + "cmp" + "reflect" + "slices" +) + +// Note: Throughout this package we avoid calling reflect.Value.Interface as +// it is not always legal to do so and it's easier to avoid the issue than to face it. + +// SortedMap is a slice of KeyValue pairs that simplifies sorting +// and iterating over map entries. +// +// Each KeyValue pair contains a map key and its corresponding value. +type SortedMap []KeyValue + +// KeyValue holds a single key and value pair found in a map. +type KeyValue struct { + Key, Value reflect.Value +} + +// Sort accepts a map and returns a SortedMap that has the same keys and +// values but in a stable sorted order according to the keys, modulo issues +// raised by unorderable key values such as NaNs. +// +// The ordering rules are more general than with Go's < operator: +// +// - when applicable, nil compares low +// - ints, floats, and strings order by < +// - NaN compares less than non-NaN floats +// - bool compares false before true +// - complex compares real, then imag +// - pointers compare by machine address +// - channel values compare by machine address +// - structs compare each field in turn +// - arrays compare each element in turn. +// Otherwise identical arrays compare by length. +// - interface values compare first by reflect.Type describing the concrete type +// and then by concrete value as described in the previous rules. +func Sort(mapValue reflect.Value) SortedMap { + if mapValue.Type().Kind() != reflect.Map { + return nil + } + // Note: this code is arranged to not panic even in the presence + // of a concurrent map update. The runtime is responsible for + // yelling loudly if that happens. See issue 33275. + n := mapValue.Len() + sorted := make(SortedMap, 0, n) + iter := mapValue.MapRange() + for iter.Next() { + sorted = append(sorted, KeyValue{iter.Key(), iter.Value()}) + } + slices.SortStableFunc(sorted, func(a, b KeyValue) int { + return compare(a.Key, b.Key) + }) + return sorted +} + +// compare compares two values of the same type. It returns -1, 0, 1 +// according to whether a > b (1), a == b (0), or a < b (-1). +// If the types differ, it returns -1. +// See the comment on Sort for the comparison rules. +func compare(aVal, bVal reflect.Value) int { + aType, bType := aVal.Type(), bVal.Type() + if aType != bType { + return -1 // No good answer possible, but don't return 0: they're not equal. + } + switch aVal.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return cmp.Compare(aVal.Int(), bVal.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return cmp.Compare(aVal.Uint(), bVal.Uint()) + case reflect.String: + return cmp.Compare(aVal.String(), bVal.String()) + case reflect.Float32, reflect.Float64: + return cmp.Compare(aVal.Float(), bVal.Float()) + case reflect.Complex64, reflect.Complex128: + a, b := aVal.Complex(), bVal.Complex() + if c := cmp.Compare(real(a), real(b)); c != 0 { + return c + } + return cmp.Compare(imag(a), imag(b)) + case reflect.Bool: + a, b := aVal.Bool(), bVal.Bool() + switch { + case a == b: + return 0 + case a: + return 1 + default: + return -1 + } + case reflect.Pointer, reflect.UnsafePointer: + return cmp.Compare(aVal.Pointer(), bVal.Pointer()) + case reflect.Chan: + if c, ok := nilCompare(aVal, bVal); ok { + return c + } + return cmp.Compare(aVal.Pointer(), bVal.Pointer()) + case reflect.Struct: + for i := 0; i < aVal.NumField(); i++ { + if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 { + return c + } + } + return 0 + case reflect.Array: + for i := 0; i < aVal.Len(); i++ { + if c := compare(aVal.Index(i), bVal.Index(i)); c != 0 { + return c + } + } + return 0 + case reflect.Interface: + if c, ok := nilCompare(aVal, bVal); ok { + return c + } + c := compare(reflect.ValueOf(aVal.Elem().Type()), reflect.ValueOf(bVal.Elem().Type())) + if c != 0 { + return c + } + return compare(aVal.Elem(), bVal.Elem()) + default: + // Certain types cannot appear as keys (maps, funcs, slices), but be explicit. + panic("bad type in compare: " + aType.String()) + } +} + +// nilCompare checks whether either value is nil. If not, the boolean is false. +// If either value is nil, the boolean is true and the integer is the comparison +// value. The comparison is defined to be 0 if both are nil, otherwise the one +// nil value compares low. Both arguments must represent a chan, func, +// interface, map, pointer, or slice. +func nilCompare(aVal, bVal reflect.Value) (int, bool) { + if aVal.IsNil() { + if bVal.IsNil() { + return 0, true + } + return -1, true + } + if bVal.IsNil() { + return 1, true + } + return 0, false +} diff --git a/internal/template/option.go b/internal/template/option.go new file mode 100644 index 0000000..ea2fd80 --- /dev/null +++ b/internal/template/option.go @@ -0,0 +1,72 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains the code to handle template options. + +package template + +import "strings" + +// missingKeyAction defines how to respond to indexing a map with a key that is not present. +type missingKeyAction int + +const ( + mapInvalid missingKeyAction = iota // Return an invalid reflect.Value. + mapZeroValue // Return the zero value for the map element. + mapError // Error out +) + +type option struct { + missingKey missingKeyAction +} + +// Option sets options for the template. Options are described by +// strings, either a simple string or "key=value". There can be at +// most one equals sign in an option string. If the option string +// is unrecognized or otherwise invalid, Option panics. +// +// Known options: +// +// missingkey: Control the behavior during execution if a map is +// indexed with a key that is not present in the map. +// +// "missingkey=default" or "missingkey=invalid" +// The default behavior: Do nothing and continue execution. +// If printed, the result of the index operation is the string +// "". +// "missingkey=zero" +// The operation returns the zero value for the map type's element. +// "missingkey=error" +// Execution stops immediately with an error. +func (t *Template) Option(opt ...string) *Template { + t.init() + for _, s := range opt { + t.setOption(s) + } + return t +} + +func (t *Template) setOption(opt string) { + if opt == "" { + panic("empty option string") + } + // key=value + if key, value, ok := strings.Cut(opt, "="); ok { + switch key { + case "missingkey": + switch value { + case "invalid", "default": + t.option.missingKey = mapInvalid + return + case "zero": + t.option.missingKey = mapZeroValue + return + case "error": + t.option.missingKey = mapError + return + } + } + } + panic("unrecognized option: " + opt) +} diff --git a/internal/template/template.go b/internal/template/template.go new file mode 100644 index 0000000..78067af --- /dev/null +++ b/internal/template/template.go @@ -0,0 +1,235 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package template + +import ( + "maps" + "reflect" + "sync" + "text/template/parse" +) + +// common holds the information shared by related templates. +type common struct { + tmpl map[string]*Template // Map from name to defined templates. + muTmpl sync.RWMutex // protects tmpl + option option + // We use two maps, one for parsing and one for execution. + // This separation makes the API cleaner since it doesn't + // expose reflection to the client. + muFuncs sync.RWMutex // protects parseFuncs and execFuncs + parseFuncs FuncMap + execFuncs map[string]reflect.Value +} + +// Template is the representation of a parsed template. The *parse.Tree +// field is exported only for use by [html/template] and should be treated +// as unexported by all other clients. +type Template struct { + name string + *parse.Tree + *common + leftDelim string + rightDelim string +} + +// New allocates a new, undefined template with the given name. +func New(name string) *Template { + t := &Template{ + name: name, + } + t.init() + return t +} + +// Name returns the name of the template. +func (t *Template) Name() string { + return t.name +} + +// New allocates a new, undefined template associated with the given one and with the same +// delimiters. The association, which is transitive, allows one template to +// invoke another with a {{template}} action. +// +// Because associated templates share underlying data, template construction +// cannot be done safely in parallel. Once the templates are constructed, they +// can be executed in parallel. +func (t *Template) New(name string) *Template { + t.init() + nt := &Template{ + name: name, + common: t.common, + leftDelim: t.leftDelim, + rightDelim: t.rightDelim, + } + return nt +} + +// init guarantees that t has a valid common structure. +func (t *Template) init() { + if t.common == nil { + c := new(common) + c.tmpl = make(map[string]*Template) + c.parseFuncs = make(FuncMap) + c.execFuncs = make(map[string]reflect.Value) + t.common = c + } +} + +// Clone returns a duplicate of the template, including all associated +// templates. The actual representation is not copied, but the name space of +// associated templates is, so further calls to [Template.Parse] in the copy will add +// templates to the copy but not to the original. Clone can be used to prepare +// common templates and use them with variant definitions for other templates +// by adding the variants after the clone is made. +func (t *Template) Clone() (*Template, error) { + nt := t.copy(nil) + nt.init() + if t.common == nil { + return nt, nil + } + t.muTmpl.RLock() + defer t.muTmpl.RUnlock() + for k, v := range t.tmpl { + if k == t.name { + nt.tmpl[t.name] = nt + continue + } + // The associated templates share nt's common structure. + tmpl := v.copy(nt.common) + nt.tmpl[k] = tmpl + } + t.muFuncs.RLock() + defer t.muFuncs.RUnlock() + maps.Copy(nt.parseFuncs, t.parseFuncs) + maps.Copy(nt.execFuncs, t.execFuncs) + return nt, nil +} + +// copy returns a shallow copy of t, with common set to the argument. +func (t *Template) copy(c *common) *Template { + return &Template{ + name: t.name, + Tree: t.Tree, + common: c, + leftDelim: t.leftDelim, + rightDelim: t.rightDelim, + } +} + +// AddParseTree associates the argument parse tree with the template t, giving +// it the specified name. If the template has not been defined, this tree becomes +// its definition. If it has been defined and already has that name, the existing +// definition is replaced; otherwise a new template is created, defined, and returned. +func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) { + t.init() + t.muTmpl.Lock() + defer t.muTmpl.Unlock() + nt := t + if name != t.name { + nt = t.New(name) + } + // Even if nt == t, we need to install it in the common.tmpl map. + if t.associate(nt, tree) || nt.Tree == nil { + nt.Tree = tree + } + return nt, nil +} + +// Templates returns a slice of defined templates associated with t. +func (t *Template) Templates() []*Template { + if t.common == nil { + return nil + } + // Return a slice so we don't expose the map. + t.muTmpl.RLock() + defer t.muTmpl.RUnlock() + m := make([]*Template, 0, len(t.tmpl)) + for _, v := range t.tmpl { + m = append(m, v) + } + return m +} + +// Delims sets the action delimiters to the specified strings, to be used in +// subsequent calls to [Template.Parse], [Template.ParseFiles], or [Template.ParseGlob]. Nested template +// definitions will inherit the settings. An empty delimiter stands for the +// corresponding default: {{ or }}. +// The return value is the template, so calls can be chained. +func (t *Template) Delims(left, right string) *Template { + t.init() + t.leftDelim = left + t.rightDelim = right + return t +} + +// Funcs adds the elements of the argument map to the template's function map. +// It must be called before the template is parsed. +// It panics if a value in the map is not a function with appropriate return +// type or if the name cannot be used syntactically as a function in a template. +// It is legal to overwrite elements of the map. The return value is the template, +// so calls can be chained. +func (t *Template) Funcs(funcMap FuncMap) *Template { + t.init() + t.muFuncs.Lock() + defer t.muFuncs.Unlock() + addValueFuncs(t.execFuncs, funcMap) + addFuncs(t.parseFuncs, funcMap) + return t +} + +// Lookup returns the template with the given name that is associated with t. +// It returns nil if there is no such template or the template has no definition. +func (t *Template) Lookup(name string) *Template { + if t.common == nil { + return nil + } + t.muTmpl.RLock() + defer t.muTmpl.RUnlock() + return t.tmpl[name] +} + +// Parse parses text as a template body for t. +// Named template definitions ({{define ...}} or {{block ...}} statements) in text +// define additional templates associated with t and are removed from the +// definition of t itself. +// +// Templates can be redefined in successive calls to Parse. +// A template definition with a body containing only white space and comments +// is considered empty and will not replace an existing template's body. +// This allows using Parse to add new named template definitions without +// overwriting the main template body. +func (t *Template) Parse(text string) (*Template, error) { + t.init() + t.muFuncs.RLock() + trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins()) + t.muFuncs.RUnlock() + if err != nil { + return nil, err + } + // Add the newly parsed trees, including the one for t, into our common structure. + for name, tree := range trees { + if _, err := t.AddParseTree(name, tree); err != nil { + return nil, err + } + } + return t, nil +} + +// associate installs the new template into the group of templates associated +// with t. The two are already known to share the common structure. +// The boolean return value reports whether to store this tree as t.Tree. +func (t *Template) associate(new *Template, tree *parse.Tree) bool { + if new.common != t.common { + panic("internal error: associate not common") + } + if old := t.tmpl[new.name]; old != nil && parse.IsEmptyTree(tree.Root) && old.Tree != nil { + // If a template by that name exists, + // don't replace it with an empty template. + return false + } + t.tmpl[new.name] = new + return true +} diff --git a/pkg/valuesvalidation/values_validation.go b/internal/valuesvalidation/values_validation.go similarity index 98% rename from pkg/valuesvalidation/values_validation.go rename to internal/valuesvalidation/values_validation.go index cc1d351..9051d29 100644 --- a/pkg/valuesvalidation/values_validation.go +++ b/internal/valuesvalidation/values_validation.go @@ -10,7 +10,7 @@ import ( "helm.sh/helm/v3/pkg/chartutil" "sigs.k8s.io/yaml" - "github.com/deckhouse/d8-lint/pkg/logger" + "github.com/deckhouse/d8-lint/internal/logger" ) type ValuesValidator struct { diff --git a/pkg/valuesvalidation/values_validation_test.go b/internal/valuesvalidation/values_validation_test.go similarity index 100% rename from pkg/valuesvalidation/values_validation_test.go rename to internal/valuesvalidation/values_validation_test.go diff --git a/pkg/config/loader.go b/pkg/config/loader.go index 92795b3..5bad5f4 100644 --- a/pkg/config/loader.go +++ b/pkg/config/loader.go @@ -11,8 +11,8 @@ import ( "github.com/mitchellh/mapstructure" "github.com/spf13/viper" - "github.com/deckhouse/d8-lint/pkg/fsutils" - "github.com/deckhouse/d8-lint/pkg/logger" + "github.com/deckhouse/d8-lint/internal/fsutils" + "github.com/deckhouse/d8-lint/internal/logger" ) type LoaderOptions struct { diff --git a/pkg/linters/copyright/copyright.go b/pkg/linters/copyright/copyright.go index d81976d..b822316 100644 --- a/pkg/linters/copyright/copyright.go +++ b/pkg/linters/copyright/copyright.go @@ -4,10 +4,10 @@ import ( "slices" "strings" + "github.com/deckhouse/d8-lint/internal/fsutils" + "github.com/deckhouse/d8-lint/internal/module" "github.com/deckhouse/d8-lint/pkg/config" "github.com/deckhouse/d8-lint/pkg/errors" - "github.com/deckhouse/d8-lint/pkg/fsutils" - "github.com/deckhouse/d8-lint/pkg/module" ) // Copyright linter diff --git a/pkg/linters/no-cyrillic/no-cyrillic.go b/pkg/linters/no-cyrillic/no-cyrillic.go index 7910d77..a242086 100644 --- a/pkg/linters/no-cyrillic/no-cyrillic.go +++ b/pkg/linters/no-cyrillic/no-cyrillic.go @@ -6,10 +6,10 @@ import ( "slices" "strings" + "github.com/deckhouse/d8-lint/internal/fsutils" + "github.com/deckhouse/d8-lint/internal/module" "github.com/deckhouse/d8-lint/pkg/config" "github.com/deckhouse/d8-lint/pkg/errors" - "github.com/deckhouse/d8-lint/pkg/fsutils" - "github.com/deckhouse/d8-lint/pkg/module" ) // NoCyrillic linter diff --git a/pkg/linters/openapi/library.go b/pkg/linters/openapi/library.go index 3d93b5f..fd0822d 100644 --- a/pkg/linters/openapi/library.go +++ b/pkg/linters/openapi/library.go @@ -7,12 +7,12 @@ import ( "reflect" "strings" + "github.com/hashicorp/go-multierror" + + "github.com/deckhouse/d8-lint/internal/fsutils" + "github.com/deckhouse/d8-lint/internal/logger" "github.com/deckhouse/d8-lint/pkg/config" - "github.com/deckhouse/d8-lint/pkg/fsutils" "github.com/deckhouse/d8-lint/pkg/linters/openapi/validators" - "github.com/deckhouse/d8-lint/pkg/logger" - - "github.com/hashicorp/go-multierror" "gopkg.in/yaml.v3" ) diff --git a/pkg/linters/openapi/openapi.go b/pkg/linters/openapi/openapi.go index bf176a7..465016a 100644 --- a/pkg/linters/openapi/openapi.go +++ b/pkg/linters/openapi/openapi.go @@ -1,9 +1,9 @@ package openapi import ( + "github.com/deckhouse/d8-lint/internal/module" "github.com/deckhouse/d8-lint/pkg/config" "github.com/deckhouse/d8-lint/pkg/errors" - "github.com/deckhouse/d8-lint/pkg/module" ) // OpenAPI linter diff --git a/pkg/linters/openapi/validators/ha_and_https.go b/pkg/linters/openapi/validators/ha_and_https.go index 634cb07..a41399a 100644 --- a/pkg/linters/openapi/validators/ha_and_https.go +++ b/pkg/linters/openapi/validators/ha_and_https.go @@ -4,8 +4,8 @@ import ( "fmt" "reflect" + "github.com/deckhouse/d8-lint/internal/logger" "github.com/deckhouse/d8-lint/pkg/config" - "github.com/deckhouse/d8-lint/pkg/logger" ) type HAValidator struct { diff --git a/pkg/linters/openapi/validators/keys_name_check.go b/pkg/linters/openapi/validators/keys_name_check.go index 0c34e25..f2bb464 100644 --- a/pkg/linters/openapi/validators/keys_name_check.go +++ b/pkg/linters/openapi/validators/keys_name_check.go @@ -4,8 +4,8 @@ import ( "fmt" "reflect" + "github.com/deckhouse/d8-lint/internal/logger" "github.com/deckhouse/d8-lint/pkg/config" - "github.com/deckhouse/d8-lint/pkg/logger" ) type KeyNameValidator struct { diff --git a/pkg/linters/probes/probes.go b/pkg/linters/probes/probes.go index d81a431..9619510 100644 --- a/pkg/linters/probes/probes.go +++ b/pkg/linters/probes/probes.go @@ -8,11 +8,11 @@ import ( "github.com/sourcegraph/conc/pool" v1 "k8s.io/api/core/v1" + "github.com/deckhouse/d8-lint/internal/k8s" + "github.com/deckhouse/d8-lint/internal/module" + "github.com/deckhouse/d8-lint/internal/storage" "github.com/deckhouse/d8-lint/pkg/config" "github.com/deckhouse/d8-lint/pkg/errors" - "github.com/deckhouse/d8-lint/pkg/k8s" - "github.com/deckhouse/d8-lint/pkg/module" - "github.com/deckhouse/d8-lint/pkg/storage" ) // Probes linter