github.com/verrazzano/verrazzano@v1.7.0/tools/eventually-checker/check_eventually.go (about)

     1  // Copyright (c) 2021, 2022, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  package main
     4  
     5  import (
     6  	"flag"
     7  	"fmt"
     8  	"go/ast"
     9  	"go/token"
    10  	"io"
    11  	"os"
    12  	"strings"
    13  
    14  	"golang.org/x/tools/go/packages"
    15  )
    16  
    17  const mode packages.LoadMode = packages.NeedName |
    18  	packages.NeedTypes |
    19  	packages.NeedSyntax |
    20  	packages.NeedTypesInfo
    21  
    22  // funcCall contains information about a function call, including the function name and its position in a file
    23  type funcCall struct {
    24  	name string
    25  	pos  token.Pos
    26  }
    27  
    28  // funcMap is a map of function names to functions called directly by the function
    29  var funcMap = make(map[string][]funcCall)
    30  
    31  // eventuallyMap is a map of locations of Eventually calls and the functions called directly by those Eventually calls
    32  var eventuallyMap = make(map[token.Pos][]funcCall)
    33  
    34  // if reportOnly is true, we always return a zero exit code
    35  var reportOnly bool
    36  
    37  // parseFlags sets up command line arg and flag parsing
    38  func parseFlags() {
    39  	flag.Usage = func() {
    40  		help := "Usage of %s: [options] path\nScans all packages in path and outputs function calls that cause Eventually to exit prematurely\n\n"
    41  		fmt.Fprintf(flag.CommandLine.Output(), help, os.Args[0])
    42  		flag.PrintDefaults()
    43  	}
    44  	flag.BoolVar(&reportOnly, "report", false, "report on problems but always exits with a zero status code")
    45  	flag.Parse()
    46  }
    47  
    48  // main loads the packages from the specified directories, analyzes the file sets, and displays information about
    49  // calls that should not be called from Eventually
    50  func main() {
    51  	parseFlags()
    52  	if flag.NArg() != 1 {
    53  		flag.Usage()
    54  		os.Exit(1)
    55  	}
    56  
    57  	// load all packages from the specified directory
    58  	fset, pkgs, err := loadPackages(flag.Args()[0])
    59  	if err != nil {
    60  		fmt.Fprintln(flag.CommandLine.Output(), err)
    61  		os.Exit(1)
    62  	}
    63  
    64  	// analyze each package
    65  	for _, pkg := range pkgs {
    66  		analyze(pkg.Syntax)
    67  	}
    68  
    69  	// check for calls that should not be in Eventually blocks and display results
    70  	results := checkForBadCalls()
    71  	displayResults(results, fset, flag.CommandLine.Output())
    72  
    73  	if len(results) > 0 && !reportOnly {
    74  		os.Exit(1)
    75  	}
    76  
    77  	os.Exit(0)
    78  }
    79  
    80  // loadPackages loads the packages from the specified path and returns the FileSet, the slice of packages,
    81  // and an error
    82  func loadPackages(path string) (*token.FileSet, []*packages.Package, error) {
    83  	fset := token.NewFileSet()
    84  	cfg := &packages.Config{Tests: true, Fset: fset, Mode: mode, Dir: path}
    85  	pkgs, err := packages.Load(cfg, "./...")
    86  	if err != nil {
    87  		return nil, nil, err
    88  	}
    89  	return fset, pkgs, nil
    90  }
    91  
    92  // analyze analyzes all of the files in the file set, looking for calls to Fail & Expect inside of Eventually.
    93  // We use ast.Inspect to walk the abstract syntax tree, searching for function declarations and function calls.
    94  // If we find a call to Eventually, we record it in the map of Eventually calls, otherwise we record the
    95  // call in funcMap.
    96  func analyze(files []*ast.File) {
    97  	for _, file := range files {
    98  		var currentFuncDecl string
    99  		var funcEnd token.Pos
   100  		ast.Inspect(file, func(n ast.Node) bool {
   101  			if n != nil && n.Pos() > funcEnd {
   102  				// this node is after the current function decl end position, so reset
   103  				currentFuncDecl = ""
   104  				funcEnd = 0
   105  			}
   106  
   107  			pkg := file.Name.Name
   108  			switch x := n.(type) {
   109  			case *ast.FuncDecl:
   110  				currentFuncDecl = getFuncDeclName(pkg, x)
   111  				funcEnd = x.End()
   112  			case *ast.CallExpr:
   113  				name, pos := getNameAndPosFromCallExpr(x, file.Name.Name)
   114  
   115  				if name != "" {
   116  					if strings.HasSuffix(name, ".Eventually") {
   117  						f, isAnonFunc := getEventuallyFuncName(pkg, x.Args)
   118  						if isAnonFunc {
   119  							inspectEventuallyAnonFunc(pos, x.Args[0], pkg, currentFuncDecl)
   120  							// returning false tells the inspector there's no need to continue walking this
   121  							// part of the tree
   122  							return false
   123  						}
   124  						addCallToEventuallyMap(pos, f, pos)
   125  					} else if currentFuncDecl != "" {
   126  						if _, ok := funcMap[currentFuncDecl]; !ok {
   127  							funcMap[currentFuncDecl] = make([]funcCall, 0)
   128  						}
   129  						funcMap[currentFuncDecl] = append(funcMap[currentFuncDecl], funcCall{name: name, pos: pos})
   130  					}
   131  				}
   132  			}
   133  			return true
   134  		})
   135  	}
   136  }
   137  
   138  // getFuncDeclName constructs a function name of the form pkg.func_name or pkg.type.func_name if the
   139  // function is a method receiver
   140  func getFuncDeclName(pkg string, funcDecl *ast.FuncDecl) string {
   141  	baseFuncName := pkg
   142  
   143  	if funcDecl.Recv != nil {
   144  		// this function decl is a method receiver so include the type in the name
   145  		recType := funcDecl.Recv.List[0].Type
   146  		switch x := recType.(type) {
   147  		case *ast.StarExpr:
   148  			// pointer receiver
   149  			baseFuncName = fmt.Sprintf("%s.%s", baseFuncName, x.X)
   150  		case *ast.Ident:
   151  			// value receiver
   152  			baseFuncName = fmt.Sprintf("%s.%s", baseFuncName, x)
   153  		}
   154  	}
   155  
   156  	return fmt.Sprintf("%s.%s", baseFuncName, funcDecl.Name.Name)
   157  }
   158  
   159  // inspectEventuallyAnonFunc finds all function calls in an anonymous function passed to Eventually
   160  func inspectEventuallyAnonFunc(eventuallyPos token.Pos, node ast.Node, pkgName string, parent string) {
   161  	ast.Inspect(node, func(n ast.Node) bool {
   162  		switch x := n.(type) {
   163  		case *ast.CallExpr:
   164  			name, pos := getNameAndPosFromCallExpr(x, pkgName)
   165  			addCallToEventuallyMap(eventuallyPos, name, pos)
   166  		}
   167  		return true
   168  	})
   169  }
   170  
   171  // addCallToEventuallyMap adds a function name at the given position to the map of Eventually calls
   172  func addCallToEventuallyMap(eventuallyPos token.Pos, funcName string, pos token.Pos) {
   173  	if _, ok := eventuallyMap[eventuallyPos]; !ok {
   174  		eventuallyMap[eventuallyPos] = make([]funcCall, 0)
   175  	}
   176  	eventuallyMap[eventuallyPos] = append(eventuallyMap[eventuallyPos], funcCall{name: funcName, pos: pos})
   177  }
   178  
   179  // getNameAndPosFromCallExpr gets the function name and position from an ast.CallExpr
   180  func getNameAndPosFromCallExpr(expr *ast.CallExpr, pkgName string) (string, token.Pos) {
   181  	switch x := expr.Fun.(type) {
   182  	case *ast.Ident:
   183  		// ast.Ident means the call is in the same package as the enclosing function declaration, so use the
   184  		// package from the func decl
   185  		name := pkgName + "." + x.Name
   186  		pos := x.NamePos
   187  		return name, pos
   188  	case *ast.SelectorExpr:
   189  		var pkg string
   190  		var pos token.Pos
   191  		if ident, ok := x.X.(*ast.Ident); ok {
   192  			pos = ident.NamePos
   193  			if ident.Obj != nil {
   194  				// call is a method receiver so find the type of the receiver
   195  				if valueSpec, ok := ident.Obj.Decl.(*ast.ValueSpec); ok {
   196  					if selExpr, ok := valueSpec.Type.(*ast.SelectorExpr); ok {
   197  						// type is not in the same package as the calling function
   198  						if ident, ok = selExpr.X.(*ast.Ident); ok {
   199  							pkg = ident.Name + "." + selExpr.Sel.Name + "."
   200  						}
   201  					} else if id, ok := valueSpec.Type.(*ast.Ident); ok {
   202  						// type is in the same package as the caller
   203  						pkg = pkgName + "." + id.Name + "."
   204  					}
   205  				}
   206  			} else {
   207  				pkg = ident.Name + "."
   208  			}
   209  		}
   210  		name := pkg + x.Sel.Name
   211  		return name, pos
   212  	default:
   213  		// ignore other function call types
   214  		return "", 0
   215  	}
   216  }
   217  
   218  // getEventuallyFuncName returns the name of the function (prefixed with package name) passed to
   219  // Eventually and a boolean that will be true if an anonymous function is passed to Eventually
   220  func getEventuallyFuncName(pkg string, args []ast.Expr) (string, bool) {
   221  	if len(args) == 0 {
   222  		panic("No args passed to Eventually call")
   223  	}
   224  
   225  	switch x := args[0].(type) {
   226  	case *ast.FuncLit:
   227  		return "", true
   228  	case *ast.Ident:
   229  		return pkg + "." + x.Name, false
   230  	case *ast.SelectorExpr:
   231  		var p = pkg + "."
   232  		if ident, ok := x.X.(*ast.Ident); ok {
   233  			p = ident.Name + "."
   234  		}
   235  		return p + x.Sel.Name, false
   236  	default:
   237  		panic(fmt.Sprintf("Unexpected AST node type found: %s", x))
   238  	}
   239  }
   240  
   241  // checkForBadCalls searches all the functions called by Eventually functions looking for bad calls and
   242  // returns a map of results, where the key has the position of the bad call and the values contains
   243  // a slice with all of the positions of Eventually calls that call the function (directly or indirectly)
   244  func checkForBadCalls() map[token.Pos][]token.Pos {
   245  	var resultsMap = make(map[token.Pos][]token.Pos)
   246  
   247  	for key, val := range eventuallyMap {
   248  		for i := range val {
   249  			if fc := findBadCall(&val[i], 0); fc != nil {
   250  				if _, ok := resultsMap[fc.pos]; !ok {
   251  					resultsMap[fc.pos] = make([]token.Pos, 0)
   252  				}
   253  				resultsMap[fc.pos] = append(resultsMap[fc.pos], key)
   254  			}
   255  		}
   256  	}
   257  
   258  	return resultsMap
   259  }
   260  
   261  // findBadCall does a depth-first search of function calls looking for calls to Fail or Expect - it returns
   262  // nil if no bad calls are found, or information describing the call (name and file position) if a bad call is found
   263  func findBadCall(fc *funcCall, depth int) *funcCall {
   264  	// if there are any cycles in the call graph due to recursion, use depth value to prevent running forever
   265  	if depth > 30 {
   266  		return nil
   267  	}
   268  
   269  	if strings.HasSuffix(fc.name, ".Fail") || strings.HasSuffix(fc.name, ".Expect") {
   270  		return fc
   271  	}
   272  	fn := funcMap[fc.name]
   273  	for i := range fn {
   274  		if childFuncCall := findBadCall(&fn[i], depth+1); childFuncCall != nil {
   275  			return childFuncCall
   276  		}
   277  	}
   278  
   279  	return nil
   280  }
   281  
   282  // displayResults outputs the analysis results
   283  func displayResults(results map[token.Pos][]token.Pos, fset *token.FileSet, out io.Writer) {
   284  	for key, val := range results {
   285  		fmt.Fprintf(out, "eventuallyChecker: Fail/Expect at %s\n    called from Eventually at:\n", fset.PositionFor(key, true))
   286  		for _, calls := range val {
   287  			fmt.Fprintf(out, "        %s\n", fset.PositionFor(calls, true))
   288  		}
   289  		fmt.Fprintln(out)
   290  	}
   291  }