This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+83 -81
View File
@@ -29,10 +29,12 @@ type isZeroer interface {
var isZeroerType = reflect.TypeFor[isZeroer]()
type structFields struct {
flattened []structField // listed in depth-first ordering
byActualName map[string]*structField
byFoldedName map[string][]*structField
inlinedFallback *structField
flattened []structField // listed in depth-first ordering
byActualName map[string]*structField
byFoldedName map[string][]*structField
embeddedFallback *structField
errUnsupportedFormat *SemanticError
}
// reindex recomputes index to avoid bounds check during runtime.
@@ -51,8 +53,8 @@ func (sf *structFields) reindex() {
for i := range sf.flattened {
reindex(&sf.flattened[i])
}
if sf.inlinedFallback != nil {
reindex(sf.inlinedFallback)
if sf.embeddedFallback != nil {
reindex(sf.embeddedFallback)
}
}
@@ -80,25 +82,25 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
return cmp.Or(serr, &SemanticError{GoType: t, Err: fmt.Errorf(f, a...)})
}
// Setup a queue for a breath-first search.
// Setup a queue for a breadth-first search.
var queueIndex int
type queueEntry struct {
typ reflect.Type
index []int
visitChildren bool // whether to recursively visit inlined field in this struct
visitChildren bool // whether to recursively visit embedded field in this struct
}
queue := []queueEntry{{root, nil, true}}
seen := map[reflect.Type]bool{root: true}
// Perform a breadth-first search over all reachable fields.
// This ensures that len(f.index) will be monotonically increasing.
var allFields, inlinedFallbacks []structField
var allFields, embeddedFallbacks []structField
for queueIndex < len(queue) {
qe := queue[queueIndex]
queueIndex++
t := qe.typ
inlinedFallbackIndex := -1 // index of last inlined fallback field in current struct
embeddedFallbackIndex := -1 // index of last embedded fallback field in current struct
namesIndex := make(map[string]int) // index of each field with a given JSON object name in current struct
var hasAnyJSONTag bool // whether any Go struct field has a `json` tag
var hasAnyJSONField bool // whether any JSON serializable fields exist in current struct
@@ -126,80 +128,73 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
if indirectType(f.typ).Kind() != reflect.Struct {
serr = orErrorf(serr, t, "embedded Go struct field %s of non-struct type must be explicitly given a JSON name", sf.Name)
} else {
f.inline = true // implied by use of Go embedding without an explicit name
f.embed = true // implied by use of Go embedding without an explicit name
}
}
if f.inline || f.unknown {
// Handle an inlined field that serializes to/from
var handleEmbed, handleField func()
handleEmbed = func() {
// Handle an embedded field that serializes to/from
// zero or more JSON object members.
switch f.fieldOptions {
case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true}:
case fieldOptions{name: f.name, quotedName: f.quotedName, unknown: true}:
case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true, unknown: true}:
serr = orErrorf(serr, t, "Go struct field %s cannot have both `inline` and `unknown` specified", sf.Name)
f.inline = false // let `unknown` take precedence
default:
serr = orErrorf(serr, t, "Go struct field %s cannot have any options other than `inline` or `unknown` specified", sf.Name)
if f.fieldOptions != (fieldOptions{name: f.name, quotedName: f.quotedName, embed: true}) {
serr = orErrorf(serr, t, "Go struct field %s cannot have any options other than `embed` specified", sf.Name)
if f.hasName {
continue // invalid inlined field; treat as ignored
}
f.fieldOptions = fieldOptions{name: f.name, quotedName: f.quotedName, inline: f.inline, unknown: f.unknown}
if f.inline && f.unknown {
f.inline = false // let `unknown` take precedence
handleField()
return // invalid embedded field; treat as regular field
}
f.fieldOptions = fieldOptions{name: f.name, quotedName: f.quotedName, embed: f.embed}
}
// Reject any types with custom serialization otherwise
// it becomes impossible to know what sub-fields to inline.
// it becomes impossible to know what sub-fields to embed.
tf := indirectType(f.typ)
if implementsAny(tf, allMethodTypes...) && tf != jsontextValueType {
serr = orErrorf(serr, t, "inlined Go struct field %s of type %s must not implement marshal or unmarshal methods", sf.Name, tf)
serr = orErrorf(serr, t, "embedded Go struct field %s of type %s must not implement marshal or unmarshal methods", sf.Name, tf)
}
// Handle an inlined field that serializes to/from
// Handle an embedded field that serializes to/from
// a finite number of JSON object members backed by a Go struct.
if tf.Kind() == reflect.Struct {
if f.unknown {
serr = orErrorf(serr, t, "inlined Go struct field %s of type %s with `unknown` tag must be a Go map of string key or a jsontext.Value", sf.Name, tf)
continue // invalid inlined field; treat as ignored
}
if qe.visitChildren {
queue = append(queue, queueEntry{tf, f.index, !seen[tf]})
}
seen[tf] = true
continue
return
} else if !sf.IsExported() {
serr = orErrorf(serr, t, "inlined Go struct field %s is not exported", sf.Name)
continue // invalid inlined field; treat as ignored
serr = orErrorf(serr, t, "embedded Go struct field %s is not exported", sf.Name)
return // invalid embedded field; treat as ignored
}
// Handle an inlined field that serializes to/from any number of
// Handle an embedded field that serializes to/from any number of
// JSON object members back by a Go map or jsontext.Value.
switch {
case tf == jsontextValueType:
f.fncs = nil // specially handled in arshal_inlined.go
f.fncs = nil // specially handled in arshal_embedded.go
case tf.Kind() == reflect.Map && tf.Key().Kind() == reflect.String:
if implementsAny(tf.Key(), allMethodTypes...) {
serr = orErrorf(serr, t, "inlined map field %s of type %s must have a string key that does not implement marshal or unmarshal methods", sf.Name, tf)
continue // invalid inlined field; treat as ignored
serr = orErrorf(serr, t, "embedded map field %s of type %s must have a string key that does not implement marshal or unmarshal methods", sf.Name, tf)
handleField()
return // invalid embedded field; treat as regular field
}
f.fncs = lookupArshaler(tf.Elem())
default:
serr = orErrorf(serr, t, "inlined Go struct field %s of type %s must be a Go struct, Go map of string key, or jsontext.Value", sf.Name, tf)
continue // invalid inlined field; treat as ignored
serr = orErrorf(serr, t, "embedded Go struct field %s of type %s must be a Go struct, Go map of string key, or jsontext.Value", sf.Name, tf)
handleField()
return // invalid embedded field; treat as regular field
}
// Reject multiple inlined fallback fields within the same struct.
if inlinedFallbackIndex >= 0 {
serr = orErrorf(serr, t, "inlined Go struct fields %s and %s cannot both be a Go map or jsontext.Value", t.Field(inlinedFallbackIndex).Name, sf.Name)
// Still append f to inlinedFallbacks as there is still a
// check for a dominant inlined fallback before returning.
// Reject multiple embedded fallback fields within the same struct.
if embeddedFallbackIndex >= 0 {
serr = orErrorf(serr, t, "embedded Go struct fields %s and %s cannot both be a Go map or jsontext.Value", t.Field(embeddedFallbackIndex).Name, sf.Name)
// Still append f to embeddedFallbacks as there is still a
// check for a dominant embedded fallback before returning.
}
inlinedFallbackIndex = i
embeddedFallbackIndex = i
inlinedFallbacks = append(inlinedFallbacks, f)
} else {
embeddedFallbacks = append(embeddedFallbacks, f)
}
handleField = func() {
// Handle normal Go struct field that serializes to/from
// a single JSON object member.
@@ -210,14 +205,14 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
tf := indirectType(f.typ)
if !(sf.Anonymous && tf.Kind() == reflect.Struct) {
serr = orErrorf(serr, t, "Go struct field %s is not exported", sf.Name)
continue
return
}
// Unfortunately, methods on the unexported field
// still cannot be called.
if implementsAny(tf, allMethodTypes...) ||
(f.omitzero && implementsAny(tf, isZeroerType)) {
serr = orErrorf(serr, t, "Go struct field %s is not exported for method calls", sf.Name)
continue
return
}
}
@@ -260,6 +255,15 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
f.id = len(allFields)
f.fncs = lookupArshaler(sf.Type)
allFields = append(allFields, f)
if f.format != "" && fs.errUnsupportedFormat == nil {
fs.errUnsupportedFormat = &SemanticError{GoType: t, Err: fmt.Errorf("Go struct field %s has unsupported `format` tag option", sf.Name)}
}
}
if f.embed {
handleEmbed()
} else {
handleField()
}
}
@@ -325,6 +329,8 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
flattened: flattened,
byActualName: make(map[string]*structField, len(flattened)),
byFoldedName: make(map[string][]*structField, len(flattened)),
errUnsupportedFormat: fs.errUnsupportedFormat,
}
for i, f := range fs.flattened {
foldedName := string(foldName([]byte(f.name)))
@@ -333,7 +339,7 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
}
for foldedName, fields := range fs.byFoldedName {
if len(fields) > 1 {
// The precedence order for conflicting ignoreCase names
// The precedence order for conflicting case-insensitive names
// is by breadth-first order, rather than depth-first order.
slices.SortFunc(fields, func(x, y *structField) int {
return cmp.Compare(x.id, y.id)
@@ -341,8 +347,8 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
fs.byFoldedName[foldedName] = fields
}
}
if n := len(inlinedFallbacks); n == 1 || (n > 1 && len(inlinedFallbacks[0].index) != len(inlinedFallbacks[1].index)) {
fs.inlinedFallback = &inlinedFallbacks[0] // dominant inlined fallback field
if n := len(embeddedFallbacks); n == 1 || (n > 1 && len(embeddedFallbacks[0].index) != len(embeddedFallbacks[1].index)) {
fs.embeddedFallback = &embeddedFallbacks[0] // dominant embedded fallback field
}
fs.reindex()
return fs, serr
@@ -391,8 +397,7 @@ type fieldOptions struct {
hasName bool
nameNeedEscape bool
casing int8 // either 0, caseIgnore, or caseStrict
inline bool
unknown bool
embed bool
omitzero bool
omitempty bool
string bool
@@ -404,7 +409,6 @@ type fieldOptions struct {
// the JSON member name and other features.
func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, err error) {
tag, hasTag := sf.Tag.Lookup("json")
tagOrig := tag
// Check whether this field is explicitly ignored.
if tag == "-" {
@@ -428,10 +432,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
return fieldOptions{}, true, err
}
// Determine the JSON member name for this Go field. A user-specified name
// may be provided as either an identifier or a single-quoted string.
// The single-quoted string allows arbitrary characters in the name.
// See https://go.dev/issue/2718 and https://go.dev/issue/3546.
// Determine the JSON member name for this Go field.
out.name = sf.Name // always starts with an uppercase character
if len(tag) > 0 && !strings.HasPrefix(tag, ",") {
// For better compatibility with v1, accept almost any unescaped name.
@@ -445,7 +446,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
// In either case, call consumeTagOption to handle it further.
var err2 error
if !strings.HasPrefix(tag[n:], ",") && len(name) != len(tag) {
name, n, err2 = consumeTagOption(tag)
name, n, err2 = consumeTagOption(tag, false)
if err2 != nil {
err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2))
}
@@ -454,22 +455,15 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q with invalid UTF-8", sf.Name, name))
name = string([]rune(name)) // replace invalid UTF-8 with utf8.RuneError
}
if name == "-" && tag[0] == '-' {
defer func() { // defer to let other errors take precedence
err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q; either "+
"use `json:\"-\"` to ignore the field or "+
"use `json:\"'-'%s` to specify %q as the name", sf.Name, out.name, strings.TrimPrefix(strconv.Quote(tagOrig), `"-`), name))
}()
}
if err2 == nil {
out.hasName = true
out.name = name
}
tag = tag[n:]
}
b, _ := jsonwire.AppendQuote(nil, out.name, &jsonflags.Flags{})
b, _ := jsonwire.AppendQuote(nil, []byte(out.name), &jsonflags.Flags{})
out.quotedName = string(b)
out.nameNeedEscape = jsonwire.NeedEscape(out.name)
out.nameNeedEscape = jsonwire.NeedEscape([]byte(out.name))
// Handle any additional tag options (if any).
var wasFormat bool
@@ -487,7 +481,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
}
// Consume and process the tag option.
opt, n, err2 := consumeTagOption(tag)
opt, n, err2 := consumeTagOption(tag, false)
if err2 != nil {
err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2))
}
@@ -506,7 +500,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
break
}
tag = tag[len(":"):]
opt, n, err2 := consumeTagOption(tag)
opt, n, err2 := consumeTagOption(tag, false)
if err2 != nil {
err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `case` tag option: %v", sf.Name, err2))
break
@@ -524,10 +518,8 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
default:
err = cmp.Or(err, fmt.Errorf("Go struct field %s has unknown `case:%s` tag value", sf.Name, rawOpt))
}
case "inline":
out.inline = true
case "unknown":
out.unknown = true
case "embed":
out.embed = true
case "omitzero":
out.omitzero = true
case "omitempty":
@@ -540,10 +532,13 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
break
}
tag = tag[len(":"):]
opt, n, err2 := consumeTagOption(tag)
opt, n, err2 := consumeTagOption(tag, true)
if err2 != nil {
err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `format` tag option: %v", sf.Name, err2))
break
} else if opt == "" {
err = cmp.Or(err, fmt.Errorf("Go struct field %s cannot have empty value for `format` tag option", sf.Name))
break
}
tag = tag[n:]
out.format = opt
@@ -553,7 +548,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
// This catches invalid mutants such as "omitEmpty" or "omit_empty".
normOpt := strings.ReplaceAll(strings.ToLower(opt), "_", "")
switch normOpt {
case "case", "inline", "unknown", "omitzero", "omitempty", "string", "format":
case "case", "embed", "omitzero", "omitempty", "string", "format":
err = cmp.Or(err, fmt.Errorf("Go struct field %s has invalid appearance of `%s` tag option; specify `%s` instead", sf.Name, opt, normOpt))
}
@@ -578,7 +573,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
// which is either a Go identifier or a single-quoted string.
// If the next option is invalid, it returns all of in until the next comma,
// and reports an error.
func consumeTagOption(in string) (string, int, error) {
func consumeTagOption(in string, allowQuoted bool) (string, int, error) {
// For legacy compatibility with v1, assume options are comma-separated.
i := strings.IndexByte(in, ',')
if i < 0 {
@@ -592,6 +587,10 @@ func consumeTagOption(in string) (string, int, error) {
return in[:n], n, nil
// Option as a single-quoted string.
case r == '\'':
if !allowQuoted {
return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter)", r)
}
// The grammar is nearly identical to a double-quoted Go string literal,
// but uses single quotes as the terminators. The reason for a custom
// grammar is because both backtick and double quotes cannot be used
@@ -633,6 +632,9 @@ func consumeTagOption(in string) (string, int, error) {
case len(in) == 0:
return in[:i], i, io.ErrUnexpectedEOF
default:
if !allowQuoted {
return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter)", r)
}
return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter or single quote)", r)
}
}