diff --git a/docs/decisions.md b/docs/decisions.md index 1d223e4..b3dc118 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -298,8 +298,8 @@ not agree on one among themselves; each must only hold that type. The fields an embedded struct promotes are the struct's own — `e.First`, as `encoding/json` and SQL mappers read them — so they are columns of its record and share -its names: one tag may bind `{/person as p}{p.first}` and another read `{p.sex}`, while a -path tag draws afresh as `{/path}` does (goal 4.1); a tagged field that another field hides is refused, not dropped. A named +its names: one tag may bind `{/person as p}{p.first}` and another read `{p.sex}` (goal 4.1), +while a path tag draws afresh as `{/path}` does (goal 5.2); a tagged field that another field hides is refused, not dropped. A named struct field is another entity and a record of its own. `fake:"-"` leaves a struct field, embedded or named, unfilled, so no name may be `-`; a pointer back to a struct already being filled is left alone, since filling it would never end. `New` cannot see a diff --git a/graph.go b/graph.go index 0a58860..ebf47d7 100644 --- a/graph.go +++ b/graph.go @@ -174,6 +174,9 @@ func checkNoCycles(s nodeScope) error { inScope := map[node]bool{} _ = s(func(_ string, n node) error { inScope[n] = true + if t, isTable := n.(*table); isTable { + inScope[t.rowNode] = true // a selector lands on it, and contained lists it nowhere + } return nil }) color := map[node]int{} diff --git a/namefence.go b/namefence.go index bcb63e4..004d853 100644 --- a/namefence.go +++ b/namefence.go @@ -58,7 +58,9 @@ func rendersInside(nodes []node, t *template) bool { } // checkUses refuses a binding read once at a spot the bound spelling can stand: that spelling -// draws the same way without the name. A field stands only beside its binding. +// draws the same way without the name. A field stands only beside its binding. Where the spelling +// would be a CLI argument or tag of one reference alone, that entry point's own refusal then +// names the bare path. func (b *nameBinding) checkUses() error { r := b.uses[0] if len(b.uses) > 1 || r.nested || b.bindsField() && r.in != b.binder { diff --git a/reference_test.go b/reference_test.go index 34c5dcc..6f860b9 100644 --- a/reference_test.go +++ b/reference_test.go @@ -339,3 +339,14 @@ func TestSlashAfterAReferenceSigilIsRejected(t *testing.T) { } } } + +func TestACycleThroughASelectedRowIsRefused(t *testing.T) { + _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, map[string]string{ + "x.json": `{"format":"{v} {/y}","rows":"x.tsv","key":"code"}`, + "x.tsv": "code\tv\n1\ta\n2\tb\n", + "y.json": `"{/x[1]}"`, + }))) + if err == nil || !strings.Contains(err.Error(), "cycle") { + t.Fatalf("New = %v, want the cycle through x[1] refused", err) + } +}