~bigbes/tarantool

tarantool-protobuf

ref: f5c5ee6c333c5f993cfaf4e3870d01f7b6d71b35 tarantool-protobuf/cmd/protoc-gen-tarantool/internal/gen/emmylua.go -rw-r--r-- 7.1 KiB
f5c5ee6c — Eugene Blikh codec: preserve -0.0 for proto3 float/double scalars 3 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// 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 <pkg.Enum> integer        (one per enum)
//   2. ---@class <pkg.Message>             (one per message)
//      ---@field <name> <type>
//      ...
//   3. ---@param / ---@return on each emitted M.<Name>_* 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<Name>          -> <Name>          (alias declared elsewhere)
//   - message<Name>       -> <Name>          (class declared elsewhere)
//   - repeated<T>         -> T[]
//   - map<K,V>            -> table<K, V>
//
// 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 <pkg.Enum> integer` for each enum.
// Could be tightened to `<value> | <value> | ...` 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.<Name>_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.<Name>_*(...)` 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()))
}