1// Package warn implements functions that generate warnings for BUILD files. 2package warn 3 4import ( 5 "fmt" 6 "log" 7 "os" 8 "sort" 9 10 "github.com/bazelbuild/buildtools/build" 11 "github.com/bazelbuild/buildtools/edit" 12) 13 14// LintMode is an enum representing a linter mode. Can be either "warn", "fix", or "suggest" 15type LintMode int 16 17const ( 18 // ModeWarn means only warnings should be returned for each finding. 19 ModeWarn LintMode = iota 20 // ModeFix means that all warnings that can be fixed automatically should be fixed and 21 // no warnings should be returned for them. 22 ModeFix 23 // ModeSuggest means that automatic fixes shouldn't be applied, but instead corresponding 24 // suggestions should be attached to all warnings that can be fixed automatically. 25 ModeSuggest 26) 27 28// LinterFinding is a low-level warning reported by single linter/fixer functions. 29type LinterFinding struct { 30 Start build.Position 31 End build.Position 32 Message string 33 URL string 34 Replacement []LinterReplacement 35} 36 37// LinterReplacement is a low-level object returned by single fixer functions. 38type LinterReplacement struct { 39 Old *build.Expr 40 New build.Expr 41} 42 43// A Finding is a warning reported by the analyzer. It may contain an optional suggested fix. 44type Finding struct { 45 File *build.File 46 Start build.Position 47 End build.Position 48 Category string 49 Message string 50 URL string 51 Actionable bool 52 Replacement *Replacement 53} 54 55// A Replacement is a suggested fix. Text between Start and End should be replaced with Content. 56type Replacement struct { 57 Description string 58 Start int 59 End int 60 Content string 61} 62 63func docURL(cat string) string { 64 return "https://github.com/bazelbuild/buildtools/blob/master/WARNINGS.md#" + cat 65} 66 67// makeFinding creates a Finding object 68func makeFinding(f *build.File, start, end build.Position, cat, url, msg string, actionable bool, fix *Replacement) *Finding { 69 if url == "" { 70 url = docURL(cat) 71 } 72 return &Finding{ 73 File: f, 74 Start: start, 75 End: end, 76 Category: cat, 77 URL: url, 78 Message: msg, 79 Actionable: actionable, 80 Replacement: fix, 81 } 82} 83 84// makeLinterFinding creates a LinterFinding object 85func makeLinterFinding(node build.Expr, message string, replacement ...LinterReplacement) *LinterFinding { 86 start, end := node.Span() 87 return &LinterFinding{ 88 Start: start, 89 End: end, 90 Message: message, 91 Replacement: replacement, 92 } 93} 94 95// RuleWarningMap lists the warnings that run on a single rule. 96// These warnings run only on BUILD files (not bzl files). 97var RuleWarningMap = map[string]func(call *build.CallExpr, pkg string) *LinterFinding{ 98 "positional-args": positionalArgumentsWarning, 99} 100 101// FileWarningMap lists the warnings that run on the whole file. 102var FileWarningMap = map[string]func(f *build.File) []*LinterFinding{ 103 "attr-cfg": attrConfigurationWarning, 104 "attr-license": attrLicenseWarning, 105 "attr-non-empty": attrNonEmptyWarning, 106 "attr-output-default": attrOutputDefaultWarning, 107 "attr-single-file": attrSingleFileWarning, 108 "build-args-kwargs": argsKwargsInBuildFilesWarning, 109 "bzl-visibility": bzlVisibilityWarning, 110 "confusing-name": confusingNameWarning, 111 "constant-glob": constantGlobWarning, 112 "ctx-actions": ctxActionsWarning, 113 "ctx-args": contextArgsAPIWarning, 114 "depset-items": depsetItemsWarning, 115 "depset-iteration": depsetIterationWarning, 116 "depset-union": depsetUnionWarning, 117 "dict-concatenation": dictionaryConcatenationWarning, 118 "duplicated-name": duplicatedNameWarning, 119 "filetype": fileTypeWarning, 120 "function-docstring": functionDocstringWarning, 121 "function-docstring-header": functionDocstringHeaderWarning, 122 "function-docstring-args": functionDocstringArgsWarning, 123 "function-docstring-return": functionDocstringReturnWarning, 124 "git-repository": nativeGitRepositoryWarning, 125 "http-archive": nativeHTTPArchiveWarning, 126 "integer-division": integerDivisionWarning, 127 "keyword-positional-params": keywordPositionalParametersWarning, 128 "list-append": listAppendWarning, 129 "load": unusedLoadWarning, 130 "load-on-top": loadOnTopWarning, 131 "module-docstring": moduleDocstringWarning, 132 "name-conventions": nameConventionsWarning, 133 "native-android": nativeAndroidRulesWarning, 134 "native-build": nativeInBuildFilesWarning, 135 "native-cc": nativeCcRulesWarning, 136 "native-java": nativeJavaRulesWarning, 137 "native-package": nativePackageWarning, 138 "native-proto": nativeProtoRulesWarning, 139 "native-py": nativePyRulesWarning, 140 "no-effect": noEffectWarning, 141 "output-group": outputGroupWarning, 142 "out-of-order-load": outOfOrderLoadWarning, 143 "overly-nested-depset": overlyNestedDepsetWarning, 144 "package-name": packageNameWarning, 145 "package-on-top": packageOnTopWarning, 146 "print": printWarning, 147 "redefined-variable": redefinedVariableWarning, 148 "repository-name": repositoryNameWarning, 149 "rule-impl-return": ruleImplReturnWarning, 150 "return-value": missingReturnValueWarning, 151 "same-origin-load": sameOriginLoadWarning, 152 "skylark-comment": skylarkCommentWarning, 153 "skylark-docstring": skylarkDocstringWarning, 154 "string-iteration": stringIterationWarning, 155 "uninitialized": uninitializedVariableWarning, 156 "unreachable": unreachableStatementWarning, 157 "unsorted-dict-items": unsortedDictItemsWarning, 158 "unused-variable": unusedVariableWarning, 159} 160 161// MultiFileWarningMap lists the warnings that run on the whole file, but may use other files. 162var MultiFileWarningMap = map[string]func(f *build.File, fileReader *FileReader) []*LinterFinding{ 163 "deprecated-function": deprecatedFunctionWarning, 164 "unnamed-macro": unnamedMacroWarning, 165} 166 167// nonDefaultWarnings contains warnings that are enabled by default because they're not applicable 168// for all files and cause too much diff noise when applied. 169var nonDefaultWarnings = map[string]bool{ 170 "out-of-order-load": true, // load statements should be sorted by their labels 171 "unsorted-dict-items": true, // dict items should be sorted 172} 173 174// fileWarningWrapper is a wrapper that converts a file warning function to a generic function. 175// A generic function takes a `pkg string` and a `*ReadFile` arguments which are not used for file warnings, 176// so they are just removed. 177func fileWarningWrapper(fct func(f *build.File) []*LinterFinding) func(*build.File, string, *FileReader) []*LinterFinding { 178 return func(f *build.File, _ string, _ *FileReader) []*LinterFinding { 179 return fct(f) 180 } 181} 182 183// multiFileWarningWrapper is a wrapper that converts a multifile warning function to a generic function. 184// A generic function takes a `pkg string` argument which is not used for file warnings, so it's just removed. 185func multiFileWarningWrapper(fct func(f *build.File, fileReader *FileReader) []*LinterFinding) func(*build.File, string, *FileReader) []*LinterFinding { 186 return func(f *build.File, _ string, fileReader *FileReader) []*LinterFinding { 187 return fct(f, fileReader) 188 } 189} 190 191// ruleWarningWrapper is a wrapper that converts a per-rule function to a per-file function. 192// It also doesn't run on .bzl or default files, only on BUILD and WORKSPACE files. 193func ruleWarningWrapper(ruleWarning func(call *build.CallExpr, pkg string) *LinterFinding) func(*build.File, string, *FileReader) []*LinterFinding { 194 return func(f *build.File, pkg string, _ *FileReader) []*LinterFinding { 195 if f.Type != build.TypeBuild { 196 return nil 197 } 198 var findings []*LinterFinding 199 for _, stmt := range f.Stmt { 200 switch stmt := stmt.(type) { 201 case *build.CallExpr: 202 finding := ruleWarning(stmt, pkg) 203 if finding != nil { 204 findings = append(findings, finding) 205 } 206 case *build.Comprehension: 207 // Rules are often called within list comprehensions, e.g. [my_rule(foo) for foo in bar] 208 if call, ok := stmt.Body.(*build.CallExpr); ok { 209 finding := ruleWarning(call, pkg) 210 if finding != nil { 211 findings = append(findings, finding) 212 } 213 } 214 } 215 } 216 return findings 217 } 218} 219 220// runWarningsFunction runs a linter/fixer function over a file and applies the fixes conditionally 221func runWarningsFunction(category string, f *build.File, fct func(f *build.File, pkg string, fileReader *FileReader) []*LinterFinding, formatted *[]byte, mode LintMode, fileReader *FileReader) []*Finding { 222 findings := []*Finding{} 223 for _, w := range fct(f, f.Pkg, fileReader) { 224 if !DisabledWarning(f, w.Start.Line, category) { 225 finding := makeFinding(f, w.Start, w.End, category, w.URL, w.Message, true, nil) 226 if len(w.Replacement) > 0 { 227 // An automatic fix exists 228 switch mode { 229 case ModeFix: 230 // Apply the fix and discard the finding 231 for _, r := range w.Replacement { 232 *r.Old = r.New 233 } 234 finding = nil 235 case ModeSuggest: 236 // Apply the fix, calculate the diff and roll back the fix 237 newContents := formatWithFix(f, &w.Replacement) 238 239 start, end, replacement := calculateDifference(formatted, &newContents) 240 finding.Replacement = &Replacement{ 241 Description: w.Message, 242 Start: start, 243 End: end, 244 Content: replacement, 245 } 246 } 247 } 248 if finding != nil { 249 findings = append(findings, finding) 250 } 251 } 252 } 253 return findings 254} 255 256func hasDisablingComment(expr build.Expr, warning string) bool { 257 return edit.ContainsComments(expr, "buildifier: disable="+warning) || 258 edit.ContainsComments(expr, "buildozer: disable="+warning) 259} 260 261// DisabledWarning checks if the warning was disabled by a comment. 262// The comment format is buildozer: disable=<warning> 263func DisabledWarning(f *build.File, findingLine int, warning string) bool { 264 disabled := false 265 266 build.Walk(f, func(expr build.Expr, stack []build.Expr) { 267 if expr == nil { 268 return 269 } 270 271 start, end := expr.Span() 272 if findingLine < start.Line || findingLine > end.Line { 273 return 274 } 275 276 if hasDisablingComment(expr, warning) { 277 disabled = true 278 return 279 } 280 }) 281 282 return disabled 283} 284 285// FileWarnings returns a list of all warnings found in the file. 286func FileWarnings(f *build.File, enabledWarnings []string, formatted *[]byte, mode LintMode, fileReader *FileReader) []*Finding { 287 findings := []*Finding{} 288 289 // Sort the warnings to make sure they're applied in the same determined order 290 // Make a local copy first to avoid race conditions 291 warnings := append([]string{}, enabledWarnings...) 292 sort.Strings(warnings) 293 294 // If suggestions are requested and formatted file is not provided, format it to compare modified versions with 295 if mode == ModeSuggest && formatted == nil { 296 contents := build.Format(f) 297 formatted = &contents 298 } 299 300 for _, warn := range warnings { 301 if fct, ok := FileWarningMap[warn]; ok { 302 findings = append(findings, runWarningsFunction(warn, f, fileWarningWrapper(fct), formatted, mode, fileReader)...) 303 } else if fct, ok := MultiFileWarningMap[warn]; ok { 304 findings = append(findings, runWarningsFunction(warn, f, multiFileWarningWrapper(fct), formatted, mode, fileReader)...) 305 } else if fct, ok := RuleWarningMap[warn]; ok { 306 findings = append(findings, runWarningsFunction(warn, f, ruleWarningWrapper(fct), formatted, mode, fileReader)...) 307 } else { 308 log.Fatalf("unexpected warning %q", warn) 309 } 310 } 311 sort.Slice(findings, func(i, j int) bool { return findings[i].Start.Line < findings[j].Start.Line }) 312 return findings 313} 314 315// formatWithFix applies a fix, formats a file, and rolls back the fix 316func formatWithFix(f *build.File, replacements *[]LinterReplacement) []byte { 317 for i := range *replacements { 318 r := (*replacements)[i] 319 old := *r.Old 320 *r.Old = r.New 321 defer func() { *r.Old = old }() 322 } 323 324 return build.Format(f) 325} 326 327// calculateDifference compares two file contents and returns a replacement in the form of 328// a 3-tuple (byte from, byte to (non inclusive), a string to replace with). 329func calculateDifference(old, new *[]byte) (start, end int, replacement string) { 330 commonPrefix := 0 // length of the common prefix 331 for i, b := range *old { 332 if i >= len(*new) || b != (*new)[i] { 333 break 334 } 335 commonPrefix++ 336 } 337 338 commonSuffix := 0 // length of the common suffix 339 for i := range *old { 340 b := (*old)[len(*old)-1-i] 341 if i >= len(*new) || b != (*new)[len(*new)-1-i] { 342 break 343 } 344 commonSuffix++ 345 } 346 347 // In some cases common suffix and prefix can overlap. E.g. consider the following case: 348 // old = "abc" 349 // new = "abdbc" 350 // In this case the common prefix is "ab" and the common suffix is "bc". 351 // If they overlap, just shorten the suffix so that they don't. 352 // The new suffix will be just "c". 353 if commonPrefix+commonSuffix > len(*old) { 354 commonSuffix = len(*old) - commonPrefix 355 } 356 if commonPrefix+commonSuffix > len(*new) { 357 commonSuffix = len(*new) - commonPrefix 358 } 359 return commonPrefix, len(*old) - commonSuffix, string((*new)[commonPrefix:(len(*new) - commonSuffix)]) 360} 361 362// FixWarnings fixes all warnings that can be fixed automatically. 363func FixWarnings(f *build.File, enabledWarnings []string, verbose bool, fileReader *FileReader) { 364 warnings := FileWarnings(f, enabledWarnings, nil, ModeFix, fileReader) 365 if verbose { 366 fmt.Fprintf(os.Stderr, "%s: applied fixes, %d warnings left\n", 367 f.DisplayPath(), 368 len(warnings)) 369 } 370} 371 372func collectAllWarnings() []string { 373 var result []string 374 // Collect list of all warnings. 375 for k := range FileWarningMap { 376 result = append(result, k) 377 } 378 for k := range MultiFileWarningMap { 379 result = append(result, k) 380 } 381 for k := range RuleWarningMap { 382 result = append(result, k) 383 } 384 sort.Strings(result) 385 return result 386} 387 388// AllWarnings is the list of all available warnings. 389var AllWarnings = collectAllWarnings() 390 391func collectDefaultWarnings() []string { 392 warnings := []string{} 393 for _, warning := range AllWarnings { 394 if !nonDefaultWarnings[warning] { 395 warnings = append(warnings, warning) 396 } 397 } 398 return warnings 399} 400 401// DefaultWarnings is the list of all warnings that should be used inside google3 402var DefaultWarnings = collectDefaultWarnings() 403