...

Source file src/github.com/bazelbuild/buildtools/warn/warn_cosmetic.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  // Cosmetic warnings (e.g. for improved readability of Starlark files)
    18  
    19  package warn
    20  
    21  import (
    22  	"regexp"
    23  	"sort"
    24  	"strings"
    25  
    26  	"github.com/bazelbuild/buildtools/build"
    27  	"github.com/bazelbuild/buildtools/edit"
    28  )
    29  
    30  // packageOnTopWarning hoists package statements to the top after any comments / docstrings / load statements.
    31  // If applied together with loadOnTopWarning and/or outOfOrderLoadWarning, it should be applied after them.
    32  // This is currently guaranteed by sorting the warning categories names before applying them:
    33  // "load-on-top" < "out-of-order-load" < "package-on-top"
    34  func packageOnTopWarning(f *build.File) []*LinterFinding {
    35  	if f.Type == build.TypeWorkspace || f.Type == build.TypeModule {
    36  		// Not applicable to WORKSPACE or MODULE files
    37  		return nil
    38  	}
    39  
    40  	// Find the misplaced load statements
    41  	misplacedPackages := make(map[int]*build.CallExpr)
    42  	firstStmtIndex := -1 // index of the first seen non string, comment or load statement
    43  	for i := 0; i < len(f.Stmt); i++ {
    44  		stmt := f.Stmt[i]
    45  
    46  		// Assign statements may define variables that are used by the package statement,
    47  		// e.g. visibility declarations. To avoid false positive detections and also
    48  		// for keeping things simple, the warning should be just suppressed if there's
    49  		// any assignment statement, even if it's not used by the package declaration.
    50  		if _, ok := stmt.(*build.AssignExpr); ok {
    51  			break
    52  		}
    53  
    54  		_, isString := stmt.(*build.StringExpr) // typically a docstring
    55  		_, isComment := stmt.(*build.CommentBlock)
    56  		_, isLoad := stmt.(*build.LoadStmt)
    57  		_, isLicenses := edit.ExprToRule(stmt, "licenses")
    58  		_, isPackageGroup := edit.ExprToRule(stmt, "package_group")
    59  		if isString || isComment || isLoad || isLicenses || isPackageGroup || stmt == nil {
    60  			continue
    61  		}
    62  		rule, ok := edit.ExprToRule(stmt, "package")
    63  		if !ok {
    64  			if firstStmtIndex == -1 {
    65  				firstStmtIndex = i
    66  			}
    67  			continue
    68  		}
    69  		if firstStmtIndex == -1 {
    70  			continue
    71  		}
    72  		misplacedPackages[i] = rule.Call
    73  	}
    74  	offset := len(misplacedPackages)
    75  	if offset == 0 {
    76  		return nil
    77  	}
    78  
    79  	// Calculate a fix:
    80  	if firstStmtIndex == -1 {
    81  		firstStmtIndex = 0
    82  	}
    83  	var replacements []LinterReplacement
    84  	for i := range f.Stmt {
    85  		if i < firstStmtIndex {
    86  			// Docstring or comment or load in the beginning, skip
    87  			continue
    88  		} else if _, ok := misplacedPackages[i]; ok {
    89  			// A misplaced load statement, should be moved up to the `firstStmtIndex` position
    90  			replacements = append(replacements, LinterReplacement{&f.Stmt[firstStmtIndex], f.Stmt[i]})
    91  			firstStmtIndex++
    92  			offset--
    93  			if offset == 0 {
    94  				// No more statements should be moved
    95  				break
    96  			}
    97  		} else {
    98  			// An actual statement (not a docstring or a comment in the beginning), should be moved
    99  			// `offset` positions down.
   100  			replacements = append(replacements, LinterReplacement{&f.Stmt[i+offset], f.Stmt[i]})
   101  		}
   102  	}
   103  
   104  	var findings []*LinterFinding
   105  	for _, load := range misplacedPackages {
   106  		findings = append(findings, makeLinterFinding(load,
   107  			"Package declaration should be at the top of the file, after the load() statements, "+
   108  				"but before any call to a rule or a macro. "+
   109  				"package_group() and licenses() may be called before package().", replacements...))
   110  	}
   111  
   112  	return findings
   113  }
   114  
   115  func unsortedDictItemsWarning(f *build.File) []*LinterFinding {
   116  	var findings []*LinterFinding
   117  
   118  	compareItems := func(item1, item2 *build.KeyValueExpr) bool {
   119  		key1 := item1.Key.(*build.StringExpr).Value
   120  		key2 := item2.Key.(*build.StringExpr).Value
   121  		// regular keys should precede private ones (start with "_")
   122  		if strings.HasPrefix(key1, "_") {
   123  			return strings.HasPrefix(key2, "_") && key1 < key2
   124  		}
   125  		if strings.HasPrefix(key2, "_") {
   126  			return true
   127  		}
   128  
   129  		// "//conditions:default" should always be the last
   130  		const conditionsDefault = "//conditions:default"
   131  		if key1 == conditionsDefault {
   132  			return false
   133  		} else if key2 == conditionsDefault {
   134  			return true
   135  		}
   136  
   137  		return key1 < key2
   138  	}
   139  
   140  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   141  		dict, ok := (*expr).(*build.DictExpr)
   142  
   143  		mustSkipCheck := func(expr build.Expr) bool {
   144  			return edit.ContainsComments(expr, "@unsorted-dict-items")
   145  		}
   146  
   147  		if !ok || mustSkipCheck(dict) {
   148  			return
   149  		}
   150  		// do not process dictionaries nested within expressions that do not
   151  		// want dict items to be sorted
   152  		for i := len(stack) - 1; i >= 0; i-- {
   153  			if mustSkipCheck(stack[i]) {
   154  				return
   155  			}
   156  		}
   157  		var sortedItems []*build.KeyValueExpr
   158  		for _, item := range dict.List {
   159  			// include only string literal keys into consideration
   160  			if _, ok = item.Key.(*build.StringExpr); !ok {
   161  				continue
   162  			}
   163  			sortedItems = append(sortedItems, item)
   164  		}
   165  
   166  		// Fix
   167  		comp := func(i, j int) bool {
   168  			return compareItems(sortedItems[i], sortedItems[j])
   169  		}
   170  
   171  		var misplacedItems []*build.KeyValueExpr
   172  		for i := 1; i < len(sortedItems); i++ {
   173  			if comp(i, i-1) {
   174  				misplacedItems = append(misplacedItems, sortedItems[i])
   175  			}
   176  		}
   177  
   178  		if len(misplacedItems) == 0 {
   179  			// Already sorted
   180  			return
   181  		}
   182  		newDict := *dict
   183  		newDict.List = append([]*build.KeyValueExpr{}, dict.List...)
   184  
   185  		sort.SliceStable(sortedItems, comp)
   186  		sortedItemIndex := 0
   187  		for originalItemIndex := 0; originalItemIndex < len(dict.List); originalItemIndex++ {
   188  			item := dict.List[originalItemIndex]
   189  			if _, ok := item.Key.(*build.StringExpr); !ok {
   190  				continue
   191  			}
   192  			newDict.List[originalItemIndex] = sortedItems[sortedItemIndex]
   193  			sortedItemIndex++
   194  		}
   195  
   196  		for _, item := range misplacedItems {
   197  			findings = append(findings, makeLinterFinding(item,
   198  				"Dictionary items are out of their lexicographical order.",
   199  				LinterReplacement{expr, &newDict}))
   200  		}
   201  		return
   202  	})
   203  	return findings
   204  }
   205  
   206  // skylarkToStarlark converts a string "skylark" in different cases to "starlark"
   207  // trying to preserve the same case style, e.g. capitalized "Skylark" becomes "Starlark".
   208  func skylarkToStarlark(s string) string {
   209  	switch {
   210  	case s == "SKYLARK":
   211  		return "STARLARK"
   212  	case strings.HasPrefix(s, "S"):
   213  		return "Starlark"
   214  	default:
   215  		return "starlark"
   216  	}
   217  }
   218  
   219  // replaceSkylark replaces a substring "skylark" (case-insensitive) with a
   220  // similar cased string "starlark". Doesn't replace it if the previous or the
   221  // next symbol is '/', which may indicate it's a part of a URL.
   222  // Normally that should be done with look-ahead and look-behind assertions in a
   223  // regular expression, but negative look-aheads and look-behinds are not
   224  // supported by Go regexp module.
   225  func replaceSkylark(s string) (newString string, changed bool) {
   226  	skylarkRegex := regexp.MustCompile("(?i)skylark")
   227  	newString = s
   228  	for _, r := range skylarkRegex.FindAllStringIndex(s, -1) {
   229  		if r[0] > 0 && s[r[0]-1] == '/' {
   230  			continue
   231  		}
   232  		if r[1] < len(s)-1 && s[r[1]+1] == '/' {
   233  			continue
   234  		}
   235  		newString = newString[:r[0]] + skylarkToStarlark(newString[r[0]:r[1]]) + newString[r[1]:]
   236  	}
   237  	return newString, newString != s
   238  }
   239  
   240  func skylarkCommentWarning(f *build.File) []*LinterFinding {
   241  	var findings []*LinterFinding
   242  	msg := `"Skylark" is an outdated name of the language, please use "starlark" instead.`
   243  
   244  	// Check comments
   245  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   246  		comments := (*expr).Comment()
   247  		newComments := build.Comments{
   248  			Before: append([]build.Comment{}, comments.Before...),
   249  			Suffix: append([]build.Comment{}, comments.Suffix...),
   250  			After:  append([]build.Comment{}, comments.After...),
   251  		}
   252  		isModified := false
   253  		var start, end build.Position
   254  
   255  		for _, block := range []*[]build.Comment{&newComments.Before, &newComments.Suffix, &newComments.After} {
   256  			for i, comment := range *block {
   257  				// Don't trigger on disabling comments
   258  				if strings.Contains(comment.Token, "disable=skylark-docstring") {
   259  					continue
   260  				}
   261  				newValue, changed := replaceSkylark(comment.Token)
   262  				(*block)[i] = build.Comment{
   263  					Start: comment.Start,
   264  					Token: newValue,
   265  				}
   266  				if changed {
   267  					isModified = true
   268  					start, end = comment.Span()
   269  				}
   270  			}
   271  		}
   272  		if !isModified {
   273  			return
   274  		}
   275  		newExpr := (*expr).Copy()
   276  		newExpr.Comment().Before = newComments.Before
   277  		newExpr.Comment().Suffix = newComments.Suffix
   278  		newExpr.Comment().After = newComments.After
   279  		finding := makeLinterFinding(*expr, msg, LinterReplacement{expr, newExpr})
   280  		finding.Start = start
   281  		finding.End = end
   282  		findings = append(findings, finding)
   283  	})
   284  
   285  	return findings
   286  }
   287  
   288  func checkSkylarkDocstring(stmts []build.Expr) *LinterFinding {
   289  	msg := `"Skylark" is an outdated name of the language, please use "starlark" instead.`
   290  
   291  	doc, ok := getDocstring(stmts)
   292  	if !ok {
   293  		return nil
   294  	}
   295  	docString := (*doc).(*build.StringExpr)
   296  	newValue, updated := replaceSkylark(docString.Value)
   297  	if !updated {
   298  		return nil
   299  	}
   300  	newDocString := *docString
   301  	newDocString.Value = newValue
   302  	return makeLinterFinding(docString, msg, LinterReplacement{doc, &newDocString})
   303  }
   304  
   305  func skylarkDocstringWarning(f *build.File) []*LinterFinding {
   306  	var findings []*LinterFinding
   307  
   308  	// File docstring
   309  	if finding := checkSkylarkDocstring(f.Stmt); finding != nil {
   310  		findings = append(findings, finding)
   311  	}
   312  
   313  	// Function docstrings
   314  	for _, stmt := range f.Stmt {
   315  		def, ok := stmt.(*build.DefStmt)
   316  		if !ok {
   317  			continue
   318  		}
   319  		if finding := checkSkylarkDocstring(def.Body); finding != nil {
   320  			findings = append(findings, finding)
   321  		}
   322  	}
   323  
   324  	return findings
   325  }
   326  

View as plain text