summaryrefslogtreecommitdiff
path: root/vendor/github.com/bytedance/sonic/internal/resolver/fields.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/bytedance/sonic/internal/resolver/fields.go')
-rw-r--r--vendor/github.com/bytedance/sonic/internal/resolver/fields.go388
1 files changed, 388 insertions, 0 deletions
diff --git a/vendor/github.com/bytedance/sonic/internal/resolver/fields.go b/vendor/github.com/bytedance/sonic/internal/resolver/fields.go
new file mode 100644
index 000000000..614bef4e5
--- /dev/null
+++ b/vendor/github.com/bytedance/sonic/internal/resolver/fields.go
@@ -0,0 +1,388 @@
+/**
+ * Copyright 2025 ByteDance Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package resolver
+
+import (
+ "reflect"
+ "sort"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/bytedance/sonic/internal/encoder/alg"
+)
+
+type StdField struct {
+ name string
+ nameBytes []byte
+ nameNonEsc string
+ nameEscHTML string
+ tag bool
+ index []int
+ typ reflect.Type
+ omitEmpty bool
+ omitZero bool
+ isZero func(reflect.Value) bool
+ quoted bool
+}
+
+type StdStructFields struct {
+ list []StdField
+ nameIndex map[string]*StdField
+ byFoldedName map[string]*StdField
+}
+
+func typeFields(t reflect.Type) StdStructFields {
+ // Anonymous fields to explore at the current level and the next.
+ current := []StdField{}
+ next := []StdField{{typ: t}}
+
+ // Count of queued names for current level and the next.
+ var count, nextCount map[reflect.Type]int
+
+ // Types already visited at an earlier level.
+ visited := map[reflect.Type]bool{}
+
+ // Fields found.
+ var fields []StdField
+
+ // Buffer to run appendHTMLEscape on field names.
+ var nameEscBuf []byte
+
+ for len(next) > 0 {
+ current, next = next, current[:0]
+ count, nextCount = nextCount, map[reflect.Type]int{}
+
+ for _, f := range current {
+ if visited[f.typ] {
+ continue
+ }
+ visited[f.typ] = true
+
+ // Scan f.typ for fields to include.
+ for i := 0; i < f.typ.NumField(); i++ {
+ sf := f.typ.Field(i)
+ if sf.Anonymous {
+ t := sf.Type
+ if t.Kind() == reflect.Pointer {
+ t = t.Elem()
+ }
+ if !sf.IsExported() && t.Kind() != reflect.Struct {
+ // Ignore embedded fields of unexported non-struct types.
+ continue
+ }
+ // Do not ignore embedded fields of unexported struct types
+ // since they may have exported fields.
+ } else if !sf.IsExported() {
+ // Ignore unexported non-embedded fields.
+ continue
+ }
+ tag := sf.Tag.Get("json")
+ if tag == "-" {
+ continue
+ }
+ name, opts := parseTag(tag)
+ if !isValidTag(name) {
+ name = ""
+ }
+ index := make([]int, len(f.index)+1)
+ copy(index, f.index)
+ index[len(f.index)] = i
+
+ ft := sf.Type
+ if ft.Name() == "" && ft.Kind() == reflect.Pointer {
+ // Follow pointer.
+ ft = ft.Elem()
+ }
+
+ // Only strings, floats, integers, and booleans can be quoted.
+ quoted := false
+ if opts.Contains("string") {
+ switch ft.Kind() {
+ case reflect.Bool,
+ reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+ reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
+ reflect.Float32, reflect.Float64,
+ reflect.String:
+ quoted = true
+ }
+ }
+
+ // Record found field and index sequence.
+ if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
+ tagged := name != ""
+ if name == "" {
+ name = sf.Name
+ }
+ field := StdField{
+ name: name,
+ tag: tagged,
+ index: index,
+ typ: ft,
+ omitEmpty: opts.Contains("omitempty"),
+ omitZero: opts.Contains("omitzero"),
+ quoted: quoted,
+ }
+ field.nameBytes = []byte(field.name)
+
+ // Build nameEscHTML and nameNonEsc ahead of time.
+ nameEscBuf = alg.HtmlEscape(nameEscBuf[:0], field.nameBytes)
+ field.nameEscHTML = `"` + string(nameEscBuf) + `":`
+ field.nameNonEsc = `"` + field.name + `":`
+
+ if field.omitZero {
+ t := sf.Type
+ // Provide a function that uses a type's IsZero method.
+ switch {
+ case t.Kind() == reflect.Interface && t.Implements(isZeroerType):
+ field.isZero = func(v reflect.Value) bool {
+ // Avoid panics calling IsZero on a nil interface or
+ // non-nil interface with nil pointer.
+ return v.IsNil() ||
+ (v.Elem().Kind() == reflect.Pointer && v.Elem().IsNil()) ||
+ v.Interface().(isZeroer).IsZero()
+ }
+ case t.Kind() == reflect.Pointer && t.Implements(isZeroerType):
+ field.isZero = func(v reflect.Value) bool {
+ // Avoid panics calling IsZero on nil pointer.
+ return v.IsNil() || v.Interface().(isZeroer).IsZero()
+ }
+ case t.Implements(isZeroerType):
+ field.isZero = func(v reflect.Value) bool {
+ return v.Interface().(isZeroer).IsZero()
+ }
+ case reflect.PointerTo(t).Implements(isZeroerType):
+ field.isZero = func(v reflect.Value) bool {
+ if !v.CanAddr() {
+ // Temporarily box v so we can take the address.
+ v2 := reflect.New(v.Type()).Elem()
+ v2.Set(v)
+ v = v2
+ }
+ return v.Addr().Interface().(isZeroer).IsZero()
+ }
+ }
+ }
+
+ fields = append(fields, field)
+ if count[f.typ] > 1 {
+ // If there were multiple instances, add a second,
+ // so that the annihilation code will see a duplicate.
+ // It only cares about the distinction between 1 and 2,
+ // so don't bother generating any more copies.
+ fields = append(fields, fields[len(fields)-1])
+ }
+ continue
+ }
+
+ // Record new anonymous struct to explore in next round.
+ nextCount[ft]++
+ if nextCount[ft] == 1 {
+ next = append(next, StdField{name: ft.Name(), index: index, typ: ft})
+ }
+ }
+ }
+ }
+
+ sort.Slice(fields, func(i, j int) bool {
+ a, b := fields[i], fields[j]
+ // sort field by name, breaking ties with depth, then
+ // breaking ties with "name came from json tag", then
+ // breaking ties with index sequence.
+ if c := strings.Compare(a.name, b.name); c != 0 {
+ return c < 0
+ }
+ if len(a.index) != len(b.index) {
+ return len(a.index) < len(b.index)
+ }
+ if a.tag != b.tag {
+ if a.tag {
+ return true
+ }
+ return false
+ }
+ return compare(a.index, b.index) < 0
+ })
+
+ // Delete all fields that are hidden by the Go rules for embedded fields,
+ // except that fields with JSON tags are promoted.
+
+ // The fields are sorted in primary order of name, secondary order
+ // of field index length. Loop over names; for each name, delete
+ // hidden fields by choosing the one dominant field that survives.
+ out := fields[:0]
+ for advance, i := 0, 0; i < len(fields); i += advance {
+ // One iteration per name.
+ // Find the sequence of fields with the name of this first field.
+ fi := fields[i]
+ name := fi.name
+ for advance = 1; i+advance < len(fields); advance++ {
+ fj := fields[i+advance]
+ if fj.name != name {
+ break
+ }
+ }
+ if advance == 1 { // Only one field with this name
+ out = append(out, fi)
+ continue
+ }
+ dominant, ok := dominantField(fields[i : i+advance])
+ if ok {
+ out = append(out, dominant)
+ }
+ }
+
+ fields = out
+ sort.Slice(fields, func(i, j int) bool {
+ a, b := fields[i], fields[j]
+ return compare(a.index, b.index) < 0
+ })
+
+ exactNameIndex := make(map[string]*StdField, len(fields))
+ foldedNameIndex := make(map[string]*StdField, len(fields))
+ for i, field := range fields {
+ exactNameIndex[field.name] = &fields[i]
+ // For historical reasons, first folded match takes precedence.
+ if _, ok := foldedNameIndex[string(foldName(field.nameBytes))]; !ok {
+ foldedNameIndex[string(foldName(field.nameBytes))] = &fields[i]
+ }
+ }
+ return StdStructFields{fields, exactNameIndex, foldedNameIndex}
+}
+
+func compare(s1, s2 []int) int {
+ for i, v1 := range s1 {
+ if i >= len(s2) {
+ return +1
+ }
+ v2 := s2[i]
+ if v1 != v2 {
+ return v1 - v2
+ }
+ }
+ if len(s1) < len(s2) {
+ return -1
+ }
+ return 0
+}
+
+type isZeroer interface {
+ IsZero() bool
+}
+
+var isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
+
+// tagOptions is the string following a comma in a struct field's "json"
+// tag, or the empty string. It does not include the leading comma.
+type tagOptions string
+
+// parseTag splits a struct field's json tag into its name and
+// comma-separated options.
+func parseTag(tag string) (string, tagOptions) {
+ tag, opt, _ := strings.Cut(tag, ",")
+ return tag, tagOptions(opt)
+}
+
+// Contains reports whether a comma-separated list of options
+// contains a particular substr flag. substr must be surrounded by a
+// string boundary or commas.
+func (o tagOptions) Contains(optionName string) bool {
+ if len(o) == 0 {
+ return false
+ }
+ s := string(o)
+ for s != "" {
+ var name string
+ name, s, _ = strings.Cut(s, ",")
+ if name == optionName {
+ return true
+ }
+ }
+ return false
+}
+
+func isValidTag(s string) bool {
+ if s == "" {
+ return false
+ }
+ for _, c := range s {
+ switch {
+ case strings.ContainsRune("!#$%&()*+-./:;<=>?@[]^_{|}~ ", c):
+ // Backslash and quote chars are reserved, but
+ // otherwise any punctuation chars are allowed
+ // in a tag name.
+ case !unicode.IsLetter(c) && !unicode.IsDigit(c):
+ return false
+ }
+ }
+ return true
+}
+
+// dominantField looks through the fields, all of which are known to
+// have the same name, to find the single field that dominates the
+// others using Go's embedding rules, modified by the presence of
+// JSON tags. If there are multiple top-level fields, the boolean
+// will be false: This condition is an error in Go and we skip all
+// the fields.
+func dominantField(fields []StdField) (StdField, bool) {
+ // The fields are sorted in increasing index-length order, then by presence of tag.
+ // That means that the first field is the dominant one. We need only check
+ // for error cases: two fields at top level, either both tagged or neither tagged.
+ if len(fields) > 1 && len(fields[0].index) == len(fields[1].index) && fields[0].tag == fields[1].tag {
+ return StdField{}, false
+ }
+ return fields[0], true
+}
+
+
+// foldName returns a folded string such that foldName(x) == foldName(y)
+// is identical to bytes.EqualFold(x, y).
+func foldName(in []byte) []byte {
+ // This is inlinable to take advantage of "function outlining".
+ var arr [32]byte // large enough for most JSON names
+ return appendFoldedName(arr[:0], in)
+}
+
+func appendFoldedName(out, in []byte) []byte {
+ for i := 0; i < len(in); {
+ // Handle single-byte ASCII.
+ if c := in[i]; c < utf8.RuneSelf {
+ if 'a' <= c && c <= 'z' {
+ c -= 'a' - 'A'
+ }
+ out = append(out, c)
+ i++
+ continue
+ }
+ // Handle multi-byte Unicode.
+ r, n := utf8.DecodeRune(in[i:])
+ out = utf8.AppendRune(out, foldRune(r))
+ i += n
+ }
+ return out
+}
+
+// foldRune is returns the smallest rune for all runes in the same fold set.
+func foldRune(r rune) rune {
+ for {
+ r2 := unicode.SimpleFold(r)
+ if r2 <= r {
+ return r2
+ }
+ r = r2
+ }
+}