error.go raw

   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
 348