github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/cmd/tast-lint/internal/check/tast_search_flags.go (about) 1 // Copyright 2022 The ChromiumOS Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package check 6 7 import ( 8 "fmt" 9 "go/ast" 10 "go/token" 11 "strings" 12 ) 13 14 // hasImport checks if the import declaration contains specified package name. 15 // If so, it returns the name of the import, and an empty string otherwise. 16 func hasImport(f *ast.File, pkgName string) string { 17 sfmt := fmt.Sprintf("\"%s\"", pkgName) 18 for _, decl := range f.Decls { 19 genDecl, ok := decl.(*ast.GenDecl) 20 if !ok || genDecl.Tok != token.IMPORT { 21 continue 22 } 23 24 for _, spec := range genDecl.Specs { 25 importSpec, ok := spec.(*ast.ImportSpec) 26 if !ok || importSpec.Path.Kind != token.STRING { 27 continue 28 } 29 30 if importSpec.Path.Value == sfmt { 31 if importSpec.Name != nil { 32 return importSpec.Name.Name 33 } 34 35 sparts := strings.Split(pkgName, "/") 36 return sparts[len(sparts)-1] 37 } 38 } 39 40 break 41 } 42 43 return "" 44 } 45 46 // hasTastTestInit checks if the given function declaration is an init function 47 // which declares a Tast test. If so, it returns true, and false otherwise. 48 func hasTastTestInit(f ast.FuncDecl, testingPkgName string) bool { 49 if f.Name.Name != "init" || len(f.Body.List) != 1 { 50 return false 51 } 52 53 exprStmt, ok := f.Body.List[0].(*ast.ExprStmt) 54 if !ok { 55 return false 56 } 57 58 callExpr, ok := exprStmt.X.(*ast.CallExpr) 59 if !ok { 60 return false 61 } 62 63 selectorExpr, ok := callExpr.Fun.(*ast.SelectorExpr) 64 if !ok || selectorExpr.Sel.Name != "AddTest" { 65 return false 66 } 67 68 ident, ok := selectorExpr.X.(*ast.Ident) 69 if !ok || ident.Name != testingPkgName { 70 return false 71 } 72 73 return true 74 } 75 76 // extractFunctions extracts all top-level function declarations from the given 77 // File. It returns a map where the key is the name of the function, and the 78 // value is the function declaration. 79 func extractFunctions(f *ast.File) map[string]ast.FuncDecl { 80 m := make(map[string]ast.FuncDecl) 81 for _, decl := range f.Decls { 82 funcDecl, ok := decl.(*ast.FuncDecl) 83 if !ok { 84 continue 85 } 86 87 m[funcDecl.Name.Name] = *funcDecl 88 } 89 90 return m 91 } 92 93 // extractValue tries to extract the value from a SelectorExpr, or a string 94 // BasicLit. If successful, it returns the string value and the position of the 95 // token. Otherwise, it returns an empty string and token.NoPos. 96 // When the allowedValues is not nil, it returns only the values that are keys 97 // in the map, and that have a value of true. 98 func extractValue(node ast.Node, allowedValues map[string]bool, pkgName string) (string, token.Pos) { 99 var value string 100 101 // Check if the value is defined as a selector expression. 102 selectorExpr, ok := node.(*ast.SelectorExpr) 103 if ok { 104 ident, ok := selectorExpr.X.(*ast.Ident) 105 if ok && ident.Name == pkgName { 106 value = selectorExpr.Sel.Name 107 } 108 } 109 110 // Check if the value is defined as a string literal. 111 basicLit, ok := node.(*ast.BasicLit) 112 if ok && basicLit.Kind == token.STRING && len(basicLit.Value) > 2 { 113 value = basicLit.Value[1 : len(basicLit.Value)-1] 114 } 115 116 if allowedValues != nil { 117 val, ok := allowedValues[value] 118 if !ok || !val { 119 return "", token.NoPos 120 } 121 } 122 123 return value, node.Pos() 124 } 125 126 // extractValues tries to extract the value from the given nodes. 127 // To decide which values to select, extractValue is used. 128 func extractValues(nodes []ast.Node, allowedValues map[string]bool, pkgName string) map[string]token.Pos { 129 m := make(map[string]token.Pos) 130 131 // Find all Values in the Search Flags. 132 fv := funcVisitor(func(node ast.Node) { 133 value, pos := extractValue(node, allowedValues, pkgName) 134 if pos == token.NoPos { 135 return 136 } 137 138 m[value] = pos 139 }) 140 141 for _, node := range nodes { 142 ast.Walk(fv, node) 143 } 144 145 return m 146 } 147 148 // extractSearchFlagNodes tries to extract the value from the SearchFlags and 149 // ExtraSearchFlags Key-Value pairs. It returns a slice with the given nodes. 150 func extractSearchFlagNodes(f *ast.File) []ast.Node { 151 var nodes []ast.Node 152 153 // Find all SearchFlag and ExtraSearchFlag declarations. 154 fv := funcVisitor(func(node ast.Node) { 155 kvExpr, ok := node.(*ast.KeyValueExpr) 156 if !ok { 157 return 158 } 159 160 keyIdent, ok := kvExpr.Key.(*ast.Ident) 161 if !ok || (keyIdent.Name != "SearchFlags" && keyIdent.Name != "ExtraSearchFlags") { 162 return 163 } 164 165 nodes = append(nodes, kvExpr.Value) 166 }) 167 168 ast.Walk(fv, f) 169 170 return nodes 171 } 172 173 // extractSearchFlagValues tries to extract the value from the SearchFlags and 174 // ExtraSearchFlags Key-Value pairs. It returns a slice with the given nodes. 175 func extractSearchFlagValues(nodes []ast.Node, funcs map[string]ast.FuncDecl, allowedValues map[string]bool, pkgName string) (map[string]token.Pos, bool) { 176 m := make(map[string]token.Pos) 177 178 var nonFunctionNodes []ast.Node 179 for _, node := range nodes { 180 callExpr, ok := node.(*ast.CallExpr) 181 if !ok { 182 nonFunctionNodes = append(nonFunctionNodes, node) 183 continue 184 } 185 186 var name string 187 ident, ok := callExpr.Fun.(*ast.Ident) 188 if ok { 189 name = ident.Name 190 } else { 191 selectorExpr, ok := callExpr.Fun.(*ast.SelectorExpr) 192 if !ok { 193 continue 194 } 195 196 ident, ok = selectorExpr.X.(*ast.Ident) 197 if !ok { 198 continue 199 } 200 201 name = fmt.Sprintf("%s.%s", ident.Name, selectorExpr.Sel.Name) 202 } 203 204 funcDecl, ok := funcs[name] 205 if !ok { 206 // We have an imported function. 207 return nil, true 208 } 209 210 // We have a function in the same file. 211 vals := extractValues([]ast.Node{funcDecl.Body}, allowedValues, pkgName) 212 m = union(m, vals) 213 } 214 215 vals := extractValues(nonFunctionNodes, allowedValues, pkgName) 216 m = union(m, vals) 217 218 return m, false 219 } 220 221 // extractTestFileValues tries to extract the value from the file based on the 222 // extractValue function. If ignoredValues is not nil, the values that have the 223 // specified position will be ignored. 224 func extractTestFileValues(f *ast.File, values map[string]bool, pkgName string, ignoredValues map[string]token.Pos) map[string]token.Pos { 225 m := make(map[string]token.Pos) 226 227 // Find all Values in the file. 228 fv := funcVisitor(func(node ast.Node) { 229 value, pos := extractValue(node, values, pkgName) 230 if pos == token.NoPos { 231 return 232 } 233 234 if ignoredValues != nil { 235 ipos, ok := ignoredValues[value] 236 if ok && ipos == pos { 237 return 238 } 239 } 240 241 m[value] = pos 242 }) 243 244 ast.Walk(fv, f) 245 246 return m 247 } 248 249 // SearchFlags checks search flags in Tast tests definitions for policy names 250 // that have been used during the testing phase. If the file is not a Tast test, 251 // then it is skipped. 252 func SearchFlags(fs *token.FileSet, f *ast.File) (issues []*Issue) { 253 policyPkgName := hasImport(f, "go.chromium.org/tast-tests/cros/common/policy") 254 if policyPkgName == "" { 255 return 256 } 257 258 testingPkgName := hasImport(f, "go.chromium.org/tast/core/testing") 259 if testingPkgName == "" { 260 return 261 } 262 263 functions := extractFunctions(f) 264 if init, ok := functions["init"]; !ok || !hasTastTestInit(init, testingPkgName) { 265 return 266 } 267 268 policyNames := policyNames() 269 nodes := extractSearchFlagNodes(f) 270 tags, ok := extractSearchFlagValues(nodes, functions, policyNames, policyPkgName) 271 if ok { 272 return 273 } 274 275 usedPolicies := extractTestFileValues(f, policyNames, policyPkgName, tags) 276 277 for k, v := range usedPolicies { 278 _, ok := tags[k] 279 if ok { 280 continue 281 } 282 283 issues = append(issues, &Issue{ 284 Pos: fs.Position(v), 285 Msg: fmt.Sprintf("Policy %s does not have a corresponding Search Flag.", k), 286 Link: "go/remote-management/tast-codelabs/policy_coverage_insights", 287 }) 288 } 289 290 return 291 }