You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
6.4 KiB

  1. package toml
  2. import (
  3. "fmt"
  4. "strings"
  5. )
  6. // ParseError is returned when there is an error parsing the TOML syntax.
  7. //
  8. // For example invalid syntax, duplicate keys, etc.
  9. //
  10. // In addition to the error message itself, you can also print detailed location
  11. // information with context by using ErrorWithLocation():
  12. //
  13. // toml: error: Key 'fruit' was already created and cannot be used as an array.
  14. //
  15. // At line 4, column 2-7:
  16. //
  17. // 2 | fruit = []
  18. // 3 |
  19. // 4 | [[fruit]] # Not allowed
  20. // ^^^^^
  21. //
  22. // Furthermore, the ErrorWithUsage() can be used to print the above with some
  23. // more detailed usage guidance:
  24. //
  25. // toml: error: newlines not allowed within inline tables
  26. //
  27. // At line 1, column 18:
  28. //
  29. // 1 | x = [{ key = 42 #
  30. // ^
  31. //
  32. // Error help:
  33. //
  34. // Inline tables must always be on a single line:
  35. //
  36. // table = {key = 42, second = 43}
  37. //
  38. // It is invalid to split them over multiple lines like so:
  39. //
  40. // # INVALID
  41. // table = {
  42. // key = 42,
  43. // second = 43
  44. // }
  45. //
  46. // Use regular for this:
  47. //
  48. // [table]
  49. // key = 42
  50. // second = 43
  51. type ParseError struct {
  52. Message string // Short technical message.
  53. Usage string // Longer message with usage guidance; may be blank.
  54. Position Position // Position of the error
  55. LastKey string // Last parsed key, may be blank.
  56. Line int // Line the error occurred. Deprecated: use Position.
  57. err error
  58. input string
  59. }
  60. // Position of an error.
  61. type Position struct {
  62. Line int // Line number, starting at 1.
  63. Start int // Start of error, as byte offset starting at 0.
  64. Len int // Lenght in bytes.
  65. }
  66. func (pe ParseError) Error() string {
  67. msg := pe.Message
  68. if msg == "" { // Error from errorf()
  69. msg = pe.err.Error()
  70. }
  71. if pe.LastKey == "" {
  72. return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg)
  73. }
  74. return fmt.Sprintf("toml: line %d (last key %q): %s",
  75. pe.Position.Line, pe.LastKey, msg)
  76. }
  77. // ErrorWithUsage() returns the error with detailed location context.
  78. //
  79. // See the documentation on ParseError.
  80. func (pe ParseError) ErrorWithPosition() string {
  81. if pe.input == "" { // Should never happen, but just in case.
  82. return pe.Error()
  83. }
  84. var (
  85. lines = strings.Split(pe.input, "\n")
  86. col = pe.column(lines)
  87. b = new(strings.Builder)
  88. )
  89. msg := pe.Message
  90. if msg == "" {
  91. msg = pe.err.Error()
  92. }
  93. // TODO: don't show control characters as literals? This may not show up
  94. // well everywhere.
  95. if pe.Position.Len == 1 {
  96. fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n",
  97. msg, pe.Position.Line, col+1)
  98. } else {
  99. fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n",
  100. msg, pe.Position.Line, col, col+pe.Position.Len)
  101. }
  102. if pe.Position.Line > 2 {
  103. fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, lines[pe.Position.Line-3])
  104. }
  105. if pe.Position.Line > 1 {
  106. fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, lines[pe.Position.Line-2])
  107. }
  108. fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, lines[pe.Position.Line-1])
  109. fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col), strings.Repeat("^", pe.Position.Len))
  110. return b.String()
  111. }
  112. // ErrorWithUsage() returns the error with detailed location context and usage
  113. // guidance.
  114. //
  115. // See the documentation on ParseError.
  116. func (pe ParseError) ErrorWithUsage() string {
  117. m := pe.ErrorWithPosition()
  118. if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" {
  119. return m + "Error help:\n\n " +
  120. strings.ReplaceAll(strings.TrimSpace(u.Usage()), "\n", "\n ") +
  121. "\n"
  122. }
  123. return m
  124. }
  125. func (pe ParseError) column(lines []string) int {
  126. var pos, col int
  127. for i := range lines {
  128. ll := len(lines[i]) + 1 // +1 for the removed newline
  129. if pos+ll >= pe.Position.Start {
  130. col = pe.Position.Start - pos
  131. if col < 0 { // Should never happen, but just in case.
  132. col = 0
  133. }
  134. break
  135. }
  136. pos += ll
  137. }
  138. return col
  139. }
  140. type (
  141. errLexControl struct{ r rune }
  142. errLexEscape struct{ r rune }
  143. errLexUTF8 struct{ b byte }
  144. errLexInvalidNum struct{ v string }
  145. errLexInvalidDate struct{ v string }
  146. errLexInlineTableNL struct{}
  147. errLexStringNL struct{}
  148. )
  149. func (e errLexControl) Error() string {
  150. return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r)
  151. }
  152. func (e errLexControl) Usage() string { return "" }
  153. func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) }
  154. func (e errLexEscape) Usage() string { return usageEscape }
  155. func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) }
  156. func (e errLexUTF8) Usage() string { return "" }
  157. func (e errLexInvalidNum) Error() string { return fmt.Sprintf("invalid number: %q", e.v) }
  158. func (e errLexInvalidNum) Usage() string { return "" }
  159. func (e errLexInvalidDate) Error() string { return fmt.Sprintf("invalid date: %q", e.v) }
  160. func (e errLexInvalidDate) Usage() string { return "" }
  161. func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" }
  162. func (e errLexInlineTableNL) Usage() string { return usageInlineNewline }
  163. func (e errLexStringNL) Error() string { return "strings cannot contain newlines" }
  164. func (e errLexStringNL) Usage() string { return usageStringNewline }
  165. const usageEscape = `
  166. A '\' inside a "-delimited string is interpreted as an escape character.
  167. The following escape sequences are supported:
  168. \b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX
  169. To prevent a '\' from being recognized as an escape character, use either:
  170. - a ' or '''-delimited string; escape characters aren't processed in them; or
  171. - write two backslashes to get a single backslash: '\\'.
  172. If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/'
  173. instead of '\' will usually also work: "C:/Users/martin".
  174. `
  175. const usageInlineNewline = `
  176. Inline tables must always be on a single line:
  177. table = {key = 42, second = 43}
  178. It is invalid to split them over multiple lines like so:
  179. # INVALID
  180. table = {
  181. key = 42,
  182. second = 43
  183. }
  184. Use regular for this:
  185. [table]
  186. key = 42
  187. second = 43
  188. `
  189. const usageStringNewline = `
  190. Strings must always be on a single line, and cannot span more than one line:
  191. # INVALID
  192. string = "Hello,
  193. world!"
  194. Instead use """ or ''' to split strings over multiple lines:
  195. string = """Hello,
  196. world!"""
  197. `