M cmd/protoc-gen-tarantool/internal/gen/gen.go => cmd/protoc-gen-tarantool/internal/gen/gen.go +6 -5
@@ 263,7 263,7 @@ func emitFieldNamesTable(w *writer, name string, m *protogen.Message) {
w.line("M.%s_fields = pb.field_names({", name)
for _, f := range m.Fields {
fn := string(f.Desc.Name())
- w.line(" %s = %q,", fn, fn)
+ w.line(" %s = %q,", luaTableKey(fn), fn)
}
w.line("})")
}
@@ 284,7 284,7 @@ func emitOneofNamesTable(w *writer, name string, m *protogen.Message) {
}
w.line("M.%s_oneofs = pb.field_names({", name)
for _, on := range names {
- w.line(" %s = %q,", on, on)
+ w.line(" %s = %q,", luaTableKey(on), on)
}
w.line("})")
}
@@ 318,7 318,7 @@ func emitOneofTable(w *writer, name string, m *protogen.Message) {
for _, fn := range row.members {
quoted = append(quoted, fmt.Sprintf("%q", fn))
}
- w.line(" %s = {%s},", row.name, strings.Join(quoted, ", "))
+ w.line(" %s = {%s},", luaTableKey(row.name), strings.Join(quoted, ", "))
}
w.line("}")
}
@@ 469,10 469,11 @@ func emitOptionalAccessors(w *writer, name string, m *protogen.Message, fullName
continue
}
fname := string(f.Desc.Name())
+ access := luaFieldAccess("t", fname)
emitEmmyWrapperAnnotations(w, name, fullName, wrapperHas)
- w.line("function M.%s_has_%s(t) return t.%s ~= nil end", name, fname, fname)
+ w.line("function M.%s_has_%s(t) return %s ~= nil end", name, fname, access)
emitEmmyWrapperAnnotations(w, name, fullName, wrapperClear)
- w.line("function M.%s_clear_%s(t) t.%s = nil end", name, fname, fname)
+ w.line("function M.%s_clear_%s(t) %s = nil end", name, fname, access)
}
}
M cmd/protoc-gen-tarantool/internal/gen/inline.go => cmd/protoc-gen-tarantool/internal/gen/inline.go +17 -13
@@ 43,8 43,8 @@ func emitInlineEncode(w *writer, name string, m *protogen.Message, file *protoge
for _, oo := range realOneofs(m) {
w.line(" local %s", oneofVar(string(oo.Desc.Name())))
for _, f := range oo.Fields {
- w.line(" if t.%s ~= nil then %s = %q end",
- string(f.Desc.Name()),
+ w.line(" if %s ~= nil then %s = %q end",
+ luaFieldAccess("t", string(f.Desc.Name())),
oneofVar(string(oo.Desc.Name())),
string(f.Desc.Name()))
}
@@ 92,7 92,7 @@ func emitInlineEncodeField(w *writer, f *protogen.Field, file *protogen.File, se
fname := string(f.Desc.Name())
w.line(" -- field %d: %s", id, fname)
- w.line(" v = t.%s", fname)
+ w.line(" v = %s", luaFieldAccess("t", fname))
oneof := fieldRealOneof(f)
// Default presence gate: a regular nil check. For message fields we
@@ 290,20 290,21 @@ func emitInlineDecodeFieldBody(w *writer, f *protogen.Field, file *protogen.File
emitInlineDecodeRepeated(w, f, fname, file, selfPath, imports, prefix)
case f.Message != nil:
ref := typeRef(file, f.Message.Desc, selfPath, imports, "_decode", prefix)
+ dst := luaFieldAccess("result", fname)
w.line(" local payload")
w.line(" payload, pos = wire.decode_len(buf, pos)")
if isWellKnownTypeFile(f.Message.Desc.ParentFile()) {
// WKT decoders return unwrapped values (datetime, number, string),
// not Lua tables — there is nothing to merge into. Replace.
- w.line(" result.%s = %s(payload)", fname, ref)
+ w.line(" %s = %s(payload)", dst, ref)
} else {
// Per proto3 spec, repeated occurrences of a singular message
// field merge recursively. This holds for oneof branches too;
// sibling clearing below enforces oneof exclusivity.
descRef := typeRef(file, f.Message.Desc, selfPath, imports, "_descriptor", prefix)
- w.line(" local prev = result.%s", fname)
+ w.line(" local prev = %s", dst)
w.line(" if prev == nil then")
- w.line(" result.%s = %s(payload)", fname, ref)
+ w.line(" %s = %s(payload)", dst, ref)
w.line(" else")
w.line(" pb.codec.merge_message(%s, prev, %s(payload))",
descRef, ref)
@@ 312,12 313,12 @@ func emitInlineDecodeFieldBody(w *writer, f *protogen.Field, file *protogen.File
case f.Enum != nil:
w.line(" local u")
w.line(" u, pos = wire.decode_varint(buf, pos)")
- w.line(" result.%s = wire.varint_to_int32(u)", fname)
+ w.line(" %s = wire.varint_to_int32(u)", luaFieldAccess("result", fname))
default:
st := scalarName(f.Desc.Kind())
w.line(" local val")
w.line(" val, pos = wire.decode_%s(buf, pos)", st)
- w.line(" result.%s = val", fname)
+ w.line(" %s = val", luaFieldAccess("result", fname))
}
// Oneof: clear sibling branches so callers see exactly one set field.
@@ 326,14 327,16 @@ func emitInlineDecodeFieldBody(w *writer, f *protogen.Field, file *protogen.File
if sib == f {
continue
}
- w.line(" result.%s = nil", string(sib.Desc.Name()))
+ w.line(" %s = nil",
+ luaFieldAccess("result", string(sib.Desc.Name())))
}
}
}
func emitInlineDecodeRepeated(w *writer, f *protogen.Field, fname string, file *protogen.File, selfPath string, imports map[string]string, prefix string) {
- w.line(" local list = result.%s", fname)
- w.line(" if list == nil then list = {}; result.%s = list end", fname)
+ dst := luaFieldAccess("result", fname)
+ w.line(" local list = %s", dst)
+ w.line(" if list == nil then list = {}; %s = list end", dst)
switch {
case f.Message != nil:
@@ 512,8 515,9 @@ func mapSubFieldWireType(f *protogen.Field) int {
func emitInlineDecodeMap(w *writer, f *protogen.Field, fname string, file *protogen.File, selfPath string, imports map[string]string, prefix string) {
keyF, valF := f.Message.Fields[0], f.Message.Fields[1]
- w.line(" local map = result.%s", fname)
- w.line(" if map == nil then map = {}; result.%s = map end", fname)
+ dst := luaFieldAccess("result", fname)
+ w.line(" local map = %s", dst)
+ w.line(" if map == nil then map = {}; %s = map end", dst)
w.line(" local payload")
w.line(" payload, pos = wire.decode_len(buf, pos)")
w.line(" local _ep, _elim = 1, #payload")
M cmd/protoc-gen-tarantool/internal/gen/name.go => cmd/protoc-gen-tarantool/internal/gen/name.go +51 -0
@@ 1,12 1,63 @@
package gen
import (
+ "fmt"
"path"
"strings"
"google.golang.org/protobuf/reflect/protoreflect"
)
+// luaReservedWords is the frozen set of Lua 5.1 / LuaJIT keywords. A proto
+// field whose name collides with one of these can't be emitted as a bare
+// identifier in generated Lua — table-key form and bracket-index form
+// must be used instead. See luaTableKey / luaFieldAccess.
+var luaReservedWords = map[string]bool{
+ "and": true, "break": true, "do": true, "else": true,
+ "elseif": true, "end": true, "false": true, "for": true,
+ "function": true, "goto": true, "if": true, "in": true,
+ "local": true, "nil": true, "not": true, "or": true,
+ "repeat": true, "return": true, "then": true, "true": true,
+ "until": true, "while": true,
+}
+
+func isLuaReservedWord(name string) bool { return luaReservedWords[name] }
+
+func isValidLuaIdentifier(name string) bool {
+ if name == "" {
+ return false
+ }
+ for i, r := range name {
+ switch {
+ case r == '_' || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z'):
+ case i > 0 && r >= '0' && r <= '9':
+ default:
+ return false
+ }
+ }
+ return true
+}
+
+// luaTableKey returns a representation of `name` valid as a Lua table key.
+// Bare identifier when the name is a valid Lua identifier and not reserved;
+// bracket-quoted form otherwise.
+func luaTableKey(name string) string {
+ if isLuaReservedWord(name) || !isValidLuaIdentifier(name) {
+ return fmt.Sprintf("[%q]", name)
+ }
+ return name
+}
+
+// luaFieldAccess returns Lua source for `<receiver>.<name>`, falling back
+// to `<receiver>[<name:q>]` when `name` is a Lua keyword or otherwise
+// invalid as a bare identifier.
+func luaFieldAccess(receiver, name string) string {
+ if isLuaReservedWord(name) || !isValidLuaIdentifier(name) {
+ return fmt.Sprintf("%s[%q]", receiver, name)
+ }
+ return fmt.Sprintf("%s.%s", receiver, name)
+}
+
// luaPackagePath returns the dotted Lua require path for a generated file.
//
// Resolution order:
A test/codegen_lua_keywords_test.lua => test/codegen_lua_keywords_test.lua +151 -0
@@ 0,0 1,151 @@
+-- Regression test for Lua-keyword proto field names.
+--
+-- protoc-gen-tarantool used to emit bare-identifier table keys and
+-- `t.<field>` / `result.<field>` accesses for every field. When a field
+-- name happened to be a Lua reserved word (the in-the-wild hit is
+-- pprof's `repeated Function function = 5`) the generated `*_pb.lua`
+-- failed to load with `'(' expected near '<keyword>'`.
+--
+-- Coverage: emit a .proto where every Lua keyword is used as a field
+-- name, run the plugin in both codegen modes, and assert:
+-- * The generated module loads without error.
+-- * Each `M.<Type>_fields[<keyword>]` resolves to the same name string
+-- (covers the field_names table emit site).
+-- * Encoding `{[<keyword>] = ...}` and decoding the bytes round-trips
+-- (covers the inline-encoder `v = t.<name>` and inline-decoder
+-- `result.<name> =` sites).
+
+local t = require('luatest')
+local fio = require('fio')
+
+local g = t.group('codegen_lua_keywords')
+
+local REPO_ROOT = fio.abspath(fio.pathjoin(
+ fio.dirname(debug.getinfo(1, 'S').source:sub(2)), '..'))
+local OPTIONS_DIR = fio.pathjoin(REPO_ROOT, 'options')
+local PLUGIN = fio.pathjoin(REPO_ROOT, 'protoc-gen-tarantool')
+
+local LUA_KEYWORDS = {
+ 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for',
+ 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or',
+ 'repeat', 'return', 'then', 'true', 'until', 'while',
+}
+
+local function spit(path, content)
+ local f = assert(io.open(path, 'wb'))
+ f:write(content)
+ f:close()
+end
+
+local function ensure_plugin()
+ if fio.path.exists(PLUGIN) then return end
+ local cmd = string.format('cd %q && go build -o %s ./cmd/protoc-gen-tarantool',
+ REPO_ROOT, fio.basename(PLUGIN))
+ assert(os.execute(cmd) == 0 or os.execute(cmd) == true,
+ 'failed to build plugin: ' .. cmd)
+end
+
+-- Build a .proto whose every field name is a Lua keyword.
+local function build_proto()
+ local lines = {
+ 'syntax = "proto3";',
+ 'package keywords_test;',
+ 'message Inner { int32 x = 1; }',
+ 'message Outer {',
+ }
+ -- Field id 1: nested message named `function` (the pprof shape).
+ table.insert(lines, ' Inner function = 1;')
+ -- Subsequent ids: int32 scalars named after every other Lua keyword.
+ local id = 2
+ for _, kw in ipairs(LUA_KEYWORDS) do
+ if kw ~= 'function' then
+ table.insert(lines, string.format(' int32 %s = %d;', kw, id))
+ id = id + 1
+ end
+ end
+ table.insert(lines, '}')
+ return table.concat(lines, '\n') .. '\n'
+end
+
+local function run_plugin(mode)
+ local tmp = fio.tempdir()
+ local proto_dir = fio.pathjoin(tmp, 'proto')
+ local out_dir = fio.pathjoin(tmp, 'out')
+ assert(fio.mkdir(proto_dir))
+ assert(fio.mkdir(out_dir))
+ spit(fio.pathjoin(proto_dir, 'kw.proto'), build_proto())
+
+ local cmd = string.format(
+ 'protoc --plugin=%q --tarantool_out=%q '
+ ..'--tarantool_opt=mode=%s,prefix=kw_%s '
+ ..'-I %q -I %q %q',
+ PLUGIN, out_dir, mode, mode, proto_dir, OPTIONS_DIR,
+ fio.pathjoin(proto_dir, 'kw.proto'))
+ local ok = os.execute(cmd)
+ assert(ok == 0 or ok == true, 'plugin failed: ' .. cmd)
+ return out_dir, ('kw_%s.keywords_test.kw_pb'):format(mode)
+end
+
+g.before_all(function()
+ ensure_plugin()
+end)
+
+for _, mode in ipairs({'full', 'runtime'}) do
+ g['test_module_loads_with_keyword_fields_'..mode] = function()
+ local out, modname = run_plugin(mode)
+ local prev = package.path
+ package.path = fio.pathjoin(out, '?.lua') .. ';'
+ .. fio.pathjoin(out, '?/init.lua') .. ';' .. prev
+ package.loaded[modname] = nil
+ local ok, mod = pcall(require, modname)
+ package.path = prev
+ t.assert(ok, 'module failed to load: ' .. tostring(mod))
+ t.assert_type(mod.Outer_encode, 'function')
+ t.assert_type(mod.Outer_decode, 'function')
+ t.assert_type(mod.Outer_fields, 'table')
+ end
+
+ g['test_field_names_table_carries_keyword_keys_'..mode] = function()
+ local out, modname = run_plugin(mode)
+ local prev = package.path
+ package.path = fio.pathjoin(out, '?.lua') .. ';'
+ .. fio.pathjoin(out, '?/init.lua') .. ';' .. prev
+ package.loaded[modname] = nil
+ local mod = require(modname)
+ package.path = prev
+ for _, kw in ipairs(LUA_KEYWORDS) do
+ t.assert_equals(mod.Outer_fields[kw], kw,
+ 'M.Outer_fields["' .. kw .. '"] must round-trip the keyword')
+ end
+ end
+
+ g['test_round_trip_keyword_fields_'..mode] = function()
+ local out, modname = run_plugin(mode)
+ local prev = package.path
+ package.path = fio.pathjoin(out, '?.lua') .. ';'
+ .. fio.pathjoin(out, '?/init.lua') .. ';' .. prev
+ package.loaded[modname] = nil
+ local mod = require(modname)
+ package.path = prev
+
+ -- Populate every keyword-named field.
+ local input = { [ 'function' ] = { x = 42 } }
+ local n = 1
+ for _, kw in ipairs(LUA_KEYWORDS) do
+ if kw ~= 'function' then
+ input[kw] = n
+ n = n + 1
+ end
+ end
+
+ local bytes = mod.Outer_encode(input)
+ local decoded = mod.Outer_decode(bytes)
+ t.assert_equals(decoded['function'].x, 42)
+ for k, v in pairs(input) do
+ if k ~= 'function' then
+ t.assert_equals(decoded[k], v,
+ 'round-trip failed for keyword field "' .. k .. '"')
+ end
+ end
+ end
+end