github.com/cheshirekow/buildtools@v0.0.0-20200224190056-5d637702fe81/edit/fix.go (about) 1 /* 2 Copyright 2016 Google Inc. All Rights Reserved. 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 http://www.apache.org/licenses/LICENSE-2.0 7 Unless required by applicable law or agreed to in writing, software 8 distributed under the License is distributed on an "AS IS" BASIS, 9 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 See the License for the specific language governing permissions and 11 limitations under the License. 12 */ 13 // Functions to clean and fix BUILD files 14 15 package edit 16 17 import ( 18 "regexp" 19 "sort" 20 "strings" 21 22 "github.com/cheshirekow/buildtools/build" 23 ) 24 25 // splitOptionsWithSpaces is a cleanup function. 26 // It splits options strings that contain a space. This change 27 // should be safe as Blaze is splitting those strings, but we will 28 // eventually get rid of this misfeature. 29 // eg. it converts from: 30 // copts = ["-Dfoo -Dbar"] 31 // to: 32 // copts = ["-Dfoo", "-Dbar"] 33 func splitOptionsWithSpaces(_ *build.File, r *build.Rule, _ string) bool { 34 var attrToRewrite = []string{ 35 "copts", 36 "linkopts", 37 } 38 fixed := false 39 for _, attrName := range attrToRewrite { 40 attr := r.Attr(attrName) 41 if attr != nil { 42 for _, li := range AllLists(attr) { 43 fixed = splitStrings(li) || fixed 44 } 45 } 46 } 47 return fixed 48 } 49 50 func splitStrings(list *build.ListExpr) bool { 51 var all []build.Expr 52 fixed := false 53 for _, e := range list.List { 54 str, ok := e.(*build.StringExpr) 55 if !ok { 56 all = append(all, e) 57 continue 58 } 59 if strings.Contains(str.Value, " ") && !strings.Contains(str.Value, "'\"") { 60 fixed = true 61 for i, substr := range strings.Fields(str.Value) { 62 item := &build.StringExpr{Value: substr} 63 if i == 0 { 64 item.Comments = str.Comments 65 } 66 all = append(all, item) 67 } 68 } else { 69 all = append(all, str) 70 } 71 } 72 list.List = all 73 return fixed 74 } 75 76 // shortenLabels rewrites the labels in the rule using the short notation. 77 func shortenLabels(_ *build.File, r *build.Rule, pkg string) bool { 78 fixed := false 79 for _, attr := range r.AttrKeys() { 80 e := r.Attr(attr) 81 if !ContainsLabels(attr) { 82 continue 83 } 84 for _, li := range AllLists(e) { 85 for _, elem := range li.List { 86 str, ok := elem.(*build.StringExpr) 87 if ok && str.Value != ShortenLabel(str.Value, pkg) { 88 str.Value = ShortenLabel(str.Value, pkg) 89 fixed = true 90 } 91 } 92 } 93 } 94 return fixed 95 } 96 97 // removeVisibility removes useless visibility attributes. 98 func removeVisibility(f *build.File, r *build.Rule, pkg string) bool { 99 pkgDecl := PackageDeclaration(f) 100 defaultVisibility := pkgDecl.AttrStrings("default_visibility") 101 102 // If no default_visibility is given, it is implicitly private. 103 if len(defaultVisibility) == 0 { 104 defaultVisibility = []string{"//visibility:private"} 105 } 106 107 visibility := r.AttrStrings("visibility") 108 if len(visibility) == 0 || len(visibility) != len(defaultVisibility) { 109 return false 110 } 111 sort.Strings(defaultVisibility) 112 sort.Strings(visibility) 113 for i, vis := range visibility { 114 if vis != defaultVisibility[i] { 115 return false 116 } 117 } 118 r.DelAttr("visibility") 119 return true 120 } 121 122 // removeTestOnly removes the useless testonly attributes. 123 func removeTestOnly(f *build.File, r *build.Rule, pkg string) bool { 124 pkgDecl := PackageDeclaration(f) 125 126 def := strings.HasSuffix(r.Kind(), "_test") || r.Kind() == "test_suite" 127 if !def { 128 if pkgDecl.Attr("default_testonly") == nil { 129 def = strings.HasPrefix(pkg, "javatests/") 130 } else if pkgDecl.AttrLiteral("default_testonly") == "1" { 131 def = true 132 } else if pkgDecl.AttrLiteral("default_testonly") != "0" { 133 // Non-literal value: it's not safe to do a change. 134 return false 135 } 136 } 137 138 testonly := r.AttrLiteral("testonly") 139 if def && testonly == "1" { 140 r.DelAttr("testonly") 141 return true 142 } 143 if !def && testonly == "0" { 144 r.DelAttr("testonly") 145 return true 146 } 147 return false 148 } 149 150 func genruleRenameDepsTools(_ *build.File, r *build.Rule, _ string) bool { 151 return r.Kind() == "genrule" && RenameAttribute(r, "deps", "tools") == nil 152 } 153 154 // explicitHeuristicLabels adds $(location ...) for each label in the string s. 155 func explicitHeuristicLabels(s string, labels map[string]bool) string { 156 // Regexp comes from LABEL_CHAR_MATCHER in 157 // java/com/google/devtools/build/lib/analysis/LabelExpander.java 158 re := regexp.MustCompile("[a-zA-Z0-9:/_.+-]+|[^a-zA-Z0-9:/_.+-]+") 159 parts := re.FindAllString(s, -1) 160 changed := false 161 canChange := true 162 for i, part := range parts { 163 // We don't want to add $(location when it's already present. 164 // So we skip the next label when we see location(s). 165 if part == "location" || part == "locations" { 166 canChange = false 167 } 168 if !labels[part] { 169 if labels[":"+part] { // leading colon is often missing 170 part = ":" + part 171 } else { 172 continue 173 } 174 } 175 176 if !canChange { 177 canChange = true 178 continue 179 } 180 parts[i] = "$(location " + part + ")" 181 changed = true 182 } 183 if changed { 184 return strings.Join(parts, "") 185 } 186 return s 187 } 188 189 func addLabels(r *build.Rule, attr string, labels map[string]bool) { 190 a := r.Attr(attr) 191 if a == nil { 192 return 193 } 194 for _, li := range AllLists(a) { 195 for _, item := range li.List { 196 if str, ok := item.(*build.StringExpr); ok { 197 labels[str.Value] = true 198 } 199 } 200 } 201 } 202 203 // genruleFixHeuristicLabels modifies the cmd attribute of genrules, so 204 // that they don't rely on heuristic label expansion anymore. 205 // Label expansion is made explicit with the $(location ...) command. 206 func genruleFixHeuristicLabels(_ *build.File, r *build.Rule, _ string) bool { 207 if r.Kind() != "genrule" { 208 return false 209 } 210 211 cmd := r.Attr("cmd") 212 if cmd == nil { 213 return false 214 } 215 labels := make(map[string]bool) 216 addLabels(r, "tools", labels) 217 addLabels(r, "srcs", labels) 218 219 fixed := false 220 for _, str := range AllStrings(cmd) { 221 newVal := explicitHeuristicLabels(str.Value, labels) 222 if newVal != str.Value { 223 fixed = true 224 str.Value = newVal 225 } 226 } 227 return fixed 228 } 229 230 // sortExportsFiles sorts the first argument of exports_files if it is a list. 231 func sortExportsFiles(_ *build.File, r *build.Rule, _ string) bool { 232 if r.Kind() != "exports_files" || len(r.Call.List) == 0 { 233 return false 234 } 235 build.SortStringList(r.Call.List[0]) 236 return true 237 } 238 239 // removeVarref replaces all varref('x') with '$(x)'. 240 // The goal is to eventually remove varref from the build language. 241 func removeVarref(_ *build.File, r *build.Rule, _ string) bool { 242 fixed := false 243 EditFunction(r.Call, "varref", func(call *build.CallExpr, stk []build.Expr) build.Expr { 244 if len(call.List) != 1 { 245 return nil 246 } 247 str, ok := (call.List[0]).(*build.StringExpr) 248 if !ok { 249 return nil 250 } 251 fixed = true 252 str.Value = "$(" + str.Value + ")" 253 // Preserve suffix comments from the function call 254 str.Comment().Suffix = append(str.Comment().Suffix, call.Comment().Suffix...) 255 return str 256 }) 257 return fixed 258 } 259 260 // sortGlob sorts the list argument to glob. 261 func sortGlob(_ *build.File, r *build.Rule, _ string) bool { 262 fixed := false 263 EditFunction(r.Call, "glob", func(call *build.CallExpr, stk []build.Expr) build.Expr { 264 if len(call.List) == 0 { 265 return nil 266 } 267 build.SortStringList(call.List[0]) 268 fixed = true 269 return call 270 }) 271 return fixed 272 } 273 274 func evaluateListConcatenation(expr build.Expr) build.Expr { 275 if _, ok := expr.(*build.ListExpr); ok { 276 return expr 277 } 278 bin, ok := expr.(*build.BinaryExpr) 279 if !ok || bin.Op != "+" { 280 return expr 281 } 282 li1, ok1 := evaluateListConcatenation(bin.X).(*build.ListExpr) 283 li2, ok2 := evaluateListConcatenation(bin.Y).(*build.ListExpr) 284 if !ok1 || !ok2 { 285 return expr 286 } 287 res := *li1 288 res.List = append(li1.List, li2.List...) 289 return &res 290 } 291 292 // mergeLiteralLists evaluates the concatenation of two literal lists. 293 // e.g. [1, 2] + [3, 4] -> [1, 2, 3, 4] 294 func mergeLiteralLists(_ *build.File, r *build.Rule, _ string) bool { 295 fixed := false 296 build.Edit(r.Call, func(expr build.Expr, stk []build.Expr) build.Expr { 297 newexpr := evaluateListConcatenation(expr) 298 fixed = fixed || (newexpr != expr) 299 return newexpr 300 }) 301 return fixed 302 } 303 304 // usePlusEqual replaces uses of extend and append with the += operator. 305 // e.g. foo.extend(bar) => foo += bar 306 // foo.append(bar) => foo += [bar] 307 func usePlusEqual(f *build.File) bool { 308 fixed := false 309 for i, stmt := range f.Stmt { 310 call, ok := stmt.(*build.CallExpr) 311 if !ok { 312 continue 313 } 314 dot, ok := call.X.(*build.DotExpr) 315 if !ok || len(call.List) != 1 { 316 continue 317 } 318 obj, ok := dot.X.(*build.Ident) 319 if !ok { 320 continue 321 } 322 323 var fix *build.AssignExpr 324 if dot.Name == "extend" { 325 fix = &build.AssignExpr{LHS: obj, Op: "+=", RHS: call.List[0]} 326 } else if dot.Name == "append" { 327 list := &build.ListExpr{List: []build.Expr{call.List[0]}} 328 fix = &build.AssignExpr{LHS: obj, Op: "+=", RHS: list} 329 } else { 330 continue 331 } 332 fix.Comments = call.Comments // Keep original comments 333 f.Stmt[i] = fix 334 fixed = true 335 } 336 return fixed 337 } 338 339 func isNonemptyComment(comment *build.Comments) bool { 340 return len(comment.Before)+len(comment.Suffix)+len(comment.After) > 0 341 } 342 343 // Checks whether a load statement or any of its arguments have a comment 344 func hasComment(load *build.LoadStmt) bool { 345 if isNonemptyComment(load.Comment()) { 346 return true 347 } 348 if isNonemptyComment(load.Module.Comment()) { 349 return true 350 } 351 352 for i := range load.From { 353 if isNonemptyComment(load.From[i].Comment()) || isNonemptyComment(load.To[i].Comment()) { 354 return true 355 } 356 } 357 return false 358 } 359 360 // cleanUnusedLoads removes symbols from load statements that are not used in the file. 361 // It also cleans symbols loaded multiple times, sorts symbol list, and removes load 362 // statements when the list is empty. 363 func cleanUnusedLoads(f *build.File) bool { 364 symbols := UsedSymbols(f) 365 fixed := false 366 367 var all []build.Expr 368 for _, stmt := range f.Stmt { 369 load, ok := stmt.(*build.LoadStmt) 370 if !ok || hasComment(load) { 371 all = append(all, stmt) 372 continue 373 } 374 var fromSymbols, toSymbols []*build.Ident 375 for i := range load.From { 376 fromSymbol := load.From[i] 377 toSymbol := load.To[i] 378 if symbols[toSymbol.Name] { 379 // The symbol is actually used 380 fromSymbols = append(fromSymbols, fromSymbol) 381 toSymbols = append(toSymbols, toSymbol) 382 // If the same symbol is loaded twice, we'll remove it. 383 delete(symbols, toSymbol.Name) 384 } else { 385 fixed = true 386 } 387 } 388 if len(toSymbols) > 0 { // Keep the load statement if it loads at least one symbol. 389 sort.Sort(loadArgs{fromSymbols, toSymbols}) 390 load.From = fromSymbols 391 load.To = toSymbols 392 all = append(all, load) 393 } else { 394 fixed = true 395 } 396 } 397 f.Stmt = all 398 return fixed 399 } 400 401 // movePackageDeclarationToTheTop ensures that the call to package() is done 402 // before everything else (except comments). 403 func movePackageDeclarationToTheTop(f *build.File) bool { 404 pkg := ExistingPackageDeclaration(f) 405 if pkg == nil { 406 return false 407 } 408 all := []build.Expr{} 409 inserted := false // true when the package declaration has been inserted 410 for _, stmt := range f.Stmt { 411 _, isComment := stmt.(*build.CommentBlock) 412 _, isString := stmt.(*build.StringExpr) // typically a docstring 413 _, isAssignExpr := stmt.(*build.AssignExpr) // e.g. variable declaration 414 _, isLoad := stmt.(*build.LoadStmt) 415 if isComment || isString || isAssignExpr || isLoad { 416 all = append(all, stmt) 417 continue 418 } 419 if stmt == pkg.Call { 420 if inserted { 421 // remove the old package 422 continue 423 } 424 return false // the file was ok 425 } 426 if !inserted { 427 all = append(all, pkg.Call) 428 inserted = true 429 } 430 all = append(all, stmt) 431 } 432 f.Stmt = all 433 return true 434 } 435 436 // moveToPackage is an auxilliary function used by moveLicensesAndDistribs. 437 // The function shouldn't appear more than once in the file (depot cleanup has 438 // been done). 439 func moveToPackage(f *build.File, attrname string) bool { 440 var all []build.Expr 441 fixed := false 442 for _, stmt := range f.Stmt { 443 rule, ok := ExprToRule(stmt, attrname) 444 if !ok || len(rule.Call.List) != 1 { 445 all = append(all, stmt) 446 continue 447 } 448 pkgDecl := PackageDeclaration(f) 449 pkgDecl.SetAttr(attrname, rule.Call.List[0]) 450 pkgDecl.AttrDefn(attrname).Comments = *stmt.Comment() 451 fixed = true 452 } 453 f.Stmt = all 454 return fixed 455 } 456 457 // moveLicensesAndDistribs replaces the 'licenses' and 'distribs' functions 458 // with an attribute in package. 459 // Before: licenses(["notice"]) 460 // After: package(licenses = ["notice"]) 461 func moveLicensesAndDistribs(f *build.File) bool { 462 fixed1 := moveToPackage(f, "licenses") 463 fixed2 := moveToPackage(f, "distribs") 464 return fixed1 || fixed2 465 } 466 467 // AllRuleFixes is a list of all Buildozer fixes that can be applied on a rule. 468 var AllRuleFixes = []struct { 469 Name string 470 Fn func(file *build.File, rule *build.Rule, pkg string) bool 471 Message string 472 }{ 473 {"sortGlob", sortGlob, 474 "Sort the list in a call to glob"}, 475 {"splitOptions", splitOptionsWithSpaces, 476 "Each option should be given separately in the list"}, 477 {"shortenLabels", shortenLabels, 478 "Style: Use the canonical label notation"}, 479 {"removeVisibility", removeVisibility, 480 "This visibility attribute is useless (it corresponds to the default value)"}, 481 {"removeTestOnly", removeTestOnly, 482 "This testonly attribute is useless (it corresponds to the default value)"}, 483 {"genruleRenameDepsTools", genruleRenameDepsTools, 484 "'deps' attribute in genrule has been renamed 'tools'"}, 485 {"genruleFixHeuristicLabels", genruleFixHeuristicLabels, 486 "$(location) should be called explicitely"}, 487 {"sortExportsFiles", sortExportsFiles, 488 "Files in exports_files should be sorted"}, 489 {"varref", removeVarref, 490 "All varref('foo') should be replaced with '$foo'"}, 491 {"mergeLiteralLists", mergeLiteralLists, 492 "Remove useless list concatenation"}, 493 } 494 495 // FileLevelFixes is a list of all Buildozer fixes that apply on the whole file. 496 var FileLevelFixes = []struct { 497 Name string 498 Fn func(file *build.File) bool 499 Message string 500 }{ 501 {"movePackageToTop", movePackageDeclarationToTheTop, 502 "The package declaration should be the first rule in a file"}, 503 {"usePlusEqual", usePlusEqual, 504 "Prefer '+=' over 'extend' or 'append'"}, 505 {"unusedLoads", cleanUnusedLoads, 506 "Remove unused symbols from load statements"}, 507 {"moveLicensesAndDistribs", moveLicensesAndDistribs, 508 "Move licenses and distribs to the package function"}, 509 } 510 511 // FixRule aims to fix errors in BUILD files, remove deprecated features, and 512 // simplify the code. 513 func FixRule(f *build.File, pkg string, rule *build.Rule, fixes []string) *build.File { 514 fixesAsMap := make(map[string]bool) 515 for _, fix := range fixes { 516 fixesAsMap[fix] = true 517 } 518 fixed := false 519 for _, fix := range AllRuleFixes { 520 if len(fixes) == 0 || fixesAsMap[fix.Name] { 521 fixed = fix.Fn(f, rule, pkg) || fixed 522 } 523 } 524 if !fixed { 525 return nil 526 } 527 return f 528 } 529 530 // FixFile fixes everything it can in the BUILD file. 531 func FixFile(f *build.File, pkg string, fixes []string) *build.File { 532 fixesAsMap := make(map[string]bool) 533 for _, fix := range fixes { 534 fixesAsMap[fix] = true 535 } 536 fixed := false 537 for _, rule := range f.Rules("") { 538 res := FixRule(f, pkg, rule, fixes) 539 if res != nil { 540 fixed = true 541 f = res 542 } 543 } 544 for _, fix := range FileLevelFixes { 545 if len(fixes) == 0 || fixesAsMap[fix.Name] { 546 fixed = fix.Fn(f) || fixed 547 } 548 } 549 if !fixed { 550 return nil 551 } 552 return f 553 } 554 555 // A wrapper for a LoadStmt's From and To slices for consistent sorting of their contents. 556 // It's assumed that the following slices have the same length, the contents are sorted by 557 // the `To` attribute, the items of `From` are swapped exactly the same way as the items of `To`. 558 type loadArgs struct { 559 From []*build.Ident 560 To []*build.Ident 561 } 562 563 func (args loadArgs) Len() int { 564 return len(args.From) 565 } 566 567 func (args loadArgs) Swap(i, j int) { 568 args.From[i], args.From[j] = args.From[j], args.From[i] 569 args.To[i], args.To[j] = args.To[j], args.To[i] 570 } 571 572 func (args loadArgs) Less(i, j int) bool { 573 return args.To[i].Name < args.To[j].Name 574 }