github.com/chenfeining/golangci-lint@v1.0.2-0.20230730162517-14c6c67868df/pkg/golinters/errcheck.go (about)

     1  package golinters
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"os/user"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/kisielk/errcheck/errcheck"
    14  	"golang.org/x/tools/go/analysis"
    15  	"golang.org/x/tools/go/packages"
    16  
    17  	"github.com/chenfeining/golangci-lint/pkg/config"
    18  	"github.com/chenfeining/golangci-lint/pkg/fsutils"
    19  	"github.com/chenfeining/golangci-lint/pkg/golinters/goanalysis"
    20  	"github.com/chenfeining/golangci-lint/pkg/lint/linter"
    21  	"github.com/chenfeining/golangci-lint/pkg/result"
    22  )
    23  
    24  const errcheckName = "errcheck"
    25  
    26  func NewErrcheck(settings *config.ErrcheckSettings) *goanalysis.Linter {
    27  	var mu sync.Mutex
    28  	var resIssues []goanalysis.Issue
    29  
    30  	analyzer := &analysis.Analyzer{
    31  		Name: errcheckName,
    32  		Doc:  goanalysis.TheOnlyanalyzerDoc,
    33  		Run:  goanalysis.DummyRun,
    34  	}
    35  
    36  	return goanalysis.NewLinter(
    37  		errcheckName,
    38  		"errcheck is a program for checking for unchecked errors in Go code. "+
    39  			"These unchecked errors can be critical bugs in some cases",
    40  		[]*analysis.Analyzer{analyzer},
    41  		nil,
    42  	).WithContextSetter(func(lintCtx *linter.Context) {
    43  		// copied from errcheck
    44  		checker, err := getChecker(settings)
    45  		if err != nil {
    46  			lintCtx.Log.Errorf("failed to get checker: %v", err)
    47  			return
    48  		}
    49  
    50  		checker.Tags = lintCtx.Cfg.Run.BuildTags
    51  
    52  		analyzer.Run = func(pass *analysis.Pass) (any, error) {
    53  			issues := runErrCheck(lintCtx, pass, checker)
    54  			if err != nil {
    55  				return nil, err
    56  			}
    57  
    58  			if len(issues) == 0 {
    59  				return nil, nil
    60  			}
    61  
    62  			mu.Lock()
    63  			resIssues = append(resIssues, issues...)
    64  			mu.Unlock()
    65  
    66  			return nil, nil
    67  		}
    68  	}).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
    69  		return resIssues
    70  	}).WithLoadMode(goanalysis.LoadModeTypesInfo)
    71  }
    72  
    73  func runErrCheck(lintCtx *linter.Context, pass *analysis.Pass, checker *errcheck.Checker) []goanalysis.Issue {
    74  	pkg := &packages.Package{
    75  		Fset:      pass.Fset,
    76  		Syntax:    pass.Files,
    77  		Types:     pass.Pkg,
    78  		TypesInfo: pass.TypesInfo,
    79  	}
    80  
    81  	lintIssues := checker.CheckPackage(pkg).Unique()
    82  	if len(lintIssues.UncheckedErrors) == 0 {
    83  		return nil
    84  	}
    85  
    86  	issues := make([]goanalysis.Issue, len(lintIssues.UncheckedErrors))
    87  
    88  	for i, err := range lintIssues.UncheckedErrors {
    89  		text := "Error return value is not checked"
    90  
    91  		if err.FuncName != "" {
    92  			code := err.SelectorName
    93  			if err.SelectorName == "" {
    94  				code = err.FuncName
    95  			}
    96  
    97  			text = fmt.Sprintf("Error return value of %s is not checked", formatCode(code, lintCtx.Cfg))
    98  		}
    99  
   100  		issues[i] = goanalysis.NewIssue(
   101  			&result.Issue{
   102  				FromLinter: errcheckName,
   103  				Text:       text,
   104  				Pos:        err.Pos,
   105  			},
   106  			pass,
   107  		)
   108  	}
   109  
   110  	return issues
   111  }
   112  
   113  // parseIgnoreConfig was taken from errcheck in order to keep the API identical.
   114  // https://github.com/kisielk/errcheck/blob/1787c4bee836470bf45018cfbc783650db3c6501/main.go#L25-L60
   115  func parseIgnoreConfig(s string) (map[string]*regexp.Regexp, error) {
   116  	if s == "" {
   117  		return nil, nil
   118  	}
   119  
   120  	cfg := map[string]*regexp.Regexp{}
   121  
   122  	for _, pair := range strings.Split(s, ",") {
   123  		colonIndex := strings.Index(pair, ":")
   124  		var pkg, re string
   125  		if colonIndex == -1 {
   126  			pkg = ""
   127  			re = pair
   128  		} else {
   129  			pkg = pair[:colonIndex]
   130  			re = pair[colonIndex+1:]
   131  		}
   132  		regex, err := regexp.Compile(re)
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  		cfg[pkg] = regex
   137  	}
   138  
   139  	return cfg, nil
   140  }
   141  
   142  func getChecker(errCfg *config.ErrcheckSettings) (*errcheck.Checker, error) {
   143  	ignoreConfig, err := parseIgnoreConfig(errCfg.Ignore)
   144  	if err != nil {
   145  		return nil, fmt.Errorf("failed to parse 'ignore' directive: %w", err)
   146  	}
   147  
   148  	checker := errcheck.Checker{
   149  		Exclusions: errcheck.Exclusions{
   150  			BlankAssignments:       !errCfg.CheckAssignToBlank,
   151  			TypeAssertions:         !errCfg.CheckTypeAssertions,
   152  			SymbolRegexpsByPackage: map[string]*regexp.Regexp{},
   153  		},
   154  	}
   155  
   156  	if !errCfg.DisableDefaultExclusions {
   157  		checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, errcheck.DefaultExcludedSymbols...)
   158  	}
   159  
   160  	for pkg, re := range ignoreConfig {
   161  		checker.Exclusions.SymbolRegexpsByPackage[pkg] = re
   162  	}
   163  
   164  	if errCfg.Exclude != "" {
   165  		exclude, err := readExcludeFile(errCfg.Exclude)
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  
   170  		checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, exclude...)
   171  	}
   172  
   173  	checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, errCfg.ExcludeFunctions...)
   174  
   175  	return &checker, nil
   176  }
   177  
   178  func getFirstPathArg() string {
   179  	args := os.Args
   180  
   181  	// skip all args ([golangci-lint, run/linters]) before files/dirs list
   182  	for len(args) != 0 {
   183  		if args[0] == "run" {
   184  			args = args[1:]
   185  			break
   186  		}
   187  
   188  		args = args[1:]
   189  	}
   190  
   191  	// find first file/dir arg
   192  	firstArg := "./..."
   193  	for _, arg := range args {
   194  		if !strings.HasPrefix(arg, "-") {
   195  			firstArg = arg
   196  			break
   197  		}
   198  	}
   199  
   200  	return firstArg
   201  }
   202  
   203  func setupConfigFileSearch(name string) []string {
   204  	if strings.HasPrefix(name, "~") {
   205  		if u, err := user.Current(); err == nil {
   206  			name = strings.Replace(name, "~", u.HomeDir, 1)
   207  		}
   208  	}
   209  
   210  	if filepath.IsAbs(name) {
   211  		return []string{name}
   212  	}
   213  
   214  	firstArg := getFirstPathArg()
   215  
   216  	absStartPath, err := filepath.Abs(firstArg)
   217  	if err != nil {
   218  		absStartPath = filepath.Clean(firstArg)
   219  	}
   220  
   221  	// start from it
   222  	var curDir string
   223  	if fsutils.IsDir(absStartPath) {
   224  		curDir = absStartPath
   225  	} else {
   226  		curDir = filepath.Dir(absStartPath)
   227  	}
   228  
   229  	// find all dirs from it up to the root
   230  	configSearchPaths := []string{filepath.Join(".", name)}
   231  	for {
   232  		configSearchPaths = append(configSearchPaths, filepath.Join(curDir, name))
   233  		newCurDir := filepath.Dir(curDir)
   234  		if curDir == newCurDir || newCurDir == "" {
   235  			break
   236  		}
   237  		curDir = newCurDir
   238  	}
   239  
   240  	return configSearchPaths
   241  }
   242  
   243  func readExcludeFile(name string) ([]string, error) {
   244  	var err error
   245  	var fh *os.File
   246  
   247  	for _, path := range setupConfigFileSearch(name) {
   248  		if fh, err = os.Open(path); err == nil {
   249  			break
   250  		}
   251  	}
   252  
   253  	if fh == nil {
   254  		return nil, fmt.Errorf("failed reading exclude file: %s: %w", name, err)
   255  	}
   256  
   257  	scanner := bufio.NewScanner(fh)
   258  
   259  	var excludes []string
   260  	for scanner.Scan() {
   261  		excludes = append(excludes, scanner.Text())
   262  	}
   263  
   264  	if err := scanner.Err(); err != nil {
   265  		return nil, fmt.Errorf("failed scanning file: %s: %w", name, err)
   266  	}
   267  
   268  	return excludes, nil
   269  }