gopkg.in/alecthomas/gometalinter.v3@v3.0.0/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 }