github.com/errata-ai/vale/v3@v3.4.2/internal/core/error.go (about) 1 package core 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "fmt" 8 "os" 9 "path/filepath" 10 "strings" 11 12 "github.com/pterm/pterm" 13 ) 14 15 type lineError struct { 16 content string 17 line int 18 span []int 19 } 20 21 type errorCondition func(position int, line, target string) bool 22 23 func annotate(file []byte, target string, finder errorCondition) (lineError, error) { 24 var sb strings.Builder 25 26 scanner := bufio.NewScanner(bytes.NewBuffer(file)) 27 context := lineError{span: []int{1, 1}} 28 29 idx := 1 30 for scanner.Scan() { 31 markup := scanner.Text() 32 plain := StripANSI(markup) 33 if idx-context.line > 2 && context.line != 0 { //nolint:gocritic 34 break 35 } else if finder(idx, plain, target) && context.line == 0 { 36 context.line = idx 37 38 s := strings.Index(plain, target) + 1 39 context.span = []int{s, s + len(target)} 40 41 sb.WriteString( 42 fmt.Sprintf("\033[1;32m%4d\033[0m* %s\n", idx, markup)) 43 } else { 44 sb.WriteString( 45 fmt.Sprintf("\033[1;32m%4d\033[0m %s\n", idx, markup)) 46 } 47 idx++ 48 } 49 50 if err := scanner.Err(); err != nil { 51 return lineError{}, err 52 } 53 54 lines := []string{} 55 for i, l := range strings.Split(sb.String(), "\n") { 56 if context.line-i < 3 { 57 lines = append(lines, l) 58 } 59 } 60 61 context.content = strings.Join(lines, "\n") 62 return context, nil 63 } 64 65 // NewError creates a colored error from the given information. 66 // 67 // # The standard format is 68 // 69 // ``` 70 // <code> [<context>] <title> 71 // 72 // <msg> 73 // ``` 74 func NewError(code, title, msg string) error { 75 return fmt.Errorf( 76 "%s %s\n\n%s\n\n%s", 77 pterm.BgRed.Sprintf(code), 78 title, 79 msg, 80 pterm.Fuzzy.Sprint(pterm.Italic.Sprintf("Execution stopped with code 1.")), 81 ) 82 } 83 84 // NewE100 creates a new, formatted "unexpected" error. 85 // 86 // Since E100 errors can occur anywhere, we include a "context" that makes it 87 // clear where exactly the error was generated. 88 func NewE100(context string, err error) error { 89 title := fmt.Sprintf("[%s] %s", context, "Runtime error") 90 return NewError("E100", title, err.Error()) 91 } 92 93 // NewE201 creates a formatted user-generated error. 94 // 95 // 201 errors involve a specific configuration asset and should contain 96 // parsable location information on their last line of the form: 97 // 98 // <path>:<line>:<start>:<end> 99 func NewE201(msg, value, path string, finder errorCondition) error { 100 f, err := os.ReadFile(path) 101 if err != nil { 102 return NewE100("NewE201", errors.New(msg)) 103 } 104 105 ctx, err := annotate(f, value, finder) 106 if err != nil { 107 return NewE100("NewE201/annotate", err) 108 } 109 110 title := fmt.Sprintf( 111 "Invalid value [%s:%d:%d]:", 112 filepath.ToSlash(path), 113 ctx.line, 114 ctx.span[0]) 115 116 return NewError( 117 "E201", 118 title, 119 fmt.Sprintf("%s\n%s", ctx.content, msg)) 120 } 121 122 // NewE201FromTarget creates a new E201 error from a target string. 123 func NewE201FromTarget(msg, value, file string) error { 124 return NewE201( 125 msg, 126 value, 127 file, 128 func(position int, line, target string) bool { 129 return strings.Contains(line, target) 130 }) 131 } 132 133 // NewE201FromPosition creates a new E201 error from an in-file location. 134 func NewE201FromPosition(msg, file string, goal int) error { 135 return NewE201( 136 msg, 137 "", 138 file, 139 func(position int, line, target string) bool { 140 return position == goal 141 }) 142 }