in warn/warn_control_flow.go [435:547]
func findUninitializedVariables(stmts []build.Expr, previouslyInitialized map[string]bool, callback func(*build.Ident)) (bool, map[string]bool) {
// Variables that are guaranteed to be initialized
locallyInitialized := make(map[string]bool) // in the local block of `stmts`
initialized := make(map[string]bool) // anywhere before the current line
for key := range previouslyInitialized {
initialized[key] = true
}
// findUninitializedIdents traverses an expression (simple statement or a part of it), and calls
// `callback` on every *build.Ident that's not mentioned in the map of initialized variables
findUninitializedIdents := func(expr build.Expr, callback func(ident *build.Ident)) {
// Collect lValues, they shouldn't be taken into account
// For example, if the expression is `a = foo(b = c)`, only `c` can be an unused variable here.
lValues := make(map[*build.Ident]bool)
build.Walk(expr, func(expr build.Expr, stack []build.Expr) {
if as, ok := expr.(*build.AssignExpr); ok {
for _, ident := range bzlenv.CollectLValues(as.LHS) {
lValues[ident] = true
}
}
})
build.Walk(expr, func(expr build.Expr, stack []build.Expr) {
// TODO: traverse comprehensions properly
for _, node := range stack {
if _, ok := node.(*build.Comprehension); ok {
return
}
}
if ident, ok := expr.(*build.Ident); ok && !initialized[ident.Name] && !lValues[ident] {
callback(ident)
}
})
}
for _, stmt := range stmts {
newlyDefinedVariables := make(map[string]bool)
switch stmt := stmt.(type) {
case *build.DefStmt:
// Don't traverse nested functions
case *build.CallExpr:
if _, ok := isFunctionCall(stmt, "fail"); ok {
return true, locallyInitialized
}
case *build.ReturnStmt:
findUninitializedIdents(stmt, callback)
return true, locallyInitialized
case *build.BranchStmt:
if stmt.Token == "break" || stmt.Token == "continue" {
return true, locallyInitialized
}
case *build.ForStmt:
// Although loop variables are defined as local variables, buildifier doesn't know whether
// the collection will be empty or not.
// Traverse but ignore the result. Even if something is defined inside a for-loop, the loop
// may be empty and the variable initialization may not happen.
findUninitializedIdents(stmt.X, callback)
// The loop can access the variables defined above, and also the for-loop variables.
initializedInLoop := make(map[string]bool)
for name := range initialized {
initializedInLoop[name] = true
}
for _, ident := range bzlenv.CollectLValues(stmt.Vars) {
initializedInLoop[ident.Name] = true
}
findUninitializedVariables(stmt.Body, initializedInLoop, callback)
continue
case *build.IfStmt:
findUninitializedIdents(stmt.Cond, callback)
// Check the variables defined in the if- and else-clauses.
terminatedTrue, definedInTrue := findUninitializedVariables(stmt.True, initialized, callback)
terminatedFalse, definedInFalse := findUninitializedVariables(stmt.False, initialized, callback)
if terminatedTrue && terminatedFalse {
return true, locallyInitialized
} else if terminatedTrue {
// Only take definedInFalse into account
for key := range definedInFalse {
locallyInitialized[key] = true
initialized[key] = true
}
} else if terminatedFalse {
// Only take definedInTrue into account
for key := range definedInTrue {
locallyInitialized[key] = true
initialized[key] = true
}
} else {
// If a variable is defined in both if- and else-clauses, it's considered as defined
for key := range definedInTrue {
if definedInFalse[key] {
locallyInitialized[key] = true
initialized[key] = true
}
}
}
continue
case *build.AssignExpr:
// Assignment expression. Collect all definitions from the lhs
for _, ident := range bzlenv.CollectLValues(stmt.LHS) {
newlyDefinedVariables[ident.Name] = true
}
}
findUninitializedIdents(stmt, callback)
for name := range newlyDefinedVariables {
locallyInitialized[name] = true
initialized[name] = true
}
}
return false, locallyInitialized
}