github.com/songrgg/gometalinter@v2.0.6-0.20180425200507-2cbec6168e84+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: gas
   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 bool)
   159  	go func() {
   160  		err = cmd.Wait()
   161  		done <- true
   162  	}()
   163  
   164  	// Wait for process to complete or deadline to expire.
   165  	select {
   166  	case <-done:
   167  
   168  	case <-state.deadline:
   169  		err = fmt.Errorf("deadline exceeded by linter %s (try increasing --deadline)",
   170  			state.Name)
   171  		kerr := cmd.Process.Kill()
   172  		if kerr != nil {
   173  			warning("failed to kill %s: %s", state.Name, kerr)
   174  		}
   175  		return err
   176  	}
   177  
   178  	if err != nil {
   179  		dbg("warning: %s returned %s: %s", command, err, buf.String())
   180  	}
   181  
   182  	processOutput(dbg, state, buf.Bytes())
   183  	elapsed := time.Since(start)
   184  	dbg("%s linter took %s", state.Name, elapsed)
   185  	return nil
   186  }
   187  
   188  func parseCommand(command string) ([]string, error) {
   189  	args, err := shlex.Split(command)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	if len(args) == 0 {
   194  		return nil, fmt.Errorf("invalid command %q", command)
   195  	}
   196  	exe, err := exec.LookPath(args[0])
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	return append([]string{exe}, args[1:]...), nil
   201  }
   202  
   203  // nolint: gocyclo
   204  func processOutput(dbg debugFunction, state *linterState, out []byte) {
   205  	re := state.regex
   206  	all := re.FindAllSubmatchIndex(out, -1)
   207  	dbg("%s hits %d: %s", state.Name, len(all), state.Pattern)
   208  
   209  	cwd, err := os.Getwd()
   210  	if err != nil {
   211  		warning("failed to get working directory %s", err)
   212  	}
   213  
   214  	// Create a local copy of vars so they can be modified by the linter output
   215  	vars := state.vars.Copy()
   216  
   217  	for _, indices := range all {
   218  		group := [][]byte{}
   219  		for i := 0; i < len(indices); i += 2 {
   220  			var fragment []byte
   221  			if indices[i] != -1 {
   222  				fragment = out[indices[i]:indices[i+1]]
   223  			}
   224  			group = append(group, fragment)
   225  		}
   226  
   227  		issue, err := NewIssue(state.Linter.Name, config.formatTemplate)
   228  		kingpin.FatalIfError(err, "Invalid output format")
   229  
   230  		for i, name := range re.SubexpNames() {
   231  			if group[i] == nil {
   232  				continue
   233  			}
   234  			part := string(group[i])
   235  			if name != "" {
   236  				vars[name] = part
   237  			}
   238  			switch name {
   239  			case "path":
   240  				issue.Path, err = newIssuePathFromAbsPath(cwd, part)
   241  				if err != nil {
   242  					warning("failed to make %s a relative path: %s", part, err)
   243  				}
   244  			case "line":
   245  				n, err := strconv.ParseInt(part, 10, 32)
   246  				kingpin.FatalIfError(err, "line matched invalid integer")
   247  				issue.Line = int(n)
   248  
   249  			case "col":
   250  				n, err := strconv.ParseInt(part, 10, 32)
   251  				kingpin.FatalIfError(err, "col matched invalid integer")
   252  				issue.Col = int(n)
   253  
   254  			case "message":
   255  				issue.Message = part
   256  
   257  			case "":
   258  			}
   259  		}
   260  		// TODO: set messageOveride and severity on the Linter instead of reading
   261  		// them directly from the static config
   262  		if m, ok := config.MessageOverride[state.Name]; ok {
   263  			issue.Message = vars.Replace(m)
   264  		}
   265  		if sev, ok := config.Severity[state.Name]; ok {
   266  			issue.Severity = Severity(sev)
   267  		}
   268  		if state.exclude != nil && state.exclude.MatchString(issue.String()) {
   269  			continue
   270  		}
   271  		if state.include != nil && !state.include.MatchString(issue.String()) {
   272  			continue
   273  		}
   274  		state.issues <- issue
   275  	}
   276  }
   277  
   278  func maybeSortIssues(issues chan *Issue) chan *Issue {
   279  	if reflect.DeepEqual([]string{"none"}, config.Sort) {
   280  		return issues
   281  	}
   282  	return SortIssueChan(issues, config.Sort)
   283  }
   284  
   285  func maybeAggregateIssues(issues chan *Issue) chan *Issue {
   286  	if !config.Aggregate {
   287  		return issues
   288  	}
   289  	return AggregateIssueChan(issues)
   290  }