github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/go/fix.go (about)

     1  /* Copyright 2017 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  	"log"
    20  
    21  	"github.com/bazelbuild/bazel-gazelle/config"
    22  	"github.com/bazelbuild/bazel-gazelle/language/proto"
    23  	"github.com/bazelbuild/bazel-gazelle/rule"
    24  	bzl "github.com/bazelbuild/buildtools/build"
    25  )
    26  
    27  func (*goLang) Fix(c *config.Config, f *rule.File) {
    28  	migrateLibraryEmbed(c, f)
    29  	migrateGrpcCompilers(c, f)
    30  	flattenSrcs(c, f)
    31  	squashCgoLibrary(c, f)
    32  	squashXtest(c, f)
    33  	removeLegacyProto(c, f)
    34  	removeLegacyGazelle(c, f)
    35  	migrateNamingConvention(c, f)
    36  }
    37  
    38  // migrateNamingConvention renames rules according to go_naming_convention
    39  // directives.
    40  func migrateNamingConvention(c *config.Config, f *rule.File) {
    41  	// Determine old and new names for go_library and go_test.
    42  	nc := getGoConfig(c).goNamingConvention
    43  	importPath := InferImportPath(c, f.Pkg)
    44  	if importPath == "" {
    45  		return
    46  	}
    47  	var pkgName string // unknown unless there's a binary
    48  	if fileContainsGoBinary(c, f) {
    49  		pkgName = "main"
    50  	}
    51  	libName := libNameByConvention(nc, importPath, pkgName)
    52  	testName := testNameByConvention(nc, importPath)
    53  	var migrateLibName, migrateTestName string
    54  	switch nc {
    55  	case goDefaultLibraryNamingConvention:
    56  		migrateLibName = libNameByConvention(importNamingConvention, importPath, pkgName)
    57  		migrateTestName = testNameByConvention(importNamingConvention, importPath)
    58  	case importNamingConvention, importAliasNamingConvention:
    59  		migrateLibName = defaultLibName
    60  		migrateTestName = defaultTestName
    61  	default:
    62  		return
    63  	}
    64  
    65  	// Check whether the new names are in use. If there are rules with both old
    66  	// and new names, there will be a conflict.
    67  	var haveLib, haveMigrateLib, haveTest, haveMigrateTest bool
    68  	for _, r := range f.Rules {
    69  		switch {
    70  		case r.Name() == libName:
    71  			haveLib = true
    72  		case r.Kind() == "go_library" && r.Name() == migrateLibName && r.AttrString("importpath") == importPath:
    73  			haveMigrateLib = true
    74  		case r.Name() == testName:
    75  			haveTest = true
    76  		case r.Kind() == "go_test" && r.Name() == migrateTestName && strListAttrContains(r, "embed", ":"+migrateLibName):
    77  			haveMigrateTest = true
    78  		}
    79  	}
    80  	if haveLib && haveMigrateLib {
    81  		log.Printf("%[1]s: Tried to rename %[2]s to %[3]s, but %[3]s already exists.", f.Path, migrateLibName, libName)
    82  	}
    83  	if haveTest && haveMigrateTest {
    84  		log.Printf("%[1]s: Tried to rename %[2]s to %[3]s, but %[3]s already exists.", f.Path, migrateTestName, testName)
    85  	}
    86  	shouldMigrateLib := haveMigrateLib && !haveLib
    87  	shouldMigrateTest := haveMigrateTest && !haveTest
    88  
    89  	// Rename the targets and stuff in the same file that refers to them.
    90  	for _, r := range f.Rules {
    91  		// TODO(jayconrod): support map_kind directive.
    92  		// We'll need to move the metaresolver from resolve.RuleIndex to config.Config so we can access it from here.
    93  		switch r.Kind() {
    94  		case "go_binary":
    95  			if haveMigrateLib && shouldMigrateLib {
    96  				replaceInStrListAttr(r, "embed", ":"+migrateLibName, ":"+libName)
    97  			}
    98  		case "go_library":
    99  			if r.Name() == migrateLibName && shouldMigrateLib {
   100  				r.SetName(libName)
   101  			}
   102  		case "go_test":
   103  			if r.Name() == migrateTestName && shouldMigrateTest {
   104  				r.SetName(testName)
   105  			}
   106  			if shouldMigrateLib {
   107  				replaceInStrListAttr(r, "embed", ":"+migrateLibName, ":"+libName)
   108  			}
   109  		}
   110  	}
   111  }
   112  
   113  // fileContainsGoBinary returns whether the file has a go_binary rule.
   114  func fileContainsGoBinary(c *config.Config, f *rule.File) bool {
   115  	if f == nil {
   116  		return false
   117  	}
   118  	for _, r := range f.Rules {
   119  		kind := r.Kind()
   120  		if kind == "go_binary" {
   121  			return true
   122  		}
   123  
   124  		if mappedKind, ok := c.KindMap["go_binary"]; ok {
   125  			if mappedKind.KindName == kind {
   126  				return true
   127  			}
   128  		}
   129  	}
   130  	return false
   131  }
   132  
   133  func replaceInStrListAttr(r *rule.Rule, attr, old, new string) {
   134  	items := r.AttrStrings(attr)
   135  	changed := false
   136  	for i := range items {
   137  		if items[i] == old {
   138  			changed = true
   139  			items[i] = new
   140  		}
   141  	}
   142  	if changed {
   143  		r.SetAttr(attr, items)
   144  	}
   145  }
   146  
   147  func strListAttrContains(r *rule.Rule, attr, s string) bool {
   148  	items := r.AttrStrings(attr)
   149  	for _, item := range items {
   150  		if item == s {
   151  			return true
   152  		}
   153  	}
   154  	return false
   155  }
   156  
   157  // migrateLibraryEmbed converts "library" attributes to "embed" attributes,
   158  // preserving comments. This only applies to Go rules, and only if there is
   159  // no keep comment on "library" and no existing "embed" attribute.
   160  func migrateLibraryEmbed(c *config.Config, f *rule.File) {
   161  	for _, r := range f.Rules {
   162  		if !isGoRule(r.Kind()) {
   163  			continue
   164  		}
   165  		libExpr := r.Attr("library")
   166  		if libExpr == nil || rule.ShouldKeep(libExpr) || r.Attr("embed") != nil {
   167  			continue
   168  		}
   169  		r.DelAttr("library")
   170  		r.SetAttr("embed", &bzl.ListExpr{List: []bzl.Expr{libExpr}})
   171  	}
   172  }
   173  
   174  // migrateGrpcCompilers converts "go_grpc_library" rules into "go_proto_library"
   175  // rules with a "compilers" attribute.
   176  func migrateGrpcCompilers(c *config.Config, f *rule.File) {
   177  	for _, r := range f.Rules {
   178  		if r.Kind() != "go_grpc_library" || r.ShouldKeep() || r.Attr("compilers") != nil {
   179  			continue
   180  		}
   181  		r.SetKind("go_proto_library")
   182  		r.SetAttr("compilers", []string{grpcCompilerLabel})
   183  	}
   184  }
   185  
   186  // squashCgoLibrary removes cgo_library rules with the default name and
   187  // merges their attributes with go_library with the default name. If no
   188  // go_library rule exists, a new one will be created.
   189  //
   190  // Note that the library attribute is disregarded, so cgo_library and
   191  // go_library attributes will be squashed even if the cgo_library was unlinked.
   192  // MergeFile will remove unused values and attributes later.
   193  func squashCgoLibrary(c *config.Config, f *rule.File) {
   194  	// Find the default cgo_library and go_library rules.
   195  	var cgoLibrary, goLibrary *rule.Rule
   196  	for _, r := range f.Rules {
   197  		if r.Kind() == "cgo_library" && r.Name() == "cgo_default_library" && !r.ShouldKeep() {
   198  			if cgoLibrary != nil {
   199  				log.Printf("%s: when fixing existing file, multiple cgo_library rules with default name found", f.Path)
   200  				continue
   201  			}
   202  			cgoLibrary = r
   203  			continue
   204  		}
   205  		if r.Kind() == "go_library" && r.Name() == defaultLibName {
   206  			if goLibrary != nil {
   207  				log.Printf("%s: when fixing existing file, multiple go_library rules with default name referencing cgo_library found", f.Path)
   208  			}
   209  			goLibrary = r
   210  			continue
   211  		}
   212  	}
   213  
   214  	if cgoLibrary == nil {
   215  		return
   216  	}
   217  	if !c.ShouldFix {
   218  		log.Printf("%s: cgo_library is deprecated. Run 'gazelle fix' to squash with go_library.", f.Path)
   219  		return
   220  	}
   221  
   222  	if goLibrary == nil {
   223  		cgoLibrary.SetKind("go_library")
   224  		cgoLibrary.SetName(defaultLibName)
   225  		cgoLibrary.SetAttr("cgo", true)
   226  		return
   227  	}
   228  
   229  	if err := rule.SquashRules(cgoLibrary, goLibrary, f.Path); err != nil {
   230  		log.Print(err)
   231  		return
   232  	}
   233  	goLibrary.DelAttr("embed")
   234  	goLibrary.SetAttr("cgo", true)
   235  	cgoLibrary.Delete()
   236  }
   237  
   238  // squashXtest removes go_test rules with the default external name and merges
   239  // their attributes with a go_test rule with the default internal name. If
   240  // no internal go_test rule exists, a new one will be created (effectively
   241  // renaming the old rule).
   242  func squashXtest(c *config.Config, f *rule.File) {
   243  	// Search for internal and external tests.
   244  	var itest, xtest *rule.Rule
   245  	for _, r := range f.Rules {
   246  		if r.Kind() != "go_test" {
   247  			continue
   248  		}
   249  		if r.Name() == defaultTestName {
   250  			itest = r
   251  		} else if r.Name() == "go_default_xtest" {
   252  			xtest = r
   253  		}
   254  	}
   255  
   256  	if xtest == nil || xtest.ShouldKeep() || (itest != nil && itest.ShouldKeep()) {
   257  		return
   258  	}
   259  	if !c.ShouldFix {
   260  		if itest == nil {
   261  			log.Printf("%s: go_default_xtest is no longer necessary. Run 'gazelle fix' to rename to go_default_test.", f.Path)
   262  		} else {
   263  			log.Printf("%s: go_default_xtest is no longer necessary. Run 'gazelle fix' to squash with go_default_test.", f.Path)
   264  		}
   265  		return
   266  	}
   267  
   268  	// If there was no internal test, we can just rename the external test.
   269  	if itest == nil {
   270  		xtest.SetName(defaultTestName)
   271  		return
   272  	}
   273  
   274  	// Attempt to squash.
   275  	if err := rule.SquashRules(xtest, itest, f.Path); err != nil {
   276  		log.Print(err)
   277  		return
   278  	}
   279  	xtest.Delete()
   280  }
   281  
   282  // flattenSrcs transforms srcs attributes structured as concatenations of
   283  // lists and selects (generated from PlatformStrings; see
   284  // extractPlatformStringsExprs for matching details) into a sorted,
   285  // de-duplicated list. Comments are accumulated and de-duplicated across
   286  // duplicate expressions.
   287  func flattenSrcs(c *config.Config, f *rule.File) {
   288  	for _, r := range f.Rules {
   289  		if !isGoRule(r.Kind()) {
   290  			continue
   291  		}
   292  		oldSrcs := r.Attr("srcs")
   293  		if oldSrcs == nil {
   294  			continue
   295  		}
   296  		flatSrcs := rule.FlattenExpr(oldSrcs)
   297  		if flatSrcs != oldSrcs {
   298  			r.SetAttr("srcs", flatSrcs)
   299  		}
   300  	}
   301  }
   302  
   303  // removeLegacyProto removes uses of the old proto rules. It deletes loads
   304  // from go_proto_library.bzl. It deletes proto filegroups. It removes
   305  // go_proto_library attributes which are no longer recognized. New rules
   306  // are generated in place of the deleted rules, but attributes and comments
   307  // are not migrated.
   308  func removeLegacyProto(c *config.Config, f *rule.File) {
   309  	// Don't fix if the proto mode was set to something other than the default.
   310  	if pcMode := getProtoMode(c); pcMode != proto.DefaultMode {
   311  		return
   312  	}
   313  
   314  	// Scan for definitions to delete.
   315  	var protoLoads []*rule.Load
   316  	for _, l := range f.Loads {
   317  		if l.Name() == "@io_bazel_rules_go//proto:go_proto_library.bzl" {
   318  			protoLoads = append(protoLoads, l)
   319  		}
   320  	}
   321  	var protoFilegroups, protoRules []*rule.Rule
   322  	for _, r := range f.Rules {
   323  		if r.Kind() == "filegroup" && r.Name() == legacyProtoFilegroupName {
   324  			protoFilegroups = append(protoFilegroups, r)
   325  		}
   326  		if r.Kind() == "go_proto_library" {
   327  			protoRules = append(protoRules, r)
   328  		}
   329  	}
   330  	if len(protoLoads)+len(protoFilegroups) == 0 {
   331  		return
   332  	}
   333  	if !c.ShouldFix {
   334  		log.Printf("%s: go_proto_library.bzl is deprecated. Run 'gazelle fix' to replace old rules.", f.Path)
   335  		return
   336  	}
   337  
   338  	// Delete legacy proto loads and filegroups. Only delete go_proto_library
   339  	// rules if we deleted a load.
   340  	for _, l := range protoLoads {
   341  		l.Delete()
   342  	}
   343  	for _, r := range protoFilegroups {
   344  		r.Delete()
   345  	}
   346  	if len(protoLoads) > 0 {
   347  		for _, r := range protoRules {
   348  			r.Delete()
   349  		}
   350  	}
   351  }
   352  
   353  // removeLegacyGazelle removes loads of the "gazelle" macro from
   354  // @io_bazel_rules_go//go:def.bzl. The definition has moved to
   355  // @bazel_gazelle//:def.bzl, and the old one will be deleted soon.
   356  func removeLegacyGazelle(c *config.Config, f *rule.File) {
   357  	for _, l := range f.Loads {
   358  		if l.Name() == "@io_bazel_rules_go//go:def.bzl" && l.Has("gazelle") {
   359  			l.Remove("gazelle")
   360  			if l.IsEmpty() {
   361  				l.Delete()
   362  			}
   363  		}
   364  	}
   365  }
   366  
   367  func isGoRule(kind string) bool {
   368  	return kind == "go_library" ||
   369  		kind == "go_binary" ||
   370  		kind == "go_test" ||
   371  		kind == "go_proto_library" ||
   372  		kind == "go_grpc_library"
   373  }