github.com/wolfd/bazel-gazelle@v0.14.0/internal/language/go/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 golang 17 18 import ( 19 "fmt" 20 "go/build" 21 "log" 22 "path" 23 "path/filepath" 24 "sort" 25 "strings" 26 "sync" 27 28 "github.com/bazelbuild/bazel-gazelle/internal/config" 29 "github.com/bazelbuild/bazel-gazelle/internal/language/proto" 30 "github.com/bazelbuild/bazel-gazelle/internal/pathtools" 31 "github.com/bazelbuild/bazel-gazelle/internal/rule" 32 ) 33 34 func (gl *goLang) GenerateRules(c *config.Config, dir, rel string, f *rule.File, subdirs, regularFiles, genFiles []string, otherEmpty, otherGen []*rule.Rule) (empty, gen []*rule.Rule) { 35 // Extract information about proto files. We need this to exclude .pb.go 36 // files and generate go_proto_library rules. 37 gc := getGoConfig(c) 38 pc := proto.GetProtoConfig(c) 39 var protoRuleNames []string 40 protoPackages := make(map[string]proto.Package) 41 protoFileInfo := make(map[string]proto.FileInfo) 42 for _, r := range otherGen { 43 if r.Kind() != "proto_library" { 44 continue 45 } 46 pkg := r.PrivateAttr(proto.PackageKey).(proto.Package) 47 protoPackages[r.Name()] = pkg 48 for name, info := range pkg.Files { 49 protoFileInfo[name] = info 50 } 51 protoRuleNames = append(protoRuleNames, r.Name()) 52 } 53 sort.Strings(protoRuleNames) 54 var emptyProtoRuleNames []string 55 for _, r := range otherEmpty { 56 if r.Kind() == "proto_library" { 57 emptyProtoRuleNames = append(emptyProtoRuleNames, r.Name()) 58 } 59 } 60 61 // If proto rule generation is enabled, exclude .pb.go files that correspond 62 // to any .proto files present. 63 if !pc.Mode.ShouldIncludePregeneratedFiles() { 64 keep := func(f string) bool { 65 if strings.HasSuffix(f, ".pb.go") { 66 _, ok := protoFileInfo[strings.TrimSuffix(f, ".pb.go")+".proto"] 67 return !ok 68 } 69 return true 70 } 71 filterFiles(®ularFiles, keep) 72 filterFiles(&genFiles, keep) 73 } 74 75 // Split regular files into files which can determine the package name and 76 // import path and other files. 77 var goFiles, otherFiles []string 78 for _, f := range regularFiles { 79 if strings.HasSuffix(f, ".go") { 80 goFiles = append(goFiles, f) 81 } else { 82 otherFiles = append(otherFiles, f) 83 } 84 } 85 86 // Look for a subdirectory named testdata. Only treat it as data if it does 87 // not contain a buildable package. 88 var hasTestdata bool 89 for _, sub := range subdirs { 90 if sub == "testdata" { 91 hasTestdata = !gl.goPkgRels[path.Join(rel, "testdata")] 92 break 93 } 94 } 95 96 // Build a set of packages from files in this directory. 97 goPackageMap, goFilesWithUnknownPackage := buildPackages(c, dir, rel, goFiles, hasTestdata) 98 99 // Select a package to generate rules for. If there is no package, create 100 // an empty package so we can generate empty rules. 101 var protoName string 102 pkg, err := selectPackage(c, dir, goPackageMap) 103 if err != nil { 104 if _, ok := err.(*build.NoGoError); ok { 105 if len(protoPackages) == 1 { 106 for name, ppkg := range protoPackages { 107 pkg = &goPackage{ 108 name: goProtoPackageName(ppkg), 109 importPath: goProtoImportPath(gc, ppkg, rel), 110 proto: protoTargetFromProtoPackage(name, ppkg), 111 } 112 protoName = name 113 break 114 } 115 } else { 116 pkg = emptyPackage(c, dir, rel) 117 } 118 } else { 119 log.Print(err) 120 } 121 } 122 123 // Try to link the selected package with a proto package. 124 if pkg != nil { 125 if pkg.importPath == "" { 126 if err := pkg.inferImportPath(c); err != nil && pkg.firstGoFile() != "" { 127 inferImportPathErrorOnce.Do(func() { log.Print(err) }) 128 } 129 } 130 for _, name := range protoRuleNames { 131 ppkg := protoPackages[name] 132 if pkg.importPath == goProtoImportPath(gc, ppkg, rel) { 133 protoName = name 134 pkg.proto = protoTargetFromProtoPackage(name, ppkg) 135 break 136 } 137 } 138 } 139 140 // Generate rules for proto packages. These should come before the other 141 // Go rules. 142 g := newGenerator(c, f, rel) 143 var rules []*rule.Rule 144 var protoEmbed string 145 for _, name := range protoRuleNames { 146 ppkg := protoPackages[name] 147 var rs []*rule.Rule 148 if name == protoName { 149 protoEmbed, rs = g.generateProto(pc.Mode, pkg.proto, pkg.importPath) 150 } else { 151 target := protoTargetFromProtoPackage(name, ppkg) 152 importPath := goProtoImportPath(gc, ppkg, rel) 153 _, rs = g.generateProto(pc.Mode, target, importPath) 154 } 155 rules = append(rules, rs...) 156 } 157 for _, name := range emptyProtoRuleNames { 158 goProtoName := strings.TrimSuffix(name, "_proto") + "_go_proto" 159 empty = append(empty, rule.NewRule("go_proto_library", goProtoName)) 160 } 161 if pkg != nil && pc.Mode == proto.PackageMode && pkg.firstGoFile() == "" { 162 // In proto package mode, don't generate a go_library embedding a 163 // go_proto_library unless there are actually go files. 164 protoEmbed = "" 165 } 166 167 // Complete the Go package and generate rules for that. 168 if pkg != nil { 169 // Add files with unknown packages. This happens when there are parse 170 // or I/O errors. We should keep the file in the srcs list and let the 171 // compiler deal with the error. 172 cgo := pkg.haveCgo() 173 for _, info := range goFilesWithUnknownPackage { 174 if err := pkg.addFile(c, info, cgo); err != nil { 175 log.Print(err) 176 } 177 } 178 179 // Process the other static files. 180 for _, file := range otherFiles { 181 info := otherFileInfo(filepath.Join(dir, file)) 182 if err := pkg.addFile(c, info, cgo); err != nil { 183 log.Print(err) 184 } 185 } 186 187 // Process generated files. Note that generated files may have the same names 188 // as static files. Bazel will use the generated files, but we will look at 189 // the content of static files, assuming they will be the same. 190 regularFileSet := make(map[string]bool) 191 for _, f := range regularFiles { 192 regularFileSet[f] = true 193 } 194 for _, f := range genFiles { 195 if regularFileSet[f] { 196 continue 197 } 198 info := fileNameInfo(filepath.Join(dir, f)) 199 if err := pkg.addFile(c, info, cgo); err != nil { 200 log.Print(err) 201 } 202 } 203 204 // Generate Go rules. 205 if protoName == "" { 206 // Empty proto rules for deletion. 207 _, rs := g.generateProto(pc.Mode, pkg.proto, pkg.importPath) 208 rules = append(rules, rs...) 209 } 210 lib := g.generateLib(pkg, protoEmbed) 211 var libName string 212 if !lib.IsEmpty(goKinds[lib.Kind()]) { 213 libName = lib.Name() 214 } 215 rules = append(rules, lib) 216 rules = append(rules, 217 g.generateBin(pkg, libName), 218 g.generateTest(pkg, libName)) 219 } 220 221 for _, r := range rules { 222 if r.IsEmpty(goKinds[r.Kind()]) { 223 empty = append(empty, r) 224 } else { 225 gen = append(gen, r) 226 } 227 } 228 229 if f != nil || len(gen) > 0 { 230 gl.goPkgRels[rel] = true 231 } else { 232 for _, sub := range subdirs { 233 if gl.goPkgRels[path.Join(rel, sub)] { 234 gl.goPkgRels[rel] = true 235 break 236 } 237 } 238 } 239 240 return empty, gen 241 } 242 243 func filterFiles(files *[]string, pred func(string) bool) { 244 w := 0 245 for r := 0; r < len(*files); r++ { 246 f := (*files)[r] 247 if pred(f) { 248 (*files)[w] = f 249 w++ 250 } 251 } 252 *files = (*files)[:w] 253 } 254 255 func buildPackages(c *config.Config, dir, rel string, goFiles []string, hasTestdata bool) (packageMap map[string]*goPackage, goFilesWithUnknownPackage []fileInfo) { 256 // Process .go and .proto files first, since these determine the package name. 257 packageMap = make(map[string]*goPackage) 258 for _, f := range goFiles { 259 path := filepath.Join(dir, f) 260 info := goFileInfo(path, rel) 261 if info.packageName == "" { 262 goFilesWithUnknownPackage = append(goFilesWithUnknownPackage, info) 263 continue 264 } 265 if info.packageName == "documentation" { 266 // go/build ignores this package 267 continue 268 } 269 270 if _, ok := packageMap[info.packageName]; !ok { 271 packageMap[info.packageName] = &goPackage{ 272 name: info.packageName, 273 dir: dir, 274 rel: rel, 275 hasTestdata: hasTestdata, 276 } 277 } 278 if err := packageMap[info.packageName].addFile(c, info, false); err != nil { 279 log.Print(err) 280 } 281 } 282 return packageMap, goFilesWithUnknownPackage 283 } 284 285 var inferImportPathErrorOnce sync.Once 286 287 // selectPackages selects one Go packages out of the buildable packages found 288 // in a directory. If multiple packages are found, it returns the package 289 // whose name matches the directory if such a package exists. 290 func selectPackage(c *config.Config, dir string, packageMap map[string]*goPackage) (*goPackage, error) { 291 buildablePackages := make(map[string]*goPackage) 292 for name, pkg := range packageMap { 293 if pkg.isBuildable(c) { 294 buildablePackages[name] = pkg 295 } 296 } 297 298 if len(buildablePackages) == 0 { 299 return nil, &build.NoGoError{Dir: dir} 300 } 301 302 if len(buildablePackages) == 1 { 303 for _, pkg := range buildablePackages { 304 return pkg, nil 305 } 306 } 307 308 if pkg, ok := buildablePackages[defaultPackageName(c, dir)]; ok { 309 return pkg, nil 310 } 311 312 err := &build.MultiplePackageError{Dir: dir} 313 for name, pkg := range buildablePackages { 314 // Add the first file for each package for the error message. 315 // Error() method expects these lists to be the same length. File 316 // lists must be non-empty. These lists are only created by 317 // buildPackage for packages with .go files present. 318 err.Packages = append(err.Packages, name) 319 err.Files = append(err.Files, pkg.firstGoFile()) 320 } 321 return nil, err 322 } 323 324 func emptyPackage(c *config.Config, dir, rel string) *goPackage { 325 pkg := &goPackage{ 326 name: defaultPackageName(c, dir), 327 dir: dir, 328 rel: rel, 329 } 330 pkg.inferImportPath(c) 331 return pkg 332 } 333 334 func defaultPackageName(c *config.Config, rel string) string { 335 gc := getGoConfig(c) 336 return pathtools.RelBaseName(rel, gc.prefix, "") 337 } 338 339 // hasDefaultVisibility returns whether oldFile contains a "package" rule with 340 // a "default_visibility" attribute. Rules generated by Gazelle should not 341 // have their own visibility attributes if this is the case. 342 func hasDefaultVisibility(oldFile *rule.File) bool { 343 for _, r := range oldFile.Rules { 344 if r.Kind() == "package" && r.Attr("default_visibility") != nil { 345 return true 346 } 347 } 348 return false 349 } 350 351 // checkInternalVisibility overrides the given visibility if the package is 352 // internal. 353 func checkInternalVisibility(rel, visibility string) string { 354 if i := strings.LastIndex(rel, "/internal/"); i >= 0 { 355 visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i]) 356 } else if strings.HasPrefix(rel, "internal/") { 357 visibility = "//:__subpackages__" 358 } 359 return visibility 360 } 361 362 type generator struct { 363 c *config.Config 364 rel string 365 shouldSetVisibility bool 366 } 367 368 func newGenerator(c *config.Config, f *rule.File, rel string) *generator { 369 shouldSetVisibility := f == nil || !hasDefaultVisibility(f) 370 return &generator{c: c, rel: rel, shouldSetVisibility: shouldSetVisibility} 371 } 372 373 func (g *generator) generateProto(mode proto.Mode, target protoTarget, importPath string) (string, []*rule.Rule) { 374 if !mode.ShouldGenerateRules() && mode != proto.LegacyMode { 375 // Don't create or delete proto rules in this mode. Any existing rules 376 // are likely hand-written. 377 return "", nil 378 } 379 380 filegroupName := config.DefaultProtosName 381 protoName := target.name 382 if protoName == "" { 383 importPath := inferImportPath(getGoConfig(g.c), g.rel) 384 protoName = proto.RuleName(importPath) 385 } 386 goProtoName := strings.TrimSuffix(protoName, "_proto") + "_go_proto" 387 visibility := []string{checkInternalVisibility(g.rel, "//visibility:public")} 388 389 if mode == proto.LegacyMode { 390 filegroup := rule.NewRule("filegroup", filegroupName) 391 if target.sources.isEmpty() { 392 return "", []*rule.Rule{filegroup} 393 } 394 filegroup.SetAttr("srcs", target.sources.build()) 395 if g.shouldSetVisibility { 396 filegroup.SetAttr("visibility", visibility) 397 } 398 return "", []*rule.Rule{filegroup} 399 } 400 401 if target.sources.isEmpty() { 402 return "", []*rule.Rule{ 403 rule.NewRule("filegroup", filegroupName), 404 rule.NewRule("go_proto_library", goProtoName), 405 } 406 } 407 408 goProtoLibrary := rule.NewRule("go_proto_library", goProtoName) 409 goProtoLibrary.SetAttr("proto", ":"+protoName) 410 g.setImportAttrs(goProtoLibrary, importPath) 411 if target.hasServices { 412 goProtoLibrary.SetAttr("compilers", []string{"@io_bazel_rules_go//proto:go_grpc"}) 413 } 414 if g.shouldSetVisibility { 415 goProtoLibrary.SetAttr("visibility", visibility) 416 } 417 goProtoLibrary.SetPrivateAttr(config.GazelleImportsKey, target.imports.build()) 418 return goProtoName, []*rule.Rule{goProtoLibrary} 419 } 420 421 func (g *generator) generateLib(pkg *goPackage, embed string) *rule.Rule { 422 goLibrary := rule.NewRule("go_library", config.DefaultLibName) 423 if !pkg.library.sources.hasGo() && embed == "" { 424 return goLibrary // empty 425 } 426 var visibility string 427 if pkg.isCommand() { 428 // Libraries made for a go_binary should not be exposed to the public. 429 visibility = "//visibility:private" 430 } else { 431 visibility = checkInternalVisibility(pkg.rel, "//visibility:public") 432 } 433 g.setCommonAttrs(goLibrary, pkg.rel, visibility, pkg.library, embed) 434 g.setImportAttrs(goLibrary, pkg.importPath) 435 return goLibrary 436 } 437 438 func (g *generator) generateBin(pkg *goPackage, library string) *rule.Rule { 439 name := pathtools.RelBaseName(pkg.rel, getGoConfig(g.c).prefix, g.c.RepoRoot) 440 goBinary := rule.NewRule("go_binary", name) 441 if !pkg.isCommand() || pkg.binary.sources.isEmpty() && library == "" { 442 return goBinary // empty 443 } 444 visibility := checkInternalVisibility(pkg.rel, "//visibility:public") 445 g.setCommonAttrs(goBinary, pkg.rel, visibility, pkg.binary, library) 446 return goBinary 447 } 448 449 func (g *generator) generateTest(pkg *goPackage, library string) *rule.Rule { 450 goTest := rule.NewRule("go_test", config.DefaultTestName) 451 if !pkg.test.sources.hasGo() { 452 return goTest // empty 453 } 454 g.setCommonAttrs(goTest, pkg.rel, "", pkg.test, library) 455 if pkg.hasTestdata { 456 goTest.SetAttr("data", rule.GlobValue{Patterns: []string{"testdata/**"}}) 457 } 458 return goTest 459 } 460 461 func (g *generator) setCommonAttrs(r *rule.Rule, pkgRel, visibility string, target goTarget, embed string) { 462 if !target.sources.isEmpty() { 463 r.SetAttr("srcs", target.sources.buildFlat()) 464 } 465 if target.cgo { 466 r.SetAttr("cgo", true) 467 } 468 if !target.clinkopts.isEmpty() { 469 r.SetAttr("clinkopts", g.options(target.clinkopts.build(), pkgRel)) 470 } 471 if !target.copts.isEmpty() { 472 r.SetAttr("copts", g.options(target.copts.build(), pkgRel)) 473 } 474 if g.shouldSetVisibility && visibility != "" { 475 r.SetAttr("visibility", []string{visibility}) 476 } 477 if embed != "" { 478 r.SetAttr("embed", []string{":" + embed}) 479 } 480 r.SetPrivateAttr(config.GazelleImportsKey, target.imports.build()) 481 } 482 483 func (g *generator) setImportAttrs(r *rule.Rule, importPath string) { 484 r.SetAttr("importpath", importPath) 485 goConf := getGoConfig(g.c) 486 if goConf.importMapPrefix != "" { 487 fromPrefixRel := pathtools.TrimPrefix(g.rel, goConf.importMapPrefixRel) 488 importMap := path.Join(goConf.importMapPrefix, fromPrefixRel) 489 if importMap != importPath { 490 r.SetAttr("importmap", importMap) 491 } 492 } 493 } 494 495 var ( 496 // shortOptPrefixes are strings that come at the beginning of an option 497 // argument that includes a path, e.g., -Ifoo/bar. 498 shortOptPrefixes = []string{"-I", "-L", "-F"} 499 500 // longOptPrefixes are separate arguments that come before a path argument, 501 // e.g., -iquote foo/bar. 502 longOptPrefixes = []string{"-I", "-L", "-F", "-iquote", "-isystem"} 503 ) 504 505 // options transforms package-relative paths in cgo options into repository- 506 // root-relative paths that Bazel can understand. For example, if a cgo file 507 // in //foo declares an include flag in its copts: "-Ibar", this method 508 // will transform that flag into "-Ifoo/bar". 509 func (g *generator) options(opts rule.PlatformStrings, pkgRel string) rule.PlatformStrings { 510 fixPath := func(opt string) string { 511 if strings.HasPrefix(opt, "/") { 512 return opt 513 } 514 return path.Clean(path.Join(pkgRel, opt)) 515 } 516 517 fixGroups := func(groups []string) ([]string, error) { 518 fixedGroups := make([]string, len(groups)) 519 for i, group := range groups { 520 opts := strings.Split(group, optSeparator) 521 fixedOpts := make([]string, len(opts)) 522 isPath := false 523 for j, opt := range opts { 524 if isPath { 525 opt = fixPath(opt) 526 isPath = false 527 goto next 528 } 529 530 for _, short := range shortOptPrefixes { 531 if strings.HasPrefix(opt, short) && len(opt) > len(short) { 532 opt = short + fixPath(opt[len(short):]) 533 goto next 534 } 535 } 536 537 for _, long := range longOptPrefixes { 538 if opt == long { 539 isPath = true 540 goto next 541 } 542 } 543 544 next: 545 fixedOpts[j] = escapeOption(opt) 546 } 547 fixedGroups[i] = strings.Join(fixedOpts, " ") 548 } 549 550 return fixedGroups, nil 551 } 552 553 opts, errs := opts.MapSlice(fixGroups) 554 if errs != nil { 555 log.Panicf("unexpected error when transforming options with pkg %q: %v", pkgRel, errs) 556 } 557 return opts 558 } 559 560 func escapeOption(opt string) string { 561 return strings.NewReplacer( 562 `\`, `\\`, 563 `'`, `\'`, 564 `"`, `\"`, 565 ` `, `\ `, 566 "\t", "\\\t", 567 "\n", "\\\n", 568 "\r", "\\\r", 569 ).Replace(opt) 570 }