Wip errors reporting

This commit is contained in:
Thomas Pelletier
2021-03-30 10:59:35 -04:00
parent 72a1afdcb2
commit cf288a51c5
5 changed files with 303 additions and 14 deletions
+1 -1
View File
@@ -17,8 +17,8 @@ Development branch. Probably does not work.
- [x] Benchmark!
- [x] Abstract AST.
- [x] Original go-toml testgen tests pass.
- [ ] Attach comments to AST (gated by parser flag).
- [ ] Track file position (line, column) for errors.
- [ ] Attach comments to AST (gated by parser flag).
- [ ] Benchmark again!
## Further work
+147
View File
@@ -0,0 +1,147 @@
package toml
import (
"fmt"
"strconv"
"strings"
"github.com/pelletier/go-toml/v2/internal/unsafe"
)
// DecodeError represents an error encountered during the parsing or decoding
// of a TOML document.
//
// In addition to the error message, it contains the position in the document
// where it happened, as well as a human-readable representation that shows
// where the error occurred in the document.
type DecodeError struct {
message string
line int
column int
human string
}
// Error returns the error message contained in the DecodeError.
func (e *DecodeError) Error() string {
return e.message
}
// String returns the human-readable contextualized error. This string is multi-line.
func (e *DecodeError) String() string {
return e.human
}
/// Position returns the (line, column) pair indicating where the error
// occurred in the document. Positions are 1-indexed.
func (e *DecodeError) Position() (row int, column int) {
return e.line, e.column
}
// decodeErrorFromHighlight creates a DecodeError referencing to a highlighted
// range of bytes from document.
//
// highlight needs to be a sub-slice of document, or this function panics.
//
// The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated.
func decodeErrorFromHighlight(document []byte, highlight []byte, message string) error {
err := &DecodeError{
message: message,
}
offset := unsafe.SubsliceOffset(document, highlight)
err.line, err.column = positionAtEnd(document[:offset])
before, after := linesOfContext(document, highlight, offset, 3)
var buf strings.Builder
maxLine := err.line + len(after) - 1
lineColumnWidth := len(strconv.Itoa(maxLine))
for i := len(before) - 1; i > 0; i-- {
line := err.line - i
buf.WriteString(formatLineNumber(line, lineColumnWidth))
buf.WriteString("| ")
buf.Write(before[i])
buf.WriteRune('\n')
}
buf.WriteString(formatLineNumber(err.line, lineColumnWidth))
buf.WriteString("| ")
if len(before) > 0 {
buf.Write(before[0])
}
buf.Write(highlight)
if len(after) > 0 {
buf.Write(after[0])
}
buf.WriteRune('\n')
buf.WriteString(strings.Repeat(" ", lineColumnWidth))
buf.WriteString("| ")
if len(before) > 0 {
buf.WriteString(strings.Repeat(" ", len(before[0])))
}
buf.WriteString(strings.Repeat("~", len(highlight)))
buf.WriteString(" ")
buf.WriteString(err.message)
for i := 1; i < len(after); i++ {
buf.WriteRune('\n')
line := err.line + i
buf.WriteString(formatLineNumber(line, lineColumnWidth))
buf.WriteString("| ")
buf.Write(after[i])
}
err.human = buf.String()
return err
}
func formatLineNumber(line int, width int) string {
format := "%" + strconv.Itoa(width) + "d"
return fmt.Sprintf(format, line)
}
func linesOfContext(document []byte, highlight []byte, offset int, linesAround int) ([][]byte, [][]byte) {
var beforeLines [][]byte
for beforeOffset, lastOffset := offset, offset; beforeOffset >= 0 && len(beforeLines) <= linesAround; beforeOffset-- {
if document[beforeOffset] == '\n' {
beforeLines = append(beforeLines, document[beforeOffset+1:lastOffset])
lastOffset = beforeOffset
} else if beforeOffset == 0 && beforeOffset != lastOffset {
beforeLines = append(beforeLines, document[beforeOffset:lastOffset])
}
}
var afterLines [][]byte
document = document[offset+len(highlight):]
for afterOffset, lastOffset := 0, 0; afterOffset < len(document) && len(afterLines) <= linesAround; afterOffset++ {
if document[afterOffset] == '\n' {
afterLines = append(afterLines, document[lastOffset:afterOffset])
afterOffset++ // skip \n
lastOffset = afterOffset
} else if afterOffset == len(document)-1 && lastOffset != afterOffset+1 {
afterLines = append(afterLines, document[lastOffset:afterOffset+1])
}
}
return beforeLines, afterLines
}
func positionAtEnd(b []byte) (row int, column int) {
row = 1
column = 1
for _, c := range b {
if c == '\n' {
row++
column = 1
} else {
column++
}
}
return
}
+144
View File
@@ -0,0 +1,144 @@
package toml
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDecodeError(t *testing.T) {
examples := []struct {
desc string
doc [3]string
msg string
expected string
}{
{
desc: "no context",
doc: [3]string{"", "morning", ""},
msg: "this is wrong",
expected: `
1| morning
| ~~~~~~~ this is wrong`,
},
{
desc: "one line",
doc: [3]string{"good ", "morning", " everyone"},
msg: "this is wrong",
expected: `
1| good morning everyone
| ~~~~~~~ this is wrong`,
},
{
desc: "exactly 3 lines",
doc: [3]string{`line1
line2
line3
before `, "highlighted", ` after
post line 1
post line 2
post line 3`},
msg: "this is wrong",
expected: `
1| line1
2| line2
3| line3
4| before highlighted after
| ~~~~~~~~~~~ this is wrong
5| post line 1
6| post line 2
7| post line 3`,
},
{
desc: "more than 3 lines",
doc: [3]string{`should not be seen1
should not be seen2
line1
line2
line3
before `, "highlighted", ` after
post line 1
post line 2
post line 3
should not be seen3
should not be seen4`},
msg: "this is wrong",
expected: `
3| line1
4| line2
5| line3
6| before highlighted after
| ~~~~~~~~~~~ this is wrong
7| post line 1
8| post line 2
9| post line 3`,
},
{
desc: "more than 10 total lines",
doc: [3]string{`should not be seen 0
should not be seen1
should not be seen2
should not be seen3
line1
line2
line3
before `, "highlighted", ` after
post line 1
post line 2
post line 3
should not be seen3
should not be seen4`},
msg: "this is wrong",
expected: `
5| line1
6| line2
7| line3
8| before highlighted after
| ~~~~~~~~~~~ this is wrong
9| post line 1
10| post line 2
11| post line 3`,
},
{
desc: "last line of more than 10",
doc: [3]string{`should not be seen
should not be seen
should not be seen
should not be seen
should not be seen
should not be seen
should not be seen
line1
line2
line3
before `, "highlighted", ``},
msg: "this is wrong",
expected: `
8| line1
9| line2
10| line3
11| before highlighted
| ~~~~~~~~~~~ this is wrong
`,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
b := bytes.Buffer{}
b.Write([]byte(e.doc[0]))
start := b.Len()
b.Write([]byte(e.doc[1]))
end := b.Len()
b.Write([]byte(e.doc[2]))
doc := b.Bytes()
hl := doc[start:end]
err := decodeErrorFromHighlight(doc, hl, e.msg)
derr := err.(*DecodeError)
assert.Equal(t, strings.Trim(e.expected, "\n"), derr.String())
})
}
}
@@ -1,4 +1,4 @@
package errors
package unsafe
import (
"fmt"
@@ -8,12 +8,10 @@ import (
const maxInt = uintptr(int(^uint(0) >> 1))
func UnsafeSubsliceOffset(data []byte, subslice []byte) int {
func SubsliceOffset(data []byte, subslice []byte) int {
datap := (*reflect.SliceHeader)(unsafe.Pointer(&data))
hlp := (*reflect.SliceHeader)(unsafe.Pointer(&subslice))
if hlp.Data < datap.Data {
panic(fmt.Errorf("subslice address (%d) is before data address (%d)", hlp.Data, datap.Data))
}
@@ -29,7 +27,7 @@ func UnsafeSubsliceOffset(data []byte, subslice []byte) int {
panic(fmt.Errorf("slice offset (%d) is farther than data length (%d)", intoffset, datap.Len))
}
if intoffset + hlp.Len > datap.Len {
if intoffset+hlp.Len > datap.Len {
panic(fmt.Errorf("slice ends (%d+%d) is farther than data length (%d)", intoffset, hlp.Len, datap.Len))
}
@@ -1,4 +1,4 @@
package errors_test
package unsafe_test
import (
"testing"
@@ -6,13 +6,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pelletier/go-toml/v2/internal/errors"
"github.com/pelletier/go-toml/v2/internal/unsafe"
)
func TestUnsafeSubsliceOffsetValid(t *testing.T) {
examples := []struct{
desc string
test func() ([]byte, []byte)
examples := []struct {
desc string
test func() ([]byte, []byte)
offset int
}{
{
@@ -28,14 +28,14 @@ func TestUnsafeSubsliceOffsetValid(t *testing.T) {
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
offset := errors.UnsafeSubsliceOffset(d, s)
offset := unsafe.SubsliceOffset(d, s)
assert.Equal(t, e.offset, offset)
})
}
}
func TestUnsafeSubsliceOffsetInvalid(t *testing.T) {
examples := []struct{
examples := []struct {
desc string
test func() ([]byte, []byte)
}{
@@ -72,7 +72,7 @@ func TestUnsafeSubsliceOffsetInvalid(t *testing.T) {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
require.Panics(t, func() {
errors.UnsafeSubsliceOffset(d, s)
unsafe.SubsliceOffset(d, s)
})
})
}