Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package toml

import (
"fmt"
"reflect"
"strconv"
"strings"

"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/unstable"
)

Expand Down Expand Up @@ -58,14 +58,14 @@ func (s *StrictMissingError) String() string {
//
// Implements errors.Join() interface.
func (s *StrictMissingError) Unwrap() []error {
errs := make([]error, 0, len(s.Errors))
errs := make([]error, len(s.Errors))
for i := range s.Errors {
errs = append(errs, &s.Errors[i])
errs[i] = &s.Errors[i]
}
return errs
}

// Key is a slice of strings that represents a path to a value in a TOML document.
// Key represents a TOML key as a sequence of key parts.
type Key []string

// Error returns the error message contained in the DecodeError.
Expand Down Expand Up @@ -93,12 +93,14 @@ func (e *DecodeError) Key() Key {
// wrapDecodeError creates a DecodeError referencing a highlighted
// range of bytes from document.
//
// Highlight needs to be a sub-slice of document, or this function panics.
// highlight needs to be a sub-slice of document, or this function panics.
//
// The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated.
//
//nolint:funlen
func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
offset := danger.SubsliceOffset(document, de.Highlight)
offset := subsliceOffset(document, de.Highlight)

errMessage := de.Error()
errLine, errColumn := positionAtEnd(document[:offset])
Expand Down Expand Up @@ -258,5 +260,24 @@ func positionAtEnd(b []byte) (row int, column int) {
}
}

return row, column
return
}

// 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 {
if len(subslice) == 0 {
return 0
}

// Use reflect to get the data pointers of both slices.
// This is safe because we're only reading the pointer values for comparison.
dataPtr := reflect.ValueOf(data).Pointer()
subPtr := reflect.ValueOf(subslice).Pointer()

offset := int(subPtr - dataPtr)
if offset < 0 || offset > len(data) {
panic("subslice is not within data")
}
return offset
}
79 changes: 79 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/pelletier/go-toml/v2/unstable"
)

//nolint:funlen
func TestDecodeError(t *testing.T) {
examples := []struct {
desc string
Expand Down Expand Up @@ -201,6 +202,84 @@ func TestDecodeError_Accessors(t *testing.T) {
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,
},
}

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 TestStrictErrorUnwrap(t *testing.T) {
fo := bytes.NewBufferString(`
Missing = 1
Expand Down
64 changes: 0 additions & 64 deletions internal/danger/danger.go

This file was deleted.

Loading