|
| 1 | +package prost |
| 2 | + |
| 3 | +import ( |
| 4 | + "container/list" |
| 5 | + "path" |
| 6 | + "sort" |
| 7 | + "strings" |
| 8 | + |
| 9 | + "github.com/bazelbuild/bazel-gazelle/label" |
| 10 | + "github.com/bazelbuild/bazel-gazelle/rule" |
| 11 | + |
| 12 | + "github.com/stackb/rules_proto/v4/pkg/protoc" |
| 13 | +) |
| 14 | + |
| 15 | +const ( |
| 16 | + // TransitiveExternPathsKey caches the dependency-only extern_path option |
| 17 | + // strings on the library rule's private attrs. |
| 18 | + 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" |
| 23 | +) |
| 24 | + |
| 25 | +// ResolveExternPathOptions filters existing extern_path= options from |
| 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. |
| 41 | +func ResolveExternPathOptions(cfg *protoc.PluginConfiguration, r *rule.Rule, from label.Label) []string { |
| 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 | +} |
| 57 | + |
| 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 |
| 80 | + } |
| 81 | + } |
| 82 | + return false |
| 83 | +} |
| 84 | + |
| 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) |
| 104 | +} |
| 105 | + |
| 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. |
| 110 | +func ResolveTransitiveExternPaths(r *rule.Rule, from label.Label) []string { |
| 111 | + lib := r.PrivateAttr(protoc.ProtoLibraryKey) |
| 112 | + if lib == nil { |
| 113 | + return nil |
| 114 | + } |
| 115 | + library := lib.(protoc.ProtoLibrary) |
| 116 | + libRule := library.Rule() |
| 117 | + |
| 118 | + if cached, ok := libRule.PrivateAttr(TransitiveExternPathsKey).([]string); ok { |
| 119 | + return cached |
| 120 | + } |
| 121 | + |
| 122 | + resolver := protoc.GlobalResolver() |
| 123 | + |
| 124 | + ownFiles := make(map[string]bool) |
| 125 | + for _, src := range library.Srcs() { |
| 126 | + ownFiles[path.Join(from.Pkg, src)] = true |
| 127 | + } |
| 128 | + |
| 129 | + seen := make(map[string]bool) |
| 130 | + stack := list.New() |
| 131 | + for _, src := range library.Srcs() { |
| 132 | + stack.PushBack(path.Join(from.Pkg, src)) |
| 133 | + } |
| 134 | + |
| 135 | + externPathsByPackage := make(map[string]string) |
| 136 | + |
| 137 | + for stack.Len() > 0 { |
| 138 | + current := stack.Front() |
| 139 | + stack.Remove(current) |
| 140 | + |
| 141 | + protofile := current.Value.(string) |
| 142 | + if seen[protofile] { |
| 143 | + continue |
| 144 | + } |
| 145 | + seen[protofile] = true |
| 146 | + |
| 147 | + depends := resolver.Resolve("proto", "depends", protofile) |
| 148 | + for _, dep := range depends { |
| 149 | + depFile := path.Join(dep.Label.Pkg, dep.Label.Name) |
| 150 | + stack.PushBack(depFile) |
| 151 | + } |
| 152 | + |
| 153 | + if ownFiles[protofile] { |
| 154 | + continue |
| 155 | + } |
| 156 | + |
| 157 | + // Skip well-known types — prost ships these built-in. |
| 158 | + if strings.HasPrefix(protofile, "google/protobuf/") { |
| 159 | + continue |
| 160 | + } |
| 161 | + |
| 162 | + results := resolver.Resolve("proto", "prost_extern", protofile) |
| 163 | + if len(results) == 0 { |
| 164 | + continue |
| 165 | + } |
| 166 | + |
| 167 | + first := results[0] |
| 168 | + protoPackage := first.Label.Pkg |
| 169 | + crateName := first.Label.Name |
| 170 | + if protoPackage == "" { |
| 171 | + continue |
| 172 | + } |
| 173 | + if _, exists := externPathsByPackage[protoPackage]; exists { |
| 174 | + continue |
| 175 | + } |
| 176 | + |
| 177 | + // extern_path=.{proto_package}=::{crate_name}::{rust_module_path} |
| 178 | + rustModulePath := strings.ReplaceAll(protoPackage, ".", "::") |
| 179 | + externPathsByPackage[protoPackage] = "extern_path=." + protoPackage + "=::" + crateName + "::" + rustModulePath |
| 180 | + } |
| 181 | + |
| 182 | + result := make([]string, 0, len(externPathsByPackage)) |
| 183 | + for _, ep := range externPathsByPackage { |
| 184 | + result = append(result, ep) |
| 185 | + } |
| 186 | + sort.Strings(result) |
| 187 | + |
| 188 | + libRule.SetPrivateAttr(TransitiveExternPathsKey, result) |
| 189 | + return result |
| 190 | +} |
| 191 | + |
| 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+".") { |
| 280 | + return true |
| 281 | + } |
| 282 | + } |
| 283 | + return false |
| 284 | +} |
0 commit comments