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 }