1// Warnings for incompatible changes in the Bazel API 2 3package warn 4 5import ( 6 "fmt" 7 "sort" 8 9 "github.com/bazelbuild/buildtools/build" 10 "github.com/bazelbuild/buildtools/bzlenv" 11 "github.com/bazelbuild/buildtools/edit" 12 "github.com/bazelbuild/buildtools/tables" 13) 14 15// Bazel API-specific warnings 16 17// negateExpression returns an expression which is a negation of the input. 18// If it's a boolean literal (true or false), just return the opposite literal. 19// If it's a unary expression with a unary `not` operator, just remove it. 20// Otherwise, insert a `not` operator. 21// It's assumed that input is no longer needed as it may be mutated or reused by the function. 22func negateExpression(expr build.Expr) build.Expr { 23 paren, ok := expr.(*build.ParenExpr) 24 if ok { 25 newParen := *paren 26 newParen.X = negateExpression(paren.X) 27 return &newParen 28 } 29 30 unary, ok := expr.(*build.UnaryExpr) 31 if ok && unary.Op == "not" { 32 return unary.X 33 } 34 35 boolean, ok := expr.(*build.Ident) 36 if ok { 37 newBoolean := *boolean 38 if boolean.Name == "True" { 39 newBoolean.Name = "False" 40 } else { 41 newBoolean.Name = "True" 42 } 43 return &newBoolean 44 } 45 46 return &build.UnaryExpr{ 47 Op: "not", 48 X: expr, 49 } 50} 51 52// getParam search for a param with a given name in a given list of function arguments 53// and returns it with its index 54func getParam(attrs []build.Expr, paramName string) (int, *build.Ident, *build.AssignExpr) { 55 for i, attr := range attrs { 56 as, ok := attr.(*build.AssignExpr) 57 if !ok { 58 continue 59 } 60 name, ok := (as.LHS).(*build.Ident) 61 if !ok || name.Name != paramName { 62 continue 63 } 64 return i, name, as 65 } 66 return -1, nil, nil 67} 68 69// isFunctionCall checks whether expr is a call of a function with a given name 70func isFunctionCall(expr build.Expr, name string) (*build.CallExpr, bool) { 71 call, ok := expr.(*build.CallExpr) 72 if !ok { 73 return nil, false 74 } 75 if ident, ok := call.X.(*build.Ident); ok && ident.Name == name { 76 return call, true 77 } 78 return nil, false 79} 80 81// globalVariableUsageCheck checks whether there's a usage of a given global variable in the file. 82// It's ok to shadow the name with a local variable and use it. 83func globalVariableUsageCheck(f *build.File, global, alternative string) []*LinterFinding { 84 var findings []*LinterFinding 85 86 if f.Type != build.TypeBzl { 87 return findings 88 } 89 90 var walk func(e *build.Expr, env *bzlenv.Environment) 91 walk = func(e *build.Expr, env *bzlenv.Environment) { 92 defer bzlenv.WalkOnceWithEnvironment(*e, env, walk) 93 94 ident, ok := (*e).(*build.Ident) 95 if !ok { 96 return 97 } 98 if ident.Name != global { 99 return 100 } 101 if binding := env.Get(ident.Name); binding != nil { 102 return 103 } 104 105 // Fix 106 newIdent := *ident 107 newIdent.Name = alternative 108 109 findings = append(findings, makeLinterFinding(ident, 110 fmt.Sprintf(`Global variable %q is deprecated in favor of %q. Please rename it.`, global, alternative), 111 LinterReplacement{e, &newIdent})) 112 } 113 var expr build.Expr = f 114 walk(&expr, bzlenv.NewEnvironment()) 115 116 return findings 117} 118 119// insertLoad returns a *LinterReplacement object representing a replacement required for inserting 120// an additional load statement. Returns nil if nothing needs to be changed. 121func insertLoad(f *build.File, module string, symbols []string) *LinterReplacement { 122 // Try to find an existing load statement 123 for i, stmt := range f.Stmt { 124 load, ok := stmt.(*build.LoadStmt) 125 if !ok || load.Module.Value != module { 126 continue 127 } 128 129 // Modify an existing load statement 130 newLoad := *load 131 if !edit.AppendToLoad(&newLoad, symbols, symbols) { 132 return nil 133 } 134 return &LinterReplacement{&(f.Stmt[i]), &newLoad} 135 } 136 137 // Need to insert a new load statement. Can't modify the tree here, so just insert a placeholder 138 // nil statement and return a replacement for it. 139 i := 0 140 for i = range f.Stmt { 141 stmt := f.Stmt[i] 142 _, isComment := stmt.(*build.CommentBlock) 143 _, isString := stmt.(*build.StringExpr) 144 isDocString := isString && i == 0 145 if !isComment && !isDocString { 146 // Insert a nil statement here 147 break 148 } 149 } 150 stmts := append([]build.Expr{}, f.Stmt[:i]...) 151 stmts = append(stmts, nil) 152 stmts = append(stmts, f.Stmt[i:]...) 153 f.Stmt = stmts 154 155 return &LinterReplacement{&(f.Stmt[i]), edit.NewLoad(module, symbols, symbols)} 156} 157 158func notLoadedFunctionUsageCheckInternal(expr *build.Expr, env *bzlenv.Environment, globals []string, loadFrom string) ([]string, []*LinterFinding) { 159 var loads []string 160 var findings []*LinterFinding 161 162 call, ok := (*expr).(*build.CallExpr) 163 if !ok { 164 return loads, findings 165 } 166 167 var name string 168 var replacements []LinterReplacement 169 switch node := call.X.(type) { 170 case *build.DotExpr: 171 // Maybe native.`global`? 172 ident, ok := node.X.(*build.Ident) 173 if !ok || ident.Name != "native" { 174 return loads, findings 175 } 176 177 name = node.Name 178 // Replace `native.foo()` with `foo()` 179 newCall := *call 180 newCall.X = &build.Ident{Name: node.Name} 181 replacements = append(replacements, LinterReplacement{expr, &newCall}) 182 case *build.Ident: 183 // Maybe `global`()? 184 if binding := env.Get(node.Name); binding != nil { 185 return loads, findings 186 } 187 name = node.Name 188 default: 189 return loads, findings 190 } 191 192 for _, global := range globals { 193 if name == global { 194 loads = append(loads, name) 195 findings = append(findings, 196 makeLinterFinding(call.X, fmt.Sprintf(`Function %q is not global anymore and needs to be loaded from %q.`, global, loadFrom), replacements...)) 197 break 198 } 199 } 200 201 return loads, findings 202} 203 204func notLoadedSymbolUsageCheckInternal(expr *build.Expr, env *bzlenv.Environment, globals []string, loadFrom string) ([]string, []*LinterFinding) { 205 var loads []string 206 var findings []*LinterFinding 207 208 ident, ok := (*expr).(*build.Ident) 209 if !ok { 210 return loads, findings 211 } 212 if binding := env.Get(ident.Name); binding != nil { 213 return loads, findings 214 } 215 216 for _, global := range globals { 217 if ident.Name == global { 218 loads = append(loads, ident.Name) 219 findings = append(findings, 220 makeLinterFinding(ident, fmt.Sprintf(`Symbol %q is not global anymore and needs to be loaded from %q.`, global, loadFrom))) 221 break 222 } 223 } 224 225 return loads, findings 226} 227 228// notLoadedUsageCheck checks whether there's a usage of a given not imported function or symbol in the file 229// and adds a load statement if necessary. 230func notLoadedUsageCheck(f *build.File, functions, symbols []string, loadFrom string) []*LinterFinding { 231 toLoad := make(map[string]bool) 232 var findings []*LinterFinding 233 234 var walk func(expr *build.Expr, env *bzlenv.Environment) 235 walk = func(expr *build.Expr, env *bzlenv.Environment) { 236 defer bzlenv.WalkOnceWithEnvironment(*expr, env, walk) 237 238 functionLoads, functionFindings := notLoadedFunctionUsageCheckInternal(expr, env, functions, loadFrom) 239 findings = append(findings, functionFindings...) 240 for _, load := range functionLoads { 241 toLoad[load] = true 242 } 243 244 symbolLoads, symbolFindings := notLoadedSymbolUsageCheckInternal(expr, env, symbols, loadFrom) 245 findings = append(findings, symbolFindings...) 246 for _, load := range symbolLoads { 247 toLoad[load] = true 248 } 249 } 250 var expr build.Expr = f 251 walk(&expr, bzlenv.NewEnvironment()) 252 253 if len(toLoad) == 0 { 254 return nil 255 } 256 257 loads := []string{} 258 for l := range toLoad { 259 loads = append(loads, l) 260 } 261 262 sort.Strings(loads) 263 replacement := insertLoad(f, loadFrom, loads) 264 if replacement != nil { 265 // Add the same replacement to all relevant findings. 266 for _, f := range findings { 267 f.Replacement = append(f.Replacement, *replacement) 268 } 269 } 270 271 return findings 272} 273 274// NotLoadedFunctionUsageCheck checks whether there's a usage of a given not imported function in the file 275// and adds a load statement if necessary. 276func NotLoadedFunctionUsageCheck(f *build.File, globals []string, loadFrom string) []*LinterFinding { 277 return notLoadedUsageCheck(f, globals, []string{}, loadFrom) 278} 279 280// makePositional makes the function argument positional (removes the keyword if it exists) 281func makePositional(argument build.Expr) build.Expr { 282 if binary, ok := argument.(*build.AssignExpr); ok { 283 return binary.RHS 284 } 285 return argument 286} 287 288// makeKeyword makes the function argument keyword (adds or edits the keyword name) 289func makeKeyword(argument build.Expr, name string) build.Expr { 290 assign, ok := argument.(*build.AssignExpr) 291 if !ok { 292 return &build.AssignExpr{ 293 LHS: &build.Ident{Name: name}, 294 Op: "=", 295 RHS: argument, 296 } 297 } 298 ident, ok := assign.LHS.(*build.Ident) 299 if ok && ident.Name == name { 300 // Nothing to change 301 return argument 302 } 303 304 // Technically it's possible that the LHS is not an ident, but that is a syntax error anyway. 305 newAssign := *assign 306 newAssign.LHS = &build.Ident{Name: name} 307 return &newAssign 308} 309 310func attrConfigurationWarning(f *build.File) []*LinterFinding { 311 if f.Type != build.TypeBzl { 312 return nil 313 } 314 315 var findings []*LinterFinding 316 build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) { 317 // Find nodes that match the following pattern: attr.xxxx(..., cfg = "data", ...) 318 call, ok := (*expr).(*build.CallExpr) 319 if !ok { 320 return 321 } 322 dot, ok := (call.X).(*build.DotExpr) 323 if !ok { 324 return 325 } 326 base, ok := dot.X.(*build.Ident) 327 if !ok || base.Name != "attr" { 328 return 329 } 330 i, _, param := getParam(call.List, "cfg") 331 if param == nil { 332 return 333 } 334 value, ok := (param.RHS).(*build.StringExpr) 335 if !ok || value.Value != "data" { 336 return 337 } 338 newCall := *call 339 newCall.List = append(newCall.List[:i], newCall.List[i+1:]...) 340 341 findings = append(findings, 342 makeLinterFinding(param, `cfg = "data" for attr definitions has no effect and should be removed.`, 343 LinterReplacement{expr, &newCall})) 344 }) 345 return findings 346} 347 348func depsetItemsWarning(f *build.File) []*LinterFinding { 349 var findings []*LinterFinding 350 351 types := detectTypes(f) 352 build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) { 353 call, ok := (*expr).(*build.CallExpr) 354 if !ok { 355 return 356 } 357 base, ok := call.X.(*build.Ident) 358 if !ok || base.Name != "depset" { 359 return 360 } 361 if len(call.List) == 0 { 362 return 363 } 364 _, _, param := getParam(call.List, "items") 365 if param != nil { 366 findings = append(findings, 367 makeLinterFinding(param, `Parameter "items" is deprecated, use "direct" and/or "transitive" instead.`)) 368 return 369 } 370 if _, ok := call.List[0].(*build.AssignExpr); ok { 371 return 372 } 373 // We have an unnamed first parameter. Check the type. 374 if types[call.List[0]] == Depset { 375 findings = append(findings, 376 makeLinterFinding(call.List[0], `Giving a depset as first unnamed parameter to depset() is deprecated, use the "transitive" parameter instead.`)) 377 } 378 }) 379 return findings 380} 381 382func attrNonEmptyWarning(f *build.File) []*LinterFinding { 383 if f.Type != build.TypeBzl { 384 return nil 385 } 386 387 var findings []*LinterFinding 388 build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) { 389 // Find nodes that match the following pattern: attr.xxxx(..., non_empty = ..., ...) 390 call, ok := (*expr).(*build.CallExpr) 391 if !ok { 392 return 393 } 394 dot, ok := (call.X).(*build.DotExpr) 395 if !ok { 396 return 397 } 398 base, ok := dot.X.(*build.Ident) 399 if !ok || base.Name != "attr" { 400 return 401 } 402 _, name, param := getParam(call.List, "non_empty") 403 if param == nil { 404 return 405 } 406 407 // Fix 408 newName := *name 409 newName.Name = "allow_empty" 410 negatedRHS := negateExpression(param.RHS) 411 412 findings = append(findings, 413 makeLinterFinding(param, "non_empty attributes for attr definitions are deprecated in favor of allow_empty.", 414 LinterReplacement{¶m.LHS, &newName}, 415 LinterReplacement{¶m.RHS, negatedRHS}, 416 )) 417 }) 418 return findings 419} 420 421func attrSingleFileWarning(f *build.File) []*LinterFinding { 422 if f.Type != build.TypeBzl { 423 return nil 424 } 425 426 var findings []*LinterFinding 427 build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) { 428 // Find nodes that match the following pattern: attr.xxxx(..., single_file = ..., ...) 429 call, ok := (*expr).(*build.CallExpr) 430 if !ok { 431 return 432 } 433 dot, ok := (call.X).(*build.DotExpr) 434 if !ok { 435 return 436 } 437 base, ok := dot.X.(*build.Ident) 438 if !ok || base.Name != "attr" { 439 return 440 } 441 singleFileIndex, singleFileKw, singleFileParam := getParam(call.List, "single_file") 442 if singleFileParam == nil { 443 return 444 } 445 446 // Fix 447 newCall := *call 448 newCall.List = append([]build.Expr{}, call.List...) 449 450 newSingleFileKw := *singleFileKw 451 newSingleFileKw.Name = "allow_single_file" 452 singleFileValue := singleFileParam.RHS 453 454 if boolean, ok := singleFileValue.(*build.Ident); ok && boolean.Name == "False" { 455 // if the value is `False`, just remove the whole parameter 456 newCall.List = append(newCall.List[:singleFileIndex], newCall.List[singleFileIndex+1:]...) 457 } else { 458 // search for `allow_files` parameter in the same attr definition and remove it 459 allowFileIndex, _, allowFilesParam := getParam(call.List, "allow_files") 460 if allowFilesParam != nil { 461 singleFileValue = allowFilesParam.RHS 462 newCall.List = append(newCall.List[:allowFileIndex], newCall.List[allowFileIndex+1:]...) 463 if singleFileIndex > allowFileIndex { 464 singleFileIndex-- 465 } 466 } 467 } 468 findings = append(findings, 469 makeLinterFinding(singleFileParam, "single_file is deprecated in favor of allow_single_file.", 470 LinterReplacement{expr, &newCall}, 471 LinterReplacement{&singleFileParam.LHS, &newSingleFileKw}, 472 LinterReplacement{&singleFileParam.RHS, singleFileValue}, 473 )) 474 }) 475 return findings 476} 477 478func ctxActionsWarning(f *build.File) []*LinterFinding { 479 if f.Type != build.TypeBzl { 480 return nil 481 } 482 483 var findings []*LinterFinding 484 build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) { 485 // Find nodes that match the following pattern: ctx.xxxx(...) 486 call, ok := (*expr).(*build.CallExpr) 487 if !ok { 488 return 489 } 490 dot, ok := (call.X).(*build.DotExpr) 491 if !ok { 492 return 493 } 494 base, ok := dot.X.(*build.Ident) 495 if !ok || base.Name != "ctx" { 496 return 497 } 498 499 switch dot.Name { 500 case "new_file", "experimental_new_directory", "file_action", "action", "empty_action", "template_action": 501 // fix 502 default: 503 return 504 } 505 506 // Fix 507 newCall := *call 508 newCall.List = append([]build.Expr{}, call.List...) 509 newDot := *dot 510 newCall.X = &newDot 511 512 switch dot.Name { 513 case "new_file": 514 if len(call.List) > 2 { 515 // Can't fix automatically because the new API doesn't support the 3 arguments signature 516 findings = append(findings, 517 makeLinterFinding(dot, fmt.Sprintf(`"ctx.new_file" is deprecated in favor of "ctx.actions.declare_file".`))) 518 return 519 } 520 newDot.Name = "actions.declare_file" 521 if len(call.List) == 2 { 522 // swap arguments: 523 // ctx.new_file(sibling, name) -> ctx.actions.declare_file(name, sibling=sibling) 524 newCall.List[0], newCall.List[1] = makePositional(call.List[1]), makeKeyword(call.List[0], "sibling") 525 } 526 case "experimental_new_directory": 527 newDot.Name = "actions.declare_directory" 528 case "file_action": 529 newDot.Name = "actions.write" 530 i, ident, param := getParam(newCall.List, "executable") 531 if ident != nil { 532 newIdent := *ident 533 newIdent.Name = "is_executable" 534 newParam := *param 535 newParam.LHS = &newIdent 536 newCall.List[i] = &newParam 537 } 538 case "action": 539 newDot.Name = "actions.run" 540 if _, _, command := getParam(call.List, "command"); command != nil { 541 newDot.Name = "actions.run_shell" 542 } 543 case "empty_action": 544 newDot.Name = "actions.do_nothing" 545 case "template_action": 546 newDot.Name = "actions.expand_template" 547 if i, ident, param := getParam(call.List, "executable"); ident != nil { 548 newIdent := *ident 549 newIdent.Name = "is_executable" 550 newParam := *param 551 newParam.LHS = &newIdent 552 newCall.List[i] = &newParam 553 } 554 } 555 556 findings = append(findings, makeLinterFinding(dot, 557 fmt.Sprintf(`"ctx.%s" is deprecated in favor of "ctx.%s".`, dot.Name, newDot.Name), 558 LinterReplacement{expr, &newCall})) 559 }) 560 return findings 561} 562 563func fileTypeWarning(f *build.File) []*LinterFinding { 564 if f.Type != build.TypeBzl { 565 return nil 566 } 567 568 var findings []*LinterFinding 569 var walk func(e *build.Expr, env *bzlenv.Environment) 570 walk = func(e *build.Expr, env *bzlenv.Environment) { 571 defer bzlenv.WalkOnceWithEnvironment(*e, env, walk) 572 573 call, ok := isFunctionCall(*e, "FileType") 574 if !ok { 575 return 576 } 577 if binding := env.Get("FileType"); binding == nil { 578 findings = append(findings, 579 makeLinterFinding(call, "The FileType function is deprecated.")) 580 } 581 } 582 var expr build.Expr = f 583 walk(&expr, bzlenv.NewEnvironment()) 584 585 return findings 586} 587 588func packageNameWarning(f *build.File) []*LinterFinding { 589 return globalVariableUsageCheck(f, "PACKAGE_NAME", "native.package_name()") 590} 591 592func repositoryNameWarning(f *build.File) []*LinterFinding { 593 return globalVariableUsageCheck(f, "REPOSITORY_NAME", "native.repository_name()") 594} 595 596func outputGroupWarning(f *build.File) []*LinterFinding { 597 if f.Type != build.TypeBzl { 598 return nil 599 } 600 601 var findings []*LinterFinding 602 build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) { 603 // Find nodes that match the following pattern: ctx.attr.xxx.output_group 604 outputGroup, ok := (*expr).(*build.DotExpr) 605 if !ok || outputGroup.Name != "output_group" { 606 return 607 } 608 dep, ok := (outputGroup.X).(*build.DotExpr) 609 if !ok { 610 return 611 } 612 attr, ok := (dep.X).(*build.DotExpr) 613 if !ok || attr.Name != "attr" { 614 return 615 } 616 ctx, ok := (attr.X).(*build.Ident) 617 if !ok || ctx.Name != "ctx" { 618 return 619 } 620 621 // Replace `xxx.output_group` with `xxx[OutputGroupInfo]` 622 findings = append(findings, 623 makeLinterFinding(outputGroup, 624 `"ctx.attr.dep.output_group" is deprecated in favor of "ctx.attr.dep[OutputGroupInfo]".`, 625 LinterReplacement{expr, &build.IndexExpr{ 626 X: dep, 627 Y: &build.Ident{Name: "OutputGroupInfo"}, 628 }, 629 })) 630 }) 631 return findings 632} 633 634func nativeGitRepositoryWarning(f *build.File) []*LinterFinding { 635 if f.Type != build.TypeBzl { 636 return nil 637 } 638 return NotLoadedFunctionUsageCheck(f, []string{"git_repository", "new_git_repository"}, "@bazel_tools//tools/build_defs/repo:git.bzl") 639} 640 641func nativeHTTPArchiveWarning(f *build.File) []*LinterFinding { 642 if f.Type != build.TypeBzl { 643 return nil 644 } 645 return NotLoadedFunctionUsageCheck(f, []string{"http_archive"}, "@bazel_tools//tools/build_defs/repo:http.bzl") 646} 647 648func nativeAndroidRulesWarning(f *build.File) []*LinterFinding { 649 if f.Type != build.TypeBzl && f.Type != build.TypeBuild { 650 return nil 651 } 652 return NotLoadedFunctionUsageCheck(f, tables.AndroidNativeRules, tables.AndroidLoadPath) 653} 654 655func nativeCcRulesWarning(f *build.File) []*LinterFinding { 656 if f.Type != build.TypeBzl && f.Type != build.TypeBuild { 657 return nil 658 } 659 return NotLoadedFunctionUsageCheck(f, tables.CcNativeRules, tables.CcLoadPath) 660} 661 662func nativeJavaRulesWarning(f *build.File) []*LinterFinding { 663 if f.Type != build.TypeBzl && f.Type != build.TypeBuild { 664 return nil 665 } 666 return NotLoadedFunctionUsageCheck(f, tables.JavaNativeRules, tables.JavaLoadPath) 667} 668 669func nativePyRulesWarning(f *build.File) []*LinterFinding { 670 if f.Type != build.TypeBzl && f.Type != build.TypeBuild { 671 return nil 672 } 673 return NotLoadedFunctionUsageCheck(f, tables.PyNativeRules, tables.PyLoadPath) 674} 675 676func nativeProtoRulesWarning(f *build.File) []*LinterFinding { 677 if f.Type != build.TypeBzl && f.Type != build.TypeBuild { 678 return nil 679 } 680 return notLoadedUsageCheck(f, tables.ProtoNativeRules, tables.ProtoNativeSymbols, tables.ProtoLoadPath) 681} 682 683func contextArgsAPIWarning(f *build.File) []*LinterFinding { 684 if f.Type != build.TypeBzl { 685 return nil 686 } 687 688 var findings []*LinterFinding 689 types := detectTypes(f) 690 691 build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) { 692 // Search for `<ctx.actions.args>.add()` nodes 693 call, ok := (*expr).(*build.CallExpr) 694 if !ok { 695 return 696 } 697 dot, ok := call.X.(*build.DotExpr) 698 if !ok || dot.Name != "add" || types[dot.X] != CtxActionsArgs { 699 return 700 } 701 702 // If neither before_each nor join_with nor map_fn is specified, the node is ok. 703 // Otherwise if join_with is specified, use `.add_joined` instead. 704 // Otherwise use `.add_all` instead. 705 706 _, beforeEachKw, beforeEach := getParam(call.List, "before_each") 707 _, _, joinWith := getParam(call.List, "join_with") 708 _, mapFnKw, mapFn := getParam(call.List, "map_fn") 709 if beforeEach == nil && joinWith == nil && mapFn == nil { 710 // No deprecated API detected 711 return 712 } 713 714 // Fix 715 var replacements []LinterReplacement 716 717 newDot := *dot 718 newDot.Name = "add_all" 719 replacements = append(replacements, LinterReplacement{&call.X, &newDot}) 720 721 if joinWith != nil { 722 newDot.Name = "add_joined" 723 if beforeEach != nil { 724 // `add_joined` doesn't have a `before_each` parameter, replace it with `format_each`: 725 // `before_each = foo` -> `format_each = foo + "%s"` 726 newBeforeEachKw := *beforeEachKw 727 newBeforeEachKw.Name = "format_each" 728 729 replacements = append(replacements, LinterReplacement{&beforeEach.LHS, &newBeforeEachKw}) 730 replacements = append(replacements, LinterReplacement{&beforeEach.RHS, &build.BinaryExpr{ 731 X: beforeEach.RHS, 732 Op: "+", 733 Y: &build.StringExpr{Value: "%s"}, 734 }}) 735 } 736 } 737 if mapFnKw != nil { 738 // Replace `map_fn = ...` with `map_each = ...` 739 newMapFnKw := *mapFnKw 740 newMapFnKw.Name = "map_each" 741 replacements = append(replacements, LinterReplacement{&mapFn.LHS, &newMapFnKw}) 742 } 743 744 findings = append(findings, 745 makeLinterFinding(call, 746 `"ctx.actions.args().add()" for multiple arguments is deprecated in favor of "add_all()" or "add_joined()".`, 747 replacements...)) 748 749 }) 750 return findings 751} 752 753func attrOutputDefaultWarning(f *build.File) []*LinterFinding { 754 if f.Type != build.TypeBzl { 755 return nil 756 } 757 758 var findings []*LinterFinding 759 build.Walk(f, func(expr build.Expr, stack []build.Expr) { 760 // Find nodes that match the following pattern: attr.output(..., default = ...) 761 call, ok := expr.(*build.CallExpr) 762 if !ok { 763 return 764 } 765 dot, ok := (call.X).(*build.DotExpr) 766 if !ok || dot.Name != "output" { 767 return 768 } 769 base, ok := dot.X.(*build.Ident) 770 if !ok || base.Name != "attr" { 771 return 772 } 773 _, _, param := getParam(call.List, "default") 774 if param == nil { 775 return 776 } 777 findings = append(findings, 778 makeLinterFinding(param, `The "default" parameter for attr.output() is deprecated.`)) 779 }) 780 return findings 781} 782 783func attrLicenseWarning(f *build.File) []*LinterFinding { 784 if f.Type != build.TypeBzl { 785 return nil 786 } 787 788 var findings []*LinterFinding 789 build.Walk(f, func(expr build.Expr, stack []build.Expr) { 790 // Find nodes that match the following pattern: attr.license(...) 791 call, ok := expr.(*build.CallExpr) 792 if !ok { 793 return 794 } 795 dot, ok := (call.X).(*build.DotExpr) 796 if !ok || dot.Name != "license" { 797 return 798 } 799 base, ok := dot.X.(*build.Ident) 800 if !ok || base.Name != "attr" { 801 return 802 } 803 findings = append(findings, 804 makeLinterFinding(expr, `"attr.license()" is deprecated and shouldn't be used.`)) 805 }) 806 return findings 807} 808 809// ruleImplReturnWarning checks whether a rule implementation function returns an old-style struct 810func ruleImplReturnWarning(f *build.File) []*LinterFinding { 811 if f.Type != build.TypeBzl { 812 return nil 813 } 814 815 var findings []*LinterFinding 816 817 // iterate over rules and collect rule implementation function names 818 implNames := make(map[string]bool) 819 build.Walk(f, func(expr build.Expr, stack []build.Expr) { 820 call, ok := isFunctionCall(expr, "rule") 821 if !ok { 822 return 823 } 824 825 // Try to get the implementaton parameter either by name or as the first argument 826 var impl build.Expr 827 _, _, param := getParam(call.List, "implementation") 828 if param != nil { 829 impl = param.RHS 830 } else if len(call.List) > 0 { 831 impl = call.List[0] 832 } 833 if name, ok := impl.(*build.Ident); ok { 834 implNames[name.Name] = true 835 } 836 }) 837 838 // iterate over functions 839 for _, stmt := range f.Stmt { 840 def, ok := stmt.(*build.DefStmt) 841 if !ok || !implNames[def.Name] { 842 // either not a function or not used in the file as a rule implementation function 843 continue 844 } 845 // traverse the function and find all of its return statements 846 build.Walk(def, func(expr build.Expr, stack []build.Expr) { 847 ret, ok := expr.(*build.ReturnStmt) 848 if !ok { 849 return 850 } 851 // check whether it returns a struct 852 if _, ok := isFunctionCall(ret.Result, "struct"); ok { 853 findings = append(findings, makeLinterFinding(ret, `Avoid using the legacy provider syntax.`)) 854 } 855 }) 856 } 857 858 return findings 859} 860 861type signature struct { 862 Positional []string // These parameters are typePositional-only 863 Keyword []string // These parameters are typeKeyword-only 864} 865 866var signatures = map[string]signature{ 867 "all": {[]string{"elements"}, []string{}}, 868 "any": {[]string{"elements"}, []string{}}, 869 "tuple": {[]string{"x"}, []string{}}, 870 "list": {[]string{"x"}, []string{}}, 871 "len": {[]string{"x"}, []string{}}, 872 "str": {[]string{"x"}, []string{}}, 873 "repr": {[]string{"x"}, []string{}}, 874 "bool": {[]string{"x"}, []string{}}, 875 "int": {[]string{"x"}, []string{}}, 876 "dir": {[]string{"x"}, []string{}}, 877 "type": {[]string{"x"}, []string{}}, 878 "hasattr": {[]string{"x", "name"}, []string{}}, 879 "getattr": {[]string{"x", "name", "default"}, []string{}}, 880 "select": {[]string{"x"}, []string{}}, 881} 882 883// functionName returns the name of the given function if it's a direct function call (e.g. 884// `foo(...)` or `native.foo(...)`, but not `foo.bar(...)` or `x[3](...)` 885func functionName(call *build.CallExpr) (string, bool) { 886 if ident, ok := call.X.(*build.Ident); ok { 887 return ident.Name, true 888 } 889 // Also check for `native.<name>` 890 dot, ok := call.X.(*build.DotExpr) 891 if !ok { 892 return "", false 893 } 894 if ident, ok := dot.X.(*build.Ident); !ok || ident.Name != "native" { 895 return "", false 896 } 897 return dot.Name, true 898} 899 900const ( 901 typePositional int = iota 902 typeKeyword 903 typeArgs 904 typeKwargs 905) 906 907// paramType returns the type of the param. If it's a typeKeyword param, also returns its name 908func paramType(param build.Expr) (int, string) { 909 switch param := param.(type) { 910 case *build.AssignExpr: 911 if param.Op == "=" { 912 ident, ok := param.LHS.(*build.Ident) 913 if ok { 914 return typeKeyword, ident.Name 915 } 916 return typeKeyword, "" 917 } 918 case *build.UnaryExpr: 919 switch param.Op { 920 case "*": 921 return typeArgs, "" 922 case "**": 923 return typeKwargs, "" 924 } 925 } 926 return typePositional, "" 927} 928 929// keywordPositionalParametersWarning checks for deprecated typeKeyword parameters of builtins 930func keywordPositionalParametersWarning(f *build.File) []*LinterFinding { 931 var findings []*LinterFinding 932 933 // Check for legacy typeKeyword parameters 934 build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) { 935 call, ok := (*expr).(*build.CallExpr) 936 if !ok || len(call.List) == 0 { 937 return 938 } 939 function, ok := functionName(call) 940 if !ok { 941 return 942 } 943 944 // Findings and replacements for the current call expression 945 var callFindings []*LinterFinding 946 var callReplacements []LinterReplacement 947 948 signature, ok := signatures[function] 949 if !ok { 950 return 951 } 952 953 var paramTypes []int // types of the parameters (typeKeyword or not) after the replacements has been applied. 954 for i, parameter := range call.List { 955 pType, name := paramType(parameter) 956 paramTypes = append(paramTypes, pType) 957 958 if pType == typeKeyword && i < len(signature.Positional) && signature.Positional[i] == name { 959 // The parameter should be typePositional 960 callFindings = append(callFindings, makeLinterFinding( 961 parameter, 962 fmt.Sprintf(`Keyword parameter %q for %q should be positional.`, signature.Positional[i], function), 963 )) 964 callReplacements = append(callReplacements, LinterReplacement{&call.List[i], makePositional(parameter)}) 965 paramTypes[i] = typePositional 966 } 967 968 if pType == typePositional && i >= len(signature.Positional) && i < len(signature.Positional)+len(signature.Keyword) { 969 // The parameter should be typeKeyword 970 keyword := signature.Keyword[i-len(signature.Positional)] 971 callFindings = append(callFindings, makeLinterFinding( 972 parameter, 973 fmt.Sprintf(`Parameter at the position %d for %q should be keyword (%s = ...).`, i+1, function, keyword), 974 )) 975 callReplacements = append(callReplacements, LinterReplacement{&call.List[i], makeKeyword(parameter, keyword)}) 976 paramTypes[i] = typeKeyword 977 } 978 } 979 980 if len(callFindings) == 0 { 981 return 982 } 983 984 // Only apply the replacements if the signature is correct after they have been applied 985 // (i.e. the order of the parameters is typePositional, typeKeyword, typeArgs, typeKwargs) 986 // Otherwise the signature will be not correct, probably it was incorrect initially. 987 // All the replacements should be applied to the first finding for the current node. 988 989 if sort.IntsAreSorted(paramTypes) { 990 // It's possible that the parameter list had `ForceCompact` set to true because it only contained 991 // positional arguments, and now it has keyword arguments as well. Reset the flag to let the 992 // printer decide how the function call should be formatted. 993 for _, t := range paramTypes { 994 if t == typeKeyword { 995 // There's at least one keyword argument 996 newCall := *call 997 newCall.ForceCompact = false 998 callFindings[0].Replacement = append(callFindings[0].Replacement, LinterReplacement{expr, &newCall}) 999 break 1000 } 1001 } 1002 // Attach all the parameter replacements to the first finding 1003 callFindings[0].Replacement = append(callFindings[0].Replacement, callReplacements...) 1004 } 1005 1006 findings = append(findings, callFindings...) 1007 }) 1008 1009 return findings 1010} 1011