extend *Options blocks (custom options)Status: implemented Implemented in: 2026-05-01 App: reproto
Spec 0015 §6 noted that proto3 allows extend blocks on the nine *Options
messages from google/protobuf/descriptor.proto (custom options), but the
allow_extensions(ctx) helper introduced in spec 0016 returns False for any
proto3 file. As a result, reproto drops every extend block in a proto3 file
with a warning, including legitimate custom-option definitions such as:
extend google.protobuf.MethodOptions {
google.longrunning.OperationInfo operation_info = 1049;
}
This is incorrect: the block is valid proto3 and must be emitted.
extend *Options blocks in proto3 files are emitted normally (not dropped).extend UserMessage blocks in proto3 files are still dropped with a
cli_warning (unchanged behaviour).extensions N to M; ranges in proto3 files are still dropped with a
cli_warning (unchanged behaviour — extension ranges on user-defined
messages remain proto2-only).custom_options_proto3.proto exercises both a
file-level and a message-level extend *Options block in proto3.extensions N to M; guard (proto2-only, unchanged).extend on user-defined messages in proto3.Proto3 allows extend only on the nine descriptor *Options messages:
| Message | FQN |
|---|---|
FileOptions | .google.protobuf.FileOptions |
MessageOptions | .google.protobuf.MessageOptions |
FieldOptions | .google.protobuf.FieldOptions |
OneofOptions | .google.protobuf.OneofOptions |
ExtensionRangeOptions | .google.protobuf.ExtensionRangeOptions |
EnumOptions | .google.protobuf.EnumOptions |
EnumValueOptions | .google.protobuf.EnumValueOptions |
ServiceOptions | .google.protobuf.ServiceOptions |
MethodOptions | .google.protobuf.MethodOptions |
This is enforced by protoc: a proto3 file that tries to extend any other
message fails with a compile error. Reproto only processes well-formed
descriptors, so it can treat any extend block whose extendee is one of these
nine FQNs as always valid in proto3.
Extension ranges (extensions N to M;) remain proto2-only even for the
*Options messages — protoc does not allow adding extension ranges to them
in proto3 source.
syntax.py — replace allow_extensions with two helpersRemove allow_extensions(ctx) and replace with:
# The nine *Options FQNs that proto3 allows extending (custom options).
_DESCRIPTOR_OPTIONS_FQNS = frozenset({
".google.protobuf.FileOptions",
".google.protobuf.MessageOptions",
".google.protobuf.FieldOptions",
".google.protobuf.OneofOptions",
".google.protobuf.ExtensionRangeOptions",
".google.protobuf.EnumOptions",
".google.protobuf.EnumValueOptions",
".google.protobuf.ServiceOptions",
".google.protobuf.MethodOptions",
})
def allow_extend_block(ctx: Context, extendee: str) -> bool:
"""Return True iff an extend block for `extendee` is legal in target syntax.
Proto2: always True.
Proto3: True only when extendee is one of the nine descriptor *Options FQNs
(custom options are the only proto3-legal extension target).
"""
if ctx.target_syntax == "proto2":
return True
return extendee in _DESCRIPTOR_OPTIONS_FQNS
def allow_extension_ranges(ctx: Context) -> bool:
"""Return True iff `extensions N to M;` declarations are legal in target syntax."""
return ctx.target_syntax == "proto2"
Keep allow_extensions as a thin alias for backwards compatibility with any
call site that uses it for extension-range decisions, redirecting to
allow_extension_ranges. Remove the alias once all call sites are updated.
re_file.py — file-level extend blocksIn the file-level extensions rendering section, replace the single
allow_extensions(ctx) branch with per-extendee logic:
from .syntax import allow_extend_block
for e in self.extension:
extension_proto = cast(FieldDescriptorProto, e)
if not allow_extend_block(ctx, extension_proto.extendee):
cli_warning(
f"'{self.name}': top-level extend block for "
f"'{extension_proto.extendee}' is not valid in proto3; omitting"
)
# mark as excluded so extendee_short_names loop skips it
Concretely: collect only the extendees for which allow_extend_block returns
True; warn-and-skip the rest.
re_descriptor.py — message-level extend blocksIn render_extensions, replace the blanket allow_extensions(ctx) guard with
per-extendee filtering (same pattern as §2).
In render_reserved, the allow_extensions guard for extension_range is
renamed to allow_extension_ranges — semantics unchanged.
custom_options_proto3.protosyntax = "proto3";
package mockup;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions {
string custom_file_opt = 50000;
}
message MyMessage {
extend google.protobuf.FieldOptions {
bool custom_field_opt = 50001;
}
string value = 1 [(mockup.MyMessage.custom_field_opt) = true];
}
option (mockup.custom_file_opt) = "hello";
Add custom_options_proto3.proto to DEFAULT_FIXTURES in test_roundtrip.py.
test_roundtrip[custom_options_proto3.proto]: full .pb roundtrip
(byte-identical) and .proto text comparison after normalization.test_roundtrip[*] tests must continue to pass (no regression).extensions_proto2.proto fixture (user-defined message extensions in
proto2) continues to pass unchanged.