diff --git a/errors_test.go b/errors_test.go index 3703bd0..22220a9 100644 --- a/errors_test.go +++ b/errors_test.go @@ -286,6 +286,100 @@ func TestDecodeError_Position(t *testing.T) { } } +func TestDecodeError_PositionAfterComments(t *testing.T) { + examples := []struct { + name string + doc string + expectedRow int + expectedCol int + errContains string + }{ + { + name: "invalid key after comment", + doc: "# comment\n= \"value\"", + expectedRow: 2, + expectedCol: 1, + errContains: "invalid character at start of key", + }, + { + name: "invalid key after multiple comments", + doc: "# line 1\n# line 2\n= \"value\"", + expectedRow: 3, + expectedCol: 1, + errContains: "invalid character at start of key", + }, + { + name: "invalid key after valid assignment and comment", + doc: "a = 1\n# comment\n= \"value\"", + expectedRow: 3, + expectedCol: 1, + errContains: "invalid character at start of key", + }, + { + name: "invalid key on first line", + doc: "= \"value\"", + expectedRow: 1, + expectedCol: 1, + errContains: "invalid character at start of key", + }, + { + name: "invalid key with leading whitespace", + doc: "# comment\n = \"value\"", + expectedRow: 2, + expectedCol: 3, + errContains: "invalid character at start of key", + }, + } + + for _, e := range examples { + t.Run(e.name, func(t *testing.T) { + var v map[string]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("expected DecodeError, got %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 TestDecodeError_HumanStringAfterComments(t *testing.T) { + doc := "# comment\n= \"value\"" + var v map[string]interface{} + err := Unmarshal([]byte(doc), &v) + if err == nil { + t.Fatal("expected an error") + } + + var derr *DecodeError + if !errors.As(err, &derr) { + t.Fatalf("expected DecodeError, got %T: %v", err, err) + } + + human := derr.String() + if !strings.Contains(human, "= \"value\"") { + t.Errorf("human-readable error should show the offending line, got:\n%s", human) + } + if !strings.Contains(human, "2|") { + t.Errorf("human-readable error should reference line 2, got:\n%s", human) + } +} + func TestStrictErrorUnwrap(t *testing.T) { fo := bytes.NewBufferString(` Missing = 1 diff --git a/unstable/parser.go b/unstable/parser.go index e7c68dc..953941a 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,18 @@ 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 share the same backing array as p.data. 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() + offset := int(subPtr - dataPtr) + if offset < 0 || offset > len(p.data) { + panic("subslice is not within parser data") + } + return offset } // 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..7ffab8f 100644 --- a/unstable/parser_test.go +++ b/unstable/parser_test.go @@ -1,6 +1,7 @@ package unstable import ( + "errors" "fmt" "strconv" "strings" @@ -673,6 +674,98 @@ key3 = "value3" assert.Equal(t, []string{"key1", "key2", "key3"}, keys) } +func TestRangeOffsetAfterComment(t *testing.T) { + input := []byte("# comment\n= \"value\"") + + p := Parser{} + p.Reset(input) + for p.NextExpression() { + } + err := p.Error() + if err == nil { + t.Fatal("expected an error") + } + var perr *ParserError + if !errors.As(err, &perr) { + t.Fatalf("expected ParserError, got %T", err) + } + r := p.Range(perr.Highlight) + shape := p.Shape(r) + + if r.Offset != 10 { + t.Errorf("Range offset: got %d, want 10", r.Offset) + } + if shape.Start.Line != 2 || shape.Start.Column != 1 { + t.Errorf("position: got %d:%d, want 2:1", shape.Start.Line, shape.Start.Column) + } +} + +func TestErrorHighlightPositions(t *testing.T) { + examples := []struct { + desc string + input string + wantLine int + wantColumn int + }{ + { + desc: "invalid key start after comment", + input: "# comment\n= \"value\"", + wantLine: 2, + wantColumn: 1, + }, + { + desc: "invalid key start on first line", + input: "= \"value\"", + wantLine: 1, + wantColumn: 1, + }, + { + desc: "invalid key after multiple comments", + input: "# comment 1\n# comment 2\n= \"value\"", + wantLine: 3, + wantColumn: 1, + }, + { + desc: "invalid key after valid key-value", + input: "a = 1\n= \"value\"", + wantLine: 2, + wantColumn: 1, + }, + { + desc: "invalid key after whitespace on line", + input: "a = 1\n = \"value\"", + wantLine: 2, + wantColumn: 3, + }, + } + + 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") + } + var perr *ParserError + if !errors.As(err, &perr) { + t.Fatalf("expected ParserError, got %T", err) + } + r := p.Range(perr.Highlight) + shape := p.Shape(r) + + if shape.Start.Line != e.wantLine { + t.Errorf("line: got %d, want %d", shape.Start.Line, e.wantLine) + } + if shape.Start.Column != e.wantColumn { + t.Errorf("column: got %d, want %d", shape.Start.Column, e.wantColumn) + } + }) + } +} + func ExampleParser() { doc := ` hello = "world"