From e4ebfdb28d1e484a55ca9cb066c20026f85cb64d Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 15 Sep 2026 17:35:15 +0200 Subject: [PATCH] Cap a struct type's records at 1024, find a lone reference in the value a template holds, word range refusals as unproven, and scope the lone-reference Decision to arguments and tags --- README.md | 8 ++--- cmd/fejkdata/main.go | 2 +- inline.go | 19 +++++++---- struct.go | 76 +++++++++++++++++++++++--------------------- 4 files changed, 57 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index e611941..61108b1 100644 --- a/README.md +++ b/README.md @@ -538,10 +538,10 @@ tokens add cost in proportion to the output. whole — though only a leading one could collide — keeps one simple name rule instead of a leading-position special case. The JSON string is what makes the library's own advice reachable: the error for an object holding only a format - names `"…"`, and that spelling has to work where it is printed. One reference alone, - `{/users}`, is refused naming the path `users`: both render the same text, and only - the path names a record. `IsTemplate` exports the rule, so the CLI, struct tags and - any other caller read one. + names `"…"`, and that spelling has to work where it is printed. An argument or struct + tag of one reference alone, `{/users}`, is refused naming the path `users`: both + render the same text, and only the path names a record. `IsTemplate` exports the + rule, so the CLI, struct tags and any other caller read one. - **An inline template skips the cycle fence, and only that one.** `New` proves the loaded tree acyclic, an inline node is a finite tree of its own, and nothing in the tree can reference it, so no render of it reaches itself. Every other fence diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index e005fc5..bb95e71 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -418,7 +418,7 @@ const ( func classify(arg string) (argKind, error) { inline, err := fejkdata.IsTemplate(arg) if inline { - return argTemplate, err + return argTemplate, nil } return argPath, err } diff --git a/inline.go b/inline.go index 1e29542..ffd8789 100644 --- a/inline.go +++ b/inline.go @@ -72,8 +72,8 @@ func isTemplate(arg string) (bool, error) { if i := strings.IndexAny(arg, `[]}"`); i >= 0 { return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1]) } - if len(arg) > 1 && strings.HasPrefix(arg, "/") { - return false, fmt.Errorf("path %s starts with /, and every path starts at the root already; write %s", arg, arg[1:]) + if path := strings.TrimLeft(arg, "/"); path != arg && path != "" { + return false, fmt.Errorf("path %s starts with /, and every path starts at the root already; write %s", arg, path) } return false, nil } @@ -82,14 +82,19 @@ func isJSONStart(arg string) bool { return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`) } -// loneReference is the path a template spells when it is one reference token and nothing else. +// loneReference is the path a template spells when the value it holds — a format string, a +// JSON string, or an object holding only a format — is one reference token and nothing else. func loneReference(arg string) (string, bool) { - format := arg - if strings.HasPrefix(arg, `"`) && json.Unmarshal([]byte(arg), &format) != nil { - return "", false + var raw any + if json.Unmarshal([]byte(arg), &raw) != nil { + raw = arg } + if m, isObject := raw.(map[string]any); isObject && len(m) == 1 { + raw = m["format"] + } + format, isString := raw.(string) var units []ftoken - if eachToken(format, func(t ftoken) error { units = append(units, t); return nil }) != nil || len(units) != 1 { + if !isString || eachToken(format, func(t ftoken) error { units = append(units, t); return nil }) != nil || len(units) != 1 { return "", false } body := units[0].body diff --git a/struct.go b/struct.go index 380223b..692bda1 100644 --- a/struct.go +++ b/struct.go @@ -31,12 +31,15 @@ func (f *Generator) FakeStruct(v any) error { return nil } -// structResult is what compiling a struct type settled: its shape, or why it cannot be filled. type structResult struct { shape *structShape err error } +// maxStructRecords caps the records one struct type fills, which pointers between struct +// types multiply along every path. +const maxStructRecords = 1 << 10 + // structShapeOf compiles a struct type once and remembers the answer. Callers hold the // generator's lock. func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) { @@ -47,7 +50,8 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) { if label == "" { label = "struct" } - shape, err := compileStruct(f.categories, t, label, map[reflect.Type]bool{}) + sc := &structCompile{root: f.categories, visiting: map[reflect.Type]bool{}, records: maxStructRecords} + shape, err := sc.record(t, label) if err == nil && shape.empty() { err = fmt.Errorf("%s has no fake tags, so nothing to fill", t) } @@ -75,29 +79,38 @@ type nestedStruct struct { func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 } -// structFields gathers what one struct type fills: its tagged fields, those its embedded -// structs promote included, as the tags of one record, and its named struct fields as nested -// records. visiting holds the types compiling or embedded above, so a pointer back to one is -// left alone rather than filled without end. -type structFields struct { +// structCompile is what compiling one struct type shares across the records it reaches: the +// loaded tree, the types compiling or embedded above, so a pointer back to one is left alone +// rather than filled without end, and how many more records it may build. +type structCompile struct { root map[string]node - t reflect.Type - label string visiting map[reflect.Type]bool - tags map[string]any - shape *structShape + records int } -// compileStruct compiles struct type t, naming its fields from label. -func compileStruct(root map[string]node, t reflect.Type, label string, visiting map[reflect.Type]bool) (*structShape, error) { - visiting[t] = true - defer delete(visiting, t) - c := &structFields{root: root, t: t, label: label, visiting: visiting, tags: map[string]any{}, shape: &structShape{}} +// structFields gathers what one struct type fills: its tagged fields, those its embedded +// structs promote included, as the tags of one record, and its named struct fields as nested +// records. +type structFields struct { + *structCompile + t reflect.Type + label string + tags map[string]any + shape *structShape +} + +func (sc *structCompile) record(t reflect.Type, label string) (*structShape, error) { + if sc.records--; sc.records < 0 { + return nil, fmt.Errorf(`%s: the struct fields reach more than %d records; leave a pointer unfilled with fake:"-"`, label, maxStructRecords) + } + sc.visiting[t] = true + defer delete(sc.visiting, t) + c := &structFields{structCompile: sc, t: t, label: label, tags: map[string]any{}, shape: &structShape{}} if err := c.walk(t, nil); err != nil { return nil, err } if len(c.tags) > 0 { - if err := c.shape.compileRecord(root, t, label, c.tags); err != nil { + if err := c.shape.compileRecord(sc.root, t, label, c.tags); err != nil { return nil, err } } @@ -170,7 +183,6 @@ func fieldPath(t reflect.Type, index []int) string { return strings.Join(names, ".") } -// embed gathers the fields an embedded struct promotes into c's record. func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error { c.visiting[elem] = true defer delete(c.visiting, elem) @@ -179,14 +191,13 @@ func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error { return err } if sf.Type.Kind() == reflect.Pointer && !sf.IsExported() && (len(c.tags) > tags || len(c.shape.nested) > nested) { - return fmt.Errorf("%s.%s: an embedded pointer to an unexported type cannot be allocated, so the tags beneath it cannot fill; embed %s by value", c.label, fieldPath(c.t, sf.Index), elem) + return fmt.Errorf("%s.%s: an unexported embedded pointer field cannot be set, so the tags beneath it cannot fill; embed %s by value", c.label, fieldPath(c.t, sf.Index), elem) } return nil } -// nest adds a named struct field, or a pointer to one, that carries tags as a record of its own. func (c *structFields) nest(sf reflect.StructField, elem reflect.Type) error { - nested, err := compileStruct(c.root, elem, c.label+"."+sf.Name, c.visiting) + nested, err := c.record(elem, c.label+"."+sf.Name) if err != nil || nested.empty() { return err } @@ -264,7 +275,7 @@ func (s *structShape) compileRecord(root map[string]node, t reflect.Type, label } // columnKind is what a field of one Go kind holds: the datatype its text proves as, the range a -// number of it stays in, and the kind to name when a value can pass that range. +// number of it stays in, and the kind to name when a value is not proven within that range. type columnKind struct { datatype DataType lo, hi float64 @@ -288,23 +299,16 @@ var columnKinds = map[reflect.Kind]columnKind{ reflect.Uint8: {DataTypeInteger, 0, math.MaxUint8, reflect.Int64}, } -// past is the bound of v a field of this kind cannot hold, if either is. An integer prints +// holds reports whether a field of this kind holds every value v proves. An integer prints // whole, so its bounds round inward first. -func (k columnKind) past(v proven) (float64, bool) { - lo, hi := v.lo, v.hi +func (k columnKind) holds(v proven) bool { switch k.datatype { case DataTypeString, DataTypeBoolean: - return 0, false + return true case DataTypeInteger: - lo, hi = math.Ceil(lo), math.Floor(hi) + return math.Ceil(v.lo) >= k.lo && math.Floor(v.hi) <= k.hi } - switch { - case lo < k.lo: - return lo, true - case hi > k.hi: - return hi, true - } - return 0, false + return v.lo >= k.lo && v.hi <= k.hi } // checkField rejects a column some render of which a field of Go type ft cannot hold: a null @@ -326,8 +330,8 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro if reason := v.not[kind.datatype]; reason != "" { return fmt.Errorf("%s (%s): %s", label, ft, reason) } - if bound, over := kind.past(v); over { - return fmt.Errorf("%s (%s): %q can reach %s, past %s; make it %s", label, ft, it.format, strconv.FormatFloat(bound, 'g', -1, 64), elem.Kind(), kind.wider) + if !kind.holds(v) { + return fmt.Errorf("%s (%s): %q is not proven within %s; make it %s", label, ft, it.format, elem.Kind(), kind.wider) } } return nil