mirror

Mirror free and open-source projects you like with minimal effort
git clone git://git.server.ky/slackcoder/mirror
Log | Files | Refs | README

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