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