123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- /*
- Copyright 2016 Google Inc. 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.
- */
- // Functions to clean and fix BUILD files
- package edit
- import (
- "regexp"
- "sort"
- "strings"
- "github.com/bazelbuild/buildtools/build"
- )
- // splitOptionsWithSpaces is a cleanup function.
- // It splits options strings that contain a space. This change
- // should be safe as Blaze is splitting those strings, but we will
- // eventually get rid of this misfeature.
- // eg. it converts from:
- // copts = ["-Dfoo -Dbar"]
- // to:
- // copts = ["-Dfoo", "-Dbar"]
- func splitOptionsWithSpaces(_ *build.File, r *build.Rule, _ string) bool {
- var attrToRewrite = []string{
- "copts",
- "linkopts",
- }
- fixed := false
- for _, attrName := range attrToRewrite {
- attr := r.Attr(attrName)
- if attr != nil {
- for _, li := range AllLists(attr) {
- fixed = splitStrings(li) || fixed
- }
- }
- }
- return fixed
- }
- func splitStrings(list *build.ListExpr) bool {
- var all []build.Expr
- fixed := false
- for _, e := range list.List {
- str, ok := e.(*build.StringExpr)
- if !ok {
- all = append(all, e)
- continue
- }
- if strings.Contains(str.Value, " ") && !strings.Contains(str.Value, "'\"") {
- fixed = true
- for i, substr := range strings.Fields(str.Value) {
- item := &build.StringExpr{Value: substr}
- if i == 0 {
- item.Comments = str.Comments
- }
- all = append(all, item)
- }
- } else {
- all = append(all, str)
- }
- }
- list.List = all
- return fixed
- }
- // shortenLabels rewrites the labels in the rule using the short notation.
- func shortenLabels(_ *build.File, r *build.Rule, pkg string) bool {
- fixed := false
- for _, attr := range r.AttrKeys() {
- e := r.Attr(attr)
- if !ContainsLabels(attr) {
- continue
- }
- for _, li := range AllLists(e) {
- for _, elem := range li.List {
- str, ok := elem.(*build.StringExpr)
- if ok && str.Value != ShortenLabel(str.Value, pkg) {
- str.Value = ShortenLabel(str.Value, pkg)
- fixed = true
- }
- }
- }
- }
- return fixed
- }
- // removeVisibility removes useless visibility attributes.
- func removeVisibility(f *build.File, r *build.Rule, pkg string) bool {
- pkgDecl := PackageDeclaration(f)
- defaultVisibility := pkgDecl.AttrStrings("default_visibility")
- // If no default_visibility is given, it is implicitly private.
- if len(defaultVisibility) == 0 {
- defaultVisibility = []string{"//visibility:private"}
- }
- visibility := r.AttrStrings("visibility")
- if len(visibility) == 0 || len(visibility) != len(defaultVisibility) {
- return false
- }
- sort.Strings(defaultVisibility)
- sort.Strings(visibility)
- for i, vis := range visibility {
- if vis != defaultVisibility[i] {
- return false
- }
- }
- r.DelAttr("visibility")
- return true
- }
- // removeTestOnly removes the useless testonly attributes.
- func removeTestOnly(f *build.File, r *build.Rule, pkg string) bool {
- pkgDecl := PackageDeclaration(f)
- def := strings.HasSuffix(r.Kind(), "_test") || r.Kind() == "test_suite"
- if !def {
- if pkgDecl.Attr("default_testonly") == nil {
- def = strings.HasPrefix(pkg, "javatests/")
- } else if pkgDecl.AttrLiteral("default_testonly") == "1" {
- def = true
- } else if pkgDecl.AttrLiteral("default_testonly") != "0" {
- // Non-literal value: it's not safe to do a change.
- return false
- }
- }
- testonly := r.AttrLiteral("testonly")
- if def && testonly == "1" {
- r.DelAttr("testonly")
- return true
- }
- if !def && testonly == "0" {
- r.DelAttr("testonly")
- return true
- }
- return false
- }
- func genruleRenameDepsTools(_ *build.File, r *build.Rule, _ string) bool {
- return r.Kind() == "genrule" && RenameAttribute(r, "deps", "tools") == nil
- }
- // explicitHeuristicLabels adds $(location ...) for each label in the string s.
- func explicitHeuristicLabels(s string, labels map[string]bool) string {
- // Regexp comes from LABEL_CHAR_MATCHER in
- // java/com/google/devtools/build/lib/analysis/LabelExpander.java
- re := regexp.MustCompile("[a-zA-Z0-9:/_.+-]+|[^a-zA-Z0-9:/_.+-]+")
- parts := re.FindAllString(s, -1)
- changed := false
- canChange := true
- for i, part := range parts {
- // We don't want to add $(location when it's already present.
- // So we skip the next label when we see location(s).
- if part == "location" || part == "locations" {
- canChange = false
- }
- if !labels[part] {
- if labels[":"+part] { // leading colon is often missing
- part = ":" + part
- } else {
- continue
- }
- }
- if !canChange {
- canChange = true
- continue
- }
- parts[i] = "$(location " + part + ")"
- changed = true
- }
- if changed {
- return strings.Join(parts, "")
- }
- return s
- }
- func addLabels(r *build.Rule, attr string, labels map[string]bool) {
- a := r.Attr(attr)
- if a == nil {
- return
- }
- for _, li := range AllLists(a) {
- for _, item := range li.List {
- if str, ok := item.(*build.StringExpr); ok {
- labels[str.Value] = true
- }
- }
- }
- }
- // genruleFixHeuristicLabels modifies the cmd attribute of genrules, so
- // that they don't rely on heuristic label expansion anymore.
- // Label expansion is made explicit with the $(location ...) command.
- func genruleFixHeuristicLabels(_ *build.File, r *build.Rule, _ string) bool {
- if r.Kind() != "genrule" {
- return false
- }
- cmd := r.Attr("cmd")
- if cmd == nil {
- return false
- }
- labels := make(map[string]bool)
- addLabels(r, "tools", labels)
- addLabels(r, "srcs", labels)
- fixed := false
- for _, str := range AllStrings(cmd) {
- newVal := explicitHeuristicLabels(str.Value, labels)
- if newVal != str.Value {
- fixed = true
- str.Value = newVal
- }
- }
- return fixed
- }
- // sortExportsFiles sorts the first argument of exports_files if it is a list.
- func sortExportsFiles(_ *build.File, r *build.Rule, _ string) bool {
- if r.Kind() != "exports_files" || len(r.Call.List) == 0 {
- return false
- }
- build.SortStringList(r.Call.List[0])
- return true
- }
- // removeVarref replaces all varref('x') with '$(x)'.
- // The goal is to eventually remove varref from the build language.
- func removeVarref(_ *build.File, r *build.Rule, _ string) bool {
- fixed := false
- EditFunction(r.Call, "varref", func(call *build.CallExpr, stk []build.Expr) build.Expr {
- if len(call.List) != 1 {
- return nil
- }
- str, ok := (call.List[0]).(*build.StringExpr)
- if !ok {
- return nil
- }
- fixed = true
- str.Value = "$(" + str.Value + ")"
- // Preserve suffix comments from the function call
- str.Comment().Suffix = append(str.Comment().Suffix, call.Comment().Suffix...)
- return str
- })
- return fixed
- }
- // sortGlob sorts the list argument to glob.
- func sortGlob(_ *build.File, r *build.Rule, _ string) bool {
- fixed := false
- EditFunction(r.Call, "glob", func(call *build.CallExpr, stk []build.Expr) build.Expr {
- if len(call.List) == 0 {
- return nil
- }
- build.SortStringList(call.List[0])
- fixed = true
- return call
- })
- return fixed
- }
- func evaluateListConcatenation(expr build.Expr) build.Expr {
- if _, ok := expr.(*build.ListExpr); ok {
- return expr
- }
- bin, ok := expr.(*build.BinaryExpr)
- if !ok || bin.Op != "+" {
- return expr
- }
- li1, ok1 := evaluateListConcatenation(bin.X).(*build.ListExpr)
- li2, ok2 := evaluateListConcatenation(bin.Y).(*build.ListExpr)
- if !ok1 || !ok2 {
- return expr
- }
- res := *li1
- res.List = append(li1.List, li2.List...)
- return &res
- }
- // mergeLiteralLists evaluates the concatenation of two literal lists.
- // e.g. [1, 2] + [3, 4] -> [1, 2, 3, 4]
- func mergeLiteralLists(_ *build.File, r *build.Rule, _ string) bool {
- fixed := false
- build.Edit(r.Call, func(expr build.Expr, stk []build.Expr) build.Expr {
- newexpr := evaluateListConcatenation(expr)
- fixed = fixed || (newexpr != expr)
- return newexpr
- })
- return fixed
- }
- // usePlusEqual replaces uses of extend and append with the += operator.
- // e.g. foo.extend(bar) => foo += bar
- // foo.append(bar) => foo += [bar]
- func usePlusEqual(f *build.File) bool {
- fixed := false
- for i, stmt := range f.Stmt {
- call, ok := stmt.(*build.CallExpr)
- if !ok {
- continue
- }
- dot, ok := call.X.(*build.DotExpr)
- if !ok || len(call.List) != 1 {
- continue
- }
- obj, ok := dot.X.(*build.LiteralExpr)
- if !ok {
- continue
- }
- var fix *build.BinaryExpr
- if dot.Name == "extend" {
- fix = &build.BinaryExpr{X: obj, Op: "+=", Y: call.List[0]}
- } else if dot.Name == "append" {
- list := &build.ListExpr{List: []build.Expr{call.List[0]}}
- fix = &build.BinaryExpr{X: obj, Op: "+=", Y: list}
- } else {
- continue
- }
- fix.Comments = call.Comments // Keep original comments
- f.Stmt[i] = fix
- fixed = true
- }
- return fixed
- }
- func isNonemptyComment(comment *build.Comments) bool {
- return len(comment.Before)+len(comment.Suffix)+len(comment.After) > 0
- }
- // Checks whether a call or any of its arguments have a comment
- func hasComment(call *build.CallExpr) bool {
- if isNonemptyComment(call.Comment()) {
- return true
- }
- for _, arg := range call.List {
- if isNonemptyComment(arg.Comment()) {
- return true
- }
- }
- return false
- }
- // cleanUnusedLoads removes symbols from load statements that are not used in the file.
- // It also cleans symbols loaded multiple times, sorts symbol list, and removes load
- // statements when the list is empty.
- func cleanUnusedLoads(f *build.File) bool {
- // If the file needs preprocessing, leave it alone.
- for _, stmt := range f.Stmt {
- if _, ok := stmt.(*build.PythonBlock); ok {
- return false
- }
- }
- symbols := UsedSymbols(f)
- fixed := false
- var all []build.Expr
- for _, stmt := range f.Stmt {
- rule, ok := ExprToRule(stmt, "load")
- if !ok || len(rule.Call.List) == 0 || hasComment(rule.Call) {
- all = append(all, stmt)
- continue
- }
- var args []build.Expr
- for _, arg := range rule.Call.List[1:] { // first argument is the path, we keep it
- symbol, ok := loadedSymbol(arg)
- if !ok || symbols[symbol] {
- args = append(args, arg)
- if ok {
- // If the same symbol is loaded twice, we'll remove it.
- delete(symbols, symbol)
- }
- } else {
- fixed = true
- }
- }
- if len(args) > 0 { // Keep the load statement if it loads at least one symbol.
- li := &build.ListExpr{List: args}
- build.SortStringList(li)
- rule.Call.List = append(rule.Call.List[:1], li.List...)
- all = append(all, rule.Call)
- } else {
- fixed = true
- }
- }
- f.Stmt = all
- return fixed
- }
- // loadedSymbol parses the symbol token from a load statement argument,
- // supporting aliases.
- func loadedSymbol(arg build.Expr) (string, bool) {
- symbol, ok := arg.(*build.StringExpr)
- if ok {
- return symbol.Value, ok
- }
- // try an aliased symbol
- if binExpr, ok := arg.(*build.BinaryExpr); ok && binExpr.Op == "=" {
- if keyExpr, ok := binExpr.X.(*build.LiteralExpr); ok {
- return keyExpr.Token, ok
- }
- }
- return "", false
- }
- // movePackageDeclarationToTheTop ensures that the call to package() is done
- // before everything else (except comments).
- func movePackageDeclarationToTheTop(f *build.File) bool {
- pkg := ExistingPackageDeclaration(f)
- if pkg == nil {
- return false
- }
- all := []build.Expr{}
- inserted := false // true when the package declaration has been inserted
- for _, stmt := range f.Stmt {
- _, isComment := stmt.(*build.CommentBlock)
- _, isBinaryExpr := stmt.(*build.BinaryExpr) // e.g. variable declaration
- _, isLoad := ExprToRule(stmt, "load")
- if isComment || isBinaryExpr || isLoad {
- all = append(all, stmt)
- continue
- }
- if stmt == pkg.Call {
- if inserted {
- // remove the old package
- continue
- }
- return false // the file was ok
- }
- if !inserted {
- all = append(all, pkg.Call)
- inserted = true
- }
- all = append(all, stmt)
- }
- f.Stmt = all
- return true
- }
- // moveToPackage is an auxilliary function used by moveLicensesAndDistribs.
- // The function shouldn't appear more than once in the file (depot cleanup has
- // been done).
- func moveToPackage(f *build.File, attrname string) bool {
- var all []build.Expr
- fixed := false
- for _, stmt := range f.Stmt {
- rule, ok := ExprToRule(stmt, attrname)
- if !ok || len(rule.Call.List) != 1 {
- all = append(all, stmt)
- continue
- }
- pkgDecl := PackageDeclaration(f)
- pkgDecl.SetAttr(attrname, rule.Call.List[0])
- pkgDecl.AttrDefn(attrname).Comments = *stmt.Comment()
- fixed = true
- }
- f.Stmt = all
- return fixed
- }
- // moveLicensesAndDistribs replaces the 'licenses' and 'distribs' functions
- // with an attribute in package.
- // Before: licenses(["notice"])
- // After: package(licenses = ["notice"])
- func moveLicensesAndDistribs(f *build.File) bool {
- fixed1 := moveToPackage(f, "licenses")
- fixed2 := moveToPackage(f, "distribs")
- return fixed1 || fixed2
- }
- // AllRuleFixes is a list of all Buildozer fixes that can be applied on a rule.
- var AllRuleFixes = []struct {
- Name string
- Fn func(file *build.File, rule *build.Rule, pkg string) bool
- Message string
- }{
- {"sortGlob", sortGlob,
- "Sort the list in a call to glob"},
- {"splitOptions", splitOptionsWithSpaces,
- "Each option should be given separately in the list"},
- {"shortenLabels", shortenLabels,
- "Style: Use the canonical label notation"},
- {"removeVisibility", removeVisibility,
- "This visibility attribute is useless (it corresponds to the default value)"},
- {"removeTestOnly", removeTestOnly,
- "This testonly attribute is useless (it corresponds to the default value)"},
- {"genruleRenameDepsTools", genruleRenameDepsTools,
- "'deps' attribute in genrule has been renamed 'tools'"},
- {"genruleFixHeuristicLabels", genruleFixHeuristicLabels,
- "$(location) should be called explicitely"},
- {"sortExportsFiles", sortExportsFiles,
- "Files in exports_files should be sorted"},
- {"varref", removeVarref,
- "All varref('foo') should be replaced with '$foo'"},
- {"mergeLiteralLists", mergeLiteralLists,
- "Remove useless list concatenation"},
- }
- // FileLevelFixes is a list of all Buildozer fixes that apply on the whole file.
- var FileLevelFixes = []struct {
- Name string
- Fn func(file *build.File) bool
- Message string
- }{
- {"movePackageToTop", movePackageDeclarationToTheTop,
- "The package declaration should be the first rule in a file"},
- {"usePlusEqual", usePlusEqual,
- "Prefer '+=' over 'extend' or 'append'"},
- {"unusedLoads", cleanUnusedLoads,
- "Remove unused symbols from load statements"},
- {"moveLicensesAndDistribs", moveLicensesAndDistribs,
- "Move licenses and distribs to the package function"},
- }
- // FixRule aims to fix errors in BUILD files, remove deprecated features, and
- // simplify the code.
- func FixRule(f *build.File, pkg string, rule *build.Rule, fixes []string) *build.File {
- fixesAsMap := make(map[string]bool)
- for _, fix := range fixes {
- fixesAsMap[fix] = true
- }
- fixed := false
- for _, fix := range AllRuleFixes {
- if len(fixes) == 0 || fixesAsMap[fix.Name] {
- fixed = fix.Fn(f, rule, pkg) || fixed
- }
- }
- if !fixed {
- return nil
- }
- return f
- }
- // FixFile fixes everything it can in the BUILD file.
- func FixFile(f *build.File, pkg string, fixes []string) *build.File {
- fixesAsMap := make(map[string]bool)
- for _, fix := range fixes {
- fixesAsMap[fix] = true
- }
- fixed := false
- for _, rule := range f.Rules("") {
- res := FixRule(f, pkg, rule, fixes)
- if res != nil {
- fixed = true
- f = res
- }
- }
- for _, fix := range FileLevelFixes {
- if len(fixes) == 0 || fixesAsMap[fix.Name] {
- fixed = fix.Fn(f) || fixed
- }
- }
- if !fixed {
- return nil
- }
- return f
- }
|