blob: f56de534a5f0cae3299dde73a3563f75ae16aa4b [file] [log] [blame]
/* 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 merger
import (
"log"
"sort"
"github.com/bazelbuild/bazel-gazelle/internal/config"
bf "github.com/bazelbuild/buildtools/build"
)
// Much of this file could be simplified by using
// github.com/bazelbuild/buildtools/edit. However, through a transitive
// dependency, that library depends on a proto in Bazel itself, which is
// a 95MB download. Not worth it.
// FixFile updates rules in oldFile that were generated by an older version of
// Gazelle to a newer form that can be merged with freshly generated rules.
//
// FixLoads should be called after this, since it will fix load
// statements that may be broken by transformations applied by this function.
func FixFile(c *config.Config, oldFile *bf.File) *bf.File {
fixedFile := squashCgoLibrary(oldFile)
return removeLegacyProto(c, fixedFile)
}
// 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.
// MergeWithExisting will remove unused values and attributes later.
func squashCgoLibrary(oldFile *bf.File) *bf.File {
// Find the default cgo_library and go_library rules.
var cgoLibrary, goLibrary bf.Rule
cgoLibraryIndex := -1
goLibraryIndex := -1
for i, stmt := range oldFile.Stmt {
c, ok := stmt.(*bf.CallExpr)
if !ok {
continue
}
r := bf.Rule{Call: c}
if r.Kind() == "cgo_library" && r.Name() == config.DefaultCgoLibName && !shouldKeep(c) {
if cgoLibrary.Call != nil {
log.Printf("%s: when fixing existing file, multiple cgo_library rules with default name found", oldFile.Path)
continue
}
cgoLibrary = r
cgoLibraryIndex = i
continue
}
if r.Kind() == "go_library" && r.Name() == config.DefaultLibName {
if goLibrary.Call != nil {
log.Printf("%s: when fixing existing file, multiple go_library rules with default name referencing cgo_library found", oldFile.Path)
continue
}
goLibrary = r
goLibraryIndex = i
}
}
if cgoLibrary.Call == nil {
return oldFile
}
// If go_library has a '# keep' comment, just delete cgo_library.
if goLibrary.Call != nil && shouldKeep(goLibrary.Call) {
fixedFile := *oldFile
fixedFile.Stmt = append(fixedFile.Stmt[:cgoLibraryIndex], fixedFile.Stmt[cgoLibraryIndex+1:]...)
return &fixedFile
}
// Copy the comments and attributes from cgo_library into go_library. If no
// go_library exists, create an empty one.
var fixedGoLibraryExpr bf.CallExpr
fixedGoLibrary := bf.Rule{Call: &fixedGoLibraryExpr}
if goLibrary.Call == nil {
fixedGoLibrary.SetKind("go_library")
fixedGoLibrary.SetAttr("name", &bf.StringExpr{Value: config.DefaultLibName})
if vis := cgoLibrary.Attr("visibility"); vis != nil {
fixedGoLibrary.SetAttr("visibility", vis)
}
} else {
fixedGoLibraryExpr = *goLibrary.Call
fixedGoLibraryExpr.List = append([]bf.Expr{}, goLibrary.Call.List...)
}
fixedGoLibrary.DelAttr("embed")
fixedGoLibrary.SetAttr("cgo", &bf.LiteralExpr{Token: "True"})
fixedGoLibraryExpr.Comments.Before = append(fixedGoLibraryExpr.Comments.Before, cgoLibrary.Call.Comments.Before...)
fixedGoLibraryExpr.Comments.Suffix = append(fixedGoLibraryExpr.Comments.Suffix, cgoLibrary.Call.Comments.Suffix...)
fixedGoLibraryExpr.Comments.After = append(fixedGoLibraryExpr.Comments.After, cgoLibrary.Call.Comments.After...)
for _, key := range []string{"cdeps", "clinkopts", "copts", "data", "deps", "gc_goopts", "srcs"} {
goLibraryAttr := fixedGoLibrary.Attr(key)
cgoLibraryAttr := cgoLibrary.Attr(key)
if cgoLibraryAttr == nil {
continue
}
if fixedAttr, err := squashExpr(goLibraryAttr, cgoLibraryAttr); err == nil {
fixedGoLibrary.SetAttr(key, fixedAttr)
}
}
// Rebuild the file with the cgo_library removed and the go_library replaced.
// If the go_library didn't already exist, it will replace cgo_library.
fixedFile := *oldFile
if goLibrary.Call == nil {
fixedFile.Stmt = append([]bf.Expr{}, oldFile.Stmt...)
fixedFile.Stmt[cgoLibraryIndex] = &fixedGoLibraryExpr
} else {
fixedFile.Stmt = append(oldFile.Stmt[:cgoLibraryIndex], oldFile.Stmt[cgoLibraryIndex+1:]...)
if goLibraryIndex > cgoLibraryIndex {
goLibraryIndex--
}
fixedFile.Stmt[goLibraryIndex] = &fixedGoLibraryExpr
}
return &fixedFile
}
// squashExpr combines two expressions. Unlike mergeExpr, squashExpr does not
// discard information from an "old" expression. It does not sort or de-duplicate
// elements. Any non-scalar expressions that mergeExpr understands can be
// squashed.
func squashExpr(x, y bf.Expr) (bf.Expr, error) {
xExprs, err := extractPlatformStringsExprs(x)
if err != nil {
return nil, err
}
yExprs, err := extractPlatformStringsExprs(y)
if err != nil {
return nil, err
}
squashedExprs, err := squashPlatformStringsExprs(xExprs, yExprs)
if err != nil {
return nil, err
}
return makePlatformStringsExpr(squashedExprs), nil
}
func squashPlatformStringsExprs(x, y platformStringsExprs) (platformStringsExprs, error) {
var ps platformStringsExprs
var err error
ps.generic = squashList(x.generic, y.generic)
if ps.os, err = squashDict(x.os, y.os); err != nil {
return platformStringsExprs{}, err
}
if ps.arch, err = squashDict(x.arch, y.arch); err != nil {
return platformStringsExprs{}, err
}
if ps.platform, err = squashDict(x.platform, y.platform); err != nil {
return platformStringsExprs{}, err
}
return ps, nil
}
func squashList(x, y *bf.ListExpr) *bf.ListExpr {
if x == nil {
return y
}
if y == nil {
return x
}
squashed := *x
squashed.Comments.Before = append(x.Comments.Before, y.Comments.Before...)
squashed.Comments.Suffix = append(x.Comments.Suffix, y.Comments.Suffix...)
squashed.Comments.After = append(x.Comments.After, y.Comments.After...)
squashed.List = append(x.List, y.List...)
return &squashed
}
func squashDict(x, y *bf.DictExpr) (*bf.DictExpr, error) {
if x == nil {
return y, nil
}
if y == nil {
return x, nil
}
squashed := *x
squashed.Comments.Before = append(x.Comments.Before, y.Comments.Before...)
squashed.Comments.Suffix = append(x.Comments.Suffix, y.Comments.Suffix...)
squashed.Comments.After = append(x.Comments.After, y.Comments.After...)
xCaseIndex := make(map[string]int)
for i, e := range x.List {
kv, ok := e.(*bf.KeyValueExpr)
if !ok {
continue
}
key, ok := kv.Key.(*bf.StringExpr)
if !ok {
continue
}
xCaseIndex[key.Value] = i
}
for _, e := range y.List {
kv, ok := e.(*bf.KeyValueExpr)
if !ok {
squashed.List = append(squashed.List, e)
continue
}
key, ok := e.(*bf.StringExpr)
if !ok {
squashed.List = append(squashed.List, e)
continue
}
i, ok := xCaseIndex[key.Value]
if !ok {
squashed.List = append(squashed.List, e)
continue
}
squashedElem, err := squashExpr(x.List[i], kv.Value)
if err != nil {
return nil, err
}
x.List[i] = squashedElem
}
return &squashed, nil
}
// 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, oldFile *bf.File) *bf.File {
// Don't fix if the proto mode was set to something other than the default.
if c.ProtoMode != config.DefaultProtoMode {
return oldFile
}
// Scan for definitions to delete.
var deletedIndices []int
var protoIndices []int
shouldDeleteProtos := false
for i, stmt := range oldFile.Stmt {
c, ok := stmt.(*bf.CallExpr)
if !ok {
continue
}
x, ok := c.X.(*bf.LiteralExpr)
if !ok {
continue
}
if x.Token == "load" && len(c.List) > 0 {
if name, ok := c.List[0].(*bf.StringExpr); ok && name.Value == "@io_bazel_rules_go//proto:go_proto_library.bzl" {
deletedIndices = append(deletedIndices, i)
shouldDeleteProtos = true
}
continue
}
if x.Token == "filegroup" {
r := bf.Rule{Call: c}
if r.Name() == config.DefaultProtosName {
deletedIndices = append(deletedIndices, i)
}
continue
}
if x.Token == "go_proto_library" {
protoIndices = append(protoIndices, i)
}
}
if len(deletedIndices) == 0 {
return oldFile
}
// Rebuild the file without deleted statements. Only delete go_proto_library
// rules if we deleted a load.
if shouldDeleteProtos {
deletedIndices = append(deletedIndices, protoIndices...)
sort.Ints(deletedIndices)
}
fixedFile := *oldFile
fixedFile.Stmt = deleteIndices(oldFile.Stmt, deletedIndices)
return &fixedFile
}
// FixFileMinor updates rules in oldFile that were generated by an older version
// of Gazelle to a newer form that can be merged with freshly generated rules.
//
// FixFileMinor includes only small, low-risk fixes that can be applied in
// update mode. When both FixFileMinor and FixFile are called, FixFileMinor
// should be called first.
//
// FixLoads should be called after this, since it will fix load
// statements that may be broken by transformations applied by this function.
func FixFileMinor(c *config.Config, oldFile *bf.File) *bf.File {
fixedFile := migrateLibraryEmbed(c, oldFile)
fixedFile = migrateGrpcCompilers(c, fixedFile)
return removeBinaryImportPath(c, fixedFile)
}
// 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, oldFile *bf.File) *bf.File {
fixed := false
fixedFile := *oldFile
for i, stmt := range fixedFile.Stmt {
call, ok := stmt.(*bf.CallExpr)
if !ok {
continue
}
rule := bf.Rule{Call: call}
if kind := rule.Kind(); !isGoRule(kind) || shouldKeep(stmt) {
continue
}
libExpr := rule.Attr("library")
if libExpr == nil || shouldKeep(libExpr) || rule.Attr("embed") != nil {
continue
}
fixedCall := *call
rule.Call = &fixedCall
rule.DelAttr("library")
rule.SetAttr("embed", &bf.ListExpr{List: []bf.Expr{libExpr}})
fixedFile.Stmt[i] = &fixedCall
fixed = true
}
if !fixed {
return oldFile
}
return &fixedFile
}
// migrateGrpcCompilers converts "go_grpc_library" rules into "go_proto_library"
// rules with a "compilers" attribute.
func migrateGrpcCompilers(c *config.Config, oldFile *bf.File) *bf.File {
fixed := false
fixedFile := *oldFile
for i, stmt := range fixedFile.Stmt {
call, ok := stmt.(*bf.CallExpr)
if !ok {
continue
}
rule := bf.Rule{Call: call}
if rule.Kind() != "go_grpc_library" || shouldKeep(stmt) || rule.Attr("compilers") != nil {
continue
}
fixedCall := *call
fixedCall.List = make([]bf.Expr, len(call.List))
copy(fixedCall.List, call.List)
rule.Call = &fixedCall
rule.SetKind("go_proto_library")
rule.SetAttr("compilers", &bf.ListExpr{
List: []bf.Expr{&bf.StringExpr{Value: config.GrpcCompilerLabel}},
})
fixedFile.Stmt[i] = &fixedCall
fixed = true
}
if !fixed {
return oldFile
}
return &fixedFile
}
// removeBinaryImportPath removes "importpath" attributes from "go_binary"
// and "go_test" rules. These are now deprecated.
func removeBinaryImportPath(c *config.Config, oldFile *bf.File) *bf.File {
fixed := false
fixedFile := *oldFile
for i, stmt := range fixedFile.Stmt {
call, ok := stmt.(*bf.CallExpr)
if !ok {
continue
}
rule := bf.Rule{Call: call}
if rule.Kind() != "go_binary" && rule.Kind() != "go_test" || rule.Attr("importpath") == nil {
continue
}
fixedCall := *call
fixedCall.List = make([]bf.Expr, len(call.List))
copy(fixedCall.List, call.List)
rule.Call = &fixedCall
rule.DelAttr("importpath")
fixedFile.Stmt[i] = &fixedCall
fixed = true
}
if !fixed {
return oldFile
}
return &fixedFile
}
// FixLoads removes loads of unused go rules and adds loads of newly used rules.
// This should be called after FixFile and MergeWithExisting, since symbols
// may be introduced that aren't loaded.
func FixLoads(oldFile *bf.File) *bf.File {
// Make a list of load statements in the file. Keep track of loads of known
// files, since these may be changed. Keep track of known symbols loaded from
// unknown files; we will not add loads for these.
type loadInfo struct {
index int
file string
old, fixed *bf.CallExpr
}
var loads []loadInfo
otherLoadedKinds := make(map[string]bool)
for i, stmt := range oldFile.Stmt {
c, ok := stmt.(*bf.CallExpr)
if !ok {
continue
}
x, ok := c.X.(*bf.LiteralExpr)
if !ok || x.Token != "load" {
continue
}
if len(c.List) == 0 {
continue
}
label, ok := c.List[0].(*bf.StringExpr)
if !ok {
continue
}
if knownFiles[label.Value] {
loads = append(loads, loadInfo{index: i, file: label.Value, old: c})
continue
}
for _, arg := range c.List[1:] {
switch sym := arg.(type) {
case *bf.StringExpr:
otherLoadedKinds[sym.Value] = true
case *bf.BinaryExpr:
if sym.Op != "=" {
continue
}
if x, ok := sym.X.(*bf.LiteralExpr); ok {
otherLoadedKinds[x.Token] = true
}
}
}
}
// Make a map of all the symbols from known files used in this file.
usedKinds := make(map[string]map[string]bool)
for _, stmt := range oldFile.Stmt {
c, ok := stmt.(*bf.CallExpr)
if !ok {
continue
}
x, ok := c.X.(*bf.LiteralExpr)
if !ok {
continue
}
kind := x.Token
if file, ok := knownKinds[kind]; ok && !otherLoadedKinds[kind] {
if usedKinds[file] == nil {
usedKinds[file] = make(map[string]bool)
}
usedKinds[file][kind] = true
}
}
// Fix the load statements. The order is important, so we iterate over
// knownLoads instead of knownFiles.
changed := false
var newFirstLoads []*bf.CallExpr
for _, l := range knownLoads {
file := l.file
first := true
for i, _ := range loads {
li := &loads[i]
if li.file != file {
continue
}
if first {
li.fixed = fixLoad(li.old, file, usedKinds[file])
first = false
} else {
li.fixed = fixLoad(li.old, file, nil)
}
changed = changed || li.fixed != li.old
}
if first {
load := fixLoad(nil, file, usedKinds[file])
if load != nil {
newFirstLoads = append(newFirstLoads, load)
changed = true
}
}
}
if !changed {
return oldFile
}
// Rebuild the file.
fixedFile := *oldFile
fixedFile.Stmt = make([]bf.Expr, 0, len(oldFile.Stmt)+len(newFirstLoads))
for _, l := range newFirstLoads {
fixedFile.Stmt = append(fixedFile.Stmt, l)
}
loadIndex := 0
for i, stmt := range oldFile.Stmt {
if loadIndex < len(loads) && i == loads[loadIndex].index {
if loads[loadIndex].fixed != nil {
fixedFile.Stmt = append(fixedFile.Stmt, loads[loadIndex].fixed)
}
loadIndex++
continue
}
fixedFile.Stmt = append(fixedFile.Stmt, stmt)
}
return &fixedFile
}
// knownLoads is a list of files Gazelle will generate loads from and
// the symbols it knows about. All symbols Gazelle ever generated
// loads for are present, including symbols it no longer uses (e.g.,
// cgo_library). Manually loaded symbols (e.g., go_embed_data) are not
// included. The order of the files here will match the order of
// generated load statements. The symbols should be sorted
// lexicographically.
var knownLoads = []struct {
file string
kinds []string
}{
{
"@io_bazel_rules_go//go:def.bzl",
[]string{
"cgo_library",
"go_binary",
"go_library",
"go_prefix",
"go_repository",
"go_test",
},
}, {
"@io_bazel_rules_go//proto:def.bzl",
[]string{
"go_grpc_library",
"go_proto_library",
},
},
}
// knownFiles is the set of labels for files that Gazelle loads symbols from.
var knownFiles map[string]bool
// knownKinds is a map from symbols to labels of the files they are loaded
// from.
var knownKinds map[string]string
func init() {
knownFiles = make(map[string]bool)
knownKinds = make(map[string]string)
for _, l := range knownLoads {
knownFiles[l.file] = true
for _, k := range l.kinds {
knownKinds[k] = l.file
}
}
}
// fixLoad updates a load statement. load must be a load statement for
// the Go rules or nil. If nil, a new statement may be created. Symbols in
// kinds are added if they are not already present, symbols in knownKinds
// are removed if they are not in kinds, and other symbols and arguments
// are preserved. nil is returned if the statement should be deleted because
// it is empty.
func fixLoad(load *bf.CallExpr, file string, kinds map[string]bool) *bf.CallExpr {
var fixed bf.CallExpr
if load == nil {
fixed = bf.CallExpr{
X: &bf.LiteralExpr{Token: "load"},
List: []bf.Expr{
&bf.StringExpr{Value: file},
},
ForceCompact: true,
}
} else {
fixed = *load
}
var symbols []*bf.StringExpr
var otherArgs []bf.Expr
loadedKinds := make(map[string]bool)
var added, removed int
for _, arg := range fixed.List[1:] {
if s, ok := arg.(*bf.StringExpr); ok {
if knownKinds[s.Value] == "" || kinds != nil && kinds[s.Value] {
symbols = append(symbols, s)
loadedKinds[s.Value] = true
} else {
removed++
}
} else {
otherArgs = append(otherArgs, arg)
}
}
if kinds != nil {
for kind, _ := range kinds {
if _, ok := loadedKinds[kind]; !ok {
symbols = append(symbols, &bf.StringExpr{Value: kind})
added++
}
}
}
if added == 0 && removed == 0 {
if load != nil && len(load.List) == 1 {
// Special case: delete existing empty load.
return nil
}
return load
}
sort.Stable(byString(symbols))
fixed.List = fixed.List[:1]
for _, sym := range symbols {
fixed.List = append(fixed.List, sym)
}
fixed.List = append(fixed.List, otherArgs...)
if len(fixed.List) == 1 {
return nil
}
return &fixed
}
type byString []*bf.StringExpr
func (s byString) Len() int {
return len(s)
}
func (s byString) Less(i, j int) bool {
return s[i].Value < s[j].Value
}
func (s byString) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func isGoRule(kind string) bool {
return kind == "go_library" ||
kind == "go_binary" ||
kind == "go_test" ||
kind == "go_proto_library" ||
kind == "go_grpc_library"
}