From 06b2978a0f71c67829bbb516ecec6edf32f78667 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 17 May 2026 06:36:06 +0300 Subject: [PATCH] codegen: resolve (tarantool.lua_package) via global type registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom file option was being dropped silently: protoc encoded it correctly into the FileDescriptorProto, but protobuf-go parked the unknown extension in the message's unknown-fields tail because E_LuaPackage was never registered with protoregistry.GlobalTypes. As a result proto.GetExtension returned "" and every override the README documented was a no-op — every caller fell through to the default "._pb" path resolution. Register E_LuaPackage in init() and add a luatest regression that asserts both the output path and the cross-file require strings honor the option, parameterized over both codegen modes. --- .../internal/gen/options.go | 11 ++ test/codegen_lua_package_test.lua | 108 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 test/codegen_lua_package_test.lua diff --git a/cmd/protoc-gen-tarantool/internal/gen/options.go b/cmd/protoc-gen-tarantool/internal/gen/options.go index e1ff83a634f6e394639e77d65ba25e9d540810a4..bf5c8354e18a2736c05b43862b32f3e5e2c0d910 100644 --- a/cmd/protoc-gen-tarantool/internal/gen/options.go +++ b/cmd/protoc-gen-tarantool/internal/gen/options.go @@ -3,6 +3,7 @@ package gen import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/runtime/protoimpl" "google.golang.org/protobuf/types/descriptorpb" ) @@ -19,6 +20,16 @@ var E_LuaPackage = &protoimpl.ExtensionInfo{ Filename: "tarantool/tarantool.proto", } +// Register the extension in the global type registry so that +// proto.GetExtension can resolve it on FileOptions parsed by protoc-gen-go's +// plugin scaffolding. Without this, the option's value lands in the message's +// unknown fields and GetExtension returns the zero value (""). +func init() { + if err := protoregistry.GlobalTypes.RegisterExtension(E_LuaPackage); err != nil { + panic("tarantool-protobuf: register E_LuaPackage: " + err.Error()) + } +} + // luaPackageOption returns the value of (tarantool.lua_package) on the file // options, or "" when unset. func luaPackageOption(f protoreflect.FileDescriptor) string { diff --git a/test/codegen_lua_package_test.lua b/test/codegen_lua_package_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..64075423bf34fd4bac3a4b4118e8d3714c9b2ce2 --- /dev/null +++ b/test/codegen_lua_package_test.lua @@ -0,0 +1,108 @@ +-- Regression test for (tarantool.lua_package) file option. +-- +-- The option lives on FileOptions as extension field 60001 and overrides +-- the default `._pb` Lua require path. The plugin parses the +-- extension via protoreflect/protoregistry, so this test guards against +-- the option silently becoming a no-op when the extension is not +-- registered in the global type registry (the failure mode is that +-- proto.GetExtension returns "" and the override is lost). +-- +-- Coverage: +-- * The on-disk output path follows `lua_package`, not `/_pb`. +-- * Cross-file `import "other.proto"` rewrites to `require("")`. + +local t = require('luatest') +local fio = require('fio') + +local g = t.group('codegen_lua_package') + +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 function spit(path, content) + local f = assert(io.open(path, 'wb')) + f:write(content) + f:close() +end + +local function slurp(path) + local f = assert(io.open(path, 'rb')) + local s = f:read('*a') + f:close() + return s +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 + +local SAMPLE_PROTO = [[ +syntax = "proto3"; +package sample; +import "tarantool/tarantool.proto"; +option (tarantool.lua_package) = "myapp.proto.sample"; +message Foo { int32 x = 1; } +]] + +local OTHER_PROTO = [[ +syntax = "proto3"; +package other; +import "tarantool/tarantool.proto"; +import "sample.proto"; +option (tarantool.lua_package) = "myapp.proto.other"; +message Bar { sample.Foo foo = 1; } +]] + +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, 'sample.proto'), SAMPLE_PROTO) + spit(fio.pathjoin(proto_dir, 'other.proto'), OTHER_PROTO) + + local cmd = string.format( + 'protoc --plugin=%q --tarantool_out=%q --tarantool_opt=mode=%s ' + ..'-I %q -I %q %q %q', + PLUGIN, out_dir, mode, proto_dir, OPTIONS_DIR, + fio.pathjoin(proto_dir, 'sample.proto'), + fio.pathjoin(proto_dir, 'other.proto')) + local ok = os.execute(cmd) + assert(ok == 0 or ok == true, 'plugin failed: ' .. cmd) + return out_dir +end + +g.before_all(function() + ensure_plugin() +end) + +for _, mode in ipairs({'full', 'runtime'}) do + g['test_output_path_honors_lua_package_'..mode] = function() + local out = run_plugin(mode) + t.assert(fio.path.exists(fio.pathjoin(out, 'myapp', 'proto', 'sample.lua')), + 'expected sample.lua under lua_package path, got: ' + .. table.concat(fio.glob(fio.pathjoin(out, '**', '*.lua')) or {}, ', ')) + t.assert(fio.path.exists(fio.pathjoin(out, 'myapp', 'proto', 'other.lua')), + 'expected other.lua under lua_package path') + -- Default `/_pb.lua` layout must NOT be present — + -- that's the regression signature (extension dropped, fallback used). + t.assert_not(fio.path.exists(fio.pathjoin(out, 'sample', 'sample_pb.lua')), + 'fallback path was emitted — lua_package option was ignored') + end + + g['test_cross_file_require_honors_lua_package_'..mode] = function() + local out = run_plugin(mode) + local body = slurp(fio.pathjoin(out, 'myapp', 'proto', 'other.lua')) + t.assert_str_contains(body, 'require("myapp.proto.sample")', false, + 'cross-file import should resolve via lua_package, not the default path') + t.assert_not(string.find(body, 'require("sample.sample_pb")', 1, true), + 'default require path leaked into cross-file import') + end +end