From 6e0e45aafb026d050a4267170413a82e29c652ed Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 18:16:31 +0000 Subject: [PATCH 01/11] refactor: moved/simplified snippets into its own file with tests --- errors/error_taskfile_decode.go | 88 +------------ errors/snippet.go | 113 +++++++++++++++++ errors/snippet_test.go | 216 ++++++++++++++++++++++++++++++++ taskfile/reader.go | 3 +- 4 files changed, 335 insertions(+), 85 deletions(-) create mode 100644 errors/snippet.go create mode 100644 errors/snippet_test.go diff --git a/errors/error_taskfile_decode.go b/errors/error_taskfile_decode.go index a17cf1633d..6f298bd6f7 100644 --- a/errors/error_taskfile_decode.go +++ b/errors/error_taskfile_decode.go @@ -2,36 +2,16 @@ 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 @@ -39,15 +19,9 @@ type ( Line int Column int Tag string - Snippet TaskfileSnippet + Snippet *Snippet Err error } - TaskfileSnippet struct { - Lines []string - StartLine int - EndLine int - Padding int - } ) func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError { @@ -88,38 +62,7 @@ func (err *TaskfileDecodeError) Error() string { } } 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) - } - } - + fmt.Fprint(buf, err.Snippet.String()) return buf.String() } @@ -141,23 +84,9 @@ func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError { 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) - +func (err *TaskfileDecodeError) WithFileInfo(location string, snippet *Snippet) *TaskfileDecodeError { err.Location = location - err.Snippet = TaskfileSnippet{ - Lines: lines[start:end], - StartLine: start, - EndLine: end, - Padding: padding, - } - + err.Snippet = snippet return err } @@ -168,12 +97,3 @@ func extractTypeErrorMessage(message string) string { } return message } - -func digits(number int) int { - count := 0 - for number != 0 { - number /= 10 - count += 1 - } - return count -} diff --git a/errors/snippet.go b/errors/snippet.go new file mode 100644 index 0000000000..c840ae8db6 --- /dev/null +++ b/errors/snippet.go @@ -0,0 +1,113 @@ +package errors + +import ( + "bytes" + "embed" + "fmt" + "strings" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/quick" + "github.com/alecthomas/chroma/v2/styles" + "github.com/fatih/color" +) + +//go:embed themes/*.xml +var embedded embed.FS + +const ( + lineIndicator = ">" + columnIndicator = "^" +) + +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 Snippet struct { + lines []string + start int + end int + line int + column int + padding int +} + +// NewSnippet creates a new snippet from a byte slice and a line and column +// number. The line and column numbers should be 1-indexed. For example, the +// first character in the file would be 1:1 (line 1, column 1). The padding +// determines the number of lines to include before and after the chosen line. +func NewSnippet(b []byte, line, column, padding int) *Snippet { + line = max(line, 1) + column = max(column, 1) + + // Syntax highlight the snippet + buf := &bytes.Buffer{} + if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil { + buf.WriteString(string(b)) + } + + // Work out the start and end lines of the snippet + lines := strings.Split(buf.String(), "\n") + start := max(line-padding, 1) + end := min(line+padding, len(lines)-1) + + // Return the snippet + return &Snippet{ + lines: lines[start-1 : end], + start: start, + end: end, + line: line, + column: column, + padding: padding, + } +} + +func (snippet *Snippet) String() string { + buf := &bytes.Buffer{} + + maxLineNumberDigits := digits(snippet.end) + lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits) + lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits) + lineIndicatorSpacer := strings.Repeat(" ", len(lineIndicator)) + columnSpacer := strings.Repeat(" ", snippet.column-1) + + // Loop over each line in the snippet + for i, line := range snippet.lines { + if i > 0 { + fmt.Fprintln(buf) + } + + currentLine := snippet.start + i + lineNumber := fmt.Sprintf(lineNumberFormat, currentLine) + + // If this is a padding line, print it as normal + if currentLine != snippet.line { + fmt.Fprintf(buf, "%s %s | %s", lineIndicatorSpacer, lineNumber, line) + continue + } + + // Otherwise, print the line with indicators + fmt.Fprintf(buf, "%s %s | %s", color.RedString(lineIndicator), lineNumber, line) + fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator)) + } + + return buf.String() +} + +func digits(number int) int { + count := 0 + for number != 0 { + number /= 10 + count += 1 + } + return count +} diff --git a/errors/snippet_test.go b/errors/snippet_test.go new file mode 100644 index 0000000000..44b14b8244 --- /dev/null +++ b/errors/snippet_test.go @@ -0,0 +1,216 @@ +package errors + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +const sample = `version: 3 + +tasks: + default: + vars: + FOO: foo + BAR: bar + cmds: + - echo "{{.FOO}}" + - echo "{{.BAR}}" +` + +func TestNewSnippet(t *testing.T) { + tests := []struct { + name string + b []byte + line int + column int + padding int + want *Snippet + }{ + { + name: "first line, first column", + b: []byte(sample), + line: 1, + column: 1, + padding: 0, + want: &Snippet{ + lines: []string{ + "\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + }, + start: 1, + end: 1, + line: 1, + column: 1, + padding: 0, + }, + }, + { + name: "first line, first column, padding=2", + b: []byte(sample), + line: 1, + column: 1, + padding: 2, + want: &Snippet{ + lines: []string{ + "\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + "\x1b[1m\x1b[30m\x1b[0m", + "\x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + }, + start: 1, + end: 3, + line: 1, + column: 1, + padding: 2, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewSnippet(tt.b, tt.line, tt.column, tt.padding) + require.Equal(t, tt.want, got) + }) + } +} + +func TestSnippetString(t *testing.T) { + tests := []struct { + name string + b []byte + line int + column int + padding int + want string + }{ + { + name: "empty", + b: []byte{}, + line: 1, + column: 1, + padding: 0, + want: "", + }, + { + name: "1st line, 1st column", + b: []byte(sample), + line: 1, + column: 1, + padding: 0, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + }, + { + name: "1st line, 10th column", + b: []byte(sample), + line: 1, + column: 10, + padding: 0, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + }, + { + name: "1st line, 1st column, padding=2", + b: []byte(sample), + line: 1, + column: 1, + padding: 2, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + }, + { + name: "1st line, 10th column, padding=2", + b: []byte(sample), + line: 1, + column: 10, + padding: 2, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + }, + { + name: "5th line, 1st column", + b: []byte(sample), + line: 5, + column: 1, + padding: 0, + want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + }, + { + name: "5th line, 5th column", + b: []byte(sample), + line: 5, + column: 5, + padding: 0, + want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + }, + { + name: "5th line, 5th column, padding=2", + b: []byte(sample), + line: 5, + column: 5, + padding: 2, + want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + }, + { + name: "10th line, 1st column", + b: []byte(sample), + line: 10, + column: 1, + padding: 0, + want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + }, + { + name: "10th line, 23rd column", + b: []byte(sample), + line: 10, + column: 23, + padding: 0, + want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + }, + { + name: "10th line, 24th column (out of bounds)", + b: []byte(sample), + line: 10, + column: 24, + padding: 0, + want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + }, + { + name: "10th line, 23rd column, padding=2", + b: []byte(sample), + line: 10, + column: 23, + padding: 2, + want: " 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + }, + { + name: "5th line, 5th column, padding=100", + b: []byte(sample), + line: 5, + column: 5, + padding: 100, + want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + }, + { + name: "11th line (out of bounds), 1st column", + b: []byte(sample), + line: 11, + column: 1, + padding: 0, + want: "", + }, + { + name: "11th line (out of bounds), 1st column, padding=2", + b: []byte(sample), + line: 11, + column: 1, + padding: 2, + want: " 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + snippet := NewSnippet(tt.b, tt.line, tt.column, tt.padding) + got := snippet.String() + if strings.Contains(got, "\t") { + t.Fatalf("tab character found in snippet - check the sample string") + } + require.Equal(t, tt.want, got) + }) + } +} diff --git a/taskfile/reader.go b/taskfile/reader.go index d6157067d9..c1a00ba646 100644 --- a/taskfile/reader.go +++ b/taskfile/reader.go @@ -195,7 +195,8 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) { // 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) + snippet := errors.NewSnippet(b, taskfileInvalidErr.Line, taskfileInvalidErr.Column, 2) + return nil, taskfileInvalidErr.WithFileInfo(node.Location(), snippet) } return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} } From 3a573d0b551c02d777e6085265fbaf5c04f6be97 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 18:36:06 +0000 Subject: [PATCH 02/11] refactor: move snippet to taskfile package --- errors/error_taskfile_decode.go | 6 +++--- taskfile/reader.go | 8 ++++---- {errors => taskfile}/snippet.go | 2 +- {errors => taskfile}/snippet_test.go | 2 +- {errors => taskfile}/themes/task.xml | 0 5 files changed, 9 insertions(+), 9 deletions(-) rename {errors => taskfile}/snippet.go (99%) rename {errors => taskfile}/snippet_test.go (99%) rename {errors => taskfile}/themes/task.xml (100%) diff --git a/errors/error_taskfile_decode.go b/errors/error_taskfile_decode.go index 6f298bd6f7..3a0548a593 100644 --- a/errors/error_taskfile_decode.go +++ b/errors/error_taskfile_decode.go @@ -19,7 +19,7 @@ type ( Line int Column int Tag string - Snippet *Snippet + Snippet string Err error } ) @@ -62,7 +62,7 @@ func (err *TaskfileDecodeError) Error() string { } } fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column)) - fmt.Fprint(buf, err.Snippet.String()) + fmt.Fprint(buf, err.Snippet) return buf.String() } @@ -84,7 +84,7 @@ func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError { return err } -func (err *TaskfileDecodeError) WithFileInfo(location string, snippet *Snippet) *TaskfileDecodeError { +func (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *TaskfileDecodeError { err.Location = location err.Snippet = snippet return err diff --git a/taskfile/reader.go b/taskfile/reader.go index c1a00ba646..f4828e09c4 100644 --- a/taskfile/reader.go +++ b/taskfile/reader.go @@ -193,10 +193,10 @@ 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) { - snippet := errors.NewSnippet(b, taskfileInvalidErr.Line, taskfileInvalidErr.Column, 2) - return nil, taskfileInvalidErr.WithFileInfo(node.Location(), snippet) + taskfileDecodeErr := &errors.TaskfileDecodeError{} + if errors.As(err, &taskfileDecodeErr) { + snippet := NewSnippet(b, taskfileDecodeErr.Line, taskfileDecodeErr.Column, 2) + return nil, taskfileDecodeErr.WithFileInfo(node.Location(), snippet.String()) } return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} } diff --git a/errors/snippet.go b/taskfile/snippet.go similarity index 99% rename from errors/snippet.go rename to taskfile/snippet.go index c840ae8db6..a0534b2d14 100644 --- a/errors/snippet.go +++ b/taskfile/snippet.go @@ -1,4 +1,4 @@ -package errors +package taskfile import ( "bytes" diff --git a/errors/snippet_test.go b/taskfile/snippet_test.go similarity index 99% rename from errors/snippet_test.go rename to taskfile/snippet_test.go index 44b14b8244..a5ce2f03ba 100644 --- a/errors/snippet_test.go +++ b/taskfile/snippet_test.go @@ -1,4 +1,4 @@ -package errors +package taskfile import ( "strings" diff --git a/errors/themes/task.xml b/taskfile/themes/task.xml similarity index 100% rename from errors/themes/task.xml rename to taskfile/themes/task.xml From 94eb46131b706fc2e4b2f9343a0419e698d62676 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 19:15:13 +0000 Subject: [PATCH 03/11] feat: support snippets with line/col = 0 --- taskfile/snippet.go | 12 ++++++++---- taskfile/snippet_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/taskfile/snippet.go b/taskfile/snippet.go index a0534b2d14..2475806bb9 100644 --- a/taskfile/snippet.go +++ b/taskfile/snippet.go @@ -46,9 +46,6 @@ type Snippet struct { // first character in the file would be 1:1 (line 1, column 1). The padding // determines the number of lines to include before and after the chosen line. func NewSnippet(b []byte, line, column, padding int) *Snippet { - line = max(line, 1) - column = max(column, 1) - // Syntax highlight the snippet buf := &bytes.Buffer{} if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil { @@ -78,7 +75,7 @@ func (snippet *Snippet) String() string { lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits) lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits) lineIndicatorSpacer := strings.Repeat(" ", len(lineIndicator)) - columnSpacer := strings.Repeat(" ", snippet.column-1) + columnSpacer := strings.Repeat(" ", max(snippet.column-1, 0)) // Loop over each line in the snippet for i, line := range snippet.lines { @@ -97,6 +94,13 @@ func (snippet *Snippet) String() string { // Otherwise, print the line with indicators fmt.Fprintf(buf, "%s %s | %s", color.RedString(lineIndicator), lineNumber, line) + if snippet.column > 0 { + fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator)) + } + } + + // If there are lines, but no line is selected, print the column indicator under all the lines + if len(snippet.lines) > 0 && snippet.line == 0 && snippet.column > 0 { fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator)) } diff --git a/taskfile/snippet_test.go b/taskfile/snippet_test.go index a5ce2f03ba..f3661f19d6 100644 --- a/taskfile/snippet_test.go +++ b/taskfile/snippet_test.go @@ -90,6 +90,38 @@ func TestSnippetString(t *testing.T) { padding: 0, want: "", }, + { + name: "0th line, 0th column (no indicators)", + b: []byte(sample), + line: 0, + column: 0, + padding: 0, + want: "", + }, + { + name: "1st line, 0th column (line indicator only)", + b: []byte(sample), + line: 1, + column: 0, + padding: 0, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + }, + { + name: "0th line, 1st column (column indicator only)", + b: []byte(sample), + line: 0, + column: 1, + padding: 0, + want: "", + }, + { + name: "0th line, 1st column, padding=2 (column indicator only)", + b: []byte(sample), + line: 0, + column: 1, + padding: 2, + want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n | ^", + }, { name: "1st line, 1st column", b: []byte(sample), From d79a5c962c7c9123ebdac09d3efada2aa987ca45 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 19:34:56 +0000 Subject: [PATCH 04/11] feat: functional options for snippets --- taskfile/reader.go | 6 +- taskfile/snippet.go | 63 ++++++--- taskfile/snippet_test.go | 294 +++++++++++++++++++++------------------ 3 files changed, 202 insertions(+), 161 deletions(-) diff --git a/taskfile/reader.go b/taskfile/reader.go index f4828e09c4..241770c7d0 100644 --- a/taskfile/reader.go +++ b/taskfile/reader.go @@ -195,7 +195,11 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) { // Decode the taskfile and add the file info the any errors taskfileDecodeErr := &errors.TaskfileDecodeError{} if errors.As(err, &taskfileDecodeErr) { - snippet := NewSnippet(b, taskfileDecodeErr.Line, taskfileDecodeErr.Column, 2) + snippet := NewSnippet(b, + SnippetWithLine(taskfileDecodeErr.Line), + SnippetWithColumn(taskfileDecodeErr.Column), + SnippetWithPadding(2), + ) return nil, taskfileDecodeErr.WithFileInfo(node.Location(), snippet.String()) } return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} diff --git a/taskfile/snippet.go b/taskfile/snippet.go index 2475806bb9..eb380f88e2 100644 --- a/taskfile/snippet.go +++ b/taskfile/snippet.go @@ -32,39 +32,58 @@ func init() { styles.Register(style) } -type Snippet struct { - lines []string - start int - end int - line int - column int - padding int -} +type ( + SnippetOption func(*Snippet) + Snippet struct { + lines []string + start int + end int + line int + column int + padding int + } +) // NewSnippet creates a new snippet from a byte slice and a line and column // number. The line and column numbers should be 1-indexed. For example, the // first character in the file would be 1:1 (line 1, column 1). The padding // determines the number of lines to include before and after the chosen line. -func NewSnippet(b []byte, line, column, padding int) *Snippet { - // Syntax highlight the snippet +func NewSnippet(b []byte, opts ...SnippetOption) *Snippet { + snippet := &Snippet{} + for _, opt := range opts { + opt(snippet) + } + + // Syntax highlight the input and split it into lines 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") // Work out the start and end lines of the snippet - lines := strings.Split(buf.String(), "\n") - start := max(line-padding, 1) - end := min(line+padding, len(lines)-1) - - // Return the snippet - return &Snippet{ - lines: lines[start-1 : end], - start: start, - end: end, - line: line, - column: column, - padding: padding, + snippet.start = max(snippet.line-snippet.padding, 1) + snippet.end = min(snippet.line+snippet.padding, len(lines)-1) + snippet.lines = lines[snippet.start-1 : snippet.end] + + return snippet +} + +func SnippetWithLine(line int) SnippetOption { + return func(snippet *Snippet) { + snippet.line = line + } +} + +func SnippetWithColumn(column int) SnippetOption { + return func(snippet *Snippet) { + snippet.column = column + } +} + +func SnippetWithPadding(padding int) SnippetOption { + return func(snippet *Snippet) { + snippet.padding = padding } } diff --git a/taskfile/snippet_test.go b/taskfile/snippet_test.go index f3661f19d6..49a1f22d2d 100644 --- a/taskfile/snippet_test.go +++ b/taskfile/snippet_test.go @@ -21,19 +21,18 @@ tasks: func TestNewSnippet(t *testing.T) { tests := []struct { - name string - b []byte - line int - column int - padding int - want *Snippet + name string + b []byte + opts []SnippetOption + want *Snippet }{ { - name: "first line, first column", - b: []byte(sample), - line: 1, - column: 1, - padding: 0, + name: "first line, first column", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(1), + SnippetWithColumn(1), + }, want: &Snippet{ lines: []string{ "\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", @@ -46,11 +45,13 @@ func TestNewSnippet(t *testing.T) { }, }, { - name: "first line, first column, padding=2", - b: []byte(sample), - line: 1, - column: 1, - padding: 2, + name: "first line, first column, padding=2", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(1), + SnippetWithColumn(1), + SnippetWithPadding(2), + }, want: &Snippet{ lines: []string{ "\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", @@ -67,7 +68,7 @@ func TestNewSnippet(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := NewSnippet(tt.b, tt.line, tt.column, tt.padding) + got := NewSnippet(tt.b, tt.opts...) require.Equal(t, tt.want, got) }) } @@ -75,169 +76,186 @@ func TestNewSnippet(t *testing.T) { func TestSnippetString(t *testing.T) { tests := []struct { - name string - b []byte - line int - column int - padding int - want string + name string + b []byte + opts []SnippetOption + want string }{ { - name: "empty", - b: []byte{}, - line: 1, - column: 1, - padding: 0, - want: "", + name: "empty", + b: []byte{}, + opts: []SnippetOption{ + SnippetWithLine(1), + SnippetWithColumn(1), + }, + want: "", }, { - name: "0th line, 0th column (no indicators)", - b: []byte(sample), - line: 0, - column: 0, - padding: 0, - want: "", + name: "0th line, 0th column (no indicators)", + b: []byte(sample), + want: "", }, { - name: "1st line, 0th column (line indicator only)", - b: []byte(sample), - line: 1, - column: 0, - padding: 0, - want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + name: "1st line, 0th column (line indicator only)", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(1), + }, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { - name: "0th line, 1st column (column indicator only)", - b: []byte(sample), - line: 0, - column: 1, - padding: 0, - want: "", + name: "0th line, 1st column (column indicator only)", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithColumn(1), + }, + want: "", }, { - name: "0th line, 1st column, padding=2 (column indicator only)", - b: []byte(sample), - line: 0, - column: 1, - padding: 2, - want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n | ^", + name: "0th line, 1st column, padding=2 (column indicator only)", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithColumn(1), + SnippetWithPadding(2), + }, + want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n | ^", }, { - name: "1st line, 1st column", - b: []byte(sample), - line: 1, - column: 1, - padding: 0, - want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + name: "1st line, 1st column", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(1), + SnippetWithColumn(1), + }, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { - name: "1st line, 10th column", - b: []byte(sample), - line: 1, - column: 10, - padding: 0, - want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + name: "1st line, 10th column", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(1), + SnippetWithColumn(10), + }, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { - name: "1st line, 1st column, padding=2", - b: []byte(sample), - line: 1, - column: 1, - padding: 2, - want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + name: "1st line, 1st column, padding=2", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(1), + SnippetWithColumn(1), + SnippetWithPadding(2), + }, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { - name: "1st line, 10th column, padding=2", - b: []byte(sample), - line: 1, - column: 10, - padding: 2, - want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + name: "1st line, 10th column, padding=2", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(1), + SnippetWithColumn(10), + SnippetWithPadding(2), + }, + want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { - name: "5th line, 1st column", - b: []byte(sample), - line: 5, - column: 1, - padding: 0, - want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + name: "5th line, 1st column", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(5), + SnippetWithColumn(1), + }, + want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { - name: "5th line, 5th column", - b: []byte(sample), - line: 5, - column: 5, - padding: 0, - want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + name: "5th line, 5th column", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(5), + SnippetWithColumn(5), + }, + want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { - name: "5th line, 5th column, padding=2", - b: []byte(sample), - line: 5, - column: 5, - padding: 2, - want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + name: "5th line, 5th column, padding=2", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(5), + SnippetWithColumn(5), + SnippetWithPadding(2), + }, + want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { - name: "10th line, 1st column", - b: []byte(sample), - line: 10, - column: 1, - padding: 0, - want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + name: "10th line, 1st column", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(10), + SnippetWithColumn(1), + }, + want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { - name: "10th line, 23rd column", - b: []byte(sample), - line: 10, - column: 23, - padding: 0, - want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + name: "10th line, 23rd column", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(10), + SnippetWithColumn(23), + }, + want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { - name: "10th line, 24th column (out of bounds)", - b: []byte(sample), - line: 10, - column: 24, - padding: 0, - want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + name: "10th line, 24th column (out of bounds)", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(10), + SnippetWithColumn(24), + }, + want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { - name: "10th line, 23rd column, padding=2", - b: []byte(sample), - line: 10, - column: 23, - padding: 2, - want: " 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + name: "10th line, 23rd column, padding=2", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(10), + SnippetWithColumn(23), + SnippetWithPadding(2), + }, + want: " 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { - name: "5th line, 5th column, padding=100", - b: []byte(sample), - line: 5, - column: 5, - padding: 100, - want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + name: "5th line, 5th column, padding=100", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(5), + SnippetWithColumn(5), + SnippetWithPadding(100), + }, + want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { - name: "11th line (out of bounds), 1st column", - b: []byte(sample), - line: 11, - column: 1, - padding: 0, - want: "", + name: "11th line (out of bounds), 1st column", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(11), + SnippetWithColumn(1), + }, + want: "", }, { - name: "11th line (out of bounds), 1st column, padding=2", - b: []byte(sample), - line: 11, - column: 1, - padding: 2, - want: " 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + name: "11th line (out of bounds), 1st column, padding=2", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(11), + SnippetWithColumn(1), + SnippetWithPadding(2), + }, + want: " 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - snippet := NewSnippet(tt.b, tt.line, tt.column, tt.padding) + snippet := NewSnippet(tt.b, tt.opts...) got := snippet.String() if strings.Contains(got, "\t") { t.Fatalf("tab character found in snippet - check the sample string") From e0cd787c8a73d2fc5ead69287e3d9c2a4ab66a74 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 19:37:08 +0000 Subject: [PATCH 05/11] feat: added option to hide snippet indicators --- taskfile/snippet.go | 23 +++++++++++++++-------- taskfile/snippet_test.go | 11 +++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/taskfile/snippet.go b/taskfile/snippet.go index eb380f88e2..fab446a93e 100644 --- a/taskfile/snippet.go +++ b/taskfile/snippet.go @@ -35,12 +35,13 @@ func init() { type ( SnippetOption func(*Snippet) Snippet struct { - lines []string - start int - end int - line int - column int - padding int + lines []string + start int + end int + line int + column int + padding int + noIndicators bool } ) @@ -87,6 +88,12 @@ func SnippetWithPadding(padding int) SnippetOption { } } +func SnippetWithNoIndicators() SnippetOption { + return func(snippet *Snippet) { + snippet.noIndicators = true + } +} + func (snippet *Snippet) String() string { buf := &bytes.Buffer{} @@ -105,8 +112,8 @@ func (snippet *Snippet) String() string { currentLine := snippet.start + i lineNumber := fmt.Sprintf(lineNumberFormat, currentLine) - // If this is a padding line, print it as normal - if currentLine != snippet.line { + // If this is a padding line or indicators are disabled, print it as normal + if currentLine != snippet.line || snippet.noIndicators { fmt.Fprintf(buf, "%s %s | %s", lineIndicatorSpacer, lineNumber, line) continue } diff --git a/taskfile/snippet_test.go b/taskfile/snippet_test.go index 49a1f22d2d..5ae224ed36 100644 --- a/taskfile/snippet_test.go +++ b/taskfile/snippet_test.go @@ -186,6 +186,17 @@ func TestSnippetString(t *testing.T) { }, want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, + { + name: "5th line, 5th column, padding=2, no indicators", + b: []byte(sample), + opts: []SnippetOption{ + SnippetWithLine(5), + SnippetWithColumn(5), + SnippetWithPadding(2), + SnippetWithNoIndicators(), + }, + want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m", + }, { name: "10th line, 1st column", b: []byte(sample), From 7bd5fca8660b74b247dccd008a540e1062aa0688 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 19:43:48 +0000 Subject: [PATCH 06/11] feat: store raw lines for length calculations --- taskfile/snippet.go | 35 ++++++++++++++++++++--------------- taskfile/snippet_test.go | 14 +++++++++++--- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/taskfile/snippet.go b/taskfile/snippet.go index fab446a93e..2db6205d11 100644 --- a/taskfile/snippet.go +++ b/taskfile/snippet.go @@ -35,13 +35,14 @@ func init() { type ( SnippetOption func(*Snippet) Snippet struct { - lines []string - start int - end int - line int - column int - padding int - noIndicators bool + linesRaw []string + linesHighlighted []string + start int + end int + line int + column int + padding int + noIndicators bool } ) @@ -60,12 +61,14 @@ func NewSnippet(b []byte, opts ...SnippetOption) *Snippet { if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil { buf.WriteString(string(b)) } - lines := strings.Split(buf.String(), "\n") + linesRaw := strings.Split(string(b), "\n") + linesHighlighted := strings.Split(buf.String(), "\n") // Work out the start and end lines of the snippet snippet.start = max(snippet.line-snippet.padding, 1) - snippet.end = min(snippet.line+snippet.padding, len(lines)-1) - snippet.lines = lines[snippet.start-1 : snippet.end] + snippet.end = min(snippet.line+snippet.padding, len(linesRaw)-1) + snippet.linesRaw = linesRaw[snippet.start-1 : snippet.end] + snippet.linesHighlighted = linesHighlighted[snippet.start-1 : snippet.end] return snippet } @@ -104,7 +107,7 @@ func (snippet *Snippet) String() string { columnSpacer := strings.Repeat(" ", max(snippet.column-1, 0)) // Loop over each line in the snippet - for i, line := range snippet.lines { + for i, lineHighlighted := range snippet.linesHighlighted { if i > 0 { fmt.Fprintln(buf) } @@ -114,19 +117,21 @@ func (snippet *Snippet) String() string { // If this is a padding line or indicators are disabled, print it as normal if currentLine != snippet.line || snippet.noIndicators { - fmt.Fprintf(buf, "%s %s | %s", lineIndicatorSpacer, lineNumber, line) + fmt.Fprintf(buf, "%s %s | %s", lineIndicatorSpacer, lineNumber, lineHighlighted) continue } // Otherwise, print the line with indicators - fmt.Fprintf(buf, "%s %s | %s", color.RedString(lineIndicator), lineNumber, line) - if snippet.column > 0 { + fmt.Fprintf(buf, "%s %s | %s", color.RedString(lineIndicator), lineNumber, lineHighlighted) + + // Only print the column indicator if the column is in bounds + if snippet.column > 0 && snippet.column <= len(snippet.linesRaw[i]) { fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator)) } } // If there are lines, but no line is selected, print the column indicator under all the lines - if len(snippet.lines) > 0 && snippet.line == 0 && snippet.column > 0 { + if len(snippet.linesHighlighted) > 0 && snippet.line == 0 && snippet.column > 0 { fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator)) } diff --git a/taskfile/snippet_test.go b/taskfile/snippet_test.go index 5ae224ed36..96244f8707 100644 --- a/taskfile/snippet_test.go +++ b/taskfile/snippet_test.go @@ -34,7 +34,10 @@ func TestNewSnippet(t *testing.T) { SnippetWithColumn(1), }, want: &Snippet{ - lines: []string{ + linesRaw: []string{ + "version: 3", + }, + linesHighlighted: []string{ "\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, start: 1, @@ -53,7 +56,12 @@ func TestNewSnippet(t *testing.T) { SnippetWithPadding(2), }, want: &Snippet{ - lines: []string{ + linesRaw: []string{ + "version: 3", + "", + "tasks:", + }, + linesHighlighted: []string{ "\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", "\x1b[1m\x1b[30m\x1b[0m", "\x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", @@ -222,7 +230,7 @@ func TestSnippetString(t *testing.T) { SnippetWithLine(10), SnippetWithColumn(24), }, - want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", + want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { name: "10th line, 23rd column, padding=2", From b0637c5a2e84645f8da3e8cfbf501e801a521f60 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 21:36:20 +0000 Subject: [PATCH 07/11] feat: add debug function for TaskfileDecodeError --- errors/error_taskfile_decode.go | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/errors/error_taskfile_decode.go b/errors/error_taskfile_decode.go index 3a0548a593..3176215733 100644 --- a/errors/error_taskfile_decode.go +++ b/errors/error_taskfile_decode.go @@ -2,9 +2,11 @@ package errors import ( "bytes" + "cmp" "errors" "fmt" "regexp" + "strings" "github.com/fatih/color" "gopkg.in/yaml.v3" @@ -66,6 +68,43 @@ func (err *TaskfileDecodeError) Error() string { return buf.String() } +func (err *TaskfileDecodeError) Debug() string { + const indentWidth = 2 + buf := &bytes.Buffer{} + fmt.Fprintln(buf, "TaskfileDecodeError:") + + // Recursively loop through the error chain and print any details + var debug func(error, int) + debug = func(err error, indent int) { + indentStr := strings.Repeat(" ", indent*indentWidth) + + // Nothing left to unwrap + if err == nil { + fmt.Fprintf(buf, "%sEnd of chain\n", indentStr) + return + } + + // Taskfile decode error + decodeErr := &TaskfileDecodeError{} + if errors.As(err, &decodeErr) { + fmt.Fprintf(buf, "%s%s (%s:%d:%d)\n", + indentStr, + cmp.Or(decodeErr.Message, ""), + decodeErr.Location, + decodeErr.Line, + decodeErr.Column, + ) + debug(errors.Unwrap(err), indent+1) + return + } + + fmt.Fprintf(buf, "%s%s\n", indentStr, err) + debug(errors.Unwrap(err), indent+1) + } + debug(err, 0) + return buf.String() +} + func (err *TaskfileDecodeError) Unwrap() error { return err.Err } From 22eb0fb650709667722131bf3ec61b0c641814ec Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 22:41:43 +0000 Subject: [PATCH 08/11] fix: decode errors from commands --- taskfile/ast/cmd.go | 78 ++++++++++++++++++++----------------------- taskfile/ast/defer.go | 45 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 41 deletions(-) create mode 100644 taskfile/ast/defer.go diff --git a/taskfile/ast/cmd.go b/taskfile/ast/cmd.go index b36e05beb1..3dab193c55 100644 --- a/taskfile/ast/cmd.go +++ b/taskfile/ast/cmd.go @@ -51,64 +51,60 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error { return nil case yaml.MappingNode: - - // A command with additional options var cmdStruct struct { Cmd string + Task string For *For Silent bool Set []string Shopt []string + Vars *Vars IgnoreError bool `yaml:"ignore_error"` + Defer *Defer Platforms []*Platform } - if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" { - c.Cmd = cmdStruct.Cmd - c.For = cmdStruct.For - c.Silent = cmdStruct.Silent - c.Set = cmdStruct.Set - c.Shopt = cmdStruct.Shopt - c.IgnoreError = cmdStruct.IgnoreError - c.Platforms = cmdStruct.Platforms - return nil + if err := node.Decode(&cmdStruct); err != nil { + return errors.NewTaskfileDecodeError(err, node) } + if cmdStruct.Defer != nil { - // A deferred command - var deferredCmd struct { - Defer string - Silent bool - } - if err := node.Decode(&deferredCmd); err == nil && deferredCmd.Defer != "" { - c.Defer = true - c.Cmd = deferredCmd.Defer - c.Silent = deferredCmd.Silent - return nil - } + // A deferred command + if cmdStruct.Defer.Cmd != "" { + c.Defer = true + c.Cmd = cmdStruct.Defer.Cmd + c.Silent = cmdStruct.Silent + return nil + } - // A deferred task call - var deferredCall struct { - Defer Call - } - if err := node.Decode(&deferredCall); err == nil && deferredCall.Defer.Task != "" { - c.Defer = true - c.Task = deferredCall.Defer.Task - c.Vars = deferredCall.Defer.Vars - c.Silent = deferredCall.Defer.Silent + // A deferred task call + if cmdStruct.Defer.Task != "" { + c.Defer = true + c.Task = cmdStruct.Defer.Task + c.Vars = cmdStruct.Defer.Vars + c.Silent = cmdStruct.Defer.Silent + return nil + } return nil } // A task call - var taskCall struct { - Task string - Vars *Vars - For *For - Silent bool + if cmdStruct.Task != "" { + c.Task = cmdStruct.Task + c.Vars = cmdStruct.Vars + c.For = cmdStruct.For + c.Silent = cmdStruct.Silent + return nil } - if err := node.Decode(&taskCall); err == nil && taskCall.Task != "" { - c.Task = taskCall.Task - c.Vars = taskCall.Vars - c.For = taskCall.For - c.Silent = taskCall.Silent + + // A command with additional options + if cmdStruct.Cmd != "" { + c.Cmd = cmdStruct.Cmd + c.For = cmdStruct.For + c.Silent = cmdStruct.Silent + c.Set = cmdStruct.Set + c.Shopt = cmdStruct.Shopt + c.IgnoreError = cmdStruct.IgnoreError + c.Platforms = cmdStruct.Platforms return nil } diff --git a/taskfile/ast/defer.go b/taskfile/ast/defer.go new file mode 100644 index 0000000000..5705de445d --- /dev/null +++ b/taskfile/ast/defer.go @@ -0,0 +1,45 @@ +package ast + +import ( + "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/errors" +) + +type Defer struct { + Cmd string + Task string + Vars *Vars + Silent bool +} + +func (d *Defer) UnmarshalYAML(node *yaml.Node) error { + switch node.Kind { + + case yaml.ScalarNode: + var cmd string + if err := node.Decode(&cmd); err != nil { + return errors.NewTaskfileDecodeError(err, node) + } + d.Cmd = cmd + return nil + + case yaml.MappingNode: + var deferStruct struct { + Defer string + Task string + Vars *Vars + Silent bool + } + if err := node.Decode(&deferStruct); err != nil { + return errors.NewTaskfileDecodeError(err, node) + } + d.Cmd = deferStruct.Defer + d.Task = deferStruct.Task + d.Vars = deferStruct.Vars + d.Silent = deferStruct.Silent + return nil + } + + return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("defer") +} From eb5f147909feb9a96eb4a08d2cedb8bd721c45d3 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 22:42:05 +0000 Subject: [PATCH 09/11] fix: schema for defer cmd calls --- website/static/schema.json | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/website/static/schema.json b/website/static/schema.json index da25a209f9..a44df7a8d3 100644 --- a/website/static/schema.json +++ b/website/static/schema.json @@ -29,6 +29,12 @@ }, { "$ref": "#/definitions/task_call" + }, + { + "$ref": "#/definitions/defer_task_call" + }, + { + "$ref": "#/definitions/defer_cmd_call" } ] } @@ -216,7 +222,10 @@ "$ref": "#/definitions/task_call" }, { - "$ref": "#/definitions/defer_call" + "$ref": "#/definitions/defer_task_call" + }, + { + "$ref": "#/definitions/defer_cmd_call" }, { "$ref": "#/definitions/for_cmds_call" @@ -350,15 +359,12 @@ "additionalProperties": false, "required": ["cmd"] }, - "defer_call": { + "defer_task_call": { "type": "object", "properties": { "defer": { "description": "Run a command when the task completes. This command will run even when the task fails", "anyOf": [ - { - "type": "string" - }, { "$ref": "#/definitions/task_call" } @@ -368,6 +374,21 @@ "additionalProperties": false, "required": ["defer"] }, + "defer_cmd_call": { + "type": "object", + "properties": { + "defer": { + "description": "Name of the command to defer", + "type": "string" + }, + "silent": { + "description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`.", + "type": "boolean" + } + }, + "additionalProperties": false, + "required": ["defer"] + }, "for_cmds_call": { "type": "object", "properties": { From 99867b2cea94f9c5d42ecfc3643ecca43dfc4b2f Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 23:26:26 +0000 Subject: [PATCH 10/11] fix: linting issues --- taskfile/snippet_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/taskfile/snippet_test.go b/taskfile/snippet_test.go index 96244f8707..d99e140c11 100644 --- a/taskfile/snippet_test.go +++ b/taskfile/snippet_test.go @@ -20,6 +20,7 @@ tasks: ` func TestNewSnippet(t *testing.T) { + t.Parallel() tests := []struct { name string b []byte @@ -76,6 +77,7 @@ func TestNewSnippet(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() got := NewSnippet(tt.b, tt.opts...) require.Equal(t, tt.want, got) }) @@ -83,6 +85,7 @@ func TestNewSnippet(t *testing.T) { } func TestSnippetString(t *testing.T) { + t.Parallel() tests := []struct { name string b []byte @@ -274,6 +277,7 @@ func TestSnippetString(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() snippet := NewSnippet(tt.b, tt.opts...) got := snippet.String() if strings.Contains(got, "\t") { From f6683e6b7627127838e9ff08d725e8d3ef4d129c Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 17 Feb 2025 23:52:01 +0000 Subject: [PATCH 11/11] refactor: split var and vars into different files like other structures --- taskfile/ast/var.go | 156 ---------------------------------------- taskfile/ast/vars.go | 164 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 156 deletions(-) create mode 100644 taskfile/ast/vars.go diff --git a/taskfile/ast/var.go b/taskfile/ast/var.go index 486741a1d6..4237412d96 100644 --- a/taskfile/ast/var.go +++ b/taskfile/ast/var.go @@ -2,169 +2,13 @@ package ast import ( "strings" - "sync" - "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" ) -type ( - // Vars is an ordered map of variable names to values. - Vars struct { - om *orderedmap.OrderedMap[string, Var] - mutex sync.RWMutex - } - // A VarElement is a key-value pair that is used for initializing a Vars - // structure. - VarElement orderedmap.Element[string, Var] -) - -// NewVars creates a new instance of Vars and initializes it with the provided -// set of elements, if any. The elements are added in the order they are passed. -func NewVars(els ...*VarElement) *Vars { - vars := &Vars{ - om: orderedmap.NewOrderedMap[string, Var](), - } - for _, el := range els { - vars.Set(el.Key, el.Value) - } - return vars -} - -// Len returns the number of variables in the Vars map. -func (vars *Vars) Len() int { - if vars == nil || vars.om == nil { - return 0 - } - defer vars.mutex.RUnlock() - vars.mutex.RLock() - return vars.om.Len() -} - -// Get returns the value the the variable with the provided key and a boolean -// that indicates if the value was found or not. If the value is not found, the -// returned variable is a zero value and the bool is false. -func (vars *Vars) Get(key string) (Var, bool) { - if vars == nil || vars.om == nil { - return Var{}, false - } - defer vars.mutex.RUnlock() - vars.mutex.RLock() - return vars.om.Get(key) -} - -// Set sets the value of the variable with the provided key to the provided -// value. If the variable already exists, its value is updated. If the variable -// does not exist, it is created. -func (vars *Vars) Set(key string, value Var) bool { - if vars == nil { - vars = NewVars() - } - if vars.om == nil { - vars.om = orderedmap.NewOrderedMap[string, Var]() - } - defer vars.mutex.Unlock() - vars.mutex.Lock() - return vars.om.Set(key, value) -} - -// Range calls the provided function for each variable in the map. The function -// receives the variable's key and value as arguments. If the function returns -// an error, the iteration stops and the error is returned. -func (vars *Vars) Range(f func(k string, v Var) error) error { - if vars == nil || vars.om == nil { - return nil - } - for pair := vars.om.Front(); pair != nil; pair = pair.Next() { - if err := f(pair.Key, pair.Value); err != nil { - return err - } - } - return nil -} - -// ToCacheMap converts Vars to an unordered map containing only the static -// variables -func (vars *Vars) ToCacheMap() (m map[string]any) { - defer vars.mutex.RUnlock() - vars.mutex.RLock() - m = make(map[string]any, vars.Len()) - for pair := vars.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 pair.Value.Live != nil { - m[pair.Key] = pair.Value.Live - } else { - m[pair.Key] = pair.Value.Value - } - } - return -} - -// Merge loops over other and merges it values with the variables in vars. If -// the include parameter is not nil and its it is an advanced import, the -// directory is set set to the value of the include parameter. -func (vars *Vars) Merge(other *Vars, include *Include) { - if vars == nil || vars.om == nil || other == nil { - return - } - defer other.mutex.RUnlock() - other.mutex.RLock() - for pair := other.om.Front(); pair != nil; pair = pair.Next() { - if include != nil && include.AdvancedImport { - pair.Value.Dir = include.Dir - } - vars.om.Set(pair.Key, pair.Value) - } -} - -func (vs *Vars) DeepCopy() *Vars { - if vs == nil { - return nil - } - defer vs.mutex.RUnlock() - vs.mutex.RLock() - return &Vars{ - om: deepcopy.OrderedMap(vs.om), - } -} - -func (vs *Vars) UnmarshalYAML(node *yaml.Node) error { - if vs == nil || vs.om == nil { - *vs = *NewVars() - } - 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/ast/vars.go b/taskfile/ast/vars.go new file mode 100644 index 0000000000..e48aa34136 --- /dev/null +++ b/taskfile/ast/vars.go @@ -0,0 +1,164 @@ +package ast + +import ( + "sync" + + "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 ( + // Vars is an ordered map of variable names to values. + Vars struct { + om *orderedmap.OrderedMap[string, Var] + mutex sync.RWMutex + } + // A VarElement is a key-value pair that is used for initializing a Vars + // structure. + VarElement orderedmap.Element[string, Var] +) + +// NewVars creates a new instance of Vars and initializes it with the provided +// set of elements, if any. The elements are added in the order they are passed. +func NewVars(els ...*VarElement) *Vars { + vars := &Vars{ + om: orderedmap.NewOrderedMap[string, Var](), + } + for _, el := range els { + vars.Set(el.Key, el.Value) + } + return vars +} + +// Len returns the number of variables in the Vars map. +func (vars *Vars) Len() int { + if vars == nil || vars.om == nil { + return 0 + } + defer vars.mutex.RUnlock() + vars.mutex.RLock() + return vars.om.Len() +} + +// Get returns the value the the variable with the provided key and a boolean +// that indicates if the value was found or not. If the value is not found, the +// returned variable is a zero value and the bool is false. +func (vars *Vars) Get(key string) (Var, bool) { + if vars == nil || vars.om == nil { + return Var{}, false + } + defer vars.mutex.RUnlock() + vars.mutex.RLock() + return vars.om.Get(key) +} + +// Set sets the value of the variable with the provided key to the provided +// value. If the variable already exists, its value is updated. If the variable +// does not exist, it is created. +func (vars *Vars) Set(key string, value Var) bool { + if vars == nil { + vars = NewVars() + } + if vars.om == nil { + vars.om = orderedmap.NewOrderedMap[string, Var]() + } + defer vars.mutex.Unlock() + vars.mutex.Lock() + return vars.om.Set(key, value) +} + +// Range calls the provided function for each variable in the map. The function +// receives the variable's key and value as arguments. If the function returns +// an error, the iteration stops and the error is returned. +func (vars *Vars) Range(f func(k string, v Var) error) error { + if vars == nil || vars.om == nil { + return nil + } + for pair := vars.om.Front(); pair != nil; pair = pair.Next() { + if err := f(pair.Key, pair.Value); err != nil { + return err + } + } + return nil +} + +// ToCacheMap converts Vars to an unordered map containing only the static +// variables +func (vars *Vars) ToCacheMap() (m map[string]any) { + defer vars.mutex.RUnlock() + vars.mutex.RLock() + m = make(map[string]any, vars.Len()) + for pair := vars.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 pair.Value.Live != nil { + m[pair.Key] = pair.Value.Live + } else { + m[pair.Key] = pair.Value.Value + } + } + return +} + +// Merge loops over other and merges it values with the variables in vars. If +// the include parameter is not nil and its it is an advanced import, the +// directory is set set to the value of the include parameter. +func (vars *Vars) Merge(other *Vars, include *Include) { + if vars == nil || vars.om == nil || other == nil { + return + } + defer other.mutex.RUnlock() + other.mutex.RLock() + for pair := other.om.Front(); pair != nil; pair = pair.Next() { + if include != nil && include.AdvancedImport { + pair.Value.Dir = include.Dir + } + vars.om.Set(pair.Key, pair.Value) + } +} + +func (vs *Vars) DeepCopy() *Vars { + if vs == nil { + return nil + } + defer vs.mutex.RUnlock() + vs.mutex.RLock() + return &Vars{ + om: deepcopy.OrderedMap(vs.om), + } +} + +func (vs *Vars) UnmarshalYAML(node *yaml.Node) error { + if vs == nil || vs.om == nil { + *vs = *NewVars() + } + 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") +}