github.com/wolfd/bazel-gazelle@v0.14.0/internal/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 "sort" 22 "strings" 23 24 "github.com/bazelbuild/bazel-gazelle/internal/config" 25 "github.com/bazelbuild/bazel-gazelle/internal/rule" 26 ) 27 28 func (_ *protoLang) GenerateRules(c *config.Config, dir, rel string, f *rule.File, subdirs, regularFiles, genFiles []string, otherEmpty, otherGen []*rule.Rule) (empty, gen []*rule.Rule) { 29 pc := GetProtoConfig(c) 30 if !pc.Mode.ShouldGenerateRules() { 31 // Don't create or delete proto rules in this mode. Any existing rules 32 // are likely hand-written. 33 return nil, nil 34 } 35 36 var regularProtoFiles []string 37 for _, name := range regularFiles { 38 if strings.HasSuffix(name, ".proto") { 39 regularProtoFiles = append(regularProtoFiles, name) 40 } 41 } 42 var genProtoFiles []string 43 for _, name := range genFiles { 44 if strings.HasSuffix(name, ".proto") { 45 genProtoFiles = append(genFiles, name) 46 } 47 } 48 pkgs := buildPackages(pc, dir, rel, regularProtoFiles, genProtoFiles) 49 shouldSetVisibility := !hasDefaultVisibility(f) 50 for _, pkg := range pkgs { 51 r := generateProto(pc, rel, pkg, shouldSetVisibility) 52 if r.IsEmpty(protoKinds[r.Kind()]) { 53 empty = append(empty, r) 54 } else { 55 gen = append(gen, r) 56 } 57 } 58 sort.SliceStable(gen, func(i, j int) bool { 59 return gen[i].Name() < gen[j].Name() 60 }) 61 empty = append(empty, generateEmpty(f, regularProtoFiles, genProtoFiles)...) 62 return empty, gen 63 } 64 65 // RuleName returns a name for a proto_library derived from the given strings. 66 // For each string, RuleName will look for a non-empty suffix of identifier 67 // characters and then append "_proto" to that. 68 func RuleName(names ...string) string { 69 base := "root" 70 for _, name := range names { 71 notIdent := func(c rune) bool { 72 return !('A' <= c && c <= 'Z' || 73 'a' <= c && c <= 'z' || 74 '0' <= c && c <= '9' || 75 c == '_') 76 } 77 if i := strings.LastIndexFunc(name, notIdent); i >= 0 { 78 name = name[i+1:] 79 } 80 if name != "" { 81 base = name 82 break 83 } 84 } 85 return base + "_proto" 86 } 87 88 // buildPackage extracts metadata from the .proto files in a directory and 89 // constructs possibly several packages, then selects a package to generate 90 // a proto_library rule for. 91 func buildPackages(pc *ProtoConfig, dir, rel string, protoFiles, genFiles []string) []*Package { 92 packageMap := make(map[string]*Package) 93 for _, name := range protoFiles { 94 info := protoFileInfo(dir, name) 95 key := info.PackageName 96 if pc.groupOption != "" { 97 for _, opt := range info.Options { 98 if opt.Key == pc.groupOption { 99 key = opt.Value 100 break 101 } 102 } 103 } 104 if packageMap[key] == nil { 105 packageMap[key] = newPackage(info.PackageName) 106 } 107 packageMap[key].addFile(info) 108 } 109 110 switch pc.Mode { 111 case DefaultMode: 112 pkg, err := selectPackage(dir, rel, packageMap) 113 if err != nil { 114 log.Print(err) 115 } 116 if pkg == nil { 117 return nil // empty rule created in generateEmpty 118 } 119 for _, name := range genFiles { 120 pkg.addGenFile(dir, name) 121 } 122 return []*Package{pkg} 123 124 case PackageMode: 125 pkgs := make([]*Package, 0, len(packageMap)) 126 for _, pkg := range packageMap { 127 pkgs = append(pkgs, pkg) 128 } 129 return pkgs 130 131 default: 132 return nil 133 } 134 } 135 136 // selectPackage chooses a package to generate rules for. 137 func selectPackage(dir, rel string, packageMap map[string]*Package) (*Package, error) { 138 if len(packageMap) == 0 { 139 return nil, nil 140 } 141 if len(packageMap) == 1 { 142 for _, pkg := range packageMap { 143 return pkg, nil 144 } 145 } 146 defaultPackageName := strings.Replace(rel, "/", "_", -1) 147 for _, pkg := range packageMap { 148 if pkgName := goPackageName(pkg); pkgName != "" && pkgName == defaultPackageName { 149 return pkg, nil 150 } 151 } 152 return nil, fmt.Errorf("%s: directory contains multiple proto packages. Gazelle can only generate a proto_library for one package.", dir) 153 } 154 155 // goPackageName guesses the identifier in package declarations at the top of 156 // the .pb.go files that will be generated for this package. "" is returned 157 // if the package name cannot be determined. 158 // 159 // TODO(jayconrod): remove all Go-specific functionality. This is here 160 // temporarily for compatibility. 161 func goPackageName(pkg *Package) string { 162 if opt, ok := pkg.Options["go_package"]; ok { 163 if i := strings.IndexByte(opt, ';'); i >= 0 { 164 return opt[i+1:] 165 } else if i := strings.LastIndexByte(opt, '/'); i >= 0 { 166 return opt[i+1:] 167 } else { 168 return opt 169 } 170 } 171 if pkg.Name != "" { 172 return strings.Replace(pkg.Name, ".", "_", -1) 173 } 174 if len(pkg.Files) == 1 { 175 for s := range pkg.Files { 176 return strings.TrimSuffix(s, ".proto") 177 } 178 } 179 return "" 180 } 181 182 // generateProto creates a new proto_library rule for a package. The rule may 183 // be empty if there are no sources. 184 func generateProto(pc *ProtoConfig, rel string, pkg *Package, shouldSetVisibility bool) *rule.Rule { 185 var name string 186 if pc.Mode == DefaultMode { 187 name = RuleName(goPackageName(pkg), pc.GoPrefix, rel) 188 } else { 189 name = RuleName(pkg.Options[pc.groupOption], pkg.Name, rel) 190 } 191 r := rule.NewRule("proto_library", name) 192 srcs := make([]string, 0, len(pkg.Files)) 193 for f := range pkg.Files { 194 srcs = append(srcs, f) 195 } 196 sort.Strings(srcs) 197 if len(srcs) > 0 { 198 r.SetAttr("srcs", srcs) 199 } 200 r.SetPrivateAttr(PackageKey, *pkg) 201 imports := make([]string, 0, len(pkg.Imports)) 202 for i := range pkg.Imports { 203 imports = append(imports, i) 204 } 205 sort.Strings(imports) 206 r.SetPrivateAttr(config.GazelleImportsKey, imports) 207 for k, v := range pkg.Options { 208 r.SetPrivateAttr(k, v) 209 } 210 if shouldSetVisibility { 211 vis := checkInternalVisibility(rel, "//visibility:public") 212 r.SetAttr("visibility", []string{vis}) 213 } 214 return r 215 } 216 217 // generateEmpty generates a list of proto_library rules that may be deleted. 218 // This is generated from existing proto_library rules with srcs lists that 219 // don't match any static or generated files. 220 func generateEmpty(f *rule.File, regularFiles, genFiles []string) []*rule.Rule { 221 if f == nil { 222 return nil 223 } 224 knownFiles := make(map[string]bool) 225 for _, f := range regularFiles { 226 knownFiles[f] = true 227 } 228 for _, f := range genFiles { 229 knownFiles[f] = true 230 } 231 var empty []*rule.Rule 232 outer: 233 for _, r := range f.Rules { 234 if r.Kind() != "proto_library" { 235 continue 236 } 237 srcs := r.AttrStrings("srcs") 238 if len(srcs) == 0 && r.Attr("srcs") != nil { 239 // srcs is not a string list; leave it alone 240 continue 241 } 242 for _, src := range r.AttrStrings("srcs") { 243 if knownFiles[src] { 244 continue outer 245 } 246 } 247 empty = append(empty, rule.NewRule("proto_library", r.Name())) 248 } 249 return empty 250 } 251 252 // hasDefaultVisibility returns whether oldFile contains a "package" rule with 253 // a "default_visibility" attribute. Rules generated by Gazelle should not 254 // have their own visibility attributes if this is the case. 255 func hasDefaultVisibility(f *rule.File) bool { 256 if f == nil { 257 return false 258 } 259 for _, r := range f.Rules { 260 if r.Kind() == "package" && r.Attr("default_visibility") != nil { 261 return true 262 } 263 } 264 return false 265 } 266 267 // checkInternalVisibility overrides the given visibility if the package is 268 // internal. 269 func checkInternalVisibility(rel, visibility string) string { 270 if i := strings.LastIndex(rel, "/internal/"); i >= 0 { 271 visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i]) 272 } else if strings.HasPrefix(rel, "internal/") { 273 visibility = "//:__subpackages__" 274 } 275 return visibility 276 }