package gen import ( "fmt" "math" "sort" "strconv" "strings" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/dynamicpb" ) // optionsResolver re-links *Options messages so custom extensions defined // inside an input .proto file are reachable to the protoreflect walker. // // Why this is needed: protogen builds each File's descriptor BEFORE in-file // extensions get registered into its internal resolver. As a result, // `file.Desc.Options()` (and the same for nested messages / fields / // services / methods) parses options against a resolver that doesn't yet // know the in-file extensions — those land silently in the unknown-fields // bucket and don't surface via `Range`. The Plugin's re-marshal pass // (`hasNovelExtensions`) only fixes `file.Proto`, which protogen.Message // & co. don't expose. // // Workaround: build a single resolver carrying every extension defined in // every input file (transitively — nested message extensions included), and // re-decode each *Options message through it on demand. Marshal + unmarshal // per options message is cheap (options are tiny). type optionsResolver struct { types *protoregistry.Types } func newOptionsResolver(plug *protogen.Plugin) *optionsResolver { types := new(protoregistry.Types) for _, f := range plug.Files { registerExtensions(types, f.Desc) } return &optionsResolver{types: types} } // extensionContainer is the common slice of FileDescriptor / MessageDescriptor // that exposes both extensions and nested messages. type extensionContainer interface { Extensions() protoreflect.ExtensionDescriptors Messages() protoreflect.MessageDescriptors } func registerExtensions(types *protoregistry.Types, c extensionContainer) { exts := c.Extensions() for i := 0; i < exts.Len(); i++ { // Best-effort: an already-registered name (when two input files // re-import the same extension) returns AlreadyExists, which we // can safely ignore. _ = types.RegisterExtension(dynamicpb.NewExtensionType(exts.Get(i))) } msgs := c.Messages() for i := 0; i < msgs.Len(); i++ { registerExtensions(types, msgs.Get(i)) } } // relink re-decodes an *Options message through the resolver so populated // extensions move from the unknown-fields bucket into typed fields. // Returns nil on a nil input. func (r *optionsResolver) relink(opts proto.Message) (proto.Message, error) { if opts == nil { return nil, nil } if r == nil || r.types == nil { return opts, nil } b, err := proto.Marshal(opts) if err != nil { return nil, err } fresh := opts.ProtoReflect().Type().New().Interface() if err := (proto.UnmarshalOptions{Resolver: r.types}).Unmarshal(b, fresh); err != nil { return nil, err } return fresh, nil } // renderDescriptorOptions walks every populated field on a descriptor's // *Options message and emits it as a Lua table literal `{key = value, ...}`. // // Returns "" when nothing is populated — callers must omit the `options` // key entirely in that case (we never emit `options = {}`). // // The walker is generic: there are no extension-specific code paths. Every // extension defined against any *Options message surfaces with the same // shape, so consumers can pull whatever they care about (google.api.http, // versionpb.etcd_version_*, in-house extensions) without pb knowing about it. func renderDescriptorOptions(resolver *optionsResolver, opts proto.Message) (string, error) { if opts == nil { return "", nil } relinked, err := resolver.relink(opts) if err != nil { return "", err } msg := relinked.ProtoReflect() if !msg.IsValid() { return "", nil } return renderOptionsMessage(msg) } // optionEntry holds one populated field for sorted, deterministic emission. type optionEntry struct { isExt bool // sortName: proto field name for standard fields, extension full name // for extensions. Lets us sort standard-then-extension, each alphabetical. sortName string // key is the Lua source for the key (`foo` or `["full.name"]`). key string value string } func renderOptionsMessage(msg protoreflect.Message) (string, error) { var items []optionEntry var walkErr error msg.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { // UninterpretedOption is protoc's parser-internal slot. Once the // option's defining .proto is in the input set, the option lands // as a real extension and uninterpreted_option stays empty. A // populated entry means protoc couldn't resolve the extension — // surface that explicitly rather than emit opaque parser state. if !fd.IsExtension() && fd.Name() == "uninterpreted_option" { walkErr = fmt.Errorf( "%s carries unresolved UninterpretedOption %q — the "+ "defining .proto for the extension must be in the "+ "protoc input set (-I path)", msg.Descriptor().FullName(), describeUninterpreted(v)) return false } val, err := renderOptionValue(fd, v) if err != nil { walkErr = err return false } var key, sortName string if fd.IsExtension() { key = fmt.Sprintf("[%q]", string(fd.FullName())) sortName = string(fd.FullName()) } else { key = luaTableKey(string(fd.Name())) sortName = string(fd.Name()) } items = append(items, optionEntry{ isExt: fd.IsExtension(), sortName: sortName, key: key, value: val, }) return true }) if walkErr != nil { return "", walkErr } if len(items) == 0 { return "", nil } // Deterministic ordering: standard fields first (alphabetical), then // extensions (alphabetical by full name). sort.SliceStable(items, func(i, j int) bool { if items[i].isExt != items[j].isExt { return !items[i].isExt } return items[i].sortName < items[j].sortName }) parts := make([]string, 0, len(items)) for _, p := range items { parts = append(parts, p.key+" = "+p.value) } return "{" + strings.Join(parts, ", ") + "}", nil } // renderOptionValue handles cardinality (singular / repeated / map). Singular // values are dispatched to renderOptionScalar. func renderOptionValue(fd protoreflect.FieldDescriptor, v protoreflect.Value) (string, error) { switch { case fd.IsList(): list := v.List() parts := make([]string, list.Len()) for i := 0; i < list.Len(); i++ { s, err := renderOptionLeaf(fd, list.Get(i)) if err != nil { return "", err } parts[i] = s } return "{" + strings.Join(parts, ", ") + "}", nil case fd.IsMap(): // Map options are rare; emit `{[k] = v, ...}` sorted by stringified // key for stable output. m := v.Map() keyFD := fd.MapKey() valFD := fd.MapValue() type kv struct{ k, v string } var pairs []kv var err error m.Range(func(mk protoreflect.MapKey, mv protoreflect.Value) bool { ks, e := renderOptionLeaf(keyFD, mk.Value()) if e != nil { err = e return false } vs, e := renderOptionLeaf(valFD, mv) if e != nil { err = e return false } pairs = append(pairs, kv{ks, vs}) return true }) if err != nil { return "", err } sort.Slice(pairs, func(i, j int) bool { return pairs[i].k < pairs[j].k }) parts := make([]string, len(pairs)) for i, p := range pairs { parts[i] = "[" + p.k + "] = " + p.v } return "{" + strings.Join(parts, ", ") + "}", nil } return renderOptionLeaf(fd, v) } // renderOptionLeaf renders a single (non-list, non-map) value. func renderOptionLeaf(fd protoreflect.FieldDescriptor, v protoreflect.Value) (string, error) { switch fd.Kind() { case protoreflect.BoolKind: if v.Bool() { return "true", nil } return "false", nil case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: return strconv.FormatInt(int64(int32(v.Int())), 10), nil case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: return strconv.FormatUint(uint64(uint32(v.Uint())), 10), nil case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: // LuaJIT cdata literal — matches the convention pb decoders use // for proto3 int64 fields. return strconv.FormatInt(v.Int(), 10) + "LL", nil case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: return strconv.FormatUint(v.Uint(), 10) + "ULL", nil case protoreflect.FloatKind, protoreflect.DoubleKind: return formatLuaFloat(v.Float()), nil case protoreflect.StringKind: return strconv.Quote(v.String()), nil case protoreflect.BytesKind: return luaByteString(v.Bytes()), nil case protoreflect.EnumKind: ev := fd.Enum().Values().ByNumber(v.Enum()) if ev != nil { return strconv.Quote(string(ev.Name())), nil } return strconv.FormatInt(int64(v.Enum()), 10), nil case protoreflect.MessageKind, protoreflect.GroupKind: nested, err := renderOptionsMessage(v.Message()) if err != nil { return "", err } if nested == "" { // Message-valued option with all fields defaulted — emit `{}` // so the option's presence remains visible to consumers. return "{}", nil } return nested, nil } return "", fmt.Errorf("unsupported option kind: %s", fd.Kind()) } // formatLuaFloat renders a Go float64 as Lua source. NaN/Inf get the // idiomatic Lua expressions instead of strconv's "NaN"/"+Inf" tokens. func formatLuaFloat(f float64) string { switch { case math.IsNaN(f): return "0/0" case math.IsInf(f, 1): return "math.huge" case math.IsInf(f, -1): return "-math.huge" } return strconv.FormatFloat(f, 'g', -1, 64) } // mustRenderOptions is the panic-on-error variant for hot codegen sites // where threading an error return would balloon every emit signature. // A populated UninterpretedOption (the only error we surface) is a // codegen-time invariant violation — failing loudly via the plugin's // panic handler is the right escalation. func mustRenderOptions(resolver *optionsResolver, opts proto.Message) string { s, err := renderDescriptorOptions(resolver, opts) if err != nil { panic("tarantool-protobuf: " + err.Error()) } return s } // describeUninterpreted formats UninterpretedOption.name parts into the // proto dotted-identifier form so the codegen error tells the user // exactly which option went unresolved. func describeUninterpreted(v protoreflect.Value) string { list := v.List() if list.Len() == 0 { return "(empty)" } first := list.Get(0).Message() desc := first.Descriptor() nameFD := desc.Fields().ByName("name") if nameFD == nil { return "(unknown)" } parts := first.Get(nameFD).List() var sb strings.Builder partDesc := nameFD.Message() partNameFD := partDesc.Fields().ByName("name_part") partExtFD := partDesc.Fields().ByName("is_extension") for i := 0; i < parts.Len(); i++ { part := parts.Get(i).Message() if i > 0 { sb.WriteByte('.') } isExt := partExtFD != nil && part.Get(partExtFD).Bool() if isExt { sb.WriteByte('(') } sb.WriteString(part.Get(partNameFD).String()) if isExt { sb.WriteByte(')') } } return sb.String() }