From 31e29e4d42f6a3bdfbc48192cc622055fb8bd6e9 Mon Sep 17 00:00:00 2001 From: no-hup Date: Tue, 4 Aug 2026 21:12:16 +0530 Subject: [PATCH] fix: accept []string and []int in matrix refs (#2544) --- executor_test.go | 1 + testdata/for/cmds/Taskfile.yml | 11 +++++ ...estForCmds-loop-matrix-ref-computed.golden | 6 +++ variables.go | 25 ++++++++-- variables_test.go | 49 +++++++++++++++++++ 5 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref-computed.golden diff --git a/executor_test.go b/executor_test.go index 51fb1fce9d..e0ce4e2786 100644 --- a/executor_test.go +++ b/executor_test.go @@ -929,6 +929,7 @@ func TestForCmds(t *testing.T) { {name: "loop-explicit"}, {name: "loop-matrix"}, {name: "loop-matrix-ref"}, + {name: "loop-matrix-ref-computed"}, { name: "loop-matrix-ref-error", wantErr: true, diff --git a/testdata/for/cmds/Taskfile.yml b/testdata/for/cmds/Taskfile.yml index e9a95234be..589cc57797 100644 --- a/testdata/for/cmds/Taskfile.yml +++ b/testdata/for/cmds/Taskfile.yml @@ -4,6 +4,7 @@ vars: OS_VAR: ["windows", "linux", "darwin"] ARCH_VAR: ["amd64", "arm64"] NOT_A_LIST: "not a list" + OS_CSV: "windows,linux,darwin" tasks: # Loop over a list of values @@ -30,6 +31,16 @@ tasks: ref: .ARCH_VAR cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" + loop-matrix-ref-computed: + cmds: + - for: + matrix: + OS: + ref: 'splitList "," .OS_CSV' + ARCH: + ref: .ARCH_VAR + cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" + loop-matrix-ref-error: cmds: - for: diff --git a/testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref-computed.golden b/testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref-computed.golden new file mode 100644 index 0000000000..fcfed9c89b --- /dev/null +++ b/testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref-computed.golden @@ -0,0 +1,6 @@ +windows/amd64 +windows/arm64 +linux/amd64 +linux/arm64 +darwin/amd64 +darwin/arm64 diff --git a/variables.go b/variables.go index 900f87c0d9..47405555e6 100644 --- a/variables.go +++ b/variables.go @@ -356,6 +356,22 @@ func asAnySlice[T any](slice []T) []any { return ret } +// resolvedAsAnySlice converts a value resolved from a reference into a []any. +// A reference does not always resolve to a []any: lists declared in a Taskfile +// do, but template functions such as `keys` and `splitList` return a []string. +// The accepted types mirror the list types itemsFromFor already supports. +func resolvedAsAnySlice(v any) ([]any, bool) { + switch value := v.(type) { + case []any: + return value, true + case []string: + return asAnySlice(value), true + case []int: + return asAnySlice(value), true + } + return nil, false +} + func itemsFromFor( f *ast.For, dir string, @@ -477,12 +493,11 @@ func resolveMatrixRefs(matrix *ast.Matrix, cache *templater.Cache) (*ast.Matrix, if cache.Err() != nil { return nil, cache.Err() } - switch value := v.(type) { - case []any: - row.Value = value - default: + value, ok := resolvedAsAnySlice(v) + if !ok { return nil, fmt.Errorf("matrix reference %q must resolve to a list", row.Ref) } + row.Value = value } } return resolved, nil @@ -500,7 +515,7 @@ func resolveEnumRefs(requires *ast.Requires, cache *templater.Cache) error { if cache.Err() != nil { return cache.Err() } - arr, ok := resolved.([]any) + arr, ok := resolvedAsAnySlice(resolved) if !ok { return fmt.Errorf("enum reference %q must resolve to a list", v.Enum.Ref) } diff --git a/variables_test.go b/variables_test.go index 60a923da07..72e06d4a13 100644 --- a/variables_test.go +++ b/variables_test.go @@ -43,3 +43,52 @@ func TestResolveMatrixRefsDoesNotMutateInput(t *testing.T) { require.Nil(t, orig.Value, "input matrix was mutated: Ref rows must be resolved into a copy") require.Equal(t, ".ARCH_VAR", orig.Ref, "input matrix Ref was altered") } + +// TestResolveMatrixRefsListTypes is a regression test for #2544. A `ref:` is +// evaluated as a template expression, so it does not always resolve to a +// []any: a list declared in a Taskfile does, but template functions such as +// `keys` and `splitList` return a []string. Resolving a ref used to type +// assert []any, so those references failed with "must resolve to a list" even +// though the value was a list. The accepted types mirror the ones +// itemsFromFor supports for `for: var:`. +func TestResolveMatrixRefsListTypes(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value any + want []any + wantErr bool + }{ + {name: "any slice", value: []any{"amd64", "arm64"}, want: []any{"amd64", "arm64"}}, + {name: "string slice", value: []string{"amd64", "arm64"}, want: []any{"amd64", "arm64"}}, + {name: "int slice", value: []int{1, 2}, want: []any{1, 2}}, + {name: "string", value: "not a list", wantErr: true}, + {name: "map", value: map[string]any{"key": "value"}, wantErr: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + matrix := ast.NewMatrix( + &ast.MatrixElement{Key: "ARCH", Value: &ast.MatrixRow{Ref: ".ARCH_VAR"}}, + ) + + vars := ast.NewVars() + vars.Set("ARCH_VAR", ast.Var{Value: test.value}) + cache := &templater.Cache{Vars: vars} + + resolved, err := resolveMatrixRefs(matrix, cache) + if test.wantErr { + require.ErrorContains(t, err, `matrix reference ".ARCH_VAR" must resolve to a list`) + return + } + require.NoError(t, err) + + row, ok := resolved.Get("ARCH") + require.True(t, ok, "ARCH row missing from resolved matrix") + require.Equal(t, test.want, row.Value) + }) + } +}