blob: 61de4e613949a77014fb2038000df15bab3fb1f4 [file] [log] [blame]
/*
Copyright 2020 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Warnings for incompatible changes in the Bazel API
package warn
import (
"fmt"
"sort"
"strings"
"github.com/bazelbuild/buildtools/build"
"github.com/bazelbuild/buildtools/bzlenv"
"github.com/bazelbuild/buildtools/edit"
"github.com/bazelbuild/buildtools/edit/bzlmod"
"github.com/bazelbuild/buildtools/labels"
"github.com/bazelbuild/buildtools/tables"
)
// Bazel API-specific warnings
// negateExpression returns an expression which is a negation of the input.
// If it's a boolean literal (true or false), just return the opposite literal.
// If it's a unary expression with a unary `not` operator, just remove it.
// Otherwise, insert a `not` operator.
// It's assumed that input is no longer needed as it may be mutated or reused by the function.
func negateExpression(expr build.Expr) build.Expr {
paren, ok := expr.(*build.ParenExpr)
if ok {
newParen := *paren
newParen.X = negateExpression(paren.X)
return &newParen
}
unary, ok := expr.(*build.UnaryExpr)
if ok && unary.Op == "not" {
return unary.X
}
boolean, ok := expr.(*build.Ident)
if ok {
newBoolean := *boolean
if boolean.Name == "True" {
newBoolean.Name = "False"
} else {
newBoolean.Name = "True"
}
return &newBoolean
}
return &build.UnaryExpr{
Op: "not",
X: expr,
}
}
// getParam search for a param with a given name in a given list of function arguments
// and returns it with its index
func getParam(attrs []build.Expr, paramName string) (int, *build.Ident, *build.AssignExpr) {
for i, attr := range attrs {
as, ok := attr.(*build.AssignExpr)
if !ok {
continue
}
name, ok := (as.LHS).(*build.Ident)
if !ok || name.Name != paramName {
continue
}
return i, name, as
}
return -1, nil, nil
}
// isFunctionCall checks whether expr is a call of a function with a given name
func isFunctionCall(expr build.Expr, name string) (*build.CallExpr, bool) {
call, ok := expr.(*build.CallExpr)
if !ok {
return nil, false
}
if ident, ok := call.X.(*build.Ident); ok && ident.Name == name {
return call, true
}
return nil, false
}
// globalVariableUsageCheck checks whether there's a usage of a given global variable in the file.
// It's ok to shadow the name with a local variable and use it.
func globalVariableUsageCheck(f *build.File, global, alternative string) []*LinterFinding {
var findings []*LinterFinding
if f.Type != build.TypeBzl {
return findings
}
var walk func(e *build.Expr, env *bzlenv.Environment)
walk = func(e *build.Expr, env *bzlenv.Environment) {
defer bzlenv.WalkOnceWithEnvironment(*e, env, walk)
ident, ok := (*e).(*build.Ident)
if !ok {
return
}
if ident.Name != global {
return
}
if binding := env.Get(ident.Name); binding != nil {
return
}
// Fix
newIdent := *ident
newIdent.Name = alternative
findings = append(findings, makeLinterFinding(ident,
fmt.Sprintf(`Global variable %q is deprecated in favor of %q. Please rename it.`, global, alternative),
LinterReplacement{e, &newIdent}))
}
var expr build.Expr = f
walk(&expr, bzlenv.NewEnvironment())
return findings
}
// insertLoad returns a *LinterReplacement object representing a replacement required for inserting
// an additional load statement. Returns nil if nothing needs to be changed.
func insertLoad(f *build.File, module string, symbols []string) *LinterReplacement {
// Try to find an existing load statement
for i, stmt := range f.Stmt {
load, ok := stmt.(*build.LoadStmt)
if !ok || load.Module.Value != module {
continue
}
// Modify an existing load statement
newLoad := *load
if !edit.AppendToLoad(&newLoad, symbols, symbols) {
return nil
}
return &LinterReplacement{&(f.Stmt[i]), &newLoad}
}
// Need to insert a new load statement. Can't modify the tree here, so just insert a placeholder
// nil statement and return a replacement for it.
i := 0
for i = range f.Stmt {
stmt := f.Stmt[i]
if _, isComment := stmt.(*build.CommentBlock); isComment {
continue
}
if _, isDocString := stmt.(*build.StringExpr); !isDocString {
// Insert a nil statement here
break
}
}
stmts := append([]build.Expr{}, f.Stmt[:i]...)
stmts = append(stmts, nil)
stmts = append(stmts, f.Stmt[i:]...)
f.Stmt = stmts
return &LinterReplacement{&(f.Stmt[i]), edit.NewLoad(module, symbols, symbols)}
}
// Caches the result of bzlmod.ExtractModuleToApparentNameMapping.
var moduleToApparentRepoName func(string) string
// useApparentRepoNameIfExternal replaces the module name in a load statement with the apparent repository
// name used by the root Bazel module (if any).
func useApparentRepoNameIfExternal(load string, fileReader *FileReader) string {
if !strings.HasPrefix(load, "@") || fileReader == nil {
// Not a load from an external repository or we can't load external files.
return load
}
if moduleToApparentRepoName == nil {
moduleToApparentRepoName = bzlmod.ExtractModuleToApparentNameMapping(func(relPath string) *build.File {
return fileReader.GetFile("", relPath)
})
}
l := labels.Parse(load)
apparentName := moduleToApparentRepoName(l.Repository)
if apparentName == "" {
// The module that hosts the load is not a bazel_dep of the root module. We assume that's
// because it is a WORKSPACE repo, which uses the legacy name.
apparentName = tables.ModuleToLegacyRepoName[l.Repository]
if apparentName == "" {
apparentName = l.Repository
}
}
l.Repository = apparentName
return l.Format()
}
func notLoadedFunctionUsageCheckInternal(expr *build.Expr, env *bzlenv.Environment, globals []string, loadFrom string, fileReader *FileReader) ([]string, []*LinterFinding) {
var loads []string
var findings []*LinterFinding
call, ok := (*expr).(*build.CallExpr)
if !ok {
return loads, findings
}
var name string
var replacements []LinterReplacement
switch node := call.X.(type) {
case *build.DotExpr:
// Maybe native.`global`?
ident, ok := node.X.(*build.Ident)
if !ok || ident.Name != "native" {
return loads, findings
}
name = node.Name
// Replace `native.foo()` with `foo()`
newCall := *call
newCall.X = &build.Ident{Name: node.Name}
replacements = append(replacements, LinterReplacement{expr, &newCall})
case *build.Ident:
// Maybe `global`()?
if binding := env.Get(node.Name); binding != nil {
return loads, findings
}
name = node.Name
default:
return loads, findings
}
for _, global := range globals {
if name == global {
loads = append(loads, name)
findings = append(findings,
makeLinterFinding(call.X, fmt.Sprintf(`Function %q is not global anymore and needs to be loaded from %q.`, global, useApparentRepoNameIfExternal(loadFrom, fileReader)), replacements...))
break
}
}
return loads, findings
}
func notLoadedSymbolUsageCheckInternal(expr *build.Expr, env *bzlenv.Environment, globals []string, loadFrom string, fileReader *FileReader) ([]string, []*LinterFinding) {
var loads []string
var findings []*LinterFinding
ident, ok := (*expr).(*build.Ident)
if !ok {
return loads, findings
}
if binding := env.Get(ident.Name); binding != nil {
return loads, findings
}
for _, global := range globals {
if ident.Name == global {
loads = append(loads, ident.Name)
findings = append(findings,
makeLinterFinding(ident, fmt.Sprintf(`Symbol %q is not global anymore and needs to be loaded from %q.`, global, useApparentRepoNameIfExternal(loadFrom, fileReader))))
break
}
}
return loads, findings
}
// notLoadedUsageCheck checks whether there's a usage of a given not imported function or symbol in the file
// and adds a load statement if necessary.
func notLoadedUsageCheck(f *build.File, fileReader *FileReader, functions, symbols []string, loadFrom string) []*LinterFinding {
toLoad := make(map[string]bool)
var findings []*LinterFinding
var walk func(expr *build.Expr, env *bzlenv.Environment)
walk = func(expr *build.Expr, env *bzlenv.Environment) {
defer bzlenv.WalkOnceWithEnvironment(*expr, env, walk)
functionLoads, functionFindings := notLoadedFunctionUsageCheckInternal(expr, env, functions, loadFrom, fileReader)
findings = append(findings, functionFindings...)
for _, load := range functionLoads {
toLoad[load] = true
}
symbolLoads, symbolFindings := notLoadedSymbolUsageCheckInternal(expr, env, symbols, loadFrom, fileReader)
findings = append(findings, symbolFindings...)
for _, load := range symbolLoads {
toLoad[load] = true
}
}
var expr build.Expr = f
walk(&expr, bzlenv.NewEnvironment())
if len(toLoad) == 0 {
return nil
}
loadFrom = useApparentRepoNameIfExternal(loadFrom, fileReader)
loads := []string{}
for l := range toLoad {
loads = append(loads, l)
}
sort.Strings(loads)
replacement := insertLoad(f, loadFrom, loads)
if replacement != nil {
// Add the same replacement to all relevant findings.
for _, f := range findings {
f.Replacement = append(f.Replacement, *replacement)
}
}
return findings
}
// NotLoadedFunctionUsageCheck checks whether there's a usage of a given not imported function in the file
// and adds a load statement if necessary.
func NotLoadedFunctionUsageCheck(f *build.File, fileReader *FileReader, globals []string, loadFrom string) []*LinterFinding {
return notLoadedUsageCheck(f, fileReader, globals, []string{}, loadFrom)
}
// NotLoadedSymbolUsageCheck checks whether there's a usage of a given not imported function in the file
// and adds a load statement if necessary.
func NotLoadedSymbolUsageCheck(f *build.File, fileReader *FileReader, globals []string, loadFrom string) []*LinterFinding {
return notLoadedUsageCheck(f, fileReader, []string{}, globals, loadFrom)
}
// makePositional makes the function argument positional (removes the keyword if it exists)
func makePositional(argument build.Expr) build.Expr {
if binary, ok := argument.(*build.AssignExpr); ok {
return binary.RHS
}
return argument
}
// makeKeyword makes the function argument keyword (adds or edits the keyword name)
func makeKeyword(argument build.Expr, name string) build.Expr {
assign, ok := argument.(*build.AssignExpr)
if !ok {
return &build.AssignExpr{
LHS: &build.Ident{Name: name},
Op: "=",
RHS: argument,
}
}
ident, ok := assign.LHS.(*build.Ident)
if ok && ident.Name == name {
// Nothing to change
return argument
}
// Technically it's possible that the LHS is not an ident, but that is a syntax error anyway.
newAssign := *assign
newAssign.LHS = &build.Ident{Name: name}
return &newAssign
}
func attrConfigurationWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
// Find nodes that match the following pattern: attr.xxxx(..., cfg = "data", ...) and attr.xxxx(..., cfg = "host", ...)
call, ok := (*expr).(*build.CallExpr)
if !ok {
return
}
dot, ok := (call.X).(*build.DotExpr)
if !ok {
return
}
base, ok := dot.X.(*build.Ident)
if !ok || base.Name != "attr" {
return
}
i, _, param := getParam(call.List, "cfg")
if param == nil {
return
}
value, ok := (param.RHS).(*build.StringExpr)
if !ok {
return
}
newCall := *call
switch value.Value {
case "data":
newCall.List = append(newCall.List[:i], newCall.List[i+1:]...)
findings = append(findings,
makeLinterFinding(param, `cfg = "data" for attr definitions has no effect and should be removed.`,
LinterReplacement{expr, &newCall}))
case "host":
{
newCall.List = append([]build.Expr{}, newCall.List...)
newParam := newCall.List[i].Copy().(*build.AssignExpr)
newRHS := newParam.RHS.Copy().(*build.StringExpr)
newRHS.Value = "exec"
newParam.RHS = newRHS
newCall.List[i] = newParam
findings = append(findings,
makeLinterFinding(param, `cfg = "host" for attr definitions should be replaced by cfg = "exec".`,
LinterReplacement{expr, &newCall}))
}
default:
// value not matched.
return
}
})
return findings
}
func depsetItemsWarning(f *build.File) []*LinterFinding {
var findings []*LinterFinding
types := DetectTypes(f)
build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
call, ok := (*expr).(*build.CallExpr)
if !ok {
return
}
base, ok := call.X.(*build.Ident)
if !ok || base.Name != "depset" {
return
}
if len(call.List) == 0 {
return
}
_, _, param := getParam(call.List, "items")
if param != nil {
findings = append(findings,
makeLinterFinding(param, `Parameter "items" is deprecated, use "direct" and/or "transitive" instead.`))
return
}
if _, ok := call.List[0].(*build.AssignExpr); ok {
return
}
// We have an unnamed first parameter. Check the type.
if types[call.List[0]] == Depset {
findings = append(findings,
makeLinterFinding(call.List[0], `Giving a depset as first unnamed parameter to depset() is deprecated, use the "transitive" parameter instead.`))
}
})
return findings
}
func attrNonEmptyWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
// Find nodes that match the following pattern: attr.xxxx(..., non_empty = ..., ...)
call, ok := (*expr).(*build.CallExpr)
if !ok {
return
}
dot, ok := (call.X).(*build.DotExpr)
if !ok {
return
}
base, ok := dot.X.(*build.Ident)
if !ok || base.Name != "attr" {
return
}
_, name, param := getParam(call.List, "non_empty")
if param == nil {
return
}
// Fix
newName := *name
newName.Name = "allow_empty"
negatedRHS := negateExpression(param.RHS)
findings = append(findings,
makeLinterFinding(param, "non_empty attributes for attr definitions are deprecated in favor of allow_empty.",
LinterReplacement{&param.LHS, &newName},
LinterReplacement{&param.RHS, negatedRHS},
))
})
return findings
}
func attrSingleFileWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
// Find nodes that match the following pattern: attr.xxxx(..., single_file = ..., ...)
call, ok := (*expr).(*build.CallExpr)
if !ok {
return
}
dot, ok := (call.X).(*build.DotExpr)
if !ok {
return
}
base, ok := dot.X.(*build.Ident)
if !ok || base.Name != "attr" {
return
}
singleFileIndex, singleFileKw, singleFileParam := getParam(call.List, "single_file")
if singleFileParam == nil {
return
}
// Fix
newCall := *call
newCall.List = append([]build.Expr{}, call.List...)
newSingleFileKw := *singleFileKw
newSingleFileKw.Name = "allow_single_file"
singleFileValue := singleFileParam.RHS
if boolean, ok := singleFileValue.(*build.Ident); ok && boolean.Name == "False" {
// if the value is `False`, just remove the whole parameter
newCall.List = append(newCall.List[:singleFileIndex], newCall.List[singleFileIndex+1:]...)
} else {
// search for `allow_files` parameter in the same attr definition and remove it
allowFileIndex, _, allowFilesParam := getParam(call.List, "allow_files")
if allowFilesParam != nil {
singleFileValue = allowFilesParam.RHS
newCall.List = append(newCall.List[:allowFileIndex], newCall.List[allowFileIndex+1:]...)
if singleFileIndex > allowFileIndex {
singleFileIndex--
}
}
}
findings = append(findings,
makeLinterFinding(singleFileParam, "single_file is deprecated in favor of allow_single_file.",
LinterReplacement{expr, &newCall},
LinterReplacement{&singleFileParam.LHS, &newSingleFileKw},
LinterReplacement{&singleFileParam.RHS, singleFileValue},
))
})
return findings
}
func ctxActionsWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
// Find nodes that match the following pattern: ctx.xxxx(...)
call, ok := (*expr).(*build.CallExpr)
if !ok {
return
}
dot, ok := (call.X).(*build.DotExpr)
if !ok {
return
}
base, ok := dot.X.(*build.Ident)
if !ok || base.Name != "ctx" {
return
}
switch dot.Name {
case "new_file", "experimental_new_directory", "file_action", "action", "empty_action", "template_action":
// fix
default:
return
}
// Fix
newCall := *call
newCall.List = append([]build.Expr{}, call.List...)
newDot := *dot
newCall.X = &newDot
switch dot.Name {
case "new_file":
if len(call.List) > 2 {
// Can't fix automatically because the new API doesn't support the 3 arguments signature
findings = append(findings,
makeLinterFinding(dot, fmt.Sprintf(`"ctx.new_file" is deprecated in favor of "ctx.actions.declare_file".`)))
return
}
newDot.Name = "actions.declare_file"
if len(call.List) == 2 {
// swap arguments:
// ctx.new_file(sibling, name) -> ctx.actions.declare_file(name, sibling=sibling)
newCall.List[0], newCall.List[1] = makePositional(call.List[1]), makeKeyword(call.List[0], "sibling")
}
case "experimental_new_directory":
newDot.Name = "actions.declare_directory"
case "file_action":
newDot.Name = "actions.write"
i, ident, param := getParam(newCall.List, "executable")
if ident != nil {
newIdent := *ident
newIdent.Name = "is_executable"
newParam := *param
newParam.LHS = &newIdent
newCall.List[i] = &newParam
}
case "action":
newDot.Name = "actions.run"
if _, _, command := getParam(call.List, "command"); command != nil {
newDot.Name = "actions.run_shell"
}
case "empty_action":
newDot.Name = "actions.do_nothing"
case "template_action":
newDot.Name = "actions.expand_template"
if i, ident, param := getParam(call.List, "executable"); ident != nil {
newIdent := *ident
newIdent.Name = "is_executable"
newParam := *param
newParam.LHS = &newIdent
newCall.List[i] = &newParam
}
}
findings = append(findings, makeLinterFinding(dot,
fmt.Sprintf(`"ctx.%s" is deprecated in favor of "ctx.%s".`, dot.Name, newDot.Name),
LinterReplacement{expr, &newCall}))
})
return findings
}
func fileTypeWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
var walk func(e *build.Expr, env *bzlenv.Environment)
walk = func(e *build.Expr, env *bzlenv.Environment) {
defer bzlenv.WalkOnceWithEnvironment(*e, env, walk)
call, ok := isFunctionCall(*e, "FileType")
if !ok {
return
}
if binding := env.Get("FileType"); binding == nil {
findings = append(findings,
makeLinterFinding(call, "The FileType function is deprecated."))
}
}
var expr build.Expr = f
walk(&expr, bzlenv.NewEnvironment())
return findings
}
func packageNameWarning(f *build.File) []*LinterFinding {
return globalVariableUsageCheck(f, "PACKAGE_NAME", "native.package_name()")
}
func repositoryNameWarning(f *build.File) []*LinterFinding {
return globalVariableUsageCheck(f, "REPOSITORY_NAME", "native.repository_name()")
}
func outputGroupWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
// Find nodes that match the following pattern: ctx.attr.xxx.output_group
outputGroup, ok := (*expr).(*build.DotExpr)
if !ok || outputGroup.Name != "output_group" {
return
}
dep, ok := (outputGroup.X).(*build.DotExpr)
if !ok {
return
}
attr, ok := (dep.X).(*build.DotExpr)
if !ok || attr.Name != "attr" {
return
}
ctx, ok := (attr.X).(*build.Ident)
if !ok || ctx.Name != "ctx" {
return
}
// Replace `xxx.output_group` with `xxx[OutputGroupInfo]`
findings = append(findings,
makeLinterFinding(outputGroup,
`"ctx.attr.dep.output_group" is deprecated in favor of "ctx.attr.dep[OutputGroupInfo]".`,
LinterReplacement{expr, &build.IndexExpr{
X: dep,
Y: &build.Ident{Name: "OutputGroupInfo"},
},
}))
})
return findings
}
func nativeGitRepositoryWarning(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, []string{"git_repository", "new_git_repository"}, "@bazel_tools//tools/build_defs/repo:git.bzl")
}
func nativeHTTPArchiveWarning(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, []string{"http_archive"}, "@bazel_tools//tools/build_defs/repo:http.bzl")
}
func nativeAndroidRulesWarning(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, tables.AndroidNativeRules, tables.AndroidLoadPath)
}
// NativeCcRulesWarning produces a warning for missing loads of cc rules
func NativeCcRulesWarning(rule string) func(f *build.File, fileReader *FileReader) []*LinterFinding {
return func(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, []string{rule}, tables.CcLoadPathPrefix+":"+rule+".bzl")
}
}
// NativeCcToolchainRulesWarning produces a warning for missing loads of cc toolchain rules
func NativeCcToolchainRulesWarning(rule string) func(f *build.File, fileReader *FileReader) []*LinterFinding {
return func(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, []string{rule}, tables.CcLoadPathPrefix+"/toolchains:"+rule+".bzl")
}
}
// NativeCcSymbolsWarning produces a warning for missing loads of cc top-level symbols
func NativeCcSymbolsWarning(symbol string, bzlfile string) func(f *build.File, fileReader *FileReader) []*LinterFinding {
return func(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedSymbolUsageCheck(f, fileReader, []string{symbol}, tables.CcLoadPathPrefix+"/common:"+bzlfile+".bzl")
}
}
// NativeJavaRulesWarning produces a warning for missing loads of java rules
func NativeJavaRulesWarning(rule string) func(f *build.File, fileReader *FileReader) []*LinterFinding {
return func(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, []string{rule}, tables.JavaLoadPathPrefix+":"+rule+".bzl")
}
}
// NativeJavaToolchainRulesWarning produces a warning for missing loads of java toolchain rules
func NativeJavaToolchainRulesWarning(rule string) func(f *build.File, fileReader *FileReader) []*LinterFinding {
return func(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, []string{rule}, tables.JavaLoadPathPrefix+"/toolchains:"+rule+".bzl")
}
}
// NativeJavaSymbolsWarning produces a warning for missing loads of java top-level symbols
func NativeJavaSymbolsWarning(symbol string, bzlfile string) func(f *build.File, fileReader *FileReader) []*LinterFinding {
return func(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedSymbolUsageCheck(f, fileReader, []string{symbol}, tables.JavaLoadPathPrefix+"/common:"+bzlfile+".bzl")
}
}
func nativePyRulesWarning(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, tables.PyNativeRules, tables.PyLoadPath)
}
// NativeProtoRulesWarning produces a warning for missing loads of proto rules
func NativeProtoRulesWarning(rule string) func(f *build.File, fileReader *FileReader) []*LinterFinding {
return func(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, []string{rule}, tables.ProtoLoadPathPrefix+":"+rule+".bzl")
}
}
func nativeProtoLangToolchainWarning(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, []string{"proto_lang_toolchain"}, tables.ProtoLoadPathPrefix+"/toolchains:proto_lang_toolchain.bzl")
}
func nativeProtoSymbolsWarning(symbol string, bzlfile string) func(f *build.File, fileReader *FileReader) []*LinterFinding {
return func(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedSymbolUsageCheck(f, fileReader, []string{symbol}, tables.ProtoLoadPathPrefix+"/common:"+bzlfile)
}
}
// NativeShellRulesWarning produces a warning for missing loads of shell rules
func NativeShellRulesWarning(rule string) func(f *build.File, fileReader *FileReader) []*LinterFinding {
return func(f *build.File, fileReader *FileReader) []*LinterFinding {
if f.Type != build.TypeBzl && f.Type != build.TypeBuild {
return nil
}
return NotLoadedFunctionUsageCheck(f, fileReader, []string{rule}, tables.ShellLoadPathPrefix+":"+rule+".bzl")
}
}
func contextArgsAPIWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
types := DetectTypes(f)
build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
// Search for `<ctx.actions.args>.add()` nodes
call, ok := (*expr).(*build.CallExpr)
if !ok {
return
}
dot, ok := call.X.(*build.DotExpr)
if !ok || dot.Name != "add" || types[dot.X] != CtxActionsArgs {
return
}
// If neither before_each nor join_with nor map_fn is specified, the node is ok.
// Otherwise if join_with is specified, use `.add_joined` instead.
// Otherwise use `.add_all` instead.
_, beforeEachKw, beforeEach := getParam(call.List, "before_each")
_, _, joinWith := getParam(call.List, "join_with")
_, mapFnKw, mapFn := getParam(call.List, "map_fn")
if beforeEach == nil && joinWith == nil && mapFn == nil {
// No deprecated API detected
return
}
// Fix
var replacements []LinterReplacement
newDot := *dot
newDot.Name = "add_all"
replacements = append(replacements, LinterReplacement{&call.X, &newDot})
if joinWith != nil {
newDot.Name = "add_joined"
if beforeEach != nil {
// `add_joined` doesn't have a `before_each` parameter, replace it with `format_each`:
// `before_each = foo` -> `format_each = foo + "%s"`
newBeforeEachKw := *beforeEachKw
newBeforeEachKw.Name = "format_each"
replacements = append(replacements, LinterReplacement{&beforeEach.LHS, &newBeforeEachKw})
replacements = append(replacements, LinterReplacement{&beforeEach.RHS, &build.BinaryExpr{
X: beforeEach.RHS,
Op: "+",
Y: &build.StringExpr{Value: "%s"},
}})
}
}
if mapFnKw != nil {
// Replace `map_fn = ...` with `map_each = ...`
newMapFnKw := *mapFnKw
newMapFnKw.Name = "map_each"
replacements = append(replacements, LinterReplacement{&mapFn.LHS, &newMapFnKw})
}
findings = append(findings,
makeLinterFinding(call,
`"ctx.actions.args().add()" for multiple arguments is deprecated in favor of "add_all()" or "add_joined()".`,
replacements...))
})
return findings
}
func attrOutputDefaultWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
build.Walk(f, func(expr build.Expr, stack []build.Expr) {
// Find nodes that match the following pattern: attr.output(..., default = ...)
call, ok := expr.(*build.CallExpr)
if !ok {
return
}
dot, ok := (call.X).(*build.DotExpr)
if !ok || dot.Name != "output" {
return
}
base, ok := dot.X.(*build.Ident)
if !ok || base.Name != "attr" {
return
}
_, _, param := getParam(call.List, "default")
if param == nil {
return
}
findings = append(findings,
makeLinterFinding(param, `The "default" parameter for attr.output() is deprecated.`))
})
return findings
}
func attrLicenseWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
build.Walk(f, func(expr build.Expr, stack []build.Expr) {
// Find nodes that match the following pattern: attr.license(...)
call, ok := expr.(*build.CallExpr)
if !ok {
return
}
dot, ok := (call.X).(*build.DotExpr)
if !ok || dot.Name != "license" {
return
}
base, ok := dot.X.(*build.Ident)
if !ok || base.Name != "attr" {
return
}
findings = append(findings,
makeLinterFinding(expr, `"attr.license()" is deprecated and shouldn't be used.`))
})
return findings
}
// ruleImplReturnWarning checks whether a rule implementation function returns an old-style struct
func ruleImplReturnWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
// iterate over rules and collect rule implementation function names
implNames := make(map[string]bool)
build.Walk(f, func(expr build.Expr, stack []build.Expr) {
call, ok := isFunctionCall(expr, "rule")
if !ok {
return
}
// Try to get the implementaton parameter either by name or as the first argument
var impl build.Expr
_, _, param := getParam(call.List, "implementation")
if param != nil {
impl = param.RHS
} else if len(call.List) > 0 {
impl = call.List[0]
}
if name, ok := impl.(*build.Ident); ok {
implNames[name.Name] = true
}
})
// iterate over functions
for _, stmt := range f.Stmt {
def, ok := stmt.(*build.DefStmt)
if !ok || !implNames[def.Name] {
// either not a function or not used in the file as a rule implementation function
continue
}
// traverse the function and find all of its return statements
build.Walk(def, func(expr build.Expr, stack []build.Expr) {
ret, ok := expr.(*build.ReturnStmt)
if !ok {
return
}
// check whether it returns a struct
if _, ok := isFunctionCall(ret.Result, "struct"); ok {
findings = append(findings, makeLinterFinding(ret, `Avoid using the legacy provider syntax.`))
}
})
}
return findings
}
type signature struct {
Positional []string // These parameters are typePositional-only
Keyword []string // These parameters are typeKeyword-only
}
var signatures = map[string]signature{
"all": {[]string{"elements"}, []string{}},
"any": {[]string{"elements"}, []string{}},
"tuple": {[]string{"x"}, []string{}},
"list": {[]string{"x"}, []string{}},
"len": {[]string{"x"}, []string{}},
"str": {[]string{"x"}, []string{}},
"repr": {[]string{"x"}, []string{}},
"bool": {[]string{"x"}, []string{}},
"int": {[]string{"x"}, []string{}},
"dir": {[]string{"x"}, []string{}},
"type": {[]string{"x"}, []string{}},
"hasattr": {[]string{"x", "name"}, []string{}},
"getattr": {[]string{"x", "name", "default"}, []string{}},
"select": {[]string{"x"}, []string{}},
}
// functionName returns the name of the given function if it's a direct function call (e.g.
// `foo(...)` or `native.foo(...)`, but not `foo.bar(...)` or `x[3](...)`
func functionName(call *build.CallExpr) (string, bool) {
if ident, ok := call.X.(*build.Ident); ok {
return ident.Name, true
}
// Also check for `native.<name>`
dot, ok := call.X.(*build.DotExpr)
if !ok {
return "", false
}
if ident, ok := dot.X.(*build.Ident); !ok || ident.Name != "native" {
return "", false
}
return dot.Name, true
}
const (
typePositional int = iota
typeKeyword
typeArgs
typeKwargs
)
// paramType returns the type of the param. If it's a typeKeyword param, also returns its name
func paramType(param build.Expr) (int, string) {
switch param := param.(type) {
case *build.AssignExpr:
if param.Op == "=" {
ident, ok := param.LHS.(*build.Ident)
if ok {
return typeKeyword, ident.Name
}
return typeKeyword, ""
}
case *build.UnaryExpr:
switch param.Op {
case "*":
return typeArgs, ""
case "**":
return typeKwargs, ""
}
}
return typePositional, ""
}
// keywordPositionalParametersWarning checks for deprecated typeKeyword parameters of builtins
func keywordPositionalParametersWarning(f *build.File) []*LinterFinding {
var findings []*LinterFinding
// Check for legacy typeKeyword parameters
build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
call, ok := (*expr).(*build.CallExpr)
if !ok || len(call.List) == 0 {
return
}
function, ok := functionName(call)
if !ok {
return
}
// Findings and replacements for the current call expression
var callFindings []*LinterFinding
var callReplacements []LinterReplacement
signature, ok := signatures[function]
if !ok {
return
}
var paramTypes []int // types of the parameters (typeKeyword or not) after the replacements has been applied.
for i, parameter := range call.List {
pType, name := paramType(parameter)
paramTypes = append(paramTypes, pType)
if pType == typeKeyword && i < len(signature.Positional) && signature.Positional[i] == name {
// The parameter should be typePositional
callFindings = append(callFindings, makeLinterFinding(
parameter,
fmt.Sprintf(`Keyword parameter %q for %q should be positional.`, signature.Positional[i], function),
))
callReplacements = append(callReplacements, LinterReplacement{&call.List[i], makePositional(parameter)})
paramTypes[i] = typePositional
}
if pType == typePositional && i >= len(signature.Positional) && i < len(signature.Positional)+len(signature.Keyword) {
// The parameter should be typeKeyword
keyword := signature.Keyword[i-len(signature.Positional)]
callFindings = append(callFindings, makeLinterFinding(
parameter,
fmt.Sprintf(`Parameter at the position %d for %q should be keyword (%s = ...).`, i+1, function, keyword),
))
callReplacements = append(callReplacements, LinterReplacement{&call.List[i], makeKeyword(parameter, keyword)})
paramTypes[i] = typeKeyword
}
}
if len(callFindings) == 0 {
return
}
// Only apply the replacements if the signature is correct after they have been applied
// (i.e. the order of the parameters is typePositional, typeKeyword, typeArgs, typeKwargs)
// Otherwise the signature will be not correct, probably it was incorrect initially.
// All the replacements should be applied to the first finding for the current node.
if sort.IntsAreSorted(paramTypes) {
// It's possible that the parameter list had `ForceCompact` set to true because it only contained
// positional arguments, and now it has keyword arguments as well. Reset the flag to let the
// printer decide how the function call should be formatted.
for _, t := range paramTypes {
if t == typeKeyword {
// There's at least one keyword argument
newCall := *call
newCall.ForceCompact = false
callFindings[0].Replacement = append(callFindings[0].Replacement, LinterReplacement{expr, &newCall})
break
}
}
// Attach all the parameter replacements to the first finding
callFindings[0].Replacement = append(callFindings[0].Replacement, callReplacements...)
}
findings = append(findings, callFindings...)
})
return findings
}
func providerParamsWarning(f *build.File) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
build.Walk(f, func(expr build.Expr, stack []build.Expr) {
call, ok := isFunctionCall(expr, "provider")
if !ok {
return
}
_, _, fields := getParam(call.List, "fields")
_, _, doc := getParam(call.List, "doc")
// doc can also be the first positional argument
hasPositional := false
if len(call.List) > 0 {
if _, ok := call.List[0].(*build.AssignExpr); !ok {
hasPositional = true
}
}
msg := ""
if fields == nil {
msg = "a list of fields"
}
if doc == nil && !hasPositional {
if msg != "" {
msg += " and "
}
msg += "a documentation"
}
if msg != "" {
findings = append(findings, makeLinterFinding(call,
`Calls to 'provider' should provide `+msg+`:\n`+
` provider("description", fields = [...])`))
}
})
return findings
}
func attrNameWarning(f *build.File, names []string) []*LinterFinding {
if f.Type != build.TypeBzl {
return nil
}
var findings []*LinterFinding
build.WalkPointers(f, func(expr *build.Expr, stack []build.Expr) {
// Find nodes that match "attrs = {..., "license", ...}"
dict, ok := (*expr).(*build.DictExpr)
if !ok {
return
}
for _, item := range dict.List {
// include only string literal keys into consideration
value, ok := item.Key.(*build.StringExpr)
if !ok {
continue
}
for _, name := range names {
if value.Value == name {
findings = append(findings, makeLinterFinding(dict,
fmt.Sprintf(`Do not use '%s' as an attribute name.`+
` It may cause unexpected behavior.`, value.Value)))
}
}
}
})
return findings
}
func attrLicensesWarning(f *build.File) []*LinterFinding {
return attrNameWarning(f, []string{"licenses"})
}
func attrApplicableLicensesWarning(f *build.File) []*LinterFinding {
return attrNameWarning(f, []string{"applicable_licenses", "package_metadata"})
}