Compare commits

...

8 Commits

Author SHA1 Message Date
Claude fa98989475 fix: resolve lint issues and improve test coverage for TOML v1.1.0
- Fix dupl lint: add nolint:dupl to parseInlineTable and parseValArray
  which have intentionally similar loop structures
- Fix gci lint: correct alignment in multiline basic string test entries
- Add unmarshaler tests for new TOML v1.1.0 features exercising public
  APIs: multiline inline tables with comments, trailing commas, leading
  commas, error cases (comma at start, missing separator, double comma,
  incomplete table), escape sequences, and type mismatch errors
- Add parser test for inline table comment handling with KeepComments
- Coverage increases from 97.37% to 97.44% vs v2 base

https://claude.ai/code/session_01RdiWykFQdmwkQ2nbLwJwwP
2026-03-29 17:37:29 +00:00
João Fernandes 189bf9820b test: parsing inline table 2026-03-03 14:03:57 +00:00
João Fernandes 8455b10edd test: regen toml-test tests targetting TOML v1.1.0 2026-02-11 11:49:40 +00:00
João Fernandes 6de639d0ae docs: update README 2026-02-11 11:15:44 +00:00
João Fernandes 517ceb4eb8 feat: allow newlines and trailing commas in inline tables
TOML v1.1.0 relaxes inline table syntax to allow newlines, comments,
and trailing commas, matching the existing behavior of arrays.
2026-02-11 11:15:33 +00:00
João Fernandes dd7970eb93 feat: make seconds optional in datetime and time values
TOML v1.1.0 allows times to be specified as HH:MM without the seconds
component (previously HH:MM:SS was required). This applies to local
times, local datetimes, and offset datetimes.
2026-02-11 11:15:16 +00:00
João Fernandes 3405e8a1d9 test: \e escape character 2026-02-11 11:14:59 +00:00
João Fernandes 5794be6251 feat: add \xHH escape sequence support in basic strings
TOML v1.1.0 introduces the \xHH escape notation for basic strings,
allowing two-digit hex escapes for Unicode code points U+0000 to
U+00FF.

We keep emitting \u00XX for backwards compatibility.
2026-02-11 11:14:23 +00:00
8 changed files with 1910 additions and 567 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
Go library for the [TOML](https://toml.io/en/) format. Go library for the [TOML](https://toml.io/en/) format.
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0). This library supports [TOML v1.1.0](https://toml.io/en/v1.1.0).
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues) [🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
@@ -67,7 +67,7 @@ this use-case, go-toml provides [`LocalDate`][tld], [`LocalTime`][tlt], and
making them convenient yet unambiguous structures for their respective TOML making them convenient yet unambiguous structures for their respective TOML
representation. representation.
[ldt]: https://toml.io/en/v1.0.0#local-date-time [ldt]: https://toml.io/en/v1.1.0#local-date-time
[tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate [tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime [tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime [tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime
+21 -16
View File
@@ -162,7 +162,7 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
const localDateTimeByteMinLen = 11 const localDateTimeByteMinLen = 11
if len(b) < localDateTimeByteMinLen { if len(b) < localDateTimeByteMinLen {
return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]") return dt, nil, unstable.NewParserError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM[:SS[.NNNNNNNNN]]")
} }
date, err := parseLocalDate(b[:10]) date, err := parseLocalDate(b[:10])
@@ -194,10 +194,10 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
t LocalTime t LocalTime
) )
// check if b matches to have expected format HH:MM:SS[.NNNNNN] // check if b matches to have expected format HH:MM[:SS[.NNNNNN]]
const localTimeByteLen = 8 const localTimeByteMinLen = 5
if len(b) < localTimeByteLen { if len(b) < localTimeByteMinLen {
return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]") return t, nil, unstable.NewParserError(b, "times are expected to have the format HH:MM[:SS[.NNNNNN]]")
} }
var err error var err error
@@ -221,20 +221,25 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
if t.Minute > 59 { if t.Minute > 59 {
return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 59") return t, nil, unstable.NewParserError(b[3:5], "minutes cannot be greater 59")
} }
if b[5] != ':' {
return t, nil, unstable.NewParserError(b[5:6], "expecting colon between minutes and seconds")
}
t.Second, err = parseDecimalDigits(b[6:8]) b = b[5:]
if err != nil {
return t, nil, err
}
if t.Second > 59 { if len(b) >= 1 && b[0] == ':' {
return t, nil, unstable.NewParserError(b[6:8], "seconds cannot be greater than 59") if len(b) < 3 {
} return t, nil, unstable.NewParserError(b, "incomplete seconds")
}
b = b[8:] t.Second, err = parseDecimalDigits(b[1:3])
if err != nil {
return t, nil, err
}
if t.Second > 59 {
return t, nil, unstable.NewParserError(b[1:3], "seconds cannot be greater than 59")
}
b = b[3:]
}
if len(b) >= 1 && b[0] == '.' { if len(b) >= 1 && b[0] == '.' {
frac := 0 frac := 0
+7
View File
@@ -67,6 +67,13 @@ func TestLocalTime_UnmarshalMarshalText(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestLocalTime_UnmarshalText_WithoutSeconds(t *testing.T) {
d := toml.LocalTime{}
err := d.UnmarshalText([]byte("14:15"))
assert.NoError(t, err)
assert.Equal(t, toml.LocalTime{14, 15, 0, 0, 0}, d)
}
func TestLocalTime_RoundTrip(t *testing.T) { func TestLocalTime_RoundTrip(t *testing.T) {
var d struct{ A toml.LocalTime } var d struct{ A toml.LocalTime }
err := toml.Unmarshal([]byte("a=20:12:01.500"), &d) err := toml.Unmarshal([]byte("a=20:12:01.500"), &d)
+2 -2
View File
@@ -1,5 +1,5 @@
//go:generate go run github.com/toml-lang/toml-test/cmd/toml-test@v1.6.0 -copy ./tests //go:generate go run github.com/toml-lang/toml-test/v2/cmd/toml-test@v2.1.0 copy -toml 1.1 ./tests
//go:generate go run ./cmd/tomltestgen/main.go -r v1.6.0 -o toml_testgen_test.go //go:generate go run ./cmd/tomltestgen/main.go -r v2.1.0 -o toml_testgen_test.go
package toml_test package toml_test
+1151 -532
View File
File diff suppressed because it is too large Load Diff
+479 -1
View File
@@ -600,6 +600,96 @@ foo = "bar"`,
} }
}, },
}, },
{
desc: "local-time without seconds",
input: `a = 14:15`,
gen: func() test {
var v map[string]interface{}
return test{
target: &v,
expected: &map[string]interface{}{
"a": toml.LocalTime{Hour: 14, Minute: 15},
},
}
},
},
{
desc: "local-datetime without seconds using T",
input: `a = 2010-02-03T14:15`,
gen: func() test {
var v map[string]interface{}
return test{
target: &v,
expected: &map[string]interface{}{
"a": toml.LocalDateTime{
LocalDate: toml.LocalDate{2010, 2, 3},
LocalTime: toml.LocalTime{Hour: 14, Minute: 15},
},
},
}
},
},
{
desc: "local-datetime without seconds using space",
input: `a = 2010-02-03 14:15`,
gen: func() test {
var v map[string]interface{}
return test{
target: &v,
expected: &map[string]interface{}{
"a": toml.LocalDateTime{
LocalDate: toml.LocalDate{2010, 2, 3},
LocalTime: toml.LocalTime{Hour: 14, Minute: 15},
},
},
}
},
},
{
desc: "datetime without seconds with Z",
input: `a = 2010-02-03T14:15Z`,
gen: func() test {
var v map[string]time.Time
return test{
target: &v,
expected: &map[string]time.Time{
"a": time.Date(2010, 2, 3, 14, 15, 0, 0, time.UTC),
},
}
},
},
{
desc: "datetime without seconds with offset",
input: `a = 2010-02-03T14:15+05:00`,
gen: func() test {
var v map[string]time.Time
return test{
target: &v,
expected: &map[string]time.Time{
"a": time.Date(2010, 2, 3, 14, 15, 0, 0, time.FixedZone("", 5*3600)),
},
}
},
},
{
desc: "local-time with seconds and fractional regression",
input: `a = 14:15:30.123`,
gen: func() test {
var v map[string]interface{}
return test{
target: &v,
expected: &map[string]interface{}{
"a": toml.LocalTime{Hour: 14, Minute: 15, Second: 30, Nanosecond: 123000000, Precision: 3},
},
}
},
},
{ {
desc: "local-time missing digit", desc: "local-time missing digit",
input: `a = 12:08:0`, input: `a = 12:08:0`,
@@ -759,6 +849,104 @@ huey = 'dewey'
} }
}, },
}, },
{
desc: "basic string escape character",
input: `A = "\e"`,
gen: func() test {
type doc struct {
A string
}
return test{
target: &doc{},
expected: &doc{A: "\x1B"},
}
},
},
{
desc: "multiline basic string escape character",
input: `A = """\e"""`,
gen: func() test {
type doc struct {
A string
}
return test{
target: &doc{},
expected: &doc{A: "\x1B"},
}
},
},
{
desc: "escape character combined with bracket",
input: `A = "\e["`,
gen: func() test {
type doc struct {
A string
}
return test{
target: &doc{},
expected: &doc{A: "\x1B["},
}
},
},
{
desc: "basic string hex escape lowercase letter",
input: `A = "\x61"`,
gen: func() test {
type doc struct {
A string
}
return test{
target: &doc{},
expected: &doc{A: "a"},
}
},
},
{
desc: "basic string hex escape null byte",
input: `A = "\x00"`,
gen: func() test {
type doc struct {
A string
}
return test{
target: &doc{},
expected: &doc{A: "\x00"},
}
},
},
{
desc: "basic string hex escape max value",
input: `A = "\xFF"`,
gen: func() test {
type doc struct {
A string
}
return test{
target: &doc{},
expected: &doc{A: "\u00FF"},
}
},
},
{
desc: "multiline basic string hex escape",
input: `A = """\x61"""`,
gen: func() test {
type doc struct {
A string
}
return test{
target: &doc{},
expected: &doc{A: "a"},
}
},
},
{ {
desc: "spaces around dotted keys", desc: "spaces around dotted keys",
input: "a . b = 1", input: "a . b = 1",
@@ -906,6 +1094,87 @@ B = "data"`,
} }
}, },
}, },
{
desc: "multiline inline table",
input: "Name = {\n First = \"hello\",\n Last = \"world\"\n}",
gen: func() test {
type name struct {
First string
Last string
}
type doc struct {
Name name
}
return test{
target: &doc{},
expected: &doc{Name: name{
First: "hello",
Last: "world",
}},
}
},
},
{
desc: "inline table with trailing comma",
input: `Name = {First = "hello", Last = "world",}`,
gen: func() test {
type name struct {
First string
Last string
}
type doc struct {
Name name
}
return test{
target: &doc{},
expected: &doc{Name: name{
First: "hello",
Last: "world",
}},
}
},
},
{
desc: "multiline inline table with trailing comma and comments",
input: "Name = {\n # first name\n First = \"hello\",\n # last name\n Last = \"world\",\n}",
gen: func() test {
type name struct {
First string
Last string
}
type doc struct {
Name name
}
return test{
target: &doc{},
expected: &doc{Name: name{
First: "hello",
Last: "world",
}},
}
},
},
{
desc: "nested multiline inline tables",
input: "A = {\n B = {\n C = 1,\n },\n}",
gen: func() test {
var v map[string]interface{}
return test{
target: &v,
expected: &map[string]interface{}{
"A": map[string]interface{}{
"B": map[string]interface{}{
"C": int64(1),
},
},
},
}
},
},
{ {
desc: "inline table inside array", desc: "inline table inside array",
input: `Names = [{First = "hello", Last = "world"}, {First = "ab", Last = "cd"}]`, input: `Names = [{First = "hello", Last = "world"}, {First = "ab", Last = "cd"}]`,
@@ -3145,7 +3414,7 @@ world'`,
{ {
desc: "bad char between minutes and seconds", desc: "bad char between minutes and seconds",
data: `a = 2021-03-30 21:312:0`, data: `a = 2021-03-30 21:312:0`,
msg: `expecting colon between minutes and seconds`, msg: `extra characters at the end of a local date time`,
}, },
{ {
desc: "invalid hour value", desc: "invalid hour value",
@@ -3260,6 +3529,18 @@ world'`,
desc: `invalid escape char basic multiline string`, desc: `invalid escape char basic multiline string`,
data: `A = """\z"""`, data: `A = """\z"""`,
}, },
{
desc: `invalid hex escape non-hex character in basic string`,
data: `A = "\xGG"`,
},
{
desc: `incomplete hex escape in basic string`,
data: `A = "\x6"`,
},
{
desc: `invalid hex escape non-hex character in multiline basic string`,
data: `A = """\xGG"""`,
},
{ {
desc: `invalid inf`, desc: `invalid inf`,
data: `A = ick`, data: `A = ick`,
@@ -3446,6 +3727,30 @@ world'`,
desc: `backspace in comment`, desc: `backspace in comment`,
data: "# this is a test\ba=1", data: "# this is a test\ba=1",
}, },
{
desc: `inline table comma at start`,
data: `a = { , b = 1 }`,
},
{
desc: `inline table missing separator`,
data: `a = { b = 1 c = 2 }`,
},
{
desc: `inline table double comma across newline`,
data: "a = { b = 1,\n, c = 2 }",
},
{
desc: `incomplete inline table`,
data: "a = { b = 1,\n",
},
{
desc: `incomplete hex escape in multiline basic string`,
data: `A = """\x6"""`,
},
{
desc: `invalid escape char in basic string`,
data: `A = "\z"`,
},
} }
for _, e := range examples { for _, e := range examples {
@@ -4385,3 +4690,176 @@ func TestIssue1028(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
}) })
} }
// customFieldUnmarshaler implements unstable.Unmarshaler and captures all
// key-value pairs directed to it, including unknown fields.
type customFieldUnmarshaler struct {
Values map[string]string
}
func (c *customFieldUnmarshaler) UnmarshalTOML(value *unstable.Node) error {
c.Values = map[string]string{
"kind": value.Kind.String(),
"data": string(value.Data),
}
return nil
}
func TestUnmarshalerInterface_StructFieldFallback(t *testing.T) {
// When EnableUnmarshalerInterface is active and a struct field is not found,
// the decoder should fall back to the Unmarshaler interface on the struct.
type Config struct {
Name string `toml:"name"`
}
t.Run("unknown field with unmarshaler", func(t *testing.T) {
doc := `name = "hello"
unknown = "world"`
var cfg Config
decoder := toml.NewDecoder(bytes.NewReader([]byte(doc)))
decoder.EnableUnmarshalerInterface()
err := decoder.Decode(&cfg)
assert.NoError(t, err)
assert.Equal(t, "hello", cfg.Name)
})
}
func TestUnmarshalerInterface_Value(t *testing.T) {
// Test that EnableUnmarshalerInterface delegates value decoding
// to the UnmarshalTOML method.
type Config struct {
Field customFieldUnmarshaler `toml:"field"`
}
doc := `field = "test-value"`
var cfg Config
decoder := toml.NewDecoder(bytes.NewReader([]byte(doc)))
decoder.EnableUnmarshalerInterface()
err := decoder.Decode(&cfg)
assert.NoError(t, err)
assert.Equal(t, "test-value", cfg.Field.Values["data"])
}
func TestTypeMismatchString_StructFieldContext(t *testing.T) {
// Exercise the typeMismatchString code path that includes struct field info
// in the error message.
type Inner struct {
Value int `toml:"value"`
}
type Config struct {
Inner Inner `toml:"inner"`
}
doc := `inner = "not-a-table"`
var cfg Config
err := toml.Unmarshal([]byte(doc), &cfg)
assert.Error(t, err)
}
func TestUnmarshalInlineTable_IncompatibleType(t *testing.T) {
// Exercise the default branch of unmarshalInlineTable when the target
// is not a map, struct, or interface.
type doc struct {
A int `toml:"a"`
}
var v doc
err := toml.Unmarshal([]byte(`a = {b = 1}`), &v)
assert.Error(t, err)
}
func TestTypeMismatchString_NoStructContext(t *testing.T) {
// Exercise the typeMismatchString code path without struct field context (line 186).
// Decoding a string into a bare int triggers this path.
var v map[string]int
err := toml.Unmarshal([]byte(`a = "hello"`), &v)
assert.Error(t, err)
}
func TestMultilineInlineTable_EmptyWithNewlines(t *testing.T) {
doc := "a = {\n\n}"
var v map[string]interface{}
err := toml.Unmarshal([]byte(doc), &v)
assert.NoError(t, err)
inner := v["a"]
if inner == nil {
t.Fatal("expected key 'a' to be present")
}
m, ok := inner.(map[string]interface{})
if !ok {
t.Fatalf("expected map, got %T", inner)
}
if len(m) != 0 {
t.Fatalf("expected empty map, got %v", m)
}
}
func TestMultilineInlineTable_CommentsOnly(t *testing.T) {
doc := "a = {\n # just a comment\n}"
var v map[string]interface{}
err := toml.Unmarshal([]byte(doc), &v)
assert.NoError(t, err)
inner := v["a"]
if inner == nil {
t.Fatal("expected key 'a' to be present")
}
m, ok := inner.(map[string]interface{})
if !ok {
t.Fatalf("expected map, got %T", inner)
}
if len(m) != 0 {
t.Fatalf("expected empty map, got %v", m)
}
}
func TestMultilineInlineTable_CommentAfterComma(t *testing.T) {
// Exercises comment handling after comma in inline table (parser lines 518-524).
doc := "a = { b = 1, # comment\nc = 2 }"
var v map[string]interface{}
err := toml.Unmarshal([]byte(doc), &v)
assert.NoError(t, err)
m, ok := v["a"].(map[string]interface{})
if !ok {
t.Fatal("expected a map")
}
if m["b"] != int64(1) {
t.Fatalf("expected b=1, got %v", m["b"])
}
if m["c"] != int64(2) {
t.Fatalf("expected c=2, got %v", m["c"])
}
}
func TestMultilineInlineTable_CommentAfterValue(t *testing.T) {
// Exercises comment handling after keyval in inline table (parser lines 542-548).
doc := "a = { b = 1 # comment\n, c = 2 }"
var v map[string]interface{}
err := toml.Unmarshal([]byte(doc), &v)
assert.NoError(t, err)
m, ok := v["a"].(map[string]interface{})
if !ok {
t.Fatal("expected a map")
}
if m["b"] != int64(1) {
t.Fatalf("expected b=1, got %v", m["b"])
}
if m["c"] != int64(2) {
t.Fatalf("expected c=2, got %v", m["c"])
}
}
func TestMultilineInlineTable_LeadingComma(t *testing.T) {
doc := "a = { b = 1\n, c = 2 }"
var v map[string]interface{}
err := toml.Unmarshal([]byte(doc), &v)
assert.NoError(t, err)
m, ok := v["a"].(map[string]interface{})
if !ok {
t.Fatal("expected a map")
}
if m["b"] != int64(1) {
t.Fatalf("expected b=1, got %v", m["b"])
}
if m["c"] != int64(2) {
t.Fatalf("expected c=2, got %v", m["c"])
}
}
+62 -14
View File
@@ -460,12 +460,14 @@ func (p *Parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) {
return v, v[1 : len(v)-1], rest, nil return v, v[1 : len(v)-1], rest, nil
} }
//nolint:funlen,cyclop,dupl
func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) { func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
// inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close // inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
// inline-table-open = %x7B ws ; { // inline-table-open = %x7B ws ; {
// inline-table-close = ws %x7D ; } // inline-table-close = ws %x7D ; }
// inline-table-sep = ws %x2C ws ; , Comma // inline-table-sep = ws %x2C ws ; , Comma
// inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ] // inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
tableStart := b
parent := p.builder.Push(Node{ parent := p.builder.Push(Node{
Kind: InlineTable, Kind: InlineTable,
Raw: p.rangeOfToken(b[:1], b[1:]), Raw: p.rangeOfToken(b[:1], b[1:]),
@@ -473,45 +475,77 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
first := true first := true
var child reference lastChild := invalidReference
addChild := func(ref reference) {
if lastChild == invalidReference {
p.builder.AttachChild(parent, ref)
} else {
p.builder.Chain(lastChild, ref)
}
lastChild = ref
}
b = b[1:] b = b[1:]
var err error var err error
for len(b) > 0 { for len(b) > 0 {
previousB := b var cref reference
b = p.parseWhitespace(b) cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return parent, nil, err
}
if cref != invalidReference {
addChild(cref)
}
if len(b) == 0 { if len(b) == 0 {
return parent, nil, NewParserError(previousB[:1], "inline table is incomplete") return parent, nil, NewParserError(tableStart[:1], "inline table is incomplete")
} }
if b[0] == '}' { if b[0] == '}' {
break break
} }
if !first { if b[0] == ',' {
b, err = expect(',', b) if first {
return parent, nil, NewParserError(b[0:1], "inline table cannot start with comma")
}
b = b[1:]
cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil { if err != nil {
return parent, nil, err return parent, nil, err
} }
b = p.parseWhitespace(b) if cref != invalidReference {
addChild(cref)
}
} else if !first {
return parent, nil, NewParserError(b[0:1], "inline table entries must be separated by commas")
}
// trailing comma: if '}' follows, stop
if len(b) > 0 && b[0] == '}' {
break
} }
var kv reference var kv reference
kv, b, err = p.parseKeyval(b) kv, b, err = p.parseKeyval(b)
if err != nil { if err != nil {
return parent, nil, err return parent, nil, err
} }
if first { addChild(kv)
p.builder.AttachChild(parent, kv)
} else { cref, b, err = p.parseOptionalWhitespaceCommentNewline(b)
p.builder.Chain(child, kv) if err != nil {
return parent, nil, err
}
if cref != invalidReference {
addChild(cref)
} }
child = kv
first = false first = false
} }
@@ -521,7 +555,7 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) {
return parent, rest, err return parent, rest, err
} }
//nolint:funlen,cyclop //nolint:funlen,cyclop,dupl
func (p *Parser) parseValArray(b []byte) (reference, []byte, error) { func (p *Parser) parseValArray(b []byte) (reference, []byte, error) {
// array = array-open [ array-values ] ws-comment-newline array-close // array = array-open [ array-values ] ws-comment-newline array-close
// array-open = %x5B ; [ // array-open = %x5B ; [
@@ -785,6 +819,13 @@ func (p *Parser) parseMultilineBasicString(b []byte) ([]byte, []byte, []byte, er
builder.WriteByte('\t') builder.WriteByte('\t')
case 'e': case 'e':
builder.WriteByte(0x1B) builder.WriteByte(0x1B)
case 'x':
x, err := hexToRune(atmost(token[i+1:], 2), 2)
if err != nil {
return nil, nil, nil, err
}
builder.WriteRune(x)
i += 2
case 'u': case 'u':
x, err := hexToRune(atmost(token[i+1:], 4), 4) x, err := hexToRune(atmost(token[i+1:], 4), 4)
if err != nil { if err != nil {
@@ -944,6 +985,13 @@ func (p *Parser) parseBasicString(b []byte) ([]byte, []byte, []byte, error) {
builder.WriteByte('\t') builder.WriteByte('\t')
case 'e': case 'e':
builder.WriteByte(0x1B) builder.WriteByte(0x1B)
case 'x':
x, err := hexToRune(token[i+1:len(token)-1], 2)
if err != nil {
return nil, nil, nil, err
}
builder.WriteRune(x)
i += 2
case 'u': case 'u':
x, err := hexToRune(token[i+1:len(token)-1], 4) x, err := hexToRune(token[i+1:len(token)-1], 4)
if err != nil { if err != nil {
+186
View File
@@ -331,6 +331,154 @@ func TestParser_AST(t *testing.T) {
}, },
}, },
}, },
{
desc: "multiline inline table",
input: "name = {\n first = \"Tom\",\n last = \"Preston-Werner\"\n}",
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: InlineTable,
Children: []astNode{
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Tom`)},
{Kind: Key, Data: []byte(`first`)},
},
},
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Preston-Werner`)},
{Kind: Key, Data: []byte(`last`)},
},
},
},
},
{
Kind: Key,
Data: []byte(`name`),
},
},
},
},
{
desc: "inline table with trailing comma",
input: `name = { first = "Tom", last = "Preston-Werner", }`,
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: InlineTable,
Children: []astNode{
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Tom`)},
{Kind: Key, Data: []byte(`first`)},
},
},
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Preston-Werner`)},
{Kind: Key, Data: []byte(`last`)},
},
},
},
},
{
Kind: Key,
Data: []byte(`name`),
},
},
},
},
{
desc: "empty inline table with newline",
input: "name = {\n}",
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: InlineTable,
Children: nil,
},
{
Kind: Key,
Data: []byte(`name`),
},
},
},
},
{
desc: "inline table with leading comma",
input: "name = { first = \"Tom\"\n, last = \"Werner\" }",
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: InlineTable,
Children: []astNode{
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Tom`)},
{Kind: Key, Data: []byte(`first`)},
},
},
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Werner`)},
{Kind: Key, Data: []byte(`last`)},
},
},
},
},
{
Kind: Key,
Data: []byte(`name`),
},
},
},
},
{
desc: "inline table with leading trailing comma",
input: "name = { first = \"Tom\"\n, }",
ast: astNode{
Kind: KeyValue,
Children: []astNode{
{
Kind: InlineTable,
Children: []astNode{
{
Kind: KeyValue,
Children: []astNode{
{Kind: String, Data: []byte(`Tom`)},
{Kind: Key, Data: []byte(`first`)},
},
},
},
},
{
Kind: Key,
Data: []byte(`name`),
},
},
},
},
{
desc: "inline table comma at start is error",
input: "name = { , first = \"Tom\" }",
err: true,
},
{
desc: "inline table double comma across newline is error",
input: "name = { first = \"Tom\",\n, last = \"Werner\" }",
err: true,
},
} }
for _, e := range examples { for _, e := range examples {
@@ -350,6 +498,44 @@ func TestParser_AST(t *testing.T) {
} }
} }
func TestParseInlineTable_CommentsWithKeepComments(t *testing.T) {
// Exercise comment reference handling inside parseInlineTable when
// KeepComments is true. This covers the addChild(cref) branches
// at the start of the loop, after comma, and after keyval.
examples := []struct {
desc string
input string
}{
{
desc: "comment at start of inline table",
input: "a = {\n# comment\nb = 1\n}",
},
{
desc: "comment after comma",
input: "a = {b = 1,\n# comment\nc = 2\n}",
},
{
desc: "comment after keyval",
input: "a = {b = 1 # comment\n, c = 2}",
},
{
desc: "comment only in inline table",
input: "a = {\n# just a comment\n}",
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
p := Parser{KeepComments: true}
p.Reset([]byte(e.input))
p.NextExpression()
err := p.Error()
assert.NoError(t, err)
})
}
}
func BenchmarkParseBasicStringWithUnicode(b *testing.B) { func BenchmarkParseBasicStringWithUnicode(b *testing.B) {
p := &Parser{} p := &Parser{}
b.Run("4", func(b *testing.B) { b.Run("4", func(b *testing.B) {