/* 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" "github.com/bazelbuild/buildtools/build" "github.com/bazelbuild/buildtools/bzlenv" "github.com/bazelbuild/buildtools/edit" "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] _, isComment := stmt.(*build.CommentBlock) _, isString := stmt.(*build.StringExpr) isDocString := isString && i == 0 if !isComment && !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)} } func notLoadedFunctionUsageCheckInternal(expr *build.Expr, env *bzlenv.Environment, globals []string, loadFrom string) ([]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, loadFrom), replacements...)) break } } return loads, findings } func notLoadedSymbolUsageCheckInternal(expr *build.Expr, env *bzlenv.Environment, globals []string, loadFrom string) ([]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, loadFrom))) 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, 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) findings = append(findings, functionFindings...) for _, load := range functionLoads { toLoad[load] = true } symbolLoads, symbolFindings := notLoadedSymbolUsageCheckInternal(expr, env, symbols, loadFrom) 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 } 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, globals []string, loadFrom string) []*LinterFinding { return notLoadedUsageCheck(f, globals, []string{}, 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) []*LinterFinding { if f.Type != build.TypeBzl { return nil } return NotLoadedFunctionUsageCheck(f, []string{"git_repository", "new_git_repository"}, "@bazel_tools//tools/build_defs/repo:git.bzl") } func nativeHTTPArchiveWarning(f *build.File) []*LinterFinding { if f.Type != build.TypeBzl { return nil } return NotLoadedFunctionUsageCheck(f, []string{"http_archive"}, "@bazel_tools//tools/build_defs/repo:http.bzl") } func nativeAndroidRulesWarning(f *build.File) []*LinterFinding { if f.Type != build.TypeBzl && f.Type != build.TypeBuild { return nil } return NotLoadedFunctionUsageCheck(f, tables.AndroidNativeRules, tables.AndroidLoadPath) } func nativeCcRulesWarning(f *build.File) []*LinterFinding { if f.Type != build.TypeBzl && f.Type != build.TypeBuild { return nil } return NotLoadedFunctionUsageCheck(f, tables.CcNativeRules, tables.CcLoadPath) } func nativeJavaRulesWarning(f *build.File) []*LinterFinding { if f.Type != build.TypeBzl && f.Type != build.TypeBuild { return nil } return NotLoadedFunctionUsageCheck(f, tables.JavaNativeRules, tables.JavaLoadPath) } func nativePyRulesWarning(f *build.File) []*LinterFinding { if f.Type != build.TypeBzl && f.Type != build.TypeBuild { return nil } return NotLoadedFunctionUsageCheck(f, tables.PyNativeRules, tables.PyLoadPath) } func nativeProtoRulesWarning(f *build.File) []*LinterFinding { if f.Type != build.TypeBzl && f.Type != build.TypeBuild { return nil } return notLoadedUsageCheck(f, tables.ProtoNativeRules, tables.ProtoNativeSymbols, tables.ProtoLoadPath) } 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 `.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.` 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"}) }