forked from bazelbuild/buildtools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwarn_macro.go
332 lines (290 loc) · 9.53 KB
/
warn_macro.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/*
Copyright 2020 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Warnings for using deprecated functions
package warn
import (
"fmt"
"strings"
"github.com/bazelbuild/buildtools/build"
"github.com/bazelbuild/buildtools/labels"
)
// Internal constant that represents the native module
const nativeModule = "<native>"
// function represents a function identifier, which is a pair (module name, function name).
type function struct {
pkg string // package where the function is defined
filename string // name of a .bzl file relative to the package
name string // original name of the function
}
func (f function) label() string {
return f.pkg + ":" + f.filename
}
// funCall represents a call to another function. It contains information of the function itself as well as some
// information about the environment
type funCall struct {
function
nameAlias string // function name alias (it could be loaded with a different name or assigned to a new variable).
line int // line on which the function is being called
}
// acceptsNameArgument checks whether a function can accept a named argument called "name",
// either directly or via **kwargs.
func acceptsNameArgument(def *build.DefStmt) bool {
for _, param := range def.Params {
if name, op := build.GetParamName(param); name == "name" || op == "**" {
return true
}
}
return false
}
// fileData represents information about rules and functions extracted from a file
type fileData struct {
rules map[string]bool // all rules defined in the file
functions map[string]map[string]funCall // outer map: all functions defined in the file, inner map: all distinct function calls from the given function
aliases map[string]function // all top-level aliases (e.g. `foo = bar`).
}
// resolvesExternal takes a local function definition and replaces it with an external one if it's been defined
// in another file and loaded
func resolveExternal(fn function, externalSymbols map[string]function) function {
if external, ok := externalSymbols[fn.name]; ok {
return external
}
return fn
}
// exprLine returns the start line of an expression
func exprLine(expr build.Expr) int {
start, _ := expr.Span()
return start.Line
}
// getFunCalls extracts information about functions that are being called from the given function
func getFunCalls(def *build.DefStmt, pkg, filename string, externalSymbols map[string]function) map[string]funCall {
funCalls := make(map[string]funCall)
build.Walk(def, func(expr build.Expr, stack []build.Expr) {
call, ok := expr.(*build.CallExpr)
if !ok {
return
}
if ident, ok := call.X.(*build.Ident); ok {
funCalls[ident.Name] = funCall{
function: resolveExternal(function{pkg, filename, ident.Name}, externalSymbols),
nameAlias: ident.Name,
line: exprLine(call),
}
return
}
dot, ok := call.X.(*build.DotExpr)
if !ok {
return
}
if ident, ok := dot.X.(*build.Ident); !ok || ident.Name != "native" {
return
}
name := "native." + dot.Name
funCalls[name] = funCall{
function: function{
name: dot.Name,
filename: nativeModule,
},
nameAlias: name,
line: exprLine(dot),
}
})
return funCalls
}
// analyzeFile extracts the information about rules and functions defined in the file
func analyzeFile(f *build.File) fileData {
if f == nil {
return fileData{}
}
// Collect loaded symbols
externalSymbols := make(map[string]function)
for _, stmt := range f.Stmt {
load, ok := stmt.(*build.LoadStmt)
if !ok {
continue
}
label := labels.ParseRelative(load.Module.Value, f.Pkg)
if label.Repository != "" || label.Target == "" {
continue
}
for i, from := range load.From {
externalSymbols[load.To[i].Name] = function{label.Package, label.Target, from.Name}
}
}
report := fileData{
rules: make(map[string]bool),
functions: make(map[string]map[string]funCall),
aliases: make(map[string]function),
}
for _, stmt := range f.Stmt {
switch stmt := stmt.(type) {
case *build.AssignExpr:
// Analyze aliases (`foo = bar`) or rule declarations (`foo = rule(...)`)
lhsIdent, ok := stmt.LHS.(*build.Ident)
if !ok {
continue
}
if rhsIdent, ok := stmt.RHS.(*build.Ident); ok {
report.aliases[lhsIdent.Name] = resolveExternal(function{f.Pkg, f.Label, rhsIdent.Name}, externalSymbols)
continue
}
call, ok := stmt.RHS.(*build.CallExpr)
if !ok {
continue
}
ident, ok := call.X.(*build.Ident)
if !ok || ident.Name != "rule" {
continue
}
report.rules[lhsIdent.Name] = true
case *build.DefStmt:
report.functions[stmt.Name] = getFunCalls(stmt, f.Pkg, f.Label, externalSymbols)
default:
continue
}
}
return report
}
// functionReport represents the analysis result of a function
type functionReport struct {
isMacro bool // whether the function is a macro (or a rule)
fc *funCall // a call to the rule or another macro
}
// macroAnalyzer is an object that analyzes the directed graph of functions calling each other,
// loading other files lazily if necessary.
type macroAnalyzer struct {
fileReader *FileReader
files map[string]fileData
cache map[function]functionReport
}
// getFileData retrieves a file using the fileReader object and extracts information about functions and rules
// defined in the file.
func (ma macroAnalyzer) getFileData(pkg, label string) fileData {
filename := pkg + ":" + label
if fd, ok := ma.files[filename]; ok {
return fd
}
if ma.fileReader == nil {
fd := fileData{}
ma.files[filename] = fd
return fd
}
f := ma.fileReader.GetFile(pkg, label)
fd := analyzeFile(f)
ma.files[filename] = fd
return fd
}
// IsMacro is a public function that checks whether the given function is a macro
func (ma macroAnalyzer) IsMacro(fn function) (report functionReport) {
// Check the cache first
if cached, ok := ma.cache[fn]; ok {
return cached
}
// Write a negative result to the cache before analyzing. This will prevent stack overflow crashes
// if the input data contains recursion.
ma.cache[fn] = report
defer func() {
// Update the cache with the actual result
ma.cache[fn] = report
}()
// Check for native rules
if fn.filename == nativeModule {
switch fn.name {
case "glob", "existing_rule", "existing_rules", "package_name",
"repository_name", "exports_files":
// Not a rule
default:
report.isMacro = true
}
return
}
fileData := ma.getFileData(fn.pkg, fn.filename)
// Check whether fn.name is an alias for another function
if alias, ok := fileData.aliases[fn.name]; ok {
if ma.IsMacro(alias).isMacro {
report.isMacro = true
}
return
}
// Check whether fn.name is a rule
if fileData.rules[fn.name] {
report.isMacro = true
return
}
// Check whether fn.name is an ordinary function
funCalls, ok := fileData.functions[fn.name]
if !ok {
return
}
// Prioritize function calls from already loaded files. If some of the function calls are from the same file
// (or another file that has been loaded already), check them first.
var knownFunCalls, newFunCalls []funCall
for _, fc := range funCalls {
if _, ok := ma.files[fc.function.pkg+":"+fc.function.filename]; ok || fc.function.filename == nativeModule {
knownFunCalls = append(knownFunCalls, fc)
} else {
newFunCalls = append(newFunCalls, fc)
}
}
for _, fc := range append(knownFunCalls, newFunCalls...) {
if ma.IsMacro(fc.function).isMacro {
report.isMacro = true
report.fc = &fc
return
}
}
return
}
// newMacroAnalyzer creates and initiates an instance of macroAnalyzer.
func newMacroAnalyzer(fileReader *FileReader) macroAnalyzer {
return macroAnalyzer{
fileReader: fileReader,
files: make(map[string]fileData),
cache: make(map[function]functionReport),
}
}
func unnamedMacroWarning(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
macroAnalyzer := newMacroAnalyzer(fileReader)
macroAnalyzer.files[f.Pkg+":"+f.Label] = analyzeFile(f)
findings := []*LinterFinding{}
for _, stmt := range f.Stmt {
def, ok := stmt.(*build.DefStmt)
if !ok {
continue
}
if strings.HasPrefix(def.Name, "_") || acceptsNameArgument(def) {
continue
}
report := macroAnalyzer.IsMacro(function{f.Pkg, f.Label, def.Name})
if !report.isMacro {
continue
}
msg := fmt.Sprintf(`The macro %q should have a keyword argument called "name".`, def.Name)
if report.fc != nil {
// fc shouldn't be nil because that's the only node that can be found inside a function.
msg += fmt.Sprintf(`
It is considered a macro because it calls a rule or another macro %q on line %d.
By convention, every public macro needs a "name" argument (even if it doesn't use it).
This is important for tooling and automation.
* If this function is a helper function that's not supposed to be used outside of this file,
please make it private (e.g. rename it to "_%s").
* Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, report.fc.nameAlias, report.fc.line, def.Name)
}
finding := makeLinterFinding(def, msg)
finding.End = def.ColonPos
findings = append(findings, finding)
}
return findings
}