362 lines
8.4 KiB
Go
362 lines
8.4 KiB
Go
// 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 (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"go/build"
|
|
"go/token"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"runtime/pprof"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"honnef.co/go/tools/config"
|
|
"honnef.co/go/tools/lint"
|
|
"honnef.co/go/tools/lint/lintutil/format"
|
|
"honnef.co/go/tools/version"
|
|
|
|
"golang.org/x/tools/go/packages"
|
|
)
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
type list []string
|
|
|
|
func (list *list) String() string {
|
|
return `"` + strings.Join(*list, ",") + `"`
|
|
}
|
|
|
|
func (list *list) Set(s string) error {
|
|
if s == "" {
|
|
*list = nil
|
|
return nil
|
|
}
|
|
|
|
*list = strings.Split(s, ",")
|
|
return nil
|
|
}
|
|
|
|
func FlagSet(name string) *flag.FlagSet {
|
|
flags := flag.NewFlagSet("", flag.ExitOnError)
|
|
flags.Usage = usage(name, flags)
|
|
flags.String("tags", "", "List of `build tags`")
|
|
flags.String("ignore", "", "Deprecated: use linter directives instead")
|
|
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 'stylish', 'text' and 'json')")
|
|
|
|
flags.Int("debug.max-concurrent-jobs", 0, "Number of jobs to run concurrently")
|
|
flags.Bool("debug.print-stats", false, "Print debug statistics")
|
|
flags.String("debug.cpuprofile", "", "Write CPU profile to `file`")
|
|
flags.String("debug.memprofile", "", "Write memory profile to `file`")
|
|
|
|
checks := list{"inherit"}
|
|
fail := list{"all"}
|
|
flags.Var(&checks, "checks", "Comma-separated list of `checks` to enable.")
|
|
flags.Var(&fail, "fail", "Comma-separated list of `checks` that can cause a non-zero exit status.")
|
|
|
|
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
|
|
}
|
|
|
|
func ProcessFlagSet(cs []lint.Checker, 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)
|
|
formatter := 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)
|
|
|
|
maxConcurrentJobs := fs.Lookup("debug.max-concurrent-jobs").Value.(flag.Getter).Get().(int)
|
|
printStats := fs.Lookup("debug.print-stats").Value.(flag.Getter).Get().(bool)
|
|
cpuProfile := fs.Lookup("debug.cpuprofile").Value.(flag.Getter).Get().(string)
|
|
memProfile := fs.Lookup("debug.memprofile").Value.(flag.Getter).Get().(string)
|
|
|
|
cfg := config.Config{}
|
|
cfg.Checks = *fs.Lookup("checks").Value.(*list)
|
|
|
|
exit := func(code int) {
|
|
if cpuProfile != "" {
|
|
pprof.StopCPUProfile()
|
|
}
|
|
if memProfile != "" {
|
|
f, err := os.Create(memProfile)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
runtime.GC()
|
|
pprof.WriteHeapProfile(f)
|
|
}
|
|
os.Exit(code)
|
|
}
|
|
if cpuProfile != "" {
|
|
f, err := os.Create(cpuProfile)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
pprof.StartCPUProfile(f)
|
|
}
|
|
|
|
if printVersion {
|
|
version.Print()
|
|
exit(0)
|
|
}
|
|
|
|
ps, err := Lint(cs, fs.Args(), &Options{
|
|
Tags: strings.Fields(tags),
|
|
LintTests: tests,
|
|
Ignores: ignore,
|
|
GoVersion: goVersion,
|
|
ReturnIgnored: showIgnored,
|
|
Config: cfg,
|
|
|
|
MaxConcurrentJobs: maxConcurrentJobs,
|
|
PrintStats: printStats,
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
exit(1)
|
|
}
|
|
|
|
var f format.Formatter
|
|
switch formatter {
|
|
case "text":
|
|
f = format.Text{W: os.Stdout}
|
|
case "stylish":
|
|
f = &format.Stylish{W: os.Stdout}
|
|
case "json":
|
|
f = format.JSON{W: os.Stdout}
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unsupported output format %q\n", formatter)
|
|
exit(2)
|
|
}
|
|
|
|
var (
|
|
total int
|
|
errors int
|
|
warnings int
|
|
)
|
|
|
|
fail := *fs.Lookup("fail").Value.(*list)
|
|
var allChecks []string
|
|
for _, p := range ps {
|
|
allChecks = append(allChecks, p.Check)
|
|
}
|
|
|
|
shouldExit := lint.FilterChecks(allChecks, fail)
|
|
|
|
total = len(ps)
|
|
for _, p := range ps {
|
|
if shouldExit[p.Check] {
|
|
errors++
|
|
} else {
|
|
p.Severity = lint.Warning
|
|
warnings++
|
|
}
|
|
f.Format(p)
|
|
}
|
|
if f, ok := f.(format.Statter); ok {
|
|
f.Stats(total, errors, warnings)
|
|
}
|
|
if errors > 0 {
|
|
exit(1)
|
|
}
|
|
}
|
|
|
|
type Options struct {
|
|
Config config.Config
|
|
|
|
Tags []string
|
|
LintTests bool
|
|
Ignores string
|
|
GoVersion int
|
|
ReturnIgnored bool
|
|
|
|
MaxConcurrentJobs int
|
|
PrintStats bool
|
|
}
|
|
|
|
func Lint(cs []lint.Checker, paths []string, opt *Options) ([]lint.Problem, error) {
|
|
stats := lint.PerfStats{
|
|
CheckerInits: map[string]time.Duration{},
|
|
}
|
|
|
|
if opt == nil {
|
|
opt = &Options{}
|
|
}
|
|
ignores, err := parseIgnore(opt.Ignores)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conf := &packages.Config{
|
|
Mode: packages.LoadAllSyntax,
|
|
Tests: opt.LintTests,
|
|
BuildFlags: []string{
|
|
"-tags=" + strings.Join(opt.Tags, " "),
|
|
},
|
|
}
|
|
|
|
t := time.Now()
|
|
if len(paths) == 0 {
|
|
paths = []string{"."}
|
|
}
|
|
pkgs, err := packages.Load(conf, paths...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stats.PackageLoading = time.Since(t)
|
|
|
|
var problems []lint.Problem
|
|
workingPkgs := make([]*packages.Package, 0, len(pkgs))
|
|
for _, pkg := range pkgs {
|
|
if pkg.IllTyped {
|
|
problems = append(problems, compileErrors(pkg)...)
|
|
} else {
|
|
workingPkgs = append(workingPkgs, pkg)
|
|
}
|
|
}
|
|
|
|
if len(workingPkgs) == 0 {
|
|
return problems, nil
|
|
}
|
|
|
|
l := &lint.Linter{
|
|
Checkers: cs,
|
|
Ignores: ignores,
|
|
GoVersion: opt.GoVersion,
|
|
ReturnIgnored: opt.ReturnIgnored,
|
|
Config: opt.Config,
|
|
|
|
MaxConcurrentJobs: opt.MaxConcurrentJobs,
|
|
PrintStats: opt.PrintStats,
|
|
}
|
|
problems = append(problems, l.Lint(workingPkgs, &stats)...)
|
|
|
|
return problems, nil
|
|
}
|
|
|
|
var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?$`)
|
|
|
|
func parsePos(pos string) token.Position {
|
|
if pos == "-" || pos == "" {
|
|
return token.Position{}
|
|
}
|
|
parts := posRe.FindStringSubmatch(pos)
|
|
if parts == nil {
|
|
panic(fmt.Sprintf("internal error: malformed position %q", pos))
|
|
}
|
|
file := parts[1]
|
|
line, _ := strconv.Atoi(parts[2])
|
|
col, _ := strconv.Atoi(parts[3])
|
|
return token.Position{
|
|
Filename: file,
|
|
Line: line,
|
|
Column: col,
|
|
}
|
|
}
|
|
|
|
func compileErrors(pkg *packages.Package) []lint.Problem {
|
|
if !pkg.IllTyped {
|
|
return nil
|
|
}
|
|
if len(pkg.Errors) == 0 {
|
|
// transitively ill-typed
|
|
var ps []lint.Problem
|
|
for _, imp := range pkg.Imports {
|
|
ps = append(ps, compileErrors(imp)...)
|
|
}
|
|
return ps
|
|
}
|
|
var ps []lint.Problem
|
|
for _, err := range pkg.Errors {
|
|
p := lint.Problem{
|
|
Position: parsePos(err.Pos),
|
|
Text: err.Msg,
|
|
Checker: "compiler",
|
|
Check: "compile",
|
|
}
|
|
ps = append(ps, p)
|
|
}
|
|
return ps
|
|
}
|
|
|
|
func ProcessArgs(name string, cs []lint.Checker, args []string) {
|
|
flags := FlagSet(name)
|
|
flags.Parse(args)
|
|
|
|
ProcessFlagSet(cs, flags)
|
|
}
|