Implement helpers for exporting all FeatureSets to generators.

There are 4 different feature sets for every descriptor:
* Runtime/resolved - Used to make runtime decisions
* Source/resolved - Used to make codegen decisions
* Runtime/raw - Used to reason about the original proto file during runtime
* Source/raw - Used to validate features during codegen

All of these will need to be shipped to code generators.  While C++ generators can use the C++ runtime to reconstruct these, they won't end up in the FileDescriptorProtos we send to runtimes.  Similarly, non-C++ generators wouldn't be able to get them without duplicating our feature resolution logic.  This change adds a helper that allows us to bundle all of these into the protos we send to generators.

PiperOrigin-RevId: 547849243
pull/13316/head
Mike Kruskal 1 year ago committed by Mike Kruskal
parent b81d2cc8c5
commit c5a1dbeb1e
  1. 19
      src/google/protobuf/compiler/BUILD.bazel
  2. 12
      src/google/protobuf/compiler/code_generator.h
  3. 299
      src/google/protobuf/compiler/code_generator_unittest.cc
  4. 14
      src/google/protobuf/compiler/retention.cc
  5. 2
      src/google/protobuf/compiler/retention.h
  6. 65
      src/google/protobuf/compiler/retention_unittest.cc
  7. 35
      src/google/protobuf/descriptor.cc
  8. 13
      src/google/protobuf/descriptor.h
  9. 41
      src/google/protobuf/descriptor_unittest.cc
  10. 28
      src/google/protobuf/unittest_features.proto

@ -69,6 +69,7 @@ cc_library(
visibility = ["//visibility:public"],
deps = [
"//src/google/protobuf:protobuf_nowkt",
"//src/google/protobuf/compiler:retention",
"//src/google/protobuf/io:io_win32",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/log:absl_check",
@ -248,6 +249,24 @@ cc_binary(
],
)
cc_test(
name = "code_generator_unittest",
srcs = ["code_generator_unittest.cc"],
copts = COPTS,
deps = [
":code_generator",
":importer",
"//src/google/protobuf:cc_test_protos",
"//src/google/protobuf:test_textproto",
"//src/google/protobuf/io",
"//src/google/protobuf/testing",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_test(
name = "command_line_interface_unittest",
srcs = ["command_line_interface_unittest.cc"],

@ -43,7 +43,9 @@
#include <vector>
#include "absl/strings/string_view.h"
#include "google/protobuf/compiler/retention.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/descriptor.pb.h"
#include "google/protobuf/port.h"
// Must be included last.
@ -143,6 +145,16 @@ class PROTOC_EXPORT CodeGenerator {
static const FeatureSet& GetSourceRawFeatures(const DescriptorT& desc) {
return ::google::protobuf::internal::InternalFeatureHelper::GetRawFeatures(desc);
}
// Converts a FileDescriptor to a FileDescriptorProto suitable for passing off
// to a runtime. Notably, this strips all source-retention options and
// includes both raw and resolved features.
static FileDescriptorProto GetRuntimeProto(const FileDescriptor& file) {
FileDescriptorProto proto =
::google::protobuf::internal::InternalFeatureHelper::GetGeneratorProto(file);
StripSourceRetentionOptions(*file.pool(), proto);
return proto;
}
#endif // PROTOBUF_FUTURE_EDITIONS
};

@ -0,0 +1,299 @@
// Protocol Buffers - Google's data interchange format
// Copyright 2023 Google Inc. All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "google/protobuf/compiler/code_generator.h"
#include <string>
#include "google/protobuf/descriptor.pb.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "absl/log/absl_log.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/string_view.h"
#include "google/protobuf/compiler/parser.h"
#include "google/protobuf/io/tokenizer.h"
#include "google/protobuf/io/zero_copy_stream_impl_lite.h"
#include "google/protobuf/test_textproto.h"
#include "google/protobuf/unittest_features.pb.h"
namespace google {
namespace protobuf {
namespace compiler {
namespace {
using ::testing::NotNull;
class TestGenerator : public CodeGenerator {
public:
bool Generate(const FileDescriptor* file, const std::string& parameter,
GeneratorContext* generator_context,
std::string* error) const override {
return true;
}
// Expose the protected methods for testing.
using CodeGenerator::GetRuntimeProto;
using CodeGenerator::GetSourceFeatures;
using CodeGenerator::GetSourceRawFeatures;
};
class SimpleErrorCollector : public io::ErrorCollector {
public:
void RecordError(int line, int column, absl::string_view message) override {
ABSL_LOG(ERROR) << absl::StrFormat("%d:%d:%s", line, column, message);
}
};
class CodeGeneratorTest : public ::testing::Test {
protected:
void SetUp() override {
ASSERT_THAT(BuildFile(DescriptorProto::descriptor()->file()), NotNull());
ASSERT_THAT(BuildFile(pb::TestMessage::descriptor()->file()), NotNull());
}
const FileDescriptor* BuildFile(absl::string_view schema) {
io::ArrayInputStream input_stream(schema.data(),
static_cast<int>(schema.size()));
SimpleErrorCollector error_collector;
io::Tokenizer tokenizer(&input_stream, &error_collector);
Parser parser;
parser.RecordErrorsTo(&error_collector);
FileDescriptorProto proto;
ABSL_CHECK(parser.Parse(&tokenizer, &proto)) << schema;
proto.set_name("test.proto");
return pool_.BuildFile(proto);
}
const FileDescriptor* BuildFile(const FileDescriptor* file) {
FileDescriptorProto proto;
file->CopyTo(&proto);
return pool_.BuildFile(proto);
}
DescriptorPool pool_;
};
TEST_F(CodeGeneratorTest, GetSourceRawFeaturesRoot) {
auto file = BuildFile(R"schema(
edition = "2023";
package protobuf_unittest;
import "google/protobuf/unittest_features.proto";
option features.field_presence = EXPLICIT; // 2023 default
option features.enum_type = CLOSED; // override
option features.(pb.test).int_file_feature = 8;
option features.(pb.test).string_source_feature = "file";
)schema");
ASSERT_THAT(file, NotNull());
EXPECT_THAT(TestGenerator::GetSourceRawFeatures(*file),
google::protobuf::EqualsProto(R"pb(
field_presence: EXPLICIT
enum_type: CLOSED
[pb.test] { int_file_feature: 8 string_source_feature: "file" }
)pb"));
}
TEST_F(CodeGeneratorTest, GetSourceRawFeaturesInherited) {
auto file = BuildFile(R"schema(
edition = "2023";
package protobuf_unittest;
import "google/protobuf/unittest_features.proto";
option features.enum_type = OPEN;
option features.(pb.test).int_file_feature = 6;
message EditionsMessage {
option features.(pb.test).int_message_feature = 7;
option features.(pb.test).int_multiple_feature = 8;
string field = 1 [
features.field_presence = EXPLICIT,
features.(pb.test).int_multiple_feature = 9,
features.(pb.test).string_source_feature = "field"
];
}
)schema");
ASSERT_THAT(file, NotNull());
const FieldDescriptor* field =
file->FindMessageTypeByName("EditionsMessage")->FindFieldByName("field");
ASSERT_THAT(field, NotNull());
EXPECT_THAT(
TestGenerator::GetSourceRawFeatures(*field), google::protobuf::EqualsProto(R"pb(
field_presence: EXPLICIT
[pb.test] { int_multiple_feature: 9 string_source_feature: "field" }
)pb"));
}
TEST_F(CodeGeneratorTest, GetSourceFeaturesRoot) {
auto file = BuildFile(R"schema(
edition = "2023";
package protobuf_unittest;
import "google/protobuf/unittest_features.proto";
option features.field_presence = EXPLICIT; // 2023 default
option features.enum_type = CLOSED; // override
option features.(pb.test).int_file_feature = 8;
option features.(pb.test).string_source_feature = "file";
)schema");
ASSERT_THAT(file, NotNull());
const FeatureSet& features = TestGenerator::GetSourceFeatures(*file);
const pb::TestFeatures& ext = features.GetExtension(pb::test);
EXPECT_TRUE(features.has_repeated_field_encoding());
EXPECT_TRUE(features.field_presence());
EXPECT_EQ(features.field_presence(), FeatureSet::EXPLICIT);
EXPECT_EQ(features.enum_type(), FeatureSet::CLOSED);
EXPECT_TRUE(ext.has_int_message_feature());
EXPECT_EQ(ext.int_file_feature(), 8);
EXPECT_EQ(ext.string_source_feature(), "file");
}
TEST_F(CodeGeneratorTest, GetSourceFeaturesInherited) {
auto file = BuildFile(R"schema(
edition = "2023";
package protobuf_unittest;
import "google/protobuf/unittest_features.proto";
option features.enum_type = CLOSED;
option features.(pb.test).int_source_feature = 5;
option features.(pb.test).int_file_feature = 6;
message EditionsMessage {
option features.(pb.test).int_message_feature = 7;
option features.(pb.test).int_multiple_feature = 8;
option features.(pb.test).string_source_feature = "message";
string field = 1 [
features.field_presence = IMPLICIT,
features.(pb.test).int_multiple_feature = 9,
features.(pb.test).string_source_feature = "field"
];
}
)schema");
ASSERT_THAT(file, NotNull());
const FieldDescriptor* field =
file->FindMessageTypeByName("EditionsMessage")->FindFieldByName("field");
ASSERT_THAT(field, NotNull());
const FeatureSet& features = TestGenerator::GetSourceFeatures(*field);
const pb::TestFeatures& ext = features.GetExtension(pb::test);
EXPECT_EQ(features.enum_type(), FeatureSet::CLOSED);
EXPECT_EQ(features.field_presence(), FeatureSet::IMPLICIT);
EXPECT_EQ(ext.int_message_feature(), 7);
EXPECT_EQ(ext.int_file_feature(), 6);
EXPECT_EQ(ext.int_multiple_feature(), 9);
EXPECT_EQ(ext.int_source_feature(), 5);
EXPECT_EQ(ext.string_source_feature(), "field");
}
TEST_F(CodeGeneratorTest, GetRuntimeProtoRoot) {
auto file = BuildFile(R"schema(
edition = "2023";
package protobuf_unittest;
import "google/protobuf/unittest_features.proto";
option features.enum_type = CLOSED;
option features.(pb.test).int_source_feature = 5;
option features.(pb.test).int_file_feature = 6;
)schema");
ASSERT_THAT(file, NotNull());
FileDescriptorProto proto = TestGenerator::GetRuntimeProto(*file);
const FeatureSet& features = proto.options().features();
const pb::TestFeatures& ext = features.GetExtension(pb::test);
EXPECT_THAT(features.raw_features(),
EqualsProto(R"pb(enum_type: CLOSED
[pb.test] { int_file_feature: 6 })pb"));
EXPECT_EQ(features.enum_type(), FeatureSet::CLOSED);
EXPECT_TRUE(features.has_field_presence());
EXPECT_EQ(features.field_presence(), FeatureSet::EXPLICIT);
EXPECT_FALSE(ext.has_int_source_feature());
EXPECT_EQ(ext.int_file_feature(), 6);
}
TEST_F(CodeGeneratorTest, GetRuntimeProtoInherited) {
auto file = BuildFile(R"schema(
edition = "2023";
package protobuf_unittest;
import "google/protobuf/unittest_features.proto";
option features.enum_type = CLOSED;
option features.(pb.test).int_source_feature = 5;
option features.(pb.test).int_file_feature = 6;
message EditionsMessage {
option features.(pb.test).int_message_feature = 7;
option features.(pb.test).int_multiple_feature = 8;
string field = 1 [
features.field_presence = IMPLICIT,
features.(pb.test).int_multiple_feature = 9,
features.(pb.test).string_source_feature = "field"
];
}
)schema");
ASSERT_THAT(file, NotNull());
FileDescriptorProto proto = TestGenerator::GetRuntimeProto(*file);
const FieldDescriptorProto& field = proto.message_type(0).field(0);
const FeatureSet& features = field.options().features();
const pb::TestFeatures& ext = features.GetExtension(pb::test);
EXPECT_THAT(features.raw_features(), google::protobuf::EqualsProto(R"pb(
field_presence: IMPLICIT
[pb.test] { int_multiple_feature: 9 }
)pb"));
EXPECT_EQ(features.enum_type(), FeatureSet::CLOSED);
EXPECT_EQ(features.field_presence(), FeatureSet::IMPLICIT);
EXPECT_EQ(ext.int_multiple_feature(), 9);
EXPECT_EQ(ext.int_message_feature(), 7);
EXPECT_EQ(ext.int_file_feature(), 6);
EXPECT_FALSE(ext.has_int_source_feature());
EXPECT_FALSE(ext.has_string_source_feature());
}
} // namespace
} // namespace compiler
} // namespace protobuf
} // namespace google

@ -248,16 +248,22 @@ FileDescriptorProto StripSourceRetentionOptions(const FileDescriptor& file,
bool include_source_code_info) {
FileDescriptorProto file_proto;
file.CopyTo(&file_proto);
std::vector<std::vector<int>> stripped_paths;
ConvertToDynamicMessageAndStripOptions(file_proto, *file.pool(),
&stripped_paths);
if (include_source_code_info) {
file.CopySourceCodeInfoTo(&file_proto);
StripSourceCodeInfo(stripped_paths, *file_proto.mutable_source_code_info());
}
StripSourceRetentionOptions(*file.pool(), file_proto);
return file_proto;
}
void StripSourceRetentionOptions(const DescriptorPool& pool,
FileDescriptorProto& file_proto) {
std::vector<std::vector<int>> stripped_paths;
ConvertToDynamicMessageAndStripOptions(file_proto, pool, &stripped_paths);
if (file_proto.has_source_code_info()) {
StripSourceCodeInfo(stripped_paths, *file_proto.mutable_source_code_info());
}
}
DescriptorProto StripSourceRetentionOptions(const Descriptor& message) {
DescriptorProto message_proto;
message.CopyTo(&message_proto);

@ -47,6 +47,8 @@ namespace compiler {
// corresponding to source-retention options.
PROTOC_EXPORT FileDescriptorProto StripSourceRetentionOptions(
const FileDescriptor& file, bool include_source_code_info = false);
PROTOC_EXPORT void StripSourceRetentionOptions(const DescriptorPool& pool,
FileDescriptorProto& file_proto);
PROTOC_EXPORT DescriptorProto
StripSourceRetentionOptions(const Descriptor& message);
PROTOC_EXPORT DescriptorProto::ExtensionRange StripSourceRetentionOptions(

@ -115,16 +115,7 @@ class RetentionStripTest : public testing::Test {
DynamicMessageFactory factory;
std::unique_ptr<Message> dynamic_message(
factory.GetPrototype(file_options_descriptor)->New());
ABSL_CHECK(TextFormat::ParseFromString(
R"([google.protobuf.internal.options] {
i2: 456
c {}
rc {}
}
[google.protobuf.internal.repeated_options] {
i2: 222
})",
dynamic_message.get()));
ABSL_CHECK(TextFormat::ParseFromString(data, dynamic_message.get()));
ProtoType ret;
ABSL_CHECK(ret.ParseFromString(dynamic_message->SerializeAsString()));
return ret;
@ -179,6 +170,60 @@ TEST_F(RetentionStripTest, StripSourceRetentionFileOptions) {
EqualsProto(expected_options));
}
TEST_F(RetentionStripTest, StripSourceRetentionProtoFileOptions) {
const FileDescriptor* file = ParseSchema(R"schema(
option (source_retention_option) = 123;
option (options) = {
i1: 123
i2: 456
c { s: "abc" }
rc { s: "abc" }
};
option (repeated_options) = {
i1: 111 i2: 222
};
message Options {
optional int32 i1 = 1 [retention = RETENTION_SOURCE];
optional int32 i2 = 2;
message ChildMessage {
optional string s = 1 [retention = RETENTION_SOURCE];
}
optional ChildMessage c = 3;
repeated ChildMessage rc = 4;
}
extend google.protobuf.FileOptions {
optional int32 source_retention_option = 50000 [retention = RETENTION_SOURCE];
optional Options options = 50001;
repeated Options repeated_options = 50002;
}
)schema");
FileDescriptorProto proto;
file->CopyTo(&proto);
ASSERT_THAT(proto.options(), EqualsProto(BuildDynamicProto<FileOptions>(R"pb(
[google.protobuf.internal.source_retention_option]: 123
[google.protobuf.internal.options] {
i1: 123
i2: 456
c { s: "abc" }
rc { s: "abc" }
}
[google.protobuf.internal.repeated_options] { i1: 111 i2: 222 })pb")));
StripSourceRetentionOptions(*file->pool(), proto);
EXPECT_THAT(proto.options(), EqualsProto(BuildDynamicProto<FileOptions>(R"pb(
[google.protobuf.internal.options] {
i2: 456
c {}
rc {}
}
[google.protobuf.internal.repeated_options] { i2: 222 })pb")));
}
TEST_F(RetentionStripTest, StripSourceRetentionMessageOptions) {
const FileDescriptor* file = ParseSchema(R"schema(
message TestMessage {

@ -2759,6 +2759,32 @@ FileDescriptor::FileDescriptor() {}
// CopyTo methods ====================================================
#ifdef PROTOBUF_FUTURE_EDITIONS
namespace internal {
FileDescriptorProto InternalFeatureHelper::GetGeneratorProto(
const FileDescriptor& file) {
FileDescriptorProto file_proto;
file.CopyTo(&file_proto);
// Insert all the raw features back into the proto.
internal::VisitDescriptors(
file, file_proto, [](const auto& desc, auto& proto) {
const auto& features = GetFeatures(desc);
if (&features != &FeatureSet::default_instance() &&
!IsLegacyFeatureSet(features)) {
*proto.mutable_options()->mutable_features() = features;
}
const auto& raw_features = GetRawFeatures(desc);
if (&raw_features != &FeatureSet::default_instance()) {
*proto.mutable_options()->mutable_features()->mutable_raw_features() =
GetRawFeatures(desc);
}
});
return file_proto;
}
} // namespace internal
#endif // PROTOBUF_FUTURE_EDITIONS
void FileDescriptor::CopyTo(FileDescriptorProto* proto) const {
CopyHeadingTo(proto);
@ -5407,7 +5433,14 @@ void DescriptorBuilder::ResolveFeaturesImpl(
// internal details.
FeatureSet* mutable_features = alloc.AllocateArray<FeatureSet>(1);
descriptor->proto_features_ = mutable_features;
options->mutable_features()->Swap(mutable_features);
if (options->features().has_raw_features()) {
// If the raw features are specified, use those and recalculate the
// resolved features.
options->mutable_features()->mutable_raw_features()->Swap(
mutable_features);
} else {
options->mutable_features()->Swap(mutable_features);
}
options->clear_features();
} else if (!force_merge) {
// Nothing to merge, and we aren't forcing it.

@ -262,7 +262,7 @@ class PROTOBUF_EXPORT SymbolBaseN : public SymbolBase {};
// This class is for internal use only and provides access to the FeatureSets
// defined on descriptors. These features are not designed to be stable, and
// depending directly on them (vs the public descriptor APIs) is not safe.
class InternalFeatureHelper {
class PROTOBUF_EXPORT InternalFeatureHelper {
public:
template <typename DescriptorT>
static const FeatureSet& GetFeatures(const DescriptorT& desc) {
@ -271,10 +271,21 @@ class InternalFeatureHelper {
private:
friend class ::google::protobuf::compiler::CodeGenerator;
// Provides a restricted view exclusively to code generators. Raw features
// haven't been resolved, and are virtually meaningless to everyone else. Code
// generators will need them to validate their own features, and runtimes may
// need them internally to be able to properly represent the original proto
// files from generated code.
template <typename DescriptorT>
static const FeatureSet& GetRawFeatures(const DescriptorT& desc) {
return *desc.proto_features_;
}
// Provides the full descriptor tree including both resolved features (in the
// `features` fields) and unresolved features (in the `raw_features` fields)
// for every descriptor.
static FileDescriptorProto GetGeneratorProto(const FileDescriptor& file);
};
#endif // PROTOBUF_FUTURE_EDITIONS

@ -9538,6 +9538,47 @@ TEST_F(FeaturesTest, UninterpretedOptionsMergeExtension) {
9);
}
TEST_F(FeaturesTest, RawFeatures) {
BuildDescriptorMessagesInTestPool();
const FileDescriptor* file = BuildFile(R"pb(
name: "foo.proto"
syntax: "editions"
edition: "2023"
options { features { raw_features { field_presence: IMPLICIT } } }
)pb");
EXPECT_THAT(file->options(), EqualsProto(""));
EXPECT_THAT(GetFeatures(file), EqualsProto(R"pb(
field_presence: IMPLICIT
enum_type: OPEN
repeated_field_encoding: PACKED
string_field_validation: MANDATORY
message_encoding: LENGTH_PREFIXED
json_format: ALLOW)pb"));
}
TEST_F(FeaturesTest, RawFeaturesConflict) {
BuildDescriptorMessagesInTestPool();
const FileDescriptor* file = BuildFile(R"pb(
name: "foo.proto"
syntax: "editions"
edition: "2023"
options {
features {
enum_type: CLOSED
raw_features { field_presence: IMPLICIT }
}
}
)pb");
EXPECT_THAT(file->options(), EqualsProto(""));
EXPECT_THAT(GetFeatures(file), EqualsProto(R"pb(
field_presence: IMPLICIT
enum_type: OPEN
repeated_field_encoding: PACKED
string_field_validation: MANDATORY
message_encoding: LENGTH_PREFIXED
json_format: ALLOW)pb"));
}
TEST_F(FeaturesTest, InvalidJsonUniquenessDefaultWarning) {
BuildFileWithWarnings(
R"pb(

@ -164,4 +164,32 @@ message TestFeatures {
edition_defaults = { edition: "2023", value: "ENUM_VALUE1" },
edition_defaults = { edition: "2024", value: "ENUM_VALUE4" }
];
optional int32 int_source_feature = 15 [
retention = RETENTION_SOURCE,
targets = TARGET_TYPE_FILE,
targets = TARGET_TYPE_FIELD,
targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM,
targets = TARGET_TYPE_ENUM_ENTRY,
targets = TARGET_TYPE_SERVICE,
targets = TARGET_TYPE_METHOD,
targets = TARGET_TYPE_ONEOF,
targets = TARGET_TYPE_EXTENSION_RANGE,
edition_defaults = { edition: "2023", value: "1" }
];
optional string string_source_feature = 16 [
retention = RETENTION_SOURCE,
targets = TARGET_TYPE_FILE,
targets = TARGET_TYPE_FIELD,
targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM,
targets = TARGET_TYPE_ENUM_ENTRY,
targets = TARGET_TYPE_SERVICE,
targets = TARGET_TYPE_METHOD,
targets = TARGET_TYPE_ONEOF,
targets = TARGET_TYPE_EXTENSION_RANGE,
edition_defaults = { edition: "2023", value: "'2023'" }
];
}

Loading…
Cancel
Save