...

Source file src/github.com/bazelbuild/buildtools/warn/warn.go

Documentation: github.com/bazelbuild/buildtools/warn

     1  /*
     2  Copyright 2020 Google LLC
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      https://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package warn implements functions that generate warnings for BUILD files.
    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  // LintMode is an enum representing a linter mode. Can be either "warn", "fix", or "suggest"
    31  type LintMode int
    32  
    33  const (
    34  	// ModeWarn means only warnings should be returned for each finding.
    35  	ModeWarn LintMode = iota
    36  	// ModeFix means that all warnings that can be fixed automatically should be fixed and
    37  	// no warnings should be returned for them.
    38  	ModeFix
    39  	// ModeSuggest means that automatic fixes shouldn't be applied, but instead corresponding
    40  	// suggestions should be attached to all warnings that can be fixed automatically.
    41  	ModeSuggest
    42  )
    43  
    44  // LinterFinding is a low-level warning reported by single linter/fixer functions.
    45  type LinterFinding struct {
    46  	Start       build.Position
    47  	End         build.Position
    48  	Message     string
    49  	URL         string
    50  	Replacement []LinterReplacement
    51  }
    52  
    53  // LinterReplacement is a low-level object returned by single fixer functions.
    54  type LinterReplacement struct {
    55  	Old *build.Expr
    56  	New build.Expr
    57  }
    58  
    59  // A Finding is a warning reported by the analyzer. It may contain an optional suggested fix.
    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  // A Replacement is a suggested fix. Text between Start and End should be replaced with Content.
    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  // makeFinding creates a Finding object
    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  // makeLinterFinding creates a LinterFinding object
   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  // RuleWarningMap lists the warnings that run on a single rule.
   114  // 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  }
   118  
   119  // FileWarningMap lists the warnings that run on the whole file.
   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  // MultiFileWarningMap lists the warnings that run on the whole file, but may use other files.
   181  var MultiFileWarningMap = map[string]func(f *build.File, fileReader *FileReader) []*LinterFinding{
   182  	"deprecated-function": deprecatedFunctionWarning,
   183  	"unnamed-macro":       unnamedMacroWarning,
   184  }
   185  
   186  // nonDefaultWarnings contains warnings that are enabled by default because they're not applicable
   187  // for all files and cause too much diff noise when applied.
   188  var nonDefaultWarnings = map[string]bool{
   189  	"unsorted-dict-items": true, // dict items should be sorted
   190  	"native-android":      true, // disables native android rules
   191  	"native-cc":           true, // disables native cc rules
   192  	"native-java":         true, // disables native java rules
   193  	"native-proto":        true, // disables native proto rules
   194  	"native-py":           true, // disables native python rules
   195  }
   196  
   197  // fileWarningWrapper is a wrapper that converts a file warning function to a generic function.
   198  // A generic function takes a `pkg string` and a `*ReadFile` arguments which are not used for file warnings,
   199  // so they are just removed.
   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  // multiFileWarningWrapper is a wrapper that converts a multifile warning function to a generic function.
   207  // A generic function takes a `pkg string` argument which is not used for file warnings, so it's just removed.
   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  // ruleWarningWrapper is a wrapper that converts a per-rule function to a per-file function.
   215  // It also doesn't run on .bzl or default files, only on BUILD and WORKSPACE files.
   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  				// Rules are often called within list comprehensions, e.g. [my_rule(foo) for foo in bar]
   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  // runWarningsFunction runs a linter/fixer function over a file and applies the fixes conditionally
   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  				// An automatic fix exists
   251  				switch mode {
   252  				case ModeFix:
   253  					// Apply the fix and discard the finding
   254  					for _, r := range w.Replacement {
   255  						*r.Old = r.New
   256  					}
   257  					finding = nil
   258  				case ModeSuggest:
   259  					// Apply the fix, calculate the diff and roll back the fix
   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  // HasDisablingComment checks if a node has a comment that disables a certain warning
   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  // DisabledWarning checks if the warning was disabled by a comment.
   286  // The comment format is buildozer: disable=<warning>
   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  // FileWarnings returns a list of all warnings found in the file.
   317  func FileWarnings(f *build.File, enabledWarnings []string, formatted *[]byte, mode LintMode, fileReader *FileReader) []*Finding {
   318  	findings := []*Finding{}
   319  
   320  	// Sort the warnings to make sure they're applied in the same determined order
   321  	// Make a local copy first to avoid race conditions
   322  	warnings := append([]string{}, enabledWarnings...)
   323  	sort.Strings(warnings)
   324  
   325  	// If suggestions are requested and formatted file is not provided, format it to compare modified versions with
   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  // formatWithFix applies a fix, formats a file, and rolls back the fix
   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  // calculateDifference compares two file contents and returns a replacement in the form of
   359  // a 3-tuple (byte from, byte to (non inclusive), a string to replace with).
   360  func calculateDifference(old, new *[]byte) (start, end int, replacement string) {
   361  	commonPrefix := 0 // length of the common prefix
   362  	for i, b := range *old {
   363  		if i >= len(*new) || b != (*new)[i] {
   364  			break
   365  		}
   366  		commonPrefix++
   367  	}
   368  
   369  	commonSuffix := 0 // length of the common suffix
   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  	// In some cases common suffix and prefix can overlap. E.g. consider the following case:
   379  	//   old = "abc"
   380  	//   new = "abdbc"
   381  	// In this case the common prefix is "ab" and the common suffix is "bc".
   382  	// If they overlap, just shorten the suffix so that they don't.
   383  	// The new suffix will be just "c".
   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  // FixWarnings fixes all warnings that can be fixed automatically.
   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  	// Collect list of all warnings.
   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  // AllWarnings is the list of all available warnings.
   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  // DefaultWarnings is the list of all warnings that should be used inside google3
   433  var DefaultWarnings = collectDefaultWarnings()
   434  

View as plain text