| /* |
| Copyright 2016 Google LLC |
| |
| 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 |
| |
| https://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" |
| "github.com/bazelbuild/buildtools/labels" |
| ) |
| |
| // 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, "'\"") && !strings.Contains(str.Value, "$(location") { |
| 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(r.Kind(), attr) { |
| continue |
| } |
| for _, li := range AllLists(e) { |
| for _, elem := range li.List { |
| str, ok := elem.(*build.StringExpr) |
| if ok && str.Value != labels.Shorten(str.Value, pkg) { |
| str.Value = labels.Shorten(str.Value, pkg) |
| fixed = true |
| } |
| } |
| } |
| } |
| return fixed |
| } |
| |
| // removeVisibility removes useless visibility attributes. |
| func removeVisibility(f *build.File, r *build.Rule, pkg string) bool { |
| // If no default_visibility is given, it is implicitly private. |
| defaultVisibility := []string{"//visibility:private"} |
| if pkgDecl := ExistingPackageDeclaration(f); pkgDecl != nil { |
| if pkgDecl.Attr("default_visibility") != nil { |
| defaultVisibility = pkgDecl.AttrStrings("default_visibility") |
| } |
| } |
| |
| 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 := ExistingPackageDeclaration(f) |
| |
| def := strings.HasSuffix(r.Kind(), "_test") || r.Kind() == "test_suite" |
| if !def { |
| if pkgDecl == nil || 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.Ident) |
| if !ok { |
| continue |
| } |
| |
| var fix *build.AssignExpr |
| if dot.Name == "extend" { |
| fix = &build.AssignExpr{LHS: obj, Op: "+=", RHS: call.List[0]} |
| } else if dot.Name == "append" { |
| list := &build.ListExpr{List: []build.Expr{call.List[0]}} |
| fix = &build.AssignExpr{LHS: obj, Op: "+=", RHS: list} |
| } else { |
| continue |
| } |
| fix.Comments = call.Comments // Keep original comments |
| f.Stmt[i] = fix |
| fixed = true |
| } |
| return fixed |
| } |
| |
| // 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 { |
| symbols := UsedSymbols(f) |
| fixed := false |
| |
| // Map of symbol in this file -> modules it's loaded from |
| symbolsToModules := make(map[string][]string) |
| |
| var all []build.Expr |
| for _, stmt := range f.Stmt { |
| load, ok := stmt.(*build.LoadStmt) |
| if !ok || ContainsComments(load, "@unused") { |
| all = append(all, stmt) |
| continue |
| } |
| var fromSymbols, toSymbols []*build.Ident |
| for i := range load.From { |
| fromSymbol := load.From[i] |
| toSymbol := load.To[i] |
| if symbols[toSymbol.Name] { |
| // The symbol is actually used |
| |
| // If the most recent load for this symbol was from the same file, remove it. |
| previousModules := symbolsToModules[toSymbol.Name] |
| if len(previousModules) > 0 { |
| if previousModules[len(previousModules)-1] == load.Module.Value { |
| fixed = true |
| continue |
| } |
| } |
| symbolsToModules[toSymbol.Name] = append(symbolsToModules[toSymbol.Name], load.Module.Value) |
| |
| fromSymbols = append(fromSymbols, fromSymbol) |
| toSymbols = append(toSymbols, toSymbol) |
| } else { |
| fixed = true |
| } |
| } |
| if len(toSymbols) > 0 { // Keep the load statement if it loads at least one symbol. |
| sort.Sort(loadArgs{fromSymbols, toSymbols}) |
| load.From = fromSymbols |
| load.To = toSymbols |
| all = append(all, load) |
| } else { |
| fixed = true |
| // If the load statement contains before- or after-comments, |
| // keep them by re-attaching to a new CommentBlock node. |
| if len(load.Comment().Before) == 0 && len(load.Comment().After) == 0 { |
| continue |
| } |
| cb := &build.CommentBlock{} |
| cb.Comment().After = load.Comment().Before |
| cb.Comment().After = append(cb.Comment().After, load.Comment().After...) |
| all = append(all, cb) |
| } |
| } |
| f.Stmt = all |
| return fixed |
| } |
| |
| // 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) |
| _, isString := stmt.(*build.StringExpr) // typically a docstring |
| _, isAssignExpr := stmt.(*build.AssignExpr) // e.g. variable declaration |
| _, isLoad := stmt.(*build.LoadStmt) |
| if isComment || isString || isAssignExpr || 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 auxiliary function used by moveLicenses. |
| // 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 |
| } |
| |
| // moveLicenses replaces the 'licenses' function with an attribute |
| // in package. |
| // Before: licenses(["notice"]) |
| // After: package(licenses = ["notice"]) |
| func moveLicenses(f *build.File) bool { |
| return moveToPackage(f, "licenses") |
| } |
| |
| // 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 explicitly"}, |
| {"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"}, |
| {"moveLicenses", moveLicenses, |
| "Move licenses 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 |
| } |
| |
| // A wrapper for a LoadStmt's From and To slices for consistent sorting of their contents. |
| // It's assumed that the following slices have the same length, the contents are sorted by |
| // the `To` attribute, the items of `From` are swapped exactly the same way as the items of `To`. |
| type loadArgs struct { |
| From []*build.Ident |
| To []*build.Ident |
| } |
| |
| func (args loadArgs) Len() int { |
| return len(args.From) |
| } |
| |
| func (args loadArgs) Swap(i, j int) { |
| args.From[i], args.From[j] = args.From[j], args.From[i] |
| args.To[i], args.To[j] = args.To[j], args.To[i] |
| } |
| |
| func (args loadArgs) Less(i, j int) bool { |
| return args.To[i].Name < args.To[j].Name |
| } |