| /* 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 ( |
| "errors" |
| "fmt" |
| "log" |
| "sort" |
| "strings" |
| |
| "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 f that were generated by an older version of |
| // Gazelle to a newer form that can be merged with freshly generated rules. |
| // |
| // If c.ShouldFix is true, FixFile may perform potentially destructive |
| // transformations, such as squashing or deleting rules (e.g., cgo_library). |
| // If not, FixFile will perform a set of low-risk transformations (e.g., removing |
| // unused attributes) and will print a message about transformations it |
| // would have performed. |
| // |
| // 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, f *bf.File) { |
| migrateLibraryEmbed(c, f) |
| migrateGrpcCompilers(c, f) |
| flattenSrcs(c, f) |
| squashCgoLibrary(c, f) |
| squashXtest(c, f) |
| removeLegacyProto(c, f) |
| } |
| |
| // 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 *bf.File) { |
| for _, stmt := range f.Stmt { |
| call, ok := stmt.(*bf.CallExpr) |
| if !ok || shouldKeep(stmt) { |
| continue |
| } |
| rule := bf.Rule{Call: call} |
| if !isGoRule(rule.Kind()) { |
| continue |
| } |
| libExpr := rule.Attr("library") |
| if libExpr == nil || shouldKeep(libExpr) || rule.Attr("embed") != nil { |
| continue |
| } |
| rule.DelAttr("library") |
| rule.SetAttr("embed", &bf.ListExpr{List: []bf.Expr{libExpr}}) |
| } |
| } |
| |
| // migrateGrpcCompilers converts "go_grpc_library" rules into "go_proto_library" |
| // rules with a "compilers" attribute. |
| func migrateGrpcCompilers(c *config.Config, f *bf.File) { |
| for _, stmt := range f.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 |
| } |
| rule.SetKind("go_proto_library") |
| rule.SetAttr("compilers", &bf.ListExpr{ |
| List: []bf.Expr{&bf.StringExpr{Value: config.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 *bf.File) { |
| // Find the default cgo_library and go_library rules. |
| var cgoLibrary, goLibrary bf.Rule |
| cgoLibraryIndex := -1 |
| |
| for i, stmt := range f.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", f.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", f.Path) |
| continue |
| } |
| goLibrary = r |
| } |
| } |
| |
| if cgoLibrary.Call == nil { |
| return |
| } |
| if !c.ShouldFix { |
| log.Printf("%s: cgo_library is deprecated. Run 'gazelle fix' to squash with go_library.", f.Path) |
| return |
| } |
| |
| // Delete cgo_library. |
| f.Stmt = append(f.Stmt[:cgoLibraryIndex], f.Stmt[cgoLibraryIndex+1:]...) |
| |
| // Copy the comments and attributes from cgo_library into go_library. If no |
| // go_library exists, create an empty one. |
| if goLibrary.Call != nil && shouldKeep(goLibrary.Call) { |
| return |
| } |
| if goLibrary.Call == nil { |
| goLibrary.Call = &bf.CallExpr{} |
| goLibrary.SetKind("go_library") |
| goLibrary.SetAttr("name", &bf.StringExpr{Value: config.DefaultLibName}) |
| if vis := cgoLibrary.Attr("visibility"); vis != nil { |
| goLibrary.SetAttr("visibility", vis) |
| } |
| f.Stmt = append(f.Stmt, goLibrary.Call) |
| } |
| |
| goLibrary.DelAttr("embed") |
| goLibrary.SetAttr("cgo", &bf.LiteralExpr{Token: "True"}) |
| goLibrary.Call.Comments.Before = append(goLibrary.Call.Comments.Before, cgoLibrary.Call.Comments.Before...) |
| goLibrary.Call.Comments.Suffix = append(goLibrary.Call.Comments.Suffix, cgoLibrary.Call.Comments.Suffix...) |
| goLibrary.Call.Comments.After = append(goLibrary.Call.Comments.After, cgoLibrary.Call.Comments.After...) |
| for _, key := range []string{"cdeps", "clinkopts", "copts", "data", "deps", "gc_goopts", "srcs"} { |
| goLibraryAttr := goLibrary.Attr(key) |
| cgoLibraryAttr := cgoLibrary.Attr(key) |
| if cgoLibraryAttr == nil { |
| continue |
| } |
| if fixedAttr, err := squashExpr(goLibraryAttr, cgoLibraryAttr); err == nil { |
| goLibrary.SetAttr(key, fixedAttr) |
| } |
| } |
| } |
| |
| // 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 *bf.File) { |
| // Search for internal and external tests. |
| var itest, xtest bf.Rule |
| xtestIndex := -1 |
| for i, stmt := range f.Stmt { |
| call, ok := stmt.(*bf.CallExpr) |
| if !ok { |
| continue |
| } |
| rule := bf.Rule{Call: call} |
| if rule.Kind() != "go_test" { |
| continue |
| } |
| if name := rule.Name(); name == config.DefaultTestName { |
| itest = rule |
| } else if name == config.DefaultXTestName { |
| xtest = rule |
| xtestIndex = i |
| } |
| } |
| |
| if xtest.Call == nil || shouldKeep(xtest.Call) || (itest.Call != nil && shouldKeep(itest.Call)) { |
| return |
| } |
| if !c.ShouldFix { |
| if itest.Call == 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.Call == nil { |
| xtest.SetAttr("name", &bf.StringExpr{Value: config.DefaultTestName}) |
| return |
| } |
| |
| // Attempt to squash. |
| if err := squashRule(xtest.Call, itest.Call, f.Path); err != nil { |
| log.Print(err) |
| return |
| } |
| |
| // Delete the external test. |
| f.Stmt = append(f.Stmt[:xtestIndex], f.Stmt[xtestIndex+1:]...) |
| |
| // Copy comments and attributes from external test to internal test. |
| itest.Call.Comments.Before = append(itest.Call.Comments.Before, xtest.Call.Comments.Before...) |
| itest.Call.Comments.Suffix = append(itest.Call.Comments.Suffix, xtest.Call.Comments.Suffix...) |
| itest.Call.Comments.After = append(itest.Call.Comments.After, xtest.Call.Comments.After...) |
| } |
| |
| // squashRule copies information in mergeable attributes from src into dst. This |
| // works similarly to mergeRule, but it doesn't discard information from dst. It |
| // detects duplicate elements, but it doesn't sort elements after squashing. |
| // If squashing fails because the expression is understood, an error is |
| // returned, and neither rule is modified. |
| func squashRule(src, dst *bf.CallExpr, filename string) error { |
| srcRule := bf.Rule{Call: src} |
| dstRule := bf.Rule{Call: dst} |
| kind := dstRule.Kind() |
| type squashedAttr struct { |
| key string |
| attr bf.Expr |
| } |
| var squashedAttrs []squashedAttr |
| for _, k := range srcRule.AttrKeys() { |
| srcExpr := srcRule.Attr(k) |
| dstExpr := dstRule.Attr(k) |
| if dstExpr == nil { |
| dstRule.SetAttr(k, srcExpr) |
| continue |
| } |
| if !PreResolveAttrs[kind][k] && !PostResolveAttrs[kind][k] { |
| // keep non-mergeable attributes in dst (e.g., name, visibility) |
| continue |
| } |
| squashedExpr, err := squashExpr(srcExpr, dstExpr) |
| if err != nil { |
| start, end := dstExpr.Span() |
| return fmt.Errorf("%s:%d.%d-%d.%d: could not squash expression", filename, start.Line, start.LineRune, end.Line, end.LineRune) |
| } |
| squashedAttrs = append(squashedAttrs, squashedAttr{key: k, attr: squashedExpr}) |
| } |
| for _, a := range squashedAttrs { |
| dstRule.SetAttr(a.key, a.attr) |
| } |
| return nil |
| } |
| |
| func squashExpr(src, dst bf.Expr) (bf.Expr, error) { |
| if shouldKeep(dst) { |
| return dst, nil |
| } |
| if isScalar(dst) { |
| // may lose src, but they should always be the same. |
| return dst, nil |
| } |
| srcExprs, err := extractPlatformStringsExprs(src) |
| if err != nil { |
| return nil, err |
| } |
| dstExprs, err := extractPlatformStringsExprs(dst) |
| if err != nil { |
| return nil, err |
| } |
| squashedExprs, err := squashPlatformStringsExprs(srcExprs, dstExprs) |
| if err != nil { |
| return nil, err |
| } |
| return makePlatformStringsExpr(squashedExprs), nil |
| } |
| |
| func squashPlatformStringsExprs(x, y platformStringsExprs) (platformStringsExprs, error) { |
| var ps platformStringsExprs |
| var err error |
| if ps.generic, err = squashList(x.generic, y.generic); err != nil { |
| return platformStringsExprs{}, err |
| } |
| 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, error) { |
| if x == nil { |
| return y, nil |
| } |
| if y == nil { |
| return x, nil |
| } |
| |
| ls := makeListSquasher() |
| for _, e := range x.List { |
| s, ok := e.(*bf.StringExpr) |
| if !ok { |
| return nil, errors.New("could not squash non-string") |
| } |
| ls.add(s) |
| } |
| for _, e := range y.List { |
| s, ok := e.(*bf.StringExpr) |
| if !ok { |
| return nil, errors.New("could not squash non-string") |
| } |
| ls.add(s) |
| } |
| squashed := ls.list() |
| 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...) |
| return squashed, nil |
| } |
| |
| func squashDict(x, y *bf.DictExpr) (*bf.DictExpr, error) { |
| if x == nil { |
| return y, nil |
| } |
| if y == nil { |
| return x, nil |
| } |
| |
| cases := make(map[string]*bf.KeyValueExpr) |
| addCase := func(e bf.Expr) error { |
| kv := e.(*bf.KeyValueExpr) |
| key, ok := kv.Key.(*bf.StringExpr) |
| if !ok { |
| return errors.New("could not squash non-string dict key") |
| } |
| if _, ok := kv.Value.(*bf.ListExpr); !ok { |
| return errors.New("could not squash non-list dict value") |
| } |
| if c, ok := cases[key.Value]; ok { |
| if sq, err := squashList(kv.Value.(*bf.ListExpr), c.Value.(*bf.ListExpr)); err != nil { |
| return err |
| } else { |
| c.Value = sq |
| } |
| } else { |
| kvCopy := *kv |
| cases[key.Value] = &kvCopy |
| } |
| return nil |
| } |
| |
| for _, e := range x.List { |
| if err := addCase(e); err != nil { |
| return nil, err |
| } |
| } |
| for _, e := range y.List { |
| if err := addCase(e); err != nil { |
| return nil, err |
| } |
| } |
| |
| keys := make([]string, 0, len(cases)) |
| haveDefault := false |
| for k := range cases { |
| if k == "//conditions:default" { |
| haveDefault = true |
| continue |
| } |
| keys = append(keys, k) |
| } |
| sort.Strings(keys) |
| if haveDefault { |
| keys = append(keys, "//conditions:default") // must be last |
| } |
| |
| 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 = make([]bf.Expr, 0, len(cases)) |
| for _, k := range keys { |
| squashed.List = append(squashed.List, cases[k]) |
| } |
| 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, f *bf.File) { |
| // Don't fix if the proto mode was set to something other than the default. |
| if c.ProtoMode != config.DefaultProtoMode { |
| return |
| } |
| |
| // Scan for definitions to delete. |
| var deletedIndices []int |
| var protoIndices []int |
| shouldDeleteProtos := false |
| for i, stmt := range f.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 |
| } |
| if !c.ShouldFix { |
| log.Printf("%s: go_proto_library.bzl is deprecated. Run 'gazelle fix' to replace old rules.", f.Path) |
| return |
| } |
| |
| // 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) |
| } |
| f.Stmt = deleteIndices(f.Stmt, deletedIndices) |
| } |
| |
| // 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 *bf.File) { |
| for _, stmt := range f.Stmt { |
| call, ok := stmt.(*bf.CallExpr) |
| if !ok { |
| continue |
| } |
| rule := bf.Rule{Call: call} |
| if !isGoRule(rule.Kind()) { |
| continue |
| } |
| oldSrcs := rule.Attr("srcs") |
| if oldSrcs == nil { |
| continue |
| } |
| flatSrcs := flattenSrcsExpr(oldSrcs) |
| rule.SetAttr("srcs", flatSrcs) |
| } |
| } |
| |
| func flattenSrcsExpr(oldSrcs bf.Expr) bf.Expr { |
| oldExprs, err := extractPlatformStringsExprs(oldSrcs) |
| if err != nil { |
| return oldSrcs |
| } |
| |
| ls := makeListSquasher() |
| addElem := func(e bf.Expr) bool { |
| s, ok := e.(*bf.StringExpr) |
| if !ok { |
| return false |
| } |
| ls.add(s) |
| return true |
| } |
| addList := func(e bf.Expr) bool { |
| l, ok := e.(*bf.ListExpr) |
| if !ok { |
| return false |
| } |
| for _, elem := range l.List { |
| if !addElem(elem) { |
| return false |
| } |
| } |
| return true |
| } |
| addDict := func(d *bf.DictExpr) bool { |
| for _, kv := range d.List { |
| if !addList(kv.(*bf.KeyValueExpr).Value) { |
| return false |
| } |
| } |
| return true |
| } |
| |
| if oldExprs.generic != nil { |
| if !addList(oldExprs.generic) { |
| return oldSrcs |
| } |
| } |
| for _, d := range []*bf.DictExpr{oldExprs.os, oldExprs.arch, oldExprs.platform} { |
| if d == nil { |
| continue |
| } |
| if !addDict(d) { |
| return oldSrcs |
| } |
| } |
| |
| return ls.list() |
| } |
| |
| // FixLoads removes loads of unused go rules and adds loads of newly used rules. |
| // This should be called after FixFile and MergeFile, since symbols |
| // may be introduced that aren't loaded. |
| func FixLoads(f *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 f.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 f.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 |
| type newLoad struct { |
| index int |
| load *bf.CallExpr |
| } |
| var newLoads []newLoad |
| 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 { |
| index := newLoadIndex(f.Stmt, l.after) |
| newLoads = append(newLoads, newLoad{index, load}) |
| changed = true |
| } |
| } |
| } |
| if !changed { |
| return |
| } |
| sort.Slice(newLoads, func(i, j int) bool { |
| return newLoads[i].index < newLoads[j].index |
| }) |
| |
| // Rebuild the file. Insert new loads at appropriate indices, replace fixed |
| // loads, and drop deleted loads. |
| oldStmt := f.Stmt |
| f.Stmt = make([]bf.Expr, 0, len(oldStmt)+len(newLoads)) |
| newLoadIndex := 0 |
| loadIndex := 0 |
| for i, stmt := range oldStmt { |
| for newLoadIndex < len(newLoads) && i == newLoads[newLoadIndex].index { |
| f.Stmt = append(f.Stmt, newLoads[newLoadIndex].load) |
| newLoadIndex++ |
| } |
| if loadIndex < len(loads) && i == loads[loadIndex].index { |
| if loads[loadIndex].fixed != nil { |
| f.Stmt = append(f.Stmt, loads[loadIndex].fixed) |
| } |
| loadIndex++ |
| continue |
| } |
| f.Stmt = append(f.Stmt, stmt) |
| } |
| } |
| |
| // 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. |
| // |
| // Some symbols have a list of function calls that they should be loaded |
| // after. This is important for WORKSPACE, where function calls may |
| // introduce new repository names. |
| // |
| // The order of the files here will match the order of generated load |
| // statements. The symbols should be sorted lexicographically. If a |
| // symbol appears in more than one file (e.g., because it was moved), |
| // it will be loaded from the last file in this list. |
| var knownLoads = []struct { |
| file string |
| kinds []string |
| after []string |
| }{ |
| { |
| file: "@io_bazel_rules_go//go:def.bzl", |
| kinds: []string{ |
| "cgo_library", |
| "go_binary", |
| "go_library", |
| "go_prefix", |
| "go_repository", |
| "go_test", |
| }, |
| }, { |
| file: "@io_bazel_rules_go//proto:def.bzl", |
| kinds: []string{ |
| "go_grpc_library", |
| "go_proto_library", |
| }, |
| }, { |
| file: "@bazel_gazelle//:deps.bzl", |
| kinds: []string{ |
| "go_repository", |
| }, |
| after: []string{ |
| "go_rules_dependencies", |
| "go_register_toolchains", |
| "gazelle_dependencies", |
| }, |
| }, |
| } |
| |
| // 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 |
| } |
| |
| // newLoadIndex returns the index in stmts where a new load statement should |
| // be inserted. after is a list of function names that the load should not |
| // be inserted before. |
| func newLoadIndex(stmts []bf.Expr, after []string) int { |
| if len(after) == 0 { |
| return 0 |
| } |
| index := 0 |
| for i, stmt := range stmts { |
| call, ok := stmt.(*bf.CallExpr) |
| if !ok { |
| continue |
| } |
| x, ok := call.X.(*bf.LiteralExpr) |
| if !ok { |
| continue |
| } |
| for _, a := range after { |
| if x.Token == a { |
| index = i + 1 |
| } |
| } |
| } |
| return index |
| } |
| |
| // FixWorkspace updates rules in the WORKSPACE file f that were used with an |
| // older version of rules_go or gazelle. |
| func FixWorkspace(f *bf.File) { |
| removeLegacyGoRepository(f) |
| } |
| |
| // CheckGazelleLoaded searches the given WORKSPACE file for a repository named |
| // "bazel_gazelle". If no such repository is found *and* the repo is not |
| // declared with a directive *and* at least one load statement mentions |
| // the repository, a descriptive error will be returned. |
| // |
| // This should be called after modifications have been made to WORKSPACE |
| // (i.e., after FixLoads) before writing it to disk. |
| func CheckGazelleLoaded(f *bf.File) error { |
| needGazelle := false |
| for _, stmt := range f.Stmt { |
| call, ok := stmt.(*bf.CallExpr) |
| if !ok { |
| continue |
| } |
| x, ok := call.X.(*bf.LiteralExpr) |
| if !ok { |
| continue |
| } |
| if x.Token == "load" { |
| if len(call.List) == 0 { |
| continue |
| } |
| if s, ok := call.List[0].(*bf.StringExpr); ok && strings.HasPrefix(s.Value, "@bazel_gazelle//") { |
| needGazelle = true |
| } |
| continue |
| } |
| rule := bf.Rule{Call: call} |
| if rule.Name() == "bazel_gazelle" { |
| return nil |
| } |
| } |
| if !needGazelle { |
| return nil |
| } |
| for _, d := range config.ParseDirectives(f) { |
| if d.Key != "repo" { |
| continue |
| } |
| if fs := strings.Fields(d.Value); len(fs) > 0 && fs[0] == "bazel_gazelle" { |
| return nil |
| } |
| } |
| return fmt.Errorf(`%s: error: bazel_gazelle is not declared in WORKSPACE. |
| Without this repository, Gazelle cannot safely modify the WORKSPACE file. |
| See the instructions at https://github.com/bazelbuild/bazel-gazelle. |
| If the bazel_gazelle is declared inside a macro, you can suppress this error |
| by adding a comment like this to WORKSPACE: |
| # gazelle:repo bazel_gazelle |
| `, f.Path) |
| } |
| |
| // removeLegacyGoRepository removes loads of go_repository from |
| // @io_bazel_rules_go. FixLoads should be called after this; it will load from |
| // @bazel_gazelle. |
| func removeLegacyGoRepository(f *bf.File) { |
| var deletedStmtIndices []int |
| for stmtIndex, stmt := range f.Stmt { |
| call, ok := stmt.(*bf.CallExpr) |
| if !ok || len(call.List) < 1 { |
| continue |
| } |
| if x, ok := call.X.(*bf.LiteralExpr); !ok || x.Token != "load" { |
| continue |
| } |
| if path, ok := call.List[0].(*bf.StringExpr); !ok || path.Value != "@io_bazel_rules_go//go:def.bzl" { |
| continue |
| } |
| var deletedArgIndices []int |
| for argIndex, arg := range call.List { |
| str, ok := arg.(*bf.StringExpr) |
| if !ok { |
| continue |
| } |
| if str.Value == "go_repository" { |
| deletedArgIndices = append(deletedArgIndices, argIndex) |
| } |
| } |
| if len(call.List)-len(deletedArgIndices) == 1 { |
| deletedStmtIndices = append(deletedStmtIndices, stmtIndex) |
| } else { |
| call.List = deleteIndices(call.List, deletedArgIndices) |
| } |
| } |
| f.Stmt = deleteIndices(f.Stmt, deletedStmtIndices) |
| } |
| |
| // listSquasher builds a sorted, deduplicated list of string expressions. If |
| // a string expression is added multiple times, comments are consolidated. |
| // The original expressions are not modified. |
| type listSquasher struct { |
| unique map[string]*bf.StringExpr |
| seenComments map[elemComment]bool |
| } |
| |
| type elemComment struct { |
| elem, com string |
| } |
| |
| func makeListSquasher() listSquasher { |
| return listSquasher{ |
| unique: make(map[string]*bf.StringExpr), |
| seenComments: make(map[elemComment]bool), |
| } |
| } |
| |
| func (ls *listSquasher) add(s *bf.StringExpr) { |
| sCopy, ok := ls.unique[s.Value] |
| if !ok { |
| // Make a copy of s. We may modify it when we consolidate comments from |
| // duplicate strings. We don't want to modify the original in case this |
| // function fails (due to a later failed pattern match). |
| sCopy = new(bf.StringExpr) |
| *sCopy = *s |
| sCopy.Comments.Before = make([]bf.Comment, 0, len(s.Comments.Before)) |
| sCopy.Comments.Suffix = make([]bf.Comment, 0, len(s.Comments.Suffix)) |
| ls.unique[s.Value] = sCopy |
| } |
| for _, c := range s.Comment().Before { |
| if key := (elemComment{s.Value, c.Token}); !ls.seenComments[key] { |
| sCopy.Comments.Before = append(sCopy.Comments.Before, c) |
| ls.seenComments[key] = true |
| } |
| } |
| for _, c := range s.Comment().Suffix { |
| if key := (elemComment{s.Value, c.Token}); !ls.seenComments[key] { |
| sCopy.Comments.Suffix = append(sCopy.Comments.Suffix, c) |
| ls.seenComments[key] = true |
| } |
| } |
| } |
| |
| func (ls *listSquasher) list() *bf.ListExpr { |
| sortedExprs := make([]bf.Expr, 0, len(ls.unique)) |
| for _, e := range ls.unique { |
| sortedExprs = append(sortedExprs, e) |
| } |
| sort.Slice(sortedExprs, func(i, j int) bool { |
| return sortedExprs[i].(*bf.StringExpr).Value < sortedExprs[j].(*bf.StringExpr).Value |
| }) |
| return &bf.ListExpr{List: sortedExprs} |
| } |
| |
| 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" |
| } |