Compare commits
2 Commits
e4ebfdb28d
...
3cd3f8f317
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cd3f8f317 | |||
| c212c6bef4 |
@@ -193,9 +193,10 @@ naming a kind that holds it. The fields an embedded struct promotes are columns
|
|||||||
same record; a named struct field, or a pointer to one, fills from its own tags as a
|
same record; a named struct field, or a pointer to one, fills from its own tags as a
|
||||||
record of its own, so its references draw apart from its parent's. `fake:"-"` leaves a
|
record of its own, so its references draw apart from its parent's. `fake:"-"` leaves a
|
||||||
struct field, embedded or named, or a pointer to one, unfilled. Untagged fields keep
|
struct field, embedded or named, or a pointer to one, unfilled. Untagged fields keep
|
||||||
their values, and so does a pointer back to a struct already being filled. The first call for a type compiles its tags and reports
|
their values, and so does a pointer back to a struct already being filled; a type
|
||||||
what they get wrong, with the same error on every later call; a `datatype` in a tag
|
whose fields reach more than 1024 structs is refused, naming `fake:"-"` to cut it. The
|
||||||
names the Go type that already sets it.
|
first call for a type compiles its tags and reports what they get wrong, with the same
|
||||||
|
error on every later call; a `datatype` in a tag names the Go type that already sets it.
|
||||||
|
|
||||||
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
||||||
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func IsTemplate(arg string) (bool, error) {
|
|||||||
func isTemplate(arg string) (bool, error) {
|
func isTemplate(arg string) (bool, error) {
|
||||||
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
||||||
if path, lone := loneReference(arg); lone {
|
if path, lone := loneReference(arg); lone {
|
||||||
return false, fmt.Errorf("%s is the path %s written as a template; write %s", arg, path, path)
|
return false, pathAdvice(path, fmt.Sprintf("%s is the path %s written as a template", arg, path))
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ func isTemplate(arg string) (bool, error) {
|
|||||||
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])
|
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 path := strings.TrimLeft(arg, "/"); path != arg && path != "" {
|
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, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg))
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@@ -82,6 +82,14 @@ func isJSONStart(arg string) bool {
|
|||||||
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it.
|
||||||
|
func pathAdvice(path, refusal string) error {
|
||||||
|
if err := checkPathNames(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s; write %s", refusal, path)
|
||||||
|
}
|
||||||
|
|
||||||
// loneReference is the path a template spells when the value it holds — a format string, a
|
// 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.
|
// JSON string, or an object holding only a format — is one reference token and nothing else.
|
||||||
func loneReference(arg string) (string, bool) {
|
func loneReference(arg string) (string, bool) {
|
||||||
@@ -101,6 +109,9 @@ func loneReference(arg string) (string, bool) {
|
|||||||
if units[0].kind != 'b' || !isRef(body) || strings.ContainsAny(body, "|(") {
|
if units[0].kind != 'b' || !isRef(body) || strings.ContainsAny(body, "|(") {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(body, "/") {
|
||||||
|
body = "/" + strings.TrimLeft(body, "/")
|
||||||
|
}
|
||||||
_, path, err := refShape(body)
|
_, path, err := refShape(body)
|
||||||
return path, err == nil
|
return path, err == nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,6 +185,9 @@ func TestIsTemplate(t *testing.T) {
|
|||||||
` "{/sv_SE.person}"`: "write sv_SE.person",
|
` "{/sv_SE.person}"`: "write sv_SE.person",
|
||||||
`{"format":"{/sv_SE.person}"}`: "write sv_SE.person",
|
`{"format":"{/sv_SE.person}"}`: "write sv_SE.person",
|
||||||
"//sv_SE.person": "write sv_SE.person",
|
"//sv_SE.person": "write sv_SE.person",
|
||||||
|
"{//sv_SE.person}": "{//sv_SE.person} is the path sv_SE.person written as a template; write sv_SE.person",
|
||||||
|
"/sv_SE/person": `path "sv_SE/person" contains "/"`,
|
||||||
|
"{/-}": `path "-" is reserved`,
|
||||||
} {
|
} {
|
||||||
if _, err := IsTemplate(arg); err == nil || !strings.Contains(err.Error(), want) {
|
if _, err := IsTemplate(arg); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
t.Errorf("IsTemplate(%q) = %v; want it rejected naming %s", arg, err, want)
|
t.Errorf("IsTemplate(%q) = %v; want it rejected naming %s", arg, err, want)
|
||||||
|
|||||||
@@ -396,6 +396,16 @@ func checkName(name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkPathNames rejects a dotted path with a segment no name may be.
|
||||||
|
func checkPathNames(path string) error {
|
||||||
|
for _, seg := range strings.Split(path, ".") {
|
||||||
|
if err := checkName(seg); err != nil {
|
||||||
|
return fmt.Errorf("path %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// isOption reports whether a template key configures the node instead of naming a
|
// isOption reports whether a template key configures the node instead of naming a
|
||||||
// field. These names can never be fields.
|
// field. These names can never be fields.
|
||||||
func isOption(name string) bool {
|
func isOption(name string) bool {
|
||||||
|
|||||||
@@ -36,9 +36,8 @@ type structResult struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// maxStructRecords caps the records one struct type fills, which pointers between struct
|
// maxStructs caps the structs compiling one type walks through its fields.
|
||||||
// types multiply along every path.
|
const maxStructs = 1 << 10
|
||||||
const maxStructRecords = 1 << 10
|
|
||||||
|
|
||||||
// structShapeOf compiles a struct type once and remembers the answer. Callers hold the
|
// structShapeOf compiles a struct type once and remembers the answer. Callers hold the
|
||||||
// generator's lock.
|
// generator's lock.
|
||||||
@@ -50,7 +49,7 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) {
|
|||||||
if label == "" {
|
if label == "" {
|
||||||
label = "struct"
|
label = "struct"
|
||||||
}
|
}
|
||||||
sc := &structCompile{root: f.categories, visiting: map[reflect.Type]bool{}, records: maxStructRecords}
|
sc := &structCompile{root: f.categories, visiting: map[reflect.Type]bool{}, structs: maxStructs}
|
||||||
shape, err := sc.record(t, label)
|
shape, err := sc.record(t, label)
|
||||||
if err == nil && shape.empty() {
|
if err == nil && shape.empty() {
|
||||||
err = fmt.Errorf("%s has no fake tags, so nothing to fill", t)
|
err = fmt.Errorf("%s has no fake tags, so nothing to fill", t)
|
||||||
@@ -79,13 +78,13 @@ type nestedStruct struct {
|
|||||||
|
|
||||||
func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 }
|
func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 }
|
||||||
|
|
||||||
// structCompile is what compiling one struct type shares across the records it reaches: the
|
// structCompile is what compiling one struct type shares across the structs it reaches: the
|
||||||
// loaded tree, the types compiling or embedded above, so a pointer back to one is left alone
|
// 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.
|
// rather than filled without end, and how many more structs it may walk.
|
||||||
type structCompile struct {
|
type structCompile struct {
|
||||||
root map[string]node
|
root map[string]node
|
||||||
visiting map[reflect.Type]bool
|
visiting map[reflect.Type]bool
|
||||||
records int
|
structs int
|
||||||
}
|
}
|
||||||
|
|
||||||
// structFields gathers what one struct type fills: its tagged fields, those its embedded
|
// structFields gathers what one struct type fills: its tagged fields, those its embedded
|
||||||
@@ -100,8 +99,8 @@ type structFields struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sc *structCompile) record(t reflect.Type, label string) (*structShape, error) {
|
func (sc *structCompile) record(t reflect.Type, label string) (*structShape, error) {
|
||||||
if sc.records--; sc.records < 0 {
|
if err := sc.spend(label); err != nil {
|
||||||
return nil, fmt.Errorf(`%s: the struct fields reach more than %d records; leave a pointer unfilled with fake:"-"`, label, maxStructRecords)
|
return nil, err
|
||||||
}
|
}
|
||||||
sc.visiting[t] = true
|
sc.visiting[t] = true
|
||||||
defer delete(sc.visiting, t)
|
defer delete(sc.visiting, t)
|
||||||
@@ -117,6 +116,13 @@ func (sc *structCompile) record(t reflect.Type, label string) (*structShape, err
|
|||||||
return c.shape, nil
|
return c.shape, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sc *structCompile) spend(label string) error {
|
||||||
|
if sc.structs--; sc.structs >= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf(`%s: the struct fields reach more than %d structs; leave a struct field unfilled with fake:"-"`, label, maxStructs)
|
||||||
|
}
|
||||||
|
|
||||||
// walk gathers the fields of struct type t, which sits at index within c.t.
|
// walk gathers the fields of struct type t, which sits at index within c.t.
|
||||||
func (c *structFields) walk(t reflect.Type, index []int) error {
|
func (c *structFields) walk(t reflect.Type, index []int) error {
|
||||||
for i := 0; i < t.NumField(); i++ {
|
for i := 0; i < t.NumField(); i++ {
|
||||||
@@ -184,6 +190,9 @@ func fieldPath(t reflect.Type, index []int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error {
|
func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error {
|
||||||
|
if err := c.spend(c.label + "." + fieldPath(c.t, sf.Index)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
c.visiting[elem] = true
|
c.visiting[elem] = true
|
||||||
defer delete(c.visiting, elem)
|
defer delete(c.visiting, elem)
|
||||||
tags, nested := len(c.tags), len(c.shape.nested)
|
tags, nested := len(c.tags), len(c.shape.nested)
|
||||||
@@ -218,10 +227,8 @@ func tagValue(sf reflect.StructField, tag string) (any, error) {
|
|||||||
case inline:
|
case inline:
|
||||||
return inputValue(tag)
|
return inputValue(tag)
|
||||||
}
|
}
|
||||||
for _, seg := range strings.Split(tag, ".") {
|
if err := checkPathNames(tag); err != nil {
|
||||||
if err := checkName(seg); err != nil {
|
return nil, err
|
||||||
return nil, fmt.Errorf("path %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return "{/" + tag + "}", nil
|
return "{/" + tag + "}", nil
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-10
@@ -243,17 +243,20 @@ func TestFakeStructErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFakeStructBoundsTheRecordsATypeReaches(t *testing.T) {
|
func TestFakeStructBoundsTheStructsATypeReaches(t *testing.T) {
|
||||||
f := structData(t)
|
f := structData(t)
|
||||||
tree := reflect.TypeOf(structPlace{})
|
place := reflect.TypeOf(structPlace{})
|
||||||
for depth := 1; depth <= 10; depth++ {
|
tree := place
|
||||||
|
for depth := 1; depth <= 8; depth++ {
|
||||||
tree = reflect.StructOf([]reflect.StructField{{Name: "L", Type: reflect.PointerTo(tree)}, {Name: "R", Type: reflect.PointerTo(tree)}})
|
tree = reflect.StructOf([]reflect.StructField{{Name: "L", Type: reflect.PointerTo(tree)}, {Name: "R", Type: reflect.PointerTo(tree)}})
|
||||||
err := f.FakeStruct(reflect.New(tree).Interface())
|
}
|
||||||
switch {
|
fields := []reflect.StructField{{Name: "L", Type: reflect.PointerTo(tree)}, {Name: "R", Type: reflect.PointerTo(tree)}, {Name: "P", Type: place}}
|
||||||
case depth < 10 && err != nil:
|
if err := f.FakeStruct(reflect.New(reflect.StructOf(fields)).Interface()); err != nil {
|
||||||
t.Fatalf("a tree %d deep, %d records: FakeStruct = %v, want it filled", depth, 1<<(depth+1)-1, err)
|
t.Fatalf("a type reaching 1024 structs: FakeStruct = %v, want it filled", err)
|
||||||
case depth == 10 && (err == nil || !strings.Contains(err.Error(), "more than 1024 records") || !strings.Contains(err.Error(), `fake:"-"`)):
|
}
|
||||||
t.Errorf("a tree 10 deep, 2047 records: FakeStruct = %v, want it refused naming the cap and fake:\"-\"", err)
|
embedded := reflect.StructField{Name: "StructFamily", Type: reflect.TypeOf(StructFamily{}), Anonymous: true}
|
||||||
}
|
err := f.FakeStruct(reflect.New(reflect.StructOf(append(fields, embedded))).Interface())
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "more than 1024 structs") || !strings.Contains(err.Error(), `leave a struct field unfilled with fake:"-"`) {
|
||||||
|
t.Errorf("a type reaching 1025 structs, the last embedded: FakeStruct = %v, want it refused naming the cap and fake:\"-\"", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user