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 8 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.pool = descriptor_pool.DescriptorPool()
defaults = descriptor_pb2.FeatureSetDefaults(
defaults=[
descriptor_pb2.FeatureSetDefaults.FeatureSetEditionDefault(
@ -1465,6 +1466,13 @@ class FeatureInheritanceTest(unittest.TestCase):
].multiple_feature = 1
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.top_message = ret.pool.FindMessageTypeByName(
'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) {
CreateTempFile("foo.proto", R"schema(
edition = "2023";
@ -1958,26 +2015,26 @@ TEST_F(CommandLineInterfaceTest, EditionDefaultsWithExtension) {
FeatureSetDefaults defaults = ReadEditionDefaults("defaults");
EXPECT_EQ(defaults.minimum_edition(), EDITION_PROTO2);
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(1).edition(), EDITION_PROTO3);
EXPECT_EQ(defaults.defaults(2).edition(), EDITION_2023);
EXPECT_EQ(defaults.defaults(3).edition(), EDITION_99997_TEST_ONLY);
EXPECT_EQ(defaults.defaults(4).edition(), EDITION_99998_TEST_ONLY);
EXPECT_EQ(defaults.defaults(3).edition(), EDITION_2024);
EXPECT_EQ(defaults.defaults(4).edition(), EDITION_99997_TEST_ONLY);
EXPECT_EQ(defaults.defaults(5).edition(), EDITION_99998_TEST_ONLY);
EXPECT_EQ(
defaults.defaults(0).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE1);
EXPECT_EQ(
defaults.defaults(1).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE2);
EXPECT_EQ(
defaults.defaults(2).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE3);
EXPECT_EQ(
defaults.defaults(3).features().GetExtension(pb::test).file_feature(),
pb::EnumFeature::VALUE4);
pb::EnumFeature::VALUE3);
EXPECT_EQ(
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);
}

@ -74,6 +74,7 @@
#include "google/protobuf/generated_message_util.h"
#include "google/protobuf/io/strtod.h"
#include "google/protobuf/io/tokenizer.h"
#include "google/protobuf/message.h"
#include "google/protobuf/message_lite.h"
#include "google/protobuf/parse_context.h"
#include "google/protobuf/port.h"
@ -1122,6 +1123,22 @@ bool HasFeatures(const OptionsT& options) {
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) {
return FeatureSet::default_instance();
}
@ -1305,6 +1322,100 @@ class FlatAllocator
} // 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
@ -1591,25 +1702,33 @@ Symbol DescriptorPool::Tables::FindByNameHelper(const DescriptorPool* pool,
if (!result.IsNull()) return result;
}
}
DescriptorPool::DeferredValidation deferred_validation(pool);
Symbol result;
{
absl::MutexLockMaybe lock(pool->mutex_);
if (pool->fallback_database_ != nullptr) {
known_bad_symbols_.clear();
known_bad_files_.clear();
}
Symbol result = FindSymbol(name);
result = FindSymbol(name);
if (result.IsNull() && pool->underlay_ != nullptr) {
// 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()) {
// Symbol still not found, so check fallback database.
if (pool->TryFindSymbolInFallbackDatabase(name)) {
if (pool->TryFindSymbolInFallbackDatabase(name, deferred_validation)) {
result = FindSymbol(name);
}
}
}
if (!deferred_validation.Validate()) {
return Symbol();
}
return result;
}
@ -2133,26 +2252,35 @@ void DescriptorPool::InternalAddGeneratedFile(
const FileDescriptor* DescriptorPool::FindFileByName(
absl::string_view name) const {
DeferredValidation deferred_validation(this);
const FileDescriptor* result = nullptr;
{
absl::MutexLockMaybe lock(mutex_);
if (fallback_database_ != nullptr) {
tables_->known_bad_symbols_.clear();
tables_->known_bad_files_.clear();
}
const FileDescriptor* result = tables_->FindFile(name);
result = tables_->FindFile(name);
if (result != nullptr) return result;
if (underlay_ != nullptr) {
result = underlay_->FindFileByName(name);
if (result != nullptr) return result;
}
if (TryFindFileInFallbackDatabase(name)) {
if (TryFindFileInFallbackDatabase(name, deferred_validation)) {
result = tables_->FindFile(name);
if (result != nullptr) return result;
}
}
if (!deferred_validation.Validate()) {
return nullptr;
}
return result;
}
const FileDescriptor* DescriptorPool::FindFileContainingSymbol(
absl::string_view symbol_name) const {
const FileDescriptor* file_result = nullptr;
DeferredValidation deferred_validation(this);
{
absl::MutexLockMaybe lock(mutex_);
if (fallback_database_ != nullptr) {
tables_->known_bad_symbols_.clear();
@ -2161,15 +2289,18 @@ const FileDescriptor* DescriptorPool::FindFileContainingSymbol(
Symbol result = tables_->FindSymbol(symbol_name);
if (!result.IsNull()) return result.GetFile();
if (underlay_ != nullptr) {
const FileDescriptor* file_result =
underlay_->FindFileContainingSymbol(symbol_name);
file_result = underlay_->FindFileContainingSymbol(symbol_name);
if (file_result != nullptr) return file_result;
}
if (TryFindSymbolInFallbackDatabase(symbol_name)) {
if (TryFindSymbolInFallbackDatabase(symbol_name, deferred_validation)) {
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 file_result;
}
const Descriptor* DescriptorPool::FindMessageTypeByName(
@ -2236,12 +2367,15 @@ const FieldDescriptor* DescriptorPool::FindExtensionByNumber(
return result;
}
}
const FieldDescriptor* result = nullptr;
DeferredValidation deferred_validation(this);
{
absl::MutexLockMaybe lock(mutex_);
if (fallback_database_ != nullptr) {
tables_->known_bad_symbols_.clear();
tables_->known_bad_files_.clear();
}
const FieldDescriptor* result = tables_->FindExtension(extendee, number);
result = tables_->FindExtension(extendee, number);
if (result != nullptr) {
return result;
}
@ -2249,13 +2383,15 @@ const FieldDescriptor* DescriptorPool::FindExtensionByNumber(
result = underlay_->FindExtensionByNumber(extendee, number);
if (result != nullptr) return result;
}
if (TryFindExtensionInFallbackDatabase(extendee, number)) {
if (TryFindExtensionInFallbackDatabase(extendee, number,
deferred_validation)) {
result = tables_->FindExtension(extendee, number);
if (result != nullptr) {
return result;
}
}
if (!deferred_validation.Validate()) {
return nullptr;
}
return result;
}
const FieldDescriptor* DescriptorPool::InternalFindExtensionByNumberNoLock(
@ -2305,6 +2441,9 @@ const FieldDescriptor* DescriptorPool::FindExtensionByPrintableName(
void DescriptorPool::FindAllExtensions(
const Descriptor* extendee,
std::vector<const FieldDescriptor*>* out) const {
DeferredValidation deferred_validation(this);
std::vector<const FieldDescriptor*> extensions;
{
absl::MutexLockMaybe lock(mutex_);
if (fallback_database_ != nullptr) {
tables_->known_bad_symbols_.clear();
@ -2320,16 +2459,21 @@ void DescriptorPool::FindAllExtensions(
&numbers)) {
for (int number : numbers) {
if (tables_->FindExtension(extendee, number) == nullptr) {
TryFindExtensionInFallbackDatabase(extendee, number);
TryFindExtensionInFallbackDatabase(extendee, number,
deferred_validation);
}
}
tables_->extensions_loaded_from_db_.insert(extendee);
}
}
tables_->FindAllExtensions(extendee, out);
tables_->FindAllExtensions(extendee, &extensions);
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(
absl::string_view name) const {
absl::string_view name, DeferredValidation& deferred_validation) const {
if (fallback_database_ == nullptr) 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);
};
auto file_proto = absl::make_unique<FileDescriptorProto>();
if (!find_file(*fallback_database_, name, *file_proto) ||
BuildFileFromDatabase(*file_proto) == nullptr) {
auto& file_proto = deferred_validation.CreateProto();
if (!find_file(*fallback_database_, name, file_proto) ||
BuildFileFromDatabase(file_proto, deferred_validation) == nullptr) {
tables_->known_bad_files_.emplace(name);
return false;
}
@ -2594,13 +2738,13 @@ bool DescriptorPool::IsSubSymbolOfBuiltType(absl::string_view name) const {
}
bool DescriptorPool::TryFindSymbolInFallbackDatabase(
absl::string_view name) const {
absl::string_view name, DeferredValidation& deferred_validation) const {
if (fallback_database_ == nullptr) return false;
if (tables_->known_bad_symbols_.contains(name)) return false;
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
// of any descriptor that already exists in the descriptor pool (except
// for package descriptors). This is valid because all symbols except
@ -2620,16 +2764,15 @@ bool DescriptorPool::TryFindSymbolInFallbackDatabase(
IsSubSymbolOfBuiltType(name)
// Look up file containing this symbol in fallback database.
|| !fallback_database_->FindFileContainingSymbol(name_string,
file_proto.get())
|| !fallback_database_->FindFileContainingSymbol(name_string, &file_proto)
// Check if we've already built this file. If so, it apparently doesn't
// contain the symbol we're looking for. Some DescriptorDatabases
// return false positives.
|| tables_->FindFile(file_proto->name()) != nullptr
|| tables_->FindFile(file_proto.name()) != nullptr
// Build the file.
|| BuildFileFromDatabase(*file_proto) == nullptr) {
|| BuildFileFromDatabase(file_proto, deferred_validation) == nullptr) {
tables_->known_bad_symbols_.insert(std::move(name_string));
return false;
}
@ -2638,23 +2781,24 @@ bool DescriptorPool::TryFindSymbolInFallbackDatabase(
}
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;
auto file_proto = absl::make_unique<FileDescriptorProto>();
auto& file_proto = deferred_validation.CreateProto();
if (!fallback_database_->FindFileContainingExtension(
containing_type->full_name(), field_number, file_proto.get())) {
containing_type->full_name(), field_number, &file_proto)) {
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
// extension we're looking for. Some DescriptorDatabases return false
// positives.
return false;
}
if (BuildFileFromDatabase(*file_proto) == nullptr) {
if (BuildFileFromDatabase(file_proto, deferred_validation) == nullptr) {
return false;
}
@ -3976,9 +4120,10 @@ class DescriptorBuilder {
public:
static std::unique_ptr<DescriptorBuilder> New(
const DescriptorPool* pool, DescriptorPool::Tables* tables,
DescriptorPool::DeferredValidation& deferred_validation,
DescriptorPool::ErrorCollector* error_collector) {
return std::unique_ptr<DescriptorBuilder>(
new DescriptorBuilder(pool, tables, error_collector));
return std::unique_ptr<DescriptorBuilder>(new DescriptorBuilder(
pool, tables, deferred_validation, error_collector));
}
~DescriptorBuilder();
@ -3987,6 +4132,7 @@ class DescriptorBuilder {
private:
DescriptorBuilder(const DescriptorPool* pool, DescriptorPool::Tables* tables,
DescriptorPool::DeferredValidation& deferred_validation,
DescriptorPool::ErrorCollector* error_collector);
friend class OptionInterpreter;
@ -3997,6 +4143,7 @@ class DescriptorBuilder {
const DescriptorPool* pool_;
DescriptorPool::Tables* tables_; // for convenience
DescriptorPool::DeferredValidation& deferred_validation_;
DescriptorPool::ErrorCollector* error_collector_;
absl::optional<FeatureResolver> feature_resolver_ = absl::nullopt;
@ -4541,12 +4688,20 @@ const FileDescriptor* DescriptorPool::BuildFileCollectingErrors(
tables_->known_bad_symbols_.clear();
tables_->known_bad_files_.clear();
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);
if (deferred_validation.Validate()) {
return file;
}
return nullptr;
}
const FileDescriptor* DescriptorPool::BuildFileFromDatabase(
const FileDescriptorProto& proto) const {
const FileDescriptorProto& proto,
DeferredValidation& deferred_validation) const {
mutex_->AssertHeld();
build_started_ = true;
if (tables_->known_bad_files_.contains(proto.name())) {
@ -4554,8 +4709,8 @@ const FileDescriptor* DescriptorPool::BuildFileFromDatabase(
}
const FileDescriptor* result;
const auto build_file = [&] {
result =
DescriptorBuilder::New(this, tables_.get(), default_error_collector_)
result = DescriptorBuilder::New(this, tables_.get(), deferred_validation,
default_error_collector_)
->BuildFile(proto);
};
if (dispatcher_ != nullptr) {
@ -4601,9 +4756,11 @@ absl::Status DescriptorPool::SetFeatureSetDefaults(FeatureSetDefaults spec) {
DescriptorBuilder::DescriptorBuilder(
const DescriptorPool* pool, DescriptorPool::Tables* tables,
DescriptorPool::DeferredValidation& deferred_validation,
DescriptorPool::ErrorCollector* error_collector)
: pool_(pool),
tables_(tables),
deferred_validation_(deferred_validation),
error_collector_(error_collector),
had_errors_(false),
possible_undeclared_dependency_(nullptr),
@ -4739,7 +4896,8 @@ Symbol DescriptorBuilder::FindSymbolNotEnforcingDepsHelper(
// 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
// 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);
}
}
@ -5637,7 +5795,8 @@ const FileDescriptor* DescriptorBuilder::BuildFile(
pool_->underlay_->FindFileByName(proto.dependency(i)) ==
nullptr)) {
// 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();
@ -5952,9 +6111,9 @@ FileDescriptor* DescriptorBuilder::BuildFileImpl(
// Validate options. See comments at InternalSetLazilyBuildDependencies about
// error checking and lazy import building.
if (!had_errors_ && !pool_->lazily_build_dependencies_) {
internal::VisitDescriptors(*result, proto,
[&](const auto& descriptor, const auto& proto) {
ValidateOptions(&descriptor, proto);
internal::VisitDescriptors(
*result, proto, [&](const auto& descriptor, const auto& desc_proto) {
ValidateOptions(&descriptor, desc_proto);
});
}
@ -5975,6 +6134,19 @@ FileDescriptor* DescriptorBuilder::BuildFileImpl(
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_) {
return nullptr;
} else {

@ -2366,10 +2366,17 @@ class PROTOBUF_EXPORT DescriptorPool {
// corresponding proto file. Returns true if successful, in which case
// the caller should search for the thing again. These are declared
// const because they are called by (semantically) const methods.
bool TryFindFileInFallbackDatabase(absl::string_view name) const;
bool TryFindSymbolInFallbackDatabase(absl::string_view name) const;
bool TryFindExtensionInFallbackDatabase(const Descriptor* containing_type,
int field_number) const;
// DeferredValidation stores temporary information necessary to run validation
// checks that can't be done inside the database lock. This is generally
// reflective operations that also require the lock to do safely.
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
// 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)
// const methods.
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
// 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) {
BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
const FileDescriptor* file = BuildFile(R"pb(
name: "foo.proto"
syntax: "editions"
edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
options { features { enum_type: CLOSED } }
enum_type {
name: "Foo"
@ -9010,10 +9012,12 @@ TEST_F(FeaturesTest, OneofFeaturesDefault) {
TEST_F(FeaturesTest, OneofFeaturesInherit) {
BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
const FileDescriptor* file = BuildFile(R"pb(
name: "foo.proto"
syntax: "editions"
edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
options { features { enum_type: CLOSED } }
message_type {
name: "Foo"
@ -9113,10 +9117,12 @@ TEST_F(FeaturesTest, ExtensionRangeFeaturesDefault) {
TEST_F(FeaturesTest, ExtensionRangeFeaturesInherit) {
BuildDescriptorMessagesInTestPool();
BuildFileInTestPool(pb::TestFeatures::descriptor()->file());
const FileDescriptor* file = BuildFile(R"pb(
name: "foo.proto"
syntax: "editions"
edition: EDITION_2023
dependency: "google/protobuf/unittest_features.proto"
options { features { enum_type: CLOSED } }
message_type {
name: "Foo"
@ -10359,6 +10365,93 @@ TEST_F(FeaturesTest, InvalidGroupLabel) {
"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
// the original descriptors.
class FeaturesDebugStringTest
@ -11402,7 +11495,7 @@ TEST_F(ValidationErrorTest, PackageTooLong) {
// DescriptorDatabase
static void AddToDatabase(SimpleDescriptorDatabase* database,
const char* file_text) {
absl::string_view file_text) {
FileDescriptorProto file_proto;
EXPECT_TRUE(TextFormat::ParseFromString(file_text, &file_proto));
database->Add(file_proto);
@ -11723,6 +11816,84 @@ TEST_F(DatabaseBackedPoolTest, UnittestProto) {
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) {
// 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

@ -60,7 +60,7 @@ TEST(DefaultsTest, Check2023) {
TEST(DefaultsTest, CheckFuture) {
auto defaults = ReadDefaults("test_defaults_future");
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->maximum_edition(), EDITION_99997_TEST_ONLY);
@ -72,18 +72,24 @@ TEST(DefaultsTest, CheckFuture) {
EXPECT_EQ(
defaults->defaults()[2].features().GetExtension(pb::test).file_feature(),
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(),
FeatureSet::EXPLICIT);
EXPECT_EQ(
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);
}
TEST(DefaultsTest, CheckFarFuture) {
auto defaults = ReadDefaults("test_defaults_far_future");
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->maximum_edition(), EDITION_99999_TEST_ONLY);
@ -95,17 +101,23 @@ TEST(DefaultsTest, CheckFarFuture) {
EXPECT_EQ(
defaults->defaults()[2].features().GetExtension(pb::test).file_feature(),
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(),
FeatureSet::EXPLICIT);
EXPECT_EQ(
defaults->defaults()[3].features().GetExtension(pb::test).file_feature(),
pb::VALUE4);
EXPECT_EQ(defaults->defaults()[4].edition(), EDITION_99998_TEST_ONLY);
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);
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);
}

@ -78,6 +78,79 @@ absl::Status ValidateDescriptor(const Descriptor& descriptor) {
return Error("Feature field ", field.full_name(),
" 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();
@ -121,6 +194,11 @@ void CollectEditions(const Descriptor& descriptor, Edition maximum_edition,
for (int i = 0; i < descriptor.field_count(); ++i) {
for (const auto& def : descriptor.field(i)->options().edition_defaults()) {
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());
}
}
@ -141,6 +219,7 @@ absl::Status FillDefaults(Edition edition, Message& msg) {
msg.GetReflection()->ClearField(&msg, &field);
ABSL_CHECK(!field.is_repeated());
ABSL_CHECK(field.cpp_type() != FieldDescriptor::CPPTYPE_MESSAGE);
std::vector<FieldOptions::EditionDefault> defaults{
field.options().edition_defaults().begin(),
@ -153,23 +232,12 @@ absl::Status FillDefaults(Edition edition, Message& msg) {
" 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();
if (!TextFormat::ParseFieldValueFromString(def, &field, &msg)) {
return Error("Parsing error in edition_defaults for feature field ",
field.full_name(), ". Could not parse: ", def);
}
}
}
return absl::OkStatus();
}
@ -200,6 +268,43 @@ absl::Status ValidateMergedFeatures(const FeatureSet& features) {
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
absl::StatusOr<FeatureSetDefaults> FeatureResolver::CompileDefaults(
@ -229,10 +334,14 @@ absl::StatusOr<FeatureSetDefaults> FeatureResolver::CompileDefaults(
for (const auto* extension : extensions) {
CollectEditions(*extension->message_type(), maximum_edition, editions);
}
if (editions.empty() || *editions.begin() > minimum_edition) {
// Always insert the minimum edition to make sure the full range is covered
// in valid defaults.
editions.insert(minimum_edition);
// Sanity check validation conditions above.
ABSL_CHECK(!editions.empty());
ABSL_CHECK_LE(*editions.begin(), EDITION_PROTO2);
if (*editions.begin() > minimum_edition) {
return Error("Minimum edition ", minimum_edition,
" is earlier than the oldest valid edition ",
*editions.begin());
}
// Fill the default spec.
@ -316,6 +425,33 @@ absl::StatusOr<FeatureSet> FeatureResolver::MergeFeatures(
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 google

@ -11,6 +11,7 @@
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
@ -55,6 +56,23 @@ class PROTOBUF_EXPORT FeatureResolver {
absl::StatusOr<FeatureSet> MergeFeatures(
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:
explicit FeatureResolver(FeatureSet defaults)
: defaults_(std::move(defaults)) {}

@ -40,8 +40,11 @@ namespace protobuf {
namespace {
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::ExplainMatchResult;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::UnorderedElementsAre;
// TODO: Use the gtest versions once that's available in OSS.
template <typename T>
@ -459,6 +462,100 @@ TEST(FeatureResolverTest, MergeFeaturesDistantFuture) {
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 {
public:
FakeErrorCollector() = default;
@ -560,7 +657,8 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithExtensions) {
extend Foo {
optional Foo bar2 = 1 [
targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_2023, value: "" }
feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "" }
];
}
)schema");
@ -586,11 +684,13 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithOneof) {
oneof x {
int32 int_field = 1 [
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 [
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 {
required int32 required_field = 1 [
targets = TARGET_TYPE_FIELD,
edition_defaults = { edition: EDITION_2023, value: "" }
feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_LEGACY, value: "" }
];
}
)schema");
@ -641,7 +742,8 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithRepeated) {
message Foo {
repeated int32 repeated_field = 1 [
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");
@ -665,7 +767,8 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithMissingTarget) {
}
message Foo {
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");
@ -678,6 +781,298 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidWithMissingTarget) {
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,
CompileDefaultsInvalidDefaultsScalarParsingError) {
const FileDescriptor* file = ParseSchema(R"schema(
@ -691,7 +1086,8 @@ TEST_F(FeatureResolverPoolTest,
message Foo {
optional bool field_feature = 12 [
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");
@ -717,8 +1113,9 @@ TEST_F(FeatureResolverPoolTest,
message Foo {
optional bool field_feature = 12 [
targets = TARGET_TYPE_FIELD,
feature_support.edition_introduced = EDITION_2023,
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");
@ -747,7 +1144,9 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsInvalidDefaultsTooEarly) {
message Foo {
optional bool field_feature = 12 [
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");
@ -772,7 +1171,8 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsMinimumTooEarly) {
message Foo {
optional bool field_feature = 12 [
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");
@ -782,7 +1182,7 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsMinimumTooEarly) {
EXPECT_THAT(
FeatureResolver::CompileDefaults(feature_set_, {ext}, EDITION_1_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) {
@ -803,9 +1203,10 @@ TEST_F(FeatureResolverPoolTest, CompileDefaultsMinimumCovered) {
message Foo {
optional Bar file_feature = 1 [
targets = TARGET_TYPE_FIELD,
feature_support.edition_introduced = EDITION_2023,
edition_defaults = { edition: EDITION_99998_TEST_ONLY, value: "VALUE3" },
edition_defaults = { edition: EDITION_2023, value: "VALUE2" },
edition_defaults = { edition: EDITION_PROTO2, value: "VALUE1" }
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" }
];
}
)schema");
@ -889,7 +1290,8 @@ TEST_P(FeatureUnboundedTypeTest, CompileDefaults) {
message Foo {
optional $0 field_feature = 12 [
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",

@ -49,7 +49,8 @@ message TestFeatures {
optional EnumFeature file_feature = 1 [
retention = RETENTION_RUNTIME,
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_2023, value: "VALUE3" },
edition_defaults = { edition: EDITION_99997_TEST_ONLY, value: "VALUE4" },
@ -58,42 +59,50 @@ message TestFeatures {
optional EnumFeature extension_range_feature = 2 [
retention = RETENTION_RUNTIME,
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 [
retention = RETENTION_RUNTIME,
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 [
retention = RETENTION_RUNTIME,
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 [
retention = RETENTION_RUNTIME,
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 [
retention = RETENTION_RUNTIME,
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 [
retention = RETENTION_RUNTIME,
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 [
retention = RETENTION_RUNTIME,
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 [
retention = RETENTION_RUNTIME,
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 [
retention = RETENTION_RUNTIME,
@ -106,13 +115,15 @@ message TestFeatures {
targets = TARGET_TYPE_METHOD,
targets = TARGET_TYPE_ONEOF,
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 [
retention = RETENTION_RUNTIME,
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" }
];
@ -127,7 +138,8 @@ message TestFeatures {
targets = TARGET_TYPE_METHOD,
targets = TARGET_TYPE_ONEOF,
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 [
@ -141,6 +153,43 @@ message TestFeatures {
targets = TARGET_TYPE_METHOD,
targets = TARGET_TYPE_ONEOF,
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