github.com/cheshirekow/buildtools@v0.0.0-20200224190056-5d637702fe81/edit/fix.go (about)

     1  /*
     2  Copyright 2016 Google Inc. All Rights Reserved.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6      http://www.apache.org/licenses/LICENSE-2.0
     7  Unless required by applicable law or agreed to in writing, software
     8  distributed under the License is distributed on an "AS IS" BASIS,
     9   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    10   See the License for the specific language governing permissions and
    11   limitations under the License.
    12  */
    13  // Functions to clean and fix BUILD files
    14  
    15  package edit
    16  
    17  import (
    18  	"regexp"
    19  	"sort"
    20  	"strings"
    21  
    22  	"github.com/cheshirekow/buildtools/build"
    23  )
    24  
    25  // splitOptionsWithSpaces is a cleanup function.
    26  // It splits options strings that contain a space. This change
    27  // should be safe as Blaze is splitting those strings, but we will
    28  // eventually get rid of this misfeature.
    29  //   eg. it converts from:
    30  //     copts = ["-Dfoo -Dbar"]
    31  //   to:
    32  //     copts = ["-Dfoo", "-Dbar"]
    33  func splitOptionsWithSpaces(_ *build.File, r *build.Rule, _ string) bool {
    34  	var attrToRewrite = []string{
    35  		"copts",
    36  		"linkopts",
    37  	}
    38  	fixed := false
    39  	for _, attrName := range attrToRewrite {
    40  		attr := r.Attr(attrName)
    41  		if attr != nil {
    42  			for _, li := range AllLists(attr) {
    43  				fixed = splitStrings(li) || fixed
    44  			}
    45  		}
    46  	}
    47  	return fixed
    48  }
    49  
    50  func splitStrings(list *build.ListExpr) bool {
    51  	var all []build.Expr
    52  	fixed := false
    53  	for _, e := range list.List {
    54  		str, ok := e.(*build.StringExpr)
    55  		if !ok {
    56  			all = append(all, e)
    57  			continue
    58  		}
    59  		if strings.Contains(str.Value, " ") && !strings.Contains(str.Value, "'\"") {
    60  			fixed = true
    61  			for i, substr := range strings.Fields(str.Value) {
    62  				item := &build.StringExpr{Value: substr}
    63  				if i == 0 {
    64  					item.Comments = str.Comments
    65  				}
    66  				all = append(all, item)
    67  			}
    68  		} else {
    69  			all = append(all, str)
    70  		}
    71  	}
    72  	list.List = all
    73  	return fixed
    74  }
    75  
    76  // shortenLabels rewrites the labels in the rule using the short notation.
    77  func shortenLabels(_ *build.File, r *build.Rule, pkg string) bool {
    78  	fixed := false
    79  	for _, attr := range r.AttrKeys() {
    80  		e := r.Attr(attr)
    81  		if !ContainsLabels(attr) {
    82  			continue
    83  		}
    84  		for _, li := range AllLists(e) {
    85  			for _, elem := range li.List {
    86  				str, ok := elem.(*build.StringExpr)
    87  				if ok && str.Value != ShortenLabel(str.Value, pkg) {
    88  					str.Value = ShortenLabel(str.Value, pkg)
    89  					fixed = true
    90  				}
    91  			}
    92  		}
    93  	}
    94  	return fixed
    95  }
    96  
    97  // removeVisibility removes useless visibility attributes.
    98  func removeVisibility(f *build.File, r *build.Rule, pkg string) bool {
    99  	pkgDecl := PackageDeclaration(f)
   100  	defaultVisibility := pkgDecl.AttrStrings("default_visibility")
   101  
   102  	// If no default_visibility is given, it is implicitly private.
   103  	if len(defaultVisibility) == 0 {
   104  		defaultVisibility = []string{"//visibility:private"}
   105  	}
   106  
   107  	visibility := r.AttrStrings("visibility")
   108  	if len(visibility) == 0 || len(visibility) != len(defaultVisibility) {
   109  		return false
   110  	}
   111  	sort.Strings(defaultVisibility)
   112  	sort.Strings(visibility)
   113  	for i, vis := range visibility {
   114  		if vis != defaultVisibility[i] {
   115  			return false
   116  		}
   117  	}
   118  	r.DelAttr("visibility")
   119  	return true
   120  }
   121  
   122  // removeTestOnly removes the useless testonly attributes.
   123  func removeTestOnly(f *build.File, r *build.Rule, pkg string) bool {
   124  	pkgDecl := PackageDeclaration(f)
   125  
   126  	def := strings.HasSuffix(r.Kind(), "_test") || r.Kind() == "test_suite"
   127  	if !def {
   128  		if pkgDecl.Attr("default_testonly") == nil {
   129  			def = strings.HasPrefix(pkg, "javatests/")
   130  		} else if pkgDecl.AttrLiteral("default_testonly") == "1" {
   131  			def = true
   132  		} else if pkgDecl.AttrLiteral("default_testonly") != "0" {
   133  			// Non-literal value: it's not safe to do a change.
   134  			return false
   135  		}
   136  	}
   137  
   138  	testonly := r.AttrLiteral("testonly")
   139  	if def && testonly == "1" {
   140  		r.DelAttr("testonly")
   141  		return true
   142  	}
   143  	if !def && testonly == "0" {
   144  		r.DelAttr("testonly")
   145  		return true
   146  	}
   147  	return false
   148  }
   149  
   150  func genruleRenameDepsTools(_ *build.File, r *build.Rule, _ string) bool {
   151  	return r.Kind() == "genrule" && RenameAttribute(r, "deps", "tools") == nil
   152  }
   153  
   154  // explicitHeuristicLabels adds $(location ...) for each label in the string s.
   155  func explicitHeuristicLabels(s string, labels map[string]bool) string {
   156  	// Regexp comes from LABEL_CHAR_MATCHER in
   157  	//   java/com/google/devtools/build/lib/analysis/LabelExpander.java
   158  	re := regexp.MustCompile("[a-zA-Z0-9:/_.+-]+|[^a-zA-Z0-9:/_.+-]+")
   159  	parts := re.FindAllString(s, -1)
   160  	changed := false
   161  	canChange := true
   162  	for i, part := range parts {
   163  		// We don't want to add $(location when it's already present.
   164  		// So we skip the next label when we see location(s).
   165  		if part == "location" || part == "locations" {
   166  			canChange = false
   167  		}
   168  		if !labels[part] {
   169  			if labels[":"+part] { // leading colon is often missing
   170  				part = ":" + part
   171  			} else {
   172  				continue
   173  			}
   174  		}
   175  
   176  		if !canChange {
   177  			canChange = true
   178  			continue
   179  		}
   180  		parts[i] = "$(location " + part + ")"
   181  		changed = true
   182  	}
   183  	if changed {
   184  		return strings.Join(parts, "")
   185  	}
   186  	return s
   187  }
   188  
   189  func addLabels(r *build.Rule, attr string, labels map[string]bool) {
   190  	a := r.Attr(attr)
   191  	if a == nil {
   192  		return
   193  	}
   194  	for _, li := range AllLists(a) {
   195  		for _, item := range li.List {
   196  			if str, ok := item.(*build.StringExpr); ok {
   197  				labels[str.Value] = true
   198  			}
   199  		}
   200  	}
   201  }
   202  
   203  // genruleFixHeuristicLabels modifies the cmd attribute of genrules, so
   204  // that they don't rely on heuristic label expansion anymore.
   205  // Label expansion is made explicit with the $(location ...) command.
   206  func genruleFixHeuristicLabels(_ *build.File, r *build.Rule, _ string) bool {
   207  	if r.Kind() != "genrule" {
   208  		return false
   209  	}
   210  
   211  	cmd := r.Attr("cmd")
   212  	if cmd == nil {
   213  		return false
   214  	}
   215  	labels := make(map[string]bool)
   216  	addLabels(r, "tools", labels)
   217  	addLabels(r, "srcs", labels)
   218  
   219  	fixed := false
   220  	for _, str := range AllStrings(cmd) {
   221  		newVal := explicitHeuristicLabels(str.Value, labels)
   222  		if newVal != str.Value {
   223  			fixed = true
   224  			str.Value = newVal
   225  		}
   226  	}
   227  	return fixed
   228  }
   229  
   230  // sortExportsFiles sorts the first argument of exports_files if it is a list.
   231  func sortExportsFiles(_ *build.File, r *build.Rule, _ string) bool {
   232  	if r.Kind() != "exports_files" || len(r.Call.List) == 0 {
   233  		return false
   234  	}
   235  	build.SortStringList(r.Call.List[0])
   236  	return true
   237  }
   238  
   239  // removeVarref replaces all varref('x') with '$(x)'.
   240  // The goal is to eventually remove varref from the build language.
   241  func removeVarref(_ *build.File, r *build.Rule, _ string) bool {
   242  	fixed := false
   243  	EditFunction(r.Call, "varref", func(call *build.CallExpr, stk []build.Expr) build.Expr {
   244  		if len(call.List) != 1 {
   245  			return nil
   246  		}
   247  		str, ok := (call.List[0]).(*build.StringExpr)
   248  		if !ok {
   249  			return nil
   250  		}
   251  		fixed = true
   252  		str.Value = "$(" + str.Value + ")"
   253  		// Preserve suffix comments from the function call
   254  		str.Comment().Suffix = append(str.Comment().Suffix, call.Comment().Suffix...)
   255  		return str
   256  	})
   257  	return fixed
   258  }
   259  
   260  // sortGlob sorts the list argument to glob.
   261  func sortGlob(_ *build.File, r *build.Rule, _ string) bool {
   262  	fixed := false
   263  	EditFunction(r.Call, "glob", func(call *build.CallExpr, stk []build.Expr) build.Expr {
   264  		if len(call.List) == 0 {
   265  			return nil
   266  		}
   267  		build.SortStringList(call.List[0])
   268  		fixed = true
   269  		return call
   270  	})
   271  	return fixed
   272  }
   273  
   274  func evaluateListConcatenation(expr build.Expr) build.Expr {
   275  	if _, ok := expr.(*build.ListExpr); ok {
   276  		return expr
   277  	}
   278  	bin, ok := expr.(*build.BinaryExpr)
   279  	if !ok || bin.Op != "+" {
   280  		return expr
   281  	}
   282  	li1, ok1 := evaluateListConcatenation(bin.X).(*build.ListExpr)
   283  	li2, ok2 := evaluateListConcatenation(bin.Y).(*build.ListExpr)
   284  	if !ok1 || !ok2 {
   285  		return expr
   286  	}
   287  	res := *li1
   288  	res.List = append(li1.List, li2.List...)
   289  	return &res
   290  }
   291  
   292  // mergeLiteralLists evaluates the concatenation of two literal lists.
   293  // e.g. [1, 2] + [3, 4]  ->  [1, 2, 3, 4]
   294  func mergeLiteralLists(_ *build.File, r *build.Rule, _ string) bool {
   295  	fixed := false
   296  	build.Edit(r.Call, func(expr build.Expr, stk []build.Expr) build.Expr {
   297  		newexpr := evaluateListConcatenation(expr)
   298  		fixed = fixed || (newexpr != expr)
   299  		return newexpr
   300  	})
   301  	return fixed
   302  }
   303  
   304  // usePlusEqual replaces uses of extend and append with the += operator.
   305  // e.g. foo.extend(bar)  =>  foo += bar
   306  //      foo.append(bar)  =>  foo += [bar]
   307  func usePlusEqual(f *build.File) bool {
   308  	fixed := false
   309  	for i, stmt := range f.Stmt {
   310  		call, ok := stmt.(*build.CallExpr)
   311  		if !ok {
   312  			continue
   313  		}
   314  		dot, ok := call.X.(*build.DotExpr)
   315  		if !ok || len(call.List) != 1 {
   316  			continue
   317  		}
   318  		obj, ok := dot.X.(*build.Ident)
   319  		if !ok {
   320  			continue
   321  		}
   322  
   323  		var fix *build.AssignExpr
   324  		if dot.Name == "extend" {
   325  			fix = &build.AssignExpr{LHS: obj, Op: "+=", RHS: call.List[0]}
   326  		} else if dot.Name == "append" {
   327  			list := &build.ListExpr{List: []build.Expr{call.List[0]}}
   328  			fix = &build.AssignExpr{LHS: obj, Op: "+=", RHS: list}
   329  		} else {
   330  			continue
   331  		}
   332  		fix.Comments = call.Comments // Keep original comments
   333  		f.Stmt[i] = fix
   334  		fixed = true
   335  	}
   336  	return fixed
   337  }
   338  
   339  func isNonemptyComment(comment *build.Comments) bool {
   340  	return len(comment.Before)+len(comment.Suffix)+len(comment.After) > 0
   341  }
   342  
   343  // Checks whether a load statement or any of its arguments have a comment
   344  func hasComment(load *build.LoadStmt) bool {
   345  	if isNonemptyComment(load.Comment()) {
   346  		return true
   347  	}
   348  	if isNonemptyComment(load.Module.Comment()) {
   349  		return true
   350  	}
   351  
   352  	for i := range load.From {
   353  		if isNonemptyComment(load.From[i].Comment()) || isNonemptyComment(load.To[i].Comment()) {
   354  			return true
   355  		}
   356  	}
   357  	return false
   358  }
   359  
   360  // cleanUnusedLoads removes symbols from load statements that are not used in the file.
   361  // It also cleans symbols loaded multiple times, sorts symbol list, and removes load
   362  // statements when the list is empty.
   363  func cleanUnusedLoads(f *build.File) bool {
   364  	symbols := UsedSymbols(f)
   365  	fixed := false
   366  
   367  	var all []build.Expr
   368  	for _, stmt := range f.Stmt {
   369  		load, ok := stmt.(*build.LoadStmt)
   370  		if !ok || hasComment(load) {
   371  			all = append(all, stmt)
   372  			continue
   373  		}
   374  		var fromSymbols, toSymbols []*build.Ident
   375  		for i := range load.From {
   376  			fromSymbol := load.From[i]
   377  			toSymbol := load.To[i]
   378  			if symbols[toSymbol.Name] {
   379  				// The symbol is actually used
   380  				fromSymbols = append(fromSymbols, fromSymbol)
   381  				toSymbols = append(toSymbols, toSymbol)
   382  				// If the same symbol is loaded twice, we'll remove it.
   383  				delete(symbols, toSymbol.Name)
   384  			} else {
   385  				fixed = true
   386  			}
   387  		}
   388  		if len(toSymbols) > 0 { // Keep the load statement if it loads at least one symbol.
   389  			sort.Sort(loadArgs{fromSymbols, toSymbols})
   390  			load.From = fromSymbols
   391  			load.To = toSymbols
   392  			all = append(all, load)
   393  		} else {
   394  			fixed = true
   395  		}
   396  	}
   397  	f.Stmt = all
   398  	return fixed
   399  }
   400  
   401  // movePackageDeclarationToTheTop ensures that the call to package() is done
   402  // before everything else (except comments).
   403  func movePackageDeclarationToTheTop(f *build.File) bool {
   404  	pkg := ExistingPackageDeclaration(f)
   405  	if pkg == nil {
   406  		return false
   407  	}
   408  	all := []build.Expr{}
   409  	inserted := false // true when the package declaration has been inserted
   410  	for _, stmt := range f.Stmt {
   411  		_, isComment := stmt.(*build.CommentBlock)
   412  		_, isString := stmt.(*build.StringExpr)     // typically a docstring
   413  		_, isAssignExpr := stmt.(*build.AssignExpr) // e.g. variable declaration
   414  		_, isLoad := stmt.(*build.LoadStmt)
   415  		if isComment || isString || isAssignExpr || isLoad {
   416  			all = append(all, stmt)
   417  			continue
   418  		}
   419  		if stmt == pkg.Call {
   420  			if inserted {
   421  				// remove the old package
   422  				continue
   423  			}
   424  			return false // the file was ok
   425  		}
   426  		if !inserted {
   427  			all = append(all, pkg.Call)
   428  			inserted = true
   429  		}
   430  		all = append(all, stmt)
   431  	}
   432  	f.Stmt = all
   433  	return true
   434  }
   435  
   436  // moveToPackage is an auxilliary function used by moveLicensesAndDistribs.
   437  // The function shouldn't appear more than once in the file (depot cleanup has
   438  // been done).
   439  func moveToPackage(f *build.File, attrname string) bool {
   440  	var all []build.Expr
   441  	fixed := false
   442  	for _, stmt := range f.Stmt {
   443  		rule, ok := ExprToRule(stmt, attrname)
   444  		if !ok || len(rule.Call.List) != 1 {
   445  			all = append(all, stmt)
   446  			continue
   447  		}
   448  		pkgDecl := PackageDeclaration(f)
   449  		pkgDecl.SetAttr(attrname, rule.Call.List[0])
   450  		pkgDecl.AttrDefn(attrname).Comments = *stmt.Comment()
   451  		fixed = true
   452  	}
   453  	f.Stmt = all
   454  	return fixed
   455  }
   456  
   457  // moveLicensesAndDistribs replaces the 'licenses' and 'distribs' functions
   458  // with an attribute in package.
   459  // Before:  licenses(["notice"])
   460  // After:   package(licenses = ["notice"])
   461  func moveLicensesAndDistribs(f *build.File) bool {
   462  	fixed1 := moveToPackage(f, "licenses")
   463  	fixed2 := moveToPackage(f, "distribs")
   464  	return fixed1 || fixed2
   465  }
   466  
   467  // AllRuleFixes is a list of all Buildozer fixes that can be applied on a rule.
   468  var AllRuleFixes = []struct {
   469  	Name    string
   470  	Fn      func(file *build.File, rule *build.Rule, pkg string) bool
   471  	Message string
   472  }{
   473  	{"sortGlob", sortGlob,
   474  		"Sort the list in a call to glob"},
   475  	{"splitOptions", splitOptionsWithSpaces,
   476  		"Each option should be given separately in the list"},
   477  	{"shortenLabels", shortenLabels,
   478  		"Style: Use the canonical label notation"},
   479  	{"removeVisibility", removeVisibility,
   480  		"This visibility attribute is useless (it corresponds to the default value)"},
   481  	{"removeTestOnly", removeTestOnly,
   482  		"This testonly attribute is useless (it corresponds to the default value)"},
   483  	{"genruleRenameDepsTools", genruleRenameDepsTools,
   484  		"'deps' attribute in genrule has been renamed 'tools'"},
   485  	{"genruleFixHeuristicLabels", genruleFixHeuristicLabels,
   486  		"$(location) should be called explicitely"},
   487  	{"sortExportsFiles", sortExportsFiles,
   488  		"Files in exports_files should be sorted"},
   489  	{"varref", removeVarref,
   490  		"All varref('foo') should be replaced with '$foo'"},
   491  	{"mergeLiteralLists", mergeLiteralLists,
   492  		"Remove useless list concatenation"},
   493  }
   494  
   495  // FileLevelFixes is a list of all Buildozer fixes that apply on the whole file.
   496  var FileLevelFixes = []struct {
   497  	Name    string
   498  	Fn      func(file *build.File) bool
   499  	Message string
   500  }{
   501  	{"movePackageToTop", movePackageDeclarationToTheTop,
   502  		"The package declaration should be the first rule in a file"},
   503  	{"usePlusEqual", usePlusEqual,
   504  		"Prefer '+=' over 'extend' or 'append'"},
   505  	{"unusedLoads", cleanUnusedLoads,
   506  		"Remove unused symbols from load statements"},
   507  	{"moveLicensesAndDistribs", moveLicensesAndDistribs,
   508  		"Move licenses and distribs to the package function"},
   509  }
   510  
   511  // FixRule aims to fix errors in BUILD files, remove deprecated features, and
   512  // simplify the code.
   513  func FixRule(f *build.File, pkg string, rule *build.Rule, fixes []string) *build.File {
   514  	fixesAsMap := make(map[string]bool)
   515  	for _, fix := range fixes {
   516  		fixesAsMap[fix] = true
   517  	}
   518  	fixed := false
   519  	for _, fix := range AllRuleFixes {
   520  		if len(fixes) == 0 || fixesAsMap[fix.Name] {
   521  			fixed = fix.Fn(f, rule, pkg) || fixed
   522  		}
   523  	}
   524  	if !fixed {
   525  		return nil
   526  	}
   527  	return f
   528  }
   529  
   530  // FixFile fixes everything it can in the BUILD file.
   531  func FixFile(f *build.File, pkg string, fixes []string) *build.File {
   532  	fixesAsMap := make(map[string]bool)
   533  	for _, fix := range fixes {
   534  		fixesAsMap[fix] = true
   535  	}
   536  	fixed := false
   537  	for _, rule := range f.Rules("") {
   538  		res := FixRule(f, pkg, rule, fixes)
   539  		if res != nil {
   540  			fixed = true
   541  			f = res
   542  		}
   543  	}
   544  	for _, fix := range FileLevelFixes {
   545  		if len(fixes) == 0 || fixesAsMap[fix.Name] {
   546  			fixed = fix.Fn(f) || fixed
   547  		}
   548  	}
   549  	if !fixed {
   550  		return nil
   551  	}
   552  	return f
   553  }
   554  
   555  // A wrapper for a LoadStmt's From and To slices for consistent sorting of their contents.
   556  // It's assumed that the following slices have the same length, the contents are sorted by
   557  // the `To` attribute, the items of `From` are swapped exactly the same way as the items of `To`.
   558  type loadArgs struct {
   559  	From []*build.Ident
   560  	To   []*build.Ident
   561  }
   562  
   563  func (args loadArgs) Len() int {
   564  	return len(args.From)
   565  }
   566  
   567  func (args loadArgs) Swap(i, j int) {
   568  	args.From[i], args.From[j] = args.From[j], args.From[i]
   569  	args.To[i], args.To[j] = args.To[j], args.To[i]
   570  }
   571  
   572  func (args loadArgs) Less(i, j int) bool {
   573  	return args.To[i].Name < args.To[j].Name
   574  }