error_taskfile_decode.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package errors
  2. import (
  3. "bytes"
  4. "embed"
  5. "errors"
  6. "fmt"
  7. "regexp"
  8. "strings"
  9. "github.com/alecthomas/chroma/v2"
  10. "github.com/alecthomas/chroma/v2/quick"
  11. "github.com/alecthomas/chroma/v2/styles"
  12. "github.com/fatih/color"
  13. "gopkg.in/yaml.v3"
  14. )
  15. //go:embed themes/*.xml
  16. var embedded embed.FS
  17. var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
  18. func init() {
  19. r, err := embedded.Open("themes/task.xml")
  20. if err != nil {
  21. panic(err)
  22. }
  23. style, err := chroma.NewXMLStyle(r)
  24. if err != nil {
  25. panic(err)
  26. }
  27. styles.Register(style)
  28. }
  29. type (
  30. TaskfileDecodeError struct {
  31. Message string
  32. Location string
  33. Line int
  34. Column int
  35. Tag string
  36. Snippet TaskfileSnippet
  37. Err error
  38. }
  39. TaskfileSnippet struct {
  40. Lines []string
  41. StartLine int
  42. EndLine int
  43. Padding int
  44. }
  45. )
  46. func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError {
  47. // If the error is already a DecodeError, return it
  48. taskfileInvalidErr := &TaskfileDecodeError{}
  49. if errors.As(err, &taskfileInvalidErr) {
  50. return taskfileInvalidErr
  51. }
  52. return &TaskfileDecodeError{
  53. Line: node.Line,
  54. Column: node.Column,
  55. Tag: node.ShortTag(),
  56. Err: err,
  57. }
  58. }
  59. func (err *TaskfileDecodeError) Error() string {
  60. buf := &bytes.Buffer{}
  61. // Print the error message
  62. if err.Message != "" {
  63. fmt.Fprintln(buf, color.RedString("err: %s", err.Message))
  64. } else {
  65. // Extract the errors from the TypeError
  66. te := &yaml.TypeError{}
  67. if errors.As(err.Err, &te) {
  68. if len(te.Errors) > 1 {
  69. fmt.Fprintln(buf, color.RedString("errs:"))
  70. for _, message := range te.Errors {
  71. fmt.Fprintln(buf, color.RedString("- %s", extractTypeErrorMessage(message)))
  72. }
  73. } else {
  74. fmt.Fprintln(buf, color.RedString("err: %s", extractTypeErrorMessage(te.Errors[0])))
  75. }
  76. } else {
  77. // Otherwise print the error message normally
  78. fmt.Fprintln(buf, color.RedString("err: %s", err.Err))
  79. }
  80. }
  81. fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column))
  82. // Print the snippet
  83. maxLineNumberDigits := digits(err.Snippet.EndLine)
  84. lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits)
  85. columnSpacer := strings.Repeat(" ", err.Column-1)
  86. for i, line := range err.Snippet.Lines {
  87. currentLine := err.Snippet.StartLine + i + 1
  88. lineIndicator := " "
  89. if currentLine == err.Line {
  90. lineIndicator = ">"
  91. }
  92. columnIndicator := "^"
  93. // Print each line
  94. lineIndicator = color.RedString(lineIndicator)
  95. columnIndicator = color.RedString(columnIndicator)
  96. lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits)
  97. lineNumber := fmt.Sprintf(lineNumberFormat, currentLine)
  98. fmt.Fprintf(buf, "%s %s | %s", lineIndicator, lineNumber, line)
  99. // Print the column indicator
  100. if currentLine == err.Line {
  101. fmt.Fprintf(buf, "\n %s | %s%s", lineNumberSpacer, columnSpacer, columnIndicator)
  102. }
  103. // If there are more lines to print, add a newline
  104. if i < len(err.Snippet.Lines)-1 {
  105. fmt.Fprintln(buf)
  106. }
  107. }
  108. return buf.String()
  109. }
  110. func (err *TaskfileDecodeError) Unwrap() error {
  111. return err.Err
  112. }
  113. func (err *TaskfileDecodeError) Code() int {
  114. return CodeTaskfileDecode
  115. }
  116. func (err *TaskfileDecodeError) WithMessage(format string, a ...any) *TaskfileDecodeError {
  117. err.Message = fmt.Sprintf(format, a...)
  118. return err
  119. }
  120. func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError {
  121. err.Message = fmt.Sprintf("cannot unmarshal %s into %s", err.Tag, t)
  122. return err
  123. }
  124. func (err *TaskfileDecodeError) WithFileInfo(location string, b []byte, padding int) *TaskfileDecodeError {
  125. buf := &bytes.Buffer{}
  126. if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil {
  127. buf.WriteString(string(b))
  128. }
  129. lines := strings.Split(buf.String(), "\n")
  130. start := max(err.Line-1-padding, 0)
  131. end := min(err.Line+padding, len(lines)-1)
  132. err.Location = location
  133. err.Snippet = TaskfileSnippet{
  134. Lines: lines[start:end],
  135. StartLine: start,
  136. EndLine: end,
  137. Padding: padding,
  138. }
  139. return err
  140. }
  141. func extractTypeErrorMessage(message string) string {
  142. matches := typeErrorRegex.FindStringSubmatch(message)
  143. if len(matches) == 2 {
  144. return matches[1]
  145. }
  146. return message
  147. }
  148. func digits(number int) int {
  149. count := 0
  150. for number != 0 {
  151. number /= 10
  152. count += 1
  153. }
  154. return count
  155. }