1
0
mirror of https://github.com/go-task/task.git synced 2025-11-23 22:24:45 +02:00

feat: use external package for ordered maps (#1797)

This commit is contained in:
Pete Davison
2024-12-30 17:54:36 +00:00
committed by GitHub
parent dbe6e41ac8
commit 2965841eb7
24 changed files with 499 additions and 486 deletions

View File

@@ -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

View File

@@ -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 {

95
taskfile/ast/matrix.go Normal file
View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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",
},
},
),
},
},
}

View File

@@ -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
}

View File

@@ -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
// <no value> 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

View File

@@ -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})
}
}