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