diff --git a/pkg/template/analysis.go b/pkg/template/analysis.go index 342e484..5473cde 100644 --- a/pkg/template/analysis.go +++ b/pkg/template/analysis.go @@ -3,6 +3,7 @@ package template import ( "fmt" "io/fs" + "log/slog" "os" "path/filepath" "strings" @@ -91,6 +92,9 @@ func analyseExpr( } tmpl, err := texttemplate.New("").Delims(delims.Left, delims.Right).Funcs(funcMap).Parse(src) if err != nil || tmpl == nil || tmpl.Tree == nil || tmpl.Tree.Root == nil { + if err != nil { + slog.Debug("analyseExpr: failed to parse expression; conditional analysis skipped", "err", err) + } return } walkNode(tmpl.Tree.Root, outerGate, funcMap, conds, always) diff --git a/pkg/template/context.go b/pkg/template/context.go index ade9cdc..d9f0dec 100644 --- a/pkg/template/context.go +++ b/pkg/template/context.go @@ -272,18 +272,23 @@ func topoSort(keys []string, deps map[string][]string) ([]string, error) { // extractRefs parses a delimited template expression and returns all // top-level .Key references found in it. +// It uses texttemplate.New (not parse.New) so that Go's builtin functions +// such as eq, ne, and, or, not are registered and the parser accepts them. func extractRefs(expr string, funcMap texttemplate.FuncMap, delims specs.Delimiters) []string { if !strings.Contains(expr, delims.Left) { return nil } - funcs := map[string]any(funcMap) - tree, err := parse.New("t").Parse(expr, delims.Left, delims.Right, map[string]*parse.Tree{}, funcs) - if err != nil || tree == nil || tree.Root == nil { + tmpl, err := texttemplate.New(""). + Delims(delims.Left, delims.Right). + Funcs(funcMap). + Parse(expr) + if err != nil || tmpl == nil || tmpl.Tree == nil || tmpl.Tree.Root == nil { + slog.Debug("extractRefs: failed to parse expression; dependency detection skipped", "err", err) return nil // parse errors surface during actual rendering } seen := make(map[string]bool) var refs []string - walkForRefs(tree.Root, seen, &refs) + walkForRefs(tmpl.Tree.Root, seen, &refs) return refs } diff --git a/pkg/template/context_test.go b/pkg/template/context_test.go index 5c06cb5..01b1781 100644 --- a/pkg/template/context_test.go +++ b/pkg/template/context_test.go @@ -246,6 +246,32 @@ computed: } } +func TestApplyComputed_ChainWithCustomDelimitersAndBuiltins(t *testing.T) { + // Regression: extractRefs used parse.New (raw parser) which does not include + // Go's builtin template functions (eq, ne, and, or, not, …). Expressions using + // those builtins silently produced nil deps, causing non-deterministic topo sort + // and a "map has no entry" error at execution time. + ctx := map[string]any{"PhpVersion": "8.5"} + delims := specs.Delimiters{Left: "[[", Right: "]]"} + defs := map[string]string{ + "Php85DockerTag": "0.5.3", + "Php84DockerTag": "1.5.3", + "Php83DockerTag": "1.8.3", + "PhpDockerTag": `[[ eq .PhpVersion "8.4" | ternary .Php84DockerTag (eq .PhpVersion "8.3" | ternary .Php83DockerTag .Php85DockerTag) ]]`, + } + + result, err := pkgtemplate.ApplyComputed(ctx, defs, pkgtemplate.FuncMap(pkgtemplate.Config{}), delims) + if err != nil { + t.Fatalf("ApplyComputed: %v", err) + } + if result["PhpDockerTag"] != "0.5.3" { + t.Errorf("PhpDockerTag = %q, want %q", result["PhpDockerTag"], "0.5.3") + } + if result["Php84DockerTag"] != "1.5.3" { + t.Errorf("Php84DockerTag = %q, want %q", result["Php84DockerTag"], "1.5.3") + } +} + func TestApplyComputed_NoDefs(t *testing.T) { ctx := map[string]any{"Name": "test"} result, err := pkgtemplate.ApplyComputed(ctx, nil, pkgtemplate.FuncMap(pkgtemplate.Config{}), specs.DefaultDelimiters)