Skip to content

Commit 662e465

Browse files
committed
Fix more rust extern path bugs; add reexports
1 parent 7ac2086 commit 662e465

5 files changed

Lines changed: 288 additions & 84 deletions

File tree

pkg/plugin/neoeinstein/prost/extern_paths.go

Lines changed: 175 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,100 @@ import (
1313
)
1414

1515
const (
16-
// TransitiveExternPathsKey caches computed extern_path options on the
17-
// library rule's private attrs.
16+
// TransitiveExternPathsKey caches the dependency-only extern_path option
17+
// strings on the library rule's private attrs.
1818
TransitiveExternPathsKey = "_transitive_extern_paths"
19+
// OwnProtoPackagesKey caches the set of proto packages the library
20+
// itself contributes, used to compute self-extern overrides for
21+
// reference-emitting plugins (serde, tonic).
22+
OwnProtoPackagesKey = "_own_proto_packages"
1923
)
2024

2125
// ResolveExternPathOptions filters existing extern_path= options from
22-
// cfg.Options, resolves transitive extern paths, and returns the combined
23-
// options list. This is the common implementation shared by prost, serde,
24-
// and tonic plugins.
26+
// cfg.Options, resolves transitive dependency extern paths, and returns the
27+
// combined options list.
28+
//
29+
// This variant is used by protoc-gen-prost. It does NOT add self-extern
30+
// overrides for the library's own packages because prost interprets such an
31+
// entry as "this package is external — skip generating types for it" and
32+
// emits an empty stub.
33+
//
34+
// It also drops any dependency extern_path whose proto package is a strict
35+
// prefix-parent of one of the library's own packages, for the same reason:
36+
// prost's prefix-matching extern_path semantics treat a sub-package as
37+
// matched and skip generation. Cross-crate references that would otherwise
38+
// have used those filtered extern_paths emerge from prost as relative
39+
// super::... paths; the proto_rust_library macro's generated lib.rs adds
40+
// re-export shims to satisfy them.
2541
func ResolveExternPathOptions(cfg *protoc.PluginConfiguration, r *rule.Rule, from label.Label) []string {
26-
externPaths := ResolveTransitiveExternPaths(r, from)
42+
parents := ResolveTransitiveExternPaths(r, from)
43+
owns := ownProtoPackages(r, from)
44+
if len(owns) > 0 {
45+
filtered := make([]string, 0, len(parents))
46+
for _, ep := range parents {
47+
pkg := externPathPackage(ep)
48+
if pkg != "" && isParentOfAnyOwn(pkg, owns) {
49+
continue
50+
}
51+
filtered = append(filtered, ep)
52+
}
53+
parents = filtered
54+
}
55+
return mergeExternPathOptions(cfg, parents)
56+
}
2757

28-
options := make([]string, 0)
29-
for _, opt := range cfg.Options {
30-
if !strings.HasPrefix(opt, "extern_path=") {
31-
options = append(options, opt)
58+
// externPathPackage extracts the proto package from an "extern_path=.{pkg}=..."
59+
// option string, or returns "" if the input doesn't match the expected
60+
// format.
61+
func externPathPackage(opt string) string {
62+
const prefix = "extern_path=."
63+
if !strings.HasPrefix(opt, prefix) {
64+
return ""
65+
}
66+
rest := opt[len(prefix):]
67+
eq := strings.IndexByte(rest, '=')
68+
if eq < 0 {
69+
return ""
70+
}
71+
return rest[:eq]
72+
}
73+
74+
// isParentOfAnyOwn reports whether pkg equals, or is a strict
75+
// proto-package-prefix parent of, any package in ownPackages.
76+
func isParentOfAnyOwn(pkg string, ownPackages map[string]bool) bool {
77+
for own := range ownPackages {
78+
if own == pkg || strings.HasPrefix(own, pkg+".") {
79+
return true
3280
}
3381
}
82+
return false
83+
}
3484

35-
options = append(options, externPaths...)
36-
return options
85+
// ResolveExternPathOptionsForReferences returns ResolveExternPathOptions plus
86+
// self extern_path entries for the library's own proto packages whenever any
87+
// of those packages is a strict sub-package of an imported (parent) package.
88+
//
89+
// Used by protoc-gen-prost-serde and protoc-gen-tonic. Both emit Rust code at
90+
// crate-root using absolute crate-qualified paths; without a self-extern
91+
// override prost's longest-prefix-wins matching would route a reference like
92+
// ".pkg.sub.MyType" through the parent's external crate instead of resolving
93+
// it to crate::pkg::sub::MyType.
94+
func ResolveExternPathOptionsForReferences(cfg *protoc.PluginConfiguration, r *rule.Rule, from label.Label) []string {
95+
parents := ResolveTransitiveExternPaths(r, from)
96+
owns := ownProtoPackages(r, from)
97+
selves := selfExternPathsForOverride(owns, parents)
98+
99+
all := make([]string, 0, len(parents)+len(selves))
100+
all = append(all, parents...)
101+
all = append(all, selves...)
102+
sort.Strings(all)
103+
return mergeExternPathOptions(cfg, all)
37104
}
38105

39-
// ResolveTransitiveExternPaths walks the transitive dependency graph of proto
40-
// files and builds extern_path option strings for each dependency package.
106+
// ResolveTransitiveExternPaths walks the transitive dependency graph of
107+
// proto files and builds an extern_path option string for each dependency
108+
// package. Self-extern overrides are NOT included — see
109+
// ResolveExternPathOptionsForReferences for the variant that adds them.
41110
func ResolveTransitiveExternPaths(r *rule.Rule, from label.Label) []string {
42111
lib := r.PrivateAttr(protoc.ProtoLibraryKey)
43112
if lib == nil {
@@ -46,34 +115,17 @@ func ResolveTransitiveExternPaths(r *rule.Rule, from label.Label) []string {
46115
library := lib.(protoc.ProtoLibrary)
47116
libRule := library.Rule()
48117

49-
// Check cache
50118
if cached, ok := libRule.PrivateAttr(TransitiveExternPathsKey).([]string); ok {
51119
return cached
52120
}
53121

54122
resolver := protoc.GlobalResolver()
55123

56-
// Build set of own proto files to exclude from extern_paths
57124
ownFiles := make(map[string]bool)
58125
for _, src := range library.Srcs() {
59126
ownFiles[path.Join(from.Pkg, src)] = true
60127
}
61128

62-
// Build set of own proto packages. prost's extern_path matches by package
63-
// prefix, so any imported package that equals or is a parent of one of our
64-
// own packages would cause prost to rewrite our own types as references
65-
// into the imported crate. Collect own packages here and use them below
66-
// to filter such entries out.
67-
ownPackages := make(map[string]bool)
68-
for ownFile := range ownFiles {
69-
for _, ext := range resolver.Resolve("proto", "prost_extern", ownFile) {
70-
if ext.Label.Pkg != "" {
71-
ownPackages[ext.Label.Pkg] = true
72-
}
73-
}
74-
}
75-
76-
// BFS over transitive proto file dependencies
77129
seen := make(map[string]bool)
78130
stack := list.New()
79131
for _, src := range library.Srcs() {
@@ -92,54 +144,39 @@ func ResolveTransitiveExternPaths(r *rule.Rule, from label.Label) []string {
92144
}
93145
seen[protofile] = true
94146

95-
// Walk dependencies
96147
depends := resolver.Resolve("proto", "depends", protofile)
97148
for _, dep := range depends {
98149
depFile := path.Join(dep.Label.Pkg, dep.Label.Name)
99150
stack.PushBack(depFile)
100151
}
101152

102-
// Skip own files
103153
if ownFiles[protofile] {
104154
continue
105155
}
106156

107-
// Skip well-known types
157+
// Skip well-known types — prost ships these built-in.
108158
if strings.HasPrefix(protofile, "google/protobuf/") {
109159
continue
110160
}
111161

112-
// Look up prost_extern data for this proto file
113162
results := resolver.Resolve("proto", "prost_extern", protofile)
114163
if len(results) == 0 {
115164
continue
116165
}
117166

118167
first := results[0]
119-
protoPackage := first.Label.Pkg // proto package name
120-
crateName := first.Label.Name // crate name (e.g., "v1beta1_rs")
121-
168+
protoPackage := first.Label.Pkg
169+
crateName := first.Label.Name
122170
if protoPackage == "" {
123171
continue
124172
}
125-
126-
// Skip extern_path entries that would shadow our own packages. prost's
127-
// extern_path matches by package prefix, so emitting one for a package
128-
// that equals or is a parent of one of our own packages would cause
129-
// prost to rewrite our own type references into the imported crate.
130-
if isOwnOrParentOfOwn(protoPackage, ownPackages) {
131-
continue
132-
}
133-
134-
// Deduplicate by proto package
135173
if _, exists := externPathsByPackage[protoPackage]; exists {
136174
continue
137175
}
138176

139177
// extern_path=.{proto_package}=::{crate_name}::{rust_module_path}
140178
rustModulePath := strings.ReplaceAll(protoPackage, ".", "::")
141-
externPath := "extern_path=." + protoPackage + "=::" + crateName + "::" + rustModulePath
142-
externPathsByPackage[protoPackage] = externPath
179+
externPathsByPackage[protoPackage] = "extern_path=." + protoPackage + "=::" + crateName + "::" + rustModulePath
143180
}
144181

145182
result := make([]string, 0, len(externPathsByPackage))
@@ -148,20 +185,98 @@ func ResolveTransitiveExternPaths(r *rule.Rule, from label.Label) []string {
148185
}
149186
sort.Strings(result)
150187

151-
// Cache on the library rule
152188
libRule.SetPrivateAttr(TransitiveExternPathsKey, result)
153-
154189
return result
155190
}
156191

157-
// isOwnOrParentOfOwn reports whether protoPackage equals one of ownPackages
158-
// or is a proto-package-prefix parent of one (e.g. "a.b" is a parent of
159-
// "a.b.c"). Used to filter dependency extern_path entries that would
160-
// otherwise shadow the current library's own type references through prost's
161-
// prefix-matching extern_path semantics.
162-
func isOwnOrParentOfOwn(protoPackage string, ownPackages map[string]bool) bool {
163-
for own := range ownPackages {
164-
if own == protoPackage || strings.HasPrefix(own, protoPackage+".") {
192+
// mergeExternPathOptions strips any pre-existing extern_path= entries from
193+
// cfg.Options and returns the remainder concatenated with the supplied
194+
// extern_path strings.
195+
func mergeExternPathOptions(cfg *protoc.PluginConfiguration, externPaths []string) []string {
196+
options := make([]string, 0, len(cfg.Options)+len(externPaths))
197+
for _, opt := range cfg.Options {
198+
if !strings.HasPrefix(opt, "extern_path=") {
199+
options = append(options, opt)
200+
}
201+
}
202+
options = append(options, externPaths...)
203+
return options
204+
}
205+
206+
// ownProtoPackages returns the set of proto packages the library itself
207+
// contributes, computed from prost_extern resolver entries for each own
208+
// proto file. Cached on the library rule.
209+
func ownProtoPackages(r *rule.Rule, from label.Label) map[string]bool {
210+
lib := r.PrivateAttr(protoc.ProtoLibraryKey)
211+
if lib == nil {
212+
return nil
213+
}
214+
library := lib.(protoc.ProtoLibrary)
215+
libRule := library.Rule()
216+
217+
if cached, ok := libRule.PrivateAttr(OwnProtoPackagesKey).(map[string]bool); ok {
218+
return cached
219+
}
220+
221+
resolver := protoc.GlobalResolver()
222+
out := make(map[string]bool)
223+
for _, src := range library.Srcs() {
224+
ownFile := path.Join(from.Pkg, src)
225+
for _, ext := range resolver.Resolve("proto", "prost_extern", ownFile) {
226+
if ext.Label.Pkg != "" {
227+
out[ext.Label.Pkg] = true
228+
}
229+
}
230+
}
231+
232+
libRule.SetPrivateAttr(OwnProtoPackagesKey, out)
233+
return out
234+
}
235+
236+
// selfExternPathsForOverride returns "extern_path=.{ownPkg}=crate::..."
237+
// entries for every own proto package whose path is a strict sub-package of
238+
// any package present in parents. parents is the slice of dependency
239+
// extern_path option strings (as returned by ResolveTransitiveExternPaths).
240+
func selfExternPathsForOverride(ownPackages map[string]bool, parents []string) []string {
241+
if len(ownPackages) == 0 || len(parents) == 0 {
242+
return nil
243+
}
244+
parentPkgs := parentExternPackages(parents)
245+
out := make([]string, 0)
246+
for ownPkg := range ownPackages {
247+
if !hasParentInImports(ownPkg, parentPkgs) {
248+
continue
249+
}
250+
rustModulePath := strings.ReplaceAll(ownPkg, ".", "::")
251+
out = append(out, "extern_path=."+ownPkg+"=crate::"+rustModulePath)
252+
}
253+
return out
254+
}
255+
256+
// parentExternPackages parses a slice of "extern_path=.{pkg}=..." strings
257+
// and returns the set of proto packages they cover.
258+
func parentExternPackages(opts []string) map[string]bool {
259+
out := make(map[string]bool, len(opts))
260+
const prefix = "extern_path=."
261+
for _, opt := range opts {
262+
if !strings.HasPrefix(opt, prefix) {
263+
continue
264+
}
265+
rest := opt[len(prefix):]
266+
eq := strings.IndexByte(rest, '=')
267+
if eq < 0 {
268+
continue
269+
}
270+
out[rest[:eq]] = true
271+
}
272+
return out
273+
}
274+
275+
// hasParentInImports reports whether any of importedPackages is a proto-
276+
// package-prefix parent of ownPkg (e.g. "a.b" is a parent of "a.b.c").
277+
func hasParentInImports(ownPkg string, importedPackages map[string]bool) bool {
278+
for imp := range importedPackages {
279+
if strings.HasPrefix(ownPkg, imp+".") {
165280
return true
166281
}
167282
}

0 commit comments

Comments
 (0)