1// Warnings related to the control flow 2 3package warn 4 5import ( 6 "fmt" 7 "github.com/bazelbuild/buildtools/build" 8 "github.com/bazelbuild/buildtools/bzlenv" 9 "github.com/bazelbuild/buildtools/edit" 10) 11 12// findReturnsWithoutValue searches for return statements without a value, calls `callback` on 13// them and returns whether the current list of statements terminates (either by a return or fail() 14// statements on the current level in all subranches. 15func findReturnsWithoutValue(stmts []build.Expr, callback func(*build.ReturnStmt)) bool { 16 if len(stmts) == 0 { 17 // May occur in empty else-clauses 18 return false 19 } 20 terminated := false 21 for _, stmt := range stmts { 22 switch stmt := stmt.(type) { 23 case *build.ReturnStmt: 24 if stmt.Result == nil { 25 callback(stmt) 26 } 27 terminated = true 28 case *build.CallExpr: 29 ident, ok := stmt.X.(*build.Ident) 30 if ok && ident.Name == "fail" { 31 terminated = true 32 } 33 case *build.ForStmt: 34 // Call recursively to find all return statements without a value there. 35 // Even if a for-loop is guaranteed to terminate in each iteration, buildifier still can't 36 // check whether the loop is not empty, so we can't say that the statement after the ForStmt 37 // is unreachable. 38 findReturnsWithoutValue(stmt.Body, callback) 39 case *build.IfStmt: 40 // Save to separate values to avoid short circuit evaluation 41 term1 := findReturnsWithoutValue(stmt.True, callback) 42 term2 := findReturnsWithoutValue(stmt.False, callback) 43 if term1 && term2 { 44 terminated = true 45 } 46 } 47 } 48 return terminated 49} 50 51// missingReturnValueWarning warns if a function returns both explicit and implicit values. 52func missingReturnValueWarning(f *build.File) []*LinterFinding { 53 findings := []*LinterFinding{} 54 55 for _, stmt := range f.Stmt { 56 function, ok := stmt.(*build.DefStmt) 57 if !ok { 58 continue 59 } 60 61 var hasNonEmptyReturns bool 62 build.Walk(function, func(expr build.Expr, stack []build.Expr) { 63 if ret, ok := expr.(*build.ReturnStmt); ok && ret.Result != nil { 64 hasNonEmptyReturns = true 65 } 66 }) 67 68 if !hasNonEmptyReturns { 69 continue 70 } 71 explicitReturn := findReturnsWithoutValue(function.Body, func(ret *build.ReturnStmt) { 72 findings = append(findings, 73 makeLinterFinding(ret, fmt.Sprintf("Some but not all execution paths of %q return a value.", function.Name))) 74 }) 75 if !explicitReturn { 76 findings = append(findings, 77 makeLinterFinding(function, fmt.Sprintf(`Some but not all execution paths of %q return a value. 78The function may terminate by an implicit return in the end.`, function.Name))) 79 } 80 } 81 return findings 82} 83 84// findUnreachableStatements searches for unreachable statements (i.e. statements that immediately 85// follow `return`, `break`, `continue`, and `fail()` statements and calls `callback` on them. 86// If there are several consequent unreachable statements, it only reports the first of them. 87// Returns whether the execution is terminated explicitly. 88func findUnreachableStatements(stmts []build.Expr, callback func(build.Expr)) bool { 89 unreachable := false 90 for _, stmt := range stmts { 91 if unreachable { 92 callback(stmt) 93 return true 94 } 95 switch stmt := stmt.(type) { 96 case *build.ReturnStmt: 97 unreachable = true 98 case *build.CallExpr: 99 ident, ok := stmt.X.(*build.Ident) 100 if ok && ident.Name == "fail" { 101 unreachable = true 102 } 103 case *build.BranchStmt: 104 if stmt.Token != "pass" { 105 // either break or continue 106 unreachable = true 107 } 108 case *build.ForStmt: 109 findUnreachableStatements(stmt.Body, callback) 110 case *build.IfStmt: 111 // Save to separate values to avoid short circuit evaluation 112 term1 := findUnreachableStatements(stmt.True, callback) 113 term2 := findUnreachableStatements(stmt.False, callback) 114 if term1 && term2 { 115 unreachable = true 116 } 117 } 118 } 119 return unreachable 120} 121 122func unreachableStatementWarning(f *build.File) []*LinterFinding { 123 findings := []*LinterFinding{} 124 125 for _, stmt := range f.Stmt { 126 function, ok := stmt.(*build.DefStmt) 127 if !ok { 128 continue 129 } 130 131 findUnreachableStatements(function.Body, func(expr build.Expr) { 132 findings = append(findings, 133 makeLinterFinding(expr, `The statement is unreachable.`)) 134 }) 135 } 136 return findings 137} 138 139func noEffectStatementsCheck(body []build.Expr, isTopLevel, isFunc bool, findings []*LinterFinding) []*LinterFinding { 140 seenNonComment := false 141 for _, stmt := range body { 142 if stmt == nil { 143 continue 144 } 145 146 _, isString := stmt.(*build.StringExpr) 147 if isString { 148 if !seenNonComment && (isTopLevel || isFunc) { 149 // It's a docstring. 150 seenNonComment = true 151 continue 152 } 153 } 154 if _, ok := stmt.(*build.CommentBlock); !ok { 155 seenNonComment = true 156 } 157 switch s := (stmt).(type) { 158 case *build.DefStmt, *build.ForStmt, *build.IfStmt, *build.LoadStmt, *build.ReturnStmt, 159 *build.CallExpr, *build.CommentBlock, *build.BranchStmt, *build.AssignExpr: 160 continue 161 case *build.Comprehension: 162 if !isTopLevel || s.Curly { 163 // List comprehensions are allowed on top-level. 164 findings = append(findings, 165 makeLinterFinding(stmt, "Expression result is not used. Use a for-loop instead of a list comprehension.")) 166 } 167 continue 168 } 169 170 msg := "Expression result is not used." 171 if isString { 172 msg += " Docstrings should be the first statements of a file or a function (they may follow comment lines)." 173 } 174 findings = append(findings, makeLinterFinding(stmt, msg)) 175 } 176 return findings 177} 178 179func noEffectWarning(f *build.File) []*LinterFinding { 180 findings := []*LinterFinding{} 181 findings = noEffectStatementsCheck(f.Stmt, true, false, findings) 182 build.Walk(f, func(expr build.Expr, stack []build.Expr) { 183 // The AST should have a ExprStmt node. 184 // Since we don't have that, we match on the nodes that contain a block to get the list of statements. 185 switch expr := expr.(type) { 186 case *build.ForStmt: 187 findings = noEffectStatementsCheck(expr.Body, false, false, findings) 188 case *build.DefStmt: 189 findings = noEffectStatementsCheck(expr.Function.Body, false, true, findings) 190 case *build.IfStmt: 191 findings = noEffectStatementsCheck(expr.True, false, false, findings) 192 findings = noEffectStatementsCheck(expr.False, false, false, findings) 193 } 194 }) 195 return findings 196} 197 198// unusedVariableCheck checks for unused variables inside a given node `stmt` (either *build.File or 199// *build.DefStmt) and reports unused and already defined variables. 200func unusedVariableCheck(f *build.File, stmts []build.Expr, findings []*LinterFinding) []*LinterFinding { 201 if f.Type == build.TypeDefault || f.Type == build.TypeBzl { 202 // Not applicable to .bzl files, unused symbols may be loaded and used in other files. 203 return findings 204 } 205 usedSymbols := make(map[string]bool) 206 207 for _, stmt := range stmts { 208 for key := range edit.UsedSymbols(stmt) { 209 usedSymbols[key] = true 210 } 211 } 212 213 for _, s := range stmts { 214 if defStmt, ok := s.(*build.DefStmt); ok { 215 findings = unusedVariableCheck(f, defStmt.Body, findings) 216 continue 217 } 218 219 // look for all assignments in the scope 220 as, ok := s.(*build.AssignExpr) 221 if !ok { 222 continue 223 } 224 left, ok := as.LHS.(*build.Ident) 225 if !ok { 226 continue 227 } 228 if usedSymbols[left.Name] { 229 continue 230 } 231 if edit.ContainsComments(s, "@unused") { 232 // To disable the warning, put a comment that contains '@unused' 233 continue 234 } 235 findings = append(findings, 236 makeLinterFinding(as.LHS, fmt.Sprintf(`Variable %q is unused. Please remove it. 237To disable the warning, add '@unused' in a comment.`, left.Name))) 238 } 239 return findings 240} 241 242func unusedVariableWarning(f *build.File) []*LinterFinding { 243 return unusedVariableCheck(f, f.Stmt, []*LinterFinding{}) 244} 245 246func redefinedVariableWarning(f *build.File) []*LinterFinding { 247 findings := []*LinterFinding{} 248 definedSymbols := make(map[string]bool) 249 250 for _, s := range f.Stmt { 251 // look for all assignments in the scope 252 as, ok := s.(*build.AssignExpr) 253 if !ok { 254 continue 255 } 256 left, ok := as.LHS.(*build.Ident) 257 if !ok { 258 continue 259 } 260 if definedSymbols[left.Name] { 261 findings = append(findings, 262 makeLinterFinding(as.LHS, fmt.Sprintf(`Variable %q has already been defined. 263Redefining a global value is discouraged and will be forbidden in the future. 264Consider using a new variable instead.`, left.Name))) 265 continue 266 } 267 definedSymbols[left.Name] = true 268 } 269 return findings 270} 271 272func unusedLoadWarning(f *build.File) []*LinterFinding { 273 findings := []*LinterFinding{} 274 loaded := make(map[string]struct { 275 label, from string 276 line int 277 }) 278 279 symbols := edit.UsedSymbols(f) 280 for stmtIndex := 0; stmtIndex < len(f.Stmt); stmtIndex++ { 281 originalLoad, ok := f.Stmt[stmtIndex].(*build.LoadStmt) 282 if !ok { 283 continue 284 } 285 286 // Findings related to the current load statement 287 loadFindings := []*LinterFinding{} 288 289 // Copy the `load` object to provide a replacement if needed 290 load := *originalLoad 291 load.From = append([]*build.Ident{}, load.From...) 292 load.To = append([]*build.Ident{}, load.To...) 293 294 for i := 0; i < len(load.To); i++ { 295 from := load.From[i] 296 to := load.To[i] 297 // Check if the symbol was already loaded 298 origin, alreadyLoaded := loaded[to.Name] 299 start, _ := from.Span() 300 loaded[to.Name] = struct { 301 label, from string 302 line int 303 }{load.Module.Token, from.Name, start.Line} 304 305 if alreadyLoaded { 306 // The same symbol has already been loaded earlier 307 if origin.label == load.Module.Token && origin.from == from.Name { 308 // Only fix if it's loaded from the same label and variable 309 load.To = append(load.To[:i], load.To[i+1:]...) 310 load.From = append(load.From[:i], load.From[i+1:]...) 311 i-- 312 } 313 314 loadFindings = append(loadFindings, makeLinterFinding(to, 315 fmt.Sprintf("Symbol %q has already been loaded on line %d. Please remove it.", to.Name, origin.line))) 316 continue 317 } 318 _, ok := symbols[to.Name] 319 if !ok && !edit.ContainsComments(originalLoad, "@unused") && !edit.ContainsComments(to, "@unused") && !edit.ContainsComments(from, "@unused") { 320 // The loaded symbol is not used and is not protected by a special "@unused" comment 321 load.To = append(load.To[:i], load.To[i+1:]...) 322 load.From = append(load.From[:i], load.From[i+1:]...) 323 i-- 324 325 loadFindings = append(loadFindings, makeLinterFinding(to, 326 fmt.Sprintf("Loaded symbol %q is unused. Please remove it.\nTo disable the warning, add '@unused' in a comment.", to.Name))) 327 if f.Type == build.TypeDefault || f.Type == build.TypeBzl { 328 loadFindings[len(loadFindings)-1].Message += fmt.Sprintf(` 329If you want to re-export a symbol, use the following pattern: 330 331 load(..., _%s = %q, ...) 332 %s = _%s 333`, to.Name, from.Name, to.Name, to.Name) 334 } 335 } 336 } 337 338 if len(loadFindings) == 0 { 339 // No problems with the current load statement 340 continue 341 } 342 343 build.SortLoadArgs(&load) 344 var newStmt build.Expr = &load 345 if len(load.To) == 0 { 346 // If there are no loaded symbols left remove the entire load statement 347 newStmt = nil 348 } 349 replacement := LinterReplacement{&f.Stmt[stmtIndex], newStmt} 350 351 // Individual replacements can't be combined together: assume we need to remove both loaded 352 // symbols from 353 // 354 // load(":foo.bzl", "a", "b") 355 // 356 // Individual replacements are just to remove each of the symbols, but if these replacements 357 // are applied together, the result will be incorrect and a syntax error in Bazel: 358 // 359 // load(":foo.bzl") 360 // 361 // A workaround is to attach the full replacement to the first finding. 362 loadFindings[0].Replacement = []LinterReplacement{replacement} 363 findings = append(findings, loadFindings...) 364 } 365 return findings 366} 367 368// collectLocalVariables traverses statements (e.g. of a function definition) and returns a list 369// of idents for variables defined anywhere inside the function. 370func collectLocalVariables(stmts []build.Expr) []*build.Ident { 371 variables := []*build.Ident{} 372 373 for _, stmt := range stmts { 374 switch stmt := stmt.(type) { 375 case *build.DefStmt: 376 // Don't traverse nested functions 377 case *build.ForStmt: 378 variables = append(variables, bzlenv.CollectLValues(stmt.Vars)...) 379 variables = append(variables, collectLocalVariables(stmt.Body)...) 380 case *build.IfStmt: 381 variables = append(variables, collectLocalVariables(stmt.True)...) 382 variables = append(variables, collectLocalVariables(stmt.False)...) 383 case *build.AssignExpr: 384 variables = append(variables, bzlenv.CollectLValues(stmt.LHS)...) 385 } 386 } 387 return variables 388} 389 390// searchUninitializedVariables takes a list of statements (e.g. body of a block statement) 391// and a map of previously initialized statements, and calls `callback` on all idents that are not 392// initialized. An ident is considered initialized if it's initialized by every possible execution 393// path (before or by `stmts`). 394// Returns a boolean indicating whether the current list of statements is guaranteed to be 395// terminated explicitly (by return or fail() statements) and a map of variables that are guaranteed 396// to be defined by `stmts`. 397func findUninitializedVariables(stmts []build.Expr, previouslyInitialized map[string]bool, callback func(*build.Ident)) (bool, map[string]bool) { 398 // Variables that are guaranteed to be de initialized 399 locallyInitialized := make(map[string]bool) // in the local block of `stmts` 400 initialized := make(map[string]bool) // anywhere before the current line 401 for key := range previouslyInitialized { 402 initialized[key] = true 403 } 404 405 // findUninitializedIdents traverses an expression (simple statement or a part of it), and calls 406 // `callback` on every *build.Ident that's not mentioned in the map of initialized variables 407 findUninitializedIdents := func(expr build.Expr, callback func(ident *build.Ident)) { 408 // Collect lValues, they shouldn't be taken into account 409 // For example, if the expression is `a = foo(b = c)`, only `c` can be an unused variable here. 410 lValues := make(map[*build.Ident]bool) 411 build.Walk(expr, func(expr build.Expr, stack []build.Expr) { 412 if as, ok := expr.(*build.AssignExpr); ok { 413 for _, ident := range bzlenv.CollectLValues(as.LHS) { 414 lValues[ident] = true 415 } 416 } 417 }) 418 419 build.Walk(expr, func(expr build.Expr, stack []build.Expr) { 420 // TODO: traverse comprehensions properly 421 for _, node := range stack { 422 if _, ok := node.(*build.Comprehension); ok { 423 return 424 } 425 } 426 427 if ident, ok := expr.(*build.Ident); ok && !initialized[ident.Name] && !lValues[ident] { 428 callback(ident) 429 } 430 }) 431 } 432 433 for _, stmt := range stmts { 434 newlyDefinedVariables := make(map[string]bool) 435 switch stmt := stmt.(type) { 436 case *build.DefStmt: 437 // Don't traverse nested functions 438 case *build.CallExpr: 439 if _, ok := isFunctionCall(stmt, "fail"); ok { 440 return true, locallyInitialized 441 } 442 case *build.ReturnStmt: 443 findUninitializedIdents(stmt, callback) 444 return true, locallyInitialized 445 case *build.ForStmt: 446 // Although loop variables are defined as local variables, buildifier doesn't know whether 447 // the collection will be empty or not. 448 449 // Traverse but ignore the result. Even if something is defined inside a for-loop, the loop 450 // may be empty and the variable initialization may not happen. 451 findUninitializedIdents(stmt.X, callback) 452 453 // The loop can access the variables defined above, and also the for-loop variables. 454 initializedInLoop := make(map[string]bool) 455 for name := range initialized { 456 initializedInLoop[name] = true 457 } 458 for _, ident := range bzlenv.CollectLValues(stmt.Vars) { 459 initializedInLoop[ident.Name] = true 460 } 461 findUninitializedVariables(stmt.Body, initializedInLoop, callback) 462 continue 463 case *build.IfStmt: 464 findUninitializedIdents(stmt.Cond, callback) 465 // Check the variables defined in the if- and else-clauses. 466 terminatedTrue, definedInTrue := findUninitializedVariables(stmt.True, initialized, callback) 467 terminatedFalse, definedInFalse := findUninitializedVariables(stmt.False, initialized, callback) 468 if terminatedTrue && terminatedFalse { 469 return true, locallyInitialized 470 } else if terminatedTrue { 471 // Only take definedInFalse into account 472 for key := range definedInFalse { 473 locallyInitialized[key] = true 474 initialized[key] = true 475 } 476 } else if terminatedFalse { 477 // Only take definedInTrue into account 478 for key := range definedInTrue { 479 locallyInitialized[key] = true 480 initialized[key] = true 481 } 482 } else { 483 // If a variable is defined in both if- and else-clauses, it's considered as defined 484 for key := range definedInTrue { 485 if definedInFalse[key] { 486 locallyInitialized[key] = true 487 initialized[key] = true 488 } 489 } 490 } 491 continue 492 case *build.AssignExpr: 493 // Assignment expression. Collect all definitions from the lhs 494 for _, ident := range bzlenv.CollectLValues(stmt.LHS) { 495 newlyDefinedVariables[ident.Name] = true 496 } 497 } 498 findUninitializedIdents(stmt, callback) 499 for name := range newlyDefinedVariables { 500 locallyInitialized[name] = true 501 initialized[name] = true 502 } 503 } 504 return false, locallyInitialized 505} 506 507func getFunctionParams(def *build.DefStmt) []*build.Ident { 508 params := []*build.Ident{} 509 for _, node := range def.Params { 510 switch node := node.(type) { 511 case *build.Ident: 512 params = append(params, node) 513 case *build.UnaryExpr: 514 // either *args or **kwargs 515 if ident, ok := node.X.(*build.Ident); ok { 516 params = append(params, ident) 517 } 518 case *build.AssignExpr: 519 // x = value 520 if ident, ok := node.LHS.(*build.Ident); ok { 521 params = append(params, ident) 522 } 523 } 524 } 525 return params 526} 527 528// uninitializedVariableWarning warns about usages of values that may not have been initialized. 529func uninitializedVariableWarning(f *build.File) []*LinterFinding { 530 findings := []*LinterFinding{} 531 for _, stmt := range f.Stmt { 532 def, ok := stmt.(*build.DefStmt) 533 if !ok { 534 continue 535 } 536 537 // Get all variables defined in the function body. 538 // If a variable is not defined there, it can be builtin, global, or loaded. 539 localVars := make(map[string]bool) 540 for _, ident := range collectLocalVariables(def.Body) { 541 localVars[ident.Name] = true 542 } 543 544 // Function parameters are guaranteed to be defined everywhere in the function, even if they 545 // are redefined inside the function body. They shouldn't be taken into consideration. 546 for _, ident := range getFunctionParams(def) { 547 delete(localVars, ident.Name) 548 } 549 550 // Search for all potentially initialized variables in the function body 551 findUninitializedVariables(def.Body, make(map[string]bool), func(ident *build.Ident) { 552 // Check that the found ident represents a local variable 553 if localVars[ident.Name] { 554 findings = append(findings, 555 makeLinterFinding(ident, fmt.Sprintf(`Variable "%s" may not have been initialized.`, ident.Name))) 556 } 557 }) 558 } 559 return findings 560} 561