From 2965841eb7df004297090b92bac721c35c9319c9 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 30 Dec 2024 17:54:36 +0000 Subject: [PATCH] feat: use external package for ordered maps (#1797) --- args/args.go | 2 +- args/args_test.go | 82 +++++++++------- go.mod | 1 + go.sum | 2 + internal/compiler/env.go | 2 +- internal/deepcopy/deepcopy.go | 17 ++++ internal/omap/orderedmap.go | 164 ------------------------------- internal/omap/orderedmap_test.go | 137 -------------------------- internal/output/output_test.go | 12 +-- internal/summary/summary.go | 4 +- internal/summary/summary_test.go | 2 +- requires.go | 5 +- 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 | 147 +++++++++++++++++++-------- taskfile/ast/var.go | 120 ++++++++++++++++------ taskfile/dotenv.go | 4 +- variables.go | 13 ++- 24 files changed, 499 insertions(+), 486 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 4499b28e..1896ae31 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 252d9c8d..14238c7c 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" ) @@ -34,30 +33,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"}, @@ -65,14 +74,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, @@ -85,15 +94,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", + }, + }, + ), }, } @@ -104,8 +118,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 5ac6da82..90fc5e74 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/chainguard-dev/git-urls v1.0.2 github.com/davecgh/go-spew v1.1.1 github.com/dominikbraun/graph v0.23.0 + github.com/elliotchance/orderedmap/v2 v2.6.0 github.com/fatih/color v1.18.0 github.com/go-git/go-billy/v5 v5.6.0 github.com/go-git/go-git/v5 v5.12.0 diff --git a/go.sum b/go.sum index 6b076bdf..71d783e8 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucV github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elliotchance/orderedmap/v2 v2.6.0 h1:Zzo4k/u6hTRSt4NbYVphwOn5fBKlLpcbaV00INfJ1WI= +github.com/elliotchance/orderedmap/v2 v2.6.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= diff --git a/internal/compiler/env.go b/internal/compiler/env.go index 61552d0b..393af2b9 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 8305755b..c42dd141 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 cf7fc22c..00000000 --- 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 2e198833..00000000 --- a/internal/omap/orderedmap_test.go +++ /dev/null @@ -1,137 +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) { - t.Parallel() - - 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) { - t.Parallel() - - 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) { - t.Parallel() - - 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) { - t.Parallel() - - 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) { - t.Parallel() - - 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) { - t.Helper() - - 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) { - t.Parallel() - - 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) { - t.Parallel() - - 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 c278dd1b..ba03c9ad 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" @@ -55,11 +54,12 @@ func TestGroupWithBeginEnd(t *testing.T) { t.Parallel() 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 a1bcdabe..7e63cee7 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 52ca90bb..f72d4f2f 100644 --- a/internal/summary/summary_test.go +++ b/internal/summary/summary_test.go @@ -172,7 +172,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 2c59bc62..156a2dc6 100644 --- a/requires.go +++ b/requires.go @@ -20,10 +20,11 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error { var missingVars []string var notAllowedValuesVars []errors.NotAllowedVar for _, requiredVar := range t.Requires.Vars { - value, isString := vars.Get(requiredVar.Name).Value.(string) - if !vars.Exists(requiredVar.Name) { + value, ok := vars.Get(requiredVar.Name) + if !ok { missingVars = append(missingVars, requiredVar.Name) } else { + value, isString := value.Value.(string) if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) { notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{ Value: value, diff --git a/setup.go b/setup.go index 6e203471..336e73b8 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 e1fbe5d4..f4e15279 100644 --- a/task.go +++ b/task.go @@ -451,7 +451,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 a43b79a2..5dd84ca9 100644 --- a/task_test.go +++ b/task_test.go @@ -2755,7 +2755,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 35d7ce7f..544f71d3 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 a190154d..630f6537 100644 --- a/taskfile/ast/include.go +++ b/taskfile/ast/include.go @@ -1,11 +1,11 @@ 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" - omap "github.com/go-task/task/v3/internal/omap" ) // Include represents information about included taskfiles @@ -22,27 +22,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 @@ -51,22 +104,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 00000000..e5e2a8b6 --- /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 9e0c0b79..4aad932d 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 12a8d4be..be364e17 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" ) @@ -40,15 +39,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", + }, + }, + ), }, }, { @@ -60,14 +65,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, }, }, @@ -80,15 +86,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", }, - []string{"PARAM1", "PARAM2"}, - ), - }, + }, + &ast.VarElement{ + Key: "PARAM2", + Value: ast.Var{ + Value: "VALUE2", + }, + }, + ), }, }, } diff --git a/taskfile/ast/tasks.go b/taskfile/ast/tasks.go index df3d331b..d1cb3684 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. @@ -100,13 +169,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, @@ -118,52 +187,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) - } + // 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] - // nolint: errcheck - tasks.Range(func(name string, task *Task) error { - // Set the task's name - if task == nil { - task = &Task{ - Task: name, - } + // 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 88d40794..d582aba9 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 != nil && *v.Sh != "" { + for pair := vs.om.Front(); pair != nil; pair = pair.Next() { + if pair.Value.Sh != nil && *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,10 +103,36 @@ 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. type Var struct { Value any diff --git a/taskfile/dotenv.go b/taskfile/dotenv.go index cabb4016..0ce05cfd 100644 --- a/taskfile/dotenv.go +++ b/taskfile/dotenv.go @@ -22,7 +22,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 { @@ -41,7 +41,7 @@ func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, erro return nil, fmt.Errorf("error reading env file %s: %w", dotEnvPath, 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 67703e81..5258ef23 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 == nil { + if ok && v.Sh == nil { switch value := v.Value.(type) { case string: if f.Split != "" { @@ -333,7 +332,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 }