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  }