@@ -13,31 +13,100 @@ import (
1313)
1414
1515const (
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.
2541func 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.
41110func 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