From 0248fc4c8c0ae7092ba4a93294261136f724cd4a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 11:52:16 +0000 Subject: [PATCH] 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 --- errors_test.go | 69 ++++++++++++++++++++++++++++++++++++ unstable/parser.go | 11 ++++-- unstable/parser_test.go | 78 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 3 deletions(-) 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) + } + }) + } +}