github.com/pankona/gometalinter@v2.0.11+incompatible/_linters/src/honnef.co/go/tools/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 "honnef.co/go/tools/lint/lintutil"
     9  
    10  import (
    11  	"encoding/json"
    12  	"errors"
    13  	"flag"
    14  	"fmt"
    15  	"go/build"
    16  	"go/parser"
    17  	"go/token"
    18  	"go/types"
    19  	"io"
    20  	"os"
    21  	"path/filepath"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"honnef.co/go/tools/lint"
    26  	"honnef.co/go/tools/version"
    27  
    28  	"github.com/kisielk/gotool"
    29  	"golang.org/x/tools/go/loader"
    30  )
    31  
    32  type OutputFormatter interface {
    33  	Format(p lint.Problem)
    34  }
    35  
    36  type TextOutput struct {
    37  	w io.Writer
    38  }
    39  
    40  func (o TextOutput) Format(p lint.Problem) {
    41  	fmt.Fprintf(o.w, "%v: %s\n", relativePositionString(p.Position), p.String())
    42  }
    43  
    44  type JSONOutput struct {
    45  	w io.Writer
    46  }
    47  
    48  func (o JSONOutput) Format(p lint.Problem) {
    49  	type location struct {
    50  		File   string `json:"file"`
    51  		Line   int    `json:"line"`
    52  		Column int    `json:"column"`
    53  	}
    54  	jp := struct {
    55  		Checker  string   `json:"checker"`
    56  		Code     string   `json:"code"`
    57  		Severity string   `json:"severity,omitempty"`
    58  		Location location `json:"location"`
    59  		Message  string   `json:"message"`
    60  		Ignored  bool     `json:"ignored"`
    61  	}{
    62  		p.Checker,
    63  		p.Check,
    64  		"", // TODO(dh): support severity
    65  		location{
    66  			p.Position.Filename,
    67  			p.Position.Line,
    68  			p.Position.Column,
    69  		},
    70  		p.Text,
    71  		p.Ignored,
    72  	}
    73  	_ = json.NewEncoder(o.w).Encode(jp)
    74  }
    75  func usage(name string, flags *flag.FlagSet) func() {
    76  	return func() {
    77  		fmt.Fprintf(os.Stderr, "Usage of %s:\n", name)
    78  		fmt.Fprintf(os.Stderr, "\t%s [flags] # runs on package in current directory\n", name)
    79  		fmt.Fprintf(os.Stderr, "\t%s [flags] packages\n", name)
    80  		fmt.Fprintf(os.Stderr, "\t%s [flags] directory\n", name)
    81  		fmt.Fprintf(os.Stderr, "\t%s [flags] files... # must be a single package\n", name)
    82  		fmt.Fprintf(os.Stderr, "Flags:\n")
    83  		flags.PrintDefaults()
    84  	}
    85  }
    86  
    87  type runner struct {
    88  	checker       lint.Checker
    89  	tags          []string
    90  	ignores       []lint.Ignore
    91  	version       int
    92  	returnIgnored bool
    93  }
    94  
    95  func resolveRelative(importPaths []string, tags []string) (goFiles bool, err error) {
    96  	if len(importPaths) == 0 {
    97  		return false, nil
    98  	}
    99  	if strings.HasSuffix(importPaths[0], ".go") {
   100  		// User is specifying a package in terms of .go files, don't resolve
   101  		return true, nil
   102  	}
   103  	wd, err := os.Getwd()
   104  	if err != nil {
   105  		return false, err
   106  	}
   107  	ctx := build.Default
   108  	ctx.BuildTags = tags
   109  	for i, path := range importPaths {
   110  		bpkg, err := ctx.Import(path, wd, build.FindOnly)
   111  		if err != nil {
   112  			return false, fmt.Errorf("can't load package %q: %v", path, err)
   113  		}
   114  		importPaths[i] = bpkg.ImportPath
   115  	}
   116  	return false, nil
   117  }
   118  
   119  func parseIgnore(s string) ([]lint.Ignore, error) {
   120  	var out []lint.Ignore
   121  	if len(s) == 0 {
   122  		return nil, nil
   123  	}
   124  	for _, part := range strings.Fields(s) {
   125  		p := strings.Split(part, ":")
   126  		if len(p) != 2 {
   127  			return nil, errors.New("malformed ignore string")
   128  		}
   129  		path := p[0]
   130  		checks := strings.Split(p[1], ",")
   131  		out = append(out, &lint.GlobIgnore{Pattern: path, Checks: checks})
   132  	}
   133  	return out, nil
   134  }
   135  
   136  type versionFlag int
   137  
   138  func (v *versionFlag) String() string {
   139  	return fmt.Sprintf("1.%d", *v)
   140  }
   141  
   142  func (v *versionFlag) Set(s string) error {
   143  	if len(s) < 3 {
   144  		return errors.New("invalid Go version")
   145  	}
   146  	if s[0] != '1' {
   147  		return errors.New("invalid Go version")
   148  	}
   149  	if s[1] != '.' {
   150  		return errors.New("invalid Go version")
   151  	}
   152  	i, err := strconv.Atoi(s[2:])
   153  	*v = versionFlag(i)
   154  	return err
   155  }
   156  
   157  func (v *versionFlag) Get() interface{} {
   158  	return int(*v)
   159  }
   160  
   161  func FlagSet(name string) *flag.FlagSet {
   162  	flags := flag.NewFlagSet("", flag.ExitOnError)
   163  	flags.Usage = usage(name, flags)
   164  	flags.Float64("min_confidence", 0, "Deprecated; use -ignore instead")
   165  	flags.String("tags", "", "List of `build tags`")
   166  	flags.String("ignore", "", "Space separated list of checks to ignore, in the following format: 'import/path/file.go:Check1,Check2,...' Both the import path and file name sections support globbing, e.g. 'os/exec/*_test.go'")
   167  	flags.Bool("tests", true, "Include tests")
   168  	flags.Bool("version", false, "Print version and exit")
   169  	flags.Bool("show-ignored", false, "Don't filter ignored problems")
   170  	flags.String("f", "text", "Output `format` (valid choices are 'text' and 'json')")
   171  
   172  	tags := build.Default.ReleaseTags
   173  	v := tags[len(tags)-1][2:]
   174  	version := new(versionFlag)
   175  	if err := version.Set(v); err != nil {
   176  		panic(fmt.Sprintf("internal error: %s", err))
   177  	}
   178  
   179  	flags.Var(version, "go", "Target Go `version` in the format '1.x'")
   180  	return flags
   181  }
   182  
   183  type CheckerConfig struct {
   184  	Checker     lint.Checker
   185  	ExitNonZero bool
   186  }
   187  
   188  func ProcessFlagSet(confs []CheckerConfig, fs *flag.FlagSet) {
   189  	tags := fs.Lookup("tags").Value.(flag.Getter).Get().(string)
   190  	ignore := fs.Lookup("ignore").Value.(flag.Getter).Get().(string)
   191  	tests := fs.Lookup("tests").Value.(flag.Getter).Get().(bool)
   192  	goVersion := fs.Lookup("go").Value.(flag.Getter).Get().(int)
   193  	format := fs.Lookup("f").Value.(flag.Getter).Get().(string)
   194  	printVersion := fs.Lookup("version").Value.(flag.Getter).Get().(bool)
   195  	showIgnored := fs.Lookup("show-ignored").Value.(flag.Getter).Get().(bool)
   196  
   197  	if printVersion {
   198  		version.Print()
   199  		os.Exit(0)
   200  	}
   201  
   202  	var cs []lint.Checker
   203  	for _, conf := range confs {
   204  		cs = append(cs, conf.Checker)
   205  	}
   206  	pss, err := Lint(cs, fs.Args(), &Options{
   207  		Tags:          strings.Fields(tags),
   208  		LintTests:     tests,
   209  		Ignores:       ignore,
   210  		GoVersion:     goVersion,
   211  		ReturnIgnored: showIgnored,
   212  	})
   213  	if err != nil {
   214  		fmt.Fprintln(os.Stderr, err)
   215  		os.Exit(1)
   216  	}
   217  
   218  	var ps []lint.Problem
   219  	for _, p := range pss {
   220  		ps = append(ps, p...)
   221  	}
   222  
   223  	var f OutputFormatter
   224  	switch format {
   225  	case "text":
   226  		f = TextOutput{os.Stdout}
   227  	case "json":
   228  		f = JSONOutput{os.Stdout}
   229  	default:
   230  		fmt.Fprintf(os.Stderr, "unsupported output format %q\n", format)
   231  		os.Exit(2)
   232  	}
   233  
   234  	for _, p := range ps {
   235  		f.Format(p)
   236  	}
   237  	for i, p := range pss {
   238  		if len(p) != 0 && confs[i].ExitNonZero {
   239  			os.Exit(1)
   240  		}
   241  	}
   242  }
   243  
   244  type Options struct {
   245  	Tags          []string
   246  	LintTests     bool
   247  	Ignores       string
   248  	GoVersion     int
   249  	ReturnIgnored bool
   250  }
   251  
   252  func Lint(cs []lint.Checker, pkgs []string, opt *Options) ([][]lint.Problem, error) {
   253  	if opt == nil {
   254  		opt = &Options{}
   255  	}
   256  	ignores, err := parseIgnore(opt.Ignores)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	paths := gotool.ImportPaths(pkgs)
   261  	goFiles, err := resolveRelative(paths, opt.Tags)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	ctx := build.Default
   266  	ctx.BuildTags = opt.Tags
   267  	hadError := false
   268  	conf := &loader.Config{
   269  		Build:      &ctx,
   270  		ParserMode: parser.ParseComments,
   271  		ImportPkgs: map[string]bool{},
   272  		TypeChecker: types.Config{
   273  			Error: func(err error) {
   274  				// Only print the first error found
   275  				if hadError {
   276  					return
   277  				}
   278  				hadError = true
   279  				fmt.Fprintln(os.Stderr, err)
   280  			},
   281  		},
   282  	}
   283  	if goFiles {
   284  		conf.CreateFromFilenames("adhoc", paths...)
   285  	} else {
   286  		for _, path := range paths {
   287  			conf.ImportPkgs[path] = opt.LintTests
   288  		}
   289  	}
   290  	lprog, err := conf.Load()
   291  	if err != nil {
   292  		return nil, err
   293  	}
   294  
   295  	var problems [][]lint.Problem
   296  	for _, c := range cs {
   297  		runner := &runner{
   298  			checker:       c,
   299  			tags:          opt.Tags,
   300  			ignores:       ignores,
   301  			version:       opt.GoVersion,
   302  			returnIgnored: opt.ReturnIgnored,
   303  		}
   304  		problems = append(problems, runner.lint(lprog, conf))
   305  	}
   306  	return problems, nil
   307  }
   308  
   309  func shortPath(path string) string {
   310  	cwd, err := os.Getwd()
   311  	if err != nil {
   312  		return path
   313  	}
   314  	if rel, err := filepath.Rel(cwd, path); err == nil && len(rel) < len(path) {
   315  		return rel
   316  	}
   317  	return path
   318  }
   319  
   320  func relativePositionString(pos token.Position) string {
   321  	s := shortPath(pos.Filename)
   322  	if pos.IsValid() {
   323  		if s != "" {
   324  			s += ":"
   325  		}
   326  		s += fmt.Sprintf("%d:%d", pos.Line, pos.Column)
   327  	}
   328  	if s == "" {
   329  		s = "-"
   330  	}
   331  	return s
   332  }
   333  
   334  func ProcessArgs(name string, cs []CheckerConfig, args []string) {
   335  	flags := FlagSet(name)
   336  	flags.Parse(args)
   337  
   338  	ProcessFlagSet(cs, flags)
   339  }
   340  
   341  func (runner *runner) lint(lprog *loader.Program, conf *loader.Config) []lint.Problem {
   342  	l := &lint.Linter{
   343  		Checker:       runner.checker,
   344  		Ignores:       runner.ignores,
   345  		GoVersion:     runner.version,
   346  		ReturnIgnored: runner.returnIgnored,
   347  	}
   348  	return l.Lint(lprog, conf)
   349  }