github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/cmd/tast-lint/internal/check/forbidden_calls.go (about) 1 // Copyright 2018 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 "strconv" 12 "strings" 13 14 "golang.org/x/tools/go/ast/astutil" 15 ) 16 17 // Ignore known valid use cases needing to override forbidden calls 18 var allowList = []string{ 19 // meta tests to test the test over-running the test's timeout 20 "src/go.chromium.org/tast-tests/cros/local/bundles/cros/meta/local_timeout.go", 21 "src/go.chromium.org/tast-tests/cros/remote/bundles/cros/meta/remote_timeout.go", 22 } 23 24 // ForbiddenCalls checks if any forbidden functions are called. 25 func ForbiddenCalls(fs *token.FileSet, f *ast.File, fix bool) []*Issue { 26 isUnitTest := isUnitTestFile(fs.Position(f.Package).Filename) 27 var issues []*Issue 28 var importsRequired []string 29 30 filepath := fs.Position(f.Package).Filename 31 for _, p := range allowList { 32 if strings.HasSuffix(filepath, p) { 33 return issues 34 } 35 } 36 37 // Being able to run goimports is a precondition to being able to make any fixes. 38 fixable := false 39 if src, err := formatASTNode(fs, f); err == nil { 40 fixable = goimportApplicable(src) 41 } 42 // Checks for the possibility of linting fmt.Errorf with errors from "go.chromium.org/tast/core/errors" including existing 43 // 'errors' identifiers (if any) and the requirement of importing errors package (if not imported). 44 hasErrorsImport, errorsErr := checkErrors(f) 45 46 astutil.Apply(f, func(c *astutil.Cursor) bool { 47 sel, ok := c.Node().(*ast.SelectorExpr) 48 if !ok { 49 return true 50 } 51 // TODO(nya): Support imports with different aliases. 52 x, ok := sel.X.(*ast.Ident) 53 if !ok { 54 return true 55 } 56 57 methodName := sel.Sel.Name 58 call := fmt.Sprintf("%s.%s", x.Name, methodName) 59 switch call { 60 case "context.Background", "context.TODO": 61 if !isUnitTest { 62 issues = append(issues, &Issue{ 63 Pos: fs.Position(x.Pos()), 64 Msg: call + " ignores test timeout; use test function's ctx arg instead", 65 Link: "https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/writing_tests.md#Contexts-and-timeouts", 66 }) 67 } 68 case "fmt.Errorf": 69 if !fix { 70 msg := "go.chromium.org/tast/core/errors.Errorf should be used instead of fmt.Errorf" 71 if errorsErr != nil { 72 msg = fmt.Sprintf("%s. Also found an error: %s", msg, errorsErr) 73 } 74 issues = append(issues, &Issue{ 75 Pos: fs.Position(x.Pos()), 76 Msg: msg, 77 Link: "https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/writing_tests.md#Error-construction", 78 Fixable: errorsErr == nil, 79 }) 80 } else if fixable && errorsErr == nil { 81 c.Replace(&ast.SelectorExpr{ 82 X: &ast.Ident{ 83 Name: "errors", 84 }, 85 Sel: &ast.Ident{ 86 Name: "Errorf", 87 }, 88 }) 89 90 if !hasErrorsImport { 91 importsRequired = append(importsRequired, "go.chromium.org/tast/core/errors") 92 } 93 } 94 case "time.Sleep": 95 issues = append(issues, &Issue{ 96 Pos: fs.Position(x.Pos()), 97 Msg: "time.Sleep ignores context deadline; use testing.Poll or testing.Sleep instead", 98 Link: "https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/writing_tests.md#Contexts-and-timeouts", 99 }) 100 case "testing.FixtSerializedValue": 101 issues = append(issues, &Issue{ 102 Pos: fs.Position(x.Pos()), 103 Msg: "testing.FixtSerializedValue is deprecated; use testing.FixtFillValue", 104 Link: "https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/writing_tests.md#fixtures", 105 }) 106 case "dbus.SystemBus", "dbus.SystemBusPrivate": 107 if !fix { 108 issues = append(issues, &Issue{ 109 Pos: fs.Position(x.Pos()), 110 Msg: fmt.Sprintf("dbus.%v may reorder signals; use go.chromium.org/tast-tests/cros/local/dbusutil.%v instead", methodName, methodName), 111 Link: "https://github.com/godbus/dbus/issues/187", 112 Fixable: fixable, 113 }) 114 } else if fixable { 115 c.Replace(&ast.SelectorExpr{ 116 X: &ast.Ident{ 117 Name: "dbusutil", 118 }, 119 Sel: &ast.Ident{ 120 Name: methodName, 121 }, 122 }) 123 importsRequired = append(importsRequired, "go.chromium.org/tast-tests/cros/local/dbusutil") 124 } 125 case "os.Chdir": 126 if !isUnitTest { 127 issues = append(issues, &Issue{ 128 Pos: fs.Position(x.Pos()), 129 Msg: "os.Chdir changes the shared CWD of the process, reference files using absolute paths.", 130 Link: "https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/code_review_comments.md#os_Chdir", 131 }) 132 } 133 } 134 135 return true 136 }, nil) 137 138 for _, pkg := range importsRequired { 139 astutil.AddImport(fs, f, pkg) 140 } 141 142 // Rearrange Imports for new imports or when fmt.Errorf has been handled with conditional import. 143 if len(importsRequired) > 0 || (fix && errorsErr == nil) { 144 newf, err := ImportOrderAutoFix(fs, f) 145 if err != nil { 146 return issues 147 } 148 *f = *newf 149 } 150 return issues 151 } 152 153 // hasIdentErrors returns true if there is an identifier node "errors" which is not a part of 154 // "go.chromium.org/tast/core/errors" package, otherwise returns false. 155 func hasIdentErrors(f *ast.File) bool { 156 hasErrors := false 157 ast.Inspect(f, func(node ast.Node) bool { 158 if hasErrors { 159 return false // no further deep exploration 160 } 161 switch node.(type) { 162 case *ast.SelectorExpr: 163 // Removes the possibility of <exp>.<sel> with something like errors.New, errors.Wrap etc. Also it prunes the subtree from inspection. 164 return false 165 case *ast.Ident: 166 if id := node.(*ast.Ident); id.Name == "errors" { 167 hasErrors = true 168 return false 169 } 170 } 171 return true 172 }) 173 174 return hasErrors 175 } 176 177 // checkErrors checks if the "go.chromium.org/tast/core/errors" package could replace the occurrences of fmt.Errorf 178 // automatically or manual intervention is solicited. 179 func checkErrors(f *ast.File) (bool, error) { 180 if hasIdentErrors(f) { 181 return false, fmt.Errorf("manual fix required, detected 'errors' as an identifier") 182 } 183 184 const pkg = "go.chromium.org/tast/core/errors" 185 for _, imp := range f.Imports { 186 iPath, err := strconv.Unquote(imp.Path.Value) 187 if err != nil || iPath != pkg { 188 continue 189 } 190 // An existing import of "go.chromium.org/tast/core/errors" without alias 191 if imp.Name == nil { 192 return true, nil 193 } 194 195 if alias := imp.Name.Name; alias == "." || alias == "_" { 196 return false, fmt.Errorf("importing '%s' as '.' or '_' is highly discouraged, please fix it", pkg) 197 } 198 return false, fmt.Errorf("importing '%s' with alias when there is no name conflict, is highly discouraged", pkg) 199 } 200 return false, nil 201 }