blob: 69007650591ae3afdb468a4b2c449e79db97750e [file]
#include "google/protobuf/symbol_checker.h"
#include <string>
#include <vector>
#include "absl/strings/str_cat.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/descriptor.pb.h"
#include "google/protobuf/descriptor_visitor.h"
// Must be included last.
#include "google/protobuf/port_def.inc"
namespace google {
namespace protobuf {
std::string SymbolCheckerError::message() const {
switch (type_) {
case NESTED_MESSAGE_STRICT_VIOLATION:
return absl::StrCat(
"\"", symbol_name_,
"\" is a nested message and cannot be `export` with STRICT "
"default_symbol_visibility. It must be moved to top-level, "
"ideally "
"in its own file in order to be `export`.");
case NESTED_ENUM_STRICT_VIOLATION:
return absl::StrCat(
"\"", symbol_name_,
"\" is a nested enum and cannot be marked `export` with "
"STRICT "
"default_symbol_visibility. It must be moved to "
"top-level, ideally "
"in its own file in order to be `export`. For C++ "
"namespacing of enums in a messages use: `local "
"message <OuterNamespace> { export enum ",
symbol_name_, " {...} reserved 1 to max; }`");
default:
return "";
}
}
namespace {
// Populate VisibilityCheckerState for messages.
void VisitType(const Descriptor& message, const DescriptorProto& proto,
internal::SymbolCheckerState* state) {
if (message.containing_type() != nullptr) {
state->nested_messages.push_back(
internal::MessageDescriptorAndProto{&message, &proto});
}
}
void VisitType(const EnumDescriptor& enm, const EnumDescriptorProto& proto,
internal::SymbolCheckerState* state) {
if (enm.containing_type() != nullptr) {
if (SymbolChecker::IsNamespacedEnum(enm)) {
state->namespaced_enums.push_back(
internal::EnumDescriptorAndProto{&enm, &proto});
} else {
state->nested_enums.push_back(
internal::EnumDescriptorAndProto{&enm, &proto});
}
}
}
void VisitType(const FileDescriptor& file, const FileDescriptorProto& proto,
internal::SymbolCheckerState* state) {}
void VisitType(const FieldDescriptor& field, const FieldDescriptorProto& proto,
internal::SymbolCheckerState* state) {}
void VisitType(const EnumValueDescriptor& value,
const EnumValueDescriptorProto& proto,
internal::SymbolCheckerState* state) {}
void VisitType(const OneofDescriptor& one, const OneofDescriptorProto& proto,
internal::SymbolCheckerState* state) {}
void VisitType(const Descriptor::ExtensionRange&,
const DescriptorProto::ExtensionRange& proto,
internal::SymbolCheckerState* state) {}
void VisitType(const MethodDescriptor& method,
const MethodDescriptorProto& proto,
internal::SymbolCheckerState* state) {}
void VisitType(const ServiceDescriptor& service,
const ServiceDescriptorProto& proto,
internal::SymbolCheckerState* state) {}
} // namespace
SymbolChecker::SymbolChecker(const FileDescriptor* file,
const FileDescriptorProto& proto)
: initialized_(false), descriptor_(file), proto_(proto) {}
bool SymbolChecker::IsEnumNamespaceMessage(const Descriptor& container) {
const FeatureSet::VisibilityFeature::DefaultSymbolVisibility
default_visibility =
container.file()->features().default_symbol_visibility();
// Only allowed for top-level messages
if (container.containing_type() != nullptr) {
return false;
}
bool default_to_local =
default_visibility == FeatureSet::VisibilityFeature::STRICT ||
default_visibility == FeatureSet::VisibilityFeature::LOCAL_ALL;
bool is_container_local =
container.visibility_keyword() == SymbolVisibility::VISIBILITY_LOCAL ||
(container.visibility_keyword() == SymbolVisibility::VISIBILITY_UNSET &&
default_to_local);
// the containing message must either be marked local, or unset with file
// default making it local
if (!is_container_local) {
return false;
}
// We also require a single reserved range of 1 to max.
if (container.reserved_range_count() != 1) {
return false;
}
// Always not-null due to reserved_range_count check above.
const Descriptor::ReservedRange* range = container.reserved_range(0);
// The reserved range must be 1 to max. Max is exclusive so results in
// kMaxNumber + 1
return range->start == 1 && range->end == (FieldDescriptor::kMaxNumber + 1);
}
bool SymbolChecker::IsNamespacedEnum(const EnumDescriptor& enm) {
// If the enum is top-level, it definitely isn't namespaced.
if (enm.containing_type() == nullptr) {
return false;
}
const FeatureSet::VisibilityFeature::DefaultSymbolVisibility
default_visibility = enm.file()->features().default_symbol_visibility();
bool default_to_local =
default_visibility == FeatureSet::VisibilityFeature::STRICT ||
default_visibility == FeatureSet::VisibilityFeature::LOCAL_ALL;
// This check only cares if the enum is explicitly marked export, the
// containing message can be local or unset with the file default making it
// local.
bool is_exported =
!default_to_local ||
enm.visibility_keyword() == SymbolVisibility::VISIBILITY_EXPORT;
return is_exported && IsEnumNamespaceMessage(*enm.containing_type());
}
void SymbolChecker::Initialize() {
if (initialized_) {
return;
}
internal::VisitDescriptors(*descriptor_, proto_,
[&](const auto& descriptor, const auto& proto) {
VisitType(descriptor, proto, &state_);
});
initialized_ = true;
}
// Enforce File-wide visibility and future co-location rules.
std::vector<SymbolCheckerError> SymbolChecker::CheckSymbolVisibilityRules() {
std::vector<SymbolCheckerError> errors;
Initialize();
// Check DefaultSymbolVisibility first.
//
// For Edition 2024:
// If DefaultSymbolVisibility is STRICT enforce it with caveats for:
// Build our state object so we can apply rules based on type.
// In edition 2024 we only enforce STRICT visibility rules. There are possibly
// more rules to come in future editions, but for now just apply the rule for
// enforcing nested symbol local visibility. There is a single caveat for,
// allowing nested enums to have visibility set only when
//
// local msg { export enum {} reserved 1 to max; }
for (auto& nested : state_.nested_messages) {
if (nested.descriptor->visibility_keyword() ==
SymbolVisibility::VISIBILITY_EXPORT &&
nested.descriptor->features().default_symbol_visibility() ==
FeatureSet::VisibilityFeature::STRICT) {
errors.push_back(SymbolCheckerError(nested.descriptor->full_name(),
*nested.proto,
NESTED_MESSAGE_STRICT_VIOLATION));
}
}
for (auto& nested : state_.nested_enums) {
if (nested.descriptor->visibility_keyword() ==
SymbolVisibility::VISIBILITY_EXPORT &&
nested.descriptor->features().default_symbol_visibility() ==
FeatureSet::VisibilityFeature::STRICT) {
// This list contains only enums not considered 'namespaced' by
// IsEnumNamespaceMessage
errors.push_back(SymbolCheckerError(nested.descriptor->full_name(),
*nested.proto,
NESTED_ENUM_STRICT_VIOLATION));
}
}
// Enforce Future rules here:
return errors;
}
} // namespace protobuf
} // namespace google
#include "google/protobuf/port_undef.inc"