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{&param.LHS, &newName},
415				LinterReplacement{&param.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