1
16
17
18 package warn
19
20 import (
21 "fmt"
22 "log"
23 "os"
24 "sort"
25
26 "github.com/bazelbuild/buildtools/build"
27 "github.com/bazelbuild/buildtools/edit"
28 )
29
30
31 type LintMode int
32
33 const (
34
35 ModeWarn LintMode = iota
36
37
38 ModeFix
39
40
41 ModeSuggest
42 )
43
44
45 type LinterFinding struct {
46 Start build.Position
47 End build.Position
48 Message string
49 URL string
50 Replacement []LinterReplacement
51 }
52
53
54 type LinterReplacement struct {
55 Old *build.Expr
56 New build.Expr
57 }
58
59
60 type Finding struct {
61 File *build.File
62 Start build.Position
63 End build.Position
64 Category string
65 Message string
66 URL string
67 Actionable bool
68 AutoFixable bool
69 Replacement *Replacement
70 }
71
72
73 type Replacement struct {
74 Description string
75 Start int
76 End int
77 Content string
78 }
79
80 func docURL(cat string) string {
81 return "https://github.com/bazelbuild/buildtools/blob/master/WARNINGS.md#" + cat
82 }
83
84
85 func makeFinding(f *build.File, start, end build.Position, cat, url, msg string, actionable bool, autoFixable bool, fix *Replacement) *Finding {
86 if url == "" {
87 url = docURL(cat)
88 }
89 return &Finding{
90 File: f,
91 Start: start,
92 End: end,
93 Category: cat,
94 URL: url,
95 Message: msg,
96 Actionable: actionable,
97 AutoFixable: autoFixable,
98 Replacement: fix,
99 }
100 }
101
102
103 func makeLinterFinding(node build.Expr, message string, replacement ...LinterReplacement) *LinterFinding {
104 start, end := node.Span()
105 return &LinterFinding{
106 Start: start,
107 End: end,
108 Message: message,
109 Replacement: replacement,
110 }
111 }
112
113
114
115 var RuleWarningMap = map[string]func(call *build.CallExpr, pkg string) *LinterFinding{
116 "positional-args": positionalArgumentsWarning,
117 }
118
119
120 var FileWarningMap = map[string]func(f *build.File) []*LinterFinding{
121 "attr-applicable_licenses": attrApplicableLicensesWarning,
122 "attr-cfg": attrConfigurationWarning,
123 "attr-license": attrLicenseWarning,
124 "attr-licenses": attrLicensesWarning,
125 "attr-non-empty": attrNonEmptyWarning,
126 "attr-output-default": attrOutputDefaultWarning,
127 "attr-single-file": attrSingleFileWarning,
128 "build-args-kwargs": argsKwargsInBuildFilesWarning,
129 "bzl-visibility": bzlVisibilityWarning,
130 "confusing-name": confusingNameWarning,
131 "constant-glob": constantGlobWarning,
132 "ctx-actions": ctxActionsWarning,
133 "ctx-args": contextArgsAPIWarning,
134 "depset-items": depsetItemsWarning,
135 "depset-iteration": depsetIterationWarning,
136 "depset-union": depsetUnionWarning,
137 "dict-method-named-arg": dictMethodNamedArgWarning,
138 "dict-concatenation": dictionaryConcatenationWarning,
139 "duplicated-name": duplicatedNameWarning,
140 "filetype": fileTypeWarning,
141 "function-docstring": functionDocstringWarning,
142 "function-docstring-header": functionDocstringHeaderWarning,
143 "function-docstring-args": functionDocstringArgsWarning,
144 "function-docstring-return": functionDocstringReturnWarning,
145 "git-repository": nativeGitRepositoryWarning,
146 "http-archive": nativeHTTPArchiveWarning,
147 "integer-division": integerDivisionWarning,
148 "keyword-positional-params": keywordPositionalParametersWarning,
149 "list-append": listAppendWarning,
150 "load": unusedLoadWarning,
151 "module-docstring": moduleDocstringWarning,
152 "name-conventions": nameConventionsWarning,
153 "native-android": nativeAndroidRulesWarning,
154 "native-build": nativeInBuildFilesWarning,
155 "native-cc": nativeCcRulesWarning,
156 "native-java": nativeJavaRulesWarning,
157 "native-package": nativePackageWarning,
158 "native-proto": nativeProtoRulesWarning,
159 "native-py": nativePyRulesWarning,
160 "no-effect": noEffectWarning,
161 "output-group": outputGroupWarning,
162 "overly-nested-depset": overlyNestedDepsetWarning,
163 "package-name": packageNameWarning,
164 "package-on-top": packageOnTopWarning,
165 "print": printWarning,
166 "provider-params": providerParamsWarning,
167 "redefined-variable": redefinedVariableWarning,
168 "repository-name": repositoryNameWarning,
169 "rule-impl-return": ruleImplReturnWarning,
170 "return-value": missingReturnValueWarning,
171 "skylark-comment": skylarkCommentWarning,
172 "skylark-docstring": skylarkDocstringWarning,
173 "string-iteration": stringIterationWarning,
174 "uninitialized": uninitializedVariableWarning,
175 "unreachable": unreachableStatementWarning,
176 "unsorted-dict-items": unsortedDictItemsWarning,
177 "unused-variable": unusedVariableWarning,
178 }
179
180
181 var MultiFileWarningMap = map[string]func(f *build.File, fileReader *FileReader) []*LinterFinding{
182 "deprecated-function": deprecatedFunctionWarning,
183 "unnamed-macro": unnamedMacroWarning,
184 }
185
186
187
188 var nonDefaultWarnings = map[string]bool{
189 "unsorted-dict-items": true,
190 "native-android": true,
191 "native-cc": true,
192 "native-java": true,
193 "native-proto": true,
194 "native-py": true,
195 }
196
197
198
199
200 func fileWarningWrapper(fct func(f *build.File) []*LinterFinding) func(*build.File, string, *FileReader) []*LinterFinding {
201 return func(f *build.File, _ string, _ *FileReader) []*LinterFinding {
202 return fct(f)
203 }
204 }
205
206
207
208 func multiFileWarningWrapper(fct func(f *build.File, fileReader *FileReader) []*LinterFinding) func(*build.File, string, *FileReader) []*LinterFinding {
209 return func(f *build.File, _ string, fileReader *FileReader) []*LinterFinding {
210 return fct(f, fileReader)
211 }
212 }
213
214
215
216 func ruleWarningWrapper(ruleWarning func(call *build.CallExpr, pkg string) *LinterFinding) func(*build.File, string, *FileReader) []*LinterFinding {
217 return func(f *build.File, pkg string, _ *FileReader) []*LinterFinding {
218 if f.Type != build.TypeBuild {
219 return nil
220 }
221 var findings []*LinterFinding
222 for _, stmt := range f.Stmt {
223 switch stmt := stmt.(type) {
224 case *build.CallExpr:
225 finding := ruleWarning(stmt, pkg)
226 if finding != nil {
227 findings = append(findings, finding)
228 }
229 case *build.Comprehension:
230
231 if call, ok := stmt.Body.(*build.CallExpr); ok {
232 finding := ruleWarning(call, pkg)
233 if finding != nil {
234 findings = append(findings, finding)
235 }
236 }
237 }
238 }
239 return findings
240 }
241 }
242
243
244 func runWarningsFunction(category string, f *build.File, fct func(f *build.File, pkg string, fileReader *FileReader) []*LinterFinding, formatted *[]byte, mode LintMode, fileReader *FileReader) []*Finding {
245 findings := []*Finding{}
246 for _, w := range fct(f, f.Pkg, fileReader) {
247 if !DisabledWarning(f, w.Start.Line, category) {
248 finding := makeFinding(f, w.Start, w.End, category, w.URL, w.Message, true, len(w.Replacement) > 0, nil)
249 if len(w.Replacement) > 0 {
250
251 switch mode {
252 case ModeFix:
253
254 for _, r := range w.Replacement {
255 *r.Old = r.New
256 }
257 finding = nil
258 case ModeSuggest:
259
260 newContents := formatWithFix(f, &w.Replacement)
261
262 start, end, replacement := calculateDifference(formatted, &newContents)
263 finding.Replacement = &Replacement{
264 Description: w.Message,
265 Start: start,
266 End: end,
267 Content: replacement,
268 }
269 }
270 }
271 if finding != nil {
272 findings = append(findings, finding)
273 }
274 }
275 }
276 return findings
277 }
278
279
280 func HasDisablingComment(expr build.Expr, warning string) bool {
281 return edit.ContainsComments(expr, "buildifier: disable="+warning) ||
282 edit.ContainsComments(expr, "buildozer: disable="+warning)
283 }
284
285
286
287 func DisabledWarning(f *build.File, findingLine int, warning string) bool {
288 disabled := false
289
290 build.Walk(f, func(expr build.Expr, stack []build.Expr) {
291 if expr == nil {
292 return
293 }
294
295 start, end := expr.Span()
296 comments := expr.Comment()
297 if len(comments.Before) > 0 {
298 start, _ = comments.Before[0].Span()
299 }
300 if len(comments.After) > 0 {
301 _, end = comments.After[len(comments.After)-1].Span()
302 }
303 if findingLine < start.Line || findingLine > end.Line {
304 return
305 }
306
307 if HasDisablingComment(expr, warning) {
308 disabled = true
309 return
310 }
311 })
312
313 return disabled
314 }
315
316
317 func FileWarnings(f *build.File, enabledWarnings []string, formatted *[]byte, mode LintMode, fileReader *FileReader) []*Finding {
318 findings := []*Finding{}
319
320
321
322 warnings := append([]string{}, enabledWarnings...)
323 sort.Strings(warnings)
324
325
326 if mode == ModeSuggest && formatted == nil {
327 contents := build.Format(f)
328 formatted = &contents
329 }
330
331 for _, warn := range warnings {
332 if fct, ok := FileWarningMap[warn]; ok {
333 findings = append(findings, runWarningsFunction(warn, f, fileWarningWrapper(fct), formatted, mode, fileReader)...)
334 } else if fct, ok := MultiFileWarningMap[warn]; ok {
335 findings = append(findings, runWarningsFunction(warn, f, multiFileWarningWrapper(fct), formatted, mode, fileReader)...)
336 } else if fct, ok := RuleWarningMap[warn]; ok {
337 findings = append(findings, runWarningsFunction(warn, f, ruleWarningWrapper(fct), formatted, mode, fileReader)...)
338 } else {
339 log.Fatalf("unexpected warning %q", warn)
340 }
341 }
342 sort.Slice(findings, func(i, j int) bool { return findings[i].Start.Line < findings[j].Start.Line })
343 return findings
344 }
345
346
347 func formatWithFix(f *build.File, replacements *[]LinterReplacement) []byte {
348 for i := range *replacements {
349 r := (*replacements)[i]
350 old := *r.Old
351 *r.Old = r.New
352 defer func() { *r.Old = old }()
353 }
354
355 return build.Format(f)
356 }
357
358
359
360 func calculateDifference(old, new *[]byte) (start, end int, replacement string) {
361 commonPrefix := 0
362 for i, b := range *old {
363 if i >= len(*new) || b != (*new)[i] {
364 break
365 }
366 commonPrefix++
367 }
368
369 commonSuffix := 0
370 for i := range *old {
371 b := (*old)[len(*old)-1-i]
372 if i >= len(*new) || b != (*new)[len(*new)-1-i] {
373 break
374 }
375 commonSuffix++
376 }
377
378
379
380
381
382
383
384 if commonPrefix+commonSuffix > len(*old) {
385 commonSuffix = len(*old) - commonPrefix
386 }
387 if commonPrefix+commonSuffix > len(*new) {
388 commonSuffix = len(*new) - commonPrefix
389 }
390 return commonPrefix, len(*old) - commonSuffix, string((*new)[commonPrefix:(len(*new) - commonSuffix)])
391 }
392
393
394 func FixWarnings(f *build.File, enabledWarnings []string, verbose bool, fileReader *FileReader) {
395 warnings := FileWarnings(f, enabledWarnings, nil, ModeFix, fileReader)
396 if verbose {
397 fmt.Fprintf(os.Stderr, "%s: applied fixes, %d warnings left\n",
398 f.DisplayPath(),
399 len(warnings))
400 }
401 }
402
403 func collectAllWarnings() []string {
404 var result []string
405
406 for k := range FileWarningMap {
407 result = append(result, k)
408 }
409 for k := range MultiFileWarningMap {
410 result = append(result, k)
411 }
412 for k := range RuleWarningMap {
413 result = append(result, k)
414 }
415 sort.Strings(result)
416 return result
417 }
418
419
420 var AllWarnings = collectAllWarnings()
421
422 func collectDefaultWarnings() []string {
423 warnings := []string{}
424 for _, warning := range AllWarnings {
425 if !nonDefaultWarnings[warning] {
426 warnings = append(warnings, warning)
427 }
428 }
429 return warnings
430 }
431
432
433 var DefaultWarnings = collectDefaultWarnings()
434
View as plain text