// EmmyLua / lua-language-server annotation emission. // // Annotations are pure comments — they have no runtime effect — but // they teach the Lua language server about generated message shapes, // enum aliases, and wrapper function signatures. Users of generated // code get autocomplete, type-checking, and rename refactoring for // free in any editor that consumes EmmyLua (VS Code + sumneko, Neovim // LSP, JetBrains EmmyLua plugin). // // Output (per file): // 1. ---@alias integer (one per enum) // 2. ---@class (one per message) // ---@field // ... // 3. ---@param / ---@return on each emitted M._* function. // // All three blocks are emitted by gen.go at well-defined points so // they live alongside the code they describe. // // Type mapping (proto -> EmmyLua): // - bool -> boolean // - string / bytes -> string // - float / double -> number // - all int kinds -> integer (64-bit are uint64_t/int64_t cdata // at runtime; LSP has no cdata model, // so they're typed as integer with // an inline note) // - enum -> (alias declared elsewhere) // - message -> (class declared elsewhere) // - repeated -> T[] // - map -> table // // Field presence: // - proto3 explicit `optional` -> trailing `?` on the field name // - oneof branch -> trailing `?` (only one is set at a time) // - everything else -> no marker (proto3 defaults imply // a typed zero value if absent) package gen import ( "fmt" "strings" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/reflect/protoreflect" ) // emmyTypeName returns the qualified EmmyLua class/alias identifier for a // message or enum. We use the proto full name verbatim (with dots), which // lua-language-server accepts as a class identifier. This keeps cross-file // references straightforward — every file referring to "hello.Person" // resolves to the same `---@class hello.Person` block, regardless of which // generated `.lua` declared it. func emmyTypeName(td protoreflect.Descriptor) string { return string(td.FullName()) } // emmyScalarType maps a proto3 scalar kind to its EmmyLua surface type. func emmyScalarType(k protoreflect.Kind) string { switch k { case protoreflect.BoolKind: return "boolean" case protoreflect.StringKind, protoreflect.BytesKind: return "string" case protoreflect.FloatKind, protoreflect.DoubleKind: return "number" default: // All int kinds. 64-bit are int64_t/uint64_t cdata at runtime, // but EmmyLua has no model for that; users either compare via // pb.to_int64/pb.to_uint64 or convert with tonumber for small // values. Typing as integer matches user mental model. return "integer" } } // emmyFieldType returns the surface type for one field, accounting for // kind (scalar/enum/message/map) and the repeated modifier. func emmyFieldType(f *protogen.Field) string { if f.Desc.IsMap() { kf := f.Message.Fields[0] vf := f.Message.Fields[1] return fmt.Sprintf("table<%s, %s>", emmyFieldTypeSingular(kf), emmyFieldTypeSingular(vf)) } t := emmyFieldTypeSingular(f) if f.Desc.IsList() { return t + "[]" } return t } func emmyFieldTypeSingular(f *protogen.Field) string { switch { case f.Message != nil: return emmyTypeName(f.Message.Desc) case f.Enum != nil: return emmyTypeName(f.Enum.Desc) default: return emmyScalarType(f.Desc.Kind()) } } // emmyFieldOptional reports whether the field should carry a trailing `?`. // Two cases: proto3 explicit optional, and oneof branches (only one is // set at a time, so every branch is presence-tracked). func emmyFieldOptional(f *protogen.Field) bool { if f.Desc.HasOptionalKeyword() { return true } if f.Oneof != nil && !f.Desc.HasOptionalKeyword() { return true } return false } // emitEmmyEnumAlias emits `---@alias integer` for each enum. // Could be tightened to ` | | ...` but that locks the // schema in the annotation; users typically write `M.Status.OK` (an // integer literal) so `integer` is the honest type. func emitEmmyEnumAlias(w *writer, e *protogen.Enum) { w.line("---@alias %s integer", emmyTypeName(e.Desc)) } // emitEmmyMessageClass emits the `---@class` block for one message, // listing every field with its surface type and optional marker. func emitEmmyMessageClass(w *writer, m *protogen.Message) { w.line("---@class %s", emmyTypeName(m.Desc)) for _, f := range m.Fields { name := string(f.Desc.Name()) if emmyFieldOptional(f) { name = name + "?" } w.line("---@field %s %s", name, emmyFieldType(f)) } } // emitEmmyWrappersHeader prefaces the M._new / _encode / _decode / // _decode_lazy / has_/clear_ stubs with their EmmyLua annotations. Each // wrapper gets its own `---@param` / `---@return` lines printed // immediately before the corresponding `function M._*(...)` line. // // emitMessageWrappers (runtime mode) and emitInlineMessage (full mode) // each call into this for the typed prelude; the function bodies stay // where they were. func emitEmmyWrapperAnnotations(w *writer, name string, fullName string, kind emmyWrapperKind) { t := fullName switch kind { case wrapperNew: w.line("---@param t? %s", t) w.line("---@return %s", t) case wrapperEncode: w.line("---@param t %s", t) w.line("---@return string") case wrapperDecode: w.line("---@param b string") w.line("---@return %s", t) case wrapperDecodeLazy: w.line("---@param b string") w.line("---@return pb.MessageView") case wrapperText: w.line("---@param t %s", t) w.line("---@param opts? {single_line: boolean?, indent: string?}") w.line("---@return string") case wrapperHas: w.line("---@param t %s", t) w.line("---@return boolean") case wrapperClear: w.line("---@param t %s", t) } } type emmyWrapperKind int const ( wrapperNew emmyWrapperKind = iota wrapperEncode wrapperDecode wrapperDecodeLazy wrapperText wrapperHas wrapperClear ) // emitEmmyTypesBlock emits the file's whole types section: every enum // alias, every message class. Called once per file by GenerateFile, // after the descriptor pre-declarations and before the wrappers. func emitEmmyTypesBlock(w *writer, enums []*protogen.Enum, msgs []*protogen.Message) { if len(enums) == 0 && len(msgs) == 0 { return } w.line("-- EmmyLua / lua-language-server type annotations.") w.line("-- These are comments — no runtime effect. They give editors") w.line("-- autocomplete and type-checking for the generated wrappers.") for _, e := range enums { emitEmmyEnumAlias(w, e) } if len(enums) > 0 { w.line("") } for i, m := range msgs { emitEmmyMessageClass(w, m) if i < len(msgs)-1 { w.line("") } } w.line("") } // emmyMessageFullName is a convenience wrapper used by the wrapper // emitters to resolve the right `---@class` identifier for a message. func emmyMessageFullName(m *protogen.Message) string { return strings.TrimSpace(string(m.Desc.FullName())) }