github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/proto/generate.go (about) 1 /* Copyright 2018 The Bazel Authors. All rights reserved. 2 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package proto 17 18 import ( 19 "fmt" 20 "log" 21 "path" 22 "sort" 23 "strings" 24 25 "github.com/bazelbuild/bazel-gazelle/config" 26 "github.com/bazelbuild/bazel-gazelle/language" 27 "github.com/bazelbuild/bazel-gazelle/pathtools" 28 "github.com/bazelbuild/bazel-gazelle/rule" 29 ) 30 31 func (*protoLang) GenerateRules(args language.GenerateArgs) language.GenerateResult { 32 c := args.Config 33 pc := GetProtoConfig(c) 34 if !pc.Mode.ShouldGenerateRules() { 35 // Don't create or delete proto rules in this mode. Any existing rules 36 // are likely hand-written. 37 return language.GenerateResult{} 38 } 39 40 var regularProtoFiles []string 41 for _, name := range args.RegularFiles { 42 if strings.HasSuffix(name, ".proto") { 43 regularProtoFiles = append(regularProtoFiles, name) 44 } 45 } 46 47 // Some of the generated files may have been consumed by other rules 48 consumedFileSet := make(map[string]bool) 49 for _, r := range args.OtherGen { 50 if r.Kind() != "proto_library" { 51 continue 52 } 53 for _, f := range r.AttrStrings("srcs") { 54 consumedFileSet[f] = true 55 } 56 } 57 58 // genProtoFilesNotConsumed represents only not consumed generated files. 59 // genProtoFiles represents all generated files. 60 // This is required for not generating empty rules for consumed generated 61 // files. 62 var genProtoFiles, genProtoFilesNotConsumed []string 63 for _, name := range args.GenFiles { 64 if strings.HasSuffix(name, ".proto") { 65 genProtoFiles = append(genProtoFiles, name) 66 if !consumedFileSet[name] { 67 genProtoFilesNotConsumed = append(genProtoFilesNotConsumed, name) 68 } 69 } 70 } 71 pkgs := buildPackages(pc, args.Dir, args.Rel, regularProtoFiles, genProtoFilesNotConsumed) 72 shouldSetVisibility := args.File == nil || !args.File.HasDefaultVisibility() 73 var res language.GenerateResult 74 for _, pkg := range pkgs { 75 r := generateProto(pc, args.Rel, pkg, shouldSetVisibility) 76 if r.IsEmpty(protoKinds[r.Kind()]) { 77 res.Empty = append(res.Empty, r) 78 } else { 79 res.Gen = append(res.Gen, r) 80 } 81 } 82 sort.SliceStable(res.Gen, func(i, j int) bool { 83 return res.Gen[i].Name() < res.Gen[j].Name() 84 }) 85 res.Imports = make([]interface{}, len(res.Gen)) 86 for i, r := range res.Gen { 87 res.Imports[i] = r.PrivateAttr(config.GazelleImportsKey) 88 } 89 res.Empty = append(res.Empty, generateEmpty(args.File, regularProtoFiles, genProtoFiles)...) 90 return res 91 } 92 93 // RuleName returns a name for a proto_library derived from the given strings. 94 // For each string, RuleName will look for a non-empty suffix of identifier 95 // characters and then append "_proto" to that. 96 func RuleName(names ...string) string { 97 base := "root" 98 for _, name := range names { 99 notIdent := func(c rune) bool { 100 return !('A' <= c && c <= 'Z' || 101 'a' <= c && c <= 'z' || 102 '0' <= c && c <= '9' || 103 c == '_') 104 } 105 if i := strings.LastIndexFunc(name, notIdent); i >= 0 { 106 name = name[i+1:] 107 } 108 if name != "" { 109 base = name 110 break 111 } 112 } 113 return base + "_proto" 114 } 115 116 // buildPackage extracts metadata from the .proto files in a directory and 117 // constructs possibly several packages, then selects a package to generate 118 // a proto_library rule for. 119 func buildPackages(pc *ProtoConfig, dir, rel string, protoFiles, genFiles []string) []*Package { 120 packageMap := make(map[string]*Package) 121 for _, name := range protoFiles { 122 info := protoFileInfo(dir, name) 123 key := info.PackageName 124 125 if pc.Mode == FileMode { 126 key = strings.TrimSuffix(name, ".proto") 127 } else if pc.groupOption != "" { // implicitly PackageMode 128 for _, opt := range info.Options { 129 if opt.Key == pc.groupOption { 130 key = opt.Value 131 break 132 } 133 } 134 } 135 136 if packageMap[key] == nil { 137 packageMap[key] = newPackage(info.PackageName) 138 } 139 packageMap[key].addFile(info) 140 if key != info.PackageName { 141 packageMap[key].RuleName = key 142 } 143 } 144 145 switch pc.Mode { 146 case DefaultMode: 147 pkg, err := selectPackage(dir, rel, packageMap) 148 if err != nil { 149 log.Print(err) 150 } 151 if pkg == nil { 152 return nil // empty rule created in generateEmpty 153 } 154 for _, name := range genFiles { 155 pkg.addGenFile(dir, name) 156 } 157 return []*Package{pkg} 158 159 case PackageMode, FileMode: 160 pkgs := make([]*Package, 0, len(packageMap)) 161 for _, pkg := range packageMap { 162 pkgs = append(pkgs, pkg) 163 } 164 return pkgs 165 166 default: 167 return nil 168 } 169 } 170 171 // selectPackage chooses a package to generate rules for. 172 func selectPackage(dir, rel string, packageMap map[string]*Package) (*Package, error) { 173 if len(packageMap) == 0 { 174 return nil, nil 175 } 176 if len(packageMap) == 1 { 177 for _, pkg := range packageMap { 178 return pkg, nil 179 } 180 } 181 defaultPackageName := strings.Replace(rel, "/", "_", -1) 182 for _, pkg := range packageMap { 183 if pkgName := goPackageName(pkg); pkgName != "" && pkgName == defaultPackageName { 184 return pkg, nil 185 } 186 } 187 return nil, fmt.Errorf("%s: directory contains multiple proto packages. Gazelle can only generate a proto_library for one package.", dir) 188 } 189 190 // goPackageName guesses the identifier in package declarations at the top of 191 // the .pb.go files that will be generated for this package. "" is returned 192 // if the package name cannot be determined. 193 // 194 // TODO(jayconrod): remove all Go-specific functionality. This is here 195 // temporarily for compatibility. 196 func goPackageName(pkg *Package) string { 197 if opt, ok := pkg.Options["go_package"]; ok { 198 if i := strings.IndexByte(opt, ';'); i >= 0 { 199 return opt[i+1:] 200 } else if i := strings.LastIndexByte(opt, '/'); i >= 0 { 201 return opt[i+1:] 202 } else { 203 return opt 204 } 205 } 206 if pkg.Name != "" { 207 return strings.Replace(pkg.Name, ".", "_", -1) 208 } 209 if len(pkg.Files) == 1 { 210 for s := range pkg.Files { 211 return strings.TrimSuffix(s, ".proto") 212 } 213 } 214 return "" 215 } 216 217 // generateProto creates a new proto_library rule for a package. The rule may 218 // be empty if there are no sources. 219 func generateProto(pc *ProtoConfig, rel string, pkg *Package, shouldSetVisibility bool) *rule.Rule { 220 var name string 221 if pc.Mode == DefaultMode { 222 name = RuleName(goPackageName(pkg), pc.GoPrefix, rel) 223 } else { 224 name = RuleName(pkg.RuleName, pkg.Name, rel) 225 } 226 r := rule.NewRule("proto_library", name) 227 srcs := make([]string, 0, len(pkg.Files)) 228 for f := range pkg.Files { 229 srcs = append(srcs, f) 230 } 231 sort.Strings(srcs) 232 if len(srcs) > 0 { 233 r.SetAttr("srcs", srcs) 234 } 235 r.SetPrivateAttr(PackageKey, *pkg) 236 imports := make([]string, 0, len(pkg.Imports)) 237 for i := range pkg.Imports { 238 // If the proto import is a self import (an import between the same package), skip it 239 if _, ok := pkg.Files[path.Base(i)]; ok && getPrefix(pc, path.Dir(i)) == getPrefix(pc, rel) { 240 delete(pkg.Imports, i) 241 continue 242 } 243 imports = append(imports, i) 244 } 245 sort.Strings(imports) 246 // NOTE: This attribute should not be used outside this extension. It's still 247 // convenient for testing though. 248 r.SetPrivateAttr(config.GazelleImportsKey, imports) 249 for k, v := range pkg.Options { 250 r.SetPrivateAttr(k, v) 251 } 252 if shouldSetVisibility { 253 vis := rule.CheckInternalVisibility(rel, "//visibility:public") 254 r.SetAttr("visibility", []string{vis}) 255 } 256 if pc.StripImportPrefix != "" { 257 r.SetAttr("strip_import_prefix", pc.StripImportPrefix) 258 } 259 if pc.ImportPrefix != "" { 260 r.SetAttr("import_prefix", pc.ImportPrefix) 261 } 262 return r 263 } 264 265 func getPrefix(pc *ProtoConfig, rel string) string { 266 prefix := rel 267 if strings.HasPrefix(pc.StripImportPrefix, "/") { 268 prefix = pathtools.TrimPrefix(rel, pc.StripImportPrefix[len("/"):]) 269 } else if pc.StripImportPrefix != "" { 270 prefix = pathtools.TrimPrefix(rel, path.Join(rel, pc.StripImportPrefix)) 271 } 272 if pc.ImportPrefix != "" { 273 return path.Join(pc.ImportPrefix, prefix) 274 } 275 return prefix 276 } 277 278 // generateEmpty generates a list of proto_library rules that may be deleted. 279 // This is generated from existing proto_library rules with srcs lists that 280 // don't match any static or generated files. 281 func generateEmpty(f *rule.File, regularFiles, genFiles []string) []*rule.Rule { 282 if f == nil { 283 return nil 284 } 285 knownFiles := make(map[string]bool) 286 for _, f := range regularFiles { 287 knownFiles[f] = true 288 } 289 for _, f := range genFiles { 290 knownFiles[f] = true 291 } 292 var empty []*rule.Rule 293 outer: 294 for _, r := range f.Rules { 295 if r.Kind() != "proto_library" { 296 continue 297 } 298 srcs := r.AttrStrings("srcs") 299 if len(srcs) == 0 && r.Attr("srcs") != nil { 300 // srcs is not a string list; leave it alone 301 continue 302 } 303 for _, src := range r.AttrStrings("srcs") { 304 if knownFiles[src] { 305 continue outer 306 } 307 } 308 empty = append(empty, rule.NewRule("proto_library", r.Name())) 309 } 310 return empty 311 }