forked from TrueCloudLab/rclone
290 lines
6.7 KiB
Go
290 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/shlex"
|
|
kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
|
|
)
|
|
|
|
type Vars map[string]string
|
|
|
|
func (v Vars) Copy() Vars {
|
|
out := Vars{}
|
|
for k, v := range v {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (v Vars) Replace(s string) string {
|
|
for k, v := range v {
|
|
prefix := regexp.MustCompile(fmt.Sprintf("{%s=([^}]*)}", k))
|
|
if v != "" {
|
|
s = prefix.ReplaceAllString(s, "$1")
|
|
} else {
|
|
s = prefix.ReplaceAllString(s, "")
|
|
}
|
|
s = strings.Replace(s, fmt.Sprintf("{%s}", k), v, -1)
|
|
}
|
|
return s
|
|
}
|
|
|
|
type linterState struct {
|
|
*Linter
|
|
issues chan *Issue
|
|
vars Vars
|
|
exclude *regexp.Regexp
|
|
include *regexp.Regexp
|
|
deadline <-chan time.Time
|
|
}
|
|
|
|
func (l *linterState) Partitions(paths []string) ([][]string, error) {
|
|
cmdArgs, err := parseCommand(l.command())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts, err := l.Linter.PartitionStrategy(cmdArgs, paths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parts, nil
|
|
}
|
|
|
|
func (l *linterState) command() string {
|
|
return l.vars.Replace(l.Command)
|
|
}
|
|
|
|
func runLinters(linters map[string]*Linter, paths []string, concurrency int, exclude, include *regexp.Regexp) (chan *Issue, chan error) {
|
|
errch := make(chan error, len(linters))
|
|
concurrencych := make(chan bool, concurrency)
|
|
incomingIssues := make(chan *Issue, 1000000)
|
|
|
|
directiveParser := newDirectiveParser()
|
|
if config.WarnUnmatchedDirective {
|
|
directiveParser.LoadFiles(paths)
|
|
}
|
|
|
|
processedIssues := maybeSortIssues(filterIssuesViaDirectives(
|
|
directiveParser, maybeAggregateIssues(incomingIssues)))
|
|
|
|
vars := Vars{
|
|
"duplthreshold": fmt.Sprintf("%d", config.DuplThreshold),
|
|
"mincyclo": fmt.Sprintf("%d", config.Cyclo),
|
|
"maxlinelength": fmt.Sprintf("%d", config.LineLength),
|
|
"misspelllocale": fmt.Sprintf("%s", config.MisspellLocale),
|
|
"min_confidence": fmt.Sprintf("%f", config.MinConfidence),
|
|
"min_occurrences": fmt.Sprintf("%d", config.MinOccurrences),
|
|
"min_const_length": fmt.Sprintf("%d", config.MinConstLength),
|
|
"tests": "",
|
|
"not_tests": "true",
|
|
}
|
|
if config.Test {
|
|
vars["tests"] = "true"
|
|
vars["not_tests"] = ""
|
|
}
|
|
|
|
wg := &sync.WaitGroup{}
|
|
id := 1
|
|
for _, linter := range linters {
|
|
deadline := time.After(config.Deadline.Duration())
|
|
state := &linterState{
|
|
Linter: linter,
|
|
issues: incomingIssues,
|
|
vars: vars,
|
|
exclude: exclude,
|
|
include: include,
|
|
deadline: deadline,
|
|
}
|
|
|
|
partitions, err := state.Partitions(paths)
|
|
if err != nil {
|
|
errch <- err
|
|
continue
|
|
}
|
|
for _, args := range partitions {
|
|
wg.Add(1)
|
|
concurrencych <- true
|
|
// Call the goroutine with a copy of the args array so that the
|
|
// contents of the array are not modified by the next iteration of
|
|
// the above for loop
|
|
go func(id int, args []string) {
|
|
err := executeLinter(id, state, args)
|
|
if err != nil {
|
|
errch <- err
|
|
}
|
|
<-concurrencych
|
|
wg.Done()
|
|
}(id, args)
|
|
id++
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(incomingIssues)
|
|
close(errch)
|
|
}()
|
|
return processedIssues, errch
|
|
}
|
|
|
|
func executeLinter(id int, state *linterState, args []string) error {
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("missing linter command")
|
|
}
|
|
|
|
start := time.Now()
|
|
dbg := namespacedDebug(fmt.Sprintf("[%s.%d]: ", state.Name, id))
|
|
dbg("executing %s", strings.Join(args, " "))
|
|
buf := bytes.NewBuffer(nil)
|
|
command := args[0]
|
|
cmd := exec.Command(command, args[1:]...) // nolint: gosec
|
|
cmd.Stdout = buf
|
|
cmd.Stderr = buf
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute linter %s: %s", command, err)
|
|
}
|
|
|
|
done := make(chan bool)
|
|
go func() {
|
|
err = cmd.Wait()
|
|
done <- true
|
|
}()
|
|
|
|
// Wait for process to complete or deadline to expire.
|
|
select {
|
|
case <-done:
|
|
|
|
case <-state.deadline:
|
|
err = fmt.Errorf("deadline exceeded by linter %s (try increasing --deadline)",
|
|
state.Name)
|
|
kerr := cmd.Process.Kill()
|
|
if kerr != nil {
|
|
warning("failed to kill %s: %s", state.Name, kerr)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err != nil {
|
|
dbg("warning: %s returned %s: %s", command, err, buf.String())
|
|
}
|
|
|
|
processOutput(dbg, state, buf.Bytes())
|
|
elapsed := time.Since(start)
|
|
dbg("%s linter took %s", state.Name, elapsed)
|
|
return nil
|
|
}
|
|
|
|
func parseCommand(command string) ([]string, error) {
|
|
args, err := shlex.Split(command)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(args) == 0 {
|
|
return nil, fmt.Errorf("invalid command %q", command)
|
|
}
|
|
exe, err := exec.LookPath(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append([]string{exe}, args[1:]...), nil
|
|
}
|
|
|
|
// nolint: gocyclo
|
|
func processOutput(dbg debugFunction, state *linterState, out []byte) {
|
|
re := state.regex
|
|
all := re.FindAllSubmatchIndex(out, -1)
|
|
dbg("%s hits %d: %s", state.Name, len(all), state.Pattern)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
warning("failed to get working directory %s", err)
|
|
}
|
|
|
|
// Create a local copy of vars so they can be modified by the linter output
|
|
vars := state.vars.Copy()
|
|
|
|
for _, indices := range all {
|
|
group := [][]byte{}
|
|
for i := 0; i < len(indices); i += 2 {
|
|
var fragment []byte
|
|
if indices[i] != -1 {
|
|
fragment = out[indices[i]:indices[i+1]]
|
|
}
|
|
group = append(group, fragment)
|
|
}
|
|
|
|
issue, err := NewIssue(state.Linter.Name, config.formatTemplate)
|
|
kingpin.FatalIfError(err, "Invalid output format")
|
|
|
|
for i, name := range re.SubexpNames() {
|
|
if group[i] == nil {
|
|
continue
|
|
}
|
|
part := string(group[i])
|
|
if name != "" {
|
|
vars[name] = part
|
|
}
|
|
switch name {
|
|
case "path":
|
|
issue.Path, err = newIssuePathFromAbsPath(cwd, part)
|
|
if err != nil {
|
|
warning("failed to make %s a relative path: %s", part, err)
|
|
}
|
|
case "line":
|
|
n, err := strconv.ParseInt(part, 10, 32)
|
|
kingpin.FatalIfError(err, "line matched invalid integer")
|
|
issue.Line = int(n)
|
|
|
|
case "col":
|
|
n, err := strconv.ParseInt(part, 10, 32)
|
|
kingpin.FatalIfError(err, "col matched invalid integer")
|
|
issue.Col = int(n)
|
|
|
|
case "message":
|
|
issue.Message = part
|
|
|
|
case "":
|
|
}
|
|
}
|
|
// TODO: set messageOveride and severity on the Linter instead of reading
|
|
// them directly from the static config
|
|
if m, ok := config.MessageOverride[state.Name]; ok {
|
|
issue.Message = vars.Replace(m)
|
|
}
|
|
if sev, ok := config.Severity[state.Name]; ok {
|
|
issue.Severity = Severity(sev)
|
|
}
|
|
if state.exclude != nil && state.exclude.MatchString(issue.String()) {
|
|
continue
|
|
}
|
|
if state.include != nil && !state.include.MatchString(issue.String()) {
|
|
continue
|
|
}
|
|
state.issues <- issue
|
|
}
|
|
}
|
|
|
|
func maybeSortIssues(issues chan *Issue) chan *Issue {
|
|
if reflect.DeepEqual([]string{"none"}, config.Sort) {
|
|
return issues
|
|
}
|
|
return SortIssueChan(issues, config.Sort)
|
|
}
|
|
|
|
func maybeAggregateIssues(issues chan *Issue) chan *Issue {
|
|
if !config.Aggregate {
|
|
return issues
|
|
}
|
|
return AggregateIssueChan(issues)
|
|
}
|