codegen: fix silently-ignored per-type extern_path mappings (#111)#153
Conversation
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
|
[claude code] Thanks for the contribution — the implementation reviewed well: the resolution order (exact FQN → file-level → longest prefix → local) is correct, the package-prefix behaviour is unchanged from before, and the workspace builds and tests cleanly with the branch merged into current I pushed one follow-up commit (6fdc28c) directly to this branch with documentation and CHANGELOG updates so the new behaviour is discoverable outside the builder rustdoc:
No changes to your code itself. A couple of smaller items (an emit-time diagnostic when a view-referenced type is mapped to a non-buffa crate, and some extra test coverage for repeated/map/oneof references to overridden types) may get filed as separate follow-up issues rather than expanding this PR. |
…cs#111) (anthropics#153) extern_path entries naming a single type FQN (the prost/tonic idiom, e.g. .google.protobuf.Timestamp = ::pbjson_types::Timestamp) parsed but never matched, because resolution only considered package prefixes at file registration. Resolution now happens per type: an exact type-FQN entry wins over the internal descriptor.proto routing, then the longest matching package prefix, then local generation. Nested types inherit an enclosing message's override. Package-prefix mappings produce byte-identical output to before (regression-tested). Also documents the per-type form across the guide, prost migration notes, plugin help text, and Config::extern_path rustdoc, and adds the CHANGELOG entry. Co-authored-by: Omar Garcia <12629920+ogarciarevett@users.noreply.github.com> Co-authored-by: Iain McGinniss <309153+iainmcgin@users.noreply.github.com>
e18edb9 to
06be737
Compare
Problem
Closes #111.
extern_pathonly matched at the proto package-prefix level. A mapping thatnames a specific type FQN — e.g.
.google.protobuf.Timestamp = ::pbjson_types::Timestamp, a normal prost/tonic idiom — was silentlyignored: the file's package (
google.protobuf) doesn't have.google.protobuf.Timestampas a prefix, so the type resolved to a(non-existent) local module with no diagnostic.
This happened because resolution lived at file registration:
CodeGenContextcomputed one
rust_moduleper file fromresolve_extern_prefix(package, …)andregistered every top-level type under it. prost instead resolves at type
registration against the type's whole FQN (exact-match first, then longest
dotted prefix). Users migrating from prost reach for the per-type form and find
it does nothing.
Solution
Move extern resolution from per-file to per-type, layering prost-style matching
on top of the existing logic (
buffa-codegen/src/context.rs):resolve_extern_type(fqn, extern_paths)— exact FQN match first, then thelongest dotted-prefix entry (a package or an enclosing type). For a prefix
match the trailing proto segments become
snake_casemodules and the finalsegment is kept as the Rust type name. For package prefixes this produces
byte-identical paths to before (guarded by a regression test).
resolve_type_path— applies resolution per type with priority: exactper-type FQN → file-level mapping (the internal
descriptor.proto→buffa-descriptorsplit) → package/prefix → local. Returns whether the typeis extern, so local-module deconfliction (Module redefinition error when messages and packages share a name #135) is unaffected.
register_nested_typeshonors an exact per-type override on a nested FQN,and otherwise inherits the parent's resolved module — so a parent override
cascades to nested types and enums.
__buffa::view/oneof boundary recovery inrust_type_relative_splitkeeps working because
package_ofstill stores the proto package; it nowclamps gracefully instead of asserting.
Config::extern_pathdocs describe the per-type form and precedence.Behavior compatibility
Existing package-level mappings resolve identically to before
(
test_extern_path_package_prefix_still_resolvespins this; a no-leak guardconfirms a per-type entry doesn't bleed onto unrelated types). No checked-in
generated code changes — this only affects resolution.
Limitations
An extern type referenced by a generated view must map to another
buffa-generated crate: the view path is composed as
<rust_path_root>::__buffa::view::…, which a non-buffa crate (e.g.pbjson_types) doesn't provide. For such types, map per-type to a buffa crateor disable views. This is documented on
Config::extern_path. Thediagnostic-warning idea from the issue would make a good follow-up.
Test plan
resolve_extern_type: exact, exact-wins-over-prefix,package-prefix-appends-type, catch-all, no-match, dot-boundary no-match.
CodeGenContexttests: exact per-type, per-type-overrides-package,nested cascade, enum override, package-prefix regression guard, no-leak guard.
rust_type_relative_splitper-type override (top-level + nested) —validates the
__buffa::boundary recovery.cross_package_pertype.protomaps.basic.Person/.basic.Statusper-type tocrate::basic, round-trips end-to-end.cargo fmt/clippy/cargo test --workspaceclean; generated code(WKT + bootstrap) regenerates with zero drift.