Skip to content

Commit

Permalink
Add "extended errors"
Browse files Browse the repository at this point in the history
Add ExtError() to display "extended errors" with context:

        toml: error: expected a digit but got '+'
                     on line 1; last key parsed was "double-sign-plus"

             1 | double-sign-plus = ++99
                                     ^

Still WIP; the position is usually but not always always quite correct;
for example:

        toml: error: Invalid float value: "0.1.2"
                     on line 1; last key parsed was "double-point-2"

             1 | double-point-2 = 0.1.2
                              ^

This is mostly because the Parser doesn't always set it correctly.

Would also be nice to expand it a bit so it would show:

             1 | double-sign-plus = ++99
                                    ^~

             1 | double-point-2 = 0.1.2
                                  ^~~~~

Also:

- The "Line" in ParseError isn't always accurate, but Pos is. The
  "Line" should always be accurate.

- Maybe record as Col instead? I just used Pos because it was easier for
  now.

- It's a (hard-coded) option, since this attached the input to the error
  and is slower. Need to be able to pass that option, and maybe also
  just have Error() print out the "extended error" if it's enabled
  instead of an ExtError() method.

- Quite a few of the error messages could do with improving.
  • Loading branch information
arp242 committed Jun 24, 2021
1 parent 8485579 commit d45efc1
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 65 deletions.
34 changes: 0 additions & 34 deletions decode_test.go
@@ -1,7 +1,6 @@
package toml

import (
"errors"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -666,39 +665,6 @@ cauchy = """ cat 2
}
}

func TestParseError(t *testing.T) {
file :=
`a = "a"
b = "b"
c = 001 # invalid
`

var s struct {
A, B string
C int
}
_, err := Decode(file, &s)
if err == nil {
t.Fatal("err is nil")
}

var pErr ParseError
if !errors.As(err, &pErr) {
t.Fatalf("err is not a ParseError: %T %[1]v", err)
}

want := ParseError{
Line: 3,
LastKey: "c",
Message: `Invalid integer "001": cannot have leading zeroes`,
}
if !strings.Contains(pErr.Message, want.Message) ||
pErr.Line != want.Line ||
pErr.LastKey != want.LastKey {
t.Errorf("unexpected data\nhave: %#v\nwant: %#v", pErr, want)
}
}

// errorContains checks if the error message in have contains the text in
// want.
//
Expand Down
90 changes: 90 additions & 0 deletions error.go
@@ -0,0 +1,90 @@
package toml

import (
"fmt"
"strings"
)

// ParseError is used when there is an error decoding TOML data.
//
// For example invalid TOML syntax, duplicate keys, etc.
type ParseError struct {
Message string
Line int
Pos int // Byte offset
LastKey string // Last parsed key, may be blank.
Input string
}

func (pe ParseError) Error() string {
if pe.LastKey == "" {
return fmt.Sprintf("toml: line %d: %s", pe.Line, pe.Message)
}
return fmt.Sprintf("toml: line %d (last key parsed '%s'): %s",
pe.Line, pe.LastKey, pe.Message)
}

// Clang error:
//
// a.c:2:9: warning: incompatible pointer to integer conversion returning 'char [4]' from a function with result type 'int' [-Wint-conversion]
// return "zxc";
// ^~~~~
// 1 warning generated.
//
// Rust:
//
// error[E0425]: cannot find value `err` in this scope
// --> a.rs:3:5
// |
// 3 | err
// | ^^^ help: a tuple variant with a similar name exists: `Err`
//
// error: aborting due to previous error
//
// For more information about this error, try `rustc --explain E0425`.

func (pe ParseError) ExtError() string {
if pe.Input == "" {
return pe.Error()
}

lines := strings.Split(pe.Input, "\n")
var line, pos, col int
for i := range lines {
ll := len(lines[i]) + 1 // +1 for the removed newline
if pos+ll >= pe.Pos {
line = i
col = pe.Pos - pos - 1
if col < 0 { // Should never happen, but just in case.
col = 0
}
break
}
pos += ll
}

b := new(strings.Builder)
//fmt.Fprintf(b, "toml: error on line %d: %s\n", line, pe.Message)
fmt.Fprintf(b, "toml: error: %s\n", pe.Message)
fmt.Fprintf(b, " on line %d", line+1)
if pe.LastKey != "" {
fmt.Fprintf(b, "; last key parsed was %q", pe.LastKey)
}
b.WriteString("\n\n")

if line > 1 {
fmt.Fprintf(b, "% 6d | %s\n", line-1, lines[line-2])
}
if line > 0 {
fmt.Fprintf(b, "% 6d | %s\n", line, lines[line-1])
}

fmt.Fprintf(b, "% 6d | %s\n", line+1, lines[line])
fmt.Fprintf(b, "% 9s%s^\n", "", strings.Repeat(" ", col))

// if len(lines)-1 > line && lines[line+1] != "" {
// fmt.Fprintf(b, "% 6d | %s\n", line+1, lines[line+1])
// }

return b.String()
}
87 changes: 87 additions & 0 deletions error_test.go
@@ -0,0 +1,87 @@
package toml_test

import (
"errors"
"fmt"
"io/fs"
"strings"
"testing"

"github.com/BurntSushi/toml"
tomltest "github.com/BurntSushi/toml-test"
)

/*
func TestDecodeError(t *testing.T) {
file :=
`a = "a"
b = "b"
c = 001 # invalid
`
var s struct {
A, B string
C int
}
_, err := Decode(file, &s)
if err == nil {
t.Fatal("err is nil")
}
var dErr DecodeError
if !errors.As(err, &dErr) {
t.Fatalf("err is not a DecodeError: %T %[1]v", err)
}
want := DecodeError{
Line: 3,
Pos: 17,
LastKey: "c",
Message: `Invalid integer "001": cannot have leading zeroes`,
}
if !reflect.DeepEqual(dErr, want) {
t.Errorf("unexpected data\nhave: %#v\nwant: %#v", dErr, want)
}
}
*/

func TestParseError(t *testing.T) {
fsys := tomltest.EmbeddedTests()
ls, err := fs.ReadDir(fsys, "invalid")
if err != nil {
t.Fatal(err)
}

for _, f := range ls {
if !strings.HasSuffix(f.Name(), ".toml") {
continue
}

if f.Name() == "string-multiline-escape-space.toml" {
continue
}

input, err := fs.ReadFile(fsys, "invalid/"+f.Name())
if err != nil {
t.Fatal(err)
}

var x interface{}
_, err = toml.Decode(string(input), &x)
if err == nil {
continue
}

var dErr toml.ParseError
if !errors.As(err, &dErr) {
t.Errorf("err is not a ParseError: %T %[1]v", err)
continue
}

fmt.Println()
fmt.Println("–––", f.Name(), strings.Repeat("–", 65-len(f.Name())))
fmt.Print(dErr.ExtError())
fmt.Println(strings.Repeat("–", 70))
}
}
11 changes: 4 additions & 7 deletions lex.go
Expand Up @@ -87,6 +87,7 @@ type item struct {
typ itemType
val string
line int
pos int
}

func (lx *lexer) nextItem() item {
Expand Down Expand Up @@ -130,12 +131,12 @@ func (lx *lexer) current() string {
}

func (lx *lexer) emit(typ itemType) {
lx.items <- item{typ, lx.current(), lx.line}
lx.items <- item{typ, lx.current(), lx.line, lx.pos}
lx.start = lx.pos
}

func (lx *lexer) emitTrim(typ itemType) {
lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line}
lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line, lx.pos}
lx.start = lx.pos
}

Expand Down Expand Up @@ -227,11 +228,7 @@ func (lx *lexer) skip(pred func(rune) bool) {
// Note that any value that is a character is escaped if it's a special
// character (newlines, tabs, etc.).
func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
lx.items <- item{
itemError,
fmt.Sprintf(format, values...),
lx.line,
}
lx.items <- item{itemError, fmt.Sprintf(format, values...), lx.line, lx.pos}
return nil
}

Expand Down
45 changes: 21 additions & 24 deletions parse.go
@@ -1,7 +1,6 @@
package toml

import (
"errors"
"fmt"
"strconv"
"strings"
Expand All @@ -18,27 +17,20 @@ type parser struct {
context Key // Full key for the current hash in scope.
currentKey string // Base key name for everything except hashes.
approxLine int // Rough approximation of line number
pos int // Rough approximation of position (byte offset from start).
implicits map[string]bool // Record implied keys (e.g. 'key.group.names').
}

// ParseError is used when a file can't be parsed: for example invalid integer
// literals, duplicate keys, etc.
type ParseError struct {
Message string
Line int
LastKey string
}

func (pe ParseError) Error() string {
return fmt.Sprintf("Near line %d (last key parsed '%s'): %s",
pe.Line, pe.LastKey, pe.Message)
}
var extendedError = true

func parse(data string) (p *parser, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
if err, ok = r.(ParseError); ok {
if pErr, ok := r.(ParseError); ok {
if extendedError {
pErr.Input = data
}
err = pErr
return
}
panic(r)
Expand All @@ -58,8 +50,12 @@ func parse(data string) (p *parser, err error) {
if len(data) < 6 {
ex = len(data)
}
if strings.ContainsRune(data[:ex], 0) {
return nil, errors.New("files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8")
if i := strings.IndexRune(data[:ex], 0); i > -1 {
return nil, ParseError{
Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8",
Pos: i,
Line: 1,
}
}

p = &parser{
Expand All @@ -81,10 +77,10 @@ func parse(data string) (p *parser, err error) {
}

func (p *parser) panicf(format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
panic(ParseError{
Message: msg,
Message: fmt.Sprintf(format, v...),
Line: p.approxLine,
Pos: p.pos,
LastKey: p.current(),
})
}
Expand All @@ -93,6 +89,7 @@ func (p *parser) next() item {
it := p.lx.nextItem()
//fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.line, it.val)
if it.typ == itemError {
p.pos = it.pos
p.panicf("%s", it.val)
}
return it
Expand All @@ -117,11 +114,11 @@ func (p *parser) assertEqual(expected, got itemType) {
func (p *parser) topLevel(item item) {
switch item.typ {
case itemCommentStart: // # ..
p.approxLine = item.line
p.approxLine, p.pos = item.line, item.pos
p.expect(itemText)
case itemTableStart: // [ .. ]
name := p.next()
p.approxLine = name.line
p.approxLine, p.pos = name.line, name.pos

var key Key
for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() {
Expand All @@ -134,7 +131,7 @@ func (p *parser) topLevel(item item) {
p.ordered = append(p.ordered, key)
case itemArrayTableStart: // [[ .. ]]
name := p.next()
p.approxLine = name.line
p.approxLine, p.pos = name.line, name.pos

var key Key
for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() {
Expand All @@ -149,7 +146,7 @@ func (p *parser) topLevel(item item) {
outerContext := p.context
/// Read all the key parts (e.g. 'a' and 'b' in 'a.b')
k := p.next()
p.approxLine = k.line
p.approxLine, p.pos = k.line, k.pos
var key Key
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
key = append(key, p.keyString(k))
Expand Down Expand Up @@ -364,7 +361,7 @@ func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tom

/// Read all key parts.
k := p.next()
p.approxLine = k.line
p.approxLine, p.pos = k.line, k.pos
var key Key
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
key = append(key, p.keyString(k))
Expand Down

0 comments on commit d45efc1

Please sign in to comment.