a646ffd9fa
Thread byte offset information through all error creation sites, eliminating the need for SubsliceOffset to recover position from pointer comparison. Changes: - Add Offset field to ParserError struct - Add offset parameter to NewParserError - Add Parser.offsetOf helper for suffix-length arithmetic - Thread base offset through scanner functions (scanComment, scanBasicString, scanMultilineBasicString, scanLiteralString, scanMultilineLiteralString, scanWindowsNewline) - Thread base offset through standalone functions (expect, hexToRune) - Thread base offset through all decode functions (parseInteger, parseFloat, parseLocalDate, parseLocalTime, parseLocalDateTime, parseDateTime, checkAndRemoveUnderscores*) - Update all unmarshaler call sites to pass value.Raw.Offset - Update localtime.go UnmarshalText methods with base=0 - Update strict.go to populate Offset from key ranges - Change wrapDecodeError to read de.Offset directly - Change Utf8TomlValidAlreadyEscaped to return int index (-1 if valid) instead of a byte subslice - Unexport SubsliceOffset (now only used internally by Range()) This makes error positions self-describing: each ParserError carries its own byte offset, so callers no longer need the original document slice and address arithmetic to determine where an error occurred. Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
419 lines
8.9 KiB
Go
419 lines
8.9 KiB
Go
package toml
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/pelletier/go-toml/v2/internal/assert"
|
|
"github.com/pelletier/go-toml/v2/unstable"
|
|
)
|
|
|
|
//nolint:funlen
|
|
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
|
|
`,
|
|
},
|
|
{
|
|
desc: "handle empty lines in the before/after blocks",
|
|
doc: [3]string{
|
|
`line1
|
|
|
|
line 2
|
|
before `, "highlighted", ` after
|
|
line 3
|
|
|
|
line 4
|
|
line 5`,
|
|
},
|
|
expected: `1| line1
|
|
2|
|
|
3| line 2
|
|
4| before highlighted after
|
|
| ~~~~~~~~~~~
|
|
5| line 3
|
|
6|
|
|
7| line 4`,
|
|
},
|
|
{
|
|
desc: "handle remainder of the error line when there is only one line",
|
|
doc: [3]string{`P=`, `[`, `#`},
|
|
msg: "array is incomplete",
|
|
expected: `1| P=[#
|
|
| ~ array is incomplete`,
|
|
},
|
|
}
|
|
|
|
for _, e := range examples {
|
|
e := e
|
|
t.Run(e.desc, func(t *testing.T) {
|
|
b := bytes.Buffer{}
|
|
b.WriteString(e.doc[0])
|
|
start := b.Len()
|
|
b.WriteString(e.doc[1])
|
|
end := b.Len()
|
|
b.WriteString(e.doc[2])
|
|
doc := b.Bytes()
|
|
hl := doc[start:end]
|
|
|
|
err := wrapDecodeError(doc, &unstable.ParserError{
|
|
Highlight: hl,
|
|
Offset: start,
|
|
Message: e.msg,
|
|
})
|
|
|
|
var derr *DecodeError
|
|
if !errors.As(err, &derr) {
|
|
t.Errorf("error not in expected format")
|
|
|
|
return
|
|
}
|
|
|
|
assert.Equal(t, strings.Trim(e.expected, "\n"), derr.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeError_Accessors(t *testing.T) {
|
|
e := DecodeError{
|
|
message: "foo",
|
|
line: 1,
|
|
column: 2,
|
|
key: []string{"one", "two"},
|
|
human: "bar",
|
|
}
|
|
assert.Equal(t, "toml: foo", e.Error())
|
|
r, c := e.Position()
|
|
assert.Equal(t, 1, r)
|
|
assert.Equal(t, 2, c)
|
|
assert.Equal(t, Key{"one", "two"}, e.Key())
|
|
assert.Equal(t, "bar", e.String())
|
|
}
|
|
|
|
func TestDecodeError_DuplicateContent(t *testing.T) {
|
|
// This test verifies that when the same content appears multiple times
|
|
// in the document, the error correctly points to the actual location
|
|
// of the error, not the first occurrence of the content.
|
|
//
|
|
// The document has "1__2" on line 1 and "3__4" on line 2.
|
|
// Both have "__" which is invalid, but we want to ensure errors
|
|
// on line 2 report line 2, not line 1.
|
|
|
|
doc := `a = 1
|
|
b = 3__4`
|
|
|
|
var v map[string]int
|
|
err := Unmarshal([]byte(doc), &v)
|
|
|
|
var derr *DecodeError
|
|
if !errors.As(err, &derr) {
|
|
t.Fatal("error not in expected format")
|
|
}
|
|
|
|
row, col := derr.Position()
|
|
// The error should be on line 2 where "3__4" is
|
|
if row != 2 {
|
|
t.Errorf("expected error on row 2, got row %d", row)
|
|
}
|
|
// Column should point to the "__" part (after "3")
|
|
if col < 5 {
|
|
t.Errorf("expected error at column >= 5, got column %d", col)
|
|
}
|
|
}
|
|
|
|
func TestDecodeError_Position(t *testing.T) {
|
|
// Test that error positions are correctly reported for various error locations
|
|
examples := []struct {
|
|
name string
|
|
doc string
|
|
expectedRow int
|
|
minCol int
|
|
}{
|
|
{
|
|
name: "error on first line",
|
|
doc: `a = 1__2`,
|
|
expectedRow: 1,
|
|
minCol: 5,
|
|
},
|
|
{
|
|
name: "error on second line",
|
|
doc: "a = 1\nb = 2__3",
|
|
expectedRow: 2,
|
|
minCol: 5,
|
|
},
|
|
{
|
|
name: "error on third line",
|
|
doc: "a = 1\nb = 2\nc = 3__4",
|
|
expectedRow: 3,
|
|
minCol: 5,
|
|
},
|
|
{
|
|
name: "missing equals on last line without trailing newline",
|
|
doc: "a = 1\nb = 2\nc",
|
|
expectedRow: 3,
|
|
minCol: 1,
|
|
},
|
|
}
|
|
|
|
for _, e := range examples {
|
|
t.Run(e.name, func(t *testing.T) {
|
|
var v map[string]int
|
|
err := Unmarshal([]byte(e.doc), &v)
|
|
|
|
var derr *DecodeError
|
|
if !errors.As(err, &derr) {
|
|
t.Fatal("error not in expected format")
|
|
}
|
|
|
|
row, col := derr.Position()
|
|
assert.Equal(t, e.expectedRow, row)
|
|
if col < e.minCol {
|
|
t.Errorf("expected column >= %d, got %d", e.minCol, col)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeError_PositionAfterComments(t *testing.T) {
|
|
examples := []struct {
|
|
name string
|
|
doc string
|
|
expectedRow int
|
|
expectedCol int
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "invalid key after comment",
|
|
doc: "# comment\n= \"value\"",
|
|
expectedRow: 2,
|
|
expectedCol: 1,
|
|
errContains: "invalid character at start of key",
|
|
},
|
|
{
|
|
name: "invalid key after multiple comments",
|
|
doc: "# line 1\n# line 2\n= \"value\"",
|
|
expectedRow: 3,
|
|
expectedCol: 1,
|
|
errContains: "invalid character at start of key",
|
|
},
|
|
{
|
|
name: "invalid key after valid assignment and comment",
|
|
doc: "a = 1\n# comment\n= \"value\"",
|
|
expectedRow: 3,
|
|
expectedCol: 1,
|
|
errContains: "invalid character at start of key",
|
|
},
|
|
{
|
|
name: "invalid key on first line",
|
|
doc: "= \"value\"",
|
|
expectedRow: 1,
|
|
expectedCol: 1,
|
|
errContains: "invalid character at start of key",
|
|
},
|
|
{
|
|
name: "invalid key with leading whitespace",
|
|
doc: "# comment\n = \"value\"",
|
|
expectedRow: 2,
|
|
expectedCol: 3,
|
|
errContains: "invalid character at start of key",
|
|
},
|
|
}
|
|
|
|
for _, e := range examples {
|
|
t.Run(e.name, func(t *testing.T) {
|
|
var v map[string]interface{}
|
|
err := Unmarshal([]byte(e.doc), &v)
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
|
|
var derr *DecodeError
|
|
if !errors.As(err, &derr) {
|
|
t.Fatalf("expected DecodeError, got %T: %v", err, err)
|
|
}
|
|
|
|
row, col := derr.Position()
|
|
if row != e.expectedRow {
|
|
t.Errorf("row: got %d, want %d (error: %s)", row, e.expectedRow, derr.String())
|
|
}
|
|
if col != e.expectedCol {
|
|
t.Errorf("col: got %d, want %d (error: %s)", col, e.expectedCol, derr.String())
|
|
}
|
|
if !strings.Contains(derr.Error(), e.errContains) {
|
|
t.Errorf("error %q does not contain %q", derr.Error(), e.errContains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeError_HumanStringAfterComments(t *testing.T) {
|
|
doc := "# comment\n= \"value\""
|
|
var v map[string]interface{}
|
|
err := Unmarshal([]byte(doc), &v)
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
|
|
var derr *DecodeError
|
|
if !errors.As(err, &derr) {
|
|
t.Fatalf("expected DecodeError, got %T: %v", err, err)
|
|
}
|
|
|
|
human := derr.String()
|
|
if !strings.Contains(human, "= \"value\"") {
|
|
t.Errorf("human-readable error should show the offending line, got:\n%s", human)
|
|
}
|
|
if !strings.Contains(human, "2|") {
|
|
t.Errorf("human-readable error should reference line 2, got:\n%s", human)
|
|
}
|
|
}
|
|
|
|
func TestStrictErrorUnwrap(t *testing.T) {
|
|
fo := bytes.NewBufferString(`
|
|
Missing = 1
|
|
OtherMissing = 1
|
|
`)
|
|
var out struct{}
|
|
err := NewDecoder(fo).DisallowUnknownFields().Decode(&out)
|
|
assert.Error(t, err)
|
|
|
|
strictErr := &StrictMissingError{}
|
|
assert.True(t, errors.As(err, &strictErr))
|
|
|
|
assert.Equal(t, 2, len(strictErr.Unwrap()))
|
|
}
|
|
|
|
func ExampleDecodeError() {
|
|
doc := `name = 123__456`
|
|
|
|
s := map[string]interface{}{}
|
|
err := Unmarshal([]byte(doc), &s)
|
|
|
|
fmt.Println(err)
|
|
|
|
var derr *DecodeError
|
|
if errors.As(err, &derr) {
|
|
fmt.Println(derr.String())
|
|
row, col := derr.Position()
|
|
fmt.Println("error occurred at row", row, "column", col)
|
|
}
|
|
// Output:
|
|
// toml: number must have at least one digit between underscores
|
|
// 1| name = 123__456
|
|
// | ~~ number must have at least one digit between underscores
|
|
// error occurred at row 1 column 11
|
|
}
|