github.com/bhcleek/gometalinter@v2.0.6-0.20180316043659-b25b44d18fb6+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 }