271 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			271 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package golinters
 | 
						|
 | 
						|
import (
 | 
						|
	"bufio"
 | 
						|
	"fmt"
 | 
						|
	"os"
 | 
						|
	"os/user"
 | 
						|
	"path/filepath"
 | 
						|
	"regexp"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
 | 
						|
	"github.com/kisielk/errcheck/errcheck"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"golang.org/x/tools/go/analysis"
 | 
						|
	"golang.org/x/tools/go/packages"
 | 
						|
 | 
						|
	"github.com/golangci/golangci-lint/pkg/config"
 | 
						|
	"github.com/golangci/golangci-lint/pkg/fsutils"
 | 
						|
	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
 | 
						|
	"github.com/golangci/golangci-lint/pkg/lint/linter"
 | 
						|
	"github.com/golangci/golangci-lint/pkg/result"
 | 
						|
)
 | 
						|
 | 
						|
const errcheckName = "errcheck"
 | 
						|
 | 
						|
func NewErrcheck(settings *config.ErrcheckSettings) *goanalysis.Linter {
 | 
						|
	var mu sync.Mutex
 | 
						|
	var resIssues []goanalysis.Issue
 | 
						|
 | 
						|
	analyzer := &analysis.Analyzer{
 | 
						|
		Name: errcheckName,
 | 
						|
		Doc:  goanalysis.TheOnlyanalyzerDoc,
 | 
						|
		Run:  goanalysis.DummyRun,
 | 
						|
	}
 | 
						|
 | 
						|
	return goanalysis.NewLinter(
 | 
						|
		errcheckName,
 | 
						|
		"Errcheck is a program for checking for unchecked errors "+
 | 
						|
			"in go programs. These unchecked errors can be critical bugs in some cases",
 | 
						|
		[]*analysis.Analyzer{analyzer},
 | 
						|
		nil,
 | 
						|
	).WithContextSetter(func(lintCtx *linter.Context) {
 | 
						|
		// copied from errcheck
 | 
						|
		checker, err := getChecker(settings)
 | 
						|
		if err != nil {
 | 
						|
			lintCtx.Log.Errorf("failed to get checker: %v", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		checker.Tags = lintCtx.Cfg.Run.BuildTags
 | 
						|
 | 
						|
		analyzer.Run = func(pass *analysis.Pass) (interface{}, error) {
 | 
						|
			issues := runErrCheck(lintCtx, pass, checker)
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
 | 
						|
			if len(issues) == 0 {
 | 
						|
				return nil, nil
 | 
						|
			}
 | 
						|
 | 
						|
			mu.Lock()
 | 
						|
			resIssues = append(resIssues, issues...)
 | 
						|
			mu.Unlock()
 | 
						|
 | 
						|
			return nil, nil
 | 
						|
		}
 | 
						|
	}).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
 | 
						|
		return resIssues
 | 
						|
	}).WithLoadMode(goanalysis.LoadModeTypesInfo)
 | 
						|
}
 | 
						|
 | 
						|
func runErrCheck(lintCtx *linter.Context, pass *analysis.Pass, checker *errcheck.Checker) []goanalysis.Issue {
 | 
						|
	pkg := &packages.Package{
 | 
						|
		Fset:      pass.Fset,
 | 
						|
		Syntax:    pass.Files,
 | 
						|
		Types:     pass.Pkg,
 | 
						|
		TypesInfo: pass.TypesInfo,
 | 
						|
	}
 | 
						|
 | 
						|
	lintIssues := checker.CheckPackage(pkg).Unique()
 | 
						|
	if len(lintIssues.UncheckedErrors) == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	issues := make([]goanalysis.Issue, len(lintIssues.UncheckedErrors))
 | 
						|
 | 
						|
	for i, err := range lintIssues.UncheckedErrors {
 | 
						|
		text := "Error return value is not checked"
 | 
						|
 | 
						|
		if err.FuncName != "" {
 | 
						|
			code := err.SelectorName
 | 
						|
			if err.SelectorName == "" {
 | 
						|
				code = err.FuncName
 | 
						|
			}
 | 
						|
 | 
						|
			text = fmt.Sprintf("Error return value of %s is not checked", formatCode(code, lintCtx.Cfg))
 | 
						|
		}
 | 
						|
 | 
						|
		issues[i] = goanalysis.NewIssue(
 | 
						|
			&result.Issue{
 | 
						|
				FromLinter: errcheckName,
 | 
						|
				Text:       text,
 | 
						|
				Pos:        err.Pos,
 | 
						|
			},
 | 
						|
			pass,
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	return issues
 | 
						|
}
 | 
						|
 | 
						|
// parseIgnoreConfig was taken from errcheck in order to keep the API identical.
 | 
						|
// https://github.com/kisielk/errcheck/blob/1787c4bee836470bf45018cfbc783650db3c6501/main.go#L25-L60
 | 
						|
func parseIgnoreConfig(s string) (map[string]*regexp.Regexp, error) {
 | 
						|
	if s == "" {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
 | 
						|
	cfg := map[string]*regexp.Regexp{}
 | 
						|
 | 
						|
	for _, pair := range strings.Split(s, ",") {
 | 
						|
		colonIndex := strings.Index(pair, ":")
 | 
						|
		var pkg, re string
 | 
						|
		if colonIndex == -1 {
 | 
						|
			pkg = ""
 | 
						|
			re = pair
 | 
						|
		} else {
 | 
						|
			pkg = pair[:colonIndex]
 | 
						|
			re = pair[colonIndex+1:]
 | 
						|
		}
 | 
						|
		regex, err := regexp.Compile(re)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		cfg[pkg] = regex
 | 
						|
	}
 | 
						|
 | 
						|
	return cfg, nil
 | 
						|
}
 | 
						|
 | 
						|
func getChecker(errCfg *config.ErrcheckSettings) (*errcheck.Checker, error) {
 | 
						|
	ignoreConfig, err := parseIgnoreConfig(errCfg.Ignore)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errors.Wrap(err, "failed to parse 'ignore' directive")
 | 
						|
	}
 | 
						|
 | 
						|
	checker := errcheck.Checker{
 | 
						|
		Exclusions: errcheck.Exclusions{
 | 
						|
			BlankAssignments:       !errCfg.CheckAssignToBlank,
 | 
						|
			TypeAssertions:         !errCfg.CheckTypeAssertions,
 | 
						|
			SymbolRegexpsByPackage: map[string]*regexp.Regexp{},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	if !errCfg.DisableDefaultExclusions {
 | 
						|
		checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, errcheck.DefaultExcludedSymbols...)
 | 
						|
	}
 | 
						|
 | 
						|
	for pkg, re := range ignoreConfig {
 | 
						|
		checker.Exclusions.SymbolRegexpsByPackage[pkg] = re
 | 
						|
	}
 | 
						|
 | 
						|
	if errCfg.Exclude != "" {
 | 
						|
		exclude, err := readExcludeFile(errCfg.Exclude)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, exclude...)
 | 
						|
	}
 | 
						|
 | 
						|
	checker.Exclusions.Symbols = append(checker.Exclusions.Symbols, errCfg.ExcludeFunctions...)
 | 
						|
 | 
						|
	return &checker, nil
 | 
						|
}
 | 
						|
 | 
						|
func getFirstPathArg() string {
 | 
						|
	args := os.Args
 | 
						|
 | 
						|
	// skip all args ([golangci-lint, run/linters]) before files/dirs list
 | 
						|
	for len(args) != 0 {
 | 
						|
		if args[0] == "run" {
 | 
						|
			args = args[1:]
 | 
						|
			break
 | 
						|
		}
 | 
						|
 | 
						|
		args = args[1:]
 | 
						|
	}
 | 
						|
 | 
						|
	// find first file/dir arg
 | 
						|
	firstArg := "./..."
 | 
						|
	for _, arg := range args {
 | 
						|
		if !strings.HasPrefix(arg, "-") {
 | 
						|
			firstArg = arg
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return firstArg
 | 
						|
}
 | 
						|
 | 
						|
func setupConfigFileSearch(name string) []string {
 | 
						|
	if strings.HasPrefix(name, "~") {
 | 
						|
		if u, err := user.Current(); err == nil {
 | 
						|
			name = strings.Replace(name, "~", u.HomeDir, 1)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if filepath.IsAbs(name) {
 | 
						|
		return []string{name}
 | 
						|
	}
 | 
						|
 | 
						|
	firstArg := getFirstPathArg()
 | 
						|
 | 
						|
	absStartPath, err := filepath.Abs(firstArg)
 | 
						|
	if err != nil {
 | 
						|
		absStartPath = filepath.Clean(firstArg)
 | 
						|
	}
 | 
						|
 | 
						|
	// start from it
 | 
						|
	var curDir string
 | 
						|
	if fsutils.IsDir(absStartPath) {
 | 
						|
		curDir = absStartPath
 | 
						|
	} else {
 | 
						|
		curDir = filepath.Dir(absStartPath)
 | 
						|
	}
 | 
						|
 | 
						|
	// find all dirs from it up to the root
 | 
						|
	configSearchPaths := []string{filepath.Join(".", name)}
 | 
						|
	for {
 | 
						|
		configSearchPaths = append(configSearchPaths, filepath.Join(curDir, name))
 | 
						|
		newCurDir := filepath.Dir(curDir)
 | 
						|
		if curDir == newCurDir || newCurDir == "" {
 | 
						|
			break
 | 
						|
		}
 | 
						|
		curDir = newCurDir
 | 
						|
	}
 | 
						|
 | 
						|
	return configSearchPaths
 | 
						|
}
 | 
						|
 | 
						|
func readExcludeFile(name string) ([]string, error) {
 | 
						|
	var err error
 | 
						|
	var fh *os.File
 | 
						|
 | 
						|
	for _, path := range setupConfigFileSearch(name) {
 | 
						|
		if fh, err = os.Open(path); err == nil {
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if fh == nil {
 | 
						|
		return nil, errors.Wrapf(err, "failed reading exclude file: %s", name)
 | 
						|
	}
 | 
						|
 | 
						|
	scanner := bufio.NewScanner(fh)
 | 
						|
 | 
						|
	var excludes []string
 | 
						|
	for scanner.Scan() {
 | 
						|
		excludes = append(excludes, scanner.Text())
 | 
						|
	}
 | 
						|
 | 
						|
	if err := scanner.Err(); err != nil {
 | 
						|
		return nil, errors.Wrapf(err, "failed scanning file: %s", name)
 | 
						|
	}
 | 
						|
 | 
						|
	return excludes, nil
 | 
						|
}
 |