...

Source file src/github.com/bazelbuild/buildtools/warn/warn_macro.go

Documentation: github.com/bazelbuild/buildtools/warn

     1  /*
     2  Copyright 2020 Google LLC
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      https://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Warnings for using deprecated functions
    18  
    19  package warn
    20  
    21  import (
    22  	"fmt"
    23  	"strings"
    24  
    25  	"github.com/bazelbuild/buildtools/build"
    26  	"github.com/bazelbuild/buildtools/labels"
    27  )
    28  
    29  // Internal constant that represents the native module
    30  const nativeModule = "<native>"
    31  
    32  // function represents a function identifier, which is a pair (module name, function name).
    33  type function struct {
    34  	pkg      string // package where the function is defined
    35  	filename string // name of a .bzl file relative to the package
    36  	name     string // original name of the function
    37  }
    38  
    39  func (f function) label() string {
    40  	return f.pkg + ":" + f.filename
    41  }
    42  
    43  // funCall represents a call to another function. It contains information of the function itself as well as some
    44  // information about the environment
    45  type funCall struct {
    46  	function
    47  	nameAlias string // function name alias (it could be loaded with a different name or assigned to a new variable).
    48  	line      int    // line on which the function is being called
    49  }
    50  
    51  // acceptsNameArgument checks whether a function can accept a named argument called "name",
    52  // either directly or via **kwargs.
    53  func acceptsNameArgument(def *build.DefStmt) bool {
    54  	for _, param := range def.Params {
    55  		if name, op := build.GetParamName(param); name == "name" || op == "**" {
    56    		return true
    57  		}
    58  	}
    59  	return false
    60  }
    61  
    62  // fileData represents information about rules and functions extracted from a file
    63  type fileData struct {
    64  	rules     map[string]bool               // all rules defined in the file
    65  	functions map[string]map[string]funCall // outer map: all functions defined in the file, inner map: all distinct function calls from the given function
    66  	aliases   map[string]function           // all top-level aliases (e.g. `foo = bar`).
    67  }
    68  
    69  // resolvesExternal takes a local function definition and replaces it with an external one if it's been defined
    70  // in another file and loaded
    71  func resolveExternal(fn function, externalSymbols map[string]function) function {
    72  	if external, ok := externalSymbols[fn.name]; ok {
    73  		return external
    74  	}
    75  	return fn
    76  }
    77  
    78  // exprLine returns the start line of an expression
    79  func exprLine(expr build.Expr) int {
    80  	start, _ := expr.Span()
    81  	return start.Line
    82  }
    83  
    84  // getFunCalls extracts information about functions that are being called from the given function
    85  func getFunCalls(def *build.DefStmt, pkg, filename string, externalSymbols map[string]function) map[string]funCall {
    86  	funCalls := make(map[string]funCall)
    87  	build.Walk(def, func(expr build.Expr, stack []build.Expr) {
    88  		call, ok := expr.(*build.CallExpr)
    89  		if !ok {
    90  			return
    91  		}
    92  		if ident, ok := call.X.(*build.Ident); ok {
    93  			funCalls[ident.Name] = funCall{
    94  				function:  resolveExternal(function{pkg, filename, ident.Name}, externalSymbols),
    95  				nameAlias: ident.Name,
    96  				line:      exprLine(call),
    97  			}
    98  			return
    99  		}
   100  		dot, ok := call.X.(*build.DotExpr)
   101  		if !ok {
   102  			return
   103  		}
   104  		if ident, ok := dot.X.(*build.Ident); !ok || ident.Name != "native" {
   105  			return
   106  		}
   107  		name := "native." + dot.Name
   108  		funCalls[name] = funCall{
   109  			function: function{
   110  				name:     dot.Name,
   111  				filename: nativeModule,
   112  			},
   113  			nameAlias: name,
   114  			line:      exprLine(dot),
   115  		}
   116  	})
   117  	return funCalls
   118  }
   119  
   120  // analyzeFile extracts the information about rules and functions defined in the file
   121  func analyzeFile(f *build.File) fileData {
   122  	if f == nil {
   123  		return fileData{}
   124  	}
   125  
   126  	// Collect loaded symbols
   127  	externalSymbols := make(map[string]function)
   128  	for _, stmt := range f.Stmt {
   129  		load, ok := stmt.(*build.LoadStmt)
   130  		if !ok {
   131  			continue
   132  		}
   133  		label := labels.ParseRelative(load.Module.Value, f.Pkg)
   134  		if label.Repository != "" || label.Target == "" {
   135  			continue
   136  		}
   137  		for i, from := range load.From {
   138  			externalSymbols[load.To[i].Name] = function{label.Package, label.Target, from.Name}
   139  		}
   140  	}
   141  
   142  	report := fileData{
   143  		rules:     make(map[string]bool),
   144  		functions: make(map[string]map[string]funCall),
   145  		aliases:   make(map[string]function),
   146  	}
   147  	for _, stmt := range f.Stmt {
   148  		switch stmt := stmt.(type) {
   149  		case *build.AssignExpr:
   150  			// Analyze aliases (`foo = bar`) or rule declarations (`foo = rule(...)`)
   151  			lhsIdent, ok := stmt.LHS.(*build.Ident)
   152  			if !ok {
   153  				continue
   154  			}
   155  			if rhsIdent, ok := stmt.RHS.(*build.Ident); ok {
   156  				report.aliases[lhsIdent.Name] = resolveExternal(function{f.Pkg, f.Label, rhsIdent.Name}, externalSymbols)
   157  				continue
   158  			}
   159  
   160  			call, ok := stmt.RHS.(*build.CallExpr)
   161  			if !ok {
   162  				continue
   163  			}
   164  			ident, ok := call.X.(*build.Ident)
   165  			if !ok || ident.Name != "rule" {
   166  				continue
   167  			}
   168  			report.rules[lhsIdent.Name] = true
   169  		case *build.DefStmt:
   170  			report.functions[stmt.Name] = getFunCalls(stmt, f.Pkg, f.Label, externalSymbols)
   171  		default:
   172  			continue
   173  		}
   174  	}
   175  	return report
   176  }
   177  
   178  // functionReport represents the analysis result of a function
   179  type functionReport struct {
   180  	isMacro bool     // whether the function is a macro (or a rule)
   181  	fc      *funCall // a call to the rule or another macro
   182  }
   183  
   184  // macroAnalyzer is an object that analyzes the directed graph of functions calling each other,
   185  // loading other files lazily if necessary.
   186  type macroAnalyzer struct {
   187  	fileReader *FileReader
   188  	files      map[string]fileData
   189  	cache      map[function]functionReport
   190  }
   191  
   192  // getFileData retrieves a file using the fileReader object and extracts information about functions and rules
   193  // defined in the file.
   194  func (ma macroAnalyzer) getFileData(pkg, label string) fileData {
   195  	filename := pkg + ":" + label
   196  	if fd, ok := ma.files[filename]; ok {
   197  		return fd
   198  	}
   199  	if ma.fileReader == nil {
   200  		fd := fileData{}
   201  		ma.files[filename] = fd
   202  		return fd
   203  	}
   204  	f := ma.fileReader.GetFile(pkg, label)
   205  	fd := analyzeFile(f)
   206  	ma.files[filename] = fd
   207  	return fd
   208  }
   209  
   210  // IsMacro is a public function that checks whether the given function is a macro
   211  func (ma macroAnalyzer) IsMacro(fn function) (report functionReport) {
   212  	// Check the cache first
   213  	if cached, ok := ma.cache[fn]; ok {
   214  		return cached
   215  	}
   216  	// Write a negative result to the cache before analyzing. This will prevent stack overflow crashes
   217  	// if the input data contains recursion.
   218  	ma.cache[fn] = report
   219  	defer func() {
   220  		// Update the cache with the actual result
   221  		ma.cache[fn] = report
   222  	}()
   223  
   224  	// Check for native rules
   225  	if fn.filename == nativeModule {
   226  		switch fn.name {
   227  		case "glob", "existing_rule", "existing_rules", "package_name",
   228  			"repository_name", "exports_files":
   229  			// Not a rule
   230  		default:
   231  			report.isMacro = true
   232  		}
   233  		return
   234  	}
   235  
   236  	fileData := ma.getFileData(fn.pkg, fn.filename)
   237  
   238  	// Check whether fn.name is an alias for another function
   239  	if alias, ok := fileData.aliases[fn.name]; ok {
   240  		if ma.IsMacro(alias).isMacro {
   241  			report.isMacro = true
   242  		}
   243  		return
   244  	}
   245  
   246  	// Check whether fn.name is a rule
   247  	if fileData.rules[fn.name] {
   248  		report.isMacro = true
   249  		return
   250  	}
   251  
   252  	// Check whether fn.name is an ordinary function
   253  	funCalls, ok := fileData.functions[fn.name]
   254  	if !ok {
   255  		return
   256  	}
   257  
   258  	// Prioritize function calls from already loaded files. If some of the function calls are from the same file
   259  	// (or another file that has been loaded already), check them first.
   260  	var knownFunCalls, newFunCalls []funCall
   261  	for _, fc := range funCalls {
   262  		if _, ok := ma.files[fc.function.pkg+":"+fc.function.filename]; ok || fc.function.filename == nativeModule {
   263  			knownFunCalls = append(knownFunCalls, fc)
   264  		} else {
   265  			newFunCalls = append(newFunCalls, fc)
   266  		}
   267  	}
   268  
   269  	for _, fc := range append(knownFunCalls, newFunCalls...) {
   270  		if ma.IsMacro(fc.function).isMacro {
   271  			report.isMacro = true
   272  			report.fc = &fc
   273  			return
   274  		}
   275  	}
   276  
   277  	return
   278  }
   279  
   280  // newMacroAnalyzer creates and initiates an instance of macroAnalyzer.
   281  func newMacroAnalyzer(fileReader *FileReader) macroAnalyzer {
   282  	return macroAnalyzer{
   283  		fileReader: fileReader,
   284  		files:      make(map[string]fileData),
   285  		cache:      make(map[function]functionReport),
   286  	}
   287  }
   288  
   289  func unnamedMacroWarning(f *build.File, fileReader *FileReader) []*LinterFinding {
   290  	if f.Type != build.TypeBzl {
   291  		return nil
   292  	}
   293  
   294  	macroAnalyzer := newMacroAnalyzer(fileReader)
   295  	macroAnalyzer.files[f.Pkg+":"+f.Label] = analyzeFile(f)
   296  
   297  	findings := []*LinterFinding{}
   298  	for _, stmt := range f.Stmt {
   299  		def, ok := stmt.(*build.DefStmt)
   300  		if !ok {
   301  			continue
   302  		}
   303  
   304  		if strings.HasPrefix(def.Name, "_") || acceptsNameArgument(def) {
   305  			continue
   306  		}
   307  
   308  		report := macroAnalyzer.IsMacro(function{f.Pkg, f.Label, def.Name})
   309  		if !report.isMacro {
   310  			continue
   311  		}
   312  		msg := fmt.Sprintf(`The macro %q should have a keyword argument called "name".`, def.Name)
   313  		if report.fc != nil {
   314  			// fc shouldn't be nil because that's the only node that can be found inside a function.
   315  			msg += fmt.Sprintf(`
   316  
   317  It is considered a macro because it calls a rule or another macro %q on line %d.
   318  
   319  By convention, every public macro needs a "name" argument (even if it doesn't use it).
   320  This is important for tooling and automation.
   321  
   322    * If this function is a helper function that's not supposed to be used outside of this file,
   323      please make it private (e.g. rename it to "_%s").
   324    * Otherwise, add a "name" argument. If possible, use that name when calling other macros/rules.`, report.fc.nameAlias, report.fc.line, def.Name)
   325  		}
   326  		finding := makeLinterFinding(def, msg)
   327  		finding.End = def.ColonPos
   328  		findings = append(findings, finding)
   329  	}
   330  
   331  	return findings
   332  }
   333  

View as plain text