1
0

Fix lint errs (#59)

This commit is contained in:
konrad
2019-02-18 19:32:41 +00:00
committed by Gitea
parent 15ef6deabc
commit 1b84292332
90 changed files with 10877 additions and 2179 deletions

View File

@ -0,0 +1,38 @@
package lint
import (
"bufio"
"bytes"
"io"
)
var (
// used by cgo before Go 1.11
oldCgo = []byte("// Created by cgo - DO NOT EDIT")
prefix = []byte("// Code generated ")
suffix = []byte(" DO NOT EDIT.")
nl = []byte("\n")
crnl = []byte("\r\n")
)
func isGenerated(r io.Reader) bool {
br := bufio.NewReader(r)
for {
s, err := br.ReadBytes('\n')
if err != nil && err != io.EOF {
return false
}
s = bytes.TrimSuffix(s, crnl)
s = bytes.TrimSuffix(s, nl)
if bytes.HasPrefix(s, prefix) && bytes.HasSuffix(s, suffix) {
return true
}
if bytes.Equal(s, oldCgo) {
return true
}
if err == io.EOF {
break
}
}
return false
}

View File

@ -1,25 +1,22 @@
// 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 lint provides the foundation for tools like gosimple.
// Package lint provides the foundation for tools like staticcheck
package lint // import "honnef.co/go/tools/lint"
import (
"fmt"
"go/ast"
"go/build"
"go/token"
"go/types"
"io"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"unicode"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/packages"
"honnef.co/go/tools/config"
"honnef.co/go/tools/ssa"
"honnef.co/go/tools/ssa/ssautil"
)
@ -28,8 +25,10 @@ type Job struct {
Program *Program
checker string
check string
check Check
problems []Problem
duration time.Duration
}
type Ignore interface {
@ -89,7 +88,7 @@ type GlobIgnore struct {
func (gi *GlobIgnore) Match(p Problem) bool {
if gi.Pattern != "*" {
pkgpath := p.Package.Path()
pkgpath := p.Package.Types.Path()
if strings.HasSuffix(pkgpath, "_test") {
pkgpath = pkgpath[:len(pkgpath)-len("_test")]
}
@ -107,31 +106,44 @@ func (gi *GlobIgnore) Match(p Problem) bool {
}
type Program struct {
SSA *ssa.Program
Prog *loader.Program
// TODO(dh): Rename to InitialPackages?
Packages []*Pkg
SSA *ssa.Program
InitialPackages []*Pkg
InitialFunctions []*ssa.Function
AllPackages []*packages.Package
AllFunctions []*ssa.Function
Files []*ast.File
Info *types.Info
GoVersion int
tokenFileMap map[*token.File]*ast.File
astFileMap map[*ast.File]*Pkg
packagesMap map[string]*packages.Package
genMu sync.RWMutex
generatedMap map[string]bool
}
func (prog *Program) Fset() *token.FileSet {
return prog.InitialPackages[0].Fset
}
type Func func(*Job)
type Severity uint8
const (
Error Severity = iota
Warning
Ignored
)
// Problem represents a problem in some source code.
type Problem struct {
pos token.Pos
Position token.Position // position in source file
Text string // the prose that describes the problem
Check string
Checker string
Package *types.Package
Ignored bool
Package *Pkg
Severity Severity
}
func (p *Problem) String() string {
@ -145,15 +157,25 @@ type Checker interface {
Name() string
Prefix() string
Init(*Program)
Funcs() map[string]Func
Checks() []Check
}
type Check struct {
Fn Func
ID string
FilterGenerated bool
}
// A Linter lints Go source code.
type Linter struct {
Checker Checker
Checkers []Checker
Ignores []Ignore
GoVersion int
ReturnIgnored bool
Config config.Config
MaxConcurrentJobs int
PrintStats bool
automaticIgnores []Ignore
}
@ -191,36 +213,6 @@ func (j *Job) File(node Positioner) *ast.File {
return j.Program.File(node)
}
// TODO(dh): switch to sort.Slice when Go 1.9 lands.
type byPosition struct {
fset *token.FileSet
ps []Problem
}
func (ps byPosition) Len() int {
return len(ps.ps)
}
func (ps byPosition) Less(i int, j int) bool {
pi, pj := ps.ps[i].Position, ps.ps[j].Position
if pi.Filename != pj.Filename {
return pi.Filename < pj.Filename
}
if pi.Line != pj.Line {
return pi.Line < pj.Line
}
if pi.Column != pj.Column {
return pi.Column < pj.Column
}
return ps.ps[i].Text < ps.ps[j].Text
}
func (ps byPosition) Swap(i int, j int) {
ps.ps[i], ps.ps[j] = ps.ps[j], ps.ps[i]
}
func parseDirective(s string) (cmd string, args []string) {
if !strings.HasPrefix(s, "//lint:") {
return "", nil
@ -230,79 +222,131 @@ func parseDirective(s string) (cmd string, args []string) {
return fields[0], fields[1:]
}
func (l *Linter) Lint(lprog *loader.Program, conf *loader.Config) []Problem {
ssaprog := ssautil.CreateProgram(lprog, ssa.GlobalDebug)
type PerfStats struct {
PackageLoading time.Duration
SSABuild time.Duration
OtherInitWork time.Duration
CheckerInits map[string]time.Duration
Jobs []JobStat
}
type JobStat struct {
Job string
Duration time.Duration
}
func (stats *PerfStats) Print(w io.Writer) {
fmt.Fprintln(w, "Package loading:", stats.PackageLoading)
fmt.Fprintln(w, "SSA build:", stats.SSABuild)
fmt.Fprintln(w, "Other init work:", stats.OtherInitWork)
fmt.Fprintln(w, "Checker inits:")
for checker, d := range stats.CheckerInits {
fmt.Fprintf(w, "\t%s: %s\n", checker, d)
}
fmt.Fprintln(w)
fmt.Fprintln(w, "Jobs:")
sort.Slice(stats.Jobs, func(i, j int) bool {
return stats.Jobs[i].Duration < stats.Jobs[j].Duration
})
var total time.Duration
for _, job := range stats.Jobs {
fmt.Fprintf(w, "\t%s: %s\n", job.Job, job.Duration)
total += job.Duration
}
fmt.Fprintf(w, "\tTotal: %s\n", total)
}
func (l *Linter) Lint(initial []*packages.Package, stats *PerfStats) []Problem {
allPkgs := allPackages(initial)
t := time.Now()
ssaprog, _ := ssautil.Packages(allPkgs, ssa.GlobalDebug)
ssaprog.Build()
if stats != nil {
stats.SSABuild = time.Since(t)
}
t = time.Now()
pkgMap := map[*ssa.Package]*Pkg{}
var pkgs []*Pkg
for _, pkginfo := range lprog.InitialPackages() {
ssapkg := ssaprog.Package(pkginfo.Pkg)
var bp *build.Package
if len(pkginfo.Files) != 0 {
path := lprog.Fset.Position(pkginfo.Files[0].Pos()).Filename
for _, pkg := range initial {
ssapkg := ssaprog.Package(pkg.Types)
var cfg config.Config
if len(pkg.GoFiles) != 0 {
path := pkg.GoFiles[0]
dir := filepath.Dir(path)
var err error
ctx := conf.Build
if ctx == nil {
ctx = &build.Default
}
bp, err = ctx.ImportDir(dir, 0)
// OPT(dh): we're rebuilding the entire config tree for
// each package. for example, if we check a/b/c and
// a/b/c/d, we'll process a, a/b, a/b/c, a, a/b, a/b/c,
// a/b/c/d we should cache configs per package and only
// load the new levels.
cfg, err = config.Load(dir)
if err != nil {
// shouldn't happen
// FIXME(dh): we couldn't load the config, what are we
// supposed to do? probably tell the user somehow
}
cfg = cfg.Merge(l.Config)
}
pkg := &Pkg{
Package: ssapkg,
Info: pkginfo,
BuildPkg: bp,
SSA: ssapkg,
Package: pkg,
Config: cfg,
}
pkgMap[ssapkg] = pkg
pkgs = append(pkgs, pkg)
}
prog := &Program{
SSA: ssaprog,
Prog: lprog,
Packages: pkgs,
Info: &types.Info{},
GoVersion: l.GoVersion,
tokenFileMap: map[*token.File]*ast.File{},
astFileMap: map[*ast.File]*Pkg{},
SSA: ssaprog,
InitialPackages: pkgs,
AllPackages: allPkgs,
GoVersion: l.GoVersion,
tokenFileMap: map[*token.File]*ast.File{},
astFileMap: map[*ast.File]*Pkg{},
generatedMap: map[string]bool{},
}
prog.packagesMap = map[string]*packages.Package{}
for _, pkg := range allPkgs {
prog.packagesMap[pkg.Types.Path()] = pkg
}
initial := map[*types.Package]struct{}{}
isInitial := map[*types.Package]struct{}{}
for _, pkg := range pkgs {
initial[pkg.Info.Pkg] = struct{}{}
isInitial[pkg.Types] = struct{}{}
}
for fn := range ssautil.AllFunctions(ssaprog) {
if fn.Pkg == nil {
continue
}
prog.AllFunctions = append(prog.AllFunctions, fn)
if _, ok := initial[fn.Pkg.Pkg]; ok {
if _, ok := isInitial[fn.Pkg.Pkg]; ok {
prog.InitialFunctions = append(prog.InitialFunctions, fn)
}
}
for _, pkg := range pkgs {
prog.Files = append(prog.Files, pkg.Info.Files...)
prog.Files = append(prog.Files, pkg.Syntax...)
ssapkg := ssaprog.Package(pkg.Info.Pkg)
for _, f := range pkg.Info.Files {
ssapkg := ssaprog.Package(pkg.Types)
for _, f := range pkg.Syntax {
prog.astFileMap[f] = pkgMap[ssapkg]
}
}
for _, pkginfo := range lprog.AllPackages {
for _, f := range pkginfo.Files {
tf := lprog.Fset.File(f.Pos())
for _, pkg := range allPkgs {
for _, f := range pkg.Syntax {
tf := pkg.Fset.File(f.Pos())
prog.tokenFileMap[tf] = f
}
}
var out []Problem
l.automaticIgnores = nil
for _, pkginfo := range lprog.InitialPackages() {
for _, f := range pkginfo.Files {
cm := ast.NewCommentMap(lprog.Fset, f, f.Comments)
for _, pkg := range initial {
for _, f := range pkg.Syntax {
cm := ast.NewCommentMap(pkg.Fset, f, f.Comments)
for node, cgs := range cm {
for _, cg := range cgs {
for _, c := range cg.List {
@ -315,11 +359,10 @@ func (l *Linter) Lint(lprog *loader.Program, conf *loader.Config) []Problem {
if len(args) < 2 {
// FIXME(dh): this causes duplicated warnings when using megacheck
p := Problem{
pos: c.Pos(),
Position: prog.DisplayPosition(c.Pos()),
Text: "malformed linter directive; missing the required reason field?",
Check: "",
Checker: l.Checker.Name(),
Checker: "lint",
Package: nil,
}
out = append(out, p)
@ -362,75 +405,84 @@ func (l *Linter) Lint(lprog *loader.Program, conf *loader.Config) []Problem {
scopes int
}{}
for _, pkg := range pkgs {
sizes.types += len(pkg.Info.Info.Types)
sizes.defs += len(pkg.Info.Info.Defs)
sizes.uses += len(pkg.Info.Info.Uses)
sizes.implicits += len(pkg.Info.Info.Implicits)
sizes.selections += len(pkg.Info.Info.Selections)
sizes.scopes += len(pkg.Info.Info.Scopes)
sizes.types += len(pkg.TypesInfo.Types)
sizes.defs += len(pkg.TypesInfo.Defs)
sizes.uses += len(pkg.TypesInfo.Uses)
sizes.implicits += len(pkg.TypesInfo.Implicits)
sizes.selections += len(pkg.TypesInfo.Selections)
sizes.scopes += len(pkg.TypesInfo.Scopes)
}
prog.Info.Types = make(map[ast.Expr]types.TypeAndValue, sizes.types)
prog.Info.Defs = make(map[*ast.Ident]types.Object, sizes.defs)
prog.Info.Uses = make(map[*ast.Ident]types.Object, sizes.uses)
prog.Info.Implicits = make(map[ast.Node]types.Object, sizes.implicits)
prog.Info.Selections = make(map[*ast.SelectorExpr]*types.Selection, sizes.selections)
prog.Info.Scopes = make(map[ast.Node]*types.Scope, sizes.scopes)
for _, pkg := range pkgs {
for k, v := range pkg.Info.Info.Types {
prog.Info.Types[k] = v
}
for k, v := range pkg.Info.Info.Defs {
prog.Info.Defs[k] = v
}
for k, v := range pkg.Info.Info.Uses {
prog.Info.Uses[k] = v
}
for k, v := range pkg.Info.Info.Implicits {
prog.Info.Implicits[k] = v
}
for k, v := range pkg.Info.Info.Selections {
prog.Info.Selections[k] = v
}
for k, v := range pkg.Info.Info.Scopes {
prog.Info.Scopes[k] = v
}
}
l.Checker.Init(prog)
funcs := l.Checker.Funcs()
var keys []string
for k := range funcs {
keys = append(keys, k)
if stats != nil {
stats.OtherInitWork = time.Since(t)
}
for _, checker := range l.Checkers {
t := time.Now()
checker.Init(prog)
if stats != nil {
stats.CheckerInits[checker.Name()] = time.Since(t)
}
}
sort.Strings(keys)
var jobs []*Job
for _, k := range keys {
j := &Job{
Program: prog,
checker: l.Checker.Name(),
check: k,
var allChecks []string
for _, checker := range l.Checkers {
checks := checker.Checks()
for _, check := range checks {
allChecks = append(allChecks, check.ID)
j := &Job{
Program: prog,
checker: checker.Name(),
check: check,
}
jobs = append(jobs, j)
}
jobs = append(jobs, j)
}
max := len(jobs)
if l.MaxConcurrentJobs > 0 {
max = l.MaxConcurrentJobs
}
sem := make(chan struct{}, max)
wg := &sync.WaitGroup{}
for _, j := range jobs {
wg.Add(1)
go func(j *Job) {
defer wg.Done()
fn := funcs[j.check]
sem <- struct{}{}
defer func() { <-sem }()
fn := j.check.Fn
if fn == nil {
return
}
t := time.Now()
fn(j)
j.duration = time.Since(t)
}(j)
}
wg.Wait()
for _, j := range jobs {
if stats != nil {
stats.Jobs = append(stats.Jobs, JobStat{j.check.ID, j.duration})
}
for _, p := range j.problems {
p.Ignored = l.ignore(p)
if l.ReturnIgnored || !p.Ignored {
allowedChecks := FilterChecks(allChecks, p.Package.Config.Checks)
if l.ignore(p) {
p.Severity = Ignored
}
// TODO(dh): support globs in check white/blacklist
// OPT(dh): this approach doesn't actually disable checks,
// it just discards their results. For the moment, that's
// fine. None of our checks are super expensive. In the
// future, we may want to provide opt-in expensive
// analysis, which shouldn't run at all. It may be easiest
// to implement this in the individual checks.
if (l.ReturnIgnored || p.Severity != Ignored) && allowedChecks[p.Check] {
out = append(out, p)
}
}
@ -444,39 +496,128 @@ func (l *Linter) Lint(lprog *loader.Program, conf *loader.Config) []Problem {
if ig.matched {
continue
}
for _, c := range ig.Checks {
idx := strings.IndexFunc(c, func(r rune) bool {
return unicode.IsNumber(r)
})
if idx == -1 {
// malformed check name, backing out
couldveMatched := false
for f, pkg := range prog.astFileMap {
if prog.Fset().Position(f.Pos()).Filename != ig.File {
continue
}
if c[:idx] != l.Checker.Prefix() {
// not for this checker
continue
allowedChecks := FilterChecks(allChecks, pkg.Config.Checks)
for _, c := range ig.Checks {
if !allowedChecks[c] {
continue
}
couldveMatched = true
break
}
p := Problem{
pos: ig.pos,
Position: prog.DisplayPosition(ig.pos),
Text: "this linter directive didn't match anything; should it be removed?",
Check: "",
Checker: l.Checker.Name(),
Package: nil,
}
out = append(out, p)
break
}
if !couldveMatched {
// The ignored checks were disabled for the containing package.
// Don't flag the ignore for not having matched.
continue
}
p := Problem{
Position: prog.DisplayPosition(ig.pos),
Text: "this linter directive didn't match anything; should it be removed?",
Check: "",
Checker: "lint",
Package: nil,
}
out = append(out, p)
}
sort.Sort(byPosition{lprog.Fset, out})
return out
sort.Slice(out, func(i int, j int) bool {
pi, pj := out[i].Position, out[j].Position
if pi.Filename != pj.Filename {
return pi.Filename < pj.Filename
}
if pi.Line != pj.Line {
return pi.Line < pj.Line
}
if pi.Column != pj.Column {
return pi.Column < pj.Column
}
return out[i].Text < out[j].Text
})
if l.PrintStats && stats != nil {
stats.Print(os.Stderr)
}
if len(out) < 2 {
return out
}
uniq := make([]Problem, 0, len(out))
uniq = append(uniq, out[0])
prev := out[0]
for _, p := range out[1:] {
if prev.Position == p.Position && prev.Text == p.Text {
continue
}
prev = p
uniq = append(uniq, p)
}
return uniq
}
func FilterChecks(allChecks []string, checks []string) map[string]bool {
// OPT(dh): this entire computation could be cached per package
allowedChecks := map[string]bool{}
for _, check := range checks {
b := true
if len(check) > 1 && check[0] == '-' {
b = false
check = check[1:]
}
if check == "*" || check == "all" {
// Match all
for _, c := range allChecks {
allowedChecks[c] = b
}
} else if strings.HasSuffix(check, "*") {
// Glob
prefix := check[:len(check)-1]
isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1
for _, c := range allChecks {
idx := strings.IndexFunc(c, func(r rune) bool { return unicode.IsNumber(r) })
if isCat {
// Glob is S*, which should match S1000 but not SA1000
cat := c[:idx]
if prefix == cat {
allowedChecks[c] = b
}
} else {
// Glob is S1*
if strings.HasPrefix(c, prefix) {
allowedChecks[c] = b
}
}
}
} else {
// Literal check name
allowedChecks[check] = b
}
}
return allowedChecks
}
func (prog *Program) Package(path string) *packages.Package {
return prog.packagesMap[path]
}
// Pkg represents a package being linted.
type Pkg struct {
*ssa.Package
Info *loader.PackageInfo
BuildPkg *build.Package
SSA *ssa.Package
*packages.Package
Config config.Config
}
type Positioner interface {
@ -484,52 +625,61 @@ type Positioner interface {
}
func (prog *Program) DisplayPosition(p token.Pos) token.Position {
// The //line compiler directive can be used to change the file
// name and line numbers associated with code. This can, for
// example, be used by code generation tools. The most prominent
// example is 'go tool cgo', which uses //line directives to refer
// back to the original source code.
//
// In the context of our linters, we need to treat these
// directives differently depending on context. For cgo files, we
// want to honour the directives, so that line numbers are
// adjusted correctly. For all other files, we want to ignore the
// directives, so that problems are reported at their actual
// position and not, for example, a yacc grammar file. This also
// affects the ignore mechanism, since it operates on the position
// information stored within problems. With this implementation, a
// user will ignore foo.go, not foo.y
// Only use the adjusted position if it points to another Go file.
// This means we'll point to the original file for cgo files, but
// we won't point to a YACC grammar file.
pkg := prog.astFileMap[prog.tokenFileMap[prog.Prog.Fset.File(p)]]
bp := pkg.BuildPkg
adjPos := prog.Prog.Fset.Position(p)
if bp == nil {
// couldn't find the package for some reason (deleted? faulty
// file system?)
pos := prog.Fset().PositionFor(p, false)
adjPos := prog.Fset().PositionFor(p, true)
if filepath.Ext(adjPos.Filename) == ".go" {
return adjPos
}
base := filepath.Base(adjPos.Filename)
for _, f := range bp.CgoFiles {
if f == base {
// this is a cgo file, use the adjusted position
return adjPos
}
return pos
}
func (prog *Program) isGenerated(path string) bool {
// This function isn't very efficient in terms of lock contention
// and lack of parallelism, but it really shouldn't matter.
// Projects consists of thousands of files, and have hundreds of
// errors. That's not a lot of calls to isGenerated.
prog.genMu.RLock()
if b, ok := prog.generatedMap[path]; ok {
prog.genMu.RUnlock()
return b
}
// not a cgo file, ignore //line directives
return prog.Prog.Fset.PositionFor(p, false)
prog.genMu.RUnlock()
prog.genMu.Lock()
defer prog.genMu.Unlock()
// recheck to avoid doing extra work in case of race
if b, ok := prog.generatedMap[path]; ok {
return b
}
f, err := os.Open(path)
if err != nil {
return false
}
defer f.Close()
b := isGenerated(f)
prog.generatedMap[path] = b
return b
}
func (j *Job) Errorf(n Positioner, format string, args ...interface{}) *Problem {
tf := j.Program.SSA.Fset.File(n.Pos())
f := j.Program.tokenFileMap[tf]
pkg := j.Program.astFileMap[f].Pkg
pkg := j.Program.astFileMap[f]
pos := j.Program.DisplayPosition(n.Pos())
if j.Program.isGenerated(pos.Filename) && j.check.FilterGenerated {
return nil
}
problem := Problem{
pos: n.Pos(),
Position: pos,
Text: fmt.Sprintf(format, args...),
Check: j.check,
Check: j.check.ID,
Checker: j.checker,
Package: pkg,
}
@ -541,3 +691,16 @@ func (j *Job) NodePackage(node Positioner) *Pkg {
f := j.File(node)
return j.Program.astFileMap[f]
}
func allPackages(pkgs []*packages.Package) []*packages.Package {
var out []*packages.Package
packages.Visit(
pkgs,
func(pkg *packages.Package) bool {
out = append(out, pkg)
return true
},
nil,
)
return out
}

View File

@ -103,10 +103,21 @@ func IsZero(expr ast.Expr) bool {
return IsIntLiteral(expr, "0")
}
func TypeOf(j *lint.Job, expr ast.Expr) types.Type { return j.Program.Info.TypeOf(expr) }
func TypeOf(j *lint.Job, expr ast.Expr) types.Type {
if expr == nil {
return nil
}
return j.NodePackage(expr).TypesInfo.TypeOf(expr)
}
func IsOfType(j *lint.Job, expr ast.Expr, name string) bool { return IsType(TypeOf(j, expr), name) }
func ObjectOf(j *lint.Job, ident *ast.Ident) types.Object { return j.Program.Info.ObjectOf(ident) }
func ObjectOf(j *lint.Job, ident *ast.Ident) types.Object {
if ident == nil {
return nil
}
return j.NodePackage(ident).TypesInfo.ObjectOf(ident)
}
func IsInTest(j *lint.Job, node lint.Positioner) bool {
// FIXME(dh): this doesn't work for global variables with
@ -123,14 +134,15 @@ func IsInMain(j *lint.Job, node lint.Positioner) bool {
if pkg == nil {
return false
}
return pkg.Pkg.Name() == "main"
return pkg.Types.Name() == "main"
}
func SelectorName(j *lint.Job, expr *ast.SelectorExpr) string {
sel := j.Program.Info.Selections[expr]
info := j.NodePackage(expr).TypesInfo
sel := info.Selections[expr]
if sel == nil {
if x, ok := expr.X.(*ast.Ident); ok {
pkg, ok := j.Program.Info.ObjectOf(x).(*types.PkgName)
pkg, ok := info.ObjectOf(x).(*types.PkgName)
if !ok {
// This shouldn't happen
return fmt.Sprintf("%s.%s", x.Name, expr.Sel.Name)
@ -143,11 +155,11 @@ func SelectorName(j *lint.Job, expr *ast.SelectorExpr) string {
}
func IsNil(j *lint.Job, expr ast.Expr) bool {
return j.Program.Info.Types[expr].IsNil()
return j.NodePackage(expr).TypesInfo.Types[expr].IsNil()
}
func BoolConst(j *lint.Job, expr ast.Expr) bool {
val := j.Program.Info.ObjectOf(expr.(*ast.Ident)).(*types.Const).Val()
val := j.NodePackage(expr).TypesInfo.ObjectOf(expr.(*ast.Ident)).(*types.Const).Val()
return constant.BoolVal(val)
}
@ -160,7 +172,7 @@ func IsBoolConst(j *lint.Job, expr ast.Expr) bool {
if !ok {
return false
}
obj := j.Program.Info.ObjectOf(ident)
obj := j.NodePackage(expr).TypesInfo.ObjectOf(ident)
c, ok := obj.(*types.Const)
if !ok {
return false
@ -176,7 +188,7 @@ func IsBoolConst(j *lint.Job, expr ast.Expr) bool {
}
func ExprToInt(j *lint.Job, expr ast.Expr) (int64, bool) {
tv := j.Program.Info.Types[expr]
tv := j.NodePackage(expr).TypesInfo.Types[expr]
if tv.Value == nil {
return 0, false
}
@ -187,7 +199,7 @@ func ExprToInt(j *lint.Job, expr ast.Expr) (int64, bool) {
}
func ExprToString(j *lint.Job, expr ast.Expr) (string, bool) {
val := j.Program.Info.Types[expr].Value
val := j.NodePackage(expr).TypesInfo.Types[expr].Value
if val == nil {
return "", false
}
@ -220,17 +232,35 @@ func IsGoVersion(j *lint.Job, minor int) bool {
return j.Program.GoVersion >= minor
}
func CallNameAST(j *lint.Job, call *ast.CallExpr) string {
switch fun := call.Fun.(type) {
case *ast.SelectorExpr:
fn, ok := ObjectOf(j, fun.Sel).(*types.Func)
if !ok {
return ""
}
return fn.FullName()
case *ast.Ident:
obj := ObjectOf(j, fun)
switch obj := obj.(type) {
case *types.Func:
return obj.FullName()
case *types.Builtin:
return obj.Name()
default:
return ""
}
default:
return ""
}
}
func IsCallToAST(j *lint.Job, node ast.Node, name string) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return false
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return false
}
fn, ok := j.Program.Info.ObjectOf(sel.Sel).(*types.Func)
return ok && fn.FullName() == name
return CallNameAST(j, call) == name
}
func IsCallToAnyAST(j *lint.Job, node ast.Node, names ...string) bool {
@ -280,3 +310,70 @@ func Inspect(node ast.Node, fn func(node ast.Node) bool) {
}
ast.Inspect(node, fn)
}
func GroupSpecs(j *lint.Job, specs []ast.Spec) [][]ast.Spec {
if len(specs) == 0 {
return nil
}
fset := j.Program.SSA.Fset
groups := make([][]ast.Spec, 1)
groups[0] = append(groups[0], specs[0])
for _, spec := range specs[1:] {
g := groups[len(groups)-1]
if fset.PositionFor(spec.Pos(), false).Line-1 !=
fset.PositionFor(g[len(g)-1].End(), false).Line {
groups = append(groups, nil)
}
groups[len(groups)-1] = append(groups[len(groups)-1], spec)
}
return groups
}
func IsObject(obj types.Object, name string) bool {
var path string
if pkg := obj.Pkg(); pkg != nil {
path = pkg.Path() + "."
}
return path+obj.Name() == name
}
type Field struct {
Var *types.Var
Tag string
Path []int
}
// FlattenFields recursively flattens T and embedded structs,
// returning a list of fields. If multiple fields with the same name
// exist, all will be returned.
func FlattenFields(T *types.Struct) []Field {
return flattenFields(T, nil, nil)
}
func flattenFields(T *types.Struct, path []int, seen map[types.Type]bool) []Field {
if seen == nil {
seen = map[types.Type]bool{}
}
if seen[T] {
return nil
}
seen[T] = true
var out []Field
for i := 0; i < T.NumFields(); i++ {
field := T.Field(i)
tag := T.Tag(i)
np := append(path[:len(path):len(path)], i)
if field.Anonymous() {
if s, ok := Dereference(field.Type()).Underlying().(*types.Struct); ok {
out = append(out, flattenFields(s, np, seen)...)
}
} else {
out = append(out, Field{field, tag, np})
}
}
return out
}

View File

@ -0,0 +1,128 @@
// Package format provides formatters for linter problems.
package format
import (
"encoding/json"
"fmt"
"go/token"
"io"
"os"
"path/filepath"
"text/tabwriter"
"honnef.co/go/tools/lint"
)
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
}
type Statter interface {
Stats(total, errors, warnings int)
}
type Formatter interface {
Format(p lint.Problem)
}
type Text struct {
W io.Writer
}
func (o Text) Format(p lint.Problem) {
fmt.Fprintf(o.W, "%v: %s\n", relativePositionString(p.Position), p.String())
}
type JSON struct {
W io.Writer
}
func severity(s lint.Severity) string {
switch s {
case lint.Error:
return "error"
case lint.Warning:
return "warning"
case lint.Ignored:
return "ignored"
}
return ""
}
func (o JSON) Format(p lint.Problem) {
type location struct {
File string `json:"file"`
Line int `json:"line"`
Column int `json:"column"`
}
jp := struct {
Code string `json:"code"`
Severity string `json:"severity,omitempty"`
Location location `json:"location"`
Message string `json:"message"`
}{
Code: p.Check,
Severity: severity(p.Severity),
Location: location{
File: p.Position.Filename,
Line: p.Position.Line,
Column: p.Position.Column,
},
Message: p.Text,
}
_ = json.NewEncoder(o.W).Encode(jp)
}
type Stylish struct {
W io.Writer
prevFile string
tw *tabwriter.Writer
}
func (o *Stylish) Format(p lint.Problem) {
if p.Position.Filename == "" {
p.Position.Filename = "-"
}
if p.Position.Filename != o.prevFile {
if o.prevFile != "" {
o.tw.Flush()
fmt.Fprintln(o.W)
}
fmt.Fprintln(o.W, p.Position.Filename)
o.prevFile = p.Position.Filename
o.tw = tabwriter.NewWriter(o.W, 0, 4, 2, ' ', 0)
}
fmt.Fprintf(o.tw, " (%d, %d)\t%s\t%s\n", p.Position.Line, p.Position.Column, p.Check, p.Text)
}
func (o *Stylish) Stats(total, errors, warnings int) {
if o.tw != nil {
o.tw.Flush()
fmt.Fprintln(o.W)
}
fmt.Fprintf(o.W, " ✖ %d problems (%d errors, %d warnings)\n",
total, errors, warnings)
}

View File

@ -8,70 +8,28 @@
package lintutil // import "honnef.co/go/tools/lint/lintutil"
import (
"encoding/json"
"errors"
"flag"
"fmt"
"go/build"
"go/parser"
"go/token"
"go/types"
"io"
"log"
"os"
"path/filepath"
"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"
"github.com/kisielk/gotool"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/packages"
)
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)
@ -84,38 +42,6 @@ func usage(name string, flags *flag.FlagSet) func() {
}
}
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 {
@ -158,16 +84,41 @@ 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.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.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 'text' and 'json')")
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:]
@ -180,76 +131,129 @@ func FlagSet(name string) *flag.FlagSet {
return flags
}
type CheckerConfig struct {
Checker lint.Checker
ExitNonZero bool
}
func ProcessFlagSet(confs []CheckerConfig, fs *flag.FlagSet) {
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)
format := fs.Lookup("f").Value.(flag.Getter).Get().(string)
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)
if printVersion {
version.Print()
os.Exit(0)
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)
}
var cs []lint.Checker
for _, conf := range confs {
cs = append(cs, conf.Checker)
if printVersion {
version.Print()
exit(0)
}
pss, err := Lint(cs, fs.Args(), &Options{
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)
os.Exit(1)
exit(1)
}
var ps []lint.Problem
for _, p := range pss {
ps = append(ps, p...)
}
var f OutputFormatter
switch format {
var f format.Formatter
switch formatter {
case "text":
f = TextOutput{os.Stdout}
f = format.Text{W: os.Stdout}
case "stylish":
f = &format.Stylish{W: os.Stdout}
case "json":
f = JSONOutput{os.Stdout}
f = format.JSON{W: os.Stdout}
default:
fmt.Fprintf(os.Stderr, "unsupported output format %q\n", format)
os.Exit(2)
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)
}
for i, p := range pss {
if len(p) != 0 && confs[i].ExitNonZero {
os.Exit(1)
}
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, pkgs []string, opt *Options) ([][]lint.Problem, error) {
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{}
}
@ -257,94 +261,102 @@ func Lint(cs []lint.Checker, pkgs []string, opt *Options) ([][]lint.Problem, err
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)
},
conf := &packages.Config{
Mode: packages.LoadAllSyntax,
Tests: opt.LintTests,
BuildFlags: []string{
"-tags=" + strings.Join(opt.Tags, " "),
},
}
if goFiles {
conf.CreateFromFilenames("adhoc", paths...)
} else {
for _, path := range paths {
conf.ImportPkgs[path] = opt.LintTests
}
t := time.Now()
if len(paths) == 0 {
paths = []string{"."}
}
lprog, err := conf.Load()
pkgs, err := packages.Load(conf, paths...)
if err != nil {
return nil, err
}
stats.PackageLoading = time.Since(t)
var problems [][]lint.Problem
for _, c := range cs {
runner := &runner{
checker: c,
tags: opt.Tags,
ignores: ignores,
version: opt.GoVersion,
returnIgnored: opt.ReturnIgnored,
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)
}
problems = append(problems, runner.lint(lprog, conf))
}
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
}
func shortPath(path string) string {
cwd, err := os.Getwd()
if err != nil {
return path
var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?$`)
func parsePos(pos string) token.Position {
if pos == "-" || pos == "" {
return token.Position{}
}
if rel, err := filepath.Rel(cwd, path); err == nil && len(rel) < len(path) {
return rel
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,
}
return path
}
func relativePositionString(pos token.Position) string {
s := shortPath(pos.Filename)
if pos.IsValid() {
if s != "" {
s += ":"
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)...)
}
s += fmt.Sprintf("%d:%d", pos.Line, pos.Column)
return ps
}
if s == "" {
s = "-"
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 s
return ps
}
func ProcessArgs(name string, cs []CheckerConfig, args []string) {
func ProcessArgs(name string, cs []lint.Checker, 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)
}