errors.go (5992B)
1 package toml 2 3 import ( 4 "fmt" 5 "strconv" 6 "strings" 7 8 "github.com/pelletier/go-toml/v2/internal/danger" 9 "github.com/pelletier/go-toml/v2/unstable" 10 ) 11 12 // DecodeError represents an error encountered during the parsing or decoding 13 // of a TOML document. 14 // 15 // In addition to the error message, it contains the position in the document 16 // where it happened, as well as a human-readable representation that shows 17 // where the error occurred in the document. 18 type DecodeError struct { 19 message string 20 line int 21 column int 22 key Key 23 24 human string 25 } 26 27 // StrictMissingError occurs in a TOML document that does not have a 28 // corresponding field in the target value. It contains all the missing fields 29 // in Errors. 30 // 31 // Emitted by Decoder when DisallowUnknownFields() was called. 32 type StrictMissingError struct { 33 // One error per field that could not be found. 34 Errors []DecodeError 35 } 36 37 // Error returns the canonical string for this error. 38 func (s *StrictMissingError) Error() string { 39 return "strict mode: fields in the document are missing in the target struct" 40 } 41 42 // String returns a human readable description of all errors. 43 func (s *StrictMissingError) String() string { 44 var buf strings.Builder 45 46 for i, e := range s.Errors { 47 if i > 0 { 48 buf.WriteString("\n---\n") 49 } 50 51 buf.WriteString(e.String()) 52 } 53 54 return buf.String() 55 } 56 57 type Key []string 58 59 // Error returns the error message contained in the DecodeError. 60 func (e *DecodeError) Error() string { 61 return "toml: " + e.message 62 } 63 64 // String returns the human-readable contextualized error. This string is multi-line. 65 func (e *DecodeError) String() string { 66 return e.human 67 } 68 69 // Position returns the (line, column) pair indicating where the error 70 // occurred in the document. Positions are 1-indexed. 71 func (e *DecodeError) Position() (row int, column int) { 72 return e.line, e.column 73 } 74 75 // Key that was being processed when the error occurred. The key is present only 76 // if this DecodeError is part of a StrictMissingError. 77 func (e *DecodeError) Key() Key { 78 return e.key 79 } 80 81 // decodeErrorFromHighlight creates a DecodeError referencing a highlighted 82 // range of bytes from document. 83 // 84 // highlight needs to be a sub-slice of document, or this function panics. 85 // 86 // The function copies all bytes used in DecodeError, so that document and 87 // highlight can be freely deallocated. 88 // 89 //nolint:funlen 90 func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError { 91 offset := danger.SubsliceOffset(document, de.Highlight) 92 93 errMessage := de.Error() 94 errLine, errColumn := positionAtEnd(document[:offset]) 95 before, after := linesOfContext(document, de.Highlight, offset, 3) 96 97 var buf strings.Builder 98 99 maxLine := errLine + len(after) - 1 100 lineColumnWidth := len(strconv.Itoa(maxLine)) 101 102 // Write the lines of context strictly before the error. 103 for i := len(before) - 1; i > 0; i-- { 104 line := errLine - i 105 buf.WriteString(formatLineNumber(line, lineColumnWidth)) 106 buf.WriteString("|") 107 108 if len(before[i]) > 0 { 109 buf.WriteString(" ") 110 buf.Write(before[i]) 111 } 112 113 buf.WriteRune('\n') 114 } 115 116 // Write the document line that contains the error. 117 118 buf.WriteString(formatLineNumber(errLine, lineColumnWidth)) 119 buf.WriteString("| ") 120 121 if len(before) > 0 { 122 buf.Write(before[0]) 123 } 124 125 buf.Write(de.Highlight) 126 127 if len(after) > 0 { 128 buf.Write(after[0]) 129 } 130 131 buf.WriteRune('\n') 132 133 // Write the line with the error message itself (so it does not have a line 134 // number). 135 136 buf.WriteString(strings.Repeat(" ", lineColumnWidth)) 137 buf.WriteString("| ") 138 139 if len(before) > 0 { 140 buf.WriteString(strings.Repeat(" ", len(before[0]))) 141 } 142 143 buf.WriteString(strings.Repeat("~", len(de.Highlight))) 144 145 if len(errMessage) > 0 { 146 buf.WriteString(" ") 147 buf.WriteString(errMessage) 148 } 149 150 // Write the lines of context strictly after the error. 151 152 for i := 1; i < len(after); i++ { 153 buf.WriteRune('\n') 154 line := errLine + i 155 buf.WriteString(formatLineNumber(line, lineColumnWidth)) 156 buf.WriteString("|") 157 158 if len(after[i]) > 0 { 159 buf.WriteString(" ") 160 buf.Write(after[i]) 161 } 162 } 163 164 return &DecodeError{ 165 message: errMessage, 166 line: errLine, 167 column: errColumn, 168 key: de.Key, 169 human: buf.String(), 170 } 171 } 172 173 func formatLineNumber(line int, width int) string { 174 format := "%" + strconv.Itoa(width) + "d" 175 176 return fmt.Sprintf(format, line) 177 } 178 179 func linesOfContext(document []byte, highlight []byte, offset int, linesAround int) ([][]byte, [][]byte) { 180 return beforeLines(document, offset, linesAround), afterLines(document, highlight, offset, linesAround) 181 } 182 183 func beforeLines(document []byte, offset int, linesAround int) [][]byte { 184 var beforeLines [][]byte 185 186 // Walk the document backward from the highlight to find previous lines 187 // of context. 188 rest := document[:offset] 189 backward: 190 for o := len(rest) - 1; o >= 0 && len(beforeLines) <= linesAround && len(rest) > 0; { 191 switch { 192 case rest[o] == '\n': 193 // handle individual lines 194 beforeLines = append(beforeLines, rest[o+1:]) 195 rest = rest[:o] 196 o = len(rest) - 1 197 case o == 0: 198 // add the first line only if it's non-empty 199 beforeLines = append(beforeLines, rest) 200 201 break backward 202 default: 203 o-- 204 } 205 } 206 207 return beforeLines 208 } 209 210 func afterLines(document []byte, highlight []byte, offset int, linesAround int) [][]byte { 211 var afterLines [][]byte 212 213 // Walk the document forward from the highlight to find the following 214 // lines of context. 215 rest := document[offset+len(highlight):] 216 forward: 217 for o := 0; o < len(rest) && len(afterLines) <= linesAround; { 218 switch { 219 case rest[o] == '\n': 220 // handle individual lines 221 afterLines = append(afterLines, rest[:o]) 222 rest = rest[o+1:] 223 o = 0 224 225 case o == len(rest)-1: 226 // add last line only if it's non-empty 227 afterLines = append(afterLines, rest) 228 229 break forward 230 default: 231 o++ 232 } 233 } 234 235 return afterLines 236 } 237 238 func positionAtEnd(b []byte) (row int, column int) { 239 row = 1 240 column = 1 241 242 for _, c := range b { 243 if c == '\n' { 244 row++ 245 column = 1 246 } else { 247 column++ 248 } 249 } 250 251 return 252 }