mirror of
https://github.com/go-task/task.git
synced 2025-06-23 00:38:19 +02:00
feat: better yaml parsing and error handling (#1619)
This commit is contained in:
179
errors/error_taskfile_decode.go
Normal file
179
errors/error_taskfile_decode.go
Normal file
@ -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
|
||||
}
|
Reference in New Issue
Block a user