1
16
17
18
19 package warn
20
21 import (
22 "fmt"
23 "strings"
24
25 "github.com/bazelbuild/buildtools/build"
26 "github.com/bazelbuild/buildtools/bzlenv"
27 "github.com/bazelbuild/buildtools/edit"
28 )
29
30
31
32
33 func findReturnsWithoutValue(stmts []build.Expr, callback func(*build.ReturnStmt)) bool {
34 if len(stmts) == 0 {
35
36 return false
37 }
38 terminated := false
39 for _, stmt := range stmts {
40 switch stmt := stmt.(type) {
41 case *build.ReturnStmt:
42 if stmt.Result == nil {
43 callback(stmt)
44 }
45 terminated = true
46 case *build.CallExpr:
47 ident, ok := stmt.X.(*build.Ident)
48 if ok && ident.Name == "fail" {
49 terminated = true
50 }
51 case *build.ForStmt:
52
53
54
55
56 findReturnsWithoutValue(stmt.Body, callback)
57 case *build.IfStmt:
58
59 term1 := findReturnsWithoutValue(stmt.True, callback)
60 term2 := findReturnsWithoutValue(stmt.False, callback)
61 if term1 && term2 {
62 terminated = true
63 }
64 }
65 }
66 return terminated
67 }
68
69
70 func missingReturnValueWarning(f *build.File) []*LinterFinding {
71 findings := []*LinterFinding{}
72
73
74 defStmts := []*build.DefStmt{}
75 build.WalkStatements(f, func(expr build.Expr, stack []build.Expr) (err error) {
76 if def, ok := expr.(*build.DefStmt); ok {
77 defStmts = append(defStmts, def)
78 }
79 return
80 })
81
82 for _, function := range defStmts {
83 var hasNonEmptyReturns bool
84 build.WalkStatements(function, func(expr build.Expr, stack []build.Expr) (err error) {
85 if _, ok := expr.(*build.DefStmt); ok {
86 if len(stack) > 0 {
87 return &build.StopTraversalError{}
88 }
89 }
90
91 if ret, ok := expr.(*build.ReturnStmt); ok && ret.Result != nil {
92 hasNonEmptyReturns = true
93 }
94 return err
95 })
96
97 if !hasNonEmptyReturns {
98 continue
99 }
100 explicitReturn := findReturnsWithoutValue(function.Body, func(ret *build.ReturnStmt) {
101 findings = append(findings,
102 makeLinterFinding(ret, fmt.Sprintf("Some but not all execution paths of %q return a value.", function.Name)))
103 })
104 if !explicitReturn {
105 findings = append(findings,
106 makeLinterFinding(function, fmt.Sprintf(`Some but not all execution paths of %q return a value.
107 The function may terminate by an implicit return in the end.`, function.Name)))
108 }
109 }
110 return findings
111 }
112
113
114
115
116
117 func findUnreachableStatements(stmts []build.Expr, callback func(build.Expr)) bool {
118 unreachable := false
119 for _, stmt := range stmts {
120 if unreachable {
121 callback(stmt)
122 return true
123 }
124 switch stmt := stmt.(type) {
125 case *build.ReturnStmt:
126 unreachable = true
127 case *build.CallExpr:
128 ident, ok := stmt.X.(*build.Ident)
129 if ok && ident.Name == "fail" {
130 unreachable = true
131 }
132 case *build.BranchStmt:
133 if stmt.Token != "pass" {
134
135 unreachable = true
136 }
137 case *build.ForStmt:
138 findUnreachableStatements(stmt.Body, callback)
139 case *build.IfStmt:
140
141 term1 := findUnreachableStatements(stmt.True, callback)
142 term2 := findUnreachableStatements(stmt.False, callback)
143 if term1 && term2 {
144 unreachable = true
145 }
146 }
147 }
148 return unreachable
149 }
150
151 func unreachableStatementWarning(f *build.File) []*LinterFinding {
152 findings := []*LinterFinding{}
153
154 build.WalkStatements(f, func(expr build.Expr, stack []build.Expr) (err error) {
155 def, ok := expr.(*build.DefStmt)
156 if !ok {
157 return
158 }
159 findUnreachableStatements(def.Body, func(expr build.Expr) {
160 findings = append(findings,
161 makeLinterFinding(expr, `The statement is unreachable.`))
162 })
163 return
164 })
165 return findings
166 }
167
168 func noEffectStatementsCheck(body []build.Expr, isTopLevel, isFunc bool, findings []*LinterFinding) []*LinterFinding {
169 seenNonComment := false
170 for _, stmt := range body {
171 if stmt == nil {
172 continue
173 }
174
175 _, isString := stmt.(*build.StringExpr)
176 if isString {
177 if !seenNonComment && (isTopLevel || isFunc) {
178
179 seenNonComment = true
180 continue
181 }
182 }
183 if _, ok := stmt.(*build.CommentBlock); !ok {
184 seenNonComment = true
185 }
186 switch s := (stmt).(type) {
187 case *build.DefStmt, *build.ForStmt, *build.IfStmt, *build.LoadStmt, *build.ReturnStmt,
188 *build.CallExpr, *build.CommentBlock, *build.BranchStmt, *build.AssignExpr:
189 continue
190 case *build.Comprehension:
191 if !isTopLevel || s.Curly {
192
193 findings = append(findings,
194 makeLinterFinding(stmt, "Expression result is not used. Use a for-loop instead of a list comprehension."))
195 }
196 continue
197 }
198
199 msg := "Expression result is not used."
200 if isString {
201 msg += " Docstrings should be the first statements of a file or a function (they may follow comment lines)."
202 }
203 findings = append(findings, makeLinterFinding(stmt, msg))
204 }
205 return findings
206 }
207
208 func noEffectWarning(f *build.File) []*LinterFinding {
209 findings := []*LinterFinding{}
210 findings = noEffectStatementsCheck(f.Stmt, true, false, findings)
211 build.WalkStatements(f, func(expr build.Expr, stack []build.Expr) (err error) {
212
213
214 switch expr := expr.(type) {
215 case *build.ForStmt:
216 findings = noEffectStatementsCheck(expr.Body, false, false, findings)
217 case *build.DefStmt:
218 findings = noEffectStatementsCheck(expr.Function.Body, false, true, findings)
219 case *build.IfStmt:
220 findings = noEffectStatementsCheck(expr.True, false, false, findings)
221 findings = noEffectStatementsCheck(expr.False, false, false, findings)
222 }
223 return
224 })
225 return findings
226 }
227
228
229
230
231
232
233
234
235
236
237
238 func extractIdentsFromStmt(stmt build.Expr) (assigned, used map[*build.Ident]bool) {
239
240
241
242
243
244 assigned = make(map[*build.Ident]bool)
245 used = make(map[*build.Ident]bool)
246
247
248 scopes := make(map[build.Expr]map[string]bool)
249
250
251 blockedNodes := make(map[build.Expr]bool)
252
253 build.WalkInterruptable(stmt, func(node build.Expr, stack []build.Expr) (err error) {
254
255 if _, ok := blockedNodes[node]; ok {
256 return &build.StopTraversalError{}
257 }
258
259 switch expr := node.(type) {
260 case *build.AssignExpr:
261
262
263
264 if node != stmt {
265
266
267
268 blockedNodes[expr.LHS] = true
269 return
270 }
271
272 hasUnusedComment := edit.ContainsComments(expr, "@unused")
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294 lValues := bzlenv.CollectLValues(expr.LHS)
295 allLValuesUnderscored := true
296 for _, lValue := range lValues {
297 if !strings.HasPrefix(lValue.Name, "_") {
298 allLValuesUnderscored = false
299 break
300 }
301 }
302
303 for _, lValue := range bzlenv.CollectLValues(expr.LHS) {
304 assigned[lValue] = hasUnusedComment ||
305 (!allLValuesUnderscored && strings.HasPrefix(lValue.Name, "_"))
306 }
307
308 case *build.ForStmt:
309
310
311
312
313
314
315
316
317
318
319
320
321 for _, lValue := range bzlenv.CollectLValues(expr.Vars) {
322 assigned[lValue] = strings.HasPrefix(lValue.Name, "_")
323 }
324
325
326
327 for _, substmt := range expr.Body {
328 blockedNodes[substmt] = true
329 }
330
331 case *build.IfStmt:
332
333
334 for _, substmt := range expr.True {
335 blockedNodes[substmt] = true
336 }
337 for _, substmt := range expr.False {
338 blockedNodes[substmt] = true
339 }
340
341 case *build.Comprehension:
342
343
344
345
346 scope := make(map[string]bool)
347 for _, clause := range expr.Clauses {
348 forClause, ok := clause.(*build.ForClause)
349 if !ok {
350
351 continue
352 }
353 for _, lValue := range bzlenv.CollectLValues(forClause.Vars) {
354 scope[lValue.Name] = true
355 }
356 }
357 scopes[expr] = scope
358
359 case *build.LambdaExpr:
360
361 scope := make(map[string]bool)
362 for _, param := range expr.Params {
363 for _, lValue := range bzlenv.CollectLValues(param) {
364 scope[lValue.Name] = true
365 }
366 }
367 scopes[expr] = scope
368
369 case *build.Ident:
370
371 for _, node := range stack {
372 if scope, ok := scopes[node]; ok {
373 if _, ok := scope[expr.Name]; ok {
374 return
375 }
376 }
377 }
378 used[expr] = true
379
380 default:
381
382 }
383 return
384 })
385
386 for ident := range assigned {
387
388
389
390 delete(used, ident)
391 }
392 return assigned, used
393 }
394
395
396
397
398 func unusedVariableCheck(f *build.File, root build.Expr) (map[string]bool, []*LinterFinding) {
399 findings := []*LinterFinding{}
400
401
402 definedSymbols := make(map[string]*build.Ident)
403
404
405 definedFunctions := make(map[string]*build.DefStmt)
406
407
408 usedSymbols := make(map[string]bool)
409
410
411 suppressedWarnings := make(map[string]bool)
412
413 build.WalkStatements(root, func(expr build.Expr, stack []build.Expr) (err error) {
414 switch expr := expr.(type) {
415 case *build.File:
416
417
418 case *build.DefStmt:
419 if len(stack) > 0 {
420
421
422
423
424 if _, ok := definedFunctions[expr.Name]; !ok {
425 definedFunctions[expr.Name] = expr
426 }
427 if edit.ContainsComments(expr, "@unused") {
428 suppressedWarnings[expr.Name] = true
429 }
430
431 usedSymbolsInFunction, findingsInFunction := unusedVariableCheck(f, expr)
432 findings = append(findings, findingsInFunction...)
433 for symbol := range usedSymbolsInFunction {
434 usedSymbols[symbol] = true
435 }
436 return &build.StopTraversalError{}
437 }
438
439
440
441 for _, param := range expr.Params {
442
443 if ident, _ := build.GetParamIdent(param); ident != nil {
444 definedSymbols[ident.Name] = ident
445 if ident.Name == "name" || strings.HasPrefix(ident.Name, "_") || edit.ContainsComments(param, "@unused") {
446
447
448
449
450
451 suppressedWarnings[ident.Name] = true
452 }
453 }
454
455
456 assign, ok := param.(*build.AssignExpr)
457 if !ok {
458 continue
459 }
460
461
462
463
464 _, used := extractIdentsFromStmt(assign.RHS)
465 for ident := range used {
466 usedSymbols[ident.Name] = true
467 }
468 }
469
470 case *build.LoadStmt:
471
472
473
474
475 return
476
477 default:
478 assigned, used := extractIdentsFromStmt(expr)
479
480 for symbol := range used {
481 usedSymbols[symbol.Name] = true
482 }
483 for symbol, isSuppressed := range assigned {
484 if _, ok := definedSymbols[symbol.Name]; !ok {
485 definedSymbols[symbol.Name] = symbol
486 if isSuppressed {
487 suppressedWarnings[symbol.Name] = true
488 }
489 }
490 }
491 }
492 return
493 })
494
495
496
497
498
499
500 usedSymbolsFromOuterScope := make(map[string]bool)
501 for symbol := range usedSymbols {
502 if _, ok := definedSymbols[symbol]; ok {
503 continue
504 }
505 if _, ok := definedFunctions[symbol]; ok {
506 continue
507 }
508 usedSymbolsFromOuterScope[symbol] = true
509 }
510
511
512
513
514 ignoreTopLevel := (f.Type == build.TypeBzl || f.Type == build.TypeDefault) && root == f
515
516 for name, ident := range definedSymbols {
517 if _, ok := usedSymbols[name]; ok {
518
519 continue
520 }
521 if _, ok := suppressedWarnings[name]; ok {
522
523 continue
524 }
525 if ignoreTopLevel && !strings.HasPrefix(name, "_") {
526 continue
527 }
528 findings = append(findings,
529 makeLinterFinding(ident, fmt.Sprintf(`Variable %q is unused. Please remove it.`, ident.Name)))
530 }
531
532 for name, def := range definedFunctions {
533 if _, ok := usedSymbols[name]; ok {
534
535 continue
536 }
537 if ignoreTopLevel && !strings.HasPrefix(name, "_") {
538 continue
539 }
540 if _, ok := suppressedWarnings[name]; ok {
541
542 continue
543 }
544 findings = append(findings,
545 makeLinterFinding(def, fmt.Sprintf(`Function %q is unused. Please remove it.`, def.Name)))
546 }
547
548 return usedSymbolsFromOuterScope, findings
549 }
550
551 func unusedVariableWarning(f *build.File) []*LinterFinding {
552 _, findings := unusedVariableCheck(f, f)
553 return findings
554 }
555
556 func redefinedVariableWarning(f *build.File) []*LinterFinding {
557 findings := []*LinterFinding{}
558 definedSymbols := make(map[string]bool)
559
560 types := DetectTypes(f)
561 for _, s := range f.Stmt {
562
563 as, ok := s.(*build.AssignExpr)
564 if !ok {
565 continue
566 }
567 left, ok := as.LHS.(*build.Ident)
568 if !ok {
569 continue
570 }
571 if !definedSymbols[left.Name] {
572 definedSymbols[left.Name] = true
573 continue
574 }
575
576 if as.Op == "+=" && (types[as.LHS] == List || types[as.RHS] == List) {
577
578 continue
579 }
580
581 findings = append(findings,
582 makeLinterFinding(as.LHS, fmt.Sprintf(`Variable %q has already been defined.
583 Redefining a global value is discouraged and will be forbidden in the future.
584 Consider using a new variable instead.`, left.Name)))
585 }
586 return findings
587 }
588
589 func unusedLoadWarning(f *build.File) []*LinterFinding {
590 findings := []*LinterFinding{}
591 loaded := make(map[string]struct {
592 label, from string
593 line int
594 })
595
596 symbols := edit.UsedSymbols(f)
597 types := edit.UsedTypes(f)
598 for stmtIndex := 0; stmtIndex < len(f.Stmt); stmtIndex++ {
599 originalLoad, ok := f.Stmt[stmtIndex].(*build.LoadStmt)
600 if !ok {
601 continue
602 }
603
604
605 loadFindings := []*LinterFinding{}
606
607
608 load := *originalLoad
609 load.From = append([]*build.Ident{}, load.From...)
610 load.To = append([]*build.Ident{}, load.To...)
611
612 for i := 0; i < len(load.To); i++ {
613 from := load.From[i]
614 to := load.To[i]
615
616 origin, alreadyLoaded := loaded[to.Name]
617 start, _ := from.Span()
618 loaded[to.Name] = struct {
619 label, from string
620 line int
621 }{load.Module.Token, from.Name, start.Line}
622
623 if alreadyLoaded {
624
625 if origin.label == load.Module.Token && origin.from == from.Name {
626
627 load.To = append(load.To[:i], load.To[i+1:]...)
628 load.From = append(load.From[:i], load.From[i+1:]...)
629 i--
630 loadFindings = append(loadFindings, makeLinterFinding(to,
631 fmt.Sprintf("Symbol %q has already been loaded on line %d. Please remove it.", to.Name, origin.line)))
632 continue
633 }
634
635 loadFindings = append(loadFindings, makeLinterFinding(to,
636 fmt.Sprintf("A different symbol %q has already been loaded on line %d. Please use a different local name.", to.Name, origin.line)))
637 continue
638 }
639 _, ok := symbols[to.Name]
640 if !ok {
641
642 _, ok = types[to.Name]
643 }
644 if !ok && !edit.ContainsComments(originalLoad, "@unused") && !edit.ContainsComments(to, "@unused") && !edit.ContainsComments(from, "@unused") {
645
646 load.To = append(load.To[:i], load.To[i+1:]...)
647 load.From = append(load.From[:i], load.From[i+1:]...)
648 i--
649
650 loadFindings = append(loadFindings, makeLinterFinding(to,
651 fmt.Sprintf("Loaded symbol %q is unused. Please remove it.\nTo disable the warning, add '@unused' in a comment.", to.Name)))
652 if f.Type == build.TypeDefault || f.Type == build.TypeBzl {
653 loadFindings[len(loadFindings)-1].Message += fmt.Sprintf(`
654 If you want to re-export a symbol, use the following pattern:
655
656 load(..., _%s = %q, ...)
657 %s = _%s
658 `, to.Name, from.Name, to.Name, to.Name)
659 }
660 }
661 }
662
663 if len(loadFindings) == 0 {
664
665 continue
666 }
667
668 build.SortLoadArgs(&load)
669 var newStmt build.Expr = &load
670 if len(load.To) == 0 {
671
672 newStmt = nil
673 }
674 replacement := LinterReplacement{&f.Stmt[stmtIndex], newStmt}
675
676
677
678
679
680
681
682
683
684
685
686
687 loadFindings[0].Replacement = []LinterReplacement{replacement}
688 findings = append(findings, loadFindings...)
689 }
690 return findings
691 }
692
693
694
695 func collectLocalVariables(stmts []build.Expr) []*build.Ident {
696 variables := []*build.Ident{}
697
698 for _, stmt := range stmts {
699 switch stmt := stmt.(type) {
700 case *build.DefStmt:
701
702 case *build.ForStmt:
703 variables = append(variables, bzlenv.CollectLValues(stmt.Vars)...)
704 variables = append(variables, collectLocalVariables(stmt.Body)...)
705 case *build.IfStmt:
706 variables = append(variables, collectLocalVariables(stmt.True)...)
707 variables = append(variables, collectLocalVariables(stmt.False)...)
708 case *build.AssignExpr:
709 variables = append(variables, bzlenv.CollectLValues(stmt.LHS)...)
710 }
711 }
712 return variables
713 }
714
715
716
717
718
719
720
721
722 func findUninitializedVariables(stmts []build.Expr, previouslyInitialized map[string]bool, callback func(*build.Ident)) (bool, map[string]bool) {
723
724 locallyInitialized := make(map[string]bool)
725 initialized := make(map[string]bool)
726 for key := range previouslyInitialized {
727 initialized[key] = true
728 }
729
730
731
732 findUninitializedIdents := func(expr build.Expr, callback func(ident *build.Ident)) {
733
734
735 lValues := make(map[*build.Ident]bool)
736 build.WalkInterruptable(expr, func(expr build.Expr, stack []build.Expr) (err error) {
737 switch expr := expr.(type) {
738 case *build.DefStmt:
739
740
741
742 for _, param := range expr.Params {
743 if ident, _ := build.GetParamIdent(param); ident != nil {
744 lValues[ident] = true
745 }
746 }
747
748 return &build.StopTraversalError{}
749 case *build.AssignExpr:
750 for _, ident := range bzlenv.CollectLValues(expr.LHS) {
751 lValues[ident] = true
752 }
753 }
754 return
755 })
756
757
758 callbackIfNeeded := func(ident *build.Ident) {
759 if !initialized[ident.Name] && !lValues[ident] {
760 callback(ident)
761 }
762 }
763
764 walkBlockList := map[build.Expr]bool{}
765 build.WalkInterruptable(expr, func(expr build.Expr, stack []build.Expr) (err error) {
766 if walkBlockList[expr] {
767 return &build.StopTraversalError{}
768 }
769 switch expr := expr.(type) {
770 case *build.DefStmt:
771
772
773
774
775 for _, stmt := range expr.Body {
776 walkBlockList[stmt] = true
777 }
778 case *build.Comprehension, *build.LambdaExpr:
779
780
781
782
783
784
785 _, used := extractIdentsFromStmt(expr)
786 for ident := range used {
787 callbackIfNeeded(ident)
788 }
789
790 return &build.StopTraversalError{}
791 case *build.Ident:
792 callbackIfNeeded(expr)
793 default:
794
795 }
796 return
797 })
798 }
799
800 for _, stmt := range stmts {
801 newlyDefinedVariables := make(map[string]bool)
802 switch stmt := stmt.(type) {
803 case *build.DefStmt:
804
805 case *build.CallExpr:
806 if _, ok := isFunctionCall(stmt, "fail"); ok {
807 return true, locallyInitialized
808 }
809 case *build.ReturnStmt:
810 findUninitializedIdents(stmt, callback)
811 return true, locallyInitialized
812 case *build.BranchStmt:
813 if stmt.Token == "break" || stmt.Token == "continue" {
814 return true, locallyInitialized
815 }
816 case *build.ForStmt:
817
818
819
820
821
822 findUninitializedIdents(stmt.X, callback)
823
824
825 initializedInLoop := make(map[string]bool)
826 for name := range initialized {
827 initializedInLoop[name] = true
828 }
829 for _, ident := range bzlenv.CollectLValues(stmt.Vars) {
830 initializedInLoop[ident.Name] = true
831 }
832 findUninitializedVariables(stmt.Body, initializedInLoop, callback)
833 continue
834 case *build.IfStmt:
835 findUninitializedIdents(stmt.Cond, callback)
836
837 terminatedTrue, definedInTrue := findUninitializedVariables(stmt.True, initialized, callback)
838 terminatedFalse, definedInFalse := findUninitializedVariables(stmt.False, initialized, callback)
839 if terminatedTrue && terminatedFalse {
840 return true, locallyInitialized
841 } else if terminatedTrue {
842
843 for key := range definedInFalse {
844 locallyInitialized[key] = true
845 initialized[key] = true
846 }
847 } else if terminatedFalse {
848
849 for key := range definedInTrue {
850 locallyInitialized[key] = true
851 initialized[key] = true
852 }
853 } else {
854
855 for key := range definedInTrue {
856 if definedInFalse[key] {
857 locallyInitialized[key] = true
858 initialized[key] = true
859 }
860 }
861 }
862 continue
863 case *build.AssignExpr:
864
865 for _, ident := range bzlenv.CollectLValues(stmt.LHS) {
866 newlyDefinedVariables[ident.Name] = true
867 }
868 }
869 findUninitializedIdents(stmt, callback)
870 for name := range newlyDefinedVariables {
871 locallyInitialized[name] = true
872 initialized[name] = true
873 }
874 }
875 return false, locallyInitialized
876 }
877
878
879 func uninitializedVariableWarning(f *build.File) []*LinterFinding {
880 findings := []*LinterFinding{}
881 build.WalkStatements(f, func(expr build.Expr, stack []build.Expr) (err error) {
882 def, ok := expr.(*build.DefStmt)
883 if !ok {
884 return
885 }
886
887
888
889 localVars := make(map[string]bool)
890 for _, ident := range collectLocalVariables(def.Body) {
891 localVars[ident.Name] = true
892 }
893
894
895
896 for _, param := range def.Params {
897 if name, _ := build.GetParamName(param); name != "" {
898 delete(localVars, name)
899 }
900 }
901
902
903 findUninitializedVariables(def.Body, make(map[string]bool), func(ident *build.Ident) {
904
905 if localVars[ident.Name] {
906 findings = append(findings,
907 makeLinterFinding(ident, fmt.Sprintf(`Variable "%s" may not have been initialized.`, ident.Name)))
908 }
909 })
910 return
911 })
912 return findings
913 }
914
View as plain text