diff --git a/unmarshaler_test.go b/unmarshaler_test.go index 5fb2120..a5bc57e 100644 --- a/unmarshaler_test.go +++ b/unmarshaler_test.go @@ -864,7 +864,7 @@ huey = 'dewey' }, }, { - desc: "multiline basic string escape character", + desc: "multiline basic string escape character", input: `A = """\e"""`, gen: func() test { type doc struct { @@ -934,7 +934,7 @@ huey = 'dewey' }, }, { - desc: "multiline basic string hex escape", + desc: "multiline basic string hex escape", input: `A = """\x61"""`, gen: func() test { type doc struct { @@ -3727,6 +3727,30 @@ world'`, desc: `backspace in comment`, 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 { @@ -4666,3 +4690,176 @@ func TestIssue1028(t *testing.T) { 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"]) + } +} diff --git a/unstable/parser.go b/unstable/parser.go index 55ea50a..08e93b0 100644 --- a/unstable/parser.go +++ b/unstable/parser.go @@ -460,7 +460,7 @@ func (p *Parser) parseLiteralString(b []byte) ([]byte, []byte, []byte, error) { return v, v[1 : len(v)-1], rest, nil } -//nolint:funlen,cyclop +//nolint:funlen,cyclop,dupl func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) { // inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close // inline-table-open = %x7B ws ; { @@ -555,7 +555,7 @@ func (p *Parser) parseInlineTable(b []byte) (reference, []byte, error) { return parent, rest, err } -//nolint:funlen,cyclop +//nolint:funlen,cyclop,dupl func (p *Parser) parseValArray(b []byte) (reference, []byte, error) { // array = array-open [ array-values ] ws-comment-newline array-close // array-open = %x5B ; [ diff --git a/unstable/parser_test.go b/unstable/parser_test.go index e580cb1..0fc15b7 100644 --- a/unstable/parser_test.go +++ b/unstable/parser_test.go @@ -498,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) { p := &Parser{} b.Run("4", func(b *testing.B) {