mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-13 12:06:29 +00:00
252 lines
5.6 KiB
Go
252 lines
5.6 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"
|
||
|
)
|
||
|
|
||
|
func NewErrcheck() *goanalysis.Linter {
|
||
|
const linterName = "errcheck"
|
||
|
|
||
|
var mu sync.Mutex
|
||
|
var res []goanalysis.Issue
|
||
|
|
||
|
analyzer := &analysis.Analyzer{
|
||
|
Name: linterName,
|
||
|
Doc: goanalysis.TheOnlyanalyzerDoc,
|
||
|
}
|
||
|
|
||
|
return goanalysis.NewLinter(
|
||
|
linterName,
|
||
|
"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(&lintCtx.Settings().Errcheck)
|
||
|
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) {
|
||
|
pkg := &packages.Package{
|
||
|
Fset: pass.Fset,
|
||
|
Syntax: pass.Files,
|
||
|
Types: pass.Pkg,
|
||
|
TypesInfo: pass.TypesInfo,
|
||
|
}
|
||
|
|
||
|
errcheckIssues := checker.CheckPackage(pkg).Unique()
|
||
|
if len(errcheckIssues.UncheckedErrors) == 0 {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
issues := make([]goanalysis.Issue, len(errcheckIssues.UncheckedErrors))
|
||
|
for i, err := range errcheckIssues.UncheckedErrors {
|
||
|
var text string
|
||
|
if err.FuncName != "" {
|
||
|
text = fmt.Sprintf(
|
||
|
"Error return value of %s is not checked",
|
||
|
formatCode(err.SelectorName, lintCtx.Cfg),
|
||
|
)
|
||
|
} else {
|
||
|
text = "Error return value is not checked"
|
||
|
}
|
||
|
|
||
|
issues[i] = goanalysis.NewIssue(
|
||
|
&result.Issue{
|
||
|
FromLinter: linterName,
|
||
|
Text: text,
|
||
|
Pos: err.Pos,
|
||
|
},
|
||
|
pass,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
mu.Lock()
|
||
|
res = append(res, issues...)
|
||
|
mu.Unlock()
|
||
|
|
||
|
return nil, nil
|
||
|
}
|
||
|
}).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
|
||
|
return res
|
||
|
}).WithLoadMode(goanalysis.LoadModeTypesInfo)
|
||
|
}
|
||
|
|
||
|
// 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{},
|
||
|
Symbols: append([]string{}, 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
|
||
|
}
|