diff --git a/CHANGELOG.md b/CHANGELOG.md index fa37668..3961246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 check covered `[table]` headers and dotted keys but not the intermediate steps of an array-of-tables header, so such a document silently extended the inline table. +- A new element of an array of tables starts a fresh scope for dotted-key paths + and nested arrays of tables: `[[a]]`, `b.c = 1`, `[[a]]`, `[a.b]` parses, as + the TOML examples in the spec shape it. The records of the previous element + falsely rejected the same paths in the next one. - A float with an exponent marker but no digits (`1e`, `0.0E`) is rejected; the exponent requires at least one digit. - A date-time offset outside 00:00 through 23:59 is rejected; such offsets diff --git a/interpres_test.go b/interpres_test.go index dbcbcd8..8f80028 100644 --- a/interpres_test.go +++ b/interpres_test.go @@ -498,6 +498,36 @@ func TestRejectsInlineTableExtension(t *testing.T) { } } +// A new element of an array of tables starts a fresh scope: sub-table headers, +// nested arrays of tables, and dotted-key paths recorded for the previous +// element must not block the same paths in the next one. +func TestArrayOfTablesFreshScopePerElement(t *testing.T) { + cases := map[string]string{ + "nested array of tables": "[[a]]\n[[a.b]]\nx = 1\n[[a]]\n[a.b]\ny = 2\n", + "dotted key": "[[a]]\nb.c = 1\n[[a]]\n[a.b]\nd = 2\n", + } + for name, doc := range cases { + tree, err := Parse([]byte(doc)) + if err != nil { + t.Errorf("%s: %v", name, err) + continue + } + elements := tree["a"].([]map[string]any) + if len(elements) != 2 { + t.Errorf("%s: len(a) = %d, want 2", name, len(elements)) + } + } + // Within one element the redefinition rules keep applying. + for name, doc := range map[string]string{ + "header over dotted in one element": "[[a]]\nb.c = 1\n[a.b]\nd = 2\n", + "table over nested array": "[[a]]\n[[a.b]]\n[a.b]\nx = 1\n", + } { + if _, err := Parse([]byte(doc)); err == nil { + t.Errorf("%s: expected an error, got none", name) + } + } +} + func TestRejectsSpecInvalid(t *testing.T) { cases := map[string]string{ "single-digit hour": "a = 2023-10-01T1:32:00Z\n", diff --git a/parser.go b/parser.go index 9037df9..32dfd6d 100644 --- a/parser.go +++ b/parser.go @@ -274,18 +274,17 @@ func (p *parser) freezeInline(path []string, val any) { } } -// resetScopeUnder forgets the header and freeze records nested under key, which -// belong to the previous element of an array of tables. +// resetScopeUnder forgets the definition records nested under key, which +// belong to the previous element of an array of tables: headers, frozen +// inline tables, dotted-key paths, and nested arrays of tables all start +// fresh in the new element. func (p *parser) resetScopeUnder(key []string) { prefix := pathKey(key) + "\x00" - for k := range p.headers { - if strings.HasPrefix(k, prefix) { - delete(p.headers, k) - } - } - for k := range p.frozen { - if strings.HasPrefix(k, prefix) { - delete(p.frozen, k) + for _, m := range []map[string]bool{p.headers, p.frozen, p.dotted, p.arrays} { + for k := range m { + if strings.HasPrefix(k, prefix) { + delete(m, k) + } } } }