Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/template/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package template
import (
"fmt"
"io/fs"
"log/slog"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 9 additions & 4 deletions pkg/template/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
26 changes: 26 additions & 0 deletions pkg/template/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down