...

Source file src/github.com/bazelbuild/buildtools/warn/warn_bazel_api.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  // Warnings for incompatible changes in the Bazel API
    18  
    19  package warn
    20  
    21  import (
    22  	"fmt"
    23  	"sort"
    24  
    25  	"github.com/bazelbuild/buildtools/build"
    26  	"github.com/bazelbuild/buildtools/bzlenv"
    27  	"github.com/bazelbuild/buildtools/edit"
    28  	"github.com/bazelbuild/buildtools/tables"
    29  )
    30  
    31  // Bazel API-specific warnings
    32  
    33  // negateExpression returns an expression which is a negation of the input.
    34  // If it's a boolean literal (true or false), just return the opposite literal.
    35  // If it's a unary expression with a unary `not` operator, just remove it.
    36  // Otherwise, insert a `not` operator.
    37  // It's assumed that input is no longer needed as it may be mutated or reused by the function.
    38  func negateExpression(expr build.Expr) build.Expr {
    39  	paren, ok := expr.(*build.ParenExpr)
    40  	if ok {
    41  		newParen := *paren
    42  		newParen.X = negateExpression(paren.X)
    43  		return &newParen
    44  	}
    45  
    46  	unary, ok := expr.(*build.UnaryExpr)
    47  	if ok && unary.Op == "not" {
    48  		return unary.X
    49  	}
    50  
    51  	boolean, ok := expr.(*build.Ident)
    52  	if ok {
    53  		newBoolean := *boolean
    54  		if boolean.Name == "True" {
    55  			newBoolean.Name = "False"
    56  		} else {
    57  			newBoolean.Name = "True"
    58  		}
    59  		return &newBoolean
    60  	}
    61  
    62  	return &build.UnaryExpr{
    63  		Op: "not",
    64  		X:  expr,
    65  	}
    66  }
    67  
    68  // getParam search for a param with a given name in a given list of function arguments
    69  // and returns it with its index
    70  func getParam(attrs []build.Expr, paramName string) (int, *build.Ident, *build.AssignExpr) {
    71  	for i, attr := range attrs {
    72  		as, ok := attr.(*build.AssignExpr)
    73  		if !ok {
    74  			continue
    75  		}
    76  		name, ok := (as.LHS).(*build.Ident)
    77  		if !ok || name.Name != paramName {
    78  			continue
    79  		}
    80  		return i, name, as
    81  	}
    82  	return -1, nil, nil
    83  }
    84  
    85  // isFunctionCall checks whether expr is a call of a function with a given name
    86  func isFunctionCall(expr build.Expr, name string) (*build.CallExpr, bool) {
    87  	call, ok := expr.(*build.CallExpr)
    88  	if !ok {
    89  		return nil, false
    90  	}
    91  	if ident, ok := call.X.(*build.Ident); ok && ident.Name == name {
    92  		return call, true
    93  	}
    94  	return nil, false
    95  }
    96  
    97  // globalVariableUsageCheck checks whether there's a usage of a given global variable in the file.
    98  // It's ok to shadow the name with a local variable and use it.
    99  func globalVariableUsageCheck(f *build.File, global, alternative string) []*LinterFinding {
   100  	var findings []*LinterFinding
   101  
   102  	if f.Type != build.TypeBzl {
   103  		return findings
   104  	}
   105  
   106  	var walk func(e *build.Expr, env *bzlenv.Environment)
   107  	walk = func(e *build.Expr, env *bzlenv.Environment) {
   108  		defer bzlenv.WalkOnceWithEnvironment(*e, env, walk)
   109  
   110  		ident, ok := (*e).(*build.Ident)
   111  		if !ok {
   112  			return
   113  		}
   114  		if ident.Name != global {
   115  			return
   116  		}
   117  		if binding := env.Get(ident.Name); binding != nil {
   118  			return
   119  		}
   120  
   121  		// Fix
   122  		newIdent := *ident
   123  		newIdent.Name = alternative
   124  
   125  		findings = append(findings, makeLinterFinding(ident,
   126  			fmt.Sprintf(`Global variable %q is deprecated in favor of %q. Please rename it.`, global, alternative),
   127  			LinterReplacement{e, &newIdent}))
   128  	}
   129  	var expr build.Expr = f
   130  	walk(&expr, bzlenv.NewEnvironment())
   131  
   132  	return findings
   133  }
   134  
   135  // insertLoad returns a *LinterReplacement object representing a replacement required for inserting
   136  // an additional load statement. Returns nil if nothing needs to be changed.
   137  func insertLoad(f *build.File, module string, symbols []string) *LinterReplacement {
   138  	// Try to find an existing load statement
   139  	for i, stmt := range f.Stmt {
   140  		load, ok := stmt.(*build.LoadStmt)
   141  		if !ok || load.Module.Value != module {
   142  			continue
   143  		}
   144  
   145  		// Modify an existing load statement
   146  		newLoad := *load
   147  		if !edit.AppendToLoad(&newLoad, symbols, symbols) {
   148  			return nil
   149  		}
   150  		return &LinterReplacement{&(f.Stmt[i]), &newLoad}
   151  	}
   152  
   153  	// Need to insert a new load statement. Can't modify the tree here, so just insert a placeholder
   154  	// nil statement and return a replacement for it.
   155  	i := 0
   156  	for i = range f.Stmt {
   157  		stmt := f.Stmt[i]
   158  		_, isComment := stmt.(*build.CommentBlock)
   159  		_, isString := stmt.(*build.StringExpr)
   160  		isDocString := isString && i == 0
   161  		if !isComment && !isDocString {
   162  			// Insert a nil statement here
   163  			break
   164  		}
   165  	}
   166  	stmts := append([]build.Expr{}, f.Stmt[:i]...)
   167  	stmts = append(stmts, nil)
   168  	stmts = append(stmts, f.Stmt[i:]...)
   169  	f.Stmt = stmts
   170  
   171  	return &LinterReplacement{&(f.Stmt[i]), edit.NewLoad(module, symbols, symbols)}
   172  }
   173  
   174  func notLoadedFunctionUsageCheckInternal(expr *build.Expr, env *bzlenv.Environment, globals []string, loadFrom string) ([]string, []*LinterFinding) {
   175  	var loads []string
   176  	var findings []*LinterFinding
   177  
   178  	call, ok := (*expr).(*build.CallExpr)
   179  	if !ok {
   180  		return loads, findings
   181  	}
   182  
   183  	var name string
   184  	var replacements []LinterReplacement
   185  	switch node := call.X.(type) {
   186  	case *build.DotExpr:
   187  		// Maybe native.`global`?
   188  		ident, ok := node.X.(*build.Ident)
   189  		if !ok || ident.Name != "native" {
   190  			return loads, findings
   191  		}
   192  
   193  		name = node.Name
   194  		// Replace `native.foo()` with `foo()`
   195  		newCall := *call
   196  		newCall.X = &build.Ident{Name: node.Name}
   197  		replacements = append(replacements, LinterReplacement{expr, &newCall})
   198  	case *build.Ident:
   199  		// Maybe `global`()?
   200  		if binding := env.Get(node.Name); binding != nil {
   201  			return loads, findings
   202  		}
   203  		name = node.Name
   204  	default:
   205  		return loads, findings
   206  	}
   207  
   208  	for _, global := range globals {
   209  		if name == global {
   210  			loads = append(loads, name)
   211  			findings = append(findings,
   212  				makeLinterFinding(call.X, fmt.Sprintf(`Function %q is not global anymore and needs to be loaded from %q.`, global, loadFrom), replacements...))
   213  			break
   214  		}
   215  	}
   216  
   217  	return loads, findings
   218  }
   219  
   220  func notLoadedSymbolUsageCheckInternal(expr *build.Expr, env *bzlenv.Environment, globals []string, loadFrom string) ([]string, []*LinterFinding) {
   221  	var loads []string
   222  	var findings []*LinterFinding
   223  
   224  	ident, ok := (*expr).(*build.Ident)
   225  	if !ok {
   226  		return loads, findings
   227  	}
   228  	if binding := env.Get(ident.Name); binding != nil {
   229  		return loads, findings
   230  	}
   231  
   232  	for _, global := range globals {
   233  		if ident.Name == global {
   234  			loads = append(loads, ident.Name)
   235  			findings = append(findings,
   236  				makeLinterFinding(ident, fmt.Sprintf(`Symbol %q is not global anymore and needs to be loaded from %q.`, global, loadFrom)))
   237  			break
   238  		}
   239  	}
   240  
   241  	return loads, findings
   242  }
   243  
   244  // notLoadedUsageCheck checks whether there's a usage of a given not imported function or symbol in the file
   245  // and adds a load statement if necessary.
   246  func notLoadedUsageCheck(f *build.File, functions, symbols []string, loadFrom string) []*LinterFinding {
   247  	toLoad := make(map[string]bool)
   248  	var findings []*LinterFinding
   249  
   250  	var walk func(expr *build.Expr, env *bzlenv.Environment)
   251  	walk = func(expr *build.Expr, env *bzlenv.Environment) {
   252  		defer bzlenv.WalkOnceWithEnvironment(*expr, env, walk)
   253  
   254  		functionLoads, functionFindings := notLoadedFunctionUsageCheckInternal(expr, env, functions, loadFrom)
   255  		findings = append(findings, functionFindings...)
   256  		for _, load := range functionLoads {
   257  			toLoad[load] = true
   258  		}
   259  
   260  		symbolLoads, symbolFindings := notLoadedSymbolUsageCheckInternal(expr, env, symbols, loadFrom)
   261  		findings = append(findings, symbolFindings...)
   262  		for _, load := range symbolLoads {
   263  			toLoad[load] = true
   264  		}
   265  	}
   266  	var expr build.Expr = f
   267  	walk(&expr, bzlenv.NewEnvironment())
   268  
   269  	if len(toLoad) == 0 {
   270  		return nil
   271  	}
   272  
   273  	loads := []string{}
   274  	for l := range toLoad {
   275  		loads = append(loads, l)
   276  	}
   277  
   278  	sort.Strings(loads)
   279  	replacement := insertLoad(f, loadFrom, loads)
   280  	if replacement != nil {
   281  		// Add the same replacement to all relevant findings.
   282  		for _, f := range findings {
   283  			f.Replacement = append(f.Replacement, *replacement)
   284  		}
   285  	}
   286  
   287  	return findings
   288  }
   289  
   290  // NotLoadedFunctionUsageCheck checks whether there's a usage of a given not imported function in the file
   291  // and adds a load statement if necessary.
   292  func NotLoadedFunctionUsageCheck(f *build.File, globals []string, loadFrom string) []*LinterFinding {
   293  	return notLoadedUsageCheck(f, globals, []string{}, loadFrom)
   294  }
   295  
   296  // makePositional makes the function argument positional (removes the keyword if it exists)
   297  func makePositional(argument build.Expr) build.Expr {
   298  	if binary, ok := argument.(*build.AssignExpr); ok {
   299  		return binary.RHS
   300  	}
   301  	return argument
   302  }
   303  
   304  // makeKeyword makes the function argument keyword (adds or edits the keyword name)
   305  func makeKeyword(argument build.Expr, name string) build.Expr {
   306  	assign, ok := argument.(*build.AssignExpr)
   307  	if !ok {
   308  		return &build.AssignExpr{
   309  			LHS: &build.Ident{Name: name},
   310  			Op:  "=",
   311  			RHS: argument,
   312  		}
   313  	}
   314  	ident, ok := assign.LHS.(*build.Ident)
   315  	if ok && ident.Name == name {
   316  		// Nothing to change
   317  		return argument
   318  	}
   319  
   320  	// Technically it's possible that the LHS is not an ident, but that is a syntax error anyway.
   321  	newAssign := *assign
   322  	newAssign.LHS = &build.Ident{Name: name}
   323  	return &newAssign
   324  }
   325  
   326  func attrConfigurationWarning(f *build.File) []*LinterFinding {
   327  	if f.Type != build.TypeBzl {
   328  		return nil
   329  	}
   330  
   331  	var findings []*LinterFinding
   332  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   333  		// Find nodes that match the following pattern: attr.xxxx(..., cfg = "data", ...) and attr.xxxx(..., cfg = "host", ...)
   334  		call, ok := (*expr).(*build.CallExpr)
   335  		if !ok {
   336  			return
   337  		}
   338  		dot, ok := (call.X).(*build.DotExpr)
   339  		if !ok {
   340  			return
   341  		}
   342  		base, ok := dot.X.(*build.Ident)
   343  		if !ok || base.Name != "attr" {
   344  			return
   345  		}
   346  		i, _, param := getParam(call.List, "cfg")
   347  		if param == nil {
   348  			return
   349  		}
   350  		value, ok := (param.RHS).(*build.StringExpr)
   351  		if !ok {
   352  			return
   353  		}
   354  
   355  		newCall := *call
   356  		switch value.Value {
   357  		case "data":
   358  			newCall.List = append(newCall.List[:i], newCall.List[i+1:]...)
   359  			findings = append(findings,
   360  				makeLinterFinding(param, `cfg = "data" for attr definitions has no effect and should be removed.`,
   361  					LinterReplacement{expr, &newCall}))
   362  
   363  		case "host":
   364  			{
   365  				newCall.List = append([]build.Expr{}, newCall.List...)
   366  				newParam := newCall.List[i].Copy().(*build.AssignExpr)
   367  				newRHS := newParam.RHS.Copy().(*build.StringExpr)
   368  				newRHS.Value = "exec"
   369  				newParam.RHS = newRHS
   370  				newCall.List[i] = newParam
   371  				findings = append(findings,
   372  					makeLinterFinding(param, `cfg = "host" for attr definitions should be replaced by cfg = "exec".`,
   373  						LinterReplacement{expr, &newCall}))
   374  			}
   375  
   376  		default:
   377  			// value not matched.
   378  			return
   379  		}
   380  	})
   381  	return findings
   382  }
   383  
   384  func depsetItemsWarning(f *build.File) []*LinterFinding {
   385  	var findings []*LinterFinding
   386  
   387  	types := DetectTypes(f)
   388  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   389  		call, ok := (*expr).(*build.CallExpr)
   390  		if !ok {
   391  			return
   392  		}
   393  		base, ok := call.X.(*build.Ident)
   394  		if !ok || base.Name != "depset" {
   395  			return
   396  		}
   397  		if len(call.List) == 0 {
   398  			return
   399  		}
   400  		_, _, param := getParam(call.List, "items")
   401  		if param != nil {
   402  			findings = append(findings,
   403  				makeLinterFinding(param, `Parameter "items" is deprecated, use "direct" and/or "transitive" instead.`))
   404  			return
   405  		}
   406  		if _, ok := call.List[0].(*build.AssignExpr); ok {
   407  			return
   408  		}
   409  		// We have an unnamed first parameter. Check the type.
   410  		if types[call.List[0]] == Depset {
   411  			findings = append(findings,
   412  				makeLinterFinding(call.List[0], `Giving a depset as first unnamed parameter to depset() is deprecated, use the "transitive" parameter instead.`))
   413  		}
   414  	})
   415  	return findings
   416  }
   417  
   418  func attrNonEmptyWarning(f *build.File) []*LinterFinding {
   419  	if f.Type != build.TypeBzl {
   420  		return nil
   421  	}
   422  
   423  	var findings []*LinterFinding
   424  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   425  		// Find nodes that match the following pattern: attr.xxxx(..., non_empty = ..., ...)
   426  		call, ok := (*expr).(*build.CallExpr)
   427  		if !ok {
   428  			return
   429  		}
   430  		dot, ok := (call.X).(*build.DotExpr)
   431  		if !ok {
   432  			return
   433  		}
   434  		base, ok := dot.X.(*build.Ident)
   435  		if !ok || base.Name != "attr" {
   436  			return
   437  		}
   438  		_, name, param := getParam(call.List, "non_empty")
   439  		if param == nil {
   440  			return
   441  		}
   442  
   443  		// Fix
   444  		newName := *name
   445  		newName.Name = "allow_empty"
   446  		negatedRHS := negateExpression(param.RHS)
   447  
   448  		findings = append(findings,
   449  			makeLinterFinding(param, "non_empty attributes for attr definitions are deprecated in favor of allow_empty.",
   450  				LinterReplacement{&param.LHS, &newName},
   451  				LinterReplacement{&param.RHS, negatedRHS},
   452  			))
   453  	})
   454  	return findings
   455  }
   456  
   457  func attrSingleFileWarning(f *build.File) []*LinterFinding {
   458  	if f.Type != build.TypeBzl {
   459  		return nil
   460  	}
   461  
   462  	var findings []*LinterFinding
   463  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   464  		// Find nodes that match the following pattern: attr.xxxx(..., single_file = ..., ...)
   465  		call, ok := (*expr).(*build.CallExpr)
   466  		if !ok {
   467  			return
   468  		}
   469  		dot, ok := (call.X).(*build.DotExpr)
   470  		if !ok {
   471  			return
   472  		}
   473  		base, ok := dot.X.(*build.Ident)
   474  		if !ok || base.Name != "attr" {
   475  			return
   476  		}
   477  		singleFileIndex, singleFileKw, singleFileParam := getParam(call.List, "single_file")
   478  		if singleFileParam == nil {
   479  			return
   480  		}
   481  
   482  		// Fix
   483  		newCall := *call
   484  		newCall.List = append([]build.Expr{}, call.List...)
   485  
   486  		newSingleFileKw := *singleFileKw
   487  		newSingleFileKw.Name = "allow_single_file"
   488  		singleFileValue := singleFileParam.RHS
   489  
   490  		if boolean, ok := singleFileValue.(*build.Ident); ok && boolean.Name == "False" {
   491  			// if the value is `False`, just remove the whole parameter
   492  			newCall.List = append(newCall.List[:singleFileIndex], newCall.List[singleFileIndex+1:]...)
   493  		} else {
   494  			// search for `allow_files` parameter in the same attr definition and remove it
   495  			allowFileIndex, _, allowFilesParam := getParam(call.List, "allow_files")
   496  			if allowFilesParam != nil {
   497  				singleFileValue = allowFilesParam.RHS
   498  				newCall.List = append(newCall.List[:allowFileIndex], newCall.List[allowFileIndex+1:]...)
   499  				if singleFileIndex > allowFileIndex {
   500  					singleFileIndex--
   501  				}
   502  			}
   503  		}
   504  		findings = append(findings,
   505  			makeLinterFinding(singleFileParam, "single_file is deprecated in favor of allow_single_file.",
   506  				LinterReplacement{expr, &newCall},
   507  				LinterReplacement{&singleFileParam.LHS, &newSingleFileKw},
   508  				LinterReplacement{&singleFileParam.RHS, singleFileValue},
   509  			))
   510  	})
   511  	return findings
   512  }
   513  
   514  func ctxActionsWarning(f *build.File) []*LinterFinding {
   515  	if f.Type != build.TypeBzl {
   516  		return nil
   517  	}
   518  
   519  	var findings []*LinterFinding
   520  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   521  		// Find nodes that match the following pattern: ctx.xxxx(...)
   522  		call, ok := (*expr).(*build.CallExpr)
   523  		if !ok {
   524  			return
   525  		}
   526  		dot, ok := (call.X).(*build.DotExpr)
   527  		if !ok {
   528  			return
   529  		}
   530  		base, ok := dot.X.(*build.Ident)
   531  		if !ok || base.Name != "ctx" {
   532  			return
   533  		}
   534  
   535  		switch dot.Name {
   536  		case "new_file", "experimental_new_directory", "file_action", "action", "empty_action", "template_action":
   537  			// fix
   538  		default:
   539  			return
   540  		}
   541  
   542  		// Fix
   543  		newCall := *call
   544  		newCall.List = append([]build.Expr{}, call.List...)
   545  		newDot := *dot
   546  		newCall.X = &newDot
   547  
   548  		switch dot.Name {
   549  		case "new_file":
   550  			if len(call.List) > 2 {
   551  				// Can't fix automatically because the new API doesn't support the 3 arguments signature
   552  				findings = append(findings,
   553  					makeLinterFinding(dot, fmt.Sprintf(`"ctx.new_file" is deprecated in favor of "ctx.actions.declare_file".`)))
   554  				return
   555  			}
   556  			newDot.Name = "actions.declare_file"
   557  			if len(call.List) == 2 {
   558  				// swap arguments:
   559  				// ctx.new_file(sibling, name) -> ctx.actions.declare_file(name, sibling=sibling)
   560  				newCall.List[0], newCall.List[1] = makePositional(call.List[1]), makeKeyword(call.List[0], "sibling")
   561  			}
   562  		case "experimental_new_directory":
   563  			newDot.Name = "actions.declare_directory"
   564  		case "file_action":
   565  			newDot.Name = "actions.write"
   566  			i, ident, param := getParam(newCall.List, "executable")
   567  			if ident != nil {
   568  				newIdent := *ident
   569  				newIdent.Name = "is_executable"
   570  				newParam := *param
   571  				newParam.LHS = &newIdent
   572  				newCall.List[i] = &newParam
   573  			}
   574  		case "action":
   575  			newDot.Name = "actions.run"
   576  			if _, _, command := getParam(call.List, "command"); command != nil {
   577  				newDot.Name = "actions.run_shell"
   578  			}
   579  		case "empty_action":
   580  			newDot.Name = "actions.do_nothing"
   581  		case "template_action":
   582  			newDot.Name = "actions.expand_template"
   583  			if i, ident, param := getParam(call.List, "executable"); ident != nil {
   584  				newIdent := *ident
   585  				newIdent.Name = "is_executable"
   586  				newParam := *param
   587  				newParam.LHS = &newIdent
   588  				newCall.List[i] = &newParam
   589  			}
   590  		}
   591  
   592  		findings = append(findings, makeLinterFinding(dot,
   593  			fmt.Sprintf(`"ctx.%s" is deprecated in favor of "ctx.%s".`, dot.Name, newDot.Name),
   594  			LinterReplacement{expr, &newCall}))
   595  	})
   596  	return findings
   597  }
   598  
   599  func fileTypeWarning(f *build.File) []*LinterFinding {
   600  	if f.Type != build.TypeBzl {
   601  		return nil
   602  	}
   603  
   604  	var findings []*LinterFinding
   605  	var walk func(e *build.Expr, env *bzlenv.Environment)
   606  	walk = func(e *build.Expr, env *bzlenv.Environment) {
   607  		defer bzlenv.WalkOnceWithEnvironment(*e, env, walk)
   608  
   609  		call, ok := isFunctionCall(*e, "FileType")
   610  		if !ok {
   611  			return
   612  		}
   613  		if binding := env.Get("FileType"); binding == nil {
   614  			findings = append(findings,
   615  				makeLinterFinding(call, "The FileType function is deprecated."))
   616  		}
   617  	}
   618  	var expr build.Expr = f
   619  	walk(&expr, bzlenv.NewEnvironment())
   620  
   621  	return findings
   622  }
   623  
   624  func packageNameWarning(f *build.File) []*LinterFinding {
   625  	return globalVariableUsageCheck(f, "PACKAGE_NAME", "native.package_name()")
   626  }
   627  
   628  func repositoryNameWarning(f *build.File) []*LinterFinding {
   629  	return globalVariableUsageCheck(f, "REPOSITORY_NAME", "native.repository_name()")
   630  }
   631  
   632  func outputGroupWarning(f *build.File) []*LinterFinding {
   633  	if f.Type != build.TypeBzl {
   634  		return nil
   635  	}
   636  
   637  	var findings []*LinterFinding
   638  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   639  		// Find nodes that match the following pattern: ctx.attr.xxx.output_group
   640  		outputGroup, ok := (*expr).(*build.DotExpr)
   641  		if !ok || outputGroup.Name != "output_group" {
   642  			return
   643  		}
   644  		dep, ok := (outputGroup.X).(*build.DotExpr)
   645  		if !ok {
   646  			return
   647  		}
   648  		attr, ok := (dep.X).(*build.DotExpr)
   649  		if !ok || attr.Name != "attr" {
   650  			return
   651  		}
   652  		ctx, ok := (attr.X).(*build.Ident)
   653  		if !ok || ctx.Name != "ctx" {
   654  			return
   655  		}
   656  
   657  		// Replace `xxx.output_group` with `xxx[OutputGroupInfo]`
   658  		findings = append(findings,
   659  			makeLinterFinding(outputGroup,
   660  				`"ctx.attr.dep.output_group" is deprecated in favor of "ctx.attr.dep[OutputGroupInfo]".`,
   661  				LinterReplacement{expr, &build.IndexExpr{
   662  					X: dep,
   663  					Y: &build.Ident{Name: "OutputGroupInfo"},
   664  				},
   665  				}))
   666  	})
   667  	return findings
   668  }
   669  
   670  func nativeGitRepositoryWarning(f *build.File) []*LinterFinding {
   671  	if f.Type != build.TypeBzl {
   672  		return nil
   673  	}
   674  	return NotLoadedFunctionUsageCheck(f, []string{"git_repository", "new_git_repository"}, "@bazel_tools//tools/build_defs/repo:git.bzl")
   675  }
   676  
   677  func nativeHTTPArchiveWarning(f *build.File) []*LinterFinding {
   678  	if f.Type != build.TypeBzl {
   679  		return nil
   680  	}
   681  	return NotLoadedFunctionUsageCheck(f, []string{"http_archive"}, "@bazel_tools//tools/build_defs/repo:http.bzl")
   682  }
   683  
   684  func nativeAndroidRulesWarning(f *build.File) []*LinterFinding {
   685  	if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
   686  		return nil
   687  	}
   688  	return NotLoadedFunctionUsageCheck(f, tables.AndroidNativeRules, tables.AndroidLoadPath)
   689  }
   690  
   691  func nativeCcRulesWarning(f *build.File) []*LinterFinding {
   692  	if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
   693  		return nil
   694  	}
   695  	return NotLoadedFunctionUsageCheck(f, tables.CcNativeRules, tables.CcLoadPath)
   696  }
   697  
   698  func nativeJavaRulesWarning(f *build.File) []*LinterFinding {
   699  	if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
   700  		return nil
   701  	}
   702  	return NotLoadedFunctionUsageCheck(f, tables.JavaNativeRules, tables.JavaLoadPath)
   703  }
   704  
   705  func nativePyRulesWarning(f *build.File) []*LinterFinding {
   706  	if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
   707  		return nil
   708  	}
   709  	return NotLoadedFunctionUsageCheck(f, tables.PyNativeRules, tables.PyLoadPath)
   710  }
   711  
   712  func nativeProtoRulesWarning(f *build.File) []*LinterFinding {
   713  	if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
   714  		return nil
   715  	}
   716  	return notLoadedUsageCheck(f, tables.ProtoNativeRules, tables.ProtoNativeSymbols, tables.ProtoLoadPath)
   717  }
   718  
   719  func contextArgsAPIWarning(f *build.File) []*LinterFinding {
   720  	if f.Type != build.TypeBzl {
   721  		return nil
   722  	}
   723  
   724  	var findings []*LinterFinding
   725  	types := DetectTypes(f)
   726  
   727  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   728  		// Search for `<ctx.actions.args>.add()` nodes
   729  		call, ok := (*expr).(*build.CallExpr)
   730  		if !ok {
   731  			return
   732  		}
   733  		dot, ok := call.X.(*build.DotExpr)
   734  		if !ok || dot.Name != "add" || types[dot.X] != CtxActionsArgs {
   735  			return
   736  		}
   737  
   738  		// If neither before_each nor join_with nor map_fn is specified, the node is ok.
   739  		// Otherwise if join_with is specified, use `.add_joined` instead.
   740  		// Otherwise use `.add_all` instead.
   741  
   742  		_, beforeEachKw, beforeEach := getParam(call.List, "before_each")
   743  		_, _, joinWith := getParam(call.List, "join_with")
   744  		_, mapFnKw, mapFn := getParam(call.List, "map_fn")
   745  		if beforeEach == nil && joinWith == nil && mapFn == nil {
   746  			// No deprecated API detected
   747  			return
   748  		}
   749  
   750  		// Fix
   751  		var replacements []LinterReplacement
   752  
   753  		newDot := *dot
   754  		newDot.Name = "add_all"
   755  		replacements = append(replacements, LinterReplacement{&call.X, &newDot})
   756  
   757  		if joinWith != nil {
   758  			newDot.Name = "add_joined"
   759  			if beforeEach != nil {
   760  				// `add_joined` doesn't have a `before_each` parameter, replace it with `format_each`:
   761  				// `before_each = foo` -> `format_each = foo + "%s"`
   762  				newBeforeEachKw := *beforeEachKw
   763  				newBeforeEachKw.Name = "format_each"
   764  
   765  				replacements = append(replacements, LinterReplacement{&beforeEach.LHS, &newBeforeEachKw})
   766  				replacements = append(replacements, LinterReplacement{&beforeEach.RHS, &build.BinaryExpr{
   767  					X:  beforeEach.RHS,
   768  					Op: "+",
   769  					Y:  &build.StringExpr{Value: "%s"},
   770  				}})
   771  			}
   772  		}
   773  		if mapFnKw != nil {
   774  			// Replace `map_fn = ...` with `map_each = ...`
   775  			newMapFnKw := *mapFnKw
   776  			newMapFnKw.Name = "map_each"
   777  			replacements = append(replacements, LinterReplacement{&mapFn.LHS, &newMapFnKw})
   778  		}
   779  
   780  		findings = append(findings,
   781  			makeLinterFinding(call,
   782  				`"ctx.actions.args().add()" for multiple arguments is deprecated in favor of "add_all()" or "add_joined()".`,
   783  				replacements...))
   784  
   785  	})
   786  	return findings
   787  }
   788  
   789  func attrOutputDefaultWarning(f *build.File) []*LinterFinding {
   790  	if f.Type != build.TypeBzl {
   791  		return nil
   792  	}
   793  
   794  	var findings []*LinterFinding
   795  	build.Walk(f, func(expr build.Expr, stack []build.Expr) {
   796  		// Find nodes that match the following pattern: attr.output(..., default = ...)
   797  		call, ok := expr.(*build.CallExpr)
   798  		if !ok {
   799  			return
   800  		}
   801  		dot, ok := (call.X).(*build.DotExpr)
   802  		if !ok || dot.Name != "output" {
   803  			return
   804  		}
   805  		base, ok := dot.X.(*build.Ident)
   806  		if !ok || base.Name != "attr" {
   807  			return
   808  		}
   809  		_, _, param := getParam(call.List, "default")
   810  		if param == nil {
   811  			return
   812  		}
   813  		findings = append(findings,
   814  			makeLinterFinding(param, `The "default" parameter for attr.output() is deprecated.`))
   815  	})
   816  	return findings
   817  }
   818  
   819  func attrLicenseWarning(f *build.File) []*LinterFinding {
   820  	if f.Type != build.TypeBzl {
   821  		return nil
   822  	}
   823  
   824  	var findings []*LinterFinding
   825  	build.Walk(f, func(expr build.Expr, stack []build.Expr) {
   826  		// Find nodes that match the following pattern: attr.license(...)
   827  		call, ok := expr.(*build.CallExpr)
   828  		if !ok {
   829  			return
   830  		}
   831  		dot, ok := (call.X).(*build.DotExpr)
   832  		if !ok || dot.Name != "license" {
   833  			return
   834  		}
   835  		base, ok := dot.X.(*build.Ident)
   836  		if !ok || base.Name != "attr" {
   837  			return
   838  		}
   839  		findings = append(findings,
   840  			makeLinterFinding(expr, `"attr.license()" is deprecated and shouldn't be used.`))
   841  	})
   842  	return findings
   843  }
   844  
   845  // ruleImplReturnWarning checks whether a rule implementation function returns an old-style struct
   846  func ruleImplReturnWarning(f *build.File) []*LinterFinding {
   847  	if f.Type != build.TypeBzl {
   848  		return nil
   849  	}
   850  
   851  	var findings []*LinterFinding
   852  
   853  	// iterate over rules and collect rule implementation function names
   854  	implNames := make(map[string]bool)
   855  	build.Walk(f, func(expr build.Expr, stack []build.Expr) {
   856  		call, ok := isFunctionCall(expr, "rule")
   857  		if !ok {
   858  			return
   859  		}
   860  
   861  		// Try to get the implementaton parameter either by name or as the first argument
   862  		var impl build.Expr
   863  		_, _, param := getParam(call.List, "implementation")
   864  		if param != nil {
   865  			impl = param.RHS
   866  		} else if len(call.List) > 0 {
   867  			impl = call.List[0]
   868  		}
   869  		if name, ok := impl.(*build.Ident); ok {
   870  			implNames[name.Name] = true
   871  		}
   872  	})
   873  
   874  	// iterate over functions
   875  	for _, stmt := range f.Stmt {
   876  		def, ok := stmt.(*build.DefStmt)
   877  		if !ok || !implNames[def.Name] {
   878  			// either not a function or not used in the file as a rule implementation function
   879  			continue
   880  		}
   881  		// traverse the function and find all of its return statements
   882  		build.Walk(def, func(expr build.Expr, stack []build.Expr) {
   883  			ret, ok := expr.(*build.ReturnStmt)
   884  			if !ok {
   885  				return
   886  			}
   887  			// check whether it returns a struct
   888  			if _, ok := isFunctionCall(ret.Result, "struct"); ok {
   889  				findings = append(findings, makeLinterFinding(ret, `Avoid using the legacy provider syntax.`))
   890  			}
   891  		})
   892  	}
   893  
   894  	return findings
   895  }
   896  
   897  type signature struct {
   898  	Positional []string // These parameters are typePositional-only
   899  	Keyword    []string // These parameters are typeKeyword-only
   900  }
   901  
   902  var signatures = map[string]signature{
   903  	"all":     {[]string{"elements"}, []string{}},
   904  	"any":     {[]string{"elements"}, []string{}},
   905  	"tuple":   {[]string{"x"}, []string{}},
   906  	"list":    {[]string{"x"}, []string{}},
   907  	"len":     {[]string{"x"}, []string{}},
   908  	"str":     {[]string{"x"}, []string{}},
   909  	"repr":    {[]string{"x"}, []string{}},
   910  	"bool":    {[]string{"x"}, []string{}},
   911  	"int":     {[]string{"x"}, []string{}},
   912  	"dir":     {[]string{"x"}, []string{}},
   913  	"type":    {[]string{"x"}, []string{}},
   914  	"hasattr": {[]string{"x", "name"}, []string{}},
   915  	"getattr": {[]string{"x", "name", "default"}, []string{}},
   916  	"select":  {[]string{"x"}, []string{}},
   917  }
   918  
   919  // functionName returns the name of the given function if it's a direct function call (e.g.
   920  // `foo(...)` or `native.foo(...)`, but not `foo.bar(...)` or `x[3](...)`
   921  func functionName(call *build.CallExpr) (string, bool) {
   922  	if ident, ok := call.X.(*build.Ident); ok {
   923  		return ident.Name, true
   924  	}
   925  	// Also check for `native.<name>`
   926  	dot, ok := call.X.(*build.DotExpr)
   927  	if !ok {
   928  		return "", false
   929  	}
   930  	if ident, ok := dot.X.(*build.Ident); !ok || ident.Name != "native" {
   931  		return "", false
   932  	}
   933  	return dot.Name, true
   934  }
   935  
   936  const (
   937  	typePositional int = iota
   938  	typeKeyword
   939  	typeArgs
   940  	typeKwargs
   941  )
   942  
   943  // paramType returns the type of the param. If it's a typeKeyword param, also returns its name
   944  func paramType(param build.Expr) (int, string) {
   945  	switch param := param.(type) {
   946  	case *build.AssignExpr:
   947  		if param.Op == "=" {
   948  			ident, ok := param.LHS.(*build.Ident)
   949  			if ok {
   950  				return typeKeyword, ident.Name
   951  			}
   952  			return typeKeyword, ""
   953  		}
   954  	case *build.UnaryExpr:
   955  		switch param.Op {
   956  		case "*":
   957  			return typeArgs, ""
   958  		case "**":
   959  			return typeKwargs, ""
   960  		}
   961  	}
   962  	return typePositional, ""
   963  }
   964  
   965  // keywordPositionalParametersWarning checks for deprecated typeKeyword parameters of builtins
   966  func keywordPositionalParametersWarning(f *build.File) []*LinterFinding {
   967  	var findings []*LinterFinding
   968  
   969  	// Check for legacy typeKeyword parameters
   970  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
   971  		call, ok := (*expr).(*build.CallExpr)
   972  		if !ok || len(call.List) == 0 {
   973  			return
   974  		}
   975  		function, ok := functionName(call)
   976  		if !ok {
   977  			return
   978  		}
   979  
   980  		// Findings and replacements for the current call expression
   981  		var callFindings []*LinterFinding
   982  		var callReplacements []LinterReplacement
   983  
   984  		signature, ok := signatures[function]
   985  		if !ok {
   986  			return
   987  		}
   988  
   989  		var paramTypes []int // types of the parameters (typeKeyword or not) after the replacements has been applied.
   990  		for i, parameter := range call.List {
   991  			pType, name := paramType(parameter)
   992  			paramTypes = append(paramTypes, pType)
   993  
   994  			if pType == typeKeyword && i < len(signature.Positional) && signature.Positional[i] == name {
   995  				// The parameter should be typePositional
   996  				callFindings = append(callFindings, makeLinterFinding(
   997  					parameter,
   998  					fmt.Sprintf(`Keyword parameter %q for %q should be positional.`, signature.Positional[i], function),
   999  				))
  1000  				callReplacements = append(callReplacements, LinterReplacement{&call.List[i], makePositional(parameter)})
  1001  				paramTypes[i] = typePositional
  1002  			}
  1003  
  1004  			if pType == typePositional && i >= len(signature.Positional) && i < len(signature.Positional)+len(signature.Keyword) {
  1005  				// The parameter should be typeKeyword
  1006  				keyword := signature.Keyword[i-len(signature.Positional)]
  1007  				callFindings = append(callFindings, makeLinterFinding(
  1008  					parameter,
  1009  					fmt.Sprintf(`Parameter at the position %d for %q should be keyword (%s = ...).`, i+1, function, keyword),
  1010  				))
  1011  				callReplacements = append(callReplacements, LinterReplacement{&call.List[i], makeKeyword(parameter, keyword)})
  1012  				paramTypes[i] = typeKeyword
  1013  			}
  1014  		}
  1015  
  1016  		if len(callFindings) == 0 {
  1017  			return
  1018  		}
  1019  
  1020  		// Only apply the replacements if the signature is correct after they have been applied
  1021  		// (i.e. the order of the parameters is typePositional, typeKeyword, typeArgs, typeKwargs)
  1022  		// Otherwise the signature will be not correct, probably it was incorrect initially.
  1023  		// All the replacements should be applied to the first finding for the current node.
  1024  
  1025  		if sort.IntsAreSorted(paramTypes) {
  1026  			// It's possible that the parameter list had `ForceCompact` set to true because it only contained
  1027  			// positional arguments, and now it has keyword arguments as well. Reset the flag to let the
  1028  			// printer decide how the function call should be formatted.
  1029  			for _, t := range paramTypes {
  1030  				if t == typeKeyword {
  1031  					// There's at least one keyword argument
  1032  					newCall := *call
  1033  					newCall.ForceCompact = false
  1034  					callFindings[0].Replacement = append(callFindings[0].Replacement, LinterReplacement{expr, &newCall})
  1035  					break
  1036  				}
  1037  			}
  1038  			// Attach all the parameter replacements to the first finding
  1039  			callFindings[0].Replacement = append(callFindings[0].Replacement, callReplacements...)
  1040  		}
  1041  
  1042  		findings = append(findings, callFindings...)
  1043  	})
  1044  
  1045  	return findings
  1046  }
  1047  
  1048  func providerParamsWarning(f *build.File) []*LinterFinding {
  1049  	if f.Type != build.TypeBzl {
  1050  		return nil
  1051  	}
  1052  
  1053  	var findings []*LinterFinding
  1054  	build.Walk(f, func(expr build.Expr, stack []build.Expr) {
  1055  		call, ok := isFunctionCall(expr, "provider")
  1056  		if !ok {
  1057  			return
  1058  		}
  1059  
  1060  		_, _, fields := getParam(call.List, "fields")
  1061  		_, _, doc := getParam(call.List, "doc")
  1062  		// doc can also be the first positional argument
  1063  		hasPositional := false
  1064  		if len(call.List) > 0 {
  1065  			if _, ok := call.List[0].(*build.AssignExpr); !ok {
  1066  				hasPositional = true
  1067  			}
  1068  		}
  1069  		msg := ""
  1070  		if fields == nil {
  1071  			msg = "a list of fields"
  1072  		}
  1073  		if doc == nil && !hasPositional {
  1074  			if msg != "" {
  1075  				msg += " and "
  1076  			}
  1077  			msg += "a documentation"
  1078  		}
  1079  		if msg != "" {
  1080  			findings = append(findings, makeLinterFinding(call,
  1081  				`Calls to 'provider' should provide `+msg+`:\n`+
  1082  					`  provider("description", fields = [...])`))
  1083  		}
  1084  	})
  1085  	return findings
  1086  }
  1087  
  1088  
  1089  func attrNameWarning(f *build.File, names []string) []*LinterFinding {
  1090  	if f.Type != build.TypeBzl {
  1091  		return nil
  1092  	}
  1093  
  1094  	var findings []*LinterFinding
  1095  	build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
  1096  		// Find nodes that match "attrs = {..., "license", ...}"
  1097  		dict, ok := (*expr).(*build.DictExpr)
  1098  		if !ok {
  1099  			return
  1100  		}
  1101  		for _, item := range dict.List {
  1102  			// include only string literal keys into consideration
  1103  			value, ok := item.Key.(*build.StringExpr)
  1104  			if !ok {
  1105  				continue
  1106  			}
  1107  			for _, name := range names {
  1108  				if value.Value == name {
  1109  					findings = append(findings, makeLinterFinding(dict,
  1110  						fmt.Sprintf(`Do not use '%s' as an attribute name.`+
  1111  							` It may cause unexpected behavior.`, value.Value)))
  1112  
  1113  				}
  1114  			}
  1115  		}
  1116  	})
  1117  	return findings
  1118  }
  1119  
  1120  func attrLicensesWarning(f *build.File) []*LinterFinding {
  1121  	return attrNameWarning(f, []string{"licenses"})
  1122  }
  1123  
  1124  func attrApplicableLicensesWarning(f *build.File) []*LinterFinding {
  1125  	return attrNameWarning(f, []string{"applicable_licenses", "package_metadata"})
  1126  }
  1127  

View as plain text