fix: resolve oneof enum name collisions with nested types#34
fix: resolve oneof enum name collisions with nested types#34iainmcgin merged 4 commits intoanthropics:mainfrom
Conversation
) When a oneof's PascalCase name collides with a nested message or enum name in the same module, suffix the oneof enum with "Oneof" instead of returning a hard NestedTypeOneofConflict error. This unblocks protos like riderperks PerkRestrictions (message RegionCodes + oneof region_codes). The suffix is applied consistently across all code paths: struct fields, enum definitions, view enums, binary encode/decode, text format, and JSON serde. If the suffixed name also collides (pathological case), an OneofSuffixConflict error is returned. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Made-with: Cursor
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
When a nested type triggers the `Oneof` suffix for one oneof enum (e.g., `my_field` → `MyFieldOneof`), a second oneof named `my_field_oneof` would also produce `MyFieldOneof`, creating duplicate type names. Introduce `resolve_oneof_idents` which processes oneofs sequentially, growing the reserved set after each allocation. All call sites now look up pre-computed idents instead of independently calling `oneof_enum_ident`, ensuring consistency across struct fields, binary encode/decode, text format, JSON serde, and view generation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Made-with: Cursor
|
Added a follow-up commit addressing a code review finding: sequential oneof ident allocation to prevent sibling oneofs from claiming the same suffixed name. Example: nested message |
- Tie owned and view oneof identifier selection together. When view
codegen is enabled the owned name was chosen based only on nested
owned names, so a oneof 'my_field' against a sibling nested message
'MyFieldView' produced owned enum 'MyField' plus view enum
'MyFieldView' — colliding with the nested message's own struct in
the same module. Reserved names now carry a view-side set and
insert_owned reserves '{name}View' whenever a new owned ident is
allocated; the suffix 'Oneof' is applied when EITHER side would
collide, so both enums stay in lockstep (MyFieldOneof and
MyFieldOneofView in the conflicting case).
- Add 'scope' (parent message FQN) to OneofSuffixConflict so the error
identifies which message triggered the conflict, matching
ModuleNameConflict / ViewNameConflict diagnostics.
- Tests cover the view-shadowing case and the scope field in the error.
Previously the oneof enum took the PascalCase of the oneof name, with
a fallback 'Oneof' suffix applied only when a collision was detected.
That coupling made the generated name a silent function of which
sibling types exist, so adding a nested message whose Rust name
happened to match an existing oneof would rename the enum and break
every downstream caller without any codegen-level error.
Apply the suffix uniformly: 'oneof my_field' always generates 'pub
enum MyFieldOneof' (and 'pub enum MyFieldOneofView<'a>' when view
generation is enabled), regardless of what siblings the message has.
The rule is now derivable from the .proto alone.
Hard-error CodeGenError::OneofNameConflict remains for the case where
a user has a nested type literally named '{Name}Oneof' (or a
sibling's view struct would collide with the oneof's view).
Along with the rename:
- Sequential allocation across sibling oneofs continues to prevent
within-message duplicates.
- Generated WKT and bootstrap-descriptor code regenerated; the handful
of hand-written WKT helpers (Struct/Value ergonomics, Envelope,
collision tests) updated for the new Value::KindOneof /
envelope::ContentOneof / Status oneof names.
- docs/guide.md rewritten to describe the uniform rule.
This is a source-breaking change for consumers that named oneof enums
directly. A follow-up will move all name-derived generated types
(oneof enums, view structs, ...) into dedicated sub-modules to remove
the suffix convention entirely; this PR is the intermediate step
captured in the rationale comment on CodeGenError::OneofNameConflict.
d98f960 to
be40224
Compare
|
Thanks for the PR! I took the branch forward with a few changes before review. Summary of what's changed relative to your initial version: Naming rule — switched from conflict-driven to uniform suffix Your PR kept the oneof enum at its PascalCase form (MyField) in the no-conflict case and appended Oneof only when a sibling nested type forced it. I changed this to apply the Oneof suffix unconditionally: oneof my_field always emits pub enum MyFieldOneof (and pub enum MyFieldOneofView<'a> when views are on), regardless of sibling names. The reason: the prior rule made the generated type name a silent function of which sibling types exist in the same message. Adding a nested message whose Rust name happens to match an existing oneof would rename the enum from MyField to MyFieldOneof and break every downstream caller with no codegen-level signal. Uniform suffixing makes the Rust name derivable from the .proto alone and makes the addition of nested types non-breaking. It costs a one-time source break for existing buffa users, which is worth taking before 1.0. A follow-up will likely move all name-derived generated types (oneofs, views, etc.) into dedicated sub-modules so the Oneof suffix goes away entirely — this PR is the intermediate step. Error variant renamed CodeGenError::NestedTypeOneofConflict → CodeGenError::OneofNameConflict { scope, oneof_name, attempted }. The scope field (parent message FQN) was missing from the original error; it's important for diagnosing conflicts in large descriptor sets. The variant now fires only when {Name}Oneof itself already names another item in the enclosing scope, at which point the user must rename in the .proto. View-side collision fix I caught a latent bug where the derived {Name}OneofView could collide with a sibling nested message's view struct. Reserved-name tracking now spans both the owned and view namespaces, and sibling oneofs grow each other's reserved sets so two oneofs in the same message can't end up claiming the same view enum name. Generated-code regeneration + hand-written updates Because this is a workspace-breaking rename, I regenerated buffa-types/src/generated/ and buffa-descriptor/src/generated/ (via task gen-wkt-types / task gen-bootstrap-types) and updated the hand-written call sites in buffa-types/src/value_ext.rs, the collisions::status tests, and the envelope tests to use the new names (Value::KindOneof, Status::StatusOneof, envelope::ContentOneof, etc.). Docs Rewrote the "Oneofs" section in docs/guide.md to describe the uniform naming rule. Tests
|
Summary
Fixes #31.
When a protobuf message has a nested type (message or enum) whose PascalCase name collides with a oneof enum's PascalCase name, the codegen now suffixes the oneof enum with
Oneofinstead of returning a hardNestedTypeOneofConflicterror.oneof region_codes+message RegionCodes→ generatesRegionCodesOneofenumoneof my_field+message MyField→ generatesMyFieldOneofenumOneofSuffixConflicterror is returnedThe suffix is applied consistently across all code paths: struct field types, enum definitions, view enums (
MyFieldOneofView), binary encode/decode, text format, and JSON serde.Changes
oneof.rs: Addedreserved_names_for_msg()helper and modifiedoneof_enum_ident()to accept a reserved name set and suffix withOneofon collisionmessage.rs: Builds reserved set, threads throughoneof_enum_identcall sitesimpl_message.rs,impl_text.rs,view.rs: Threadmsgparameter to compute reserved names at eachoneof_enum_identcall sitelib.rs: Removedcheck_nested_type_oneof_conflicts()upfront validation andNestedTypeOneofConflicterror variant; addedOneofSuffixConflictfor pathological double-collision casetests/naming.rs: Converted error-asserting test to success-asserting withOneofsuffix check; added tests for nested enum collision, view suffix propagation, and double-collision errorTest plan
test_nested_type_oneof_conflict_resolved_with_suffix— nested message + oneof collision generatesMyFieldOneoftest_nested_enum_oneof_conflict_resolved_with_suffix— nested enum + oneof collision (gh#31 example) generatesRegionCodesOneoftest_nested_type_oneof_conflict_view_uses_suffix— view enum usesMyFieldOneofViewtest_oneof_suffix_double_collision_errors— pathological case errors cleanlycargo testpasses (233 tests)cargo clippy --workspace -- -D warningscleanMade with Cursor