github.com/asbovelw/gometalinter@v3.0.1-0.20200515093514-37c0f45b17f4+incompatible/execute.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"reflect"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/google/shlex"
    16  	kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
    17  )
    18  
    19  type Vars map[string]string
    20  
    21  func (v Vars) Copy() Vars {
    22  	out := Vars{}
    23  	for k, v := range v {
    24  		out[k] = v
    25  	}
    26  	return out
    27  }
    28  
    29  func (v Vars) Replace(s string) string {
    30  	for k, v := range v {
    31  		prefix := regexp.MustCompile(fmt.Sprintf("{%s=([^}]*)}", k))
    32  		if v != "" {
    33  			s = prefix.ReplaceAllString(s, "$1")
    34  		} else {
    35  			s = prefix.ReplaceAllString(s, "")
    36  		}
    37  		s = strings.Replace(s, fmt.Sprintf("{%s}", k), v, -1)
    38  	}
    39  	return s
    40  }
    41  
    42  type linterState struct {
    43  	*Linter
    44  	issues   chan *Issue
    45  	vars     Vars
    46  	exclude  *regexp.Regexp
    47  	include  *regexp.Regexp
    48  	deadline <-chan time.Time
    49  }
    50  
    51  func (l *linterState) Partitions(paths []string) ([][]string, error) {
    52  	cmdArgs, err := parseCommand(l.command())
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  	parts, err := l.Linter.PartitionStrategy(cmdArgs, paths)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  	return parts, nil
    61  }
    62  
    63  func (l *linterState) command() string {
    64  	return l.vars.Replace(l.Command)
    65  }
    66  
    67  func runLinters(linters map[string]*Linter, paths []string, concurrency int, exclude, include *regexp.Regexp) (chan *Issue, chan error) {
    68  	errch := make(chan error, len(linters))
    69  	concurrencych := make(chan bool, concurrency)
    70  	incomingIssues := make(chan *Issue, 1000000)
    71  
    72  	directiveParser := newDirectiveParser()
    73  	if config.WarnUnmatchedDirective {
    74  		directiveParser.LoadFiles(paths)
    75  	}
    76  
    77  	processedIssues := maybeSortIssues(filterIssuesViaDirectives(
    78  		directiveParser, maybeAggregateIssues(incomingIssues)))
    79  
    80  	vars := Vars{
    81  		"duplthreshold":    fmt.Sprintf("%d", config.DuplThreshold),
    82  		"mincyclo":         fmt.Sprintf("%d", config.Cyclo),
    83  		"maxlinelength":    fmt.Sprintf("%d", config.LineLength),
    84  		"misspelllocale":   fmt.Sprintf("%s", config.MisspellLocale),
    85  		"min_confidence":   fmt.Sprintf("%f", config.MinConfidence),
    86  		"min_occurrences":  fmt.Sprintf("%d", config.MinOccurrences),
    87  		"min_const_length": fmt.Sprintf("%d", config.MinConstLength),
    88  		"tests":            "",
    89  		"not_tests":        "true",
    90  	}
    91  	if config.Test {
    92  		vars["tests"] = "true"
    93  		vars["not_tests"] = ""
    94  	}
    95  
    96  	wg := &sync.WaitGroup{}
    97  	id := 1
    98  	for _, linter := range linters {
    99  		deadline := time.After(config.Deadline.Duration())
   100  		state := &linterState{
   101  			Linter:   linter,
   102  			issues:   incomingIssues,
   103  			vars:     vars,
   104  			exclude:  exclude,
   105  			include:  include,
   106  			deadline: deadline,
   107  		}
   108  
   109  		partitions, err := state.Partitions(paths)
   110  		if err != nil {
   111  			errch <- err
   112  			continue
   113  		}
   114  		for _, args := range partitions {
   115  			wg.Add(1)
   116  			concurrencych <- true
   117  			// Call the goroutine with a copy of the args array so that the
   118  			// contents of the array are not modified by the next iteration of
   119  			// the above for loop
   120  			go func(id int, args []string) {
   121  				err := executeLinter(id, state, args)
   122  				if err != nil {
   123  					errch <- err
   124  				}
   125  				<-concurrencych
   126  				wg.Done()
   127  			}(id, args)
   128  			id++
   129  		}
   130  	}
   131  
   132  	go func() {
   133  		wg.Wait()
   134  		close(incomingIssues)
   135  		close(errch)
   136  	}()
   137  	return processedIssues, errch
   138  }
   139  
   140  func executeLinter(id int, state *linterState, args []string) error {
   141  	if len(args) == 0 {
   142  		return fmt.Errorf("missing linter command")
   143  	}
   144  
   145  	start := time.Now()
   146  	dbg := namespacedDebug(fmt.Sprintf("[%s.%d]: ", state.Name, id))
   147  	dbg("executing %s", strings.Join(args, " "))
   148  	buf := bytes.NewBuffer(nil)
   149  	command := args[0]
   150  	cmd := exec.Command(command, args[1:]...) // nolint: gosec
   151  	cmd.Stdout = buf
   152  	cmd.Stderr = buf
   153  	err := cmd.Start()
   154  	if err != nil {
   155  		return fmt.Errorf("failed to execute linter %s: %s", command, err)
   156  	}
   157  
   158  	done := make(chan error)
   159  	go func() {
   160  		done <- cmd.Wait()
   161  	}()
   162  
   163  	// Wait for process to complete or deadline to expire.
   164  	select {
   165  	case err = <-done:
   166  
   167  	case <-state.deadline:
   168  		err = fmt.Errorf("deadline exceeded by linter %s (try increasing --deadline)",
   169  			state.Name)
   170  		kerr := cmd.Process.Kill()
   171  		if kerr != nil {
   172  			warning("failed to kill %s: %s", state.Name, kerr)
   173  		}
   174  		return err
   175  	}
   176  
   177  	if err != nil {
   178  		dbg("warning: %s returned %s: %s", command, err, buf.String())
   179  	}
   180  
   181  	processOutput(dbg, state, buf.Bytes())
   182  	elapsed := time.Since(start)
   183  	dbg("%s linter took %s", state.Name, elapsed)
   184  	return nil
   185  }
   186  
   187  func parseCommand(command string) ([]string, error) {
   188  	args, err := shlex.Split(command)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	if len(args) == 0 {
   193  		return nil, fmt.Errorf("invalid command %q", command)
   194  	}
   195  	exe, err := exec.LookPath(args[0])
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	return append([]string{exe}, args[1:]...), nil
   200  }
   201  
   202  // nolint: gocyclo
   203  func processOutput(dbg debugFunction, state *linterState, out []byte) {
   204  	re := state.regex
   205  	all := re.FindAllSubmatchIndex(out, -1)
   206  	dbg("%s hits %d: %s", state.Name, len(all), state.Pattern)
   207  
   208  	cwd, err := os.Getwd()
   209  	if err != nil {
   210  		warning("failed to get working directory %s", err)
   211  	}
   212  
   213  	// Create a local copy of vars so they can be modified by the linter output
   214  	vars := state.vars.Copy()
   215  
   216  	for _, indices := range all {
   217  		group := [][]byte{}
   218  		for i := 0; i < len(indices); i += 2 {
   219  			var fragment []byte
   220  			if indices[i] != -1 {
   221  				fragment = out[indices[i]:indices[i+1]]
   222  			}
   223  			group = append(group, fragment)
   224  		}
   225  
   226  		issue, err := NewIssue(state.Linter.Name, config.formatTemplate)
   227  		kingpin.FatalIfError(err, "Invalid output format")
   228  
   229  		for i, name := range re.SubexpNames() {
   230  			if group[i] == nil {
   231  				continue
   232  			}
   233  			part := string(group[i])
   234  			if name != "" {
   235  				vars[name] = part
   236  			}
   237  			switch name {
   238  			case "path":
   239  				issue.Path, err = newIssuePathFromAbsPath(cwd, part)
   240  				if err != nil {
   241  					warning("failed to make %s a relative path: %s", part, err)
   242  				}
   243  			case "line":
   244  				n, err := strconv.ParseInt(part, 10, 32)
   245  				kingpin.FatalIfError(err, "line matched invalid integer")
   246  				issue.Line = int(n)
   247  
   248  			case "col":
   249  				n, err := strconv.ParseInt(part, 10, 32)
   250  				kingpin.FatalIfError(err, "col matched invalid integer")
   251  				issue.Col = int(n)
   252  
   253  			case "message":
   254  				issue.Message = part
   255  
   256  			case "":
   257  			}
   258  		}
   259  		// TODO: set messageOveride and severity on the Linter instead of reading
   260  		// them directly from the static config
   261  		if m, ok := config.MessageOverride[state.Name]; ok {
   262  			issue.Message = vars.Replace(m)
   263  		}
   264  		if sev, ok := config.Severity[state.Name]; ok {
   265  			issue.Severity = Severity(sev)
   266  		}
   267  		if state.exclude != nil && state.exclude.MatchString(issue.String()) {
   268  			continue
   269  		}
   270  		if state.include != nil && !state.include.MatchString(issue.String()) {
   271  			continue
   272  		}
   273  		state.issues <- issue
   274  	}
   275  }
   276  
   277  func maybeSortIssues(issues chan *Issue) chan *Issue {
   278  	if reflect.DeepEqual([]string{"none"}, config.Sort) {
   279  		return issues
   280  	}
   281  	return SortIssueChan(issues, config.Sort)
   282  }
   283  
   284  func maybeAggregateIssues(issues chan *Issue) chan *Issue {
   285  	if !config.Aggregate {
   286  		return issues
   287  	}
   288  	return AggregateIssueChan(issues)
   289  }