-
Notifications
You must be signed in to change notification settings - Fork 791
feat(generator): add force_int64_string option to prevent int64 preci… #1510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,6 @@ | |
#include <string> | ||
|
||
using google::protobuf::Descriptor; | ||
using google::protobuf::Edition; | ||
using google::protobuf::EnumDescriptor; | ||
using google::protobuf::FieldDescriptor; | ||
using google::protobuf::FileDescriptor; | ||
|
@@ -298,7 +297,7 @@ string JSMessageType(const Descriptor* desc) { | |
return JSMessageType(desc, nullptr); | ||
} | ||
|
||
string JSElementType(const FieldDescriptor* desc, const FileDescriptor* file) { | ||
string JSElementType(const FieldDescriptor* desc, const FileDescriptor* file, bool force_int64_string = false) { | ||
switch (desc->type()) { | ||
case FieldDescriptor::TYPE_DOUBLE: | ||
case FieldDescriptor::TYPE_FLOAT: | ||
|
@@ -314,7 +313,7 @@ string JSElementType(const FieldDescriptor* desc, const FileDescriptor* file) { | |
case FieldDescriptor::TYPE_SINT64: | ||
case FieldDescriptor::TYPE_FIXED64: | ||
case FieldDescriptor::TYPE_SFIXED64: | ||
if (desc->options().jstype() == FieldOptions::JS_STRING) { | ||
if (force_int64_string || desc->options().jstype() == FieldOptions::JS_STRING) { | ||
return "string"; | ||
} else { | ||
return "number"; | ||
|
@@ -348,11 +347,11 @@ string JSElementType(const FieldDescriptor* desc, const FileDescriptor* file) { | |
} | ||
} | ||
|
||
string JSFieldType(const FieldDescriptor* desc, const FileDescriptor* file) { | ||
string js_field_type = JSElementType(desc, file); | ||
string JSFieldType(const FieldDescriptor* desc, const FileDescriptor* file, bool force_int64_string = false) { | ||
string js_field_type = JSElementType(desc, file, force_int64_string); | ||
if (desc->is_map()) { | ||
string key_type = JSFieldType(desc->message_type()->field(0), file); | ||
string value_type = JSFieldType(desc->message_type()->field(1), file); | ||
string key_type = JSFieldType(desc->message_type()->field(0), file, force_int64_string); | ||
string value_type = JSFieldType(desc->message_type()->field(1), file, force_int64_string); | ||
return "jspb.Map<" + key_type + ", " + value_type + ">"; | ||
} | ||
if (desc->is_repeated()) { | ||
|
@@ -362,14 +361,14 @@ string JSFieldType(const FieldDescriptor* desc, const FileDescriptor* file) { | |
} | ||
|
||
string AsObjectFieldType(const FieldDescriptor* desc, | ||
const FileDescriptor* file) { | ||
const FileDescriptor* file, bool force_int64_string = false) { | ||
if (desc->type() != FieldDescriptor::TYPE_MESSAGE) { | ||
return JSFieldType(desc, file); | ||
return JSFieldType(desc, file, force_int64_string); | ||
} | ||
if (desc->is_map()) { | ||
const Descriptor* message = desc->message_type(); | ||
string key_type = AsObjectFieldType(message->field(0), file); | ||
string value_type = AsObjectFieldType(message->field(1), file); | ||
string key_type = AsObjectFieldType(message->field(0), file, force_int64_string); | ||
string value_type = AsObjectFieldType(message->field(1), file, force_int64_string); | ||
return "Array<[" + key_type + ", " + value_type + "]>"; | ||
} | ||
string field_type = JSMessageType(desc->message_type(), file) + ".AsObject"; | ||
|
@@ -814,7 +813,7 @@ void PrintProtoDtsOneofCase(Printer* printer, const OneofDescriptor* desc) { | |
} | ||
|
||
void PrintProtoDtsMessage(Printer* printer, const Descriptor* desc, | ||
const FileDescriptor* file) { | ||
const FileDescriptor* file, bool force_int64_string = false) { | ||
const string& class_name = desc->name(); | ||
std::map<string, string> vars; | ||
vars["class_name"] = class_name; | ||
|
@@ -824,7 +823,7 @@ void PrintProtoDtsMessage(Printer* printer, const Descriptor* desc, | |
for (int i = 0; i < desc->field_count(); i++) { | ||
const FieldDescriptor* field = desc->field(i); | ||
vars["js_field_name"] = JSFieldName(field); | ||
vars["js_field_type"] = JSFieldType(field, file); | ||
vars["js_field_type"] = JSFieldType(field, file, force_int64_string); | ||
if (field->type() != FieldDescriptor::TYPE_MESSAGE || | ||
field->is_repeated()) { | ||
printer->Print(vars, "get$js_field_name$(): $js_field_type$;\n"); | ||
|
@@ -856,7 +855,7 @@ void PrintProtoDtsMessage(Printer* printer, const Descriptor* desc, | |
} | ||
if (field->is_repeated() && !field->is_map()) { | ||
vars["js_field_name"] = JSElementName(field); | ||
vars["js_field_type"] = JSElementType(field, file); | ||
vars["js_field_type"] = JSElementType(field, file, force_int64_string); | ||
if (field->type() != FieldDescriptor::TYPE_MESSAGE) { | ||
printer->Print(vars, | ||
"add$js_field_name$(value: $js_field_type$, " | ||
|
@@ -871,8 +870,8 @@ void PrintProtoDtsMessage(Printer* printer, const Descriptor* desc, | |
printer->Print("\n"); | ||
} | ||
|
||
for (int i = 0; i < desc->real_oneof_decl_count(); i++) { | ||
const OneofDescriptor *oneof = desc->real_oneof_decl(i); | ||
for (int i = 0; i < desc->oneof_decl_count(); i++) { | ||
const OneofDescriptor *oneof = desc->oneof_decl(i); | ||
vars["js_oneof_name"] = ToUpperCamel(ParseLowerUnderscore(oneof->name())); | ||
printer->Print( | ||
vars, "get$js_oneof_name$Case(): $class_name$.$js_oneof_name$Case;\n"); | ||
|
@@ -905,7 +904,7 @@ void PrintProtoDtsMessage(Printer* printer, const Descriptor* desc, | |
js_field_name = "pb_" + js_field_name; | ||
} | ||
vars["js_field_name"] = js_field_name; | ||
vars["js_field_type"] = AsObjectFieldType(field, file); | ||
vars["js_field_type"] = AsObjectFieldType(field, file, force_int64_string); | ||
if (!field->has_presence()) { | ||
printer->Print(vars, "$js_field_name$: $js_field_type$;\n"); | ||
} else { | ||
|
@@ -920,7 +919,7 @@ void PrintProtoDtsMessage(Printer* printer, const Descriptor* desc, | |
continue; | ||
} | ||
printer->Print("\n"); | ||
PrintProtoDtsMessage(printer, desc->nested_type(i), file); | ||
PrintProtoDtsMessage(printer, desc->nested_type(i), file, force_int64_string); | ||
} | ||
|
||
for (int i = 0; i < desc->enum_type_count(); i++) { | ||
|
@@ -937,7 +936,7 @@ void PrintProtoDtsMessage(Printer* printer, const Descriptor* desc, | |
printer->Print("}\n\n"); | ||
} | ||
|
||
void PrintProtoDtsFile(Printer* printer, const FileDescriptor* file) { | ||
void PrintProtoDtsFile(Printer* printer, const FileDescriptor* file, bool force_int64_string = false) { | ||
printer->Print("import * as jspb from 'google-protobuf'\n\n"); | ||
|
||
for (int i = 0; i < file->dependency_count(); i++) { | ||
|
@@ -951,7 +950,7 @@ void PrintProtoDtsFile(Printer* printer, const FileDescriptor* file) { | |
printer->Print("\n\n"); | ||
|
||
for (int i = 0; i < file->message_type_count(); i++) { | ||
PrintProtoDtsMessage(printer, file->message_type(i), file); | ||
PrintProtoDtsMessage(printer, file->message_type(i), file, force_int64_string); | ||
} | ||
|
||
for (int i = 0; i < file->enum_type_count(); i++) { | ||
|
@@ -1432,6 +1431,7 @@ class GeneratorOptions { | |
bool generate_closure_es6() const { return generate_closure_es6_; } | ||
bool multiple_files() const { return multiple_files_; } | ||
bool goog_promise() const { return goog_promise_; } | ||
bool force_int64_string() const { return force_int64_string_; } | ||
|
||
private: | ||
string file_name_; | ||
|
@@ -1442,6 +1442,7 @@ class GeneratorOptions { | |
bool generate_closure_es6_; | ||
bool multiple_files_; | ||
bool goog_promise_; | ||
bool force_int64_string_; | ||
}; | ||
|
||
GeneratorOptions::GeneratorOptions() | ||
|
@@ -1452,7 +1453,8 @@ GeneratorOptions::GeneratorOptions() | |
generate_dts_(false), | ||
generate_closure_es6_(false), | ||
multiple_files_(false), | ||
goog_promise_(false) {} | ||
goog_promise_(false), | ||
force_int64_string_(false) {} | ||
|
||
bool GeneratorOptions::ParseFromOptions(const string& parameter, | ||
string* error) { | ||
|
@@ -1492,6 +1494,8 @@ bool GeneratorOptions::ParseFromOptions( | |
plugins_ = option.second; | ||
} else if ("goog_promise" == option.first) { | ||
goog_promise_ = "True" == option.second; | ||
} else if ("force_int64_string" == option.first) { | ||
force_int64_string_ = "True" == option.second; | ||
} else { | ||
*error = "unsupported option: " + option.first; | ||
return false; | ||
|
@@ -1528,13 +1532,9 @@ class GrpcCodeGenerator : public CodeGenerator { | |
|
||
uint64_t GetSupportedFeatures() const override { | ||
// Code generators must explicitly support proto3 optional. | ||
return CodeGenerator::FEATURE_PROTO3_OPTIONAL | CodeGenerator::FEATURE_SUPPORTS_EDITIONS; | ||
return CodeGenerator::FEATURE_PROTO3_OPTIONAL; | ||
} | ||
|
||
// Keep synced with protoc-gen-js: https://github.com/protocolbuffers/protobuf-javascript/blob/861c8020a5c0cba9b7cdf915dffde96a4421a1f4/generator/js_generator.h#L157-L158 | ||
Edition GetMinimumEdition() const override { return Edition::EDITION_PROTO2; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise. These seems useful to preserve? |
||
Edition GetMaximumEdition() const override { return Edition::EDITION_2023; } | ||
|
||
bool Generate(const FileDescriptor* file, const string& parameter, | ||
GeneratorContext* context, string* error) const override { | ||
GeneratorOptions generator_options; | ||
|
@@ -1575,7 +1575,7 @@ class GrpcCodeGenerator : public CodeGenerator { | |
std::unique_ptr<ZeroCopyOutputStream> proto_dts_output( | ||
context->Open(proto_dts_file_name)); | ||
Printer proto_dts_printer(proto_dts_output.get(), '$'); | ||
PrintProtoDtsFile(&proto_dts_printer, file); | ||
PrintProtoDtsFile(&proto_dts_printer, file, generator_options.force_int64_string()); | ||
} | ||
|
||
if (!file->service_count()) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
syntax = "proto3"; | ||
|
||
package test; | ||
|
||
import "google/protobuf/descriptor.proto"; | ||
|
||
message Int64PrecisionTest { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mind providing a Before v.s. After output of this file? Better yet, to include a test for it? (e.g. maybe in packages/grpc-web/test/tsc_test.js) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! Here's the before/after comparison: BEFORE (default behavior): export class Int64PrecisionTest extends jspb.Message {
getNormalInt64(): number; // ← number (may lose precision)
setNormalInt64(value: number): Int64PrecisionTest;
getStringInt64(): string; // ← string (has [jstype = JS_STRING])
setStringInt64(value: string): Int64PrecisionTest;
getNormalUint64(): number; // ← number (may lose precision)
setNormalUint64(value: number): Int64PrecisionTest;
getStringUint64(): string; // ← string (has [jstype = JS_STRING])
setStringUint64(value: string): Int64PrecisionTest;
} AFTER (with export class Int64PrecisionTest extends jspb.Message {
getNormalInt64(): string; // ← NOW string (precision preserved)
setNormalInt64(value: string): Int64PrecisionTest;
getStringInt64(): string; // ← still string
setStringInt64(value: string): Int64PrecisionTest;
getNormalUint64(): string; // ← NOW string (precision preserved)
setNormalUint64(value: string): Int64PrecisionTest;
getStringUint64(): string; // ← still string
setStringUint64(value: string): Int64PrecisionTest;
} Test AddedI've added a test in
The test checks the generated |
||
int64 normal_int64 = 1; | ||
int64 string_int64 = 2 [jstype = JS_STRING]; | ||
uint64 normal_uint64 = 3; | ||
uint64 string_uint64 = 4 [jstype = JS_STRING]; | ||
sint64 normal_sint64 = 5; | ||
sint64 string_sint64 = 6 [jstype = JS_STRING]; | ||
fixed64 normal_fixed64 = 7; | ||
fixed64 string_fixed64 = 8 [jstype = JS_STRING]; | ||
sfixed64 normal_sfixed64 = 9; | ||
sfixed64 string_sfixed64 = 10 [jstype = JS_STRING]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason you removed all Edition related code here and below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @sampajano ,
Good catch! I removed the Edition-related code because it was causing compilation errors with older protobuf versions (like 3.21.12 which many users still have installed).
The issue was:
Edition
enum was introduced in protobuf 4.23.0+FEATURE_SUPPORTS_EDITIONS
was also added in newer versionsreal_oneof_decl_count()
is a newer APIWithout the Edition support, the code wouldn't compile on older protobuf installations.
Options to fix this:
Option 1: Add version checks (cleaner approach)
Option 2: Keep Edition support and require newer protobuf (3.21.12 is quite old anyway)
Option 3: Remove Edition support entirely for now
Which approach would you prefer? I'm happy to add the version checks back in to maintain compatibility with the protoc-gen-js reference you mentioned, while still allowing compilation on older protobuf versions.
The comment about keeping synced with protoc-gen-js is definitely valuable and should be preserved!
Let me know your preference and I'll update the PR accordingly.
Thanks!