Skip to content

Commit 6c4b75d

Browse files
oreflowTim Malmström
andauthored
Updating positional-args check to be more comprehensive (#1393)
- Previous implementation just assumed that macros were called at the root of a BUILD file, and that all calls were to macros/rules except for explicit allowlist. Updating the check to more precisely validate whether something is a rule/macro, and checking all call expressions (including ones nested in dicts/attrs). - Also updating IsMacro implementation to correctly support symblic macro definitions (`my_macro = macro(...)`) Co-authored-by: Tim Malmström <[email protected]>
1 parent 4006b54 commit 6c4b75d

File tree

7 files changed

+155
-67
lines changed

7 files changed

+155
-67
lines changed

WARNINGS.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,10 +1205,11 @@ The linter allows the following to be before `package()`:
12051205
* Automatic fix: no
12061206
* [Suppress the warning](#suppress): `# buildifier: disable=positional-args`
12071207

1208-
All top level calls (except for some built-ins) should use keyword args over
1209-
positional arguments. Positional arguments can cause subtle errors if the order
1210-
is switched or if an argument is removed. Keyword args also greatly improve
1211-
readability.
1208+
All macro and rule calls should use keyword args over positional arguments.
1209+
Positional arguments can cause subtle errors if the order is switched or if
1210+
an argument is removed. Keyword args also greatly improve readability.
1211+
Additionally, positional arguments prevent migration from [Legacy Macros](https://bazel.build/extending/legacy-macros)
1212+
to [Symbolic Macros](https://bazel.build/extending/macros).
12121213

12131214
```diff
12141215
- my_macro("foo", "bar")

warn/docs/warnings.textproto

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -876,10 +876,11 @@ warnings: {
876876
name: "positional-args"
877877
header: "Keyword arguments should be used over positional arguments"
878878
description:
879-
"All top level calls (except for some built-ins) should use keyword args over\n"
880-
"positional arguments. Positional arguments can cause subtle errors if the order\n"
881-
"is switched or if an argument is removed. Keyword args also greatly improve\n"
882-
"readability.\n\n"
879+
"All macro and rule calls should use keyword args over positional arguments.\n"
880+
"Positional arguments can cause subtle errors if the order is switched or if\n"
881+
"an argument is removed. Keyword args also greatly improve readability.\n"
882+
"Additionally, positional arguments prevent migration from [Legacy Macros](https://bazel.build/extending/legacy-macros)\n"
883+
"to [Symbolic Macros](https://bazel.build/extending/macros).\n\n"
883884
"```diff\n"
884885
"- my_macro(\"foo\", \"bar\")\n"
885886
"+ my_macro(name = \"foo\", env = \"bar\")\n"

warn/warn.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,7 @@ func makeLinterFinding(node build.Expr, message string, replacement ...LinterRep
112112

113113
// RuleWarningMap lists the warnings that run on a single rule.
114114
// These warnings run only on BUILD files (not bzl files).
115-
var RuleWarningMap = map[string]func(call *build.CallExpr, pkg string) *LinterFinding{
116-
"positional-args": positionalArgumentsWarning,
117-
}
115+
var RuleWarningMap = map[string]func(call *build.CallExpr, pkg string) *LinterFinding{}
118116

119117
// FileWarningMap lists the warnings that run on the whole file.
120118
var FileWarningMap = map[string]func(f *build.File) []*LinterFinding{
@@ -219,6 +217,7 @@ var MultiFileWarningMap = map[string]func(f *build.File, fileReader *FileReader)
219217
"native-sh-binary": NativeShellRulesWarning("sh_binary"),
220218
"native-sh-library": NativeShellRulesWarning("sh_library"),
221219
"native-sh-test": NativeShellRulesWarning("sh_test"),
220+
"positional-args": positionalArgumentsWarning,
222221
"unnamed-macro": unnamedMacroWarning,
223222
}
224223

warn/warn_bazel.go

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@ import (
2525
"github.com/bazelbuild/buildtools/build"
2626
)
2727

28-
var functionsWithPositionalArguments = map[string]bool{
29-
"exports_files": true,
30-
"licenses": true,
31-
"print": true,
32-
"register_toolchains": true,
33-
"vardef": true,
34-
}
35-
3628
func constantGlobPatternWarning(patterns *build.ListExpr) []*LinterFinding {
3729
findings := []*LinterFinding{}
3830
for _, expr := range patterns.List {
@@ -179,17 +171,37 @@ func duplicatedNameWarning(f *build.File) []*LinterFinding {
179171
return findings
180172
}
181173

182-
func positionalArgumentsWarning(call *build.CallExpr, pkg string) *LinterFinding {
183-
if id, ok := call.X.(*build.Ident); !ok || functionsWithPositionalArguments[id.Name] {
174+
func positionalArgumentsWarning(f *build.File, fileReader *FileReader) (findings []*LinterFinding) {
175+
if f.Type != build.TypeBuild {
184176
return nil
185177
}
186-
for _, arg := range call.List {
187-
if _, ok := arg.(*build.AssignExpr); ok || arg == nil {
188-
continue
189-
}
190-
return makeLinterFinding(arg, "All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.")
178+
macroAnalyzer := newMacroAnalyzer(fileReader)
179+
macroAnalyzer.files[f.Pkg+":"+f.Label] = analyzeFile(f)
180+
181+
for _, expr := range f.Stmt {
182+
build.Walk(expr, func(x build.Expr, _ []build.Expr) {
183+
if fnCall, ok := x.(*build.CallExpr); ok {
184+
fnIdent, ok := fnCall.X.(*build.Ident)
185+
if !ok {
186+
return
187+
}
188+
189+
if macroAnalyzer.IsRuleOrMacro(function{pkg: f.Pkg, filename: f.Label, name: fnIdent.Name}).isRuleOrMacro {
190+
for _, arg := range fnCall.List {
191+
if _, ok := arg.(*build.AssignExpr); ok || arg == nil {
192+
continue
193+
}
194+
findings = append(findings, makeLinterFinding(fnCall, fmt.Sprintf(
195+
`All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.
196+
Found call to rule or macro %q with positional arguments.`,
197+
fnIdent.Name)))
198+
return
199+
}
200+
}
201+
}
202+
})
191203
}
192-
return nil
204+
return
193205
}
194206

195207
func argsKwargsInBuildFilesWarning(f *build.File) []*LinterFinding {
@@ -257,7 +269,6 @@ func externalPathWarning(f *build.File) []*LinterFinding {
257269
if !ok {
258270
return
259271
}
260-
261272
// Warn for "/external/" but not if "//" appears in the string (main repository paths)
262273
if strings.Contains(stringExpr.Value, "/external/") && !strings.Contains(stringExpr.Value, "//") {
263274
findings = append(findings,
@@ -280,7 +291,6 @@ func canonicalRepositoryWarning(f *build.File) []*LinterFinding {
280291
if !ok {
281292
return
282293
}
283-
284294
if strings.Contains(stringExpr.Value, "@@") {
285295
findings = append(findings,
286296
makeLinterFinding(stringExpr, `String contains "@@" which indicates a canonical repository name reference that should be avoided.`))

warn/warn_bazel_test.go

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,22 @@ exports_files(["foo.txt"])
8787
scopeBuild|scopeWorkspace)
8888
}
8989

90-
func TestPositionalArguments(t *testing.T) {
90+
func TestPositionalArgumentsDoesNotWarnForNamedArguments(t *testing.T) {
9191
checkFindings(t, "positional-args", `
92+
my_macro = macro()
93+
my_rule = rule()
94+
def my_function(foo):
95+
pass
96+
9297
my_macro(foo = "bar")
93-
my_macro("foo", "bar")
94-
my_macro(foo = bar(x))
95-
[my_macro(foo) for foo in bar]`,
96-
[]string{
97-
":2: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.",
98-
":4: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.",
99-
},
98+
my_rule(foo = "bar")
99+
my_function(foo = "bar")
100+
`,
101+
[]string{},
100102
scopeBuild)
103+
}
101104

105+
func TestPositionalArgumentsDoesNotWarnForAllowlistedFunctions(t *testing.T) {
102106
checkFindings(t, "positional-args", `
103107
register_toolchains(
104108
"//foo",
@@ -108,6 +112,55 @@ register_toolchains(
108112
scopeBuild)
109113
}
110114

115+
func TestPositionalArgumentsWarnsForPositionalMacrosOrRuleCalls(t *testing.T) {
116+
checkFindings(t, "positional-args", `
117+
my_macro = macro()
118+
my_rule = rule()
119+
def my_function(foo):
120+
pass
121+
122+
my_macro("foo", "bar")
123+
my_rule("foo", "bar")
124+
my_function("foo", "bar")
125+
`,
126+
[]string{
127+
`6: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.
128+
Found call to rule or macro "my_macro" with positional arguments.`,
129+
`7: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.
130+
Found call to rule or macro "my_rule" with positional arguments.`,
131+
},
132+
scopeBuild)
133+
}
134+
135+
func TestPositionalArgumentsWarnsWhenCalledInNestedContexts(t *testing.T) {
136+
checkFindings(t, "positional-args", `
137+
my_macro = macro()
138+
my_rule = rule()
139+
def my_function(foo):
140+
pass
141+
142+
other_function(foo = my_macro(x))
143+
other_function(foo = my_rule(x))
144+
other_function(foo = my_function(x))
145+
146+
[my_macro(foo) for foo in bar]
147+
[my_rule(foo) for foo in bar]
148+
[my_function(foo) for foo in bar]
149+
`,
150+
[]string{
151+
`6: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.
152+
Found call to rule or macro "my_macro" with positional arguments.`,
153+
`7: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.
154+
Found call to rule or macro "my_rule" with positional arguments.`,
155+
`10: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.
156+
Found call to rule or macro "my_macro" with positional arguments.`,
157+
`11: All calls to rules or macros should pass arguments by keyword (arg_name=value) syntax.
158+
Found call to rule or macro "my_rule" with positional arguments.`,
159+
},
160+
scopeBuild)
161+
162+
}
163+
111164
func TestKwargsInBuildFilesWarning(t *testing.T) {
112165
checkFindings(t, "build-args-kwargs", `
113166
cc_library(

warn/warn_macro.go

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,18 @@ type funCall struct {
5353
func acceptsNameArgument(def *build.DefStmt) bool {
5454
for _, param := range def.Params {
5555
if name, op := build.GetParamName(param); name == "name" || op == "**" {
56-
return true
56+
return true
5757
}
5858
}
5959
return false
6060
}
6161

6262
// fileData represents information about rules and functions extracted from a file
6363
type fileData struct {
64-
rules map[string]bool // all rules defined in the file
65-
functions map[string]map[string]funCall // outer map: all functions defined in the file, inner map: all distinct function calls from the given function
66-
aliases map[string]function // all top-level aliases (e.g. `foo = bar`).
64+
loadedSymbols map[string]function // Symbols loaded from other files.
65+
rulesOrMacros map[string]bool // all rules or macros defined in the file.
66+
functions map[string]map[string]funCall // outer map: all functions defined in the file, inner map: all distinct function calls from the given function
67+
aliases map[string]function // all top-level aliases (e.g. `foo = bar`).
6768
}
6869

6970
// resolvesExternal takes a local function definition and replaces it with an external one if it's been defined
@@ -123,8 +124,14 @@ func analyzeFile(f *build.File) fileData {
123124
return fileData{}
124125
}
125126

127+
report := fileData{
128+
loadedSymbols: make(map[string]function),
129+
rulesOrMacros: make(map[string]bool),
130+
functions: make(map[string]map[string]funCall),
131+
aliases: make(map[string]function),
132+
}
133+
126134
// Collect loaded symbols
127-
externalSymbols := make(map[string]function)
128135
for _, stmt := range f.Stmt {
129136
load, ok := stmt.(*build.LoadStmt)
130137
if !ok {
@@ -135,15 +142,10 @@ func analyzeFile(f *build.File) fileData {
135142
continue
136143
}
137144
for i, from := range load.From {
138-
externalSymbols[load.To[i].Name] = function{label.Package, label.Target, from.Name}
145+
report.loadedSymbols[load.To[i].Name] = function{label.Package, label.Target, from.Name}
139146
}
140147
}
141148

142-
report := fileData{
143-
rules: make(map[string]bool),
144-
functions: make(map[string]map[string]funCall),
145-
aliases: make(map[string]function),
146-
}
147149
for _, stmt := range f.Stmt {
148150
switch stmt := stmt.(type) {
149151
case *build.AssignExpr:
@@ -153,21 +155,22 @@ func analyzeFile(f *build.File) fileData {
153155
continue
154156
}
155157
if rhsIdent, ok := stmt.RHS.(*build.Ident); ok {
156-
report.aliases[lhsIdent.Name] = resolveExternal(function{f.Pkg, f.Label, rhsIdent.Name}, externalSymbols)
158+
report.aliases[lhsIdent.Name] = resolveExternal(function{f.Pkg, f.Label, rhsIdent.Name}, report.loadedSymbols)
157159
continue
158160
}
159161

160162
call, ok := stmt.RHS.(*build.CallExpr)
161163
if !ok {
162164
continue
163165
}
164-
ident, ok := call.X.(*build.Ident)
165-
if !ok || ident.Name != "rule" {
166-
continue
166+
if ident, ok := call.X.(*build.Ident); ok {
167+
if ident.Name == "rule" || ident.Name == "macro" {
168+
report.rulesOrMacros[lhsIdent.Name] = true
169+
continue
170+
}
167171
}
168-
report.rules[lhsIdent.Name] = true
169172
case *build.DefStmt:
170-
report.functions[stmt.Name] = getFunCalls(stmt, f.Pkg, f.Label, externalSymbols)
173+
report.functions[stmt.Name] = getFunCalls(stmt, f.Pkg, f.Label, report.loadedSymbols)
171174
default:
172175
continue
173176
}
@@ -177,8 +180,8 @@ func analyzeFile(f *build.File) fileData {
177180

178181
// functionReport represents the analysis result of a function
179182
type functionReport struct {
180-
isMacro bool // whether the function is a macro (or a rule)
181-
fc *funCall // a call to the rule or another macro
183+
isRuleOrMacro bool // whether the function is a macro (or a rule)
184+
fc *funCall // a call to the rule or another macro
182185
}
183186

184187
// macroAnalyzer is an object that analyzes the directed graph of functions calling each other,
@@ -208,7 +211,7 @@ func (ma macroAnalyzer) getFileData(pkg, label string) fileData {
208211
}
209212

210213
// IsMacro is a public function that checks whether the given function is a macro
211-
func (ma macroAnalyzer) IsMacro(fn function) (report functionReport) {
214+
func (ma macroAnalyzer) IsRuleOrMacro(fn function) (report functionReport) {
212215
// Check the cache first
213216
if cached, ok := ma.cache[fn]; ok {
214217
return cached
@@ -228,7 +231,7 @@ func (ma macroAnalyzer) IsMacro(fn function) (report functionReport) {
228231
"repository_name", "exports_files":
229232
// Not a rule
230233
default:
231-
report.isMacro = true
234+
report.isRuleOrMacro = true
232235
}
233236
return
234237
}
@@ -237,18 +240,26 @@ func (ma macroAnalyzer) IsMacro(fn function) (report functionReport) {
237240

238241
// Check whether fn.name is an alias for another function
239242
if alias, ok := fileData.aliases[fn.name]; ok {
240-
if ma.IsMacro(alias).isMacro {
241-
report.isMacro = true
243+
if ma.IsRuleOrMacro(alias).isRuleOrMacro {
244+
report.isRuleOrMacro = true
242245
}
243246
return
244247
}
245248

246-
// Check whether fn.name is a rule
247-
if fileData.rules[fn.name] {
248-
report.isMacro = true
249+
// Check whether fn.name is a rule or macro
250+
if fileData.rulesOrMacros[fn.name] {
251+
report.isRuleOrMacro = true
249252
return
250253
}
251254

255+
// Check whether fn.name is a loaded symbol from another file
256+
if externalFn, ok := fileData.loadedSymbols[fn.name]; ok {
257+
if ma.IsRuleOrMacro(externalFn).isRuleOrMacro {
258+
report.isRuleOrMacro = true
259+
return
260+
}
261+
}
262+
252263
// Check whether fn.name is an ordinary function
253264
funCalls, ok := fileData.functions[fn.name]
254265
if !ok {
@@ -267,8 +278,8 @@ func (ma macroAnalyzer) IsMacro(fn function) (report functionReport) {
267278
}
268279

269280
for _, fc := range append(knownFunCalls, newFunCalls...) {
270-
if ma.IsMacro(fc.function).isMacro {
271-
report.isMacro = true
281+
if ma.IsRuleOrMacro(fc.function).isRuleOrMacro {
282+
report.isRuleOrMacro = true
272283
report.fc = &fc
273284
return
274285
}
@@ -305,8 +316,8 @@ func unnamedMacroWarning(f *build.File, fileReader *FileReader) []*LinterFinding
305316
continue
306317
}
307318

308-
report := macroAnalyzer.IsMacro(function{f.Pkg, f.Label, def.Name})
309-
if !report.isMacro {
319+
report := macroAnalyzer.IsRuleOrMacro(function{f.Pkg, f.Label, def.Name})
320+
if !report.isRuleOrMacro {
310321
continue
311322
}
312323
msg := fmt.Sprintf(`The macro %q should have a keyword argument called "name".`, def.Name)

0 commit comments

Comments
 (0)