diff --git a/cmd/protoc-gen-tarantool/internal/gen/emmylua.go b/cmd/protoc-gen-tarantool/internal/gen/emmylua.go index e18bb97665b2baff7618c4918874f8ab021f4f72..9e52b900f5b2b95af9a116e5815e0a97338c01db 100644 --- a/cmd/protoc-gen-tarantool/internal/gen/emmylua.go +++ b/cmd/protoc-gen-tarantool/internal/gen/emmylua.go @@ -119,20 +119,81 @@ // 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. +// 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 + "?" } - w.line("---@field %s %s", name, emmyFieldType(f)) + 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 / diff --git a/cmd/protoc-gen-tarantool/internal/gen/gen.go b/cmd/protoc-gen-tarantool/internal/gen/gen.go index f255c7e64426dc29ac79f84f6b3a110b989554eb..2e65a11ee6527cedcfb2ee25ab8ff3f43852c1a4 100644 --- a/cmd/protoc-gen-tarantool/internal/gen/gen.go +++ b/cmd/protoc-gen-tarantool/internal/gen/gen.go @@ -212,6 +212,7 @@ name := luaTypeName(e.Desc.FullName(), file.Desc.Package()) w.line("-- Enum: %s", e.Desc.FullName()) w.line("M.%s_descriptor = pb.enum(%q, {", name, string(e.Desc.FullName())) for _, v := range e.Values { + emitProtoDocIndented(w, v.Comments.Leading, " ") w.line(" %s = %d,", string(v.Desc.Name()), v.Desc.Number()) } w.line("})") diff --git a/cmd/protoc-gen-tarantool/internal/gen/service.go b/cmd/protoc-gen-tarantool/internal/gen/service.go index 90e7a8c9d067a4659439eb6737b393c3a1becd09..6e8934a32e6d18d9112d01353edc4c16132919d5 100644 --- a/cmd/protoc-gen-tarantool/internal/gen/service.go +++ b/cmd/protoc-gen-tarantool/internal/gen/service.go @@ -37,6 +37,7 @@ name := string(svc.Desc.Name()) fullName := string(svc.Desc.FullName()) selfPath := luaPackagePath(file.Desc, prefix) + emitProtoDoc(w, svc.Comments.Leading) w.line("-- Service: %s", fullName) w.line("M.%s_service = {", name) w.line(" name = %q,", fullName) @@ -44,6 +45,7 @@ w.line(" full_name = %q,", "/"+fullName) w.line(" methods = {") for _, m := range svc.Methods { mname := string(m.Desc.Name()) + emitProtoDocIndented(w, m.Comments.Leading, " ") w.line(" %s = {", mname) w.line(" name = %q,", mname) w.line(" full_name = %q,", "/"+fullName+"/"+mname) @@ -84,6 +86,7 @@ path := "/" + string(svc.Desc.FullName()) + "/" + mname inputEnc := typeRef(file, m.Input.Desc, selfPath, imports, "_encode", prefix) outputDec := typeRef(file, m.Output.Desc, selfPath, imports, "_decode", prefix) + emitProtoDocIndented(w, m.Comments.Leading, " ") switch classify(m) { case kindUnary: w.line(" %s = function(req, ctx)", mname) @@ -139,6 +142,7 @@ path := "/" + string(svc.Desc.FullName()) + "/" + mname inputDec := typeRef(file, m.Input.Desc, selfPath, imports, "_decode", prefix) outputEnc := typeRef(file, m.Output.Desc, selfPath, imports, "_encode", prefix) + emitProtoDocIndented(w, m.Comments.Leading, " ") w.line(" [%q] = function(req_bytes, ctx)", path) w.line(" local handler = impl.%s", mname) w.line(" if handler == nil then error(\"%s.%s: handler missing\", 0) end", name, mname) @@ -159,6 +163,7 @@ path := "/" + string(svc.Desc.FullName()) + "/" + mname inputDec := typeRef(file, m.Input.Desc, selfPath, imports, "_decode", prefix) outputEnc := typeRef(file, m.Output.Desc, selfPath, imports, "_encode", prefix) + emitProtoDocIndented(w, m.Comments.Leading, " ") switch kind { case kindServerStream: w.line(" [%q] = {", path) diff --git a/examples/expected/full/conformance/conformance_pb.lua b/examples/expected/full/conformance/conformance_pb.lua index b0986f5f9df42d297cd6b5a7cb29ebeb4c635f0f..82b1299bf4415dba1c929c79f98176670639ad2b 100644 --- a/examples/expected/full/conformance/conformance_pb.lua +++ b/examples/expected/full/conformance/conformance_pb.lua @@ -23,8 +23,17 @@ M.TestCategory_descriptor = pb.enum("conformance.TestCategory", { UNSPECIFIED_TEST = 0, BINARY_TEST = 1, JSON_TEST = 2, + -- Similar to JSON_TEST. However, during parsing json, testee should ignore + -- unknown fields. This feature is optional. Each implementation can decide + -- whether to support it. See + -- https://developers.google.com/protocol-buffers/docs/proto3#json_options + -- for more detail. JSON_IGNORE_UNKNOWN_PARSING_TEST = 3, + -- Test jspb wire format. Only used inside Google. Opensource testees just + -- skip it. JSPB_TEST = 4, + -- Test text format. For cpp, java and python, testees can already deal with + -- this type. Testees of other languages can simply skip it. TEXT_FORMAT_TEST = 5, }) M.TestCategory = M.TestCategory_descriptor.by_name @@ -96,38 +105,51 @@ -- autocomplete and type-checking for the generated wrappers. ---@alias conformance.WireFormat integer ---@alias conformance.TestCategory integer +--- Meant to encapsulate all types of tests: successes, skips, failures, etc. +--- Therefore, this may or may not have a failure message. Failure messages +--- may be truncated for our failure lists. ---@class conformance.TestStatus ---@field name string ---@field failure_message string ----@field matched_name string +---@field matched_name string @ What an actual test name matched to in a failure list. Can be wildcarded or an exact match without wildcards. +--- The conformance runner will request a list of failures as the first request. +--- This will be known by message_type == "conformance.FailureSet", a conformance +--- test should return a serialized FailureSet in protobuf_payload. ---@class conformance.FailureSet ---@field test conformance.TestStatus[] +--- Represents a single test case's input. The testee should: +--- +--- 1. parse this proto (which should always succeed) +--- 2. parse the protobuf or JSON payload in "payload" (which may fail) +--- 3. if the parse succeeded, serialize the message in the requested format. ---@class conformance.ConformanceRequest ---@field protobuf_payload? string ---@field json_payload? string ----@field jspb_payload? string +---@field jspb_payload? string @ Only used inside Google. Opensource testees just skip it. ---@field text_payload? string ----@field requested_output_format conformance.WireFormat ----@field message_type string ----@field test_category conformance.TestCategory ----@field jspb_encoding_options conformance.JspbEncodingConfig ----@field print_unknown_fields boolean +---@field requested_output_format conformance.WireFormat @ Which format should the testee serialize its message to? +---@field message_type string @ The full name for the test message to use; for the moment, either: protobuf_test_messages.proto3.TestAllTypesProto3 or protobuf_test_messages.proto2.TestAllTypesProto2 or protobuf_test_messages.editions.proto2.TestAllTypesProto2 or protobuf_test_messages.editions.proto3.TestAllTypesProto3 or protobuf_test_messages.editions.TestAllTypesEdition2023 or protobuf_test_messages.edition_unstable.TestAllTypesEditionUnstable. +---@field test_category conformance.TestCategory @ Each test is given a specific test category. Some category may need specific support in testee programs. Refer to the definition of TestCategory for more information. +---@field jspb_encoding_options conformance.JspbEncodingConfig @ Specify details for how to encode jspb. +---@field print_unknown_fields boolean @ This can be used in json and text format. If true, testee should print unknown fields instead of ignore. This feature is optional. +--- Represents a single test case's output. ---@class conformance.ConformanceResponse ----@field parse_error? string ----@field serialize_error? string ----@field timeout_error? string ----@field runtime_error? string ----@field protobuf_payload? string ----@field json_payload? string ----@field skipped? string ----@field jspb_payload? string ----@field text_payload? string +---@field parse_error? string @ This string should be set to indicate parsing failed. The string can provide more information about the parse error if it is available. Setting this string does not necessarily mean the testee failed the test. Some of the test cases are intentionally invalid input. +---@field serialize_error? string @ If the input was successfully parsed but errors occurred when serializing it to the requested output format, set the error message in this field. +---@field timeout_error? string @ This should be set if the test program timed out. The string should provide more information about what the child process was doing when it was killed. +---@field runtime_error? string @ This should be set if some other error occurred. This will always indicate that the test failed. The string can provide more information about the failure. +---@field protobuf_payload? string @ If the input was successfully parsed and the requested output was protobuf, serialize it to protobuf and set it in this field. +---@field json_payload? string @ If the input was successfully parsed and the requested output was JSON, serialize to JSON and set it in this field. +---@field skipped? string @ For when the testee skipped the test, likely because a certain feature wasn't supported, like JSON input/output. +---@field jspb_payload? string @ If the input was successfully parsed and the requested output was JSPB, serialize to JSPB and set it in this field. JSPB is only used inside Google. Opensource testees can just skip it. +---@field text_payload? string @ If the input was successfully parsed and the requested output was TEXT_FORMAT, serialize to TEXT_FORMAT and set it in this field. +--- Encoding options for jspb format. ---@class conformance.JspbEncodingConfig ----@field use_jspb_array_any_format boolean +---@field use_jspb_array_any_format boolean @ Encode the value field of Any as jspb array if true, otherwise binary. ---@param t? conformance.TestStatus ---@return conformance.TestStatus diff --git a/examples/expected/full/hello/hello_pb.lua b/examples/expected/full/hello/hello_pb.lua index 84a726663930122415876dff565193bd776fcc7f..9ad63278ef5604a01450b1c322d3e3339b75fff3 100644 --- a/examples/expected/full/hello/hello_pb.lua +++ b/examples/expected/full/hello/hello_pb.lua @@ -98,18 +98,22 @@ -- These are comments — no runtime effect. They give editors -- autocomplete and type-checking for the generated wrappers. ---@alias hello.Status integer +--- Demo message for oneof handling. ---@class hello.Result ---@field id integer ---@field text? string ---@field code? integer ---@field details? hello.Address +--- gRPC service demo. Covers unary + all three streaming flavors so the +--- loopback transport exercises every codegen branch. ---@class hello.HelloRequest ---@field name string ---@class hello.HelloReply ---@field greeting string +--- Demo message exercising well-known types (M3). ---@class hello.Event ---@field title string ---@field created_at google.protobuf.Timestamp @@ -128,7 +132,7 @@ ---@class hello.Address ---@field street string ---@field city string ---@field zip integer ----@field apartment? string +---@field apartment? string @ Explicit-optional: presence is meaningful (distinct from default). ---@class hello.Person ---@field name string @@ -142,7 +146,7 @@ ---@field avatar string ---@field user_id integer ---@field balance integer ---@field weight_kg number ----@field ages_by_nickname table +---@field ages_by_nickname table @ Map fields (M2) ---@field nickname_by_age table ---@field addresses_by_label table @@ -994,6 +998,7 @@ full_name = "/hello.Greeter/Echo", input = M.HelloRequest_descriptor, output = M.HelloRequest_descriptor, }, + -- Server-streaming: one request, server pushes N replies. StreamHellos = { name = "StreamHellos", full_name = "/hello.Greeter/StreamHellos", @@ -1001,6 +1006,7 @@ input = M.HelloRequest_descriptor, output = M.HelloReply_descriptor, server_streaming = true, }, + -- Client-streaming: client pushes N requests, server returns one reply. CollectHellos = { name = "CollectHellos", full_name = "/hello.Greeter/CollectHellos", @@ -1008,6 +1014,7 @@ input = M.HelloRequest_descriptor, output = M.HelloReply_descriptor, client_streaming = true, }, + -- Bidirectional: both sides push and pull independently. Chat = { name = "Chat", full_name = "/hello.Greeter/Chat", @@ -1032,15 +1039,18 @@ local req_bytes = M.HelloRequest_encode(req) local resp_bytes = transport:unary("/hello.Greeter/Echo", req_bytes, ctx) return M.HelloRequest_decode(resp_bytes) end, + -- Server-streaming: one request, server pushes N replies. StreamHellos = function(req, ctx) local req_bytes = M.HelloRequest_encode(req) local raw = transport:server_stream("/hello.Greeter/StreamHellos", req_bytes, ctx) return pb.grpc.wrap_server_stream(raw, M.HelloReply_decode) end, + -- Client-streaming: client pushes N requests, server returns one reply. CollectHellos = function(ctx) local raw = transport:client_stream("/hello.Greeter/CollectHellos", ctx) return pb.grpc.wrap_call(raw, M.HelloRequest_encode, M.HelloReply_decode) end, + -- Bidirectional: both sides push and pull independently. Chat = function(ctx) local raw = transport:bidi("/hello.Greeter/Chat", ctx) return pb.grpc.wrap_call(raw, M.HelloRequest_encode, M.HelloReply_decode) @@ -1069,6 +1079,7 @@ return M.HelloRequest_encode(resp) end, }, streams = { + -- Server-streaming: one request, server pushes N replies. ["/hello.Greeter/StreamHellos"] = { kind = 'server_stream', handler = function(req_bytes, server_view, ctx) @@ -1079,6 +1090,7 @@ local wrapped = pb.grpc.wrap_server_view(server_view, nil, M.HelloReply_encode) handler(req, wrapped, ctx) end, }, + -- Client-streaming: client pushes N requests, server returns one reply. ["/hello.Greeter/CollectHellos"] = { kind = 'client_stream', handler = function(_, server_view, ctx) @@ -1090,6 +1102,7 @@ if resp == nil then error("Greeter.CollectHellos: handler returned nil response", 0) end server_view:send(M.HelloReply_encode(resp)) end, }, + -- Bidirectional: both sides push and pull independently. ["/hello.Greeter/Chat"] = { kind = 'bidi', handler = function(_, server_view, ctx) diff --git a/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua index 55278963b5920baaa5021cc5200065cd5e2c3c88..6533d682534de5e9ef975c88b6e7863352c370c8 100644 --- a/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +++ b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua @@ -245,8 +245,15 @@ ---@alias protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum integer ---@alias protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum integer ---@alias protobuf_test_messages.proto3.EnumOnlyProto3.Bool integer +--- This proto includes every type of field in both singular and repeated +--- forms. +--- +--- Also, crucially, all messages and enums in this file are eventually +--- submessages of this message. So for example, a fuzz test of TestAllTypes +--- could trigger bugs that occur in any message type in this file. We verify +--- this stays true in a unit test. ---@class protobuf_test_messages.proto3.TestAllTypesProto3 ----@field optional_int32 integer +---@field optional_int32 integer @ Singular test [kotlin] comment ---@field optional_int64 integer ---@field optional_uint32 integer ---@field optional_uint64 integer @@ -269,7 +276,7 @@ ---@field optional_aliased_enum protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum ---@field optional_string_piece string ---@field optional_cord string ---@field recursive_message protobuf_test_messages.proto3.TestAllTypesProto3 ----@field repeated_int32 integer[] +---@field repeated_int32 integer[] @ Repeated ---@field repeated_int64 integer[] ---@field repeated_uint32 integer[] ---@field repeated_uint64 integer[] @@ -290,7 +297,7 @@ ---@field repeated_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[] ---@field repeated_foreign_enum protobuf_test_messages.proto3.ForeignEnum[] ---@field repeated_string_piece string[] ---@field repeated_cord string[] ----@field packed_int32 integer[] +---@field packed_int32 integer[] @ Packed ---@field packed_int64 integer[] ---@field packed_uint32 integer[] ---@field packed_uint64 integer[] @@ -304,7 +311,7 @@ ---@field packed_float number[] ---@field packed_double number[] ---@field packed_bool boolean[] ---@field packed_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[] ----@field unpacked_int32 integer[] +---@field unpacked_int32 integer[] @ Unpacked ---@field unpacked_int64 integer[] ---@field unpacked_uint32 integer[] ---@field unpacked_uint64 integer[] @@ -318,7 +325,7 @@ ---@field unpacked_float number[] ---@field unpacked_double number[] ---@field unpacked_bool boolean[] ---@field unpacked_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[] ----@field map_int32_int32 table +---@field map_int32_int32 table @ Map ---@field map_int64_int64 table ---@field map_uint32_uint32 table ---@field map_uint64_uint64 table @@ -347,7 +354,7 @@ ---@field oneof_float? number ---@field oneof_double? number ---@field oneof_enum? protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum ---@field oneof_null_value? google.protobuf.NullValue ----@field optional_bool_wrapper google.protobuf.BoolValue +---@field optional_bool_wrapper google.protobuf.BoolValue @ Well-known types ---@field optional_int32_wrapper google.protobuf.Int32Value ---@field optional_int64_wrapper google.protobuf.Int64Value ---@field optional_uint32_wrapper google.protobuf.UInt32Value @@ -381,7 +388,7 @@ ---@field repeated_any google.protobuf.Any[] ---@field repeated_value google.protobuf.Value[] ---@field repeated_list_value google.protobuf.ListValue[] ---@field repeated_empty google.protobuf.Empty[] ----@field fieldname1 integer +---@field fieldname1 integer @ Test field-name-to-JSON-name convention. (protobuf says names can be any valid C/C++ identifier.) ---@field field_name2 integer ---@field _field_name3 integer ---@field field__name4_ integer diff --git a/examples/expected/runtime/conformance/conformance_pb.lua b/examples/expected/runtime/conformance/conformance_pb.lua index 57ab173d6885f72e4ca08d3f5e6c9de7b0c73bad..52cd1ec98c91d926ea6358f1484f1d12e4d1e5c5 100644 --- a/examples/expected/runtime/conformance/conformance_pb.lua +++ b/examples/expected/runtime/conformance/conformance_pb.lua @@ -23,8 +23,17 @@ M.TestCategory_descriptor = pb.enum("conformance.TestCategory", { UNSPECIFIED_TEST = 0, BINARY_TEST = 1, JSON_TEST = 2, + -- Similar to JSON_TEST. However, during parsing json, testee should ignore + -- unknown fields. This feature is optional. Each implementation can decide + -- whether to support it. See + -- https://developers.google.com/protocol-buffers/docs/proto3#json_options + -- for more detail. JSON_IGNORE_UNKNOWN_PARSING_TEST = 3, + -- Test jspb wire format. Only used inside Google. Opensource testees just + -- skip it. JSPB_TEST = 4, + -- Test text format. For cpp, java and python, testees can already deal with + -- this type. Testees of other languages can simply skip it. TEXT_FORMAT_TEST = 5, }) M.TestCategory = M.TestCategory_descriptor.by_name @@ -96,38 +105,51 @@ -- autocomplete and type-checking for the generated wrappers. ---@alias conformance.WireFormat integer ---@alias conformance.TestCategory integer +--- Meant to encapsulate all types of tests: successes, skips, failures, etc. +--- Therefore, this may or may not have a failure message. Failure messages +--- may be truncated for our failure lists. ---@class conformance.TestStatus ---@field name string ---@field failure_message string ----@field matched_name string +---@field matched_name string @ What an actual test name matched to in a failure list. Can be wildcarded or an exact match without wildcards. +--- The conformance runner will request a list of failures as the first request. +--- This will be known by message_type == "conformance.FailureSet", a conformance +--- test should return a serialized FailureSet in protobuf_payload. ---@class conformance.FailureSet ---@field test conformance.TestStatus[] +--- Represents a single test case's input. The testee should: +--- +--- 1. parse this proto (which should always succeed) +--- 2. parse the protobuf or JSON payload in "payload" (which may fail) +--- 3. if the parse succeeded, serialize the message in the requested format. ---@class conformance.ConformanceRequest ---@field protobuf_payload? string ---@field json_payload? string ----@field jspb_payload? string +---@field jspb_payload? string @ Only used inside Google. Opensource testees just skip it. ---@field text_payload? string ----@field requested_output_format conformance.WireFormat ----@field message_type string ----@field test_category conformance.TestCategory ----@field jspb_encoding_options conformance.JspbEncodingConfig ----@field print_unknown_fields boolean +---@field requested_output_format conformance.WireFormat @ Which format should the testee serialize its message to? +---@field message_type string @ The full name for the test message to use; for the moment, either: protobuf_test_messages.proto3.TestAllTypesProto3 or protobuf_test_messages.proto2.TestAllTypesProto2 or protobuf_test_messages.editions.proto2.TestAllTypesProto2 or protobuf_test_messages.editions.proto3.TestAllTypesProto3 or protobuf_test_messages.editions.TestAllTypesEdition2023 or protobuf_test_messages.edition_unstable.TestAllTypesEditionUnstable. +---@field test_category conformance.TestCategory @ Each test is given a specific test category. Some category may need specific support in testee programs. Refer to the definition of TestCategory for more information. +---@field jspb_encoding_options conformance.JspbEncodingConfig @ Specify details for how to encode jspb. +---@field print_unknown_fields boolean @ This can be used in json and text format. If true, testee should print unknown fields instead of ignore. This feature is optional. +--- Represents a single test case's output. ---@class conformance.ConformanceResponse ----@field parse_error? string ----@field serialize_error? string ----@field timeout_error? string ----@field runtime_error? string ----@field protobuf_payload? string ----@field json_payload? string ----@field skipped? string ----@field jspb_payload? string ----@field text_payload? string +---@field parse_error? string @ This string should be set to indicate parsing failed. The string can provide more information about the parse error if it is available. Setting this string does not necessarily mean the testee failed the test. Some of the test cases are intentionally invalid input. +---@field serialize_error? string @ If the input was successfully parsed but errors occurred when serializing it to the requested output format, set the error message in this field. +---@field timeout_error? string @ This should be set if the test program timed out. The string should provide more information about what the child process was doing when it was killed. +---@field runtime_error? string @ This should be set if some other error occurred. This will always indicate that the test failed. The string can provide more information about the failure. +---@field protobuf_payload? string @ If the input was successfully parsed and the requested output was protobuf, serialize it to protobuf and set it in this field. +---@field json_payload? string @ If the input was successfully parsed and the requested output was JSON, serialize to JSON and set it in this field. +---@field skipped? string @ For when the testee skipped the test, likely because a certain feature wasn't supported, like JSON input/output. +---@field jspb_payload? string @ If the input was successfully parsed and the requested output was JSPB, serialize to JSPB and set it in this field. JSPB is only used inside Google. Opensource testees can just skip it. +---@field text_payload? string @ If the input was successfully parsed and the requested output was TEXT_FORMAT, serialize to TEXT_FORMAT and set it in this field. +--- Encoding options for jspb format. ---@class conformance.JspbEncodingConfig ----@field use_jspb_array_any_format boolean +---@field use_jspb_array_any_format boolean @ Encode the value field of Any as jspb array if true, otherwise binary. ---@param t? conformance.TestStatus ---@return conformance.TestStatus diff --git a/examples/expected/runtime/hello/hello_pb.lua b/examples/expected/runtime/hello/hello_pb.lua index 4f4526ea41bca396ddedf5290f0d88893c8bde75..6f9cda5b474ff7c78d7e69f94a59522f298d23e6 100644 --- a/examples/expected/runtime/hello/hello_pb.lua +++ b/examples/expected/runtime/hello/hello_pb.lua @@ -98,18 +98,22 @@ -- These are comments — no runtime effect. They give editors -- autocomplete and type-checking for the generated wrappers. ---@alias hello.Status integer +--- Demo message for oneof handling. ---@class hello.Result ---@field id integer ---@field text? string ---@field code? integer ---@field details? hello.Address +--- gRPC service demo. Covers unary + all three streaming flavors so the +--- loopback transport exercises every codegen branch. ---@class hello.HelloRequest ---@field name string ---@class hello.HelloReply ---@field greeting string +--- Demo message exercising well-known types (M3). ---@class hello.Event ---@field title string ---@field created_at google.protobuf.Timestamp @@ -128,7 +132,7 @@ ---@class hello.Address ---@field street string ---@field city string ---@field zip integer ----@field apartment? string +---@field apartment? string @ Explicit-optional: presence is meaningful (distinct from default). ---@class hello.Person ---@field name string @@ -142,7 +146,7 @@ ---@field avatar string ---@field user_id integer ---@field balance integer ---@field weight_kg number ----@field ages_by_nickname table +---@field ages_by_nickname table @ Map fields (M2) ---@field nickname_by_age table ---@field addresses_by_label table @@ -270,6 +274,7 @@ full_name = "/hello.Greeter/Echo", input = M.HelloRequest_descriptor, output = M.HelloRequest_descriptor, }, + -- Server-streaming: one request, server pushes N replies. StreamHellos = { name = "StreamHellos", full_name = "/hello.Greeter/StreamHellos", @@ -277,6 +282,7 @@ input = M.HelloRequest_descriptor, output = M.HelloReply_descriptor, server_streaming = true, }, + -- Client-streaming: client pushes N requests, server returns one reply. CollectHellos = { name = "CollectHellos", full_name = "/hello.Greeter/CollectHellos", @@ -284,6 +290,7 @@ input = M.HelloRequest_descriptor, output = M.HelloReply_descriptor, client_streaming = true, }, + -- Bidirectional: both sides push and pull independently. Chat = { name = "Chat", full_name = "/hello.Greeter/Chat", @@ -308,15 +315,18 @@ local req_bytes = M.HelloRequest_encode(req) local resp_bytes = transport:unary("/hello.Greeter/Echo", req_bytes, ctx) return M.HelloRequest_decode(resp_bytes) end, + -- Server-streaming: one request, server pushes N replies. StreamHellos = function(req, ctx) local req_bytes = M.HelloRequest_encode(req) local raw = transport:server_stream("/hello.Greeter/StreamHellos", req_bytes, ctx) return pb.grpc.wrap_server_stream(raw, M.HelloReply_decode) end, + -- Client-streaming: client pushes N requests, server returns one reply. CollectHellos = function(ctx) local raw = transport:client_stream("/hello.Greeter/CollectHellos", ctx) return pb.grpc.wrap_call(raw, M.HelloRequest_encode, M.HelloReply_decode) end, + -- Bidirectional: both sides push and pull independently. Chat = function(ctx) local raw = transport:bidi("/hello.Greeter/Chat", ctx) return pb.grpc.wrap_call(raw, M.HelloRequest_encode, M.HelloReply_decode) @@ -345,6 +355,7 @@ return M.HelloRequest_encode(resp) end, }, streams = { + -- Server-streaming: one request, server pushes N replies. ["/hello.Greeter/StreamHellos"] = { kind = 'server_stream', handler = function(req_bytes, server_view, ctx) @@ -355,6 +366,7 @@ local wrapped = pb.grpc.wrap_server_view(server_view, nil, M.HelloReply_encode) handler(req, wrapped, ctx) end, }, + -- Client-streaming: client pushes N requests, server returns one reply. ["/hello.Greeter/CollectHellos"] = { kind = 'client_stream', handler = function(_, server_view, ctx) @@ -366,6 +378,7 @@ if resp == nil then error("Greeter.CollectHellos: handler returned nil response", 0) end server_view:send(M.HelloReply_encode(resp)) end, }, + -- Bidirectional: both sides push and pull independently. ["/hello.Greeter/Chat"] = { kind = 'bidi', handler = function(_, server_view, ctx) diff --git a/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua b/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua index 58495abf0c51d29de9cca4233148b0aeef8271fa..124f73970230453745010d49f9945ba9827ee4e9 100644 --- a/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +++ b/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua @@ -245,8 +245,15 @@ ---@alias protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum integer ---@alias protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum integer ---@alias protobuf_test_messages.proto3.EnumOnlyProto3.Bool integer +--- This proto includes every type of field in both singular and repeated +--- forms. +--- +--- Also, crucially, all messages and enums in this file are eventually +--- submessages of this message. So for example, a fuzz test of TestAllTypes +--- could trigger bugs that occur in any message type in this file. We verify +--- this stays true in a unit test. ---@class protobuf_test_messages.proto3.TestAllTypesProto3 ----@field optional_int32 integer +---@field optional_int32 integer @ Singular test [kotlin] comment ---@field optional_int64 integer ---@field optional_uint32 integer ---@field optional_uint64 integer @@ -269,7 +276,7 @@ ---@field optional_aliased_enum protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum ---@field optional_string_piece string ---@field optional_cord string ---@field recursive_message protobuf_test_messages.proto3.TestAllTypesProto3 ----@field repeated_int32 integer[] +---@field repeated_int32 integer[] @ Repeated ---@field repeated_int64 integer[] ---@field repeated_uint32 integer[] ---@field repeated_uint64 integer[] @@ -290,7 +297,7 @@ ---@field repeated_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[] ---@field repeated_foreign_enum protobuf_test_messages.proto3.ForeignEnum[] ---@field repeated_string_piece string[] ---@field repeated_cord string[] ----@field packed_int32 integer[] +---@field packed_int32 integer[] @ Packed ---@field packed_int64 integer[] ---@field packed_uint32 integer[] ---@field packed_uint64 integer[] @@ -304,7 +311,7 @@ ---@field packed_float number[] ---@field packed_double number[] ---@field packed_bool boolean[] ---@field packed_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[] ----@field unpacked_int32 integer[] +---@field unpacked_int32 integer[] @ Unpacked ---@field unpacked_int64 integer[] ---@field unpacked_uint32 integer[] ---@field unpacked_uint64 integer[] @@ -318,7 +325,7 @@ ---@field unpacked_float number[] ---@field unpacked_double number[] ---@field unpacked_bool boolean[] ---@field unpacked_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[] ----@field map_int32_int32 table +---@field map_int32_int32 table @ Map ---@field map_int64_int64 table ---@field map_uint32_uint32 table ---@field map_uint64_uint64 table @@ -347,7 +354,7 @@ ---@field oneof_float? number ---@field oneof_double? number ---@field oneof_enum? protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum ---@field oneof_null_value? google.protobuf.NullValue ----@field optional_bool_wrapper google.protobuf.BoolValue +---@field optional_bool_wrapper google.protobuf.BoolValue @ Well-known types ---@field optional_int32_wrapper google.protobuf.Int32Value ---@field optional_int64_wrapper google.protobuf.Int64Value ---@field optional_uint32_wrapper google.protobuf.UInt32Value @@ -381,7 +388,7 @@ ---@field repeated_any google.protobuf.Any[] ---@field repeated_value google.protobuf.Value[] ---@field repeated_list_value google.protobuf.ListValue[] ---@field repeated_empty google.protobuf.Empty[] ----@field fieldname1 integer +---@field fieldname1 integer @ Test field-name-to-JSON-name convention. (protobuf says names can be any valid C/C++ identifier.) ---@field field_name2 integer ---@field _field_name3 integer ---@field field__name4_ integer diff --git a/test/codegen_doc_comments_test.lua b/test/codegen_doc_comments_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..2811a2f8082a8c2cf5769f7f86e6af970270d559 --- /dev/null +++ b/test/codegen_doc_comments_test.lua @@ -0,0 +1,230 @@ +-- Verifies that leading `//` comments in a .proto file propagate into +-- the generated _pb.lua module as LuaLS-friendly `---` doc lines and +-- as plain `--` lines inside table literals where LuaLS attachment +-- doesn't apply. +-- +-- Strategy mirrors test/doc_test.lua: write a small fixture proto into a +-- tempdir, invoke the plugin via protoc, then grep the output. We run +-- the plugin in both `full` and `runtime` modes — the doc emission is +-- mode-independent, but covering both prevents a regression that wires +-- comments into only one path. + +local t = require('luatest') +local fio = require('fio') + +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 slurp(path) + local f = assert(io.open(path, 'rb')) + local s = f:read('*a') + f:close() + return s +end + +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 + +-- A purpose-built proto exercising every comment-bearing surface: +-- enum, enum value, message, field, oneof field, repeated field, +-- map field, service, RPC method (unary + streaming flavors). +local FIXTURE_PROTO = [[ +syntax = "proto3"; + +package docs; + +// Status describes the outcome of an operation. +// Multi-line description for hover docs. +enum Status { + // Sentinel zero value. + UNKNOWN = 0; + // Operation succeeded normally. + OK = 1; + // Operation failed; see Result.code for detail. + ERROR = 2; +} + +// Result is the outcome envelope returned from every RPC. +message Result { + // Unique identifier for this result row. + int32 id = 1; + // Effective status of the operation. + Status status = 2; + // Either a human-readable text or a numeric code, never both. + oneof outcome { + // Free-form success text. + string text = 3; + // Machine-readable error code. + int32 code = 4; + } + // Labels collected during processing. + repeated string labels = 5; + // Per-key counters captured by the handler. + map counters = 6; +} + +message Probe { + string id = 1; +} + +// Pinger drives a single liveness probe and an open-ended stream. +service Pinger { + // Ping issues a single round-trip probe. + rpc Ping(Probe) returns (Result); + // Watch streams results until the client cancels. + rpc Watch(Probe) returns (stream Result); +} +]] + +local function generate(mode, prefix) + 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, 'comments.proto'), FIXTURE_PROTO) + + local cmd = string.format( + 'protoc --plugin=%q --tarantool_out=%q --tarantool_opt=%s -I %q -I %q %q', + PLUGIN, out_dir, + string.format('mode=%s,prefix=%s', mode, prefix), + proto_dir, OPTIONS_DIR, + fio.pathjoin(proto_dir, 'comments.proto')) + local ok = os.execute(cmd) + assert(ok == 0 or ok == true, 'plugin failed: ' .. cmd) + + return slurp(fio.pathjoin(out_dir, prefix, 'docs', 'comments_pb.lua')) +end + +ensure_plugin() +local LUA_FULL = generate('full', 'full') +local LUA_RUNTIME = generate('runtime', 'runtime') + +-- Each modes-parameterized group runs the same assertions against both +-- modes, matching the pattern used elsewhere in the suite. +local function make_group(name, lua) + local g = t.group('codegen_doc_comments.' .. name) + + g.test_message_leading_comment = function() + -- The message description should appear as a `---` block + -- immediately above its `---@class` declaration so LuaLS picks + -- it up as the type's hover docstring. + t.assert_str_contains(lua, + '--- Result is the outcome envelope returned from every RPC.\n' + .. '---@class docs.Result') + end + + g.test_enum_leading_comment_multiline = function() + -- Multi-line proto comments should produce one `--- line` per + -- source line, preserving the user's wording in order. + t.assert_str_contains(lua, + '--- Status describes the outcome of an operation.\n' + .. '--- Multi-line description for hover docs.\n' + .. '---@alias docs.Status') + end + + g.test_field_trailing_description = function() + -- Field comments collapse to a single trailing `@ description` + -- on the `---@field` line so LuaLS shows them on hover/complete. + t.assert_str_contains(lua, + '---@field id integer @ Unique identifier for this result row.') + t.assert_str_contains(lua, + '---@field status docs.Status @ Effective status of the operation.') + t.assert_str_contains(lua, + '---@field labels string[] @ Labels collected during processing.') + t.assert_str_contains(lua, + '---@field counters table ' + .. '@ Per-key counters captured by the handler.') + end + + g.test_oneof_field_descriptions = function() + -- Oneof branches are marked optional and should still carry the + -- per-field description. + t.assert_str_contains(lua, + '---@field text? string @ Free-form success text.') + t.assert_str_contains(lua, + '---@field code? integer @ Machine-readable error code.') + end + + g.test_enum_value_inline_comment = function() + -- Inside the `pb.enum(...)` table, value comments survive as + -- regular Lua comments — they're not LSP-attachable but readers + -- of the generated module should still see them. + t.assert_str_contains(lua, + ' -- Sentinel zero value.\n' + .. ' UNKNOWN = 0,') + t.assert_str_contains(lua, + ' -- Operation succeeded normally.\n' + .. ' OK = 1,') + t.assert_str_contains(lua, + ' -- Operation failed; see Result.code for detail.\n' + .. ' ERROR = 2,') + end + + g.test_service_leading_comment = function() + -- Service-level comment lands as a `---` block above the + -- `-- Service:` banner so LuaLS can attach it to the descriptor. + t.assert_str_contains(lua, + '--- Pinger drives a single liveness probe and an open-ended stream.\n' + .. '-- Service: docs.Pinger\n' + .. 'M.Pinger_service') + end + + g.test_method_descriptor_comment = function() + -- Each method entry inside `methods = { ... }` gets its leading + -- comment indented to match the table position. + t.assert_str_contains(lua, + ' -- Ping issues a single round-trip probe.\n' + .. ' Ping = {') + t.assert_str_contains(lua, + ' -- Watch streams results until the client cancels.\n' + .. ' Watch = {') + end + + g.test_method_client_function_comment = function() + -- Client functions also carry the per-method comment so users + -- reading the client stubs see the proto context inline. + t.assert_str_contains(lua, + ' -- Ping issues a single round-trip probe.\n' + .. ' Ping = function(req, ctx)') + t.assert_str_contains(lua, + ' -- Watch streams results until the client cancels.\n' + .. ' Watch = function(req, ctx)') + end + + g.test_method_server_handler_comment = function() + -- Server handlers get the same treatment — the unary handler is + -- in `methods`, streaming handlers are in `streams`. + t.assert_str_contains(lua, + ' -- Ping issues a single round-trip probe.\n' + .. ' ["/docs.Pinger/Ping"]') + t.assert_str_contains(lua, + ' -- Watch streams results until the client cancels.\n' + .. ' ["/docs.Pinger/Watch"]') + end + + g.test_no_comment_means_no_doc_line = function() + -- The `Probe` message has no leading comment. Make sure we + -- haven't started inventing one — its class declaration should + -- be preceded only by a blank line, not a stray `---`. + t.assert_str_contains(lua, '\n---@class docs.Probe\n') + -- And the single field has no description suffix. + t.assert_str_contains(lua, '\n---@field id string\n') + end +end + +make_group('full', LUA_FULL) +make_group('runtime', LUA_RUNTIME)