Skip to content

fix: resolve oneof enum name collisions with nested types#34

Merged
iainmcgin merged 4 commits intoanthropics:mainfrom
th0114nd:fix/nested-type-oneof-conflict
Apr 16, 2026
Merged

fix: resolve oneof enum name collisions with nested types#34
iainmcgin merged 4 commits intoanthropics:mainfrom
th0114nd:fix/nested-type-oneof-conflict

Conversation

@th0114nd
Copy link
Copy Markdown
Contributor

@th0114nd th0114nd commented Apr 5, 2026

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 Oneof instead of returning a hard NestedTypeOneofConflict error.

  • oneof region_codes + message RegionCodes → generates RegionCodesOneof enum
  • oneof my_field + message MyField → generates MyFieldOneof enum
  • Non-colliding names are unchanged
  • If the suffixed name also collides with a nested type (pathological case), an OneofSuffixConflict error is returned

The 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: Added reserved_names_for_msg() helper and modified oneof_enum_ident() to accept a reserved name set and suffix with Oneof on collision
  • message.rs: Builds reserved set, threads through oneof_enum_ident call sites
  • impl_message.rs, impl_text.rs, view.rs: Thread msg parameter to compute reserved names at each oneof_enum_ident call site
  • lib.rs: Removed check_nested_type_oneof_conflicts() upfront validation and NestedTypeOneofConflict error variant; added OneofSuffixConflict for pathological double-collision case
  • tests/naming.rs: Converted error-asserting test to success-asserting with Oneof suffix check; added tests for nested enum collision, view suffix propagation, and double-collision error

Test plan

  • Existing test_nested_type_oneof_conflict_resolved_with_suffix — nested message + oneof collision generates MyFieldOneof
  • New test_nested_enum_oneof_conflict_resolved_with_suffix — nested enum + oneof collision (gh#31 example) generates RegionCodesOneof
  • New test_nested_type_oneof_conflict_view_uses_suffix — view enum uses MyFieldOneofView
  • New test_oneof_suffix_double_collision_errors — pathological case errors cleanly
  • Existing non-collision tests still pass
  • Full workspace cargo test passes (233 tests)
  • cargo clippy --workspace -- -D warnings clean
  • No WKT regeneration needed (no collisions in well-known types)

Made with Cursor

)

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
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 5, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@th0114nd
Copy link
Copy Markdown
Contributor Author

th0114nd commented Apr 5, 2026

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
@th0114nd
Copy link
Copy Markdown
Contributor Author

th0114nd commented Apr 5, 2026

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 MyField, oneof my_fieldMyFieldOneof, second oneof my_field_oneof → would also produce MyFieldOneof. The fix introduces resolve_oneof_idents() which processes oneofs in declaration order, growing the reserved set after each allocation. All call sites now look up pre-computed idents from a HashMap<usize, Ident> instead of independently calling oneof_enum_ident.

- 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.
@iainmcgin iainmcgin force-pushed the fix/nested-type-oneof-conflict branch from d98f960 to be40224 Compare April 16, 2026 00:03
@iainmcgin
Copy link
Copy Markdown
Collaborator

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

  • Renamed tests to match the new semantics (removed "conflict_resolved_with_suffix" framing since there's no longer a conflict-driven path).
  • Added test_sibling_oneof_view_names_do_not_collide covering the cross-oneof view-name collision case.
  • Added test_oneof_suffix_conflict_error_includes_scope to lock in the new error shape.
  • Existing coverage for sequential allocation and double-collision errors preserved.

@iainmcgin iainmcgin merged commit 8355a95 into anthropics:main Apr 16, 2026
7 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Apr 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NestedTypeOneofConflict: codegen fails when nested message and oneof share PascalCase name

2 participants