diff --git a/cmd/task/task.go b/cmd/task/task.go index 33600b4c..9df50193 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -131,7 +131,8 @@ func run() error { return err } - if err := e.Setup(); err != nil { + err := e.Setup() + if err != nil { return err } diff --git a/errors/error_taskfile_decode.go b/errors/error_taskfile_decode.go new file mode 100644 index 00000000..a17cf163 --- /dev/null +++ b/errors/error_taskfile_decode.go @@ -0,0 +1,179 @@ +package errors + +import ( + "bytes" + "embed" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/quick" + "github.com/alecthomas/chroma/v2/styles" + "github.com/fatih/color" + "gopkg.in/yaml.v3" +) + +//go:embed themes/*.xml +var embedded embed.FS + +var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`) + +func init() { + r, err := embedded.Open("themes/task.xml") + if err != nil { + panic(err) + } + style, err := chroma.NewXMLStyle(r) + if err != nil { + panic(err) + } + styles.Register(style) +} + +type ( + TaskfileDecodeError struct { + Message string + Location string + Line int + Column int + Tag string + Snippet TaskfileSnippet + Err error + } + TaskfileSnippet struct { + Lines []string + StartLine int + EndLine int + Padding int + } +) + +func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError { + // If the error is already a DecodeError, return it + taskfileInvalidErr := &TaskfileDecodeError{} + if errors.As(err, &taskfileInvalidErr) { + return taskfileInvalidErr + } + return &TaskfileDecodeError{ + Line: node.Line, + Column: node.Column, + Tag: node.ShortTag(), + Err: err, + } +} + +func (err *TaskfileDecodeError) Error() string { + buf := &bytes.Buffer{} + + // Print the error message + if err.Message != "" { + fmt.Fprintln(buf, color.RedString("err: %s", err.Message)) + } else { + // Extract the errors from the TypeError + te := &yaml.TypeError{} + if errors.As(err.Err, &te) { + if len(te.Errors) > 1 { + fmt.Fprintln(buf, color.RedString("errs:")) + for _, message := range te.Errors { + fmt.Fprintln(buf, color.RedString("- %s", extractTypeErrorMessage(message))) + } + } else { + fmt.Fprintln(buf, color.RedString("err: %s", extractTypeErrorMessage(te.Errors[0]))) + } + } else { + // Otherwise print the error message normally + fmt.Fprintln(buf, color.RedString("err: %s", err.Err)) + } + } + fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column)) + + // Print the snippet + maxLineNumberDigits := digits(err.Snippet.EndLine) + lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits) + columnSpacer := strings.Repeat(" ", err.Column-1) + for i, line := range err.Snippet.Lines { + currentLine := err.Snippet.StartLine + i + 1 + + lineIndicator := " " + if currentLine == err.Line { + lineIndicator = ">" + } + columnIndicator := "^" + + // Print each line + lineIndicator = color.RedString(lineIndicator) + columnIndicator = color.RedString(columnIndicator) + lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits) + lineNumber := fmt.Sprintf(lineNumberFormat, currentLine) + fmt.Fprintf(buf, "%s %s | %s", lineIndicator, lineNumber, line) + + // Print the column indicator + if currentLine == err.Line { + fmt.Fprintf(buf, "\n %s | %s%s", lineNumberSpacer, columnSpacer, columnIndicator) + } + + // If there are more lines to print, add a newline + if i < len(err.Snippet.Lines)-1 { + fmt.Fprintln(buf) + } + } + + return buf.String() +} + +func (err *TaskfileDecodeError) Unwrap() error { + return err.Err +} + +func (err *TaskfileDecodeError) Code() int { + return CodeTaskfileDecode +} + +func (err *TaskfileDecodeError) WithMessage(format string, a ...any) *TaskfileDecodeError { + err.Message = fmt.Sprintf(format, a...) + return err +} + +func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError { + err.Message = fmt.Sprintf("cannot unmarshal %s into %s", err.Tag, t) + return err +} + +func (err *TaskfileDecodeError) WithFileInfo(location string, b []byte, padding int) *TaskfileDecodeError { + buf := &bytes.Buffer{} + if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil { + buf.WriteString(string(b)) + } + lines := strings.Split(buf.String(), "\n") + start := max(err.Line-1-padding, 0) + end := min(err.Line+padding, len(lines)-1) + + err.Location = location + err.Snippet = TaskfileSnippet{ + Lines: lines[start:end], + StartLine: start, + EndLine: end, + Padding: padding, + } + + return err +} + +func extractTypeErrorMessage(message string) string { + matches := typeErrorRegex.FindStringSubmatch(message) + if len(matches) == 2 { + return matches[1] + } + return message +} + +func digits(number int) int { + count := 0 + for number != 0 { + number /= 10 + count += 1 + } + return count +} diff --git a/errors/errors.go b/errors/errors.go index 24e6a7a2..5c98f05d 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -12,14 +12,14 @@ const ( const ( CodeTaskfileNotFound int = iota + 100 CodeTaskfileAlreadyExists - CodeTaskfileInvalid + CodeTaskfileDecode CodeTaskfileFetchFailed CodeTaskfileNotTrusted CodeTaskfileNotSecure CodeTaskfileCacheNotFound CodeTaskfileVersionCheckError CodeTaskfileNetworkTimeout - _ // CodeTaskfileDuplicateInclude + CodeTaskfileInvalid CodeTaskfileCycle ) @@ -58,3 +58,8 @@ func Is(err, target error) bool { func As(err error, target any) bool { return errors.As(err, target) } + +// Unwrap wraps the standard errors.Unwrap function so that we don't need to alias that package. +func Unwrap(err error) error { + return errors.Unwrap(err) +} diff --git a/errors/themes/task.xml b/errors/themes/task.xml new file mode 100644 index 00000000..3fdd7e63 --- /dev/null +++ b/errors/themes/task.xml @@ -0,0 +1,17 @@ + diff --git a/go.mod b/go.mod index c83c2481..b0661c4c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21.0 require ( github.com/Masterminds/semver/v3 v3.2.1 + github.com/alecthomas/chroma/v2 v2.13.0 github.com/davecgh/go-spew v1.1.1 github.com/dominikbraun/graph v0.23.0 github.com/fatih/color v1.16.0 @@ -25,6 +26,7 @@ require ( ) require ( + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 6e834b09..a9d5e22b 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,17 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI= +github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -16,6 +24,8 @@ github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90 h1:JBbiZ2CXIZ9Upe github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= diff --git a/task_test.go b/task_test.go index 94c11ed7..2acf489f 100644 --- a/task_test.go +++ b/task_test.go @@ -1042,7 +1042,7 @@ func TestIncludesIncorrect(t *testing.T) { err := e.Setup() require.Error(t, err) - assert.Contains(t, err.Error(), "task: Failed to parse testdata/includes_incorrect/incomplete.yml:") + assert.Contains(t, err.Error(), "Failed to parse testdata/includes_incorrect/incomplete.yml:", err.Error()) } func TestIncludesEmptyMain(t *testing.T) { diff --git a/taskfile/ast/cmd.go b/taskfile/ast/cmd.go index 3874e14d..4602ea65 100644 --- a/taskfile/ast/cmd.go +++ b/taskfile/ast/cmd.go @@ -1,10 +1,9 @@ package ast import ( - "fmt" - "gopkg.in/yaml.v3" + "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) @@ -46,7 +45,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error { case yaml.ScalarNode: var cmd string if err := node.Decode(&cmd); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } c.Cmd = cmd return nil @@ -110,8 +109,8 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error { return nil } - return fmt.Errorf("yaml: line %d: invalid keys in command", node.Line) + return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command") } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into command", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("command") } diff --git a/taskfile/ast/dep.go b/taskfile/ast/dep.go index 5119d731..d8c6af7f 100644 --- a/taskfile/ast/dep.go +++ b/taskfile/ast/dep.go @@ -1,9 +1,9 @@ package ast import ( - "fmt" - "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/errors" ) // Dep is a task dependency @@ -32,7 +32,7 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error { case yaml.ScalarNode: var task string if err := node.Decode(&task); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } d.Task = task return nil @@ -45,7 +45,7 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error { Silent bool } if err := node.Decode(&taskCall); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } d.Task = taskCall.Task d.For = taskCall.For @@ -54,5 +54,5 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error { return nil } - return fmt.Errorf("cannot unmarshal %s into dependency", node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("dependency") } diff --git a/taskfile/ast/for.go b/taskfile/ast/for.go index cc9f3d9a..8505fbb3 100644 --- a/taskfile/ast/for.go +++ b/taskfile/ast/for.go @@ -1,10 +1,9 @@ package ast import ( - "fmt" - "gopkg.in/yaml.v3" + "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) @@ -22,7 +21,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error { case yaml.ScalarNode: var from string if err := node.Decode(&from); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } f.From = from return nil @@ -30,7 +29,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error { case yaml.SequenceNode: var list []any if err := node.Decode(&list); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } f.List = list return nil @@ -41,17 +40,19 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error { Split string As string } - if err := node.Decode(&forStruct); err == nil && forStruct.Var != "" { - f.Var = forStruct.Var - f.Split = forStruct.Split - f.As = forStruct.As - return nil + if err := node.Decode(&forStruct); err != nil { + return errors.NewTaskfileDecodeError(err, node) } - - return fmt.Errorf("yaml: line %d: invalid keys in for", node.Line) + if forStruct.Var == "" { + return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in for") + } + f.Var = forStruct.Var + f.Split = forStruct.Split + f.As = forStruct.As + return nil } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into for", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("for") } func (f *For) DeepCopy() *For { diff --git a/taskfile/ast/glob.go b/taskfile/ast/glob.go index 390b906f..3d30b45c 100644 --- a/taskfile/ast/glob.go +++ b/taskfile/ast/glob.go @@ -1,9 +1,9 @@ package ast import ( - "fmt" - "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/errors" ) type Glob struct { @@ -13,20 +13,22 @@ type Glob struct { func (g *Glob) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { + case yaml.ScalarNode: g.Glob = node.Value return nil + case yaml.MappingNode: var glob struct { Exclude string } if err := node.Decode(&glob); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } g.Glob = glob.Exclude g.Negate = true return nil - default: - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into task", node.Line, node.ShortTag()) } + + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("glob") } diff --git a/taskfile/ast/include.go b/taskfile/ast/include.go index 5b04bc27..defe7c44 100644 --- a/taskfile/ast/include.go +++ b/taskfile/ast/include.go @@ -1,10 +1,9 @@ package ast import ( - "fmt" - "gopkg.in/yaml.v3" + "github.com/go-task/task/v3/errors" omap "github.com/go-task/task/v3/internal/omap" ) @@ -38,7 +37,7 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error { var v Include if err := valueNode.Decode(&v); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } v.Namespace = keyNode.Value includes.Set(keyNode.Value, &v) @@ -46,7 +45,7 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error { return nil } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfiles", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("includes") } // Len returns the length of the map @@ -71,7 +70,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error { case yaml.ScalarNode: var str string if err := node.Decode(&str); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } include.Taskfile = str return nil @@ -86,7 +85,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error { Vars *Vars } if err := node.Decode(&includedTaskfile); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } include.Taskfile = includedTaskfile.Taskfile include.Dir = includedTaskfile.Dir @@ -98,7 +97,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error { return nil } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfile", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("include") } // DeepCopy creates a new instance of IncludedTaskfile and copies diff --git a/taskfile/ast/output.go b/taskfile/ast/output.go index 79c4c113..b820e2b5 100644 --- a/taskfile/ast/output.go +++ b/taskfile/ast/output.go @@ -1,9 +1,9 @@ package ast import ( - "fmt" - "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/errors" ) // Output of the Task output @@ -25,7 +25,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error { case yaml.ScalarNode: var name string if err := node.Decode(&name); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } s.Name = name return nil @@ -35,10 +35,10 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error { Group *OutputGroup } if err := node.Decode(&tmp); err != nil { - return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err) + return errors.NewTaskfileDecodeError(err, node) } if tmp.Group == nil { - return fmt.Errorf("task: output style must have the \"group\" key when in mapping form") + return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style must have the "group" key when in mapping form`) } *s = Output{ Name: "group", @@ -47,7 +47,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error { return nil } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into output", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("output") } // OutputGroup is the style options specific to the Group style. diff --git a/taskfile/ast/platforms.go b/taskfile/ast/platforms.go index 664b9b9a..ac258a4a 100644 --- a/taskfile/ast/platforms.go +++ b/taskfile/ast/platforms.go @@ -6,6 +6,7 @@ import ( "gopkg.in/yaml.v3" + "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/goext" ) @@ -30,7 +31,7 @@ type ErrInvalidPlatform struct { } func (err *ErrInvalidPlatform) Error() string { - return fmt.Sprintf(`task: Invalid platform "%s"`, err.Platform) + return fmt.Sprintf(`invalid platform "%s"`, err.Platform) } // UnmarshalYAML implements yaml.Unmarshaler interface. @@ -39,14 +40,14 @@ func (p *Platform) UnmarshalYAML(node *yaml.Node) error { case yaml.ScalarNode: var platform string if err := node.Decode(&platform); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } if err := p.parsePlatform(platform); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } return nil } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into platform", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("platform") } // parsePlatform takes a string representing an OS/Arch combination (or either on their own) diff --git a/taskfile/ast/platforms_test.go b/taskfile/ast/platforms_test.go index 677f9f3c..7f600ffe 100644 --- a/taskfile/ast/platforms_test.go +++ b/taskfile/ast/platforms_test.go @@ -26,10 +26,10 @@ func TestPlatformParsing(t *testing.T) { {Input: "windows/amd64", ExpectedOS: "windows", ExpectedArch: "amd64"}, {Input: "windows/arm64", ExpectedOS: "windows", ExpectedArch: "arm64"}, - {Input: "invalid", Error: `task: Invalid platform "invalid"`}, - {Input: "invalid/invalid", Error: `task: Invalid platform "invalid/invalid"`}, - {Input: "windows/invalid", Error: `task: Invalid platform "windows/invalid"`}, - {Input: "invalid/amd64", Error: `task: Invalid platform "invalid/amd64"`}, + {Input: "invalid", Error: `invalid platform "invalid"`}, + {Input: "invalid/invalid", Error: `invalid platform "invalid/invalid"`}, + {Input: "windows/invalid", Error: `invalid platform "windows/invalid"`}, + {Input: "invalid/amd64", Error: `invalid platform "invalid/amd64"`}, } for _, test := range tests { diff --git a/taskfile/ast/precondition.go b/taskfile/ast/precondition.go index ec10866d..275144c9 100644 --- a/taskfile/ast/precondition.go +++ b/taskfile/ast/precondition.go @@ -1,14 +1,12 @@ package ast import ( - "errors" "fmt" "gopkg.in/yaml.v3" -) -// ErrCantUnmarshalPrecondition is returned for invalid precond YAML. -var ErrCantUnmarshalPrecondition = errors.New("task: Can't unmarshal precondition value") + "github.com/go-task/task/v3/errors" +) // Precondition represents a precondition necessary for a task to run type Precondition struct { @@ -33,7 +31,7 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error { case yaml.ScalarNode: var cmd string if err := node.Decode(&cmd); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } p.Sh = cmd p.Msg = fmt.Sprintf("`%s` failed", cmd) @@ -45,7 +43,7 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error { Msg string } if err := node.Decode(&sh); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } p.Sh = sh.Sh p.Msg = sh.Msg @@ -55,5 +53,5 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error { return nil } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into precondition", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("precondition") } diff --git a/taskfile/ast/task.go b/taskfile/ast/task.go index 7b2ea589..e749f73c 100644 --- a/taskfile/ast/task.go +++ b/taskfile/ast/task.go @@ -7,6 +7,7 @@ import ( "gopkg.in/yaml.v3" + "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) @@ -83,7 +84,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { case yaml.ScalarNode: var cmd Cmd if err := node.Decode(&cmd); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } t.Cmds = append(t.Cmds, &cmd) return nil @@ -92,7 +93,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { case yaml.SequenceNode: var cmds []*Cmd if err := node.Decode(&cmds); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } t.Cmds = cmds return nil @@ -130,11 +131,11 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { Watch bool } if err := node.Decode(&task); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } if task.Cmd != nil { if task.Cmds != nil { - return fmt.Errorf("yaml: line %d: task cannot have both cmd and cmds", node.Line) + return errors.NewTaskfileDecodeError(nil, node).WithMessage("task cannot have both cmd and cmds") } t.Cmds = []*Cmd{task.Cmd} } else { @@ -169,7 +170,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { return nil } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into task", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("task") } // DeepCopy creates a new instance of Task and copies diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go index f8957220..42a8486a 100644 --- a/taskfile/ast/taskfile.go +++ b/taskfile/ast/taskfile.go @@ -1,12 +1,13 @@ package ast import ( - "errors" "fmt" "time" "github.com/Masterminds/semver/v3" "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/errors" ) // NamespaceSeparator contains the character that separates namespaces @@ -77,7 +78,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { Interval time.Duration } if err := node.Decode(&taskfile); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } tf.Version = taskfile.Version tf.Output = taskfile.Output @@ -101,5 +102,5 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { return nil } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into taskfile", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("taskfile") } diff --git a/taskfile/ast/tasks.go b/taskfile/ast/tasks.go index 44dcd9d8..a400f9c1 100644 --- a/taskfile/ast/tasks.go +++ b/taskfile/ast/tasks.go @@ -6,6 +6,7 @@ import ( "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" ) @@ -118,7 +119,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { case yaml.MappingNode: tasks := omap.New[string, *Task]() if err := node.Decode(&tasks); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } // nolint: errcheck @@ -150,7 +151,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { return nil } - return fmt.Errorf("yaml: line %d: cannot unmarshal %s into tasks", node.Line, node.ShortTag()) + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("tasks") } func taskNameWithNamespace(taskName string, namespace string) string { diff --git a/taskfile/ast/var.go b/taskfile/ast/var.go index 9ea1b566..ee037cbc 100644 --- a/taskfile/ast/var.go +++ b/taskfile/ast/var.go @@ -1,11 +1,11 @@ package ast import ( - "fmt" "strings" "gopkg.in/yaml.v3" + "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/omap" ) @@ -95,7 +95,7 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error { if experiments.MapVariables.Value == "1" { var value any if err := node.Decode(&value); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } // If the value is a string and it starts with $, then it's a shell command if str, ok := value.(string); ok { @@ -123,7 +123,7 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error { Yaml string } if err := node.Decode(&m); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } v.Sh = m.Sh v.Ref = m.Ref @@ -132,12 +132,12 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error { v.Yaml = m.Yaml return nil default: - return fmt.Errorf(`yaml: line %d: %q is not a valid variable type. Try "sh", "ref", "map", "json", "yaml" or using a scalar value`, node.Line, key) + return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map", "json", "yaml" or using a scalar value`, key) } default: var value any if err := node.Decode(&value); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } v.Value = value return nil @@ -149,13 +149,13 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error { case yaml.MappingNode: if len(node.Content) > 2 || node.Content[0].Value != "sh" { - return fmt.Errorf(`task: line %d: maps cannot be assigned to variables`, node.Line) + return errors.NewTaskfileDecodeError(nil, node).WithMessage("maps cannot be assigned to variables") } var sh struct { Sh string } if err := node.Decode(&sh); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } v.Sh = sh.Sh return nil @@ -163,7 +163,7 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error { default: var value any if err := node.Decode(&value); err != nil { - return err + return errors.NewTaskfileDecodeError(err, node) } v.Value = value return nil diff --git a/taskfile/reader.go b/taskfile/reader.go index 70b22993..7899ee09 100644 --- a/taskfile/reader.go +++ b/taskfile/reader.go @@ -265,6 +265,11 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) { var tf ast.Taskfile if err := yaml.Unmarshal(b, &tf); err != nil { + // Decode the taskfile and add the file info the any errors + taskfileInvalidErr := &errors.TaskfileDecodeError{} + if errors.As(err, &taskfileInvalidErr) { + return nil, taskfileInvalidErr.WithFileInfo(node.Location(), b, 2) + } return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} }