| /* |
| 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{¶m.LHS, &newName}, |
| LinterReplacement{¶m.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"}) |
| } |