Implement feature lifetime validation in protoc and the C++ runtime.

Features need to be validated within the pool being built, since the generated pool only contains extensions linked into the binary (e.g. protoc or a runtime building dynamic protos).  The generated pool may be missing extensions used in this proto or it may have version skew.  Moving to the build pool requires reflective parsing, which in general can't be done from inside the pool's database lock.  This required some refactoring to add a post-build validation phase outside of the lock.

For now, the feature support spec is optional and the checks only are only applied when it's present.  Follow-up changes will add these specs to our existing features and then require them for all FeatureSet extensions.

PiperOrigin-RevId: 623630219
pull/16448/head
Mike Kruskal 11 months ago committed by Copybara-Service
parent b267cd4e16
commit b3b4497d61
  1. 8
      python/google/protobuf/internal/descriptor_test.py
  2. 73
      src/google/protobuf/compiler/command_line_interface_unittest.cc
  3. 258
      src/google/protobuf/descriptor.cc
  4. 18
      src/google/protobuf/descriptor.h
  5. 173
      src/google/protobuf/descriptor_unittest.cc
  6. 24
      src/google/protobuf/editions/defaults_test.cc
  7. 166
      src/google/protobuf/feature_resolver.cc
  8. 18
      src/google/protobuf/feature_resolver.h
  9. 428
      src/google/protobuf/feature_resolver_test.cc
  10. 75
      src/google/protobuf/unittest_features.proto

@ -1450,6 +1450,7 @@ class FeatureInheritanceTest(unittest.TestCase):
ret = ReturnObject() ret = ReturnObject()
ret.pool = descriptor_pool.DescriptorPool() ret.pool = descriptor_pool.DescriptorPool()
defaults = descriptor_pb2.FeatureSetDefaults( defaults = descriptor_pb2.FeatureSetDefaults(
defaults=[ defaults=[
descriptor_pb2.FeatureSetDefaults.FeatureSetEditionDefault( descriptor_pb2.FeatureSetDefaults.FeatureSetEditionDefault(
@ -1465,6 +1466,13 @@ class FeatureInheritanceTest(unittest.TestCase):
].multiple_feature = 1 ].multiple_feature = 1
ret.pool.SetFeatureSetDefaults(defaults) ret.pool.SetFeatureSetDefaults(defaults)
# Add dependencies
file = descriptor_pb2.FileDescriptorProto()
descriptor_pb2.DESCRIPTOR.CopyToProto(file)
ret.pool.Add(file)
unittest_features_pb2.DESCRIPTOR.CopyToProto(file)
ret.pool.Add(file)
ret.file = ret.pool.AddSerializedFile(self.file_proto.SerializeToString()) ret.file = ret.pool.AddSerializedFile(self.file_proto.SerializeToString())
ret.top_message = ret.pool.FindMessageTypeByName( ret.top_message = ret.pool.FindMessageTypeByName(
'protobuf_unittest.TopMessage' 'protobuf_unittest.TopMessage'

@ -1761,6 +1761,63 @@ TEST_F(CommandLineInterfaceTest, Plugin_SourceFeatures) {
} }
} }
TEST_F(CommandLineInterfaceTest, GeneratorFeatureLifetimeError) {
CreateTempFile("google/protobuf/descriptor.proto",
google::protobuf::DescriptorProto::descriptor()->file()->DebugString());
CreateTempFile("google/protobuf/unittest_features.proto",
pb::TestFeatures::descriptor()->file()->DebugString());
CreateTempFile("foo.proto",
R"schema(
edition = "2024";
import "google/protobuf/unittest_features.proto";
package foo;
message Foo {
int32 b = 1 [
features.(pb.test).removed_feature = VALUE6
];
}
)schema");
Run("protocol_compiler --experimental_editions --proto_path=$tmpdir "
"--test_out=$tmpdir foo.proto");
ExpectErrorSubstring(
"foo.proto:6:13: Feature pb.TestFeatures.removed_feature has been "
"removed in edition 2024");
}
TEST_F(CommandLineInterfaceTest, PluginFeatureLifetimeError) {
CreateTempFile("google/protobuf/descriptor.proto",
google::protobuf::DescriptorProto::descriptor()->file()->DebugString());
CreateTempFile("google/protobuf/unittest_features.proto",
pb::TestFeatures::descriptor()->file()->DebugString());
CreateTempFile("foo.proto",
R"schema(
edition = "2023";
import "google/protobuf/unittest_features.proto";
package foo;
message Foo {
int32 b = 1 [
features.(pb.test).future_feature = VALUE6
];
}
)schema");
#ifdef GOOGLE_PROTOBUF_FAKE_PLUGIN_PATH
std::string plugin_path = GOOGLE_PROTOBUF_FAKE_PLUGIN_PATH;
#else
std::string plugin_path = absl::StrCat(
TestUtil::TestSourceDir(), "/google/protobuf/compiler/fake_plugin");
#endif
Run(absl::StrCat(
"protocol_compiler --fake_plugin_out=$tmpdir --proto_path=$tmpdir "
"foo.proto --plugin=prefix-gen-fake_plugin=",
plugin_path));
ExpectErrorSubstring(
"foo.proto:6:13: Feature pb.TestFeatures.future_feature wasn't "
"introduced until edition 2024");
}
TEST_F(CommandLineInterfaceTest, GeneratorNoEditionsSupport) { TEST_F(CommandLineInterfaceTest, GeneratorNoEditionsSupport) {
CreateTempFile("foo.proto", R"schema( CreateTempFile("foo.proto", R"schema(
edition = "2023"; edition = "2023";
@ -1958,26 +2015,26 @@ TEST_F(CommandLineInterfaceTest, EditionDefaultsWithExtension) {
FeatureSetDefaults defaults = ReadEditionDefaults("defaults"); FeatureSetDefaults defaults = ReadEditionDefaults("defaults");
EXPECT_EQ(defaults.minimum_edition(), EDITION_PROTO2); EXPECT_EQ(defaults.minimum_edition(), EDITION_PROTO2);
EXPECT_EQ(defaults.maximum_edition(), EDITION_99999_TEST_ONLY); EXPECT_EQ(defaults.maximum_edition(), EDITION_99999_TEST_ONLY);
ASSERT_EQ(defaults.defaults_size(), 5); ASSERT_EQ(defaults.defaults_size(), 6);
EXPECT_EQ(defaults.defaults(0).edition(), EDITION_PROTO2); EXPECT_EQ(defaults.defaults(0).edition(), EDITION_PROTO2);
EXPECT_EQ(defaults.defaults(1).edition(), EDITION_PROTO3);
EXPECT_EQ(defaults.defaults(2).edition(), EDITION_2023); EXPECT_EQ(defaults.defaults(2).edition(), EDITION_2023);
EXPECT_EQ(defaults.defaults(3).edition(), EDITION_99997_TEST_ONLY); EXPECT_EQ(defaults.defaults(3).edition(), EDITION_2024);
EXPECT_EQ(defaults.defaults(4).edition(), EDITION_99998_TEST_ONLY); EXPECT_EQ(defaults.defaults(4).edition(), EDITION_99997_TEST_ONLY);
EXPECT_EQ(defaults.defaults(5).edition(), EDITION_99998_TEST_ONLY);
EXPECT_EQ( EXPECT_EQ(
defaults.defaults(0).features().GetExtension(pb::test).file_feature(), defaults.defaults(0).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE1); pb::EnumFeature::VALUE1);
EXPECT_EQ(
defaults.defaults(1).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE2);
EXPECT_EQ( EXPECT_EQ(
defaults.defaults(2).features().GetExtension(pb::test).file_feature(), defaults.defaults(2).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE3); pb::EnumFeature::VALUE3);
EXPECT_EQ( EXPECT_EQ(
defaults.defaults(3).features().GetExtension(pb::test).file_feature(), defaults.defaults(3).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE4); pb::EnumFeature::VALUE3);
EXPECT_EQ( EXPECT_EQ(
defaults.defaults(4).features().GetExtension(pb::test).file_feature(), defaults.defaults(4).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE4);
EXPECT_EQ(
defaults.defaults(5).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE5); pb::EnumFeature::VALUE5);
} }

@ -74,6 +74,7 @@
#include "google/protobuf/generated_message_util.h" #include "google/protobuf/generated_message_util.h"
#include "google/protobuf/io/strtod.h" #include "google/protobuf/io/strtod.h"
#include "google/protobuf/io/tokenizer.h" #include "google/protobuf/io/tokenizer.h"
#include "google/protobuf/message.h"
#include "google/protobuf/message_lite.h" #include "google/protobuf/message_lite.h"
#include "google/protobuf/parse_context.h" #include "google/protobuf/parse_context.h"
#include "google/protobuf/port.h" #include "google/protobuf/port.h"
@ -1122,6 +1123,22 @@ bool HasFeatures(const OptionsT& options) {
return false; return false;
} }
template <typename DescriptorT>
absl::string_view GetFullName(const DescriptorT& desc) {
return desc.full_name();
}
absl::string_view GetFullName(const FileDescriptor& desc) {
return desc.name();
}
template <typename DescriptorT>
const FileDescriptor* GetFile(const DescriptorT& desc) {
return desc.file();
}
const FileDescriptor* GetFile(const FileDescriptor& desc) { return &desc; }
const FeatureSet& GetParentFeatures(const FileDescriptor* file) { const FeatureSet& GetParentFeatures(const FileDescriptor* file) {
return FeatureSet::default_instance(); return FeatureSet::default_instance();
} }
@ -1305,6 +1322,100 @@ class FlatAllocator
} // namespace internal } // namespace internal
// ===================================================================
// DescriptorPool::DeferredValidation
// This class stores information required to defer validation until we're
// outside the mutex lock. These are reflective checks that also require us to
// acquire the lock.
class DescriptorPool::DeferredValidation {
public:
DeferredValidation(const DescriptorPool* pool,
ErrorCollector* error_collector)
: pool_(pool), error_collector_(error_collector) {}
explicit DeferredValidation(const DescriptorPool* pool)
: pool_(pool), error_collector_(pool->default_error_collector_) {}
DeferredValidation(const DeferredValidation&) = delete;
DeferredValidation& operator=(const DeferredValidation&) = delete;
DeferredValidation(DeferredValidation&&) = delete;
DeferredValidation& operator=(DeferredValidation&&) = delete;
~DeferredValidation() {
ABSL_CHECK(lifetimes_info_map_.empty())
<< "DeferredValidation destroyed with unvalidated features";
}
struct LifetimesInfo {
const FeatureSet* proto_features;
const Message* proto;
absl::string_view full_name;
absl::string_view filename;
};
void ValidateFeatureLifetimes(const FileDescriptor* file,
LifetimesInfo info) {
lifetimes_info_map_[file].emplace_back(std::move(info));
}
// Create a new file proto with an extended lifetime for deferred error
// reporting. If any temporary file protos don't outlive this object, the
// reported errors won't be able to safely reference a location in the
// original proto file.
FileDescriptorProto& CreateProto() {
owned_protos_.push_back(Arena::Create<FileDescriptorProto>(&arena_));
return *owned_protos_.back();
}
bool Validate() {
if (lifetimes_info_map_.empty()) return true;
static absl::string_view feature_set_name = "google.protobuf.FeatureSet";
const Descriptor* feature_set =
pool_->FindMessageTypeByName(feature_set_name);
bool has_errors = false;
for (const auto& it : lifetimes_info_map_) {
const FileDescriptor* file = it.first;
for (const auto& info : it.second) {
auto results = FeatureResolver::ValidateFeatureLifetimes(
file->edition(), *info.proto_features, feature_set);
for (const auto& error : results.errors) {
has_errors = true;
if (error_collector_ == nullptr) {
ABSL_LOG(ERROR)
<< info.filename << " " << info.full_name << ": " << error;
} else {
error_collector_->RecordError(
info.filename, info.full_name, info.proto,
DescriptorPool::ErrorCollector::NAME, error);
}
}
for (const auto& warning : results.warnings) {
if (error_collector_ == nullptr) {
ABSL_LOG(WARNING)
<< info.filename << " " << info.full_name << ": " << warning;
} else {
error_collector_->RecordWarning(
info.filename, info.full_name, info.proto,
DescriptorPool::ErrorCollector::NAME, warning);
}
}
}
}
lifetimes_info_map_.clear();
return !has_errors;
}
private:
Arena arena_;
const DescriptorPool* pool_;
ErrorCollector* error_collector_;
absl::flat_hash_map<const FileDescriptor*, std::vector<LifetimesInfo>>
lifetimes_info_map_;
std::vector<FileDescriptorProto*> owned_protos_;
};
// =================================================================== // ===================================================================
// DescriptorPool::Tables // DescriptorPool::Tables
@ -1591,25 +1702,33 @@ Symbol DescriptorPool::Tables::FindByNameHelper(const DescriptorPool* pool,
if (!result.IsNull()) return result; if (!result.IsNull()) return result;
} }
} }
DescriptorPool::DeferredValidation deferred_validation(pool);
Symbol result;
{
absl::MutexLockMaybe lock(pool->mutex_); absl::MutexLockMaybe lock(pool->mutex_);
if (pool->fallback_database_ != nullptr) { if (pool->fallback_database_ != nullptr) {
known_bad_symbols_.clear(); known_bad_symbols_.clear();
known_bad_files_.clear(); known_bad_files_.clear();
} }
Symbol result = FindSymbol(name); result = FindSymbol(name);
if (result.IsNull() && pool->underlay_ != nullptr) { if (result.IsNull() && pool->underlay_ != nullptr) {
// Symbol not found; check the underlay. // Symbol not found; check the underlay.
result = pool->underlay_->tables_->FindByNameHelper(pool->underlay_, name); result =
pool->underlay_->tables_->FindByNameHelper(pool->underlay_, name);
} }
if (result.IsNull()) { if (result.IsNull()) {
// Symbol still not found, so check fallback database. // Symbol still not found, so check fallback database.
if (pool->TryFindSymbolInFallbackDatabase(name)) { if (pool->TryFindSymbolInFallbackDatabase(name, deferred_validation)) {
result = FindSymbol(name); result = FindSymbol(name);
} }
} }
}
if (!deferred_validation.Validate()) {
return Symbol();
}
return result; return result;
} }
@ -2133,26 +2252,35 @@ void DescriptorPool::InternalAddGeneratedFile(
const FileDescriptor* DescriptorPool::FindFileByName( const FileDescriptor* DescriptorPool::FindFileByName(
absl::string_view name) const { absl::string_view name) const {
DeferredValidation deferred_validation(this);
const FileDescriptor* result = nullptr;
{
absl::MutexLockMaybe lock(mutex_); absl::MutexLockMaybe lock(mutex_);
if (fallback_database_ != nullptr) { if (fallback_database_ != nullptr) {
tables_->known_bad_symbols_.clear(); tables_->known_bad_symbols_.clear();
tables_->known_bad_files_.clear(); tables_->known_bad_files_.clear();
} }
const FileDescriptor* result = tables_->FindFile(name); result = tables_->FindFile(name);
if (result != nullptr) return result; if (result != nullptr) return result;
if (underlay_ != nullptr) { if (underlay_ != nullptr) {
result = underlay_->FindFileByName(name); result = underlay_->FindFileByName(name);
if (result != nullptr) return result; if (result != nullptr) return result;
} }
if (TryFindFileInFallbackDatabase(name)) { if (TryFindFileInFallbackDatabase(name, deferred_validation)) {
result = tables_->FindFile(name); result = tables_->FindFile(name);
if (result != nullptr) return result;
} }
}
if (!deferred_validation.Validate()) {
return nullptr; return nullptr;
}
return result;
} }
const FileDescriptor* DescriptorPool::FindFileContainingSymbol( const FileDescriptor* DescriptorPool::FindFileContainingSymbol(
absl::string_view symbol_name) const { absl::string_view symbol_name) const {
const FileDescriptor* file_result = nullptr;
DeferredValidation deferred_validation(this);
{
absl::MutexLockMaybe lock(mutex_); absl::MutexLockMaybe lock(mutex_);
if (fallback_database_ != nullptr) { if (fallback_database_ != nullptr) {
tables_->known_bad_symbols_.clear(); tables_->known_bad_symbols_.clear();
@ -2161,15 +2289,18 @@ const FileDescriptor* DescriptorPool::FindFileContainingSymbol(
Symbol result = tables_->FindSymbol(symbol_name); Symbol result = tables_->FindSymbol(symbol_name);
if (!result.IsNull()) return result.GetFile(); if (!result.IsNull()) return result.GetFile();
if (underlay_ != nullptr) { if (underlay_ != nullptr) {
const FileDescriptor* file_result = file_result = underlay_->FindFileContainingSymbol(symbol_name);
underlay_->FindFileContainingSymbol(symbol_name);
if (file_result != nullptr) return file_result; if (file_result != nullptr) return file_result;
} }
if (TryFindSymbolInFallbackDatabase(symbol_name)) { if (TryFindSymbolInFallbackDatabase(symbol_name, deferred_validation)) {
result = tables_->FindSymbol(symbol_name); result = tables_->FindSymbol(symbol_name);
if (!result.IsNull()) return result.GetFile(); if (!result.IsNull()) file_result = result.GetFile();
}
} }
if (!deferred_validation.Validate()) {
return nullptr; return nullptr;
}
return file_result;
} }
const Descriptor* DescriptorPool::FindMessageTypeByName( const Descriptor* DescriptorPool::FindMessageTypeByName(
@ -2236,12 +2367,15 @@ const FieldDescriptor* DescriptorPool::FindExtensionByNumber(
return result; return result;
} }
} }
const FieldDescriptor* result = nullptr;
DeferredValidation deferred_validation(this);
{
absl::MutexLockMaybe lock(mutex_); absl::MutexLockMaybe lock(mutex_);
if (fallback_database_ != nullptr) { if (fallback_database_ != nullptr) {
tables_->known_bad_symbols_.clear(); tables_->known_bad_symbols_.clear();
tables_->known_bad_files_.clear(); tables_->known_bad_files_.clear();
} }
const FieldDescriptor* result = tables_->FindExtension(extendee, number); result = tables_->FindExtension(extendee, number);
if (result != nullptr) { if (result != nullptr) {
return result; return result;
} }
@ -2249,13 +2383,15 @@ const FieldDescriptor* DescriptorPool::FindExtensionByNumber(
result = underlay_->FindExtensionByNumber(extendee, number); result = underlay_->FindExtensionByNumber(extendee, number);
if (result != nullptr) return result; if (result != nullptr) return result;
} }
if (TryFindExtensionInFallbackDatabase(extendee, number)) { if (TryFindExtensionInFallbackDatabase(extendee, number,
deferred_validation)) {
result = tables_->FindExtension(extendee, number); result = tables_->FindExtension(extendee, number);
if (result != nullptr) {
return result;
} }
} }
if (!deferred_validation.Validate()) {
return nullptr; return nullptr;
}
return result;
} }
const FieldDescriptor* DescriptorPool::InternalFindExtensionByNumberNoLock( const FieldDescriptor* DescriptorPool::InternalFindExtensionByNumberNoLock(
@ -2305,6 +2441,9 @@ const FieldDescriptor* DescriptorPool::FindExtensionByPrintableName(
void DescriptorPool::FindAllExtensions( void DescriptorPool::FindAllExtensions(
const Descriptor* extendee, const Descriptor* extendee,
std::vector<const FieldDescriptor*>* out) const { std::vector<const FieldDescriptor*>* out) const {
DeferredValidation deferred_validation(this);
std::vector<const FieldDescriptor*> extensions;
{
absl::MutexLockMaybe lock(mutex_); absl::MutexLockMaybe lock(mutex_);
if (fallback_database_ != nullptr) { if (fallback_database_ != nullptr) {
tables_->known_bad_symbols_.clear(); tables_->known_bad_symbols_.clear();
@ -2320,16 +2459,21 @@ void DescriptorPool::FindAllExtensions(
&numbers)) { &numbers)) {
for (int number : numbers) { for (int number : numbers) {
if (tables_->FindExtension(extendee, number) == nullptr) { if (tables_->FindExtension(extendee, number) == nullptr) {
TryFindExtensionInFallbackDatabase(extendee, number); TryFindExtensionInFallbackDatabase(extendee, number,
deferred_validation);
} }
} }
tables_->extensions_loaded_from_db_.insert(extendee); tables_->extensions_loaded_from_db_.insert(extendee);
} }
} }
tables_->FindAllExtensions(extendee, out); tables_->FindAllExtensions(extendee, &extensions);
if (underlay_ != nullptr) { if (underlay_ != nullptr) {
underlay_->FindAllExtensions(extendee, out); underlay_->FindAllExtensions(extendee, &extensions);
}
}
if (deferred_validation.Validate()) {
out->insert(out->end(), extensions.begin(), extensions.end());
} }
} }
@ -2551,7 +2695,7 @@ EnumDescriptor::FindReservedRangeContainingNumber(int number) const {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
bool DescriptorPool::TryFindFileInFallbackDatabase( bool DescriptorPool::TryFindFileInFallbackDatabase(
absl::string_view name) const { absl::string_view name, DeferredValidation& deferred_validation) const {
if (fallback_database_ == nullptr) return false; if (fallback_database_ == nullptr) return false;
if (tables_->known_bad_files_.contains(name)) return false; if (tables_->known_bad_files_.contains(name)) return false;
@ -2563,9 +2707,9 @@ bool DescriptorPool::TryFindFileInFallbackDatabase(
return database.FindFileByName(std::string(filename), &output); return database.FindFileByName(std::string(filename), &output);
}; };
auto file_proto = absl::make_unique<FileDescriptorProto>(); auto& file_proto = deferred_validation.CreateProto();
if (!find_file(*fallback_database_, name, *file_proto) || if (!find_file(*fallback_database_, name, file_proto) ||
BuildFileFromDatabase(*file_proto) == nullptr) { BuildFileFromDatabase(file_proto, deferred_validation) == nullptr) {
tables_->known_bad_files_.emplace(name); tables_->known_bad_files_.emplace(name);
return false; return false;
} }
@ -2594,13 +2738,13 @@ bool DescriptorPool::IsSubSymbolOfBuiltType(absl::string_view name) const {
} }
bool DescriptorPool::TryFindSymbolInFallbackDatabase( bool DescriptorPool::TryFindSymbolInFallbackDatabase(
absl::string_view name) const { absl::string_view name, DeferredValidation& deferred_validation) const {
if (fallback_database_ == nullptr) return false; if (fallback_database_ == nullptr) return false;
if (tables_->known_bad_symbols_.contains(name)) return false; if (tables_->known_bad_symbols_.contains(name)) return false;
std::string name_string(name); std::string name_string(name);
auto file_proto = absl::make_unique<FileDescriptorProto>(); auto& file_proto = deferred_validation.CreateProto();
if ( // We skip looking in the fallback database if the name is a sub-symbol if ( // We skip looking in the fallback database if the name is a sub-symbol
// of any descriptor that already exists in the descriptor pool (except // of any descriptor that already exists in the descriptor pool (except
// for package descriptors). This is valid because all symbols except // for package descriptors). This is valid because all symbols except
@ -2620,16 +2764,15 @@ bool DescriptorPool::TryFindSymbolInFallbackDatabase(
IsSubSymbolOfBuiltType(name) IsSubSymbolOfBuiltType(name)
// Look up file containing this symbol in fallback database. // Look up file containing this symbol in fallback database.
|| !fallback_database_->FindFileContainingSymbol(name_string, || !fallback_database_->FindFileContainingSymbol(name_string, &file_proto)
file_proto.get())
// Check if we've already built this file. If so, it apparently doesn't // Check if we've already built this file. If so, it apparently doesn't
// contain the symbol we're looking for. Some DescriptorDatabases // contain the symbol we're looking for. Some DescriptorDatabases
// return false positives. // return false positives.
|| tables_->FindFile(file_proto->name()) != nullptr || tables_->FindFile(file_proto.name()) != nullptr
// Build the file. // Build the file.
|| BuildFileFromDatabase(*file_proto) == nullptr) { || BuildFileFromDatabase(file_proto, deferred_validation) == nullptr) {
tables_->known_bad_symbols_.insert(std::move(name_string)); tables_->known_bad_symbols_.insert(std::move(name_string));
return false; return false;
} }
@ -2638,23 +2781,24 @@ bool DescriptorPool::TryFindSymbolInFallbackDatabase(
} }
bool DescriptorPool::TryFindExtensionInFallbackDatabase( bool DescriptorPool::TryFindExtensionInFallbackDatabase(
const Descriptor* containing_type, int field_number) const { const Descriptor* containing_type, int field_number,
DeferredValidation& deferred_validation) const {
if (fallback_database_ == nullptr) return false; if (fallback_database_ == nullptr) return false;
auto file_proto = absl::make_unique<FileDescriptorProto>(); auto& file_proto = deferred_validation.CreateProto();
if (!fallback_database_->FindFileContainingExtension( if (!fallback_database_->FindFileContainingExtension(
containing_type->full_name(), field_number, file_proto.get())) { containing_type->full_name(), field_number, &file_proto)) {
return false; return false;
} }
if (tables_->FindFile(file_proto->name()) != nullptr) { if (tables_->FindFile(file_proto.name()) != nullptr) {
// We've already loaded this file, and it apparently doesn't contain the // We've already loaded this file, and it apparently doesn't contain the
// extension we're looking for. Some DescriptorDatabases return false // extension we're looking for. Some DescriptorDatabases return false
// positives. // positives.
return false; return false;
} }
if (BuildFileFromDatabase(*file_proto) == nullptr) { if (BuildFileFromDatabase(file_proto, deferred_validation) == nullptr) {
return false; return false;
} }
@ -3976,9 +4120,10 @@ class DescriptorBuilder {
public: public:
static std::unique_ptr<DescriptorBuilder> New( static std::unique_ptr<DescriptorBuilder> New(
const DescriptorPool* pool, DescriptorPool::Tables* tables, const DescriptorPool* pool, DescriptorPool::Tables* tables,
DescriptorPool::DeferredValidation& deferred_validation,
DescriptorPool::ErrorCollector* error_collector) { DescriptorPool::ErrorCollector* error_collector) {
return std::unique_ptr<DescriptorBuilder>( return std::unique_ptr<DescriptorBuilder>(new DescriptorBuilder(
new DescriptorBuilder(pool, tables, error_collector)); pool, tables, deferred_validation, error_collector));
} }
~DescriptorBuilder(); ~DescriptorBuilder();
@ -3987,6 +4132,7 @@ class DescriptorBuilder {
private: private:
DescriptorBuilder(const DescriptorPool* pool, DescriptorPool::Tables* tables, DescriptorBuilder(const DescriptorPool* pool, DescriptorPool::Tables* tables,
DescriptorPool::DeferredValidation& deferred_validation,
DescriptorPool::ErrorCollector* error_collector); DescriptorPool::ErrorCollector* error_collector);
friend class OptionInterpreter; friend class OptionInterpreter;
@ -3997,6 +4143,7 @@ class DescriptorBuilder {
const DescriptorPool* pool_; const DescriptorPool* pool_;
DescriptorPool::Tables* tables_; // for convenience DescriptorPool::Tables* tables_; // for convenience
DescriptorPool::DeferredValidation& deferred_validation_;
DescriptorPool::ErrorCollector* error_collector_; DescriptorPool::ErrorCollector* error_collector_;
absl::optional<FeatureResolver> feature_resolver_ = absl::nullopt; absl::optional<FeatureResolver> feature_resolver_ = absl::nullopt;
@ -4541,12 +4688,20 @@ const FileDescriptor* DescriptorPool::BuildFileCollectingErrors(
tables_->known_bad_symbols_.clear(); tables_->known_bad_symbols_.clear();
tables_->known_bad_files_.clear(); tables_->known_bad_files_.clear();
build_started_ = true; build_started_ = true;
return DescriptorBuilder::New(this, tables_.get(), error_collector) DeferredValidation deferred_validation(this, error_collector);
const FileDescriptor* file =
DescriptorBuilder::New(this, tables_.get(), deferred_validation,
error_collector)
->BuildFile(proto); ->BuildFile(proto);
if (deferred_validation.Validate()) {
return file;
}
return nullptr;
} }
const FileDescriptor* DescriptorPool::BuildFileFromDatabase( const FileDescriptor* DescriptorPool::BuildFileFromDatabase(
const FileDescriptorProto& proto) const { const FileDescriptorProto& proto,
DeferredValidation& deferred_validation) const {
mutex_->AssertHeld(); mutex_->AssertHeld();
build_started_ = true; build_started_ = true;
if (tables_->known_bad_files_.contains(proto.name())) { if (tables_->known_bad_files_.contains(proto.name())) {
@ -4554,8 +4709,8 @@ const FileDescriptor* DescriptorPool::BuildFileFromDatabase(
} }
const FileDescriptor* result; const FileDescriptor* result;
const auto build_file = [&] { const auto build_file = [&] {
result = result = DescriptorBuilder::New(this, tables_.get(), deferred_validation,
DescriptorBuilder::New(this, tables_.get(), default_error_collector_) default_error_collector_)
->BuildFile(proto); ->BuildFile(proto);
}; };
if (dispatcher_ != nullptr) { if (dispatcher_ != nullptr) {
@ -4601,9 +4756,11 @@ absl::Status DescriptorPool::SetFeatureSetDefaults(FeatureSetDefaults spec) {
DescriptorBuilder::DescriptorBuilder( DescriptorBuilder::DescriptorBuilder(
const DescriptorPool* pool, DescriptorPool::Tables* tables, const DescriptorPool* pool, DescriptorPool::Tables* tables,
DescriptorPool::DeferredValidation& deferred_validation,
DescriptorPool::ErrorCollector* error_collector) DescriptorPool::ErrorCollector* error_collector)
: pool_(pool), : pool_(pool),
tables_(tables), tables_(tables),
deferred_validation_(deferred_validation),
error_collector_(error_collector), error_collector_(error_collector),
had_errors_(false), had_errors_(false),
possible_undeclared_dependency_(nullptr), possible_undeclared_dependency_(nullptr),
@ -4739,7 +4896,8 @@ Symbol DescriptorBuilder::FindSymbolNotEnforcingDepsHelper(
// to build the file containing the symbol, and build_it will be set. // to build the file containing the symbol, and build_it will be set.
// Also, build_it will be true when !lazily_build_dependencies_, to provide // Also, build_it will be true when !lazily_build_dependencies_, to provide
// better error reporting of missing dependencies. // better error reporting of missing dependencies.
if (build_it && pool->TryFindSymbolInFallbackDatabase(name)) { if (build_it &&
pool->TryFindSymbolInFallbackDatabase(name, deferred_validation_)) {
result = pool->tables_->FindSymbol(name); result = pool->tables_->FindSymbol(name);
} }
} }
@ -5637,7 +5795,8 @@ const FileDescriptor* DescriptorBuilder::BuildFile(
pool_->underlay_->FindFileByName(proto.dependency(i)) == pool_->underlay_->FindFileByName(proto.dependency(i)) ==
nullptr)) { nullptr)) {
// We don't care what this returns since we'll find out below anyway. // We don't care what this returns since we'll find out below anyway.
pool_->TryFindFileInFallbackDatabase(proto.dependency(i)); pool_->TryFindFileInFallbackDatabase(proto.dependency(i),
deferred_validation_);
} }
} }
tables_->pending_files_.pop_back(); tables_->pending_files_.pop_back();
@ -5952,9 +6111,9 @@ FileDescriptor* DescriptorBuilder::BuildFileImpl(
// Validate options. See comments at InternalSetLazilyBuildDependencies about // Validate options. See comments at InternalSetLazilyBuildDependencies about
// error checking and lazy import building. // error checking and lazy import building.
if (!had_errors_ && !pool_->lazily_build_dependencies_) { if (!had_errors_ && !pool_->lazily_build_dependencies_) {
internal::VisitDescriptors(*result, proto, internal::VisitDescriptors(
[&](const auto& descriptor, const auto& proto) { *result, proto, [&](const auto& descriptor, const auto& desc_proto) {
ValidateOptions(&descriptor, proto); ValidateOptions(&descriptor, desc_proto);
}); });
} }
@ -5975,6 +6134,19 @@ FileDescriptor* DescriptorBuilder::BuildFileImpl(
LogUnusedDependency(proto, result); LogUnusedDependency(proto, result);
} }
// Store feature information for deferred validation outside of the database
// mutex.
if (!had_errors_ && !pool_->lazily_build_dependencies_) {
internal::VisitDescriptors(
*result, proto, [&](const auto& descriptor, const auto& desc_proto) {
if (descriptor.proto_features_ != &FeatureSet::default_instance()) {
deferred_validation_.ValidateFeatureLifetimes(
GetFile(descriptor), {descriptor.proto_features_, &desc_proto,
GetFullName(descriptor), proto.name()});
}
});
}
if (had_errors_) { if (had_errors_) {
return nullptr; return nullptr;
} else { } else {

@ -2366,10 +2366,17 @@ class PROTOBUF_EXPORT DescriptorPool {
// corresponding proto file. Returns true if successful, in which case // corresponding proto file. Returns true if successful, in which case
// the caller should search for the thing again. These are declared // the caller should search for the thing again. These are declared
// const because they are called by (semantically) const methods. // const because they are called by (semantically) const methods.
bool TryFindFileInFallbackDatabase(absl::string_view name) const; // DeferredValidation stores temporary information necessary to run validation
bool TryFindSymbolInFallbackDatabase(absl::string_view name) const; // checks that can't be done inside the database lock. This is generally
bool TryFindExtensionInFallbackDatabase(const Descriptor* containing_type, // reflective operations that also require the lock to do safely.
int field_number) const; class DeferredValidation;
bool TryFindFileInFallbackDatabase(
absl::string_view name, DeferredValidation& deferred_validation) const;
bool TryFindSymbolInFallbackDatabase(
absl::string_view name, DeferredValidation& deferred_validation) const;
bool TryFindExtensionInFallbackDatabase(
const Descriptor* containing_type, int field_number,
DeferredValidation& deferred_validation) const;
// This internal find extension method only check with its table and underlay // This internal find extension method only check with its table and underlay
// descriptor_pool's table. It does not check with fallback DB and no // descriptor_pool's table. It does not check with fallback DB and no
@ -2381,7 +2388,8 @@ class PROTOBUF_EXPORT DescriptorPool {
// fallback_database_. Declared const because it is called by (semantically) // fallback_database_. Declared const because it is called by (semantically)
// const methods. // const methods.
const FileDescriptor* BuildFileFromDatabase( const FileDescriptor* BuildFileFromDatabase(
const FileDescriptorProto& proto) const; const FileDescriptorProto& proto,
DeferredValidation& deferred_validation) const;
// Helper for when lazily_build_dependencies_ is set, can look up a symbol // Helper for when lazily_build_dependencies_ is set, can look up a symbol
// after the file's descriptor is built, and can build the file where that // after the file's descriptor is built, and can build the file where that

@ -8916,10 +8916,12 @@ TEST_F(FeaturesTest, EnumValueFeaturesDefault) {
TEST_F(FeaturesTest, EnumValueFeaturesInherit) { TEST_F(FeaturesTest, EnumValueFeaturesInherit) {
BuildDescriptorMessagesInTestPool(); BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
const FileDescriptor* file = BuildFile(R"pb( const FileDescriptor* file = BuildFile(R"pb(
name: "foo.proto" name: "foo.proto"
syntax: "editions" syntax: "editions"
edition: EDITION_2023 edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
options { features { enum_type: CLOSED } } options { features { enum_type: CLOSED } }
enum_type { enum_type {
name: "Foo" name: "Foo"
@ -9010,10 +9012,12 @@ TEST_F(FeaturesTest, OneofFeaturesDefault) {
TEST_F(FeaturesTest, OneofFeaturesInherit) { TEST_F(FeaturesTest, OneofFeaturesInherit) {
BuildDescriptorMessagesInTestPool(); BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
const FileDescriptor* file = BuildFile(R"pb( const FileDescriptor* file = BuildFile(R"pb(
name: "foo.proto" name: "foo.proto"
syntax: "editions" syntax: "editions"
edition: EDITION_2023 edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
options { features { enum_type: CLOSED } } options { features { enum_type: CLOSED } }
message_type { message_type {
name: "Foo" name: "Foo"
@ -9113,10 +9117,12 @@ TEST_F(FeaturesTest, ExtensionRangeFeaturesDefault) {
TEST_F(FeaturesTest, ExtensionRangeFeaturesInherit) { TEST_F(FeaturesTest, ExtensionRangeFeaturesInherit) {
BuildDescriptorMessagesInTestPool(); BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
const FileDescriptor* file = BuildFile(R"pb( const FileDescriptor* file = BuildFile(R"pb(
name: "foo.proto" name: "foo.proto"
syntax: "editions" syntax: "editions"
edition: EDITION_2023 edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
options { features { enum_type: CLOSED } } options { features { enum_type: CLOSED } }
message_type { message_type {
name: "Foo" name: "Foo"
@ -10359,6 +10365,93 @@ TEST_F(FeaturesTest, InvalidGroupLabel) {
"behavior.\n"); "behavior.\n");
} }
TEST_F(FeaturesTest, DeprecatedFeature) {
BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
BuildFileWithWarnings(
R"pb(
name: "foo.proto"
syntax: "editions"
edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
options {
features {
[pb.test] { removed_feature: VALUE9 }
}
}
)pb",
"foo.proto: foo.proto: NAME: Feature "
"pb.TestFeatures.removed_feature has been deprecated in edition 2023: "
"Custom feature deprecation warning\n");
const FileDescriptor* file = pool_.FindFileByName("foo.proto");
ASSERT_THAT(file, NotNull());
EXPECT_EQ(GetFeatures(file).GetExtension(pb::test).removed_feature(),
pb::VALUE9);
}
TEST_F(FeaturesTest, RemovedFeature) {
BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
BuildFileWithErrors(
R"pb(
name: "foo.proto"
syntax: "editions"
edition: EDITION_2024
dependency: "google/protobuf/unittest_features.proto"
options {
features {
[pb.test] { removed_feature: VALUE9 }
}
}
)pb",
"foo.proto: foo.proto: NAME: Feature "
"pb.TestFeatures.removed_feature has been removed in edition 2024\n");
}
TEST_F(FeaturesTest, RemovedFeatureDefault) {
BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
const FileDescriptor* file =
BuildFile(R"pb(
name: "foo.proto" syntax: "editions" edition: EDITION_2024
)pb");
ASSERT_THAT(file, NotNull());
EXPECT_EQ(GetFeatures(file).GetExtension(pb::test).removed_feature(),
pb::VALUE3);
}
TEST_F(FeaturesTest, FutureFeature) {
BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
BuildFileWithErrors(
R"pb(
name: "foo.proto"
syntax: "editions"
edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
options {
features {
[pb.test] { future_feature: VALUE9 }
}
}
)pb",
"foo.proto: foo.proto: NAME: Feature "
"pb.TestFeatures.future_feature wasn't introduced until edition 2024\n");
}
TEST_F(FeaturesTest, FutureFeatureDefault) {
BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
const FileDescriptor* file =
BuildFile(R"pb(
name: "foo.proto" syntax: "editions" edition: EDITION_2023
)pb");
ASSERT_THAT(file, NotNull());
EXPECT_EQ(GetFeatures(file).GetExtension(pb::test).future_feature(),
pb::VALUE1);
}
// Test that the result of FileDescriptor::DebugString() can be used to create // Test that the result of FileDescriptor::DebugString() can be used to create
// the original descriptors. // the original descriptors.
class FeaturesDebugStringTest class FeaturesDebugStringTest
@ -11402,7 +11495,7 @@ TEST_F(ValidationErrorTest, PackageTooLong) {
// DescriptorDatabase // DescriptorDatabase
static void AddToDatabase(SimpleDescriptorDatabase* database, static void AddToDatabase(SimpleDescriptorDatabase* database,
const char* file_text) { absl::string_view file_text) {
FileDescriptorProto file_proto; FileDescriptorProto file_proto;
EXPECT_TRUE(TextFormat::ParseFromString(file_text, &file_proto)); EXPECT_TRUE(TextFormat::ParseFromString(file_text, &file_proto));
database->Add(file_proto); database->Add(file_proto);
@ -11723,6 +11816,84 @@ TEST_F(DatabaseBackedPoolTest, UnittestProto) {
EXPECT_EQ(original_file->DebugString(), file_from_database->DebugString()); EXPECT_EQ(original_file->DebugString(), file_from_database->DebugString());
} }
TEST_F(DatabaseBackedPoolTest, FeatureResolution) {
FileDescriptorProto proto;
FileDescriptorProto::descriptor()->file()->CopyTo(&proto);
AddToDatabase(&database_, proto.DebugString());
pb::TestFeatures::descriptor()->file()->CopyTo(&proto);
AddToDatabase(&database_, proto.DebugString());
AddToDatabase(&database_, R"pb(
name: "features.proto"
syntax: "editions"
edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
options {
features {
enum_type: CLOSED
[pb.test] { file_feature: VALUE9 multiple_feature: VALUE9 }
}
}
message_type {
name: "FooFeatures"
options {
features {
[pb.test] { message_feature: VALUE8 multiple_feature: VALUE8 }
}
}
}
)pb");
MockErrorCollector error_collector;
DescriptorPool pool(&database_, &error_collector);
auto default_spec = FeatureResolver::CompileDefaults(
FeatureSet::descriptor(),
{GetExtensionReflection(pb::cpp), GetExtensionReflection(pb::test)},
EDITION_PROTO2, EDITION_99999_TEST_ONLY);
ASSERT_OK(default_spec);
ASSERT_OK(pool.SetFeatureSetDefaults(std::move(default_spec).value()));
const Descriptor* foo = pool.FindMessageTypeByName("FooFeatures");
ASSERT_TRUE(foo != nullptr);
EXPECT_EQ(GetFeatures(foo).enum_type(), FeatureSet::CLOSED);
EXPECT_EQ(GetFeatures(foo).repeated_field_encoding(), FeatureSet::PACKED);
EXPECT_EQ(GetFeatures(foo).GetExtension(pb::test).enum_feature(), pb::VALUE1);
EXPECT_EQ(GetFeatures(foo).GetExtension(pb::test).file_feature(), pb::VALUE9);
EXPECT_EQ(GetFeatures(foo).GetExtension(pb::test).message_feature(),
pb::VALUE8);
EXPECT_EQ(GetFeatures(foo).GetExtension(pb::test).multiple_feature(),
pb::VALUE8);
}
TEST_F(DatabaseBackedPoolTest, FeatureLifetimeError) {
FileDescriptorProto proto;
FileDescriptorProto::descriptor()->file()->CopyTo(&proto);
AddToDatabase(&database_, proto.DebugString());
pb::TestFeatures::descriptor()->file()->CopyTo(&proto);
AddToDatabase(&database_, proto.DebugString());
AddToDatabase(&database_, R"pb(
name: "features.proto"
syntax: "editions"
edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
message_type {
name: "FooFeatures"
options {
features {
[pb.test] { future_feature: VALUE9 }
}
}
}
)pb");
MockErrorCollector error_collector;
DescriptorPool pool(&database_, &error_collector);
EXPECT_TRUE(pool.FindMessageTypeByName("FooFeatures") == nullptr);
EXPECT_EQ(
error_collector.text_,
"features.proto: FooFeatures: NAME: Feature "
"pb.TestFeatures.future_feature wasn't introduced until edition 2024\n");
}
TEST_F(DatabaseBackedPoolTest, DoesntRetryDbUnnecessarily) { TEST_F(DatabaseBackedPoolTest, DoesntRetryDbUnnecessarily) {
// Searching for a child of an existing descriptor should never fall back // Searching for a child of an existing descriptor should never fall back
// to the DescriptorDatabase even if it isn't found, because we know all // to the DescriptorDatabase even if it isn't found, because we know all

@ -60,7 +60,7 @@ TEST(DefaultsTest, Check2023) {
TEST(DefaultsTest, CheckFuture) { TEST(DefaultsTest, CheckFuture) {
auto defaults = ReadDefaults("test_defaults_future"); auto defaults = ReadDefaults("test_defaults_future");
ASSERT_OK(defaults); ASSERT_OK(defaults);
ASSERT_EQ(defaults->defaults().size(), 4); ASSERT_EQ(defaults->defaults().size(), 5);
ASSERT_EQ(defaults->minimum_edition(), EDITION_2023); ASSERT_EQ(defaults->minimum_edition(), EDITION_2023);
ASSERT_EQ(defaults->maximum_edition(), EDITION_99997_TEST_ONLY); ASSERT_EQ(defaults->maximum_edition(), EDITION_99997_TEST_ONLY);
@ -72,18 +72,24 @@ TEST(DefaultsTest, CheckFuture) {
EXPECT_EQ( EXPECT_EQ(
defaults->defaults()[2].features().GetExtension(pb::test).file_feature(), defaults->defaults()[2].features().GetExtension(pb::test).file_feature(),
pb::VALUE3); pb::VALUE3);
EXPECT_EQ(defaults->defaults()[3].edition(), EDITION_99997_TEST_ONLY); EXPECT_EQ(defaults->defaults()[3].edition(), EDITION_2024);
EXPECT_EQ(defaults->defaults()[3].features().field_presence(), EXPECT_EQ(defaults->defaults()[3].features().field_presence(),
FeatureSet::EXPLICIT); FeatureSet::EXPLICIT);
EXPECT_EQ( EXPECT_EQ(
defaults->defaults()[3].features().GetExtension(pb::test).file_feature(), defaults->defaults()[3].features().GetExtension(pb::test).file_feature(),
pb::VALUE3);
EXPECT_EQ(defaults->defaults()[4].edition(), EDITION_99997_TEST_ONLY);
EXPECT_EQ(defaults->defaults()[4].features().field_presence(),
FeatureSet::EXPLICIT);
EXPECT_EQ(
defaults->defaults()[4].features().GetExtension(pb::test).file_feature(),
pb::VALUE4); pb::VALUE4);
} }
TEST(DefaultsTest, CheckFarFuture) { TEST(DefaultsTest, CheckFarFuture) {
auto defaults = ReadDefaults("test_defaults_far_future"); auto defaults = ReadDefaults("test_defaults_far_future");
ASSERT_OK(defaults); ASSERT_OK(defaults);
ASSERT_EQ(defaults->defaults().size(), 5); ASSERT_EQ(defaults->defaults().size(), 6);
ASSERT_EQ(defaults->minimum_edition(), EDITION_99997_TEST_ONLY); ASSERT_EQ(defaults->minimum_edition(), EDITION_99997_TEST_ONLY);
ASSERT_EQ(defaults->maximum_edition(), EDITION_99999_TEST_ONLY); ASSERT_EQ(defaults->maximum_edition(), EDITION_99999_TEST_ONLY);
@ -95,17 +101,23 @@ TEST(DefaultsTest, CheckFarFuture) {
EXPECT_EQ( EXPECT_EQ(
defaults->defaults()[2].features().GetExtension(pb::test).file_feature(), defaults->defaults()[2].features().GetExtension(pb::test).file_feature(),
pb::VALUE3); pb::VALUE3);
EXPECT_EQ(defaults->defaults()[3].edition(), EDITION_99997_TEST_ONLY); EXPECT_EQ(defaults->defaults()[3].edition(), EDITION_2024);
EXPECT_EQ(defaults->defaults()[3].features().field_presence(), EXPECT_EQ(defaults->defaults()[3].features().field_presence(),
FeatureSet::EXPLICIT); FeatureSet::EXPLICIT);
EXPECT_EQ( EXPECT_EQ(
defaults->defaults()[3].features().GetExtension(pb::test).file_feature(), defaults->defaults()[3].features().GetExtension(pb::test).file_feature(),
pb::VALUE4); pb::VALUE3);
EXPECT_EQ(defaults->defaults()[4].edition(), EDITION_99998_TEST_ONLY); EXPECT_EQ(defaults->defaults()[4].edition(), EDITION_99997_TEST_ONLY);
EXPECT_EQ(defaults->defaults()[4].features().field_presence(), EXPECT_EQ(defaults->defaults()[4].features().field_presence(),
FeatureSet::EXPLICIT); FeatureSet::EXPLICIT);
EXPECT_EQ( EXPECT_EQ(
defaults->defaults()[4].features().GetExtension(pb::test).file_feature(), defaults->defaults()[4].features().GetExtension(pb::test).file_feature(),
pb::VALUE4);
EXPECT_EQ(defaults->defaults()[5].edition(), EDITION_99998_TEST_ONLY);
EXPECT_EQ(defaults->defaults()[5].features().field_presence(),
FeatureSet::EXPLICIT);
EXPECT_EQ(
defaults->defaults()[5].features().GetExtension(pb::test).file_feature(),
pb::VALUE5); pb::VALUE5);
} }

@ -78,6 +78,79 @@ absl::Status ValidateDescriptor(const Descriptor& descriptor) {
return Error("Feature field ", field.full_name(), return Error("Feature field ", field.full_name(),
" has no target specified."); " has no target specified.");
} }
bool has_legacy_default = false;
for (const auto& d : field.options().edition_defaults()) {
if (d.edition() == Edition::EDITION_LEGACY ||
// TODO Remove this once all features use EDITION_LEGACY.
d.edition() == Edition::EDITION_PROTO2) {
has_legacy_default = true;
continue;
}
}
if (!has_legacy_default) {
return Error("Feature field ", field.full_name(),
" has no default specified for EDITION_LEGACY, before it "
"was introduced.");
}
if (!field.options().has_feature_support()) {
// TODO Enforce that feature_support is always set.
return absl::OkStatus();
}
const FieldOptions::FeatureSupport& support =
field.options().feature_support();
if (!support.has_edition_introduced()) {
return Error("Feature field ", field.full_name(),
" does not specify the edition it was introduced in.");
}
if (support.has_edition_deprecated()) {
if (!support.has_deprecation_warning()) {
return Error(
"Feature field ", field.full_name(),
" is deprecated but does not specify a deprecation warning.");
}
if (support.edition_deprecated() < support.edition_introduced()) {
return Error("Feature field ", field.full_name(),
" was deprecated before it was introduced.");
}
}
if (!support.has_edition_deprecated() &&
support.has_deprecation_warning()) {
return Error("Feature field ", field.full_name(),
" specifies a deprecation warning but is not marked "
"deprecated in any edition.");
}
if (support.has_edition_removed()) {
if (support.edition_deprecated() >= support.edition_removed()) {
return Error("Feature field ", field.full_name(),
" was deprecated after it was removed.");
}
if (support.edition_removed() < support.edition_introduced()) {
return Error("Feature field ", field.full_name(),
" was removed before it was introduced.");
}
}
for (const auto& d : field.options().edition_defaults()) {
if (d.edition() < Edition::EDITION_2023) {
// Allow defaults to be specified in proto2/proto3, predating
// editions.
continue;
}
if (d.edition() < support.edition_introduced()) {
return Error("Feature field ", field.full_name(),
" has a default specified for edition ", d.edition(),
", before it was introduced.");
}
if (support.has_edition_removed() &&
d.edition() > support.edition_removed()) {
return Error("Feature field ", field.full_name(),
" has a default specified for edition ", d.edition(),
", after it was removed.");
}
}
} }
return absl::OkStatus(); return absl::OkStatus();
@ -121,6 +194,11 @@ void CollectEditions(const Descriptor& descriptor, Edition maximum_edition,
for (int i = 0; i < descriptor.field_count(); ++i) { for (int i = 0; i < descriptor.field_count(); ++i) {
for (const auto& def : descriptor.field(i)->options().edition_defaults()) { for (const auto& def : descriptor.field(i)->options().edition_defaults()) {
if (maximum_edition < def.edition()) continue; if (maximum_edition < def.edition()) continue;
// TODO Remove this once all features use EDITION_LEGACY.
if (def.edition() == Edition::EDITION_LEGACY) {
editions.insert(Edition::EDITION_PROTO2);
continue;
}
editions.insert(def.edition()); editions.insert(def.edition());
} }
} }
@ -141,6 +219,7 @@ absl::Status FillDefaults(Edition edition, Message& msg) {
msg.GetReflection()->ClearField(&msg, &field); msg.GetReflection()->ClearField(&msg, &field);
ABSL_CHECK(!field.is_repeated()); ABSL_CHECK(!field.is_repeated());
ABSL_CHECK(field.cpp_type() != FieldDescriptor::CPPTYPE_MESSAGE);
std::vector<FieldOptions::EditionDefault> defaults{ std::vector<FieldOptions::EditionDefault> defaults{
field.options().edition_defaults().begin(), field.options().edition_defaults().begin(),
@ -153,23 +232,12 @@ absl::Status FillDefaults(Edition edition, Message& msg) {
" in feature field ", field.full_name()); " in feature field ", field.full_name());
} }
if (field.cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
for (auto it = defaults.begin(); it != first_nonmatch; ++it) {
if (!TextFormat::MergeFromString(
it->value(),
msg.GetReflection()->MutableMessage(&msg, &field))) {
return Error("Parsing error in edition_defaults for feature field ",
field.full_name(), ". Could not parse: ", it->value());
}
}
} else {
const std::string& def = std::prev(first_nonmatch)->value(); const std::string& def = std::prev(first_nonmatch)->value();
if (!TextFormat::ParseFieldValueFromString(def, &field, &msg)) { if (!TextFormat::ParseFieldValueFromString(def, &field, &msg)) {
return Error("Parsing error in edition_defaults for feature field ", return Error("Parsing error in edition_defaults for feature field ",
field.full_name(), ". Could not parse: ", def); field.full_name(), ". Could not parse: ", def);
} }
} }
}
return absl::OkStatus(); return absl::OkStatus();
} }
@ -200,6 +268,43 @@ absl::Status ValidateMergedFeatures(const FeatureSet& features) {
return absl::OkStatus(); return absl::OkStatus();
} }
void CollectLifetimeResults(Edition edition, const Message& message,
FeatureResolver::ValidationResults& results) {
std::vector<const FieldDescriptor*> fields;
message.GetReflection()->ListFields(message, &fields);
for (const FieldDescriptor* field : fields) {
// Recurse into message extension.
if (field->is_extension() &&
field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
CollectLifetimeResults(
edition, message.GetReflection()->GetMessage(message, field),
results);
continue;
}
// Skip fields that don't have feature support specified.
if (!field->options().has_feature_support()) continue;
const FieldOptions::FeatureSupport& support =
field->options().feature_support();
if (edition < support.edition_introduced()) {
results.errors.emplace_back(absl::StrCat(
"Feature ", field->full_name(), " wasn't introduced until edition ",
support.edition_introduced()));
}
if (support.has_edition_removed() && edition >= support.edition_removed()) {
results.errors.emplace_back(absl::StrCat("Feature ", field->full_name(),
" has been removed in edition ",
support.edition_removed()));
} else if (support.has_edition_deprecated() &&
edition >= support.edition_deprecated()) {
results.warnings.emplace_back(absl::StrCat(
"Feature ", field->full_name(), " has been deprecated in edition ",
support.edition_deprecated(), ": ", support.deprecation_warning()));
}
}
}
} // namespace } // namespace
absl::StatusOr<FeatureSetDefaults> FeatureResolver::CompileDefaults( absl::StatusOr<FeatureSetDefaults> FeatureResolver::CompileDefaults(
@ -229,10 +334,14 @@ absl::StatusOr<FeatureSetDefaults> FeatureResolver::CompileDefaults(
for (const auto* extension : extensions) { for (const auto* extension : extensions) {
CollectEditions(*extension->message_type(), maximum_edition, editions); CollectEditions(*extension->message_type(), maximum_edition, editions);
} }
if (editions.empty() || *editions.begin() > minimum_edition) { // Sanity check validation conditions above.
// Always insert the minimum edition to make sure the full range is covered ABSL_CHECK(!editions.empty());
// in valid defaults. ABSL_CHECK_LE(*editions.begin(), EDITION_PROTO2);
editions.insert(minimum_edition);
if (*editions.begin() > minimum_edition) {
return Error("Minimum edition ", minimum_edition,
" is earlier than the oldest valid edition ",
*editions.begin());
} }
// Fill the default spec. // Fill the default spec.
@ -316,6 +425,33 @@ absl::StatusOr<FeatureSet> FeatureResolver::MergeFeatures(
return merged; return merged;
} }
FeatureResolver::ValidationResults FeatureResolver::ValidateFeatureLifetimes(
Edition edition, const FeatureSet& features,
const Descriptor* pool_descriptor) {
const Message* pool_features = nullptr;
DynamicMessageFactory factory;
std::unique_ptr<Message> features_storage;
if (pool_descriptor == nullptr) {
// The FeatureSet descriptor can be null if no custom extensions are defined
// in any transitive dependency. In this case, we can just use the
// generated pool for validation, since there wouldn't be any feature
// extensions defined anyway.
pool_features = &features;
} else {
// Move the features back to the current pool so that we can reflect on any
// extensions.
features_storage =
absl::WrapUnique(factory.GetPrototype(pool_descriptor)->New());
features_storage->ParseFromString(features.SerializeAsString());
pool_features = features_storage.get();
}
ABSL_CHECK(pool_features != nullptr);
ValidationResults results;
CollectLifetimeResults(edition, *pool_features, results);
return results;
}
} // namespace protobuf } // namespace protobuf
} // namespace google } // namespace google

@ -11,6 +11,7 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector>
#include "absl/container/flat_hash_set.h" #include "absl/container/flat_hash_set.h"
#include "absl/status/status.h" #include "absl/status/status.h"
@ -55,6 +56,23 @@ class PROTOBUF_EXPORT FeatureResolver {
absl::StatusOr<FeatureSet> MergeFeatures( absl::StatusOr<FeatureSet> MergeFeatures(
const FeatureSet& merged_parent, const FeatureSet& unmerged_child) const; const FeatureSet& merged_parent, const FeatureSet& unmerged_child) const;
// Validates an unresolved FeatureSet object to make sure they obey the
// lifetime requirements. This needs to run *within* the pool being built, so
// that the descriptors of any feature extensions are known and can be
// validated. `pool_descriptor` should point to the FeatureSet descriptor
// inside the pool, or nullptr if one doesn't exist,
//
// This will return error messages for any explicitly set features used before
// their introduction or after their removal. Warnings will be included for
// any explicitly set features that have been deprecated.
struct ValidationResults {
std::vector<std::string> errors;
std::vector<std::string> warnings;
};
static ValidationResults ValidateFeatureLifetimes(
Edition edition, const FeatureSet& features,
const Descriptor* pool_descriptor);
private: private:
explicit FeatureResolver(FeatureSet defaults) explicit FeatureResolver(FeatureSet defaults)
: defaults_(std::move(defaults)) {} : defaults_(std::move(defaults)) {}

@ -40,8 +40,11 @@ namespace protobuf {
namespace { namespace {
using ::testing::AllOf; using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::ExplainMatchResult; using ::testing::ExplainMatchResult;
using ::testing::HasSubstr; using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::UnorderedElementsAre;
// TODO: Use the gtest versions once that's available in OSS. // TODO: Use the gtest versions once that's available in OSS.
template <typename T> template <typename T>
@ -459,6 +462,100 @@ TEST(FeatureResolverTest, MergeFeaturesDistantFuture) {
HasSubstr("maximum supported edition 99997_TEST_ONLY")))); HasSubstr("maximum supported edition 99997_TEST_ONLY"))));
} }
TEST(FeatureResolverLifetimesTest, Valid) {
FeatureSet features = ParseTextOrDie(R"pb(
[pb.test] { file_feature: VALUE1 }
)pb");
auto results = FeatureResolver::ValidateFeatureLifetimes(EDITION_2023,
features, nullptr);
EXPECT_THAT(results.errors, IsEmpty());
EXPECT_THAT(results.warnings, IsEmpty());
}
TEST(FeatureResolverLifetimesTest, DeprecatedFeature) {
FeatureSet features = ParseTextOrDie(R"pb(
[pb.test] { removed_feature: VALUE1 }
)pb");
auto results = FeatureResolver::ValidateFeatureLifetimes(EDITION_2023,
features, nullptr);
EXPECT_THAT(results.errors, IsEmpty());
EXPECT_THAT(
results.warnings,
ElementsAre(AllOf(HasSubstr("pb.TestFeatures.removed_feature"),
HasSubstr("deprecated in edition 2023"),
HasSubstr("Custom feature deprecation warning"))));
}
TEST(FeatureResolverLifetimesTest, RemovedFeature) {
FeatureSet features = ParseTextOrDie(R"pb(
[pb.test] { removed_feature: VALUE1 }
)pb");
auto results = FeatureResolver::ValidateFeatureLifetimes(EDITION_2024,
features, nullptr);
EXPECT_THAT(results.errors,
ElementsAre(AllOf(HasSubstr("pb.TestFeatures.removed_feature"),
HasSubstr("removed in edition 2024"))));
EXPECT_THAT(results.warnings, IsEmpty());
}
TEST(FeatureResolverLifetimesTest, NotIntroduced) {
FeatureSet features = ParseTextOrDie(R"pb(
[pb.test] { future_feature: VALUE1 }
)pb");
auto results = FeatureResolver::ValidateFeatureLifetimes(EDITION_2023,
features, nullptr);
EXPECT_THAT(results.errors,
ElementsAre(AllOf(HasSubstr("pb.TestFeatures.future_feature"),
HasSubstr("introduced until edition 2024"))));
EXPECT_THAT(results.warnings, IsEmpty());
}
TEST(FeatureResolverLifetimesTest, WarningsAndErrors) {
FeatureSet features = ParseTextOrDie(R"pb(
[pb.test] { future_feature: VALUE1 removed_feature: VALUE1 }
)pb");
auto results = FeatureResolver::ValidateFeatureLifetimes(EDITION_2023,
features, nullptr);
EXPECT_THAT(results.errors,
ElementsAre(HasSubstr("pb.TestFeatures.future_feature")));
EXPECT_THAT(results.warnings,
ElementsAre(HasSubstr("pb.TestFeatures.removed_feature")));
}
TEST(FeatureResolverLifetimesTest, MultipleErrors) {
FeatureSet features = ParseTextOrDie(R"pb(
[pb.test] { future_feature: VALUE1 legacy_feature: VALUE1 }
)pb");
auto results = FeatureResolver::ValidateFeatureLifetimes(EDITION_2023,
features, nullptr);
EXPECT_THAT(results.errors, UnorderedElementsAre(
HasSubstr("pb.TestFeatures.future_feature"),
HasSubstr("pb.TestFeatures.legacy_feature")));
EXPECT_THAT(results.warnings, IsEmpty());
}
TEST(FeatureResolverLifetimesTest, DynamicPool) {
DescriptorPool pool;
FileDescriptorProto file;
FileDescriptorProto::GetDescriptor()->file()->CopyTo(&file);
ASSERT_NE(pool.BuildFile(file), nullptr);
pb::TestFeatures::GetDescriptor()->file()->CopyTo(&file);
ASSERT_NE(pool.BuildFile(file), nullptr);
const Descriptor* feature_set =
pool.FindMessageTypeByName("google.protobuf.FeatureSet");
ASSERT_NE(feature_set, nullptr);
FeatureSet features = ParseTextOrDie(R"pb(
[pb.test] { future_feature: VALUE1 removed_feature: VALUE1 }
)pb");
auto results = FeatureResolver::ValidateFeatureLifetimes(
EDITION_2023, features, feature_set);
EXPECT_THAT(results.errors,
ElementsAre(HasSubstr("pb.TestFeatures.future_feature")));
EXPECT_THAT(results.warnings,
ElementsAre(HasSubstr("pb.TestFeatures.removed_feature")));
}
class FakeErrorCollector : public io::ErrorCollector { class FakeErrorCollector : public io::ErrorCollector {
public: public:
FakeErrorCollector() = default; FakeErrorCollector() = default;
@ -560,7 +657,8 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithExtensions) {
extend Foo { extend Foo {
optional Foo bar2 = 1 [ optional Foo bar2 = 1 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_2023, value: "" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "" }
]; ];
} }
)schema"); )schema");
@ -586,11 +684,13 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithOneof) {
oneof x { oneof x {
int32 int_field = 1 [ int32 int_field = 1 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_2023, value: "1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "1" }
]; ];
string string_field = 2 [ string string_field = 2 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_2023, value: "'hello'" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "'hello'" }
]; ];
} }
} }
@ -616,7 +716,8 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithRequired) {
message Foo { message Foo {
required int32 required_field = 1 [ required int32 required_field = 1 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_2023, value: "" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "" }
]; ];
} }
)schema"); )schema");
@ -641,7 +742,8 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithRepeated) {
message Foo { message Foo {
repeated int32 repeated_field = 1 [ repeated int32 repeated_field = 1 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_2023, value: "1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "1" }
]; ];
} }
)schema"); )schema");
@ -665,7 +767,8 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithMissingTarget) {
} }
message Foo { message Foo {
optional bool bool_field = 1 [ optional bool bool_field = 1 [
edition_defaults = { edition: EDITION_2023, value: "true" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "true" }
]; ];
} }
)schema"); )schema");
@ -678,6 +781,298 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithMissingTarget) {
HasSubstr("no target specified")))); HasSubstr("no target specified"))));
} }
TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithMissingSupport) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_LEGACY, value: "true" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_OK(FeatureResolver::CompileDefaults(feature_set_, {ext}, EDITION_2023,
EDITION_2023));
}
TEST_F(FeatureResolverPoolTest,
CompileDefaultsInvalidWithMissingEditionIntroduced) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
feature_support = {},
edition_defaults = { edition: EDITION_LEGACY, value: "true" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_THAT(FeatureResolver::CompileDefaults(feature_set_, {ext},
EDITION_2023, EDITION_2023),
HasError(AllOf(HasSubstr("test.Foo.bool_field"),
HasSubstr("it was introduced in"))));
}
TEST_F(FeatureResolverPoolTest,
CompileDefaultsInvalidWithMissingDeprecationWarning) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_2023
edition_deprecated: EDITION_2023
},
edition_defaults = { edition: EDITION_LEGACY, value: "true" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_THAT(FeatureResolver::CompileDefaults(feature_set_, {ext},
EDITION_2023, EDITION_2023),
HasError(AllOf(HasSubstr("test.Foo.bool_field"),
HasSubstr("deprecation warning"))));
}
TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithMissingDeprecation) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_2023
deprecation_warning: "some message"
},
edition_defaults = { edition: EDITION_LEGACY, value: "true" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_THAT(FeatureResolver::CompileDefaults(feature_set_, {ext},
EDITION_2023, EDITION_2023),
HasError(AllOf(HasSubstr("test.Foo.bool_field"),
HasSubstr("is not marked deprecated"))));
}
TEST_F(FeatureResolverPoolTest,
CompileDefaultsInvalidDeprecatedBeforeIntroduced) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_2024
edition_deprecated: EDITION_2023
deprecation_warning: "warning"
},
edition_defaults = { edition: EDITION_LEGACY, value: "true" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_THAT(
FeatureResolver::CompileDefaults(feature_set_, {ext}, EDITION_2023,
EDITION_2023),
HasError(AllOf(HasSubstr("test.Foo.bool_field"),
HasSubstr("deprecated before it was introduced"))));
}
TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidDeprecatedAfterRemoved) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_2023
edition_deprecated: EDITION_2024
deprecation_warning: "warning"
edition_removed: EDITION_2024
},
edition_defaults = { edition: EDITION_LEGACY, value: "true" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_THAT(FeatureResolver::CompileDefaults(feature_set_, {ext},
EDITION_2023, EDITION_2023),
HasError(AllOf(HasSubstr("test.Foo.bool_field"),
HasSubstr("deprecated after it was removed"))));
}
TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidRemovedBeforeIntroduced) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_2024
edition_removed: EDITION_2023
},
edition_defaults = { edition: EDITION_LEGACY, value: "true" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_THAT(FeatureResolver::CompileDefaults(feature_set_, {ext},
EDITION_2023, EDITION_2023),
HasError(AllOf(HasSubstr("test.Foo.bool_field"),
HasSubstr("removed before it was introduced"))));
}
TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidMissingLegacyDefaults) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_2024
},
edition_defaults = { edition: EDITION_2024, value: "true" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_THAT(
FeatureResolver::CompileDefaults(feature_set_, {ext}, EDITION_2023,
EDITION_2023),
HasError(AllOf(HasSubstr("test.Foo.bool_field"),
HasSubstr("no default specified for EDITION_LEGACY"))));
}
TEST_F(FeatureResolverPoolTest,
CompileDefaultsInvalidDefaultsBeforeIntroduced) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_2024
},
edition_defaults = { edition: EDITION_LEGACY, value: "true" },
edition_defaults = { edition: EDITION_2023, value: "false" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_THAT(FeatureResolver::CompileDefaults(feature_set_, {ext},
EDITION_2023, EDITION_2023),
HasError(AllOf(HasSubstr("test.Foo.bool_field"),
HasSubstr("specified for edition 2023"),
HasSubstr("before it was introduced"))));
}
TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidDefaultsAfterRemoved) {
const FileDescriptor* file = ParseSchema(R"schema(
syntax = "proto2";
package test;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
optional Foo bar = 9999;
}
message Foo {
optional bool bool_field = 1 [
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_PROTO2
edition_removed: EDITION_2023
},
edition_defaults = { edition: EDITION_LEGACY, value: "true" },
edition_defaults = { edition: EDITION_2024, value: "true" }
];
}
)schema");
ASSERT_NE(file, nullptr);
const FieldDescriptor* ext = file->extension(0);
EXPECT_THAT(FeatureResolver::CompileDefaults(feature_set_, {ext},
EDITION_2023, EDITION_2023),
HasError(AllOf(HasSubstr("test.Foo.bool_field"),
HasSubstr("specified for edition 2024"),
HasSubstr("after it was removed"))));
}
TEST_F(FeatureResolverPoolTest, TEST_F(FeatureResolverPoolTest,
CompileDefaultsInvalidDefaultsScalarParsingError) { CompileDefaultsInvalidDefaultsScalarParsingError) {
const FileDescriptor* file = ParseSchema(R"schema( const FileDescriptor* file = ParseSchema(R"schema(
@ -691,7 +1086,8 @@ TEST_F(FeatureResolverPoolTest,
message Foo { message Foo {
optional bool field_feature = 12 [ optional bool field_feature = 12 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_PROTO2, value: "1.23" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "1.23" }
]; ];
} }
)schema"); )schema");
@ -717,8 +1113,9 @@ TEST_F(FeatureResolverPoolTest,
message Foo { message Foo {
optional bool field_feature = 12 [ optional bool field_feature = 12 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_99997_TEST_ONLY, value: "1.5" }, edition_defaults = { edition: EDITION_99997_TEST_ONLY, value: "1.5" },
edition_defaults = { edition: EDITION_PROTO2, value: "true" } edition_defaults = { edition: EDITION_LEGACY, value: "true" }
]; ];
} }
)schema"); )schema");
@ -747,7 +1144,9 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidDefaultsTooEarly) {
message Foo { message Foo {
optional bool field_feature = 12 [ optional bool field_feature = 12 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_2_TEST_ONLY, value: "true" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_2_TEST_ONLY, value: "true" },
edition_defaults = { edition: EDITION_LEGACY, value: "false" }
]; ];
} }
)schema"); )schema");
@ -772,7 +1171,8 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsMinimumTooEarly) {
message Foo { message Foo {
optional bool field_feature = 12 [ optional bool field_feature = 12 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_PROTO2, value: "true" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "true" }
]; ];
} }
)schema"); )schema");
@ -782,7 +1182,7 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsMinimumTooEarly) {
EXPECT_THAT( EXPECT_THAT(
FeatureResolver::CompileDefaults(feature_set_, {ext}, EDITION_1_TEST_ONLY, FeatureResolver::CompileDefaults(feature_set_, {ext}, EDITION_1_TEST_ONLY,
EDITION_99997_TEST_ONLY), EDITION_99997_TEST_ONLY),
HasError(HasSubstr("No valid default found for edition 1_TEST_ONLY"))); HasError(HasSubstr("edition 1_TEST_ONLY is earlier than the oldest")));
} }
TEST_F(FeatureResolverPoolTest, CompileDefaultsMinimumCovered) { TEST_F(FeatureResolverPoolTest, CompileDefaultsMinimumCovered) {
@ -803,9 +1203,10 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsMinimumCovered) {
message Foo { message Foo {
optional Bar file_feature = 1 [ optional Bar file_feature = 1 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_99998_TEST_ONLY, value: "VALUE3" }, edition_defaults = { edition: EDITION_99998_TEST_ONLY, value: "VALUE3" },
edition_defaults = { edition: EDITION_2023, value: "VALUE2" }, edition_defaults = { edition: EDITION_2023, value: "VALUE2" },
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
} }
)schema"); )schema");
@ -889,7 +1290,8 @@ TEST_P(FeatureUnboundedTypeTest, CompileDefaults) {
message Foo { message Foo {
optional $0 field_feature = 12 [ optional $0 field_feature = 12 [
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_PROTO2, value: "1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "1" }
]; ];
} }
)schema", )schema",

@ -49,7 +49,8 @@ message TestFeatures {
optional EnumFeature file_feature = 1 [ optional EnumFeature file_feature = 1 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_FILE, targets = TARGET_TYPE_FILE,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" }, feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
edition_defaults = { edition: EDITION_PROTO3, value: "VALUE2" }, edition_defaults = { edition: EDITION_PROTO3, value: "VALUE2" },
edition_defaults = { edition: EDITION_2023, value: "VALUE3" }, edition_defaults = { edition: EDITION_2023, value: "VALUE3" },
edition_defaults = { edition: EDITION_99997_TEST_ONLY, value: "VALUE4" }, edition_defaults = { edition: EDITION_99997_TEST_ONLY, value: "VALUE4" },
@ -58,42 +59,50 @@ message TestFeatures {
optional EnumFeature extension_range_feature = 2 [ optional EnumFeature extension_range_feature = 2 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_EXTENSION_RANGE, targets = TARGET_TYPE_EXTENSION_RANGE,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional EnumFeature message_feature = 3 [ optional EnumFeature message_feature = 3 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_MESSAGE, targets = TARGET_TYPE_MESSAGE,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional EnumFeature field_feature = 4 [ optional EnumFeature field_feature = 4 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional EnumFeature oneof_feature = 5 [ optional EnumFeature oneof_feature = 5 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_ONEOF, targets = TARGET_TYPE_ONEOF,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional EnumFeature enum_feature = 6 [ optional EnumFeature enum_feature = 6 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_ENUM, targets = TARGET_TYPE_ENUM,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional EnumFeature enum_entry_feature = 7 [ optional EnumFeature enum_entry_feature = 7 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_ENUM_ENTRY, targets = TARGET_TYPE_ENUM_ENTRY,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional EnumFeature service_feature = 8 [ optional EnumFeature service_feature = 8 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_SERVICE, targets = TARGET_TYPE_SERVICE,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional EnumFeature method_feature = 9 [ optional EnumFeature method_feature = 9 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_METHOD, targets = TARGET_TYPE_METHOD,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional EnumFeature multiple_feature = 10 [ optional EnumFeature multiple_feature = 10 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
@ -106,13 +115,15 @@ message TestFeatures {
targets = TARGET_TYPE_METHOD, targets = TARGET_TYPE_METHOD,
targets = TARGET_TYPE_ONEOF, targets = TARGET_TYPE_ONEOF,
targets = TARGET_TYPE_EXTENSION_RANGE, targets = TARGET_TYPE_EXTENSION_RANGE,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional bool bool_field_feature = 11 [ optional bool bool_field_feature = 11 [
retention = RETENTION_RUNTIME, retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_FIELD, targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_PROTO2, value: "false" }, feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "false" },
edition_defaults = { edition: EDITION_99997_TEST_ONLY, value: "true" } edition_defaults = { edition: EDITION_99997_TEST_ONLY, value: "true" }
]; ];
@ -127,7 +138,8 @@ message TestFeatures {
targets = TARGET_TYPE_METHOD, targets = TARGET_TYPE_METHOD,
targets = TARGET_TYPE_ONEOF, targets = TARGET_TYPE_ONEOF,
targets = TARGET_TYPE_EXTENSION_RANGE, targets = TARGET_TYPE_EXTENSION_RANGE,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
]; ];
optional EnumFeature source_feature2 = 16 [ optional EnumFeature source_feature2 = 16 [
@ -141,6 +153,43 @@ message TestFeatures {
targets = TARGET_TYPE_METHOD, targets = TARGET_TYPE_METHOD,
targets = TARGET_TYPE_ONEOF, targets = TARGET_TYPE_ONEOF,
targets = TARGET_TYPE_EXTENSION_RANGE, targets = TARGET_TYPE_EXTENSION_RANGE,
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" } feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
];
optional EnumFeature removed_feature = 17 [
retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_FILE,
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_2023
edition_deprecated: EDITION_2023
deprecation_warning: "Custom feature deprecation warning"
edition_removed: EDITION_2024
},
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
edition_defaults = { edition: EDITION_2023, value: "VALUE2" },
edition_defaults = { edition: EDITION_2024, value: "VALUE3" }
];
optional EnumFeature future_feature = 18 [
retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_FILE,
targets = TARGET_TYPE_FIELD,
feature_support = { edition_introduced: EDITION_2024 },
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
edition_defaults = { edition: EDITION_2024, value: "VALUE2" }
];
optional EnumFeature legacy_feature = 19 [
retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_FILE,
targets = TARGET_TYPE_FIELD,
feature_support = {
edition_introduced: EDITION_PROTO2
edition_removed: EDITION_2023
},
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
edition_defaults = { edition: EDITION_2023, value: "VALUE2" }
]; ];
} }

Loading…
Cancel
Save