| /* |
| 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 using deprecated functions |
| |
| package warn |
| |
| import ( |
| "fmt" |
| "strings" |
| |
| "github.com/bazelbuild/buildtools/build" |
| "github.com/bazelbuild/buildtools/labels" |
| ) |
| |
| // Internal constant that represents the native module |
| const nativeModule = "<native>" |
| |
| // function represents a function identifier, which is a pair (module name, function name). |
| type function struct { |
| pkg string // package where the function is defined |
| filename string // name of a .bzl file relative to the package |
| name string // original name of the function |
| } |
| |
| func (f function) label() string { |
| return f.pkg + ":" + f.filename |
| } |
| |
| // funCall represents a call to another function. It contains information of the function itself as well as some |
| // information about the environment |
| type funCall struct { |
| function |
| nameAlias string // function name alias (it could be loaded with a different name or assigned to a new variable). |
| line int // line on which the function is being called |
| } |
| |
| // acceptsNameArgument checks whether a function can accept a named argument called "name", |
| // either directly or via **kwargs. |
| func acceptsNameArgument(def *build.DefStmt) bool { |
| for _, param := range def.Params { |
| if name, op := build.GetParamName(param); name == "name" || op == "**" { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // fileData represents information about rules and functions extracted from a file |
| type fileData struct { |
| rules map[string]bool // all rules defined in the file |
| functions map[string]map[string]funCall // outer map: all functions defined in the file, inner map: all distinct function calls from the given function |
| aliases map[string]function // all top-level aliases (e.g. `foo = bar`). |
| } |
| |
| // resolvesExternal takes a local function definition and replaces it with an external one if it's been defined |
| // in another file and loaded |
| func resolveExternal(fn function, externalSymbols map[string]function) function { |
| if external, ok := externalSymbols[fn.name]; ok { |
| return external |
| } |
| return fn |
| } |
| |
| // exprLine returns the start line of an expression |
| func exprLine(expr build.Expr) int { |
| start, _ := expr.Span() |
| return start.Line |
| } |
| |
| // getFunCalls extracts information about functions that are being called from the given function |
| func getFunCalls(def *build.DefStmt, pkg, filename string, externalSymbols map[string]function) map[string]funCall { |
| funCalls := make(map[string]funCall) |
| build.Walk(def, func(expr build.Expr, stack []build.Expr) { |
| call, ok := expr.(*build.CallExpr) |
| if !ok { |
| return |
| } |
| if ident, ok := call.X.(*build.Ident); ok { |
| funCalls[ident.Name] = funCall{ |
| function: resolveExternal(function{pkg, filename, ident.Name}, externalSymbols), |
| nameAlias: ident.Name, |
| line: exprLine(call), |
| } |
| return |
| } |
| dot, ok := call.X.(*build.DotExpr) |
| if !ok { |
| return |
| } |
| if ident, ok := dot.X.(*build.Ident); !ok || ident.Name != "native" { |
| return |
| } |
| name := "native." + dot.Name |
| funCalls[name] = funCall{ |
| function: function{ |
| name: dot.Name, |
| filename: nativeModule, |
| }, |
| nameAlias: name, |
| line: exprLine(dot), |
| } |
| }) |
| return funCalls |
| } |
| |
| // analyzeFile extracts the information about rules and functions defined in the file |
| func analyzeFile(f *build.File) fileData { |
| if f == nil { |
| return fileData{} |
| } |
| |
| // Collect loaded symbols |
| externalSymbols := make(map[string]function) |
| for _, stmt := range f.Stmt { |
| load, ok := stmt.(*build.LoadStmt) |
| if !ok { |
| continue |
| } |
| label := labels.ParseRelative(load.Module.Value, f.Pkg) |
| if label.Repository != "" || label.Target == "" { |
| continue |
| } |
| for i, from := range load.From { |
| externalSymbols[load.To[i].Name] = function{label.Package, label.Target, from.Name} |
| } |
| } |
| |
| report := fileData{ |
| rules: make(map[string]bool), |
| functions: make(map[string]map[string]funCall), |
| aliases: make(map[string]function), |
| } |
| for _, stmt := range f.Stmt { |
| switch stmt := stmt.(type) { |
| case *build.AssignExpr: |
| // Analyze aliases (`foo = bar`) or rule declarations (`foo = rule(...)`) |
| lhsIdent, ok := stmt.LHS.(*build.Ident) |
| if !ok { |
| continue |
| } |
| if rhsIdent, ok := stmt.RHS.(*build.Ident); ok { |
| report.aliases[lhsIdent.Name] = resolveExternal(function{f.Pkg, f.Label, rhsIdent.Name}, externalSymbols) |
| continue |
| } |
| |
| call, ok := stmt.RHS.(*build.CallExpr) |
| if !ok { |
| continue |
| } |
| ident, ok := call.X.(*build.Ident) |
| if !ok || ident.Name != "rule" { |
| continue |
| } |
| report.rules[lhsIdent.Name] = true |
| case *build.DefStmt: |
| report.functions[stmt.Name] = getFunCalls(stmt, f.Pkg, f.Label, externalSymbols) |
| default: |
| continue |
| } |
| } |
| return report |
| } |
| |
| // functionReport represents the analysis result of a function |
| type functionReport struct { |
| isMacro bool // whether the function is a macro (or a rule) |
| fc *funCall // a call to the rule or another macro |
| } |
| |
| // macroAnalyzer is an object that analyzes the directed graph of functions calling each other, |
| // loading other files lazily if necessary. |
| type macroAnalyzer struct { |
| fileReader *FileReader |
| files map[string]fileData |
| cache map[function]functionReport |
| } |
| |
| // getFileData retrieves a file using the fileReader object and extracts information about functions and rules |
| // defined in the file. |
| func (ma macroAnalyzer) getFileData(pkg, label string) fileData { |
| filename := pkg + ":" + label |
| if fd, ok := ma.files[filename]; ok { |
| return fd |
| } |
| if ma.fileReader == nil { |
| fd := fileData{} |
| ma.files[filename] = fd |
| return fd |
| } |
| f := ma.fileReader.GetFile(pkg, label) |
| fd := analyzeFile(f) |
| ma.files[filename] = fd |
| return fd |
| } |
| |
| // IsMacro is a public function that checks whether the given function is a macro |
| func (ma macroAnalyzer) IsMacro(fn function) (report functionReport) { |
| // Check the cache first |
| if cached, ok := ma.cache[fn]; ok { |
| return cached |
| } |
| // Write a negative result to the cache before analyzing. This will prevent stack overflow crashes |
| // if the input data contains recursion. |
| ma.cache[fn] = report |
| defer func() { |
| // Update the cache with the actual result |
| ma.cache[fn] = report |
| }() |
| |
| // Check for native rules |
| if fn.filename == nativeModule { |
| switch fn.name { |
| case "glob", "existing_rule", "existing_rules", "package_name", |
| "repository_name", "exports_files": |
| // Not a rule |
| default: |
| report.isMacro = true |
| } |
| return |
| } |
| |
| fileData := ma.getFileData(fn.pkg, fn.filename) |
| |
| // Check whether fn.name is an alias for another function |
| if alias, ok := fileData.aliases[fn.name]; ok { |
| if ma.IsMacro(alias).isMacro { |
| report.isMacro = true |
| } |
| return |
| } |
| |
| // Check whether fn.name is a rule |
| if fileData.rules[fn.name] { |
| report.isMacro = true |
| return |
| } |
| |
| // Check whether fn.name is an ordinary function |
| funCalls, ok := fileData.functions[fn.name] |
| if !ok { |
| return |
| } |
| |
| // Prioritize function calls from already loaded files. If some of the function calls are from the same file |
| // (or another file that has been loaded already), check them first. |
| var knownFunCalls, newFunCalls []funCall |
| for _, fc := range funCalls { |
| if _, ok := ma.files[fc.function.pkg+":"+fc.function.filename]; ok || fc.function.filename == nativeModule { |
| knownFunCalls = append(knownFunCalls, fc) |
| } else { |
| newFunCalls = append(newFunCalls, fc) |
| } |
| } |
| |
| for _, fc := range append(knownFunCalls, newFunCalls...) { |
| if ma.IsMacro(fc.function).isMacro { |
| report.isMacro = true |
| report.fc = &fc |
| return |
| } |
| } |
| |
| return |
| } |
| |
| // newMacroAnalyzer creates and initiates an instance of macroAnalyzer. |
| func newMacroAnalyzer(fileReader *FileReader) macroAnalyzer { |
| return macroAnalyzer{ |
| fileReader: fileReader, |
| files: make(map[string]fileData), |
| cache: make(map[function]functionReport), |
| } |
| } |
| |
| func unnamedMacroWarning(f *build.File, fileReader *FileReader) []*LinterFinding { |
| if f.Type != build.TypeBzl { |
| return nil |
| } |
| |
| macroAnalyzer := newMacroAnalyzer(fileReader) |
| macroAnalyzer.files[f.Pkg+":"+f.Label] = analyzeFile(f) |
| |
| findings := []*LinterFinding{} |
| for _, stmt := range f.Stmt { |
| def, ok := stmt.(*build.DefStmt) |
| if !ok { |
| continue |
| } |
| |
| if strings.HasPrefix(def.Name, "_") || acceptsNameArgument(def) { |
| continue |
| } |
| |
| report := macroAnalyzer.IsMacro(function{f.Pkg, f.Label, def.Name}) |
| if !report.isMacro { |
| continue |
| } |
| msg := fmt.Sprintf(`The macro %q should have a keyword argument called "name".`, def.Name) |
| if report.fc != nil { |
| // fc shouldn't be nil because that's the only node that can be found inside a function. |
| msg += fmt.Sprintf(` |
| |
| It is considered a macro because it calls a rule or another macro %q on line %d. |
| |
| By convention, every public macro needs a "name" argument (even if it doesn't use it). |
| This is important for tooling and automation. |
| |
| * If this function is a helper function that's not supposed to be used outside of this file, |
| please make it private (e.g. rename it to "_%s"). |
| * Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, report.fc.nameAlias, report.fc.line, def.Name) |
| } |
| finding := makeLinterFinding(def, msg) |
| finding.End = def.ColonPos |
| findings = append(findings, finding) |
| } |
| |
| return findings |
| } |