/* Copyright 2017 The Bazel Authors. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package golang import ( "log" "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/language/proto" "github.com/bazelbuild/bazel-gazelle/rule" bzl "github.com/bazelbuild/buildtools/build" ) func (*goLang) Fix(c *config.Config, f *rule.File) { migrateLibraryEmbed(c, f) migrateGrpcCompilers(c, f) flattenSrcs(c, f) squashCgoLibrary(c, f) squashXtest(c, f) removeLegacyProto(c, f) removeLegacyGazelle(c, f) migrateNamingConvention(c, f) } // migrateNamingConvention renames rules according to go_naming_convention // directives. func migrateNamingConvention(c *config.Config, f *rule.File) { // Determine old and new names for go_library and go_test. nc := getGoConfig(c).goNamingConvention importPath := InferImportPath(c, f.Pkg) if importPath == "" { return } var pkgName string // unknown unless there's a binary if fileContainsGoBinary(c, f) { pkgName = "main" } libName := libNameByConvention(nc, importPath, pkgName) testName := testNameByConvention(nc, importPath) var migrateLibName, migrateTestName string switch nc { case goDefaultLibraryNamingConvention: migrateLibName = libNameByConvention(importNamingConvention, importPath, pkgName) migrateTestName = testNameByConvention(importNamingConvention, importPath) case importNamingConvention, importAliasNamingConvention: migrateLibName = defaultLibName migrateTestName = defaultTestName default: return } // Check whether the new names are in use. If there are rules with both old // and new names, there will be a conflict. var haveLib, haveMigrateLib, haveTest, haveMigrateTest bool for _, r := range f.Rules { switch { case r.Name() == libName: haveLib = true case r.Kind() == "go_library" && r.Name() == migrateLibName && r.AttrString("importpath") == importPath: haveMigrateLib = true case r.Name() == testName: haveTest = true case r.Kind() == "go_test" && r.Name() == migrateTestName && strListAttrContains(r, "embed", ":"+migrateLibName): haveMigrateTest = true } } if haveLib && haveMigrateLib { log.Printf("%[1]s: Tried to rename %[2]s to %[3]s, but %[3]s already exists.", f.Path, migrateLibName, libName) } if haveTest && haveMigrateTest { log.Printf("%[1]s: Tried to rename %[2]s to %[3]s, but %[3]s already exists.", f.Path, migrateTestName, testName) } shouldMigrateLib := haveMigrateLib && !haveLib shouldMigrateTest := haveMigrateTest && !haveTest // Rename the targets and stuff in the same file that refers to them. for _, r := range f.Rules { // TODO(jayconrod): support map_kind directive. // We'll need to move the metaresolver from resolve.RuleIndex to config.Config so we can access it from here. switch r.Kind() { case "go_binary": if haveMigrateLib && shouldMigrateLib { replaceInStrListAttr(r, "embed", ":"+migrateLibName, ":"+libName) } case "go_library": if r.Name() == migrateLibName && shouldMigrateLib { r.SetName(libName) } case "go_test": if r.Name() == migrateTestName && shouldMigrateTest { r.SetName(testName) } if shouldMigrateLib { replaceInStrListAttr(r, "embed", ":"+migrateLibName, ":"+libName) } } } } // fileContainsGoBinary returns whether the file has a go_binary rule. func fileContainsGoBinary(c *config.Config, f *rule.File) bool { if f == nil { return false } for _, r := range f.Rules { kind := r.Kind() if kind == "go_binary" { return true } if mappedKind, ok := c.KindMap["go_binary"]; ok { if mappedKind.KindName == kind { return true } } } return false } func replaceInStrListAttr(r *rule.Rule, attr, old, new string) { items := r.AttrStrings(attr) changed := false for i := range items { if items[i] == old { changed = true items[i] = new } } if changed { r.SetAttr(attr, items) } } func strListAttrContains(r *rule.Rule, attr, s string) bool { items := r.AttrStrings(attr) for _, item := range items { if item == s { return true } } return false } // migrateLibraryEmbed converts "library" attributes to "embed" attributes, // preserving comments. This only applies to Go rules, and only if there is // no keep comment on "library" and no existing "embed" attribute. func migrateLibraryEmbed(c *config.Config, f *rule.File) { for _, r := range f.Rules { if !isGoRule(r.Kind()) { continue } libExpr := r.Attr("library") if libExpr == nil || rule.ShouldKeep(libExpr) || r.Attr("embed") != nil { continue } r.DelAttr("library") r.SetAttr("embed", &bzl.ListExpr{List: []bzl.Expr{libExpr}}) } } // migrateGrpcCompilers converts "go_grpc_library" rules into "go_proto_library" // rules with a "compilers" attribute. func migrateGrpcCompilers(c *config.Config, f *rule.File) { for _, r := range f.Rules { if r.Kind() != "go_grpc_library" || r.ShouldKeep() || r.Attr("compilers") != nil { continue } r.SetKind("go_proto_library") r.SetAttr("compilers", []string{grpcCompilerLabel}) } } // squashCgoLibrary removes cgo_library rules with the default name and // merges their attributes with go_library with the default name. If no // go_library rule exists, a new one will be created. // // Note that the library attribute is disregarded, so cgo_library and // go_library attributes will be squashed even if the cgo_library was unlinked. // MergeFile will remove unused values and attributes later. func squashCgoLibrary(c *config.Config, f *rule.File) { // Find the default cgo_library and go_library rules. var cgoLibrary, goLibrary *rule.Rule for _, r := range f.Rules { if r.Kind() == "cgo_library" && r.Name() == "cgo_default_library" && !r.ShouldKeep() { if cgoLibrary != nil { log.Printf("%s: when fixing existing file, multiple cgo_library rules with default name found", f.Path) continue } cgoLibrary = r continue } if r.Kind() == "go_library" && r.Name() == defaultLibName { if goLibrary != nil { log.Printf("%s: when fixing existing file, multiple go_library rules with default name referencing cgo_library found", f.Path) } goLibrary = r continue } } if cgoLibrary == nil { return } if !c.ShouldFix { log.Printf("%s: cgo_library is deprecated. Run 'gazelle fix' to squash with go_library.", f.Path) return } if goLibrary == nil { cgoLibrary.SetKind("go_library") cgoLibrary.SetName(defaultLibName) cgoLibrary.SetAttr("cgo", true) return } if err := rule.SquashRules(cgoLibrary, goLibrary, f.Path); err != nil { log.Print(err) return } goLibrary.DelAttr("embed") goLibrary.SetAttr("cgo", true) cgoLibrary.Delete() } // squashXtest removes go_test rules with the default external name and merges // their attributes with a go_test rule with the default internal name. If // no internal go_test rule exists, a new one will be created (effectively // renaming the old rule). func squashXtest(c *config.Config, f *rule.File) { // Search for internal and external tests. var itest, xtest *rule.Rule for _, r := range f.Rules { if r.Kind() != "go_test" { continue } if r.Name() == defaultTestName { itest = r } else if r.Name() == "go_default_xtest" { xtest = r } } if xtest == nil || xtest.ShouldKeep() || (itest != nil && itest.ShouldKeep()) { return } if !c.ShouldFix { if itest == nil { log.Printf("%s: go_default_xtest is no longer necessary. Run 'gazelle fix' to rename to go_default_test.", f.Path) } else { log.Printf("%s: go_default_xtest is no longer necessary. Run 'gazelle fix' to squash with go_default_test.", f.Path) } return } // If there was no internal test, we can just rename the external test. if itest == nil { xtest.SetName(defaultTestName) return } // Attempt to squash. if err := rule.SquashRules(xtest, itest, f.Path); err != nil { log.Print(err) return } xtest.Delete() } // flattenSrcs transforms srcs attributes structured as concatenations of // lists and selects (generated from PlatformStrings; see // extractPlatformStringsExprs for matching details) into a sorted, // de-duplicated list. Comments are accumulated and de-duplicated across // duplicate expressions. func flattenSrcs(c *config.Config, f *rule.File) { for _, r := range f.Rules { if !isGoRule(r.Kind()) { continue } oldSrcs := r.Attr("srcs") if oldSrcs == nil { continue } flatSrcs := rule.FlattenExpr(oldSrcs) if flatSrcs != oldSrcs { r.SetAttr("srcs", flatSrcs) } } } // removeLegacyProto removes uses of the old proto rules. It deletes loads // from go_proto_library.bzl. It deletes proto filegroups. It removes // go_proto_library attributes which are no longer recognized. New rules // are generated in place of the deleted rules, but attributes and comments // are not migrated. func removeLegacyProto(c *config.Config, f *rule.File) { // Don't fix if the proto mode was set to something other than the default. if pcMode := getProtoMode(c); pcMode != proto.DefaultMode { return } // Scan for definitions to delete. var protoLoads []*rule.Load for _, l := range f.Loads { if l.Name() == "@io_bazel_rules_go//proto:go_proto_library.bzl" { protoLoads = append(protoLoads, l) } } var protoFilegroups, protoRules []*rule.Rule for _, r := range f.Rules { if r.Kind() == "filegroup" && r.Name() == legacyProtoFilegroupName { protoFilegroups = append(protoFilegroups, r) } if r.Kind() == "go_proto_library" { protoRules = append(protoRules, r) } } if len(protoLoads)+len(protoFilegroups) == 0 { return } if !c.ShouldFix { log.Printf("%s: go_proto_library.bzl is deprecated. Run 'gazelle fix' to replace old rules.", f.Path) return } // Delete legacy proto loads and filegroups. Only delete go_proto_library // rules if we deleted a load. for _, l := range protoLoads { l.Delete() } for _, r := range protoFilegroups { r.Delete() } if len(protoLoads) > 0 { for _, r := range protoRules { r.Delete() } } } // removeLegacyGazelle removes loads of the "gazelle" macro from // @io_bazel_rules_go//go:def.bzl. The definition has moved to // @bazel_gazelle//:def.bzl, and the old one will be deleted soon. func removeLegacyGazelle(c *config.Config, f *rule.File) { for _, l := range f.Loads { if l.Name() == "@io_bazel_rules_go//go:def.bzl" && l.Has("gazelle") { l.Remove("gazelle") if l.IsEmpty() { l.Delete() } } } } func isGoRule(kind string) bool { return kind == "go_library" || kind == "go_binary" || kind == "go_test" || kind == "go_proto_library" || kind == "go_grpc_library" }