Skip to content

Commit cdb6a3f

Browse files
authored
feat: decoding improvements (go-task#2068)
* refactor: moved/simplified snippets into its own file with tests * refactor: move snippet to taskfile package * feat: support snippets with line/col = 0 * feat: functional options for snippets * feat: added option to hide snippet indicators * feat: store raw lines for length calculations * feat: add debug function for TaskfileDecodeError * fix: decode errors from commands * fix: schema for defer cmd calls * fix: linting issues * refactor: split var and vars into different files like other structures
1 parent fb27318 commit cdb6a3f

File tree

10 files changed

+753
-282
lines changed

10 files changed

+753
-282
lines changed

errors/error_taskfile_decode.go

+36-77
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,28 @@ package errors
22

33
import (
44
"bytes"
5-
"embed"
5+
"cmp"
66
"errors"
77
"fmt"
88
"regexp"
99
"strings"
1010

11-
"github.com/alecthomas/chroma/v2"
12-
"github.com/alecthomas/chroma/v2/quick"
13-
"github.com/alecthomas/chroma/v2/styles"
1411
"github.com/fatih/color"
1512
"gopkg.in/yaml.v3"
1613
)
1714

18-
//go:embed themes/*.xml
19-
var embedded embed.FS
20-
2115
var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
2216

23-
func init() {
24-
r, err := embedded.Open("themes/task.xml")
25-
if err != nil {
26-
panic(err)
27-
}
28-
style, err := chroma.NewXMLStyle(r)
29-
if err != nil {
30-
panic(err)
31-
}
32-
styles.Register(style)
33-
}
34-
3517
type (
3618
TaskfileDecodeError struct {
3719
Message string
3820
Location string
3921
Line int
4022
Column int
4123
Tag string
42-
Snippet TaskfileSnippet
24+
Snippet string
4325
Err error
4426
}
45-
TaskfileSnippet struct {
46-
Lines []string
47-
StartLine int
48-
EndLine int
49-
Padding int
50-
}
5127
)
5228

5329
func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError {
@@ -88,38 +64,44 @@ func (err *TaskfileDecodeError) Error() string {
8864
}
8965
}
9066
fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column))
67+
fmt.Fprint(buf, err.Snippet)
68+
return buf.String()
69+
}
9170

92-
// Print the snippet
93-
maxLineNumberDigits := digits(err.Snippet.EndLine)
94-
lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits)
95-
columnSpacer := strings.Repeat(" ", err.Column-1)
96-
for i, line := range err.Snippet.Lines {
97-
currentLine := err.Snippet.StartLine + i + 1
71+
func (err *TaskfileDecodeError) Debug() string {
72+
const indentWidth = 2
73+
buf := &bytes.Buffer{}
74+
fmt.Fprintln(buf, "TaskfileDecodeError:")
9875

99-
lineIndicator := " "
100-
if currentLine == err.Line {
101-
lineIndicator = ">"
102-
}
103-
columnIndicator := "^"
104-
105-
// Print each line
106-
lineIndicator = color.RedString(lineIndicator)
107-
columnIndicator = color.RedString(columnIndicator)
108-
lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits)
109-
lineNumber := fmt.Sprintf(lineNumberFormat, currentLine)
110-
fmt.Fprintf(buf, "%s %s | %s", lineIndicator, lineNumber, line)
111-
112-
// Print the column indicator
113-
if currentLine == err.Line {
114-
fmt.Fprintf(buf, "\n %s | %s%s", lineNumberSpacer, columnSpacer, columnIndicator)
76+
// Recursively loop through the error chain and print any details
77+
var debug func(error, int)
78+
debug = func(err error, indent int) {
79+
indentStr := strings.Repeat(" ", indent*indentWidth)
80+
81+
// Nothing left to unwrap
82+
if err == nil {
83+
fmt.Fprintf(buf, "%sEnd of chain\n", indentStr)
84+
return
11585
}
11686

117-
// If there are more lines to print, add a newline
118-
if i < len(err.Snippet.Lines)-1 {
119-
fmt.Fprintln(buf)
87+
// Taskfile decode error
88+
decodeErr := &TaskfileDecodeError{}
89+
if errors.As(err, &decodeErr) {
90+
fmt.Fprintf(buf, "%s%s (%s:%d:%d)\n",
91+
indentStr,
92+
cmp.Or(decodeErr.Message, "<no_message>"),
93+
decodeErr.Location,
94+
decodeErr.Line,
95+
decodeErr.Column,
96+
)
97+
debug(errors.Unwrap(err), indent+1)
98+
return
12099
}
121-
}
122100

101+
fmt.Fprintf(buf, "%s%s\n", indentStr, err)
102+
debug(errors.Unwrap(err), indent+1)
103+
}
104+
debug(err, 0)
123105
return buf.String()
124106
}
125107

@@ -141,23 +123,9 @@ func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError {
141123
return err
142124
}
143125

144-
func (err *TaskfileDecodeError) WithFileInfo(location string, b []byte, padding int) *TaskfileDecodeError {
145-
buf := &bytes.Buffer{}
146-
if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil {
147-
buf.WriteString(string(b))
148-
}
149-
lines := strings.Split(buf.String(), "\n")
150-
start := max(err.Line-1-padding, 0)
151-
end := min(err.Line+padding, len(lines)-1)
152-
126+
func (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *TaskfileDecodeError {
153127
err.Location = location
154-
err.Snippet = TaskfileSnippet{
155-
Lines: lines[start:end],
156-
StartLine: start,
157-
EndLine: end,
158-
Padding: padding,
159-
}
160-
128+
err.Snippet = snippet
161129
return err
162130
}
163131

@@ -168,12 +136,3 @@ func extractTypeErrorMessage(message string) string {
168136
}
169137
return message
170138
}
171-
172-
func digits(number int) int {
173-
count := 0
174-
for number != 0 {
175-
number /= 10
176-
count += 1
177-
}
178-
return count
179-
}

taskfile/ast/cmd.go

+37-41
Original file line numberDiff line numberDiff line change
@@ -51,64 +51,60 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
5151
return nil
5252

5353
case yaml.MappingNode:
54-
55-
// A command with additional options
5654
var cmdStruct struct {
5755
Cmd string
56+
Task string
5857
For *For
5958
Silent bool
6059
Set []string
6160
Shopt []string
61+
Vars *Vars
6262
IgnoreError bool `yaml:"ignore_error"`
63+
Defer *Defer
6364
Platforms []*Platform
6465
}
65-
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
66-
c.Cmd = cmdStruct.Cmd
67-
c.For = cmdStruct.For
68-
c.Silent = cmdStruct.Silent
69-
c.Set = cmdStruct.Set
70-
c.Shopt = cmdStruct.Shopt
71-
c.IgnoreError = cmdStruct.IgnoreError
72-
c.Platforms = cmdStruct.Platforms
73-
return nil
66+
if err := node.Decode(&cmdStruct); err != nil {
67+
return errors.NewTaskfileDecodeError(err, node)
7468
}
69+
if cmdStruct.Defer != nil {
7570

76-
// A deferred command
77-
var deferredCmd struct {
78-
Defer string
79-
Silent bool
80-
}
81-
if err := node.Decode(&deferredCmd); err == nil && deferredCmd.Defer != "" {
82-
c.Defer = true
83-
c.Cmd = deferredCmd.Defer
84-
c.Silent = deferredCmd.Silent
85-
return nil
86-
}
71+
// A deferred command
72+
if cmdStruct.Defer.Cmd != "" {
73+
c.Defer = true
74+
c.Cmd = cmdStruct.Defer.Cmd
75+
c.Silent = cmdStruct.Silent
76+
return nil
77+
}
8778

88-
// A deferred task call
89-
var deferredCall struct {
90-
Defer Call
91-
}
92-
if err := node.Decode(&deferredCall); err == nil && deferredCall.Defer.Task != "" {
93-
c.Defer = true
94-
c.Task = deferredCall.Defer.Task
95-
c.Vars = deferredCall.Defer.Vars
96-
c.Silent = deferredCall.Defer.Silent
79+
// A deferred task call
80+
if cmdStruct.Defer.Task != "" {
81+
c.Defer = true
82+
c.Task = cmdStruct.Defer.Task
83+
c.Vars = cmdStruct.Defer.Vars
84+
c.Silent = cmdStruct.Defer.Silent
85+
return nil
86+
}
9787
return nil
9888
}
9989

10090
// A task call
101-
var taskCall struct {
102-
Task string
103-
Vars *Vars
104-
For *For
105-
Silent bool
91+
if cmdStruct.Task != "" {
92+
c.Task = cmdStruct.Task
93+
c.Vars = cmdStruct.Vars
94+
c.For = cmdStruct.For
95+
c.Silent = cmdStruct.Silent
96+
return nil
10697
}
107-
if err := node.Decode(&taskCall); err == nil && taskCall.Task != "" {
108-
c.Task = taskCall.Task
109-
c.Vars = taskCall.Vars
110-
c.For = taskCall.For
111-
c.Silent = taskCall.Silent
98+
99+
// A command with additional options
100+
if cmdStruct.Cmd != "" {
101+
c.Cmd = cmdStruct.Cmd
102+
c.For = cmdStruct.For
103+
c.Silent = cmdStruct.Silent
104+
c.Set = cmdStruct.Set
105+
c.Shopt = cmdStruct.Shopt
106+
c.IgnoreError = cmdStruct.IgnoreError
107+
c.Platforms = cmdStruct.Platforms
112108
return nil
113109
}
114110

taskfile/ast/defer.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package ast
2+
3+
import (
4+
"gopkg.in/yaml.v3"
5+
6+
"github.com/go-task/task/v3/errors"
7+
)
8+
9+
type Defer struct {
10+
Cmd string
11+
Task string
12+
Vars *Vars
13+
Silent bool
14+
}
15+
16+
func (d *Defer) UnmarshalYAML(node *yaml.Node) error {
17+
switch node.Kind {
18+
19+
case yaml.ScalarNode:
20+
var cmd string
21+
if err := node.Decode(&cmd); err != nil {
22+
return errors.NewTaskfileDecodeError(err, node)
23+
}
24+
d.Cmd = cmd
25+
return nil
26+
27+
case yaml.MappingNode:
28+
var deferStruct struct {
29+
Defer string
30+
Task string
31+
Vars *Vars
32+
Silent bool
33+
}
34+
if err := node.Decode(&deferStruct); err != nil {
35+
return errors.NewTaskfileDecodeError(err, node)
36+
}
37+
d.Cmd = deferStruct.Defer
38+
d.Task = deferStruct.Task
39+
d.Vars = deferStruct.Vars
40+
d.Silent = deferStruct.Silent
41+
return nil
42+
}
43+
44+
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("defer")
45+
}

0 commit comments

Comments
 (0)