Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changes/unreleased/fixed-20261028-115331.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kind: Fixed
body: |-
**`extension_or_default` returns a closed enum's first value for an extension that declares no default** (#801). A singular extension of a closed enum (any proto2 enum, or an editions enum with `enum_type = CLOSED`) without `[default = ...]` read as 0 when absent, and 0 need not be a value of the enum: for `enum Level { LOW = 1; HIGH = 2; }` it is not. Generated code now gives such an extension the enum's first declared value as its default, the number an unset field of that enum already reads as. Extensions of open enums, whose first value is always 0, and repeated extensions are unchanged, and `extension()` still returns `None` for an absent extension.
time: 2026-10-28T11:53:31.504699+03:00
145 changes: 142 additions & 3 deletions buffa-codegen/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ fn generate_one(
// Proto2 `[default = ...]`. The proto spec only allows defaults on singular
// scalar/enum fields; protoc rejects message/group/repeated at parse time.
// We check defensively because a hand-crafted descriptor could slip through.
// A singular closed-enum extension without one defaults to the enum's
// first declared value, as a field of that enum does.
let default_fn = if field
.default_value
.as_deref()
Expand All @@ -127,6 +129,17 @@ fn generate_one(
nesting,
features,
)?)
} else if !repeated
&& ty == Type::TYPE_ENUM
&& crate::message::is_closed_enum(&crate::features::resolve_field(ctx, field, features))
{
Some(closed_enum_default_fn_tokens(
ctx,
field,
&const_ident,
current_package,
nesting,
)?)
} else {
None
};
Expand Down Expand Up @@ -361,6 +374,35 @@ fn default_fn_tokens(
))
}

/// Build the default fn of a singular closed-enum extension that declares no
/// `[default = ...]`: the enum's first declared value, which is what an unset
/// field of that enum reads as. Without it `extension_or_default` would return
/// `i32::default()`, and 0 need not be a value of a closed enum
/// (`enum Level { LOW = 1; HIGH = 2; }`). An open enum's first value is
/// always 0, so it needs none.
///
/// The fn is not `const`: it goes through the enum's `Default` impl, which
/// also covers an `extern_path` enum whose values this build cannot see.
fn closed_enum_default_fn_tokens(
ctx: &CodeGenContext,
field: &FieldDescriptorProto,
const_ident: &Ident,
current_package: &str,
nesting: usize,
) -> Result<(Ident, TokenStream), CodeGenError> {
let fn_ident = format_ident!("__{}_default", const_ident.to_string().to_lowercase());
let first = crate::reflect_view::enum_default_number(ctx, field, current_package, nesting)?;
Ok((
fn_ident.clone(),
quote! {
#[doc(hidden)]
fn #fn_ident() -> i32 {
#first
}
},
))
}

/// Map a proto scalar/enum type to its `ExtensionCodec::Value` Rust type.
///
/// Only called for types that can carry a `[default = ...]` — message/group
Expand Down Expand Up @@ -767,10 +809,10 @@ mod tests {
#[test]
fn enum_extension_doc_mentions_type() {
let mut field = ext_field("level", 50006, Type::TYPE_ENUM);
field.type_name = Some(".my.pkg.LogLevel".to_string());
let tokens = gen(&field).unwrap();
field.type_name = Some(".my.pkg.Color".to_string());
let (tokens, _) = gen_in_pkg(&field).unwrap();
assert!(tokens.contains("EnumI32"), "{tokens}");
assert!(tokens.contains("my.pkg.LogLevel"), "{tokens}");
assert!(tokens.contains("my.pkg.Color"), "{tokens}");
}

#[test]
Expand Down Expand Up @@ -1112,6 +1154,103 @@ mod tests {
assert!(tokens.contains("Color :: RED as i32"), "{tokens}");
}

/// Run `generate_one` for extension `level` of enum `.my.pkg.Level`,
/// declared in `file` (package `my.pkg`), under the file's features.
fn gen_enum_ext(file: FileDescriptorProto, label: Label) -> String {
let files = [file];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &[]);
let features = crate::features::for_file(&files[0]);
let mut field = ext_field("level", 50040, Type::TYPE_ENUM);
field.type_name = Some(".my.pkg.Level".into());
field.label = Some(label);
generate_one(&ctx, &field, "my.pkg", 0, &features, "my.pkg")
.unwrap()
.unwrap()
.0
.to_string()
}

fn level_file(syntax: &str, level: EnumDescriptorProto) -> FileDescriptorProto {
FileDescriptorProto {
name: Some("level.proto".into()),
package: Some("my.pkg".into()),
syntax: Some(syntax.into()),
edition: (syntax == "editions")
.then_some(crate::generated::descriptor::Edition::EDITION_2023),
enum_type: vec![level],
..Default::default()
}
}

fn level_enum(
enum_type: Option<crate::generated::descriptor::feature_set::EnumType>,
) -> EnumDescriptorProto {
use crate::generated::descriptor::{EnumOptions, FeatureSet};
EnumDescriptorProto {
name: Some("Level".into()),
options: match enum_type {
Some(t) => buffa::MessageField::some(EnumOptions {
features: buffa::MessageField::some(FeatureSet {
enum_type: Some(t),
..Default::default()
}),
..Default::default()
}),
None => buffa::MessageField::none(),
},
..Default::default()
}
}

const FIRST_VALUE: &str =
":: buffa :: Enumeration :: to_i32 (& < Level as :: core :: default :: Default > :: default ())";

/// A closed enum need not declare 0, so a singular extension of one with no
/// `[default]` reads absent as the enum's first value, through a non-const
/// fn, in proto2 and under editions `enum_type = CLOSED`.
#[test]
fn closed_enum_without_default_uses_first_value() {
use crate::generated::descriptor::feature_set::EnumType;
let files = [
level_file("proto2", level_enum(None)),
level_file("editions", level_enum(Some(EnumType::CLOSED))),
];
for file in files {
let tokens = gen_enum_ext(file, Label::LABEL_OPTIONAL);
assert!(tokens.contains(":: with_default"), "{tokens}");
assert!(tokens.contains("fn __level_default () -> i32"), "{tokens}");
assert!(!tokens.contains("const fn __level_default"), "{tokens}");
assert!(tokens.contains(FIRST_VALUE), "{tokens}");
}
}

/// An open enum's first value is 0, which `i32::default()` already gives,
/// and a repeated extension has no default, so neither gets a fn.
#[test]
fn open_or_repeated_enum_without_default_emits_plain_new() {
let cases = [
(
level_file("proto3", level_enum(None)),
Label::LABEL_OPTIONAL,
),
(
level_file("editions", level_enum(None)),
Label::LABEL_OPTIONAL,
),
(
level_file("proto2", level_enum(None)),
Label::LABEL_REPEATED,
),
];
for (file, label) in cases {
let tokens = gen_enum_ext(file, label);
assert!(tokens.contains(":: new"), "{tokens}");
assert!(!tokens.contains("with_default"), "{tokens}");
assert!(!tokens.contains("__level_default"), "{tokens}");
}
}

#[test]
fn default_value_absent_emits_plain_new() {
let f = ext_field("weight", 50002, Type::TYPE_SINT32);
Expand Down
7 changes: 7 additions & 0 deletions buffa-test/protos/custom_options.proto
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,11 @@ extend Carrier {
optional int32 c_priority = 110 [default = 7];
optional string c_label = 111 [default = "none"];
optional bool c_active = 112 [default = true];
optional Level c_level = 113; // closed enum, no [default]
}

// A closed enum that does not declare 0: absent `c_level` reads as LOW.
enum Level {
LOW = 1;
HIGH = 2;
}
16 changes: 16 additions & 0 deletions buffa-test/protos/group_ext.proto
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,20 @@ message Carrier {
extend Carrier {
Inner delim_inner = 100;
repeated Inner delim_repeated = 101;
Level level = 102;
Mode mode = 103;
}

// Enum extensions without a default read absent as the first value: LOW for
// the closed enum, which does not declare 0, and 0 for the open one.
enum Level {
option features.enum_type = CLOSED;

LOW = 3;
HIGH = 4;
}

enum Mode {
MODE_UNSPECIFIED = 0;
MODE_ON = 1;
}
35 changes: 30 additions & 5 deletions buffa-test/src/tests/extensions.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! Integration tests for `extend` codegen and the `ExtensionSet` runtime API.

use crate::custopts::__buffa::ext::{
ACTIVE, ANN, C_ACTIVE, C_ANN, C_FLAG, C_LABEL, C_MARKER, C_PRIORITY, C_TAGS, C_WEIGHT,
ACTIVE, ANN, C_ACTIVE, C_ANN, C_FLAG, C_LABEL, C_LEVEL, C_MARKER, C_PRIORITY, C_TAGS, C_WEIGHT,
IS_INTERNAL, LABEL, PRIORITY, TAGS, WEIGHT,
};
use crate::custopts::{carrier, Annotation, Carrier};
use buffa::{Extension, ExtensionSet, Message};
use crate::custopts::{carrier, Annotation, Carrier, Level};
use buffa::{Enumeration, Extension, ExtensionSet, Message};

/// Sanity: field numbers, extendees, and codec types match the proto source.
/// Codec types are enforced by the `const` binding itself — a mismatch is a
Expand Down Expand Up @@ -253,6 +253,20 @@ fn extension_or_default_no_declared_default_uses_type_default() {
assert!(!c.extension_or_default(&C_FLAG));
}

/// A closed enum need not declare 0, so a closed-enum extension without a
/// `[default]` reads absent as the enum's first value, the same number an
/// unset field of that enum reads as.
#[test]
fn closed_enum_extension_or_default_is_first_value() {
let mut c = Carrier::default();
assert_eq!(c.extension(&C_LEVEL), None);
assert_eq!(c.extension_or_default(&C_LEVEL), Level::LOW.to_i32());
assert_eq!(Level::default(), Level::LOW);

c.set_extension(&C_LEVEL, Level::HIGH.to_i32());
assert_eq!(c.extension_or_default(&C_LEVEL), Level::HIGH.to_i32());
}

/// The declared default survives encode/decode: a freshly-decoded carrier
/// with no extension data still returns the proto default. (The default lives
/// in the generated `const`, not on the wire.)
Expand All @@ -273,10 +287,21 @@ fn proto2_default_after_roundtrip() {
// Group-encoded extensions (editions DELIMITED)
// ────────────────────────────────────────────────────────────────────────────

use crate::groupext::__buffa::ext::{DELIM_INNER, DELIM_REPEATED};
use crate::groupext::{Carrier as GroupCarrier, Inner};
use crate::groupext::__buffa::ext::{DELIM_INNER, DELIM_REPEATED, LEVEL, MODE};
use crate::groupext::{Carrier as GroupCarrier, Inner, Level as EditionsLevel};
use buffa::extension::codecs::{GroupCodec, Repeated};

/// Editions: an extension of an `enum_type = CLOSED` enum without a default
/// reads absent as the first value; one of an open enum still reads 0.
#[test]
fn editions_enum_extension_or_default() {
let c = GroupCarrier::default();
assert_eq!(c.extension(&LEVEL), None);
assert_eq!(c.extension_or_default(&LEVEL), EditionsLevel::LOW.to_i32());
assert_eq!(c.extension(&MODE), None);
assert_eq!(c.extension_or_default(&MODE), 0);
}

/// Codec type check: DELIMITED extension gets `GroupCodec`, not `MessageCodec`.
/// A type mismatch here is a compile error.
#[test]
Expand Down
13 changes: 11 additions & 2 deletions buffa/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
//! # Defaults
//!
//! Proto2 `[default = ...]` values are surfaced by [`extension_or_default`],
//! which returns the declared default when the extension is absent. Presence
//! which returns the declared default when the extension is absent. A
//! closed-enum extension with no declared default returns the enum's first
//! value, as an unset field of that enum does. Presence
//! is still distinguishable via [`extension`] (returns `None`) or
//! [`has_extension`].
//!
Expand Down Expand Up @@ -168,7 +170,9 @@ impl<C: ExtensionCodec> Extension<C> {
/// The function pointer is called lazily on each `extension_or_default`
/// call. For `Copy` scalars codegen emits a `const fn`; for `String` and
/// `bytes` a regular `fn` (allocates on each call — same cost as a
/// hand-written `.unwrap_or_else(|| "x".into())`).
/// hand-written `.unwrap_or_else(|| "x".into())`). Codegen also passes a
/// regular `fn` for a closed-enum extension that declares no default,
/// returning the enum's first value.
///
/// # Panics
///
Expand Down Expand Up @@ -451,6 +455,11 @@ pub trait ExtensionSet {
/// Read a singular extension value, returning the proto2 `[default = ...]`
/// value if absent, or the type's `Default` if no proto default was declared.
///
/// Generated code gives a closed-enum extension without a declared default
/// the enum's first value, which need not be 0, so this returns the same
/// number an unset field of that enum reads as. An open enum's first value
/// is always 0.
///
/// Only meaningful for singular codecs (`Output = Option<Value>`). For
/// repeated codecs use [`extension`](Self::extension) — there is no such
/// thing as a repeated default.
Expand Down
Loading