Skip to content

Commit 57d8049

Browse files
mkruskal-googlecopybara-github
authored andcommitted
Editions: Refactor feature resolution to use an intermediate message.
This places all of the tricky reflection logic of feature resolution into a single location that outputs a portable protobuf message. With this message, feature resolution becomes drastically simpler and easy to reimplement in other languages. Follow-up changes will provide: - An API to specify generator features from the CodeGenerator class for C++ plugins - A CLI for building these default mappings - Bazel/CMake rules to help embed these mappings in target languages (i.e. for runtimes and non-C++ plugins) PiperOrigin-RevId: 559615720
1 parent 7d5592e commit 57d8049

File tree

9 files changed

+422
-399
lines changed

9 files changed

+422
-399
lines changed

src/google/protobuf/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,9 @@ cc_test(
11091109
"//src/google/protobuf/compiler:importer",
11101110
"//src/google/protobuf/stubs",
11111111
"//src/google/protobuf/testing",
1112+
"@com_google_absl//absl/log:absl_check",
1113+
"@com_google_absl//absl/log:absl_log",
1114+
"@com_google_absl//absl/log:die_if_null",
11121115
"@com_google_googletest//:gtest",
11131116
"@com_google_googletest//:gtest_main",
11141117
],

src/google/protobuf/compiler/code_generator_unittest.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ TEST_F(CodeGeneratorTest, GetResolvedSourceFeaturesRoot) {
177177
EXPECT_EQ(features.field_presence(), FeatureSet::EXPLICIT);
178178
EXPECT_EQ(features.enum_type(), FeatureSet::CLOSED);
179179

180-
EXPECT_TRUE(ext.has_int_message_feature());
180+
// TODO(b/296638633) Flip this once generators can specify their feature sets.
181+
EXPECT_FALSE(ext.has_int_message_feature());
181182
EXPECT_EQ(ext.int_file_feature(), 8);
182183
EXPECT_EQ(ext.string_source_feature(), "file");
183184
}

src/google/protobuf/compiler/command_line_interface_unittest.cc

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,7 @@ TEST_F(CommandLineInterfaceTest, AllowServicesHasService) {
13531353

13541354
TEST_F(CommandLineInterfaceTest, EditionsAreNotAllowed) {
13551355
CreateTempFile("foo.proto",
1356-
"edition = \"very-cool\";\n"
1356+
"edition = \"2023\";\n"
13571357
"message FooRequest {}\n");
13581358

13591359
Run("protocol_compiler --proto_path=$tmpdir --test_out=$tmpdir foo.proto");
@@ -1365,7 +1365,7 @@ TEST_F(CommandLineInterfaceTest, EditionsAreNotAllowed) {
13651365

13661366
TEST_F(CommandLineInterfaceTest, EditionsFlagExplicitTrue) {
13671367
CreateTempFile("foo.proto",
1368-
"edition = \"very-cool\";\n"
1368+
"edition = \"2023\";\n"
13691369
"message FooRequest {}\n");
13701370

13711371
Run("protocol_compiler --proto_path=$tmpdir --test_out=$tmpdir "
@@ -1477,14 +1477,16 @@ TEST_F(CommandLineInterfaceTest, FeatureExtensionError) {
14771477
import "features.proto";
14781478
message Foo {
14791479
int32 bar = 1;
1480-
int32 baz = 2 [features.(pb.test).int_feature = 5];
1480+
int32 baz = 2 [features.(pb.test).repeated_feature = 5];
14811481
})schema");
14821482

14831483
Run("protocol_compiler --proto_path=$tmpdir --test_out=$tmpdir "
14841484
"--experimental_editions foo.proto");
1485-
ExpectErrorSubstring(
1486-
"Feature field pb.TestFeatures.repeated_feature is an unsupported "
1487-
"repeated field");
1485+
// TODO(b/296638633) Flip this once generators can specify their feature sets.
1486+
ExpectNoErrors();
1487+
// ExpectErrorSubstring(
1488+
// "Feature field pb.TestFeatures.repeated_feature is an unsupported "
1489+
// "repeated field");
14881490
}
14891491

14901492
TEST_F(CommandLineInterfaceTest, Plugin_LegacyFeatures) {

src/google/protobuf/descriptor.cc

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#include <sstream>
4545
#include <string>
4646
#include <type_traits>
47+
#include <utility>
4748
#include <vector>
4849

4950
#include "google/protobuf/stubs/common.h"
@@ -57,6 +58,7 @@
5758
#include "absl/hash/hash.h"
5859
#include "absl/log/absl_check.h"
5960
#include "absl/log/absl_log.h"
61+
#include "absl/memory/memory.h"
6062
#include "absl/status/statusor.h"
6163
#include "absl/strings/ascii.h"
6264
#include "absl/strings/escaping.h"
@@ -1111,6 +1113,18 @@ bool AllowedExtendeeInProto3(const std::string& name) {
11111113
allowed_proto3_extendees->end();
11121114
}
11131115

1116+
const FeatureSetDefaults& GetCppFeatureSetDefaults() {
1117+
static const FeatureSetDefaults* default_spec = [] {
1118+
auto default_spec = FeatureResolver::CompileDefaults(
1119+
FeatureSet::descriptor(),
1120+
// TODO(b/297261063) Move this range to a central location.
1121+
{pb::CppFeatures::descriptor()->file()->extension(0)}, "2023", "2023");
1122+
ABSL_CHECK(default_spec.ok()) << default_spec.status();
1123+
return new FeatureSetDefaults(std::move(default_spec).value());
1124+
}();
1125+
return *default_spec;
1126+
}
1127+
11141128
// Create global defaults for proto2/proto3 compatibility.
11151129
FeatureSet* CreateProto2DefaultFeatures() {
11161130
FeatureSet* features = new FeatureSet();
@@ -4577,14 +4591,7 @@ class DescriptorBuilder {
45774591

45784592
const FileDescriptor* DescriptorPool::BuildFile(
45794593
const FileDescriptorProto& proto) {
4580-
ABSL_CHECK(fallback_database_ == nullptr)
4581-
<< "Cannot call BuildFile on a DescriptorPool that uses a "
4582-
"DescriptorDatabase. You must instead find a way to get your file "
4583-
"into the underlying database.";
4584-
ABSL_CHECK(mutex_ == nullptr); // Implied by the above ABSL_CHECK.
4585-
tables_->known_bad_symbols_.clear();
4586-
tables_->known_bad_files_.clear();
4587-
return DescriptorBuilder::New(this, tables_.get(), nullptr)->BuildFile(proto);
4594+
return BuildFileCollectingErrors(proto, nullptr);
45884595
}
45894596

45904597
const FileDescriptor* DescriptorPool::BuildFileCollectingErrors(
@@ -4596,13 +4603,15 @@ const FileDescriptor* DescriptorPool::BuildFileCollectingErrors(
45964603
ABSL_CHECK(mutex_ == nullptr); // Implied by the above ABSL_CHECK.
45974604
tables_->known_bad_symbols_.clear();
45984605
tables_->known_bad_files_.clear();
4606+
build_started_ = true;
45994607
return DescriptorBuilder::New(this, tables_.get(), error_collector)
46004608
->BuildFile(proto);
46014609
}
46024610

46034611
const FileDescriptor* DescriptorPool::BuildFileFromDatabase(
46044612
const FileDescriptorProto& proto) const {
46054613
mutex_->AssertHeld();
4614+
build_started_ = true;
46064615
if (tables_->known_bad_files_.contains(proto.name())) {
46074616
return nullptr;
46084617
}
@@ -4615,6 +4624,14 @@ const FileDescriptor* DescriptorPool::BuildFileFromDatabase(
46154624
return result;
46164625
}
46174626

4627+
void DescriptorPool::SetFeatureSetDefaults(FeatureSetDefaults spec) {
4628+
ABSL_CHECK(!build_started_)
4629+
<< "Feature set defaults can't be changed once the pool has started "
4630+
"building.";
4631+
feature_set_defaults_spec_ =
4632+
absl::make_unique<FeatureSetDefaults>(std::move(spec));
4633+
}
4634+
46184635
DescriptorBuilder::DescriptorBuilder(
46194636
const DescriptorPool* pool, DescriptorPool::Tables* tables,
46204637
DescriptorPool::ErrorCollector* error_collector)
@@ -5699,8 +5716,13 @@ FileDescriptor* DescriptorBuilder::BuildFileImpl(
56995716
}
57005717
ABSL_CHECK(descriptor);
57015718

5719+
const FeatureSetDefaults& defaults =
5720+
pool_->feature_set_defaults_spec_ == nullptr
5721+
? GetCppFeatureSetDefaults()
5722+
: *pool_->feature_set_defaults_spec_;
5723+
57025724
absl::StatusOr<FeatureResolver> feature_resolver =
5703-
FeatureResolver::Create(proto.edition(), descriptor);
5725+
FeatureResolver::Create(proto.edition(), defaults);
57045726
if (!feature_resolver.ok()) {
57055727
AddError(
57065728
proto.name(), proto, DescriptorPool::ErrorCollector::EDITIONS,
@@ -5816,16 +5838,6 @@ FileDescriptor* DescriptorBuilder::BuildFileImpl(
58165838
return nullptr;
58175839
}
58185840

5819-
// Look for feature extensions in regular imports.
5820-
if (feature_resolver_.has_value() && dependency != nullptr) {
5821-
absl::Status status = feature_resolver_->RegisterExtensions(*dependency);
5822-
if (!status.ok()) {
5823-
AddError(dependency->name(), proto,
5824-
DescriptorPool::ErrorCollector::EDITIONS,
5825-
[&] { return std::string(status.message()); });
5826-
}
5827-
}
5828-
58295841
if (dependency == nullptr) {
58305842
if (!pool_->lazily_build_dependencies_) {
58315843
if (pool_->allow_unknown_ ||

src/google/protobuf/descriptor.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class MethodOptions;
122122
class FileOptions;
123123
class UninterpretedOption;
124124
class FeatureSet;
125+
class FeatureSetDefaults;
125126
class SourceCodeInfo;
126127

127128
// Defined in message_lite.h
@@ -2289,6 +2290,13 @@ class PROTOBUF_EXPORT DescriptorPool {
22892290
// DescriptorPool will report a import not found error.
22902291
void EnforceWeakDependencies(bool enforce) { enforce_weak_ = enforce; }
22912292

2293+
// Sets the default feature mappings used during the build. If this function
2294+
// isn't called, the C++ feature set defaults are used. If this function is
2295+
// called, these defaults will be used instead.
2296+
// FeatureSetDefaults includes a minimum/maximum supported edition, which will
2297+
// be enforced while building proto files.
2298+
void SetFeatureSetDefaults(FeatureSetDefaults spec);
2299+
22922300
// Toggles enforcement of extension declarations.
22932301
// This enforcement is disabled by default because it requires full
22942302
// descriptors with source-retention options, which are generally not
@@ -2469,11 +2477,16 @@ class PROTOBUF_EXPORT DescriptorPool {
24692477
bool enforce_extension_declarations_;
24702478
bool disallow_enforce_utf8_;
24712479
bool deprecated_legacy_json_field_conflicts_;
2480+
mutable bool build_started_ = false;
24722481

24732482
// Set of files to track for unused imports. The bool value when true means
24742483
// unused imports are treated as errors (and as warnings when false).
24752484
absl::flat_hash_map<std::string, bool> unused_import_track_files_;
24762485

2486+
// Specification of defaults to use for feature resolution. This defaults to
2487+
// just the global and C++ features, but can be overridden for other runtimes.
2488+
std::unique_ptr<FeatureSetDefaults> feature_set_defaults_spec_;
2489+
24772490
// Returns true if the field extends an option message of descriptor.proto.
24782491
bool IsExtendingDescriptor(const FieldDescriptor& field) const;
24792492

src/google/protobuf/descriptor_unittest.cc

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include <memory>
4242
#include <string>
4343
#include <tuple>
44+
#include <utility>
4445
#include <vector>
4546

4647
#include "google/protobuf/stubs/common.h"
@@ -67,6 +68,7 @@
6768
#include "google/protobuf/descriptor_database.h"
6869
#include "google/protobuf/descriptor_legacy.h"
6970
#include "google/protobuf/dynamic_message.h"
71+
#include "google/protobuf/feature_resolver.h"
7072
#include "google/protobuf/io/tokenizer.h"
7173
#include "google/protobuf/io/zero_copy_stream_impl_lite.h"
7274
#include "google/protobuf/test_textproto.h"
@@ -514,17 +516,17 @@ TEST_F(FileDescriptorTest, Syntax) {
514516
}
515517
{
516518
proto.set_syntax("editions");
517-
proto.set_edition("very-cool");
519+
proto.set_edition("2023");
518520
DescriptorPool pool;
519521
const FileDescriptor* file = pool.BuildFile(proto);
520522
ASSERT_TRUE(file != nullptr);
521523
EXPECT_EQ(FileDescriptorLegacy::Syntax::SYNTAX_EDITIONS,
522524
FileDescriptorLegacy(file).syntax());
523-
EXPECT_EQ("very-cool", file->edition());
525+
EXPECT_EQ("2023", file->edition());
524526
FileDescriptorProto other;
525527
file->CopyTo(&other);
526528
EXPECT_EQ("editions", other.syntax());
527-
EXPECT_EQ("very-cool", other.edition());
529+
EXPECT_EQ("2023", other.edition());
528530
}
529531
}
530532

@@ -552,7 +554,7 @@ TEST_F(FileDescriptorTest, CopyHeadingTo) {
552554
EXPECT_EQ(&other.options().features(), &FeatureSet::default_instance());
553555
{
554556
proto.set_syntax("editions");
555-
proto.set_edition("very-cool");
557+
proto.set_edition("2023");
556558

557559
DescriptorPool pool;
558560
const FileDescriptor* file = pool.BuildFile(proto);
@@ -563,7 +565,7 @@ TEST_F(FileDescriptorTest, CopyHeadingTo) {
563565
EXPECT_EQ(other.name(), "foo.proto");
564566
EXPECT_EQ(other.package(), "foo.bar.baz");
565567
EXPECT_EQ(other.syntax(), "editions");
566-
EXPECT_EQ(other.edition(), "very-cool");
568+
EXPECT_EQ(other.edition(), "2023");
567569
EXPECT_EQ(other.options().java_package(), "foo.bar.baz");
568570
EXPECT_TRUE(other.message_type().empty());
569571
EXPECT_EQ(&other.options().features(), &FeatureSet::default_instance());
@@ -7235,7 +7237,24 @@ TEST_F(ValidationErrorTest, UnusedImportWithOtherError) {
72357237
"foo.proto: Foo.foo: EXTENDEE: \"Baz\" is not defined.\n");
72367238
}
72377239

7238-
using FeaturesTest = ValidationErrorTest;
7240+
using FeaturesBaseTest = ValidationErrorTest;
7241+
7242+
class FeaturesTest : public FeaturesBaseTest {
7243+
protected:
7244+
void SetUp() override {
7245+
ValidationErrorTest::SetUp();
7246+
7247+
auto default_spec = FeatureResolver::CompileDefaults(
7248+
FeatureSet::descriptor(),
7249+
{pb::CppFeatures::descriptor()->file()->extension(0),
7250+
pb::TestFeatures::descriptor()->file()->extension(0),
7251+
pb::TestMessage::descriptor()->extension(0),
7252+
pb::TestMessage::Nested::descriptor()->extension(0)},
7253+
"2023", "2025");
7254+
ASSERT_OK(default_spec);
7255+
pool_.SetFeatureSetDefaults(std::move(default_spec).value());
7256+
}
7257+
};
72397258

72407259
template <typename T>
72417260
const FeatureSet& GetFeatures(const T* descriptor) {
@@ -7544,12 +7563,36 @@ TEST_F(FeaturesTest, Edition2023Defaults) {
75447563
[pb.cpp] { legacy_closed_enum: false utf8_validation: VERIFY_PARSE }
75457564
)pb"));
75467565

7547-
// Since pb::test is linked in, it should end up with defaults in our
7548-
// FeatureSet.
7566+
// Since pb::test is registered in the pool, it should end up with defaults in
7567+
// our FeatureSet.
75497568
EXPECT_TRUE(GetFeatures(file).HasExtension(pb::test));
75507569
EXPECT_EQ(GetFeatures(file).GetExtension(pb::test).int_file_feature(), 1);
75517570
}
75527571

7572+
TEST_F(FeaturesBaseTest, DefaultEdition2023Defaults) {
7573+
BuildDescriptorMessagesInTestPool();
7574+
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
7575+
const FileDescriptor* file = BuildFile(R"pb(
7576+
name: "foo.proto"
7577+
syntax: "editions"
7578+
edition: "2023"
7579+
dependency: "google/protobuf/unittest_features.proto"
7580+
)pb");
7581+
ASSERT_NE(file, nullptr);
7582+
7583+
EXPECT_THAT(file->options(), EqualsProto(""));
7584+
EXPECT_THAT(
7585+
GetFeatures(file), EqualsProto(R"pb(
7586+
field_presence: EXPLICIT
7587+
enum_type: OPEN
7588+
repeated_field_encoding: PACKED
7589+
message_encoding: LENGTH_PREFIXED
7590+
json_format: ALLOW
7591+
[pb.cpp] { legacy_closed_enum: false utf8_validation: VERIFY_PARSE }
7592+
)pb"));
7593+
EXPECT_FALSE(GetFeatures(file).HasExtension(pb::test));
7594+
}
7595+
75537596
TEST_F(FeaturesTest, ClearsOptions) {
75547597
BuildDescriptorMessagesInTestPool();
75557598
const FileDescriptor* file = BuildFile(R"pb(
@@ -7842,9 +7885,8 @@ TEST_F(FeaturesTest, InvalidEdition) {
78427885
R"pb(
78437886
name: "foo.proto" syntax: "editions" edition: "2022"
78447887
)pb",
7845-
"foo.proto: foo.proto: EDITIONS: No valid default found for edition 2022 "
7846-
"in "
7847-
"feature field google.protobuf.FeatureSet.field_presence\n");
7888+
"foo.proto: foo.proto: EDITIONS: Edition 2022 is earlier than the "
7889+
"minimum supported edition 2023\n");
78487890
}
78497891

78507892
TEST_F(FeaturesTest, FileFeatures) {
@@ -9110,48 +9152,6 @@ TEST_F(FeaturesTest, FeaturesOutsideEditions) {
91109152
"editions.\n");
91119153
}
91129154

9113-
TEST_F(FeaturesTest, InvalidExtensionNonMessage) {
9114-
BuildDescriptorMessagesInTestPool();
9115-
ASSERT_NE(BuildFile(R"pb(
9116-
name: "unittest_invalid_features.proto"
9117-
syntax: "proto2"
9118-
package: "pb"
9119-
dependency: "google/protobuf/descriptor.proto"
9120-
message_type {
9121-
name: "TestInvalid"
9122-
extension {
9123-
name: "scalar_extension"
9124-
number: 9996
9125-
label: LABEL_OPTIONAL
9126-
type: TYPE_STRING
9127-
extendee: ".google.protobuf.FeatureSet"
9128-
}
9129-
}
9130-
)pb"),
9131-
nullptr);
9132-
BuildFileWithErrors(
9133-
R"pb(
9134-
name: "foo.proto"
9135-
syntax: "editions"
9136-
edition: "2023"
9137-
dependency: "unittest_invalid_features.proto"
9138-
options {
9139-
uninterpreted_option {
9140-
name { name_part: "features" is_extension: false }
9141-
name {
9142-
name_part: "pb.TestInvalid.scalar_extension"
9143-
is_extension: true
9144-
}
9145-
identifier_value: "hello"
9146-
}
9147-
}
9148-
)pb",
9149-
"foo.proto: unittest_invalid_features.proto: EDITIONS: FeatureSet "
9150-
"extension pb.TestInvalid.scalar_extension is not of message type. "
9151-
"Feature extensions should always use messages to allow for "
9152-
"evolution.\n");
9153-
}
9154-
91559155
TEST_F(FeaturesTest, InvalidFieldImplicitDefault) {
91569156
BuildDescriptorMessagesInTestPool();
91579157
BuildFileWithErrors(

0 commit comments

Comments
 (0)