200 lines
4.7 KiB
Go
200 lines
4.7 KiB
Go
package golinters
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"text/template"
|
|
|
|
"github.com/golangci/golangci-lint/pkg/result"
|
|
"github.com/golangci/golangci-shared/pkg/analytics"
|
|
"github.com/golangci/golangci-shared/pkg/executors"
|
|
)
|
|
|
|
type linterConfig struct {
|
|
messageTemplate *template.Template
|
|
pattern *regexp.Regexp
|
|
excludeByMessagePattern *regexp.Regexp
|
|
args []string
|
|
issuesFoundExitCode int
|
|
}
|
|
|
|
func newLinterConfig(messageTemplate, pattern, excludeByMessagePattern string, args ...string) *linterConfig {
|
|
if messageTemplate == "" {
|
|
messageTemplate = "{{.message}}"
|
|
}
|
|
|
|
var excludeByMessagePatternRe *regexp.Regexp
|
|
if excludeByMessagePattern != "" {
|
|
excludeByMessagePatternRe = regexp.MustCompile(excludeByMessagePattern)
|
|
}
|
|
|
|
return &linterConfig{
|
|
messageTemplate: template.Must(template.New("message").Parse(messageTemplate)),
|
|
pattern: regexp.MustCompile(pattern),
|
|
excludeByMessagePattern: excludeByMessagePatternRe,
|
|
args: args,
|
|
issuesFoundExitCode: 1,
|
|
}
|
|
}
|
|
|
|
type linter struct {
|
|
name string
|
|
|
|
linterConfig
|
|
}
|
|
|
|
func newLinter(name string, cfg *linterConfig) *linter {
|
|
return &linter{
|
|
name: name,
|
|
linterConfig: *cfg,
|
|
}
|
|
}
|
|
|
|
func (lint linter) Name() string {
|
|
return lint.name
|
|
}
|
|
|
|
func (lint linter) doesExitCodeMeansIssuesWereFound(err error) bool {
|
|
ee, ok := err.(*exec.ExitError)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
status, ok := ee.Sys().(syscall.WaitStatus)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
exitCode := status.ExitStatus()
|
|
return exitCode == lint.issuesFoundExitCode
|
|
}
|
|
|
|
func getOutTail(out string, linesCount int) string {
|
|
lines := strings.Split(out, "\n")
|
|
if len(lines) <= linesCount {
|
|
return out
|
|
}
|
|
|
|
return strings.Join(lines[len(lines)-linesCount:], "\n")
|
|
}
|
|
|
|
func (lint linter) Run(ctx context.Context, exec executors.Executor) (*result.Result, error) {
|
|
paths, err := getPathsForGoProject(exec.WorkDir())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't get files to analyze: %s", err)
|
|
}
|
|
|
|
retIssues := []result.Issue{}
|
|
|
|
const maxDirsPerRun = 100 // run one linter multiple times with groups of dirs: limit memory usage in the cost of higher CPU usage
|
|
|
|
for len(paths.dirs) != 0 {
|
|
args := append([]string{}, lint.args...)
|
|
|
|
dirsCount := len(paths.dirs)
|
|
if dirsCount > maxDirsPerRun {
|
|
dirsCount = maxDirsPerRun
|
|
}
|
|
dirs := paths.dirs[:dirsCount]
|
|
args = append(args, dirs...)
|
|
|
|
out, err := exec.Run(ctx, lint.name, args...)
|
|
if err != nil && !lint.doesExitCodeMeansIssuesWereFound(err) {
|
|
out = getOutTail(out, 10)
|
|
return nil, fmt.Errorf("can't run linter %s with args %v: %s, %s", lint.name, lint.args, err, out)
|
|
}
|
|
|
|
issues := lint.parseLinterOut(out)
|
|
retIssues = append(retIssues, issues...)
|
|
|
|
paths.dirs = paths.dirs[dirsCount:]
|
|
}
|
|
|
|
return &result.Result{
|
|
Issues: retIssues,
|
|
}, nil
|
|
}
|
|
|
|
type regexpVars map[string]string
|
|
|
|
func buildMatchedRegexpVars(match []string, pattern *regexp.Regexp) regexpVars {
|
|
result := regexpVars{}
|
|
for i, name := range pattern.SubexpNames() {
|
|
if i != 0 && name != "" {
|
|
result[name] = match[i]
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (lint linter) parseLinterOutLine(line string) (regexpVars, error) {
|
|
match := lint.pattern.FindStringSubmatch(line)
|
|
if match == nil {
|
|
return nil, fmt.Errorf("can't match line %q against regexp", line)
|
|
}
|
|
|
|
return buildMatchedRegexpVars(match, lint.pattern), nil
|
|
}
|
|
|
|
func (lint linter) makeIssue(vars regexpVars) (*result.Issue, error) {
|
|
var messageBuffer bytes.Buffer
|
|
err := lint.messageTemplate.Execute(&messageBuffer, vars)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't execute message template: %s", err)
|
|
}
|
|
|
|
if vars["path"] == "" {
|
|
return nil, fmt.Errorf("no path in vars %+v", vars)
|
|
}
|
|
|
|
var line int
|
|
if vars["line"] != "" {
|
|
line, err = strconv.Atoi(vars["line"])
|
|
if err != nil {
|
|
analytics.Log(context.TODO()).Warnf("Can't parse line %q: %s", vars["line"], err)
|
|
}
|
|
}
|
|
|
|
return &result.Issue{
|
|
FromLinter: lint.name,
|
|
File: vars["path"],
|
|
LineNumber: line,
|
|
Text: messageBuffer.String(),
|
|
}, nil
|
|
}
|
|
|
|
func (lint linter) parseLinterOut(out string) []result.Issue {
|
|
issues := []result.Issue{}
|
|
scanner := bufio.NewScanner(strings.NewReader(out))
|
|
for scanner.Scan() {
|
|
vars, err := lint.parseLinterOutLine(scanner.Text())
|
|
if err != nil {
|
|
analytics.Log(context.TODO()).Warnf("Can't parse linter out line: %s", err)
|
|
continue
|
|
}
|
|
|
|
message := vars["message"]
|
|
ex := lint.excludeByMessagePattern
|
|
if message != "" && ex != nil && ex.MatchString(message) {
|
|
continue
|
|
}
|
|
|
|
issue, err := lint.makeIssue(vars)
|
|
if err != nil {
|
|
analytics.Log(context.TODO()).Warnf("Can't make issue: %s", err)
|
|
continue
|
|
}
|
|
|
|
issues = append(issues, *issue)
|
|
}
|
|
|
|
return issues
|
|
}
|