Skip to content
Merged
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
54 changes: 35 additions & 19 deletions buffa-codegen/src/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Controls whether types are emitted as short names (e.g. `Option<T>`) or
//! fully-qualified paths (e.g. `::core::option::Option<T>`), by detecting
//! collisions with proto-defined type names in the current file.
//! collisions with proto-defined type names in the current scope.
//!
//! `core` prelude types (`Option`, `Default`, etc.) are in scope in both `std`
//! and `no_std` contexts and can be emitted as bare names unless shadowed.
Expand All @@ -14,7 +14,7 @@

use std::collections::HashSet;

use crate::generated::descriptor::FileDescriptorProto;
use crate::generated::descriptor::{DescriptorProto, FileDescriptorProto};
use proc_macro2::TokenStream;
use quote::quote;

Expand All @@ -27,7 +27,18 @@ use quote::quote;
/// Those types are always emitted via `::buffa::alloc::*` re-exports.
const PRELUDE_NAMES: &[&str] = &["Option"];

/// Tracks which short names are safe to use in a generated file.
fn check_names_for_prelude_collisions<'a>(names: impl Iterator<Item = &'a str>) -> HashSet<String> {
let prelude: HashSet<&str> = PRELUDE_NAMES.iter().copied().collect();
let mut blocked = HashSet::new();
for name in names {
if prelude.contains(name) {
blocked.insert(name.to_string());
}
}
blocked
}

/// Tracks which short names are safe to use in a generated scope.
pub(crate) struct ImportResolver {
/// Proto type names that collide with prelude names.
blocked: HashSet<String>,
Expand All @@ -37,24 +48,29 @@ impl ImportResolver {
/// Build a resolver for a single `.proto` file by checking top-level
/// message and enum names against the set of short names we want to use.
pub fn for_file(file: &FileDescriptorProto) -> Self {
let mut proto_names = HashSet::new();
for msg in &file.message_type {
if let Some(name) = &msg.name {
proto_names.insert(name.as_str());
}
}
for e in &file.enum_type {
if let Some(name) = &e.name {
proto_names.insert(name.as_str());
}
let names = file
.message_type
.iter()
.filter_map(|m| m.name.as_deref())
.chain(file.enum_type.iter().filter_map(|e| e.name.as_deref()));
Self {
blocked: check_names_for_prelude_collisions(names),
}
}

let mut blocked = HashSet::new();
for &name in PRELUDE_NAMES {
if proto_names.contains(name) {
blocked.insert(name.to_string());
}
}
/// Build a child resolver for a message's `pub mod` scope.
///
/// Each message module contains `use super::*`, so parent-scope blocked
/// names propagate. On top of those, the message's own nested types and
/// nested enums introduce additional names that can shadow prelude types.
pub fn child_for_message(&self, msg: &DescriptorProto) -> Self {
let mut blocked = self.blocked.clone();
let child_names = msg
.nested_type
.iter()
.filter_map(|m| m.name.as_deref())
.chain(msg.enum_type.iter().filter_map(|e| e.name.as_deref()));
blocked.extend(check_names_for_prelude_collisions(child_names));
Self { blocked }
}

Expand Down
15 changes: 11 additions & 4 deletions buffa-codegen/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ fn generate_message_with_nesting(

// Nested messages (skip map entry synthetics) — simple name, emitted
// inside the message's module.
//
// The child resolver inherits parent-scope blocked names (via
// `use super::*`) and adds this message's nested types/enums, so that
// a nested message named `Option` causes `::core::option::Option` to
// be emitted in struct fields within this module scope.
let child_resolver = resolver.child_for_message(msg);
let nested_msgs = msg
.nested_type
.iter()
Expand All @@ -137,7 +143,7 @@ fn generate_message_with_nesting(
scope.nested(&nested_fqn, &msg_features),
nested,
nested_proto_name,
resolver,
&child_resolver,
)
})
.collect::<Result<Vec<_>, _>>()?;
Expand Down Expand Up @@ -874,7 +880,7 @@ fn custom_deser_regular_field(
quote! { #json_name => { #var_ident = Some(#deser_expr); } }
};

let var_decl = quote! { let mut #var_ident: Option<#rust_type> = None; };
let var_decl = quote! { let mut #var_ident: ::core::option::Option<#rust_type> = None; };
// Overwrite only if present — missing fields keep the struct's Default
// (which honours proto2 [default = X], unlike <T>::default()).
let field_init = quote! {
Expand Down Expand Up @@ -915,7 +921,8 @@ fn custom_deser_oneof_group(
let field_ident = make_field_ident(oneof_name);

// Oneof enum lives in the message's module.
let var_decl = quote! { let mut #var_ident: Option<#mod_ident::#enum_ident> = None; };
let var_decl =
quote! { let mut #var_ident: ::core::option::Option<#mod_ident::#enum_ident> = None; };
let mut arms = Vec::new();

for field in &msg.field {
Expand Down Expand Up @@ -1631,7 +1638,7 @@ fn skip_serializing_predicate(
} else if info.is_repeated {
Some("::buffa::json_helpers::skip_if::is_empty_vec")
} else if info.is_optional {
Some("Option::is_none")
Some("::core::option::Option::is_none")
} else {
singular_skip_predicate(field_type, features)
}
Expand Down
4 changes: 2 additions & 2 deletions buffa-codegen/src/oneof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,13 +437,13 @@ pub(crate) fn oneof_variant_deser_arm(input: &OneofVariantDeserInput<'_>) -> Tok
#helper::deserialize(d)
}
}
let v: Option<#variant_type> = map.next_value_seed(
let v: ::core::option::Option<#variant_type> = map.next_value_seed(
::buffa::json_helpers::NullableDeserializeSeed(_DeserSeed)
)?;
}
} else {
quote! {
let v: Option<#variant_type> = map.next_value_seed(
let v: ::core::option::Option<#variant_type> = map.next_value_seed(
::buffa::json_helpers::NullableDeserializeSeed(
::buffa::json_helpers::DefaultDeserializeSeed::<#variant_type>::new()
)
Expand Down
175 changes: 175 additions & 0 deletions buffa-codegen/src/tests/naming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,181 @@ fn test_proto3_optional_field_name_matches_nested_enum_no_conflict() {
);
}

#[test]
fn test_nested_message_named_option_does_not_shadow_prelude() {
// Reproduces gh#36: a nested message named `Option` shadows
// `core::option::Option`, causing `pub value: Option<option::Value>` to
// resolve to the proto struct instead of the standard library type.
// The codegen must emit `::core::option::Option<...>` in this scope.
let option_msg = DescriptorProto {
name: Some("Option".to_string()),
field: vec![
make_field("title", 1, Label::LABEL_OPTIONAL, Type::TYPE_STRING),
{
let mut f = make_field("int_value", 2, Label::LABEL_OPTIONAL, Type::TYPE_UINT64);
f.oneof_index = Some(0);
f
},
],
oneof_decl: vec![OneofDescriptorProto {
name: Some("value".to_string()),
..Default::default()
}],
..Default::default()
};
let picker_msg = DescriptorProto {
name: Some("Picker".to_string()),
field: vec![{
let mut f = make_field("options", 1, Label::LABEL_REPEATED, Type::TYPE_MESSAGE);
f.type_name = Some(".test.option_shadow.Picker.Option".to_string());
f
}],
nested_type: vec![option_msg],
..Default::default()
};
let mut file = proto3_file("option_shadow.proto");
file.package = Some("test.option_shadow".to_string());
file.message_type = vec![picker_msg];

let config = CodeGenConfig {
generate_views: false,
..Default::default()
};
let result = generate(&[file], &["option_shadow.proto".to_string()], &config);
let files = result.expect("nested Option message should not break codegen");
let content = &files[0].content;
assert!(
content.contains("pub struct Option"),
"nested Option struct must exist: {content}"
);
// The oneof field on Option must use the fully-qualified
// `::core::option::Option` to avoid resolving to the proto struct.
assert!(
!content.contains("pub value: Option<"),
"bare Option<> in struct field would shadow core::option::Option: {content}"
);
assert!(
content.contains("::core::option::Option<"),
"must use fully-qualified ::core::option::Option: {content}"
);
}

#[test]
fn test_top_level_message_named_option_qualifies_option() {
// A top-level message named `Option` — file-level ImportResolver should
// detect this and qualify all Option type references in the file.
let mut file = proto3_file("option_top.proto");
file.package = Some("pkg".to_string());
file.message_type = vec![
DescriptorProto {
name: Some("Option".to_string()),
..Default::default()
},
DescriptorProto {
name: Some("Wrapper".to_string()),
field: vec![{
let mut f = make_field("tag", 1, Label::LABEL_OPTIONAL, Type::TYPE_STRING);
f.proto3_optional = Some(true);
f.oneof_index = Some(0);
f
}],
oneof_decl: vec![OneofDescriptorProto {
name: Some("_tag".to_string()),
..Default::default()
}],
..Default::default()
},
];

let config = CodeGenConfig {
generate_views: false,
..Default::default()
};
let result = generate(&[file], &["option_top.proto".to_string()], &config);
let files = result.expect("top-level Option should not break codegen");
let content = &files[0].content;
// The Wrapper struct must use qualified Option for its optional field.
assert!(
content.contains("::core::option::Option<"),
"must use fully-qualified ::core::option::Option for optional field: {content}"
);
assert!(
!content.contains("pub tag: Option<"),
"bare Option<> on Wrapper field would shadow core::option::Option: {content}"
);
}

#[test]
fn test_nested_option_blocked_propagates_through_sibling_subtree() {
// `Outer { nested Option; nested Middle { nested Inner } }` — `Option`
// is declared in `mod outer`, so it shadows the prelude there AND in
// `mod outer::middle` via `use super::*`. The child resolver for
// `Middle` must inherit the parent's blocked set so that `Inner`
// (emitted inside `mod outer::middle`) qualifies its optional field.
let inner_msg = DescriptorProto {
name: Some("Inner".to_string()),
field: vec![{
let mut f = make_field("x", 1, Label::LABEL_OPTIONAL, Type::TYPE_INT32);
f.proto3_optional = Some(true);
f.oneof_index = Some(0);
f
}],
oneof_decl: vec![OneofDescriptorProto {
name: Some("_x".to_string()),
..Default::default()
}],
..Default::default()
};
let middle_msg = DescriptorProto {
name: Some("Middle".to_string()),
field: vec![{
let mut f = make_field("note", 1, Label::LABEL_OPTIONAL, Type::TYPE_STRING);
f.proto3_optional = Some(true);
f.oneof_index = Some(0);
f
}],
oneof_decl: vec![OneofDescriptorProto {
name: Some("_note".to_string()),
..Default::default()
}],
nested_type: vec![inner_msg],
..Default::default()
};
let outer_msg = DescriptorProto {
name: Some("Outer".to_string()),
nested_type: vec![
DescriptorProto {
name: Some("Option".to_string()),
..Default::default()
},
middle_msg,
],
..Default::default()
};
let mut file = proto3_file("option_deep.proto");
file.package = Some("pkg".to_string());
file.message_type = vec![outer_msg];

let config = CodeGenConfig {
generate_views: false,
..Default::default()
};
let files = generate(&[file], &["option_deep.proto".to_string()], &config)
.expect("nested Option sibling should not break codegen");
let content = &files[0].content;
// `Middle.note` lives in `mod outer` (Option in scope); `Inner.x` lives
// in `mod outer::middle` (Option in scope via `use super::*`). Both must
// be qualified.
assert!(
!content.contains("pub note: Option<"),
"Middle.note must qualify Option (sibling collision): {content}"
);
assert!(
!content.contains("pub x: Option<"),
"Inner.x must qualify Option (inherited via use super::*): {content}"
);
}

#[test]
fn test_message_named_type_with_nested() {
// Proto message named "Type" (a Rust keyword) with a nested message.
Expand Down
10 changes: 10 additions & 0 deletions buffa-test/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ fn main() {
.compile()
.expect("buffa_build failed for name_collisions.proto");

// Prelude shadowing (gh#36) — nested `message Option` with optional/oneof
// fields, built with views + JSON so all `Option<...>` emission paths are
// exercised. Compilation is the assertion.
buffa_build::Config::new()
.files(&["protos/prelude_shadow.proto"])
.includes(&["protos/"])
.generate_json(true)
.compile()
.expect("buffa_build failed for prelude_shadow.proto");

// Proto2 with custom defaults, required fields, closed enums.
buffa_build::Config::new()
.files(&["protos/proto2_defaults.proto"])
Expand Down
Loading
Loading