// Copyright (c) 2013 The Go Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd. // Package lintutil provides helpers for writing linter command lines. package lintutil // import "honnef.co/go/tools/lint/lintutil" import ( "encoding/json" "errors" "flag" "fmt" "go/build" "go/parser" "go/token" "go/types" "io" "os" "path/filepath" "strconv" "strings" "honnef.co/go/tools/lint" "honnef.co/go/tools/version" "github.com/kisielk/gotool" "golang.org/x/tools/go/loader" ) type OutputFormatter interface { Format(p lint.Problem) } type TextOutput struct { w io.Writer } func (o TextOutput) Format(p lint.Problem) { fmt.Fprintf(o.w, "%v: %s\n", relativePositionString(p.Position), p.String()) } type JSONOutput struct { w io.Writer } func (o JSONOutput) Format(p lint.Problem) { type location struct { File string `json:"file"` Line int `json:"line"` Column int `json:"column"` } jp := struct { Checker string `json:"checker"` Code string `json:"code"` Severity string `json:"severity,omitempty"` Location location `json:"location"` Message string `json:"message"` Ignored bool `json:"ignored"` }{ p.Checker, p.Check, "", // TODO(dh): support severity location{ p.Position.Filename, p.Position.Line, p.Position.Column, }, p.Text, p.Ignored, } _ = json.NewEncoder(o.w).Encode(jp) } func usage(name string, flags *flag.FlagSet) func() { return func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", name) fmt.Fprintf(os.Stderr, "\t%s [flags] # runs on package in current directory\n", name) fmt.Fprintf(os.Stderr, "\t%s [flags] packages\n", name) fmt.Fprintf(os.Stderr, "\t%s [flags] directory\n", name) fmt.Fprintf(os.Stderr, "\t%s [flags] files... # must be a single package\n", name) fmt.Fprintf(os.Stderr, "Flags:\n") flags.PrintDefaults() } } type runner struct { checker lint.Checker tags []string ignores []lint.Ignore version int returnIgnored bool } func resolveRelative(importPaths []string, tags []string) (goFiles bool, err error) { if len(importPaths) == 0 { return false, nil } if strings.HasSuffix(importPaths[0], ".go") { // User is specifying a package in terms of .go files, don't resolve return true, nil } wd, err := os.Getwd() if err != nil { return false, err } ctx := build.Default ctx.BuildTags = tags for i, path := range importPaths { bpkg, err := ctx.Import(path, wd, build.FindOnly) if err != nil { return false, fmt.Errorf("can't load package %q: %v", path, err) } importPaths[i] = bpkg.ImportPath } return false, nil } func parseIgnore(s string) ([]lint.Ignore, error) { var out []lint.Ignore if len(s) == 0 { return nil, nil } for _, part := range strings.Fields(s) { p := strings.Split(part, ":") if len(p) != 2 { return nil, errors.New("malformed ignore string") } path := p[0] checks := strings.Split(p[1], ",") out = append(out, &lint.GlobIgnore{Pattern: path, Checks: checks}) } return out, nil } type versionFlag int func (v *versionFlag) String() string { return fmt.Sprintf("1.%d", *v) } func (v *versionFlag) Set(s string) error { if len(s) < 3 { return errors.New("invalid Go version") } if s[0] != '1' { return errors.New("invalid Go version") } if s[1] != '.' { return errors.New("invalid Go version") } i, err := strconv.Atoi(s[2:]) *v = versionFlag(i) return err } func (v *versionFlag) Get() interface{} { return int(*v) } func FlagSet(name string) *flag.FlagSet { flags := flag.NewFlagSet("", flag.ExitOnError) flags.Usage = usage(name, flags) flags.Float64("min_confidence", 0, "Deprecated; use -ignore instead") flags.String("tags", "", "List of `build tags`") 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'") flags.Bool("tests", true, "Include tests") flags.Bool("version", false, "Print version and exit") flags.Bool("show-ignored", false, "Don't filter ignored problems") flags.String("f", "text", "Output `format` (valid choices are 'text' and 'json')") tags := build.Default.ReleaseTags v := tags[len(tags)-1][2:] version := new(versionFlag) if err := version.Set(v); err != nil { panic(fmt.Sprintf("internal error: %s", err)) } flags.Var(version, "go", "Target Go `version` in the format '1.x'") return flags } type CheckerConfig struct { Checker lint.Checker ExitNonZero bool } func ProcessFlagSet(confs []CheckerConfig, fs *flag.FlagSet) { tags := fs.Lookup("tags").Value.(flag.Getter).Get().(string) ignore := fs.Lookup("ignore").Value.(flag.Getter).Get().(string) tests := fs.Lookup("tests").Value.(flag.Getter).Get().(bool) goVersion := fs.Lookup("go").Value.(flag.Getter).Get().(int) format := fs.Lookup("f").Value.(flag.Getter).Get().(string) printVersion := fs.Lookup("version").Value.(flag.Getter).Get().(bool) showIgnored := fs.Lookup("show-ignored").Value.(flag.Getter).Get().(bool) if printVersion { version.Print() os.Exit(0) } var cs []lint.Checker for _, conf := range confs { cs = append(cs, conf.Checker) } pss, err := Lint(cs, fs.Args(), &Options{ Tags: strings.Fields(tags), LintTests: tests, Ignores: ignore, GoVersion: goVersion, ReturnIgnored: showIgnored, }) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } var ps []lint.Problem for _, p := range pss { ps = append(ps, p...) } var f OutputFormatter switch format { case "text": f = TextOutput{os.Stdout} case "json": f = JSONOutput{os.Stdout} default: fmt.Fprintf(os.Stderr, "unsupported output format %q\n", format) os.Exit(2) } for _, p := range ps { f.Format(p) } for i, p := range pss { if len(p) != 0 && confs[i].ExitNonZero { os.Exit(1) } } } type Options struct { Tags []string LintTests bool Ignores string GoVersion int ReturnIgnored bool } func Lint(cs []lint.Checker, pkgs []string, opt *Options) ([][]lint.Problem, error) { if opt == nil { opt = &Options{} } ignores, err := parseIgnore(opt.Ignores) if err != nil { return nil, err } paths := gotool.ImportPaths(pkgs) goFiles, err := resolveRelative(paths, opt.Tags) if err != nil { return nil, err } ctx := build.Default ctx.BuildTags = opt.Tags hadError := false conf := &loader.Config{ Build: &ctx, ParserMode: parser.ParseComments, ImportPkgs: map[string]bool{}, TypeChecker: types.Config{ Sizes: types.SizesFor(ctx.Compiler, ctx.GOARCH), Error: func(err error) { // Only print the first error found if hadError { return } hadError = true fmt.Fprintln(os.Stderr, err) }, }, } if goFiles { conf.CreateFromFilenames("adhoc", paths...) } else { for _, path := range paths { conf.ImportPkgs[path] = opt.LintTests } } lprog, err := conf.Load() if err != nil { return nil, err } var problems [][]lint.Problem for _, c := range cs { runner := &runner{ checker: c, tags: opt.Tags, ignores: ignores, version: opt.GoVersion, returnIgnored: opt.ReturnIgnored, } problems = append(problems, runner.lint(lprog, conf)) } return problems, nil } func shortPath(path string) string { cwd, err := os.Getwd() if err != nil { return path } if rel, err := filepath.Rel(cwd, path); err == nil && len(rel) < len(path) { return rel } return path } func relativePositionString(pos token.Position) string { s := shortPath(pos.Filename) if pos.IsValid() { if s != "" { s += ":" } s += fmt.Sprintf("%d:%d", pos.Line, pos.Column) } if s == "" { s = "-" } return s } func ProcessArgs(name string, cs []CheckerConfig, args []string) { flags := FlagSet(name) flags.Parse(args) ProcessFlagSet(cs, flags) } func (runner *runner) lint(lprog *loader.Program, conf *loader.Config) []lint.Problem { l := &lint.Linter{ Checker: runner.checker, Ignores: runner.ignores, GoVersion: runner.version, ReturnIgnored: runner.returnIgnored, } return l.Lint(lprog, conf) }