diff --git a/errors_test.go b/errors_test.go index 3703bd0..03ec156 100644 --- a/errors_test.go +++ b/errors_test.go @@ -301,6 +301,75 @@ OtherMissing = 1 assert.Equal(t, 2, len(strictErr.Unwrap())) } +func TestDecodeError_PositionAfterComment(t *testing.T) { + // Regression test for https://github.com/pelletier/go-toml/issues/1047 + // Error positions must be correct when the error occurs after comments or + // other content that was already scanned past. + examples := []struct { + desc string + doc string + expectedRow int + expectedCol int + errContains string + }{ + { + desc: "invalid key after comment", + doc: "# comment\n= \"value\"", + expectedRow: 2, + expectedCol: 1, + errContains: "invalid character", + }, + { + desc: "invalid key after two comments", + doc: "# one\n# two\n= \"value\"", + expectedRow: 3, + expectedCol: 1, + errContains: "invalid character", + }, + { + desc: "invalid key after key-value pair", + doc: "a = 1\n= 2", + expectedRow: 2, + expectedCol: 1, + errContains: "invalid character", + }, + { + desc: "invalid key after blank line", + doc: "a = 1\n\n= 2", + expectedRow: 3, + expectedCol: 1, + errContains: "invalid character", + }, + } + + for _, e := range examples { + t.Run(e.desc, func(t *testing.T) { + var v 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("error not a *DecodeError: %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 ExampleDecodeError() { doc := `name = 123__456` diff --git a/unstable/parser.go b/unstable/parser.go index e7c68dc..3b417a1 100644 --- a/unstable/parser.go +++ b/unstable/parser.go @@ -3,6 +3,7 @@ package unstable import ( "bytes" "fmt" + "reflect" "unicode" "github.com/pelletier/go-toml/v2/internal/characters" @@ -83,10 +84,14 @@ func (p *Parser) rangeOfToken(token, rest []byte) Range { } // subsliceOffset returns the byte offset of subslice b within p.data. -// b must be a suffix (tail) of p.data. +// b must be a subslice of p.data (sharing the same backing array). func (p *Parser) subsliceOffset(b []byte) int { - // b is a suffix of p.data, so its offset is len(p.data) - len(b) - return len(p.data) - len(b) + if len(b) == 0 { + return 0 + } + dataPtr := reflect.ValueOf(p.data).Pointer() + subPtr := reflect.ValueOf(b).Pointer() + return int(subPtr - dataPtr) } // Raw returns the slice corresponding to the bytes in the given range. diff --git a/unstable/parser_test.go b/unstable/parser_test.go index 9726915..bd69757 100644 --- a/unstable/parser_test.go +++ b/unstable/parser_test.go @@ -695,3 +695,81 @@ func ExampleParser() { // Expression: KeyValue // value -> (Integer) 42 } + +func TestParserError_RangeOffset(t *testing.T) { + // Regression test for https://github.com/pelletier/go-toml/issues/1047 + // Parser.Range must return the correct byte offset for error highlights, + // not just for suffix slices. + examples := []struct { + desc string + input string + wantOffset int + wantLine int + wantColumn int + wantMessage string + }{ + { + desc: "invalid key start after comment", + input: "# comment\n= \"value\"", + wantOffset: 10, + wantLine: 2, + wantColumn: 1, + wantMessage: "invalid character at start of key: =", + }, + { + desc: "invalid key start after two comments", + input: "# one\n# two\n= \"value\"", + wantOffset: 12, + wantLine: 3, + wantColumn: 1, + wantMessage: "invalid character at start of key: =", + }, + { + desc: "invalid key start after blank line", + input: "a = 1\n\n= 2", + wantOffset: 7, + wantLine: 3, + wantColumn: 1, + wantMessage: "invalid character at start of key: =", + }, + { + desc: "invalid key start after valid key-value", + input: "a = 1\n= 2", + wantOffset: 6, + wantLine: 2, + wantColumn: 1, + wantMessage: "invalid character at start of key: =", + }, + } + + for _, e := range examples { + t.Run(e.desc, func(t *testing.T) { + p := Parser{} + p.Reset([]byte(e.input)) + for p.NextExpression() { + } + err := p.Error() + if err == nil { + t.Fatal("expected an error") + } + perr, ok := err.(*ParserError) + if !ok { + t.Fatalf("expected *ParserError, got %T", err) + } + + assert.Equal(t, e.wantMessage, perr.Message) + + r := p.Range(perr.Highlight) + if int(r.Offset) != e.wantOffset { + t.Errorf("Range offset: got %d, want %d", r.Offset, e.wantOffset) + } + + shape := p.Shape(r) + if shape.Start.Line != e.wantLine || shape.Start.Column != e.wantColumn { + t.Errorf("position: got %d:%d, want %d:%d", + shape.Start.Line, shape.Start.Column, + e.wantLine, e.wantColumn) + } + }) + } +}