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