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/labels"
27 )
28
29
30 const nativeModule = "<native>"
31
32
33 type function struct {
34 pkg string
35 filename string
36 name string
37 }
38
39 func (f function) label() string {
40 return f.pkg + ":" + f.filename
41 }
42
43
44
45 type funCall struct {
46 function
47 nameAlias string
48 line int
49 }
50
51
52
53 func acceptsNameArgument(def *build.DefStmt) bool {
54 for _, param := range def.Params {
55 if name, op := build.GetParamName(param); name == "name" || op == "**" {
56 return true
57 }
58 }
59 return false
60 }
61
62
63 type fileData struct {
64 rules map[string]bool
65 functions map[string]map[string]funCall
66 aliases map[string]function
67 }
68
69
70
71 func resolveExternal(fn function, externalSymbols map[string]function) function {
72 if external, ok := externalSymbols[fn.name]; ok {
73 return external
74 }
75 return fn
76 }
77
78
79 func exprLine(expr build.Expr) int {
80 start, _ := expr.Span()
81 return start.Line
82 }
83
84
85 func getFunCalls(def *build.DefStmt, pkg, filename string, externalSymbols map[string]function) map[string]funCall {
86 funCalls := make(map[string]funCall)
87 build.Walk(def, func(expr build.Expr, stack []build.Expr) {
88 call, ok := expr.(*build.CallExpr)
89 if !ok {
90 return
91 }
92 if ident, ok := call.X.(*build.Ident); ok {
93 funCalls[ident.Name] = funCall{
94 function: resolveExternal(function{pkg, filename, ident.Name}, externalSymbols),
95 nameAlias: ident.Name,
96 line: exprLine(call),
97 }
98 return
99 }
100 dot, ok := call.X.(*build.DotExpr)
101 if !ok {
102 return
103 }
104 if ident, ok := dot.X.(*build.Ident); !ok || ident.Name != "native" {
105 return
106 }
107 name := "native." + dot.Name
108 funCalls[name] = funCall{
109 function: function{
110 name: dot.Name,
111 filename: nativeModule,
112 },
113 nameAlias: name,
114 line: exprLine(dot),
115 }
116 })
117 return funCalls
118 }
119
120
121 func analyzeFile(f *build.File) fileData {
122 if f == nil {
123 return fileData{}
124 }
125
126
127 externalSymbols := make(map[string]function)
128 for _, stmt := range f.Stmt {
129 load, ok := stmt.(*build.LoadStmt)
130 if !ok {
131 continue
132 }
133 label := labels.ParseRelative(load.Module.Value, f.Pkg)
134 if label.Repository != "" || label.Target == "" {
135 continue
136 }
137 for i, from := range load.From {
138 externalSymbols[load.To[i].Name] = function{label.Package, label.Target, from.Name}
139 }
140 }
141
142 report := fileData{
143 rules: make(map[string]bool),
144 functions: make(map[string]map[string]funCall),
145 aliases: make(map[string]function),
146 }
147 for _, stmt := range f.Stmt {
148 switch stmt := stmt.(type) {
149 case *build.AssignExpr:
150
151 lhsIdent, ok := stmt.LHS.(*build.Ident)
152 if !ok {
153 continue
154 }
155 if rhsIdent, ok := stmt.RHS.(*build.Ident); ok {
156 report.aliases[lhsIdent.Name] = resolveExternal(function{f.Pkg, f.Label, rhsIdent.Name}, externalSymbols)
157 continue
158 }
159
160 call, ok := stmt.RHS.(*build.CallExpr)
161 if !ok {
162 continue
163 }
164 ident, ok := call.X.(*build.Ident)
165 if !ok || ident.Name != "rule" {
166 continue
167 }
168 report.rules[lhsIdent.Name] = true
169 case *build.DefStmt:
170 report.functions[stmt.Name] = getFunCalls(stmt, f.Pkg, f.Label, externalSymbols)
171 default:
172 continue
173 }
174 }
175 return report
176 }
177
178
179 type functionReport struct {
180 isMacro bool
181 fc *funCall
182 }
183
184
185
186 type macroAnalyzer struct {
187 fileReader *FileReader
188 files map[string]fileData
189 cache map[function]functionReport
190 }
191
192
193
194 func (ma macroAnalyzer) getFileData(pkg, label string) fileData {
195 filename := pkg + ":" + label
196 if fd, ok := ma.files[filename]; ok {
197 return fd
198 }
199 if ma.fileReader == nil {
200 fd := fileData{}
201 ma.files[filename] = fd
202 return fd
203 }
204 f := ma.fileReader.GetFile(pkg, label)
205 fd := analyzeFile(f)
206 ma.files[filename] = fd
207 return fd
208 }
209
210
211 func (ma macroAnalyzer) IsMacro(fn function) (report functionReport) {
212
213 if cached, ok := ma.cache[fn]; ok {
214 return cached
215 }
216
217
218 ma.cache[fn] = report
219 defer func() {
220
221 ma.cache[fn] = report
222 }()
223
224
225 if fn.filename == nativeModule {
226 switch fn.name {
227 case "glob", "existing_rule", "existing_rules", "package_name",
228 "repository_name", "exports_files":
229
230 default:
231 report.isMacro = true
232 }
233 return
234 }
235
236 fileData := ma.getFileData(fn.pkg, fn.filename)
237
238
239 if alias, ok := fileData.aliases[fn.name]; ok {
240 if ma.IsMacro(alias).isMacro {
241 report.isMacro = true
242 }
243 return
244 }
245
246
247 if fileData.rules[fn.name] {
248 report.isMacro = true
249 return
250 }
251
252
253 funCalls, ok := fileData.functions[fn.name]
254 if !ok {
255 return
256 }
257
258
259
260 var knownFunCalls, newFunCalls []funCall
261 for _, fc := range funCalls {
262 if _, ok := ma.files[fc.function.pkg+":"+fc.function.filename]; ok || fc.function.filename == nativeModule {
263 knownFunCalls = append(knownFunCalls, fc)
264 } else {
265 newFunCalls = append(newFunCalls, fc)
266 }
267 }
268
269 for _, fc := range append(knownFunCalls, newFunCalls...) {
270 if ma.IsMacro(fc.function).isMacro {
271 report.isMacro = true
272 report.fc = &fc
273 return
274 }
275 }
276
277 return
278 }
279
280
281 func newMacroAnalyzer(fileReader *FileReader) macroAnalyzer {
282 return macroAnalyzer{
283 fileReader: fileReader,
284 files: make(map[string]fileData),
285 cache: make(map[function]functionReport),
286 }
287 }
288
289 func unnamedMacroWarning(f *build.File, fileReader *FileReader) []*LinterFinding {
290 if f.Type != build.TypeBzl {
291 return nil
292 }
293
294 macroAnalyzer := newMacroAnalyzer(fileReader)
295 macroAnalyzer.files[f.Pkg+":"+f.Label] = analyzeFile(f)
296
297 findings := []*LinterFinding{}
298 for _, stmt := range f.Stmt {
299 def, ok := stmt.(*build.DefStmt)
300 if !ok {
301 continue
302 }
303
304 if strings.HasPrefix(def.Name, "_") || acceptsNameArgument(def) {
305 continue
306 }
307
308 report := macroAnalyzer.IsMacro(function{f.Pkg, f.Label, def.Name})
309 if !report.isMacro {
310 continue
311 }
312 msg := fmt.Sprintf(`The macro %q should have a keyword argument called "name".`, def.Name)
313 if report.fc != nil {
314
315 msg += fmt.Sprintf(`
316
317 It is considered a macro because it calls a rule or another macro %q on line %d.
318
319 By convention, every public macro needs a "name" argument (even if it doesn't use it).
320 This is important for tooling and automation.
321
322 * If this function is a helper function that's not supposed to be used outside of this file,
323 please make it private (e.g. rename it to "_%s").
324 * Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, report.fc.nameAlias, report.fc.line, def.Name)
325 }
326 finding := makeLinterFinding(def, msg)
327 finding.End = def.ColonPos
328 findings = append(findings, finding)
329 }
330
331 return findings
332 }
333
View as plain text