From 3396992ca886a12de8eb70276755b4c3b5915c13 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 9 Sep 2024 16:05:47 +0000 Subject: [PATCH] feat: use external package for ordered maps --- args/args.go | 2 +- args/args_test.go | 82 +++++++++------- go.mod | 1 + go.sum | 2 + internal/compiler/compiler.go | 2 +- internal/compiler/env.go | 2 +- internal/deepcopy/deepcopy.go | 17 ++++ internal/omap/orderedmap.go | 164 ------------------------------- internal/omap/orderedmap_test.go | 121 ----------------------- internal/output/output_test.go | 12 +-- internal/summary/summary.go | 4 +- internal/summary/summary_test.go | 2 +- requires.go | 2 +- setup.go | 2 +- task.go | 2 +- task_test.go | 2 +- taskfile/ast/for.go | 5 +- taskfile/ast/include.go | 81 ++++++++++----- taskfile/ast/matrix.go | 95 ++++++++++++++++++ taskfile/ast/taskfile.go | 24 +++-- taskfile/ast/taskfile_test.go | 60 ++++++----- taskfile/ast/tasks.go | 151 ++++++++++++++++++++-------- taskfile/ast/var.go | 120 ++++++++++++++++------ taskfile/dotenv.go | 4 +- variables.go | 13 ++- 25 files changed, 500 insertions(+), 472 deletions(-) delete mode 100644 internal/omap/orderedmap.go delete mode 100644 internal/omap/orderedmap_test.go create mode 100644 taskfile/ast/matrix.go diff --git a/args/args.go b/args/args.go index 4499b28ef0..1896ae3190 100644 --- a/args/args.go +++ b/args/args.go @@ -9,7 +9,7 @@ import ( // Parse parses command line argument: tasks and global variables func Parse(args ...string) ([]*ast.Call, *ast.Vars) { calls := []*ast.Call{} - globals := &ast.Vars{} + globals := ast.NewVars() for _, arg := range args { if !strings.Contains(arg, "=") { diff --git a/args/args_test.go b/args/args_test.go index dd69c3b323..8f1d44331d 100644 --- a/args/args_test.go +++ b/args/args_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/go-task/task/v3/args" - "github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/taskfile/ast" ) @@ -32,30 +31,40 @@ func TestArgs(t *testing.T) { {Task: "task-b"}, {Task: "task-c"}, }, - ExpectedGlobals: &ast.Vars{ - OrderedMap: omap.FromMapWithOrder( - map[string]ast.Var{ - "FOO": {Value: "bar"}, - "BAR": {Value: "baz"}, - "BAZ": {Value: "foo"}, + ExpectedGlobals: ast.NewVars( + &ast.VarElement{ + Key: "FOO", + Value: ast.Var{ + Value: "bar", }, - []string{"FOO", "BAR", "BAZ"}, - ), - }, + }, + &ast.VarElement{ + Key: "BAR", + Value: ast.Var{ + Value: "baz", + }, + }, + &ast.VarElement{ + Key: "BAZ", + Value: ast.Var{ + Value: "foo", + }, + }, + ), }, { Args: []string{"task-a", "CONTENT=with some spaces"}, ExpectedCalls: []*ast.Call{ {Task: "task-a"}, }, - ExpectedGlobals: &ast.Vars{ - OrderedMap: omap.FromMapWithOrder( - map[string]ast.Var{ - "CONTENT": {Value: "with some spaces"}, + ExpectedGlobals: ast.NewVars( + &ast.VarElement{ + Key: "CONTENT", + Value: ast.Var{ + Value: "with some spaces", }, - []string{"CONTENT"}, - ), - }, + }, + ), }, { Args: []string{"FOO=bar", "task-a", "task-b"}, @@ -63,14 +72,14 @@ func TestArgs(t *testing.T) { {Task: "task-a"}, {Task: "task-b"}, }, - ExpectedGlobals: &ast.Vars{ - OrderedMap: omap.FromMapWithOrder( - map[string]ast.Var{ - "FOO": {Value: "bar"}, + ExpectedGlobals: ast.NewVars( + &ast.VarElement{ + Key: "FOO", + Value: ast.Var{ + Value: "bar", }, - []string{"FOO"}, - ), - }, + }, + ), }, { Args: nil, @@ -83,15 +92,20 @@ func TestArgs(t *testing.T) { { Args: []string{"FOO=bar", "BAR=baz"}, ExpectedCalls: []*ast.Call{}, - ExpectedGlobals: &ast.Vars{ - OrderedMap: omap.FromMapWithOrder( - map[string]ast.Var{ - "FOO": {Value: "bar"}, - "BAR": {Value: "baz"}, + ExpectedGlobals: ast.NewVars( + &ast.VarElement{ + Key: "FOO", + Value: ast.Var{ + Value: "bar", }, - []string{"FOO", "BAR"}, - ), - }, + }, + &ast.VarElement{ + Key: "BAR", + Value: ast.Var{ + Value: "baz", + }, + }, + ), }, } @@ -100,8 +114,8 @@ func TestArgs(t *testing.T) { calls, globals := args.Parse(test.Args...) assert.Equal(t, test.ExpectedCalls, calls) if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 { - assert.Equal(t, test.ExpectedGlobals.Keys(), globals.Keys()) - assert.Equal(t, test.ExpectedGlobals.Values(), globals.Values()) + assert.Equal(t, test.ExpectedGlobals, globals) + assert.Equal(t, test.ExpectedGlobals, globals) } }) } diff --git a/go.mod b/go.mod index 1c323236c6..3a4ef15117 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/alecthomas/chroma/v2 v2.14.0 github.com/davecgh/go-spew v1.1.1 github.com/dominikbraun/graph v0.23.0 + github.com/elliotchance/orderedmap/v2 v2.4.0 github.com/fatih/color v1.17.0 github.com/go-task/slim-sprig/v3 v3.0.0 github.com/go-task/template v0.1.0 diff --git a/go.sum b/go.sum index 232baa2794..fcdf9c8d7e 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= +github.com/elliotchance/orderedmap/v2 v2.4.0 h1:6tUmMwD9F998FNpwFxA5E6NQvSpk2PVw7RKsVq3+2Cw= +github.com/elliotchance/orderedmap/v2 v2.4.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 8627909a7a..ce2bed7576 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -171,7 +171,7 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) { return result, nil } -// ResetCache clear the dymanic variables cache +// ResetCache clear the dynamic variables cache func (c *Compiler) ResetCache() { c.muDynamicCache.Lock() defer c.muDynamicCache.Unlock() diff --git a/internal/compiler/env.go b/internal/compiler/env.go index 61552d0ba5..393af2b916 100644 --- a/internal/compiler/env.go +++ b/internal/compiler/env.go @@ -10,7 +10,7 @@ import ( // GetEnviron the all return all environment variables encapsulated on a // ast.Vars func GetEnviron() *ast.Vars { - m := &ast.Vars{} + m := ast.NewVars() for _, e := range os.Environ() { keyVal := strings.SplitN(e, "=", 2) key, val := keyVal[0], keyVal[1] diff --git a/internal/deepcopy/deepcopy.go b/internal/deepcopy/deepcopy.go index 8305755b12..c42dd14161 100644 --- a/internal/deepcopy/deepcopy.go +++ b/internal/deepcopy/deepcopy.go @@ -2,6 +2,8 @@ package deepcopy import ( "reflect" + + "github.com/elliotchance/orderedmap/v2" ) type Copier[T any] interface { @@ -38,6 +40,21 @@ func Map[K comparable, V any](orig map[K]V) map[K]V { return c } +func OrderedMap[K comparable, V any](orig *orderedmap.OrderedMap[K, V]) *orderedmap.OrderedMap[K, V] { + if orig.Len() == 0 { + return orderedmap.NewOrderedMap[K, V]() + } + c := orderedmap.NewOrderedMap[K, V]() + for pair := orig.Front(); pair != nil; pair = pair.Next() { + if copyable, ok := any(pair.Value).(Copier[V]); ok { + c.Set(pair.Key, copyable.DeepCopy()) + } else { + c.Set(pair.Key, pair.Value) + } + } + return c +} + // TraverseStringsFunc runs the given function on every string in the given // value by traversing it recursively. If the given value is a string, the // function will run on a copy of the string and return it. If the value is a diff --git a/internal/omap/orderedmap.go b/internal/omap/orderedmap.go deleted file mode 100644 index cf7fc22c7f..0000000000 --- a/internal/omap/orderedmap.go +++ /dev/null @@ -1,164 +0,0 @@ -package omap - -import ( - "cmp" - "fmt" - "slices" - - "gopkg.in/yaml.v3" - - "github.com/go-task/task/v3/internal/deepcopy" - "github.com/go-task/task/v3/internal/exp" -) - -// An OrderedMap is a wrapper around a regular map that maintains an ordered -// list of the map's keys. This allows you to run deterministic and ordered -// operations on the map such as printing/serializing/iterating. -type OrderedMap[K cmp.Ordered, V any] struct { - s []K - m map[K]V -} - -// New will create a new OrderedMap of the given type and return it. -func New[K cmp.Ordered, V any]() OrderedMap[K, V] { - return OrderedMap[K, V]{ - s: make([]K, 0), - m: make(map[K]V), - } -} - -// FromMap will create a new OrderedMap from the given map. Since Golang maps -// are unordered, the order of the created OrderedMap will be random. -func FromMap[K cmp.Ordered, V any](m map[K]V) OrderedMap[K, V] { - om := New[K, V]() - om.m = m - om.s = exp.Keys(m) - return om -} - -func FromMapWithOrder[K cmp.Ordered, V any](m map[K]V, order []K) OrderedMap[K, V] { - om := New[K, V]() - if len(m) != len(order) { - panic("length of map and order must be equal") - } - om.m = m - om.s = order - for key := range om.m { - if !slices.Contains(om.s, key) { - panic("order keys must match map keys") - } - } - return om -} - -// Len will return the number of items in the map. -func (om *OrderedMap[K, V]) Len() int { - return len(om.s) -} - -// Set will set the value for a given key. -func (om *OrderedMap[K, V]) Set(key K, value V) { - if om.m == nil { - om.m = make(map[K]V) - } - if _, ok := om.m[key]; !ok { - om.s = append(om.s, key) - } - om.m[key] = value -} - -// Get will return the value for a given key. -// If the key does not exist, it will return the zero value of the value type. -func (om *OrderedMap[K, V]) Get(key K) V { - value, ok := om.m[key] - if !ok { - var zero V - return zero - } - return value -} - -// Exists will return whether or not the given key exists. -func (om *OrderedMap[K, V]) Exists(key K) bool { - _, ok := om.m[key] - return ok -} - -// Sort will sort the map. -func (om *OrderedMap[K, V]) Sort() { - slices.Sort(om.s) -} - -// SortFunc will sort the map using the given function. -func (om *OrderedMap[K, V]) SortFunc(less func(i, j K) int) { - slices.SortFunc(om.s, less) -} - -// Keys will return a slice of the map's keys in order. -func (om *OrderedMap[K, V]) Keys() []K { - return om.s -} - -// Values will return a slice of the map's values in order. -func (om *OrderedMap[K, V]) Values() []V { - var values []V - for _, key := range om.s { - values = append(values, om.m[key]) - } - return values -} - -// Range will iterate over the map and call the given function for each key/value. -func (om *OrderedMap[K, V]) Range(fn func(key K, value V) error) error { - for _, key := range om.s { - if err := fn(key, om.m[key]); err != nil { - return err - } - } - return nil -} - -// Merge merges the given Vars into the caller one -func (om *OrderedMap[K, V]) Merge(other OrderedMap[K, V]) { - // nolint: errcheck - other.Range(func(key K, value V) error { - om.Set(key, value) - return nil - }) -} - -func (om *OrderedMap[K, V]) DeepCopy() OrderedMap[K, V] { - return OrderedMap[K, V]{ - s: deepcopy.Slice(om.s), - m: deepcopy.Map(om.m), - } -} - -func (om *OrderedMap[K, V]) UnmarshalYAML(node *yaml.Node) error { - switch node.Kind { - // Even numbers contain the keys - // Odd numbers contain the values - case yaml.MappingNode: - for i := 0; i < len(node.Content); i += 2 { - // Decode the key - keyNode := node.Content[i] - var k K - if err := keyNode.Decode(&k); err != nil { - return err - } - - // Decode the value - valueNode := node.Content[i+1] - var v V - if err := valueNode.Decode(&v); err != nil { - return err - } - - // Set the key and value - om.Set(k, v) - } - return nil - } - - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variables", node.Line, node.ShortTag()) -} diff --git a/internal/omap/orderedmap_test.go b/internal/omap/orderedmap_test.go deleted file mode 100644 index c1adc9b311..0000000000 --- a/internal/omap/orderedmap_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package omap - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" -) - -func TestFromMap(t *testing.T) { - m := map[int]string{3: "three", 1: "one", 2: "two"} - om := FromMap(m) - assert.Len(t, om.m, 3) - assert.Len(t, om.s, 3) - assert.ElementsMatch(t, []int{1, 2, 3}, om.s) - for key, value := range m { - assert.Equal(t, om.Get(key), value) - } -} - -func TestSetGetExists(t *testing.T) { - om := New[int, string]() - assert.False(t, om.Exists(1)) - assert.Equal(t, "", om.Get(1)) - om.Set(1, "one") - assert.True(t, om.Exists(1)) - assert.Equal(t, "one", om.Get(1)) -} - -func TestSort(t *testing.T) { - om := New[int, string]() - om.Set(3, "three") - om.Set(1, "one") - om.Set(2, "two") - om.Sort() - assert.Equal(t, []int{1, 2, 3}, om.s) -} - -func TestSortFunc(t *testing.T) { - om := New[int, string]() - om.Set(3, "three") - om.Set(1, "one") - om.Set(2, "two") - om.SortFunc(func(a, b int) int { - return b - a - }) - assert.Equal(t, []int{3, 2, 1}, om.s) -} - -func TestKeysValues(t *testing.T) { - om := New[int, string]() - om.Set(3, "three") - om.Set(1, "one") - om.Set(2, "two") - assert.Equal(t, []int{3, 1, 2}, om.Keys()) - assert.Equal(t, []string{"three", "one", "two"}, om.Values()) -} - -func Range(t *testing.T) { - om := New[int, string]() - om.Set(3, "three") - om.Set(1, "one") - om.Set(2, "two") - - expectedKeys := []int{3, 1, 2} - expectedValues := []string{"three", "one", "two"} - - keys := make([]int, 0, len(expectedKeys)) - values := make([]string, 0, len(expectedValues)) - - err := om.Range(func(key int, value string) error { - keys = append(keys, key) - values = append(values, value) - return nil - }) - - assert.NoError(t, err) - assert.ElementsMatch(t, expectedKeys, keys) - assert.ElementsMatch(t, expectedValues, values) -} - -func TestOrderedMapMerge(t *testing.T) { - om1 := New[string, int]() - om1.Set("a", 1) - om1.Set("b", 2) - - om2 := New[string, int]() - om2.Set("b", 3) - om2.Set("c", 4) - - om1.Merge(om2) - - expectedKeys := []string{"a", "b", "c"} - expectedValues := []int{1, 3, 4} - - assert.Equal(t, len(expectedKeys), len(om1.s)) - assert.Equal(t, len(expectedKeys), len(om1.m)) - - for i, key := range expectedKeys { - assert.True(t, om1.Exists(key)) - assert.Equal(t, expectedValues[i], om1.Get(key)) - } -} - -func TestUnmarshalYAML(t *testing.T) { - yamlString := ` -3: three -1: one -2: two -` - var om OrderedMap[int, string] - err := yaml.Unmarshal([]byte(yamlString), &om) - require.NoError(t, err) - - expectedKeys := []int{3, 1, 2} - expectedValues := []string{"three", "one", "two"} - - assert.Equal(t, expectedKeys, om.Keys()) - assert.Equal(t, expectedValues, om.Values()) -} diff --git a/internal/output/output_test.go b/internal/output/output_test.go index 9a0c223353..328951e3f5 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/require" "github.com/go-task/task/v3/internal/logger" - "github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" @@ -49,11 +48,12 @@ func TestGroup(t *testing.T) { func TestGroupWithBeginEnd(t *testing.T) { tmpl := templater.Cache{ - Vars: &ast.Vars{ - OrderedMap: omap.FromMap(map[string]ast.Var{ - "VAR1": {Value: "example-value"}, - }), - }, + Vars: ast.NewVars( + &ast.VarElement{ + Key: "VAR1", + Value: ast.Var{Value: "example-value"}, + }, + ), } var o output.Output = output.Group{ diff --git a/internal/summary/summary.go b/internal/summary/summary.go index a1bcdabec7..7e63cee78d 100644 --- a/internal/summary/summary.go +++ b/internal/summary/summary.go @@ -10,7 +10,9 @@ import ( func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []*ast.Call) { for i, call := range c { PrintSpaceBetweenSummaries(l, i) - PrintTask(l, t.Tasks.Get(call.Task)) + if task, ok := t.Tasks.Get(call.Task); ok { + PrintTask(l, task) + } } } diff --git a/internal/summary/summary_test.go b/internal/summary/summary_test.go index a478b1a2fb..b4d69e69c3 100644 --- a/internal/summary/summary_test.go +++ b/internal/summary/summary_test.go @@ -156,7 +156,7 @@ func TestPrintAllWithSpaces(t *testing.T) { t2 := &ast.Task{Task: "t2"} t3 := &ast.Task{Task: "t3"} - tasks := ast.Tasks{} + tasks := ast.NewTasks() tasks.Set("t1", t1) tasks.Set("t2", t2) tasks.Set("t3", t3) diff --git a/requires.go b/requires.go index b07f7f66ee..7d555043d7 100644 --- a/requires.go +++ b/requires.go @@ -17,7 +17,7 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error { var missingVars []string for _, requiredVar := range t.Requires.Vars { - if !vars.Exists(requiredVar) { + if _, ok := vars.Get(requiredVar); !ok { missingVars = append(missingVars, requiredVar) } } diff --git a/setup.go b/setup.go index 6e2034716a..336e73b81b 100644 --- a/setup.go +++ b/setup.go @@ -213,7 +213,7 @@ func (e *Executor) readDotEnvFiles() error { } err = env.Range(func(key string, value ast.Var) error { - if ok := e.Taskfile.Env.Exists(key); !ok { + if _, ok := e.Taskfile.Env.Get(key); !ok { e.Taskfile.Env.Set(key, value) } return nil diff --git a/task.go b/task.go index d0f82a2d3a..dc6b7b09c1 100644 --- a/task.go +++ b/task.go @@ -444,7 +444,7 @@ func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) { case 0: // Carry on case 1: if call.Vars == nil { - call.Vars = &ast.Vars{} + call.Vars = ast.NewVars() } call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards}) return matchingTasks[0].Task, nil diff --git a/task_test.go b/task_test.go index 33c3a3099b..92bd2b940c 100644 --- a/task_test.go +++ b/task_test.go @@ -2205,7 +2205,7 @@ func TestSplitArgs(t *testing.T) { } require.NoError(t, e.Setup()) - vars := &ast.Vars{} + vars := ast.NewVars() vars.Set("CLI_ARGS", ast.Var{Value: "foo bar 'foo bar baz'"}) err := e.Run(context.Background(), &ast.Call{Task: "default", Vars: vars}) diff --git a/taskfile/ast/for.go b/taskfile/ast/for.go index 35d7ce7f60..544f71d3c1 100644 --- a/taskfile/ast/for.go +++ b/taskfile/ast/for.go @@ -5,13 +5,12 @@ import ( "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" - "github.com/go-task/task/v3/internal/omap" ) type For struct { From string List []any - Matrix omap.OrderedMap[string, []any] + Matrix *Matrix Var string Split string As string @@ -38,7 +37,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error { case yaml.MappingNode: var forStruct struct { - Matrix omap.OrderedMap[string, []any] + Matrix *Matrix Var string Split string As string diff --git a/taskfile/ast/include.go b/taskfile/ast/include.go index 659070de4a..f78459aeee 100644 --- a/taskfile/ast/include.go +++ b/taskfile/ast/include.go @@ -1,10 +1,10 @@ package ast import ( + "github.com/elliotchance/orderedmap/v2" "gopkg.in/yaml.v3" "github.com/go-task/task/v3/errors" - omap "github.com/go-task/task/v3/internal/omap" ) // Include represents information about included taskfiles @@ -20,27 +20,80 @@ type Include struct { Flatten bool } -// Includes represents information about included tasksfiles +// Includes represents information about included taskfiles type Includes struct { - omap.OrderedMap[string, *Include] + om *orderedmap.OrderedMap[string, *Include] +} + +type IncludeElement orderedmap.Element[string, *Include] + +func NewIncludes(els ...*IncludeElement) *Includes { + includes := &Includes{ + om: orderedmap.NewOrderedMap[string, *Include](), + } + for _, el := range els { + includes.Set(el.Key, el.Value) + } + return includes +} + +func (includes *Includes) Len() int { + if includes == nil || includes.om == nil { + return 0 + } + return includes.om.Len() +} + +func (includes *Includes) Get(key string) (*Include, bool) { + if includes == nil || includes.om == nil { + return &Include{}, false + } + return includes.om.Get(key) +} + +func (includes *Includes) Set(key string, value *Include) bool { + if includes == nil { + includes = NewIncludes() + } + if includes.om == nil { + includes.om = orderedmap.NewOrderedMap[string, *Include]() + } + return includes.om.Set(key, value) +} + +func (includes *Includes) Range(f func(k string, v *Include) error) error { + if includes == nil || includes.om == nil { + return nil + } + for pair := includes.om.Front(); pair != nil; pair = pair.Next() { + if err := f(pair.Key, pair.Value); err != nil { + return err + } + } + return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (includes *Includes) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.MappingNode: - // NOTE(@andreynering): on this style of custom unmarshalling, - // even number contains the keys, while odd numbers contains - // the values. + // NOTE: orderedmap does not have an unmarshaler, so we have to decode + // the map manually. We increment over 2 values at a time and assign + // them as a key-value pair. for i := 0; i < len(node.Content); i += 2 { keyNode := node.Content[i] valueNode := node.Content[i+1] + // Decode the value node into an Include struct var v Include if err := valueNode.Decode(&v); err != nil { return errors.NewTaskfileDecodeError(err, node) } + + // Set the include namespace v.Namespace = keyNode.Value + + // Add the include to the ordered map includes.Set(keyNode.Value, &v) } return nil @@ -49,22 +102,6 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error { return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("includes") } -// Len returns the length of the map -func (includes *Includes) Len() int { - if includes == nil { - return 0 - } - return includes.OrderedMap.Len() -} - -// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors -func (includes *Includes) Range(f func(k string, v *Include) error) error { - if includes == nil { - return nil - } - return includes.OrderedMap.Range(f) -} - func (include *Include) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { diff --git a/taskfile/ast/matrix.go b/taskfile/ast/matrix.go new file mode 100644 index 0000000000..e5e2a8b6cb --- /dev/null +++ b/taskfile/ast/matrix.go @@ -0,0 +1,95 @@ +package ast + +import ( + "github.com/elliotchance/orderedmap/v2" + "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/errors" + "github.com/go-task/task/v3/internal/deepcopy" +) + +type Matrix struct { + om *orderedmap.OrderedMap[string, []any] +} + +type MatrixElement orderedmap.Element[string, []any] + +func NewMatrix(els ...*MatrixElement) *Matrix { + matrix := &Matrix{ + om: orderedmap.NewOrderedMap[string, []any](), + } + for _, el := range els { + matrix.Set(el.Key, el.Value) + } + return matrix +} + +func (matrix *Matrix) Len() int { + if matrix == nil || matrix.om == nil { + return 0 + } + return matrix.om.Len() +} + +func (matrix *Matrix) Get(key string) ([]any, bool) { + if matrix == nil || matrix.om == nil { + return nil, false + } + return matrix.om.Get(key) +} + +func (matrix *Matrix) Set(key string, value []any) bool { + if matrix == nil { + matrix = NewMatrix() + } + if matrix.om == nil { + matrix.om = orderedmap.NewOrderedMap[string, []any]() + } + return matrix.om.Set(key, value) +} + +func (matrix *Matrix) Range(f func(k string, v []any) error) error { + if matrix == nil || matrix.om == nil { + return nil + } + for pair := matrix.om.Front(); pair != nil; pair = pair.Next() { + if err := f(pair.Key, pair.Value); err != nil { + return err + } + } + return nil +} + +func (matrix *Matrix) DeepCopy() *Matrix { + if matrix == nil { + return nil + } + return &Matrix{ + om: deepcopy.OrderedMap(matrix.om), + } +} + +func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error { + switch node.Kind { + case yaml.MappingNode: + // NOTE: orderedmap does not have an unmarshaler, so we have to decode + // the map manually. We increment over 2 values at a time and assign + // them as a key-value pair. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + valueNode := node.Content[i+1] + + // Decode the value node into a Matrix struct + var v []any + if err := valueNode.Decode(&v); err != nil { + return errors.NewTaskfileDecodeError(err, node) + } + + // Add the task to the ordered map + matrix.Set(keyNode.Value, v) + } + return nil + } + + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("matrix") +} diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go index 9e0c0b7924..4aad932da7 100644 --- a/taskfile/ast/taskfile.go +++ b/taskfile/ast/taskfile.go @@ -29,7 +29,7 @@ type Taskfile struct { Shopt []string Vars *Vars Env *Vars - Tasks Tasks + Tasks *Tasks Silent bool Dotenv []string Run string @@ -47,11 +47,17 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error { if t2.Output.IsSet() { t1.Output = t2.Output } + if t1.Includes == nil { + t1.Includes = NewIncludes() + } if t1.Vars == nil { - t1.Vars = &Vars{} + t1.Vars = NewVars() } if t1.Env == nil { - t1.Env = &Vars{} + t1.Env = NewVars() + } + if t1.Tasks == nil { + t1.Tasks = NewTasks() } t1.Vars.Merge(t2.Vars, include) t1.Env.Merge(t2.Env, include) @@ -70,7 +76,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { Shopt []string Vars *Vars Env *Vars - Tasks Tasks + Tasks *Tasks Silent bool Dotenv []string Run string @@ -92,11 +98,17 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { tf.Dotenv = taskfile.Dotenv tf.Run = taskfile.Run tf.Interval = taskfile.Interval + if tf.Includes == nil { + tf.Includes = NewIncludes() + } if tf.Vars == nil { - tf.Vars = &Vars{} + tf.Vars = NewVars() } if tf.Env == nil { - tf.Env = &Vars{} + tf.Env = NewVars() + } + if tf.Tasks == nil { + tf.Tasks = NewTasks() } return nil } diff --git a/taskfile/ast/taskfile_test.go b/taskfile/ast/taskfile_test.go index 0d0cd43cc3..67c2333d35 100644 --- a/taskfile/ast/taskfile_test.go +++ b/taskfile/ast/taskfile_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" - "github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/taskfile/ast" ) @@ -38,15 +37,21 @@ vars: yamlTaskCall, &ast.Cmd{}, &ast.Cmd{ - Task: "another-task", Vars: &ast.Vars{ - OrderedMap: omap.FromMapWithOrder( - map[string]ast.Var{ - "PARAM1": {Value: "VALUE1"}, - "PARAM2": {Value: "VALUE2"}, + Task: "another-task", + Vars: ast.NewVars( + &ast.VarElement{ + Key: "PARAM1", + Value: ast.Var{ + Value: "VALUE1", }, - []string{"PARAM1", "PARAM2"}, - ), - }, + }, + &ast.VarElement{ + Key: "PARAM2", + Value: ast.Var{ + Value: "VALUE2", + }, + }, + ), }, }, { @@ -58,14 +63,15 @@ vars: yamlDeferredCall, &ast.Cmd{}, &ast.Cmd{ - Task: "some_task", Vars: &ast.Vars{ - OrderedMap: omap.FromMapWithOrder( - map[string]ast.Var{ - "PARAM1": {Value: "var"}, + Task: "some_task", + Vars: ast.NewVars( + &ast.VarElement{ + Key: "PARAM1", + Value: ast.Var{ + Value: "var", }, - []string{"PARAM1"}, - ), - }, + }, + ), Defer: true, }, }, @@ -78,15 +84,21 @@ vars: yamlTaskCall, &ast.Dep{}, &ast.Dep{ - Task: "another-task", Vars: &ast.Vars{ - OrderedMap: omap.FromMapWithOrder( - map[string]ast.Var{ - "PARAM1": {Value: "VALUE1"}, - "PARAM2": {Value: "VALUE2"}, + Task: "another-task", + Vars: ast.NewVars( + &ast.VarElement{ + Key: "PARAM1", + Value: ast.Var{ + Value: "VALUE1", + }, + }, + &ast.VarElement{ + Key: "PARAM2", + Value: ast.Var{ + Value: "VALUE2", }, - []string{"PARAM1", "PARAM2"}, - ), - }, + }, + ), }, }, } diff --git a/taskfile/ast/tasks.go b/taskfile/ast/tasks.go index cfe29a8da3..2ecc4fbfa4 100644 --- a/taskfile/ast/tasks.go +++ b/taskfile/ast/tasks.go @@ -5,16 +5,86 @@ import ( "slices" "strings" + "github.com/elliotchance/orderedmap/v2" "gopkg.in/yaml.v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/filepathext" - "github.com/go-task/task/v3/internal/omap" ) // Tasks represents a group of tasks type Tasks struct { - omap.OrderedMap[string, *Task] + om *orderedmap.OrderedMap[string, *Task] +} + +type TaskElement orderedmap.Element[string, *Task] + +func NewTasks(els ...*TaskElement) *Tasks { + tasks := &Tasks{ + om: orderedmap.NewOrderedMap[string, *Task](), + } + for _, el := range els { + tasks.Set(el.Key, el.Value) + } + return tasks +} + +func (tasks *Tasks) Len() int { + if tasks == nil || tasks.om == nil { + return 0 + } + return tasks.om.Len() +} + +func (tasks *Tasks) Get(key string) (*Task, bool) { + if tasks == nil || tasks.om == nil { + return &Task{}, false + } + return tasks.om.Get(key) +} + +func (tasks *Tasks) Set(key string, value *Task) bool { + if tasks == nil { + tasks = NewTasks() + } + if tasks.om == nil { + tasks.om = orderedmap.NewOrderedMap[string, *Task]() + } + return tasks.om.Set(key, value) +} + +func (tasks *Tasks) Range(f func(k string, v *Task) error) error { + if tasks == nil || tasks.om == nil { + return nil + } + for pair := tasks.om.Front(); pair != nil; pair = pair.Next() { + if err := f(pair.Key, pair.Value); err != nil { + return err + } + } + return nil +} + +func (tasks *Tasks) Keys() []string { + if tasks == nil { + return nil + } + var keys []string + for pair := tasks.om.Front(); pair != nil; pair = pair.Next() { + keys = append(keys, pair.Key) + } + return keys +} + +func (tasks *Tasks) Values() []*Task { + if tasks == nil { + return nil + } + var values []*Task + for pair := tasks.om.Front(); pair != nil; pair = pair.Next() { + values = append(values, pair.Value) + } + return values } type MatchingTask struct { @@ -26,10 +96,9 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask { if call == nil { return nil } - var task *Task var matchingTasks []*MatchingTask // If there is a direct match, return it - if task = t.OrderedMap.Get(call.Task); task != nil { + if task, ok := t.Get(call.Task); ok { matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil}) return matchingTasks } @@ -47,7 +116,7 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask { return matchingTasks } -func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) error { +func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error { err := t2.Range(func(name string, v *Task) error { // We do a deep copy of the task struct here to ensure that no data can // be changed elsewhere once the taskfile is merged. @@ -94,13 +163,13 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) e if include.AdvancedImport { task.Dir = filepathext.SmartJoin(include.Dir, task.Dir) if task.IncludeVars == nil { - task.IncludeVars = &Vars{} + task.IncludeVars = NewVars() } task.IncludeVars.Merge(include.Vars, nil) task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy() } - if t1.Get(taskName) != nil { + if _, ok := t1.Get(taskName); ok { return &errors.TaskNameFlattenConflictError{ TaskName: taskName, Include: include.Namespace, @@ -112,52 +181,50 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) e return nil }) - // If the included Taskfile has a default task, being not flattened and the parent namespace has - // no task with a matching name, we can add an alias so that the user can - // run the included Taskfile's default task without specifying its full - // name. If the parent namespace has aliases, we add another alias for each - // of them. - if t2.Get("default") != nil && t1.Get(include.Namespace) == nil && !include.Flatten { + // If the included Taskfile has a default task, is not flattened and the + // parent namespace has no task with a matching name, we can add an alias so + // that the user can run the included Taskfile's default task without + // specifying its full name. If the parent namespace has aliases, we add + // another alias for each of them. + _, t2DefaultExists := t2.Get("default") + _, t1NamespaceExists := t1.Get(include.Namespace) + if t2DefaultExists && !t1NamespaceExists && !include.Flatten { defaultTaskName := fmt.Sprintf("%s:default", include.Namespace) - t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Namespace) - t1.Get(defaultTaskName).Aliases = slices.Concat(t1.Get(defaultTaskName).Aliases, include.Aliases) + t1DefaultTask, ok := t1.Get(defaultTaskName) + if ok { + t1DefaultTask.Aliases = append(t1DefaultTask.Aliases, include.Namespace) + t1DefaultTask.Aliases = slices.Concat(t1DefaultTask.Aliases, include.Aliases) + } } + return err } func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.MappingNode: - tasks := omap.New[string, *Task]() - if err := node.Decode(&tasks); err != nil { - return errors.NewTaskfileDecodeError(err, node) - } - - // nolint: errcheck - tasks.Range(func(name string, task *Task) error { - // Set the task's name - if task == nil { - task = &Task{ - Task: name, - } + // NOTE: orderedmap does not have an unmarshaler, so we have to decode + // the map manually. We increment over 2 values at a time and assign + // them as a key-value pair. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + valueNode := node.Content[i+1] + + // Decode the value node into a Task struct + var v Task + if err := valueNode.Decode(&v); err != nil { + return errors.NewTaskfileDecodeError(err, node) } - task.Task = name - - // Set the task's location - for _, keys := range node.Content { - if keys.Value == name { - task.Location = &Location{ - Line: keys.Line, - Column: keys.Column, - } - } + + // Set the task name and location + v.Task = keyNode.Value + v.Location = &Location{ + Line: keyNode.Line, + Column: keyNode.Column, } - tasks.Set(name, task) - return nil - }) - *t = Tasks{ - OrderedMap: tasks, + // Add the task to the ordered map + t.Set(keyNode.Value, &v) } return nil } diff --git a/taskfile/ast/var.go b/taskfile/ast/var.go index dce6288452..d40682a510 100644 --- a/taskfile/ast/var.go +++ b/taskfile/ast/var.go @@ -3,67 +3,97 @@ package ast import ( "strings" + "github.com/elliotchance/orderedmap/v2" "gopkg.in/yaml.v3" "github.com/go-task/task/v3/errors" + "github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/experiments" - "github.com/go-task/task/v3/internal/omap" ) // Vars is a string[string] variables map. type Vars struct { - omap.OrderedMap[string, Var] + om *orderedmap.OrderedMap[string, Var] +} + +type VarElement orderedmap.Element[string, Var] + +func NewVars(els ...*VarElement) *Vars { + vs := &Vars{ + om: orderedmap.NewOrderedMap[string, Var](), + } + for _, el := range els { + vs.Set(el.Key, el.Value) + } + return vs +} + +func (vs *Vars) Len() int { + if vs == nil || vs.om == nil { + return 0 + } + return vs.om.Len() +} + +func (vs *Vars) Get(key string) (Var, bool) { + if vs == nil || vs.om == nil { + return Var{}, false + } + return vs.om.Get(key) +} + +func (vs *Vars) Set(key string, value Var) bool { + if vs == nil { + vs = NewVars() + } + if vs.om == nil { + vs.om = orderedmap.NewOrderedMap[string, Var]() + } + return vs.om.Set(key, value) +} + +func (vs *Vars) Range(f func(k string, v Var) error) error { + if vs == nil || vs.om == nil { + return nil + } + for pair := vs.om.Front(); pair != nil; pair = pair.Next() { + if err := f(pair.Key, pair.Value); err != nil { + return err + } + } + return nil } // ToCacheMap converts Vars to a map containing only the static // variables func (vs *Vars) ToCacheMap() (m map[string]any) { m = make(map[string]any, vs.Len()) - _ = vs.Range(func(k string, v Var) error { - if v.Sh != "" { + for pair := vs.om.Front(); pair != nil; pair = pair.Next() { + if pair.Value.Sh != "" { // Dynamic variable is not yet resolved; trigger // to be used in templates. return nil } - - if v.Live != nil { - m[k] = v.Live + if pair.Value.Live != nil { + m[pair.Key] = pair.Value.Live } else { - m[k] = v.Value + m[pair.Key] = pair.Value.Value } - return nil - }) - return -} - -// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors -func (vs *Vars) Range(f func(k string, v Var) error) error { - if vs == nil { - return nil } - return vs.OrderedMap.Range(f) + return } // Wrapper around OrderedMap.Merge to ensure we don't get nil pointer errors func (vs *Vars) Merge(other *Vars, include *Include) { - if vs == nil || other == nil { + if vs == nil || vs.om == nil || other == nil { return } - _ = other.Range(func(key string, value Var) error { + for pair := other.om.Front(); pair != nil; pair = pair.Next() { if include != nil && include.AdvancedImport { - value.Dir = include.Dir + pair.Value.Dir = include.Dir } - vs.Set(key, value) - return nil - }) -} - -// Wrapper around OrderedMap.Len to ensure we don't get nil pointer errors -func (vs *Vars) Len() int { - if vs == nil { - return 0 + vs.om.Set(pair.Key, pair.Value) } - return vs.OrderedMap.Len() } // DeepCopy creates a new instance of Vars and copies @@ -73,8 +103,34 @@ func (vs *Vars) DeepCopy() *Vars { return nil } return &Vars{ - OrderedMap: vs.OrderedMap.DeepCopy(), + om: deepcopy.OrderedMap(vs.om), + } +} + +func (vs *Vars) UnmarshalYAML(node *yaml.Node) error { + vs.om = orderedmap.NewOrderedMap[string, Var]() + switch node.Kind { + case yaml.MappingNode: + // NOTE: orderedmap does not have an unmarshaler, so we have to decode + // the map manually. We increment over 2 values at a time and assign + // them as a key-value pair. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + valueNode := node.Content[i+1] + + // Decode the value node into a Task struct + var v Var + if err := valueNode.Decode(&v); err != nil { + return errors.NewTaskfileDecodeError(err, node) + } + + // Add the task to the ordered map + vs.Set(keyNode.Value, v) + } + return nil } + + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("vars") } // Var represents either a static or dynamic variable. diff --git a/taskfile/dotenv.go b/taskfile/dotenv.go index 3971a9d233..b439cd736b 100644 --- a/taskfile/dotenv.go +++ b/taskfile/dotenv.go @@ -21,7 +21,7 @@ func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, erro return nil, err } - env := &ast.Vars{} + env := ast.NewVars() cache := &templater.Cache{Vars: vars} for _, dotEnvPath := range tf.Dotenv { @@ -40,7 +40,7 @@ func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, erro return nil, err } for key, value := range envs { - if ok := env.Exists(key); !ok { + if _, ok := env.Get(key); !ok { env.Set(key, ast.Var{Value: value}) } } diff --git a/variables.go b/variables.go index cfdb70ae46..3d6289e579 100644 --- a/variables.go +++ b/variables.go @@ -11,7 +11,6 @@ import ( "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/fingerprint" - "github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) @@ -86,7 +85,7 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task, new.Prefix = new.Task } - dotenvEnvs := &ast.Vars{} + dotenvEnvs := ast.NewVars() if len(new.Dotenv) > 0 { for _, dotEnvPath := range new.Dotenv { dotEnvPath = filepathext.SmartJoin(new.Dir, dotEnvPath) @@ -98,14 +97,14 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task, return nil, err } for key, value := range envs { - if ok := dotenvEnvs.Exists(key); !ok { + if _, ok := dotenvEnvs.Get(key); !ok { dotenvEnvs.Set(key, ast.Var{Value: value}) } } } } - new.Env = &ast.Vars{} + new.Env = ast.NewVars() new.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache), nil) new.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache), nil) new.Env.Merge(templater.ReplaceVars(origTask.Env, cache), nil) @@ -297,11 +296,11 @@ func itemsFromFor( // Get the list from a variable and split it up if f.Var != "" { if vars != nil { - v := vars.Get(f.Var) + v, ok := vars.Get(f.Var) // If the variable is dynamic, then it hasn't been resolved yet // and we can't use it as a list. This happens when fast compiling a task // for use in --list or --list-all etc. - if v.Value != nil && v.Sh == "" { + if ok && v.Sh == "" { switch value := v.Value.(type) { case string: if f.Split != "" { @@ -329,7 +328,7 @@ func itemsFromFor( } // product generates the cartesian product of the input map of slices. -func product(inputMap omap.OrderedMap[string, []any]) []map[string]any { +func product(inputMap *ast.Matrix) []map[string]any { if inputMap.Len() == 0 { return nil }