error.go (10284B)
1 package toml 2 3 import ( 4 "fmt" 5 "strings" 6 ) 7 8 // ParseError is returned when there is an error parsing the TOML syntax such as 9 // invalid syntax, duplicate keys, etc. 10 // 11 // In addition to the error message itself, you can also print detailed location 12 // information with context by using [ErrorWithPosition]: 13 // 14 // toml: error: Key 'fruit' was already created and cannot be used as an array. 15 // 16 // At line 4, column 2-7: 17 // 18 // 2 | fruit = [] 19 // 3 | 20 // 4 | [[fruit]] # Not allowed 21 // ^^^^^ 22 // 23 // [ErrorWithUsage] can be used to print the above with some more detailed usage 24 // guidance: 25 // 26 // toml: error: newlines not allowed within inline tables 27 // 28 // At line 1, column 18: 29 // 30 // 1 | x = [{ key = 42 # 31 // ^ 32 // 33 // Error help: 34 // 35 // Inline tables must always be on a single line: 36 // 37 // table = {key = 42, second = 43} 38 // 39 // It is invalid to split them over multiple lines like so: 40 // 41 // # INVALID 42 // table = { 43 // key = 42, 44 // second = 43 45 // } 46 // 47 // Use regular for this: 48 // 49 // [table] 50 // key = 42 51 // second = 43 52 type ParseError struct { 53 Message string // Short technical message. 54 Usage string // Longer message with usage guidance; may be blank. 55 Position Position // Position of the error 56 LastKey string // Last parsed key, may be blank. 57 58 // Line the error occurred. 59 // 60 // Deprecated: use [Position]. 61 Line int 62 63 err error 64 input string 65 } 66 67 // Position of an error. 68 type Position struct { 69 Line int // Line number, starting at 1. 70 Start int // Start of error, as byte offset starting at 0. 71 Len int // Lenght in bytes. 72 } 73 74 func (pe ParseError) Error() string { 75 msg := pe.Message 76 if msg == "" { // Error from errorf() 77 msg = pe.err.Error() 78 } 79 80 if pe.LastKey == "" { 81 return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg) 82 } 83 return fmt.Sprintf("toml: line %d (last key %q): %s", 84 pe.Position.Line, pe.LastKey, msg) 85 } 86 87 // ErrorWithPosition returns the error with detailed location context. 88 // 89 // See the documentation on [ParseError]. 90 func (pe ParseError) ErrorWithPosition() string { 91 if pe.input == "" { // Should never happen, but just in case. 92 return pe.Error() 93 } 94 95 var ( 96 lines = strings.Split(pe.input, "\n") 97 col = pe.column(lines) 98 b = new(strings.Builder) 99 ) 100 101 msg := pe.Message 102 if msg == "" { 103 msg = pe.err.Error() 104 } 105 106 // TODO: don't show control characters as literals? This may not show up 107 // well everywhere. 108 109 if pe.Position.Len == 1 { 110 fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n", 111 msg, pe.Position.Line, col+1) 112 } else { 113 fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n", 114 msg, pe.Position.Line, col, col+pe.Position.Len) 115 } 116 if pe.Position.Line > 2 { 117 fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, expandTab(lines[pe.Position.Line-3])) 118 } 119 if pe.Position.Line > 1 { 120 fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, expandTab(lines[pe.Position.Line-2])) 121 } 122 123 /// Expand tabs, so that the ^^^s are at the correct position, but leave 124 /// "column 10-13" intact. Adjusting this to the visual column would be 125 /// better, but we don't know the tabsize of the user in their editor, which 126 /// can be 8, 4, 2, or something else. We can't know. So leaving it as the 127 /// character index is probably the "most correct". 128 expanded := expandTab(lines[pe.Position.Line-1]) 129 diff := len(expanded) - len(lines[pe.Position.Line-1]) 130 131 fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, expanded) 132 fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col+diff), strings.Repeat("^", pe.Position.Len)) 133 return b.String() 134 } 135 136 // ErrorWithUsage returns the error with detailed location context and usage 137 // guidance. 138 // 139 // See the documentation on [ParseError]. 140 func (pe ParseError) ErrorWithUsage() string { 141 m := pe.ErrorWithPosition() 142 if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" { 143 lines := strings.Split(strings.TrimSpace(u.Usage()), "\n") 144 for i := range lines { 145 if lines[i] != "" { 146 lines[i] = " " + lines[i] 147 } 148 } 149 return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n" 150 } 151 return m 152 } 153 154 func (pe ParseError) column(lines []string) int { 155 var pos, col int 156 for i := range lines { 157 ll := len(lines[i]) + 1 // +1 for the removed newline 158 if pos+ll >= pe.Position.Start { 159 col = pe.Position.Start - pos 160 if col < 0 { // Should never happen, but just in case. 161 col = 0 162 } 163 break 164 } 165 pos += ll 166 } 167 168 return col 169 } 170 171 func expandTab(s string) string { 172 var ( 173 b strings.Builder 174 l int 175 fill = func(n int) string { 176 b := make([]byte, n) 177 for i := range b { 178 b[i] = ' ' 179 } 180 return string(b) 181 } 182 ) 183 b.Grow(len(s)) 184 for _, r := range s { 185 switch r { 186 case '\t': 187 tw := 8 - l%8 188 b.WriteString(fill(tw)) 189 l += tw 190 default: 191 b.WriteRune(r) 192 l += 1 193 } 194 } 195 return b.String() 196 } 197 198 type ( 199 errLexControl struct{ r rune } 200 errLexEscape struct{ r rune } 201 errLexUTF8 struct{ b byte } 202 errParseDate struct{ v string } 203 errLexInlineTableNL struct{} 204 errLexStringNL struct{} 205 errParseRange struct { 206 i any // int or float 207 size string // "int64", "uint16", etc. 208 } 209 errUnsafeFloat struct { 210 i interface{} // float32 or float64 211 size string // "float32" or "float64" 212 } 213 errParseDuration struct{ d string } 214 ) 215 216 func (e errLexControl) Error() string { 217 return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r) 218 } 219 func (e errLexControl) Usage() string { return "" } 220 221 func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) } 222 func (e errLexEscape) Usage() string { return usageEscape } 223 func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) } 224 func (e errLexUTF8) Usage() string { return "" } 225 func (e errParseDate) Error() string { return fmt.Sprintf("invalid datetime: %q", e.v) } 226 func (e errParseDate) Usage() string { return usageDate } 227 func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" } 228 func (e errLexInlineTableNL) Usage() string { return usageInlineNewline } 229 func (e errLexStringNL) Error() string { return "strings cannot contain newlines" } 230 func (e errLexStringNL) Usage() string { return usageStringNewline } 231 func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) } 232 func (e errParseRange) Usage() string { return usageIntOverflow } 233 func (e errUnsafeFloat) Error() string { 234 return fmt.Sprintf("%v is out of the safe %s range", e.i, e.size) 235 } 236 func (e errUnsafeFloat) Usage() string { return usageUnsafeFloat } 237 func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) } 238 func (e errParseDuration) Usage() string { return usageDuration } 239 240 const usageEscape = ` 241 A '\' inside a "-delimited string is interpreted as an escape character. 242 243 The following escape sequences are supported: 244 \b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX 245 246 To prevent a '\' from being recognized as an escape character, use either: 247 248 - a ' or '''-delimited string; escape characters aren't processed in them; or 249 - write two backslashes to get a single backslash: '\\'. 250 251 If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/' 252 instead of '\' will usually also work: "C:/Users/martin". 253 ` 254 255 const usageInlineNewline = ` 256 Inline tables must always be on a single line: 257 258 table = {key = 42, second = 43} 259 260 It is invalid to split them over multiple lines like so: 261 262 # INVALID 263 table = { 264 key = 42, 265 second = 43 266 } 267 268 Use regular for this: 269 270 [table] 271 key = 42 272 second = 43 273 ` 274 275 const usageStringNewline = ` 276 Strings must always be on a single line, and cannot span more than one line: 277 278 # INVALID 279 string = "Hello, 280 world!" 281 282 Instead use """ or ''' to split strings over multiple lines: 283 284 string = """Hello, 285 world!""" 286 ` 287 288 const usageIntOverflow = ` 289 This number is too large; this may be an error in the TOML, but it can also be a 290 bug in the program that uses too small of an integer. 291 292 The maximum and minimum values are: 293 294 size │ lowest │ highest 295 ───────┼────────────────┼────────────── 296 int8 │ -128 │ 127 297 int16 │ -32,768 │ 32,767 298 int32 │ -2,147,483,648 │ 2,147,483,647 299 int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷ 300 uint8 │ 0 │ 255 301 uint16 │ 0 │ 65,535 302 uint32 │ 0 │ 4,294,967,295 303 uint64 │ 0 │ 1.8 × 10¹⁸ 304 305 int refers to int32 on 32-bit systems and int64 on 64-bit systems. 306 ` 307 308 const usageUnsafeFloat = ` 309 This number is outside of the "safe" range for floating point numbers; whole 310 (non-fractional) numbers outside the below range can not always be represented 311 accurately in a float, leading to some loss of accuracy. 312 313 Explicitly mark a number as a fractional unit by adding ".0", which will incur 314 some loss of accuracy; for example: 315 316 f = 2_000_000_000.0 317 318 Accuracy ranges: 319 320 float32 = 16,777,215 321 float64 = 9,007,199,254,740,991 322 ` 323 324 const usageDuration = ` 325 A duration must be as "number<unit>", without any spaces. Valid units are: 326 327 ns nanoseconds (billionth of a second) 328 us, µs microseconds (millionth of a second) 329 ms milliseconds (thousands of a second) 330 s seconds 331 m minutes 332 h hours 333 334 You can combine multiple units; for example "5m10s" for 5 minutes and 10 335 seconds. 336 ` 337 338 const usageDate = ` 339 A TOML datetime must be in one of the following formats: 340 341 2006-01-02T15:04:05Z07:00 Date and time, with timezone. 342 2006-01-02T15:04:05 Date and time, but without timezone. 343 2006-01-02 Date without a time or timezone. 344 15:04:05 Just a time, without any timezone. 345 346 Seconds may optionally have a fraction, up to nanosecond precision: 347 348 15:04:05.123 349 15:04:05.856018510 350 ` 351 352 // TOML 1.1: 353 // The seconds part in times is optional, and may be omitted: 354 // 2006-01-02T15:04Z07:00 355 // 2006-01-02T15:04 356 // 15:04