github.com/cheshirekow/buildtools@v0.0.0-20200224190056-5d637702fe81/edit/edit.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 14 // Package edit provides high-level auxiliary functions for AST manipulation 15 // on BUILD files. 16 package edit 17 18 import ( 19 "fmt" 20 "os" 21 "path" 22 "path/filepath" 23 "regexp" 24 "sort" 25 "strconv" 26 "strings" 27 28 "github.com/cheshirekow/buildtools/build" 29 "github.com/cheshirekow/buildtools/tables" 30 "github.com/cheshirekow/buildtools/wspace" 31 ) 32 33 var ( 34 // ShortenLabelsFlag if true converts added labels to short form , e.g. //foo:bar => :bar 35 ShortenLabelsFlag = true 36 // DeleteWithComments if true a list attribute will be be deleted in ListDelete, even if there is a comment attached to it 37 DeleteWithComments = true 38 ) 39 40 // ParseLabel parses a Blaze label (eg. //devtools/buildozer:rule), and returns 41 // the repo name ("" for the main repo), package (with leading slashes trimmed) 42 // and rule name (e.g. ["", "devtools/buildozer", "rule"]). 43 func ParseLabel(target string) (string, string, string) { 44 repo := "" 45 if strings.HasPrefix(target, "@") { 46 target = strings.TrimLeft(target, "@") 47 parts := strings.SplitN(target, "/", 2) 48 if len(parts) == 1 { 49 // "@foo" -> "foo", "", "foo" (ie @foo//:foo) 50 return target, "", target 51 } 52 repo = parts[0] 53 target = "/" + parts[1] 54 } 55 // TODO(bazel-team): check if the next line can now be deleted 56 target = strings.TrimRight(target, ":") // labels can end with ':' 57 parts := strings.SplitN(target, ":", 2) 58 parts[0] = strings.TrimPrefix(parts[0], "//") 59 if len(parts) == 1 { 60 if strings.HasPrefix(target, "//") || tables.StripLabelLeadingSlashes { 61 // "//absolute/pkg" -> "absolute/pkg", "pkg" 62 return repo, parts[0], path.Base(parts[0]) 63 } 64 // "relative/label" -> "", "relative/label" 65 return repo, "", parts[0] 66 } 67 return repo, parts[0], parts[1] 68 } 69 70 // ShortenLabel rewrites labels to use the canonical form (the form 71 // recommended by build-style). This behavior can be disabled using the 72 // --noshorten_labels flag for projects that consistently use long-form labels. 73 // "//foo/bar:bar" => "//foo/bar", or ":bar" when possible. 74 func ShortenLabel(label string, pkg string) string { 75 if !ShortenLabelsFlag { 76 return label 77 } 78 if !strings.Contains(label, "//") { 79 // It doesn't look like a long label, so we preserve it. 80 return label 81 } 82 repo, labelPkg, rule := ParseLabel(label) 83 if repo == "" && labelPkg == pkg { // local label 84 return ":" + rule 85 } 86 slash := strings.LastIndex(labelPkg, "/") 87 if (slash >= 0 && labelPkg[slash+1:] == rule) || labelPkg == rule { 88 if repo == "" { 89 return "//" + labelPkg 90 } 91 return "@" + repo + "//" + labelPkg 92 } 93 if strings.HasPrefix(label, "@") && repo == rule && labelPkg == "" { 94 return "@" + repo 95 } 96 return label 97 } 98 99 // LabelsEqual returns true if label1 and label2 are equal. The function 100 // takes care of the optional ":" prefix and differences between long-form 101 // labels and local labels. 102 func LabelsEqual(label1, label2, pkg string) bool { 103 str1 := strings.TrimPrefix(ShortenLabel(label1, pkg), ":") 104 str2 := strings.TrimPrefix(ShortenLabel(label2, pkg), ":") 105 return str1 == str2 106 } 107 108 // isFile returns true if the path refers to a regular file after following 109 // symlinks. 110 func isFile(path string) bool { 111 path, err := filepath.EvalSymlinks(path) 112 if err != nil { 113 return false 114 } 115 info, err := os.Stat(path) 116 if err != nil { 117 return false 118 } 119 return info.Mode().IsRegular() 120 } 121 122 // InterpretLabelForWorkspaceLocation returns the name of the BUILD file to 123 // edit, the full package name, and the rule. It takes a workspace-rooted 124 // directory to use. 125 func InterpretLabelForWorkspaceLocation(root string, target string) (buildFile string, pkg string, rule string) { 126 repo, pkg, rule := ParseLabel(target) 127 rootDir, relativePath := wspace.FindWorkspaceRoot(root) 128 if repo != "" { 129 files, err := wspace.FindRepoBuildFiles(rootDir) 130 if err == nil { 131 if buildFile, ok := files[repo]; ok { 132 return buildFile, pkg, rule 133 } 134 } 135 // TODO(rodrigoq): report error for other repos 136 } 137 138 if strings.HasPrefix(target, "//") { 139 buildFile = filepath.Join(rootDir, pkg, "BUILD") 140 if !isFile(buildFile) && isFile(buildFile+".bazel") { 141 // try it with the .bazel extension 142 buildFile += ".bazel" 143 } 144 return 145 } 146 if isFile(pkg) { 147 // allow operation on other files like WORKSPACE 148 buildFile = pkg 149 pkg = filepath.Join(relativePath, filepath.Dir(pkg)) 150 return 151 } 152 if pkg != "" { 153 buildFile = filepath.Join(pkg, "/BUILD") 154 } else { 155 buildFile = "BUILD" 156 } 157 if !isFile(buildFile) && isFile(buildFile+".bazel") { 158 // try it with the .bazel extension 159 buildFile += ".bazel" 160 } 161 pkg = filepath.Join(relativePath, pkg) 162 return 163 } 164 165 // InterpretLabel returns the name of the BUILD file to edit, the full 166 // package name, and the rule. It uses the pwd for resolving workspace file paths. 167 func InterpretLabel(target string) (buildFile string, pkg string, rule string) { 168 return InterpretLabelForWorkspaceLocation("", target) 169 } 170 171 // ExprToRule returns a Rule from an Expr. 172 // The boolean is false iff the Expr is not a function call, or does not have 173 // the expected kind. 174 func ExprToRule(expr build.Expr, kind string) (*build.Rule, bool) { 175 call, ok := expr.(*build.CallExpr) 176 if !ok { 177 return nil, false 178 } 179 k, ok := call.X.(*build.Ident) 180 if !ok || k.Name != kind { 181 return nil, false 182 } 183 return &build.Rule{call, ""}, true 184 } 185 186 // ExistingPackageDeclaration returns the package declaration, or nil if there is none. 187 func ExistingPackageDeclaration(f *build.File) *build.Rule { 188 for _, stmt := range f.Stmt { 189 if rule, ok := ExprToRule(stmt, "package"); ok { 190 return rule 191 } 192 } 193 return nil 194 } 195 196 // PackageDeclaration returns the package declaration. If it doesn't 197 // exist, it is created at the top of the BUILD file, after optional 198 // docstring, comments, and load statements. 199 func PackageDeclaration(f *build.File) *build.Rule { 200 if pkg := ExistingPackageDeclaration(f); pkg != nil { 201 return pkg 202 } 203 all := []build.Expr{} 204 added := false 205 call := &build.CallExpr{X: &build.Ident{Name: "package"}} 206 for _, stmt := range f.Stmt { 207 switch stmt.(type) { 208 case *build.CommentBlock, *build.LoadStmt, *build.StringExpr: 209 // Skip docstring, comments, and load statements to 210 // find a place to insert the package declaration. 211 default: 212 if !added { 213 all = append(all, call) 214 added = true 215 } 216 } 217 all = append(all, stmt) 218 } 219 if !added { // In case the file is empty. 220 all = append(all, call) 221 } 222 f.Stmt = all 223 return &build.Rule{call, ""} 224 } 225 226 // RemoveEmptyPackage removes empty package declarations from the file, i.e.: 227 // package() 228 // This might appear because of a buildozer transformation (e.g. when removing a package 229 // attribute). Removing it is required for the file to be valid. 230 func RemoveEmptyPackage(f *build.File) *build.File { 231 var all []build.Expr 232 for _, stmt := range f.Stmt { 233 if call, ok := stmt.(*build.CallExpr); ok { 234 functionName, ok := call.X.(*build.Ident) 235 if ok && functionName.Name == "package" && len(call.List) == 0 { 236 continue 237 } 238 } 239 all = append(all, stmt) 240 } 241 return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all, Type: build.TypeBuild} 242 } 243 244 // InsertAfter inserts an expression after index i. 245 func InsertAfter(i int, stmt []build.Expr, expr build.Expr) []build.Expr { 246 i = i + 1 // index after the element at i 247 result := make([]build.Expr, len(stmt)+1) 248 copy(result[0:i], stmt[0:i]) 249 result[i] = expr 250 copy(result[i+1:], stmt[i:]) 251 return result 252 } 253 254 // IndexOfLast finds the index of the last expression of a specific kind. 255 func IndexOfLast(stmt []build.Expr, Kind string) int { 256 lastIndex := -1 257 for i, s := range stmt { 258 sAsCallExpr, ok := s.(*build.CallExpr) 259 if !ok { 260 continue 261 } 262 literal, ok := sAsCallExpr.X.(*build.Ident) 263 if ok && literal.Name == Kind { 264 lastIndex = i 265 } 266 } 267 return lastIndex 268 } 269 270 // InsertAfterLastOfSameKind inserts an expression after the last expression of the same kind. 271 func InsertAfterLastOfSameKind(stmt []build.Expr, expr *build.CallExpr) []build.Expr { 272 index := IndexOfLast(stmt, expr.X.(*build.Ident).Name) 273 if index == -1 { 274 return InsertAtEnd(stmt, expr) 275 } 276 return InsertAfter(index, stmt, expr) 277 } 278 279 // InsertAtEnd inserts an expression at the end of a list, before trailing comments. 280 func InsertAtEnd(stmt []build.Expr, expr build.Expr) []build.Expr { 281 var i int 282 for i = len(stmt) - 1; i >= 0; i-- { 283 _, ok := stmt[i].(*build.CommentBlock) 284 if !ok { 285 break 286 } 287 } 288 return InsertAfter(i, stmt, expr) 289 } 290 291 // FindRuleByName returns the rule in the file that has the given name. 292 // If the name is "__pkg__", it returns the global package declaration. 293 func FindRuleByName(f *build.File, name string) *build.Rule { 294 if name == "__pkg__" { 295 return PackageDeclaration(f) 296 } 297 _, rule := IndexOfRuleByName(f, name) 298 return rule 299 } 300 301 // IndexOfRuleByName returns the index (in f.Stmt) of the CallExpr which defines a rule named `name`, or -1 if it doesn't exist. 302 func IndexOfRuleByName(f *build.File, name string) (int, *build.Rule) { 303 linenum := -1 304 if strings.HasPrefix(name, "%") { 305 // "%<LINENUM>" will match the rule which begins at LINENUM. 306 // This is for convenience, "%" is not a valid character in bazel targets. 307 if result, err := strconv.Atoi(name[1:]); err == nil { 308 linenum = result 309 } 310 } 311 312 for i, stmt := range f.Stmt { 313 call, ok := stmt.(*build.CallExpr) 314 if !ok { 315 continue 316 } 317 r := f.Rule(call) 318 start, _ := call.X.Span() 319 if r.Name() == name || start.Line == linenum { 320 return i, r 321 } 322 } 323 return -1, nil 324 } 325 326 // FindExportedFile returns the first exports_files call which contains the 327 // file 'name', or nil if not found 328 func FindExportedFile(f *build.File, name string) *build.Rule { 329 for _, r := range f.Rules("exports_files") { 330 if len(r.Call.List) == 0 { 331 continue 332 } 333 pkg := "" // Files are not affected by the package name 334 if ListFind(r.Call.List[0], name, pkg) != nil { 335 return r 336 } 337 } 338 return nil 339 } 340 341 // DeleteRule returns the AST without the specified rule 342 func DeleteRule(f *build.File, rule *build.Rule) *build.File { 343 var all []build.Expr 344 for _, stmt := range f.Stmt { 345 if stmt == rule.Call { 346 continue 347 } 348 all = append(all, stmt) 349 } 350 return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all, Type: build.TypeBuild} 351 } 352 353 // DeleteRuleByName returns the AST without the rules that have the 354 // given name. 355 func DeleteRuleByName(f *build.File, name string) *build.File { 356 var all []build.Expr 357 for _, stmt := range f.Stmt { 358 call, ok := stmt.(*build.CallExpr) 359 if !ok { 360 all = append(all, stmt) 361 continue 362 } 363 r := f.Rule(call) 364 if r.Name() != name { 365 all = append(all, stmt) 366 } 367 } 368 return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all, Type: build.TypeBuild} 369 } 370 371 // DeleteRuleByKind removes the rules of the specified kind from the AST. 372 // Returns an updated copy of f. 373 func DeleteRuleByKind(f *build.File, kind string) *build.File { 374 var all []build.Expr 375 for _, stmt := range f.Stmt { 376 call, ok := stmt.(*build.CallExpr) 377 if !ok { 378 all = append(all, stmt) 379 continue 380 } 381 k, ok := call.X.(*build.Ident) 382 if !ok || k.Name != kind { 383 all = append(all, stmt) 384 } 385 } 386 return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all, Type: build.TypeBuild} 387 } 388 389 // AllLists returns all the lists concatenated in an expression. 390 // For example, in: glob(["*.go"]) + [":rule"] 391 // the function will return [[":rule"]]. 392 func AllLists(e build.Expr) []*build.ListExpr { 393 switch e := e.(type) { 394 case *build.ListExpr: 395 return []*build.ListExpr{e} 396 case *build.BinaryExpr: 397 if e.Op == "+" { 398 return append(AllLists(e.X), AllLists(e.Y)...) 399 } 400 } 401 return nil 402 } 403 404 // AllSelects returns all the selects concatenated in an expression. 405 func AllSelects(e build.Expr) []*build.CallExpr { 406 switch e := e.(type) { 407 case *build.BinaryExpr: 408 if e.Op == "+" { 409 return append(AllSelects(e.X), AllSelects(e.Y)...) 410 } 411 case *build.CallExpr: 412 if x, ok := e.X.(*build.Ident); ok && x.Name == "select" { 413 return []*build.CallExpr{e} 414 } 415 } 416 return nil 417 } 418 419 // FirstList works in the same way as AllLists, except that it 420 // returns only one list, or nil. 421 func FirstList(e build.Expr) *build.ListExpr { 422 switch e := e.(type) { 423 case *build.ListExpr: 424 return e 425 case *build.BinaryExpr: 426 if e.Op == "+" { 427 li := FirstList(e.X) 428 if li == nil { 429 return FirstList(e.Y) 430 } 431 return li 432 } 433 } 434 return nil 435 } 436 437 // AllStrings returns all the string literals concatenated in an expression. 438 // For example, in: "foo" + x + "bar" 439 // the function will return ["foo", "bar"]. 440 func AllStrings(e build.Expr) []*build.StringExpr { 441 switch e := e.(type) { 442 case *build.StringExpr: 443 return []*build.StringExpr{e} 444 case *build.BinaryExpr: 445 if e.Op == "+" { 446 return append(AllStrings(e.X), AllStrings(e.Y)...) 447 } 448 } 449 return nil 450 } 451 452 // ListFind looks for a string in the list expression (which may be a 453 // concatenation of lists). It returns the element if it is found. nil 454 // otherwise. 455 func ListFind(e build.Expr, item string, pkg string) *build.StringExpr { 456 item = ShortenLabel(item, pkg) 457 for _, li := range AllLists(e) { 458 for _, elem := range li.List { 459 str, ok := elem.(*build.StringExpr) 460 if ok && LabelsEqual(str.Value, item, pkg) { 461 return str 462 } 463 } 464 } 465 return nil 466 } 467 468 // hasComments returns whether the StringExpr literal has a comment attached to it. 469 func hasComments(literal *build.StringExpr) bool { 470 return len(literal.Before) > 0 || len(literal.Suffix) > 0 471 } 472 473 // ContainsComments returns whether the expr has a comment that includes str. 474 func ContainsComments(expr build.Expr, str string) bool { 475 str = strings.ToLower(str) 476 com := expr.Comment() 477 comments := append(com.Before, com.Suffix...) 478 comments = append(comments, com.After...) 479 for _, c := range comments { 480 if strings.Contains(strings.ToLower(c.Token), str) { 481 return true 482 } 483 } 484 return false 485 } 486 487 // RemoveEmptySelectsAndConcatLists iterates the tree in order to turn 488 // empty selects into empty lists and adjacent lists are concatenated 489 func RemoveEmptySelectsAndConcatLists(e build.Expr) build.Expr { 490 switch e := e.(type) { 491 case *build.BinaryExpr: 492 if e.Op == "+" { 493 e.X = RemoveEmptySelectsAndConcatLists(e.X) 494 e.Y = RemoveEmptySelectsAndConcatLists(e.Y) 495 496 x, xIsList := e.X.(*build.ListExpr) 497 y, yIsList := e.Y.(*build.ListExpr) 498 499 if xIsList && yIsList { 500 return &build.ListExpr{List: append(x.List, y.List...)} 501 } 502 503 if xIsList && len(x.List) == 0 { 504 return e.Y 505 } 506 507 if yIsList && len(y.List) == 0 { 508 return e.X 509 } 510 } 511 case *build.CallExpr: 512 if x, ok := e.X.(*build.Ident); ok && x.Name == "select" { 513 if len(e.List) == 0 { 514 return &build.ListExpr{List: []build.Expr{}} 515 } 516 517 if dict, ok := e.List[0].(*build.DictExpr); ok { 518 for _, keyVal := range dict.List { 519 if keyVal, ok := keyVal.(*build.KeyValueExpr); ok { 520 val, ok := keyVal.Value.(*build.ListExpr) 521 if !ok || len(val.List) > 0 { 522 return e 523 } 524 } else { 525 return e 526 } 527 } 528 529 return &build.ListExpr{List: []build.Expr{}} 530 } 531 } 532 } 533 534 return e 535 } 536 537 // ComputeIntersection returns the intersection of the two lists given as parameters; 538 // if the containing elements are not build.StringExpr, the result will be nil. 539 func ComputeIntersection(list1, list2 []build.Expr) []build.Expr { 540 if list1 == nil || list2 == nil { 541 return nil 542 } 543 544 if len(list2) == 0 { 545 return []build.Expr{} 546 } 547 548 i := 0 549 for j, common := range list1 { 550 if common, ok := common.(*build.StringExpr); ok { 551 found := false 552 for _, elem := range list2 { 553 if str, ok := elem.(*build.StringExpr); ok { 554 if str.Value == common.Value { 555 found = true 556 break 557 } 558 } else { 559 return nil 560 } 561 } 562 563 if found { 564 list1[i] = list1[j] 565 i++ 566 } 567 } else { 568 return nil 569 } 570 } 571 return list1[:i] 572 } 573 574 // SelectListsIntersection returns the intersection of the lists of strings inside 575 // the dictionary argument of the select expression given as a parameter 576 func SelectListsIntersection(sel *build.CallExpr, pkg string) (intersection []build.Expr) { 577 if len(sel.List) == 0 || len(sel.List) > 1 { 578 return nil 579 } 580 581 dict, ok := sel.List[0].(*build.DictExpr) 582 if !ok || len(dict.List) == 0 { 583 return nil 584 } 585 586 if keyVal, ok := dict.List[0].(*build.KeyValueExpr); ok { 587 if val, ok := keyVal.Value.(*build.ListExpr); ok { 588 intersection = make([]build.Expr, len(val.List)) 589 copy(intersection, val.List) 590 } 591 } 592 593 for _, keyVal := range dict.List[1:] { 594 if keyVal, ok := keyVal.(*build.KeyValueExpr); ok { 595 if val, ok := keyVal.Value.(*build.ListExpr); ok { 596 intersection = ComputeIntersection(intersection, val.List) 597 if len(intersection) == 0 { 598 return intersection 599 } 600 } else { 601 return nil 602 } 603 } else { 604 return nil 605 } 606 } 607 608 return intersection 609 } 610 611 // ResolveAttr extracts common elements of the lists inside select dictionaries 612 // and adds them at attribute level rather than select level, as well as turns 613 // empty selects into empty lists and concatenates adjacent lists 614 func ResolveAttr(r *build.Rule, attr, pkg string) { 615 var toExtract []build.Expr 616 617 e := r.Attr(attr) 618 if e == nil { 619 return 620 } 621 622 for _, sel := range AllSelects(e) { 623 intersection := SelectListsIntersection(sel, pkg) 624 if intersection != nil { 625 toExtract = append(toExtract, intersection...) 626 } 627 } 628 629 for _, common := range toExtract { 630 e = AddValueToList(e, pkg, common, false) // this will also remove them from selects 631 } 632 633 r.SetAttr(attr, RemoveEmptySelectsAndConcatLists(e)) 634 } 635 636 // SelectDelete removes the item from all the lists which are values 637 // in the dictionary of every select 638 func SelectDelete(e build.Expr, item, pkg string, deleted **build.StringExpr) { 639 for _, sel := range AllSelects(e) { 640 if len(sel.List) == 0 { 641 continue 642 } 643 644 if dict, ok := sel.List[0].(*build.DictExpr); ok { 645 for _, keyVal := range dict.List { 646 if keyVal, ok := keyVal.(*build.KeyValueExpr); ok { 647 if val, ok := keyVal.Value.(*build.ListExpr); ok { 648 RemoveFromList(val, item, pkg, deleted) 649 } 650 } 651 } 652 } 653 } 654 } 655 656 // RemoveFromList removes one element from a ListExpr and stores 657 // the deleted StringExpr at the address pointed by the last parameter 658 func RemoveFromList(li *build.ListExpr, item, pkg string, deleted **build.StringExpr) { 659 var all []build.Expr 660 for _, elem := range li.List { 661 if str, ok := elem.(*build.StringExpr); ok { 662 if LabelsEqual(str.Value, item, pkg) && (DeleteWithComments || !hasComments(str)) { 663 if deleted != nil { 664 *deleted = str 665 } 666 667 continue 668 } 669 } 670 all = append(all, elem) 671 } 672 li.List = all 673 } 674 675 // ListDelete deletes the item from a list expression in e and returns 676 // the StringExpr deleted, or nil otherwise. 677 func ListDelete(e build.Expr, item, pkg string) (deleted *build.StringExpr) { 678 if unquoted, _, err := build.Unquote(item); err == nil { 679 item = unquoted 680 } 681 deleted = nil 682 item = ShortenLabel(item, pkg) 683 for _, li := range AllLists(e) { 684 RemoveFromList(li, item, pkg, &deleted) 685 } 686 687 SelectDelete(e, item, pkg, &deleted) 688 689 return deleted 690 } 691 692 // ListAttributeDelete deletes string item from list attribute attr, deletes attr if empty, 693 // and returns the StringExpr deleted, or nil otherwise. 694 func ListAttributeDelete(rule *build.Rule, attr, item, pkg string) *build.StringExpr { 695 deleted := ListDelete(rule.Attr(attr), item, pkg) 696 if deleted != nil { 697 if listExpr, ok := rule.Attr(attr).(*build.ListExpr); ok && len(listExpr.List) == 0 { 698 rule.DelAttr(attr) 699 } 700 } 701 return deleted 702 } 703 704 // ListReplace replaces old with value in all lists in e and returns a Boolean 705 // to indicate whether the replacement was successful. 706 func ListReplace(e build.Expr, old, value, pkg string) bool { 707 replaced := false 708 old = ShortenLabel(old, pkg) 709 for _, li := range AllLists(e) { 710 for k, elem := range li.List { 711 str, ok := elem.(*build.StringExpr) 712 if !ok || !LabelsEqual(str.Value, old, pkg) { 713 continue 714 } 715 li.List[k] = &build.StringExpr{Value: ShortenLabel(value, pkg), Comments: *elem.Comment()} 716 replaced = true 717 } 718 } 719 return replaced 720 } 721 722 // ListSubstitute replaces strings matching a regular expression in all lists 723 // in e and returns a Boolean to indicate whether the replacement was 724 // successful. 725 func ListSubstitute(e build.Expr, oldRegexp *regexp.Regexp, newTemplate string) bool { 726 substituted := false 727 for _, li := range AllLists(e) { 728 for k, elem := range li.List { 729 str, ok := elem.(*build.StringExpr) 730 if !ok { 731 continue 732 } 733 newValue, ok := stringSubstitute(str.Value, oldRegexp, newTemplate) 734 if ok { 735 li.List[k] = &build.StringExpr{Value: newValue, Comments: *elem.Comment()} 736 substituted = true 737 } 738 } 739 } 740 return substituted 741 } 742 743 func stringSubstitute(oldValue string, oldRegexp *regexp.Regexp, newTemplate string) (string, bool) { 744 match := oldRegexp.FindStringSubmatchIndex(oldValue) 745 if match == nil { 746 return oldValue, false 747 } 748 newValue := string(oldRegexp.ExpandString(nil, newTemplate, oldValue, match)) 749 if match[0] > 0 { 750 newValue = oldValue[:match[0]] + newValue 751 } 752 if match[1] < len(oldValue) { 753 newValue = newValue + oldValue[match[1]:] 754 } 755 return newValue, true 756 } 757 758 // isExprLessThan compares two Expr statements. Currently, only labels are supported. 759 func isExprLessThan(x1, x2 build.Expr) bool { 760 str1, ok1 := x1.(*build.StringExpr) 761 str2, ok2 := x2.(*build.StringExpr) 762 if ok1 != ok2 { 763 return ok2 764 } 765 if ok1 && ok2 { 766 // Labels starting with // are put at the end. 767 pre1 := strings.HasPrefix(str1.Value, "//") 768 pre2 := strings.HasPrefix(str2.Value, "//") 769 if pre1 != pre2 { 770 return pre2 771 } 772 return str1.Value < str2.Value 773 } 774 return false 775 } 776 777 func sortedInsert(list []build.Expr, item build.Expr) []build.Expr { 778 i := 0 779 for ; i < len(list); i++ { 780 if isExprLessThan(item, list[i]) { 781 break 782 } 783 } 784 res := make([]build.Expr, 0, len(list)+1) 785 res = append(res, list[:i]...) 786 res = append(res, item) 787 res = append(res, list[i:]...) 788 return res 789 } 790 791 // attributeMustNotBeSorted returns true if the list in the attribute cannot be 792 // sorted. For some attributes, it makes sense to try to do a sorted insert 793 // (e.g. deps), even when buildifier will not sort it for conservative reasons. 794 // For a few attributes, sorting will never make sense. 795 func attributeMustNotBeSorted(rule, attr string) bool { 796 // TODO(bazel-team): Come up with a more complete list. 797 return attr == "args" 798 } 799 800 // getVariable returns the binary expression that assignes a variable to expr, if expr is 801 // an identifier of a variable that vars contains a mapping for. 802 func getVariable(expr build.Expr, vars *map[string]*build.AssignExpr) (varAssignment *build.AssignExpr) { 803 if vars == nil { 804 return nil 805 } 806 807 if literal, ok := expr.(*build.Ident); ok { 808 if varAssignment = (*vars)[literal.Name]; varAssignment != nil { 809 return varAssignment 810 } 811 } 812 return nil 813 } 814 815 // AddValueToList adds a value to a list. If the expression is 816 // not a list, a list with a single element is appended to the original 817 // expression. 818 func AddValueToList(oldList build.Expr, pkg string, item build.Expr, sorted bool) build.Expr { 819 if oldList == nil { 820 return &build.ListExpr{List: []build.Expr{item}} 821 } 822 823 str, ok := item.(*build.StringExpr) 824 if ok { 825 if ListFind(oldList, str.Value, pkg) != nil { 826 // The value is already in the list. 827 return oldList 828 } 829 SelectDelete(oldList, str.Value, pkg, nil) 830 } 831 832 li := FirstList(oldList) 833 if li != nil { 834 if sorted { 835 li.List = sortedInsert(li.List, item) 836 } else { 837 li.List = append(li.List, item) 838 } 839 return oldList 840 } 841 list := &build.ListExpr{List: []build.Expr{item}} 842 concat := &build.BinaryExpr{Op: "+", X: oldList, Y: list} 843 return concat 844 } 845 846 // AddValueToListAttribute adds the given item to the list attribute identified by name and pkg. 847 func AddValueToListAttribute(r *build.Rule, name string, pkg string, item build.Expr, vars *map[string]*build.AssignExpr) { 848 old := r.Attr(name) 849 sorted := !attributeMustNotBeSorted(r.Kind(), name) 850 if varAssignment := getVariable(old, vars); varAssignment != nil { 851 varAssignment.RHS = AddValueToList(varAssignment.RHS, pkg, item, sorted) 852 } else { 853 r.SetAttr(name, AddValueToList(old, pkg, item, sorted)) 854 } 855 } 856 857 // MoveAllListAttributeValues moves all values from list attribute oldAttr to newAttr, 858 // and deletes oldAttr. 859 func MoveAllListAttributeValues(rule *build.Rule, oldAttr, newAttr, pkg string, vars *map[string]*build.AssignExpr) error { 860 if rule.Attr(oldAttr) == nil { 861 return fmt.Errorf("no attribute %s found in %s", oldAttr, rule.Name()) 862 } 863 if rule.Attr(newAttr) == nil { 864 RenameAttribute(rule, oldAttr, newAttr) 865 return nil 866 } 867 if listExpr, ok := rule.Attr(oldAttr).(*build.ListExpr); ok { 868 for _, val := range listExpr.List { 869 AddValueToListAttribute(rule, newAttr, pkg, val, vars) 870 } 871 rule.DelAttr(oldAttr) 872 return nil 873 } 874 return fmt.Errorf("%s already exists and %s is not a simple list", newAttr, oldAttr) 875 } 876 877 // DictionarySet looks for the key in the dictionary expression. If value is not nil, 878 // it replaces the current value with it. In all cases, it returns the current value. 879 func DictionarySet(dict *build.DictExpr, key string, value build.Expr) build.Expr { 880 for _, e := range dict.List { 881 kv, _ := e.(*build.KeyValueExpr) 882 if k, ok := kv.Key.(*build.StringExpr); ok && k.Value == key { 883 if value != nil { 884 kv.Value = value 885 } 886 return kv.Value 887 } 888 } 889 if value != nil { 890 kv := &build.KeyValueExpr{Key: &build.StringExpr{Value: key}, Value: value} 891 dict.List = append(dict.List, kv) 892 } 893 return nil 894 } 895 896 // DictionaryGet looks for the key in the dictionary expression, and returns the 897 // current value. If it is unset, it returns nil. 898 func DictionaryGet(dict *build.DictExpr, key string) build.Expr { 899 for _, e := range dict.List { 900 kv, ok := e.(*build.KeyValueExpr) 901 if !ok { 902 continue 903 } 904 if k, ok := kv.Key.(*build.StringExpr); ok && k.Value == key { 905 return kv.Value 906 } 907 } 908 return nil 909 } 910 911 // DictionaryDelete looks for the key in the dictionary expression. If the key exists, 912 // it removes the key-value pair and returns it. Otherwise it returns nil. 913 func DictionaryDelete(dict *build.DictExpr, key string) (deleted build.Expr) { 914 if unquoted, _, err := build.Unquote(key); err == nil { 915 key = unquoted 916 } 917 deleted = nil 918 var all []build.Expr 919 for _, e := range dict.List { 920 kv, _ := e.(*build.KeyValueExpr) 921 if k, ok := kv.Key.(*build.StringExpr); ok { 922 if k.Value == key { 923 deleted = kv 924 } else { 925 all = append(all, e) 926 } 927 } 928 } 929 dict.List = all 930 return deleted 931 } 932 933 // RenameAttribute renames an attribute in a rule. 934 func RenameAttribute(r *build.Rule, oldName, newName string) error { 935 if r.Attr(newName) != nil { 936 return fmt.Errorf("attribute %s already exists in rule %s", newName, r.Name()) 937 } 938 for _, kv := range r.Call.List { 939 as, ok := kv.(*build.AssignExpr) 940 if !ok { 941 continue 942 } 943 k, ok := as.LHS.(*build.Ident) 944 if !ok || k.Name != oldName { 945 continue 946 } 947 k.Name = newName 948 return nil 949 } 950 return fmt.Errorf("no attribute %s found in rule %s", oldName, r.Name()) 951 } 952 953 // EditFunction is a wrapper around build.Edit. The callback is called only on 954 // functions 'name'. 955 func EditFunction(v build.Expr, name string, f func(x *build.CallExpr, stk []build.Expr) build.Expr) build.Expr { 956 return build.Edit(v, func(expr build.Expr, stk []build.Expr) build.Expr { 957 call, ok := expr.(*build.CallExpr) 958 if !ok { 959 return nil 960 } 961 fct, ok := call.X.(*build.Ident) 962 if !ok || fct.Name != name { 963 return nil 964 } 965 return f(call, stk) 966 }) 967 } 968 969 // UsedSymbols returns the set of symbols used in the BUILD file (variables, function names). 970 func UsedSymbols(stmt build.Expr) map[string]bool { 971 symbols := make(map[string]bool) 972 build.Walk(stmt, func(expr build.Expr, stack []build.Expr) { 973 // Don't traverse inside load statements 974 if len(stack) > 0 { 975 if _, ok := stack[len(stack)-1].(*build.LoadStmt); ok { 976 return 977 } 978 } 979 980 literal, ok := expr.(*build.Ident) 981 if !ok { 982 return 983 } 984 // Check if we are on the left-side of an assignment 985 for _, e := range stack { 986 if as, ok := e.(*build.AssignExpr); ok { 987 if as.LHS == expr { 988 return 989 } 990 } 991 } 992 symbols[literal.Name] = true 993 }) 994 return symbols 995 } 996 997 // NewLoad creates a new LoadStmt node 998 func NewLoad(location string, from, to []string) *build.LoadStmt { 999 load := &build.LoadStmt{ 1000 Module: &build.StringExpr{ 1001 Value: location, 1002 }, 1003 ForceCompact: true, 1004 } 1005 for i := range from { 1006 load.From = append(load.From, &build.Ident{Name: from[i]}) 1007 load.To = append(load.To, &build.Ident{Name: to[i]}) 1008 } 1009 return load 1010 } 1011 1012 // AppendToLoad appends symbols to an existing load statement 1013 // Returns true if the statement was acually edited (if the required symbols haven't been 1014 // loaded yet) 1015 func AppendToLoad(load *build.LoadStmt, from, to []string) bool { 1016 symbolsToLoad := make(map[string]string) 1017 for i, s := range to { 1018 symbolsToLoad[s] = from[i] 1019 } 1020 for _, ident := range load.To { 1021 delete(symbolsToLoad, ident.Name) // Already loaded. 1022 } 1023 1024 if len(symbolsToLoad) == 0 { 1025 return false 1026 } 1027 1028 // Append the remaining loads to the load statement. 1029 sortedSymbols := []string{} 1030 for s := range symbolsToLoad { 1031 sortedSymbols = append(sortedSymbols, s) 1032 } 1033 sort.Strings(sortedSymbols) 1034 for _, s := range sortedSymbols { 1035 load.From = append(load.From, &build.Ident{Name: symbolsToLoad[s]}) 1036 load.To = append(load.To, &build.Ident{Name: s}) 1037 } 1038 return true 1039 } 1040 1041 // appendLoad tries to find an existing load location and append symbols to it. 1042 func appendLoad(stmts []build.Expr, location string, from, to []string) bool { 1043 symbolsToLoad := make(map[string]string) 1044 for i, s := range to { 1045 symbolsToLoad[s] = from[i] 1046 } 1047 var lastLoad *build.LoadStmt 1048 for _, s := range stmts { 1049 load, ok := s.(*build.LoadStmt) 1050 if !ok { 1051 continue 1052 } 1053 if load.Module.Value != location { 1054 continue // Loads a different file. 1055 } 1056 for _, ident := range load.To { 1057 delete(symbolsToLoad, ident.Name) // Already loaded. 1058 } 1059 // Remember the last insert location, but potentially remove more symbols 1060 // that are already loaded in other subsequent calls. 1061 lastLoad = load 1062 } 1063 if lastLoad == nil { 1064 return false 1065 } 1066 1067 // Append the remaining loads to the last load location. 1068 from = []string{} 1069 to = []string{} 1070 for t, f := range symbolsToLoad { 1071 from = append(from, f) 1072 to = append(to, t) 1073 } 1074 AppendToLoad(lastLoad, from, to) 1075 return true 1076 } 1077 1078 // InsertLoad inserts a load statement at the top of the list of statements. 1079 // The load statement is constructed using a string location and two slices of from- and to-symbols. 1080 // The function panics if the slices aren't of the same lentgh. Symbols that are already loaded 1081 // from the given filepath are ignored. If stmts already contains a load for the 1082 // location in arguments, appends the symbols to load to it. 1083 func InsertLoad(stmts []build.Expr, location string, from, to []string) []build.Expr { 1084 if len(from) != len(to) { 1085 panic(fmt.Errorf("length mismatch: %v (from) and %v (to)", len(from), len(to))) 1086 } 1087 1088 if appendLoad(stmts, location, from, to) { 1089 return stmts 1090 } 1091 1092 load := NewLoad(location, from, to) 1093 1094 var all []build.Expr 1095 added := false 1096 for i, stmt := range stmts { 1097 _, isComment := stmt.(*build.CommentBlock) 1098 _, isString := stmt.(*build.StringExpr) 1099 isDocString := isString && i == 0 1100 if isComment || isDocString || added { 1101 all = append(all, stmt) 1102 continue 1103 } 1104 all = append(all, load) 1105 all = append(all, stmt) 1106 added = true 1107 } 1108 if !added { // Empty file or just comments. 1109 all = append(all, load) 1110 } 1111 return all 1112 } 1113 1114 // ReplaceLoad removes load statements for passed to-symbols and replaces them with a new 1115 // load at the top of the list of statements. The new load statement is constructed using 1116 // a string location and two slices of from- and to-symbols. If stmts already contains a 1117 // load for the location in arguments, appends the symbols to load to it. 1118 // The function panics if the slices aren't of the same lentgh. 1119 func ReplaceLoad(stmts []build.Expr, location string, from, to []string) []build.Expr { 1120 if len(from) != len(to) { 1121 panic(fmt.Errorf("length mismatch: %v (from) and %v (to)", len(from), len(to))) 1122 } 1123 1124 toSymbols := make(map[string]bool, len(to)) 1125 for _, name := range to { 1126 toSymbols[name] = true 1127 } 1128 1129 // 1. Remove loads that will be replaced. 1130 var all []build.Expr 1131 for _, stmt := range stmts { 1132 load, ok := stmt.(*build.LoadStmt) 1133 if !ok { 1134 all = append(all, stmt) 1135 continue 1136 } 1137 1138 for i, to := range load.To { 1139 if toSymbols[to.Name] { 1140 if i < len(load.From)-1 { 1141 load.From = append(load.From[:i], load.From[i+1:]...) 1142 load.To = append(load.To[:i], load.To[i+1:]...) 1143 } else { 1144 load.From = load.From[:i] 1145 load.To = load.To[:i] 1146 } 1147 } 1148 } 1149 1150 if len(load.To) > 0 { 1151 all = append(all, load) 1152 } 1153 } 1154 1155 // 2. Insert new loads. 1156 return InsertLoad(all, location, from, to) 1157 }