github.com/golangci/go-tools@v0.0.0-20190318060251-af6baa5dc196/lint/lintutil/util.go (about) 1 // Copyright (c) 2013 The Go Authors. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file or at 5 // https://developers.google.com/open-source/licenses/bsd. 6 7 // Package lintutil provides helpers for writing linter command lines. 8 package lintutil // import "github.com/golangci/go-tools/lint/lintutil" 9 10 import ( 11 "errors" 12 "flag" 13 "fmt" 14 "go/build" 15 "go/token" 16 "log" 17 "os" 18 "regexp" 19 "runtime" 20 "runtime/pprof" 21 "strconv" 22 "strings" 23 "time" 24 25 "github.com/golangci/go-tools/config" 26 "github.com/golangci/go-tools/lint" 27 "github.com/golangci/go-tools/lint/lintutil/format" 28 "github.com/golangci/go-tools/version" 29 30 "golang.org/x/tools/go/packages" 31 ) 32 33 func usage(name string, flags *flag.FlagSet) func() { 34 return func() { 35 fmt.Fprintf(os.Stderr, "Usage of %s:\n", name) 36 fmt.Fprintf(os.Stderr, "\t%s [flags] # runs on package in current directory\n", name) 37 fmt.Fprintf(os.Stderr, "\t%s [flags] packages\n", name) 38 fmt.Fprintf(os.Stderr, "\t%s [flags] directory\n", name) 39 fmt.Fprintf(os.Stderr, "\t%s [flags] files... # must be a single package\n", name) 40 fmt.Fprintf(os.Stderr, "Flags:\n") 41 flags.PrintDefaults() 42 } 43 } 44 45 func parseIgnore(s string) ([]lint.Ignore, error) { 46 var out []lint.Ignore 47 if len(s) == 0 { 48 return nil, nil 49 } 50 for _, part := range strings.Fields(s) { 51 p := strings.Split(part, ":") 52 if len(p) != 2 { 53 return nil, errors.New("malformed ignore string") 54 } 55 path := p[0] 56 checks := strings.Split(p[1], ",") 57 out = append(out, &lint.GlobIgnore{Pattern: path, Checks: checks}) 58 } 59 return out, nil 60 } 61 62 type versionFlag int 63 64 func (v *versionFlag) String() string { 65 return fmt.Sprintf("1.%d", *v) 66 } 67 68 func (v *versionFlag) Set(s string) error { 69 if len(s) < 3 { 70 return errors.New("invalid Go version") 71 } 72 if s[0] != '1' { 73 return errors.New("invalid Go version") 74 } 75 if s[1] != '.' { 76 return errors.New("invalid Go version") 77 } 78 i, err := strconv.Atoi(s[2:]) 79 *v = versionFlag(i) 80 return err 81 } 82 83 func (v *versionFlag) Get() interface{} { 84 return int(*v) 85 } 86 87 type list []string 88 89 func (list *list) String() string { 90 return `"` + strings.Join(*list, ",") + `"` 91 } 92 93 func (list *list) Set(s string) error { 94 if s == "" { 95 *list = nil 96 return nil 97 } 98 99 *list = strings.Split(s, ",") 100 return nil 101 } 102 103 func FlagSet(name string) *flag.FlagSet { 104 flags := flag.NewFlagSet("", flag.ExitOnError) 105 flags.Usage = usage(name, flags) 106 flags.String("tags", "", "List of `build tags`") 107 flags.String("ignore", "", "Deprecated: use linter directives instead") 108 flags.Bool("tests", true, "Include tests") 109 flags.Bool("version", false, "Print version and exit") 110 flags.Bool("show-ignored", false, "Don't filter ignored problems") 111 flags.String("f", "text", "Output `format` (valid choices are 'stylish', 'text' and 'json')") 112 113 flags.Int("debug.max-concurrent-jobs", 0, "Number of jobs to run concurrently") 114 flags.Bool("debug.print-stats", false, "Print debug statistics") 115 flags.String("debug.cpuprofile", "", "Write CPU profile to `file`") 116 flags.String("debug.memprofile", "", "Write memory profile to `file`") 117 118 checks := list{"inherit"} 119 fail := list{"all"} 120 flags.Var(&checks, "checks", "Comma-separated list of `checks` to enable.") 121 flags.Var(&fail, "fail", "Comma-separated list of `checks` that can cause a non-zero exit status.") 122 123 tags := build.Default.ReleaseTags 124 v := tags[len(tags)-1][2:] 125 version := new(versionFlag) 126 if err := version.Set(v); err != nil { 127 panic(fmt.Sprintf("internal error: %s", err)) 128 } 129 130 flags.Var(version, "go", "Target Go `version` in the format '1.x'") 131 return flags 132 } 133 134 func ProcessFlagSet(cs []lint.Checker, fs *flag.FlagSet) { 135 tags := fs.Lookup("tags").Value.(flag.Getter).Get().(string) 136 ignore := fs.Lookup("ignore").Value.(flag.Getter).Get().(string) 137 tests := fs.Lookup("tests").Value.(flag.Getter).Get().(bool) 138 goVersion := fs.Lookup("go").Value.(flag.Getter).Get().(int) 139 formatter := fs.Lookup("f").Value.(flag.Getter).Get().(string) 140 printVersion := fs.Lookup("version").Value.(flag.Getter).Get().(bool) 141 showIgnored := fs.Lookup("show-ignored").Value.(flag.Getter).Get().(bool) 142 143 maxConcurrentJobs := fs.Lookup("debug.max-concurrent-jobs").Value.(flag.Getter).Get().(int) 144 printStats := fs.Lookup("debug.print-stats").Value.(flag.Getter).Get().(bool) 145 cpuProfile := fs.Lookup("debug.cpuprofile").Value.(flag.Getter).Get().(string) 146 memProfile := fs.Lookup("debug.memprofile").Value.(flag.Getter).Get().(string) 147 148 cfg := config.Config{} 149 cfg.Checks = *fs.Lookup("checks").Value.(*list) 150 151 exit := func(code int) { 152 if cpuProfile != "" { 153 pprof.StopCPUProfile() 154 } 155 if memProfile != "" { 156 f, err := os.Create(memProfile) 157 if err != nil { 158 panic(err) 159 } 160 runtime.GC() 161 pprof.WriteHeapProfile(f) 162 } 163 os.Exit(code) 164 } 165 if cpuProfile != "" { 166 f, err := os.Create(cpuProfile) 167 if err != nil { 168 log.Fatal(err) 169 } 170 pprof.StartCPUProfile(f) 171 } 172 173 if printVersion { 174 version.Print() 175 exit(0) 176 } 177 178 ps, err := Lint(cs, fs.Args(), &Options{ 179 Tags: strings.Fields(tags), 180 LintTests: tests, 181 Ignores: ignore, 182 GoVersion: goVersion, 183 ReturnIgnored: showIgnored, 184 Config: cfg, 185 186 MaxConcurrentJobs: maxConcurrentJobs, 187 PrintStats: printStats, 188 }) 189 if err != nil { 190 fmt.Fprintln(os.Stderr, err) 191 exit(1) 192 } 193 194 var f format.Formatter 195 switch formatter { 196 case "text": 197 f = format.Text{W: os.Stdout} 198 case "stylish": 199 f = &format.Stylish{W: os.Stdout} 200 case "json": 201 f = format.JSON{W: os.Stdout} 202 default: 203 fmt.Fprintf(os.Stderr, "unsupported output format %q\n", formatter) 204 exit(2) 205 } 206 207 var ( 208 total int 209 errors int 210 warnings int 211 ) 212 213 fail := *fs.Lookup("fail").Value.(*list) 214 var allChecks []string 215 for _, p := range ps { 216 allChecks = append(allChecks, p.Check) 217 } 218 219 shouldExit := lint.FilterChecks(allChecks, fail) 220 221 total = len(ps) 222 for _, p := range ps { 223 if shouldExit[p.Check] { 224 errors++ 225 } else { 226 p.Severity = lint.Warning 227 warnings++ 228 } 229 f.Format(p) 230 } 231 if f, ok := f.(format.Statter); ok { 232 f.Stats(total, errors, warnings) 233 } 234 if errors > 0 { 235 exit(1) 236 } 237 } 238 239 type Options struct { 240 Config config.Config 241 242 Tags []string 243 LintTests bool 244 Ignores string 245 GoVersion int 246 ReturnIgnored bool 247 248 MaxConcurrentJobs int 249 PrintStats bool 250 } 251 252 func Lint(cs []lint.Checker, paths []string, opt *Options) ([]lint.Problem, error) { 253 stats := lint.PerfStats{ 254 CheckerInits: map[string]time.Duration{}, 255 } 256 257 if opt == nil { 258 opt = &Options{} 259 } 260 ignores, err := parseIgnore(opt.Ignores) 261 if err != nil { 262 return nil, err 263 } 264 265 conf := &packages.Config{ 266 Mode: packages.LoadAllSyntax, 267 Tests: opt.LintTests, 268 BuildFlags: []string{ 269 "-tags=" + strings.Join(opt.Tags, " "), 270 }, 271 } 272 273 t := time.Now() 274 if len(paths) == 0 { 275 paths = []string{"."} 276 } 277 pkgs, err := packages.Load(conf, paths...) 278 if err != nil { 279 return nil, err 280 } 281 stats.PackageLoading = time.Since(t) 282 283 var problems []lint.Problem 284 workingPkgs := make([]*packages.Package, 0, len(pkgs)) 285 for _, pkg := range pkgs { 286 if pkg.IllTyped { 287 problems = append(problems, compileErrors(pkg)...) 288 } else { 289 workingPkgs = append(workingPkgs, pkg) 290 } 291 } 292 293 if len(workingPkgs) == 0 { 294 return problems, nil 295 } 296 297 l := &lint.Linter{ 298 Checkers: cs, 299 Ignores: ignores, 300 GoVersion: opt.GoVersion, 301 ReturnIgnored: opt.ReturnIgnored, 302 Config: opt.Config, 303 304 MaxConcurrentJobs: opt.MaxConcurrentJobs, 305 PrintStats: opt.PrintStats, 306 } 307 problems = append(problems, l.Lint(workingPkgs, &stats)...) 308 309 return problems, nil 310 } 311 312 var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?$`) 313 314 func parsePos(pos string) token.Position { 315 if pos == "-" || pos == "" { 316 return token.Position{} 317 } 318 parts := posRe.FindStringSubmatch(pos) 319 if parts == nil { 320 panic(fmt.Sprintf("internal error: malformed position %q", pos)) 321 } 322 file := parts[1] 323 line, _ := strconv.Atoi(parts[2]) 324 col, _ := strconv.Atoi(parts[3]) 325 return token.Position{ 326 Filename: file, 327 Line: line, 328 Column: col, 329 } 330 } 331 332 func compileErrors(pkg *packages.Package) []lint.Problem { 333 if !pkg.IllTyped { 334 return nil 335 } 336 if len(pkg.Errors) == 0 { 337 // transitively ill-typed 338 var ps []lint.Problem 339 for _, imp := range pkg.Imports { 340 ps = append(ps, compileErrors(imp)...) 341 } 342 return ps 343 } 344 var ps []lint.Problem 345 for _, err := range pkg.Errors { 346 p := lint.Problem{ 347 Position: parsePos(err.Pos), 348 Text: err.Msg, 349 Checker: "compiler", 350 Check: "compile", 351 } 352 ps = append(ps, p) 353 } 354 return ps 355 } 356 357 func ProcessArgs(name string, cs []lint.Checker, args []string) { 358 flags := FlagSet(name) 359 flags.Parse(args) 360 361 ProcessFlagSet(cs, flags) 362 }