// 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) { emitProtoDoc(w, e.Comments.Leading) 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. Leading // proto comments on the message become `---` lines above `---@class`; // per-field comments are appended to each `---@field` line. func emitEmmyMessageClass(w *writer, m *protogen.Message) { emitProtoDoc(w, m.Comments.Leading) w.line("---@class %s", emmyTypeName(m.Desc)) for _, f := range m.Fields { name := string(f.Desc.Name()) if emmyFieldOptional(f) { name = name + "?" } line := fmt.Sprintf("---@field %s %s", name, emmyFieldType(f)) if doc := flattenComment(f.Comments.Leading); doc != "" { line += " @ " + doc } w.line("%s", line) } } // emitProtoDoc emits each line of a proto leading-comment block as a // `--- text` line. LuaLS treats those as the description of the next // `---@class`/`---@alias`/function declaration, so they show up on hover. func emitProtoDoc(w *writer, c protogen.Comments) { s := strings.TrimSuffix(string(c), "\n") if s == "" { return } for _, line := range strings.Split(s, "\n") { line = strings.TrimPrefix(line, " ") if line == "" { w.line("---") } else { w.line("--- %s", line) } } } // emitProtoDocIndented emits each line of a proto leading-comment block // as a regular Lua `-- text` line prefixed with `indent`. Use inside // table literals (enum value rows, service method entries) where LuaLS // doc attachment doesn't apply but the human-facing context is still // worth preserving. func emitProtoDocIndented(w *writer, c protogen.Comments, indent string) { s := strings.TrimSuffix(string(c), "\n") if s == "" { return } for _, line := range strings.Split(s, "\n") { line = strings.TrimPrefix(line, " ") if line == "" { w.line("%s--", indent) } else { w.line("%s-- %s", indent, line) } } } // flattenComment collapses a (possibly multi-line) proto leading comment // into a single trimmed line, suitable for trailing `@description` on a // `---@field` line. Empty input returns "". func flattenComment(c protogen.Comments) string { s := strings.TrimSuffix(string(c), "\n") if s == "" { return "" } parts := strings.Split(s, "\n") for i, line := range parts { parts[i] = strings.TrimSpace(line) } return strings.TrimSpace(strings.Join(parts, " ")) } // 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())) }