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  }