mirror of
https://github.com/go-task/task.git
synced 2024-12-12 10:45:49 +02:00
180 lines
4.2 KiB
Go
180 lines
4.2 KiB
Go
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
|
|
}
|