Fix incorrect error positions for non-suffix subslices in unstable.Parser.Range (#1047)

The unsafe removal (#1021) replaced danger.SubsliceOffset (pointer
arithmetic) with len(p.data)-len(b), which only works for suffix
slices. Parser.Range is called with arbitrary interior subslices
(e.g. ParserError.Highlight), so the offset was wrong whenever the
error occurred after previously scanned content like comments.

Fix by using reflect.ValueOf().Pointer() to recover the actual data
pointer, matching the approach already used in errors.go.

https://claude.ai/code/session_01EXYfFXc3DDGpQ27sWdXTKq
This commit is contained in:
Claude
2026-04-12 11:52:16 +00:00
parent f36a3ece9e
commit 0248fc4c8c
3 changed files with 155 additions and 3 deletions
+69
View File
@@ -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`
+8 -3
View File
@@ -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.
+78
View File
@@ -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)
}
})
}
}