Remove optional offset and fallback, guarantee offset by construction

ParserError.Offset is now a plain exported int field, always set:
- The parser sets it via setErrOffset() when capturing parse errors
- strict.go sets it from the key's Raw range at construction
- wrapDecodeError computes it inline from cap(document) - cap(highlight)

This eliminates:
- The SetOffset/Offset() accessor methods and offsetValid flag
- The subsliceOffset fallback function in errors.go
- Any conditional logic around whether the offset is present

The offset is guaranteed by construction at every path that creates
or consumes a ParserError.

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
This commit is contained in:
Cursor Agent
2026-04-12 17:25:32 +00:00
parent f7136d052b
commit 96ac48eb74
5 changed files with 22 additions and 50 deletions
+1 -10
View File
@@ -99,10 +99,7 @@ func (e *DecodeError) Key() Key {
// //
//nolint:funlen //nolint:funlen
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError { func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
offset, ok := de.Offset() offset := cap(document) - cap(de.Highlight)
if !ok {
offset = subsliceOffset(document, de.Highlight)
}
errMessage := de.Error() errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset]) errLine, errColumn := positionAtEnd(document[:offset])
@@ -264,9 +261,3 @@ func positionAtEnd(b []byte) (row int, column int) {
return row, column return row, column
} }
// subsliceOffset returns the byte offset of subslice within data.
// subslice must share the same backing array as data.
func subsliceOffset(data []byte, subslice []byte) int {
return cap(data) - cap(subslice)
}
+1
View File
@@ -172,6 +172,7 @@ line 5`,
err := wrapDecodeError(doc, &unstable.ParserError{ err := wrapDecodeError(doc, &unstable.ParserError{
Highlight: hl, Highlight: hl,
Message: e.msg, Message: e.msg,
Offset: start,
}) })
var derr *DecodeError var derr *DecodeError
+6 -8
View File
@@ -55,13 +55,12 @@ func (s *strict) MissingTable(node *unstable.Node) {
} }
highlight, offset := s.keyLocation(node) highlight, offset := s.keyLocation(node)
pe := unstable.ParserError{ s.missing = append(s.missing, unstable.ParserError{
Highlight: highlight, Highlight: highlight,
Message: "missing table", Message: "missing table",
Key: s.key.Key(), Key: s.key.Key(),
} Offset: offset,
pe.SetOffset(offset) })
s.missing = append(s.missing, pe)
} }
func (s *strict) MissingField(node *unstable.Node) { func (s *strict) MissingField(node *unstable.Node) {
@@ -70,13 +69,12 @@ func (s *strict) MissingField(node *unstable.Node) {
} }
highlight, offset := s.keyLocation(node) highlight, offset := s.keyLocation(node)
pe := unstable.ParserError{ s.missing = append(s.missing, unstable.ParserError{
Highlight: highlight, Highlight: highlight,
Message: "missing field", Message: "missing field",
Key: s.key.Key(), Key: s.key.Key(),
} Offset: offset,
pe.SetOffset(offset) })
s.missing = append(s.missing, pe)
} }
func (s *strict) Error(doc []byte) error { func (s *strict) Error(doc []byte) error {
+11 -25
View File
@@ -18,8 +18,9 @@ type ParserError struct {
Message string Message string
Key []string // optional Key []string // optional
offset int // Offset is the byte offset of Highlight within the document.
offsetValid bool // Set by the parser when the error is captured.
Offset int
} }
// Error is the implementation of the error interface. // Error is the implementation of the error interface.
@@ -27,21 +28,6 @@ func (e *ParserError) Error() string {
return e.Message return e.Message
} }
// SetOffset records the byte offset of the error highlight within the
// document. Used by the parser to cache position information so
// downstream consumers don't need to re-derive it from pointers.
func (e *ParserError) SetOffset(offset int) {
e.offset = offset
e.offsetValid = true
}
// Offset returns the byte offset of the error highlight within the
// document, if it was previously set by the parser. The boolean
// indicates whether the offset is valid.
func (e *ParserError) Offset() (int, bool) {
return e.offset, e.offsetValid
}
// NewParserError is a convenience function to create a ParserError // NewParserError is a convenience function to create a ParserError
// //
// Warning: Highlight needs to be a subslice of Parser.data, so only slices // Warning: Highlight needs to be a subslice of Parser.data, so only slices
@@ -145,11 +131,15 @@ func (p *Parser) NextExpression() bool {
p.left, p.err = p.parseNewline(p.left) p.left, p.err = p.parseNewline(p.left)
} }
if len(p.left) == 0 || p.err != nil { if p.err != nil {
p.setErrOffset() p.setErrOffset()
return false return false
} }
if len(p.left) == 0 {
return false
}
p.ref, p.left, p.err = p.parseExpression(p.left) p.ref, p.left, p.err = p.parseExpression(p.left)
if p.err != nil { if p.err != nil {
@@ -176,9 +166,8 @@ func (p *Parser) Error() error {
return p.err return p.err
} }
// setErrOffset computes and caches the byte offset of the error's // setErrOffset sets the byte offset on the parser error from the
// highlight within p.data, so downstream consumers can use it // highlight's position within p.data.
// without pointer arithmetic.
func (p *Parser) setErrOffset() { func (p *Parser) setErrOffset() {
if p.err == nil { if p.err == nil {
return return
@@ -187,10 +176,7 @@ func (p *Parser) setErrOffset() {
if !errors.As(p.err, &perr) { if !errors.As(p.err, &perr) {
return return
} }
if perr.offsetValid || len(perr.Highlight) == 0 { perr.Offset = p.subsliceOffset(perr.Highlight)
return
}
perr.SetOffset(p.subsliceOffset(perr.Highlight))
} }
// Position describes a position in the input. // Position describes a position in the input.
+3 -7
View File
@@ -766,7 +766,7 @@ func TestErrorHighlightPositions(t *testing.T) {
} }
} }
func TestParserError_CachedOffset(t *testing.T) { func TestParserError_Offset(t *testing.T) {
examples := []struct { examples := []struct {
desc string desc string
input string input string
@@ -803,12 +803,8 @@ func TestParserError_CachedOffset(t *testing.T) {
if !errors.As(err, &perr) { if !errors.As(err, &perr) {
t.Fatalf("expected ParserError, got %T", err) t.Fatalf("expected ParserError, got %T", err)
} }
offset, ok := perr.Offset() if perr.Offset != e.wantOffset {
if !ok { t.Errorf("offset: got %d, want %d", perr.Offset, e.wantOffset)
t.Fatal("expected offset to be set")
}
if offset != e.wantOffset {
t.Errorf("cached offset: got %d, want %d", offset, e.wantOffset)
} }
}) })
} }