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