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

48
vendor/honnef.co/go/tools/arg/arg.go vendored Normal file
View File

@ -0,0 +1,48 @@
package arg
var args = map[string]int{
"(*encoding/json.Decoder).Decode.v": 0,
"(*encoding/json.Encoder).Encode.v": 0,
"(*encoding/xml.Decoder).Decode.v": 0,
"(*encoding/xml.Encoder).Encode.v": 0,
"(*sync.Pool).Put.x": 0,
"(*text/template.Template).Parse.text": 0,
"(io.Seeker).Seek.offset": 0,
"(time.Time).Sub.u": 0,
"append.elems": 1,
"append.slice": 0,
"bytes.Equal.a": 0,
"bytes.Equal.b": 1,
"encoding/binary.Write.data": 2,
"errors.New.text": 0,
"fmt.Fprintf.format": 1,
"fmt.Printf.format": 0,
"fmt.Sprintf.a[0]": 1,
"fmt.Sprintf.format": 0,
"json.Marshal.v": 0,
"json.Unmarshal.v": 1,
"len.v": 0,
"make.size[0]": 1,
"make.size[1]": 2,
"make.t": 0,
"net/url.Parse.rawurl": 0,
"os.OpenFile.flag": 1,
"os/exec.Command.name": 0,
"os/signal.Notify.c": 0,
"regexp.Compile.expr": 0,
"runtime.SetFinalizer.finalizer": 1,
"runtime.SetFinalizer.obj": 0,
"sort.Sort.data": 0,
"time.Parse.layout": 0,
"time.Sleep.d": 0,
"xml.Marshal.v": 0,
"xml.Unmarshal.v": 1,
}
func Arg(name string) int {
n, ok := args[name]
if !ok {
panic("unknown argument " + name)
}
return n
}

View File

@ -1,15 +0,0 @@
# gosimple
_gosimple_ is a linter for Go source code that specialises on
simplifying code.
## Installation
Gosimple requires Go 1.6 or later.
go get honnef.co/go/tools/cmd/gosimple
## Documentation
Detailed documentation can be found on
[staticcheck.io](https://staticcheck.io/docs/gosimple).

View File

@ -1,21 +0,0 @@
// gosimple detects code that could be rewritten in a simpler way.
package main // import "honnef.co/go/tools/cmd/gosimple"
import (
"os"
"honnef.co/go/tools/lint/lintutil"
"honnef.co/go/tools/simple"
)
func main() {
fs := lintutil.FlagSet("gosimple")
gen := fs.Bool("generated", false, "Check generated code")
fs.Parse(os.Args[1:])
c := simple.NewChecker()
c.CheckGenerated = *gen
cfg := lintutil.CheckerConfig{
Checker: c,
ExitNonZero: true,
}
lintutil.ProcessFlagSet([]lintutil.CheckerConfig{cfg}, fs)
}

View File

@ -1,16 +1,15 @@
# staticcheck
_staticcheck_ is `go vet` on steroids, applying a ton of static analysis
checks you might be used to from tools like ReSharper for C#.
_staticcheck_ offers extensive analysis of Go code, covering a myriad
of categories. It will detect bugs, suggest code simplifications,
point out dead code, and more.
## Installation
Staticcheck requires Go 1.6 or later.
go get honnef.co/go/tools/cmd/staticcheck
See [the main README](https://github.com/dominikh/go-tools#installation) for installation instructions.
## Documentation
Detailed documentation can be found on
[staticcheck.io](https://staticcheck.io/docs/staticcheck).
[staticcheck.io](https://staticcheck.io/docs/).

View File

@ -1,23 +1,30 @@
// staticcheck detects a myriad of bugs and inefficiencies in your
// code.
// staticcheck analyses Go code and makes it better.
package main // import "honnef.co/go/tools/cmd/staticcheck"
import (
"os"
"honnef.co/go/tools/lint"
"honnef.co/go/tools/lint/lintutil"
"honnef.co/go/tools/simple"
"honnef.co/go/tools/staticcheck"
"honnef.co/go/tools/stylecheck"
"honnef.co/go/tools/unused"
)
func main() {
fs := lintutil.FlagSet("staticcheck")
gen := fs.Bool("generated", false, "Check generated code")
fs.Parse(os.Args[1:])
c := staticcheck.NewChecker()
c.CheckGenerated = *gen
cfg := lintutil.CheckerConfig{
Checker: c,
ExitNonZero: true,
checkers := []lint.Checker{
simple.NewChecker(),
staticcheck.NewChecker(),
stylecheck.NewChecker(),
}
lintutil.ProcessFlagSet([]lintutil.CheckerConfig{cfg}, fs)
uc := unused.NewChecker(unused.CheckAll)
uc.ConsiderReflection = true
checkers = append(checkers, unused.NewLintChecker(uc))
lintutil.ProcessFlagSet(checkers, fs)
}

View File

@ -1,131 +0,0 @@
# unused
_unused_ checks Go code for unused constants, variables, functions and
types.
## Install
go get honnef.co/go/tools/cmd/unused
## Usage
unused -help
## Usage Tips
- When running _unused_ on multiple packages, it will first try to
check them all at once, because that's faster. If any of the
packages don't compile, however, _unused_ will check each package
individually.
The first step can, depending on the number of packages, use a lot
of memory. For the entire standard library, it uses roughly 800 MB.
For a GOPATH with thousands of packages, it can quickly use several
gigabytes. If that is an issue, consider using something like this
instead:
```
for pkg in $(go list your_selection); do unused "$pkg"; done
```
This will effectively skip the first step and always check every
package individually.
## What counts as used/unused?
_unused_ checks for unused constants, functions, types and optionally
struct fields. They will be considered used or unused under the
following conditions:
- Unexported package-level objects will be reported as unused if there
are no explicit references to them.
- Unexported methods will be reported as unused if there are no
explicit references to them and if they don't implement any
interfaces.
- The `main` function is considered as used if it's in the `main`
package.
- `init` functions are always considered as used.
- Exported objects in function scope are treated like unexported
objects.
- Exported functions in tests are treated like unexported functions,
unless they're test, benchmark or example functions.
- Struct fields will be considered as unused if there are no explicit
references to them. Unkeyed composite literals with >=1 elements
mark all fields of the struct as used.
- Neither the checks for methods nor for struct fields are aware of
the reflect package and may thus produce false positives.
## Whole program analysis
Optionally via the `-exported` flag, _unused_ can analyse all
arguments as a single program and report unused exported identifiers.
This can be useful for checking "internal" packages, or large software
projects that do not export an API to the public, but use exported
methods between components.
Do note that in the whole-program analysis, all arguments must
type-check. It is not possible to check packages individually in this
mode.
## Examples
```
$ time unused cmd/go
/usr/lib/go/src/cmd/go/build.go:1327:6: func hasString is unused
/usr/lib/go/src/cmd/go/build.go:2328:6: func toolVerify is unused
/usr/lib/go/src/cmd/go/generate.go:375:21: func identLength is unused
/usr/lib/go/src/cmd/go/get.go:474:5: var goTag is unused
/usr/lib/go/src/cmd/go/get.go:513:6: func cmpGoVersion is unused
/usr/lib/go/src/cmd/go/go_test.go:426:23: func grepCountStdout is unused
/usr/lib/go/src/cmd/go/go_test.go:432:23: func grepCountStderr is unused
/usr/lib/go/src/cmd/go/main.go:406:5: var logf is unused
/usr/lib/go/src/cmd/go/main.go:431:6: func runOut is unused
/usr/lib/go/src/cmd/go/pkg.go:91:2: field forceBuild is unused
/usr/lib/go/src/cmd/go/pkg.go:688:2: const toRoot is unused
/usr/lib/go/src/cmd/go/testflag.go:278:6: func setIntFlag is unused
unused cmd/go 3.33s user 0.25s system 447% cpu 0.799 total
```
```
$ time unused $(go list github.com/prometheus/prometheus/... | grep -v /vendor/)
/home/dominikh/prj/src/github.com/prometheus/prometheus/promql/engine_test.go:11:5: var noop is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/retrieval/discovery/dns.go:39:2: const interval is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/retrieval/discovery/dns.go:69:2: field m is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/retrieval/discovery/nerve.go:31:2: const nerveNodePrefix is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/retrieval/discovery/serverset.go:33:2: const serversetNodePrefix is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/retrieval/scrape.go:41:2: const ingestedSamplesCap is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/retrieval/scrape.go:49:2: var errSkippedScrape is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/retrieval/targetmanager.go:184:2: field providers is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/storage/local/delta.go:394:2: field error is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/storage/local/delta.go:398:3: field error is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/storage/local/doubledelta.go:500:2: field error is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/storage/local/doubledelta.go:504:3: field error is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/storage/remote/opentsdb/client.go:40:2: var illegalCharsRE is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/util/stats/timer.go:56:2: field child is unused
/home/dominikh/prj/src/github.com/prometheus/prometheus/util/treecache/treecache.go:25:2: field zkEvents is unused
unused $(go list github.com/prometheus/prometheus/... | grep -v /vendor/) 5.70s user 0.43s system 535% cpu 1.142 total
```
```
$ time unused -exported github.com/kr/pretty/...
/home/dominikh/prj/src/github.com/kr/pretty/formatter.go:14:2: const limit is unused
/home/dominikh/prj/src/github.com/kr/pretty/formatter.go:322:6: func tryDeepEqual is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:20:6: func Errorf is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:28:6: func Fprintf is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:37:6: func Log is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:45:6: func Logf is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:54:6: func Logln is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:63:6: func Print is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:71:6: func Printf is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:80:6: func Println is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:88:6: func Sprintf is unused
/home/dominikh/prj/src/github.com/kr/pretty/pretty.go:92:6: func wrap is unused
unused -exported github.com/kr/pretty/... 1.23s user 0.19s system 253% cpu 0.558 total
```

View File

@ -1,78 +0,0 @@
// unused reports unused identifiers (types, functions, ...) in your
// code.
package main // import "honnef.co/go/tools/cmd/unused"
import (
"log"
"os"
"honnef.co/go/tools/lint/lintutil"
"honnef.co/go/tools/unused"
)
var (
fConstants bool
fFields bool
fFunctions bool
fTypes bool
fVariables bool
fDebug string
fWholeProgram bool
fReflection bool
)
func newChecker(mode unused.CheckMode) *unused.Checker {
checker := unused.NewChecker(mode)
if fDebug != "" {
debug, err := os.Create(fDebug)
if err != nil {
log.Fatal("couldn't open debug file:", err)
}
checker.Debug = debug
}
checker.WholeProgram = fWholeProgram
checker.ConsiderReflection = fReflection
return checker
}
func main() {
log.SetFlags(0)
fs := lintutil.FlagSet("unused")
fs.BoolVar(&fConstants, "consts", true, "Report unused constants")
fs.BoolVar(&fFields, "fields", true, "Report unused fields")
fs.BoolVar(&fFunctions, "funcs", true, "Report unused functions and methods")
fs.BoolVar(&fTypes, "types", true, "Report unused types")
fs.BoolVar(&fVariables, "vars", true, "Report unused variables")
fs.StringVar(&fDebug, "debug", "", "Write a debug graph to `file`. Existing files will be overwritten.")
fs.BoolVar(&fWholeProgram, "exported", false, "Treat arguments as a program and report unused exported identifiers")
fs.BoolVar(&fReflection, "reflect", true, "Consider identifiers as used when it's likely they'll be accessed via reflection")
fs.Parse(os.Args[1:])
var mode unused.CheckMode
if fConstants {
mode |= unused.CheckConstants
}
if fFields {
mode |= unused.CheckFields
}
if fFunctions {
mode |= unused.CheckFunctions
}
if fTypes {
mode |= unused.CheckTypes
}
if fVariables {
mode |= unused.CheckVariables
}
checker := newChecker(mode)
l := unused.NewLintChecker(checker)
cfg := lintutil.CheckerConfig{
Checker: l,
ExitNonZero: true,
}
lintutil.ProcessFlagSet([]lintutil.CheckerConfig{cfg}, fs)
}

View File

@ -0,0 +1,162 @@
package config
import (
"os"
"path/filepath"
"github.com/BurntSushi/toml"
)
func mergeLists(a, b []string) []string {
out := make([]string, 0, len(a)+len(b))
for _, el := range b {
if el == "inherit" {
out = append(out, a...)
} else {
out = append(out, el)
}
}
return out
}
func normalizeList(list []string) []string {
if len(list) > 1 {
nlist := make([]string, 0, len(list))
nlist = append(nlist, list[0])
for i, el := range list[1:] {
if el != list[i] {
nlist = append(nlist, el)
}
}
list = nlist
}
for _, el := range list {
if el == "inherit" {
// This should never happen, because the default config
// should not use "inherit"
panic(`unresolved "inherit"`)
}
}
return list
}
func (cfg Config) Merge(ocfg Config) Config {
if ocfg.Checks != nil {
cfg.Checks = mergeLists(cfg.Checks, ocfg.Checks)
}
if ocfg.Initialisms != nil {
cfg.Initialisms = mergeLists(cfg.Initialisms, ocfg.Initialisms)
}
if ocfg.DotImportWhitelist != nil {
cfg.DotImportWhitelist = mergeLists(cfg.DotImportWhitelist, ocfg.DotImportWhitelist)
}
if ocfg.HTTPStatusCodeWhitelist != nil {
cfg.HTTPStatusCodeWhitelist = mergeLists(cfg.HTTPStatusCodeWhitelist, ocfg.HTTPStatusCodeWhitelist)
}
return cfg
}
type Config struct {
// TODO(dh): this implementation makes it impossible for external
// clients to add their own checkers with configuration. At the
// moment, we don't really care about that; we don't encourage
// that people use this package. In the future, we may. The
// obvious solution would be using map[string]interface{}, but
// that's obviously subpar.
Checks []string `toml:"checks"`
Initialisms []string `toml:"initialisms"`
DotImportWhitelist []string `toml:"dot_import_whitelist"`
HTTPStatusCodeWhitelist []string `toml:"http_status_code_whitelist"`
}
var defaultConfig = Config{
Checks: []string{"all", "-ST1000", "-ST1003", "-ST1016"},
Initialisms: []string{
"ACL", "API", "ASCII", "CPU", "CSS", "DNS",
"EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID",
"IP", "JSON", "QPS", "RAM", "RPC", "SLA",
"SMTP", "SQL", "SSH", "TCP", "TLS", "TTL",
"UDP", "UI", "GID", "UID", "UUID", "URI",
"URL", "UTF8", "VM", "XML", "XMPP", "XSRF",
"XSS",
},
DotImportWhitelist: []string{},
HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"},
}
const configName = "staticcheck.conf"
func parseConfigs(dir string) ([]Config, error) {
var out []Config
// TODO(dh): consider stopping at the GOPATH/module boundary
for dir != "" {
f, err := os.Open(filepath.Join(dir, configName))
if os.IsNotExist(err) {
ndir := filepath.Dir(dir)
if ndir == dir {
break
}
dir = ndir
continue
}
if err != nil {
return nil, err
}
var cfg Config
_, err = toml.DecodeReader(f, &cfg)
f.Close()
if err != nil {
return nil, err
}
out = append(out, cfg)
ndir := filepath.Dir(dir)
if ndir == dir {
break
}
dir = ndir
}
out = append(out, defaultConfig)
if len(out) < 2 {
return out, nil
}
for i := 0; i < len(out)/2; i++ {
out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i]
}
return out, nil
}
func mergeConfigs(confs []Config) Config {
if len(confs) == 0 {
// This shouldn't happen because we always have at least a
// default config.
panic("trying to merge zero configs")
}
if len(confs) == 1 {
return confs[0]
}
conf := confs[0]
for _, oconf := range confs[1:] {
conf = conf.Merge(oconf)
}
return conf
}
func Load(dir string) (Config, error) {
confs, err := parseConfigs(dir)
if err != nil {
return Config{}, err
}
conf := mergeConfigs(confs)
conf.Checks = normalizeList(conf.Checks)
conf.Initialisms = normalizeList(conf.Initialisms)
conf.DotImportWhitelist = normalizeList(conf.DotImportWhitelist)
conf.HTTPStatusCodeWhitelist = normalizeList(conf.HTTPStatusCodeWhitelist)
return conf, nil
}

View File

@ -0,0 +1,10 @@
checks = ["all", "-ST1003", "-ST1014"]
initialisms = ["ACL", "API", "ASCII", "CPU", "CSS", "DNS",
"EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID",
"IP", "JSON", "QPS", "RAM", "RPC", "SLA",
"SMTP", "SQL", "SSH", "TCP", "TLS", "TTL",
"UDP", "UI", "GID", "UID", "UUID", "URI",
"URL", "UTF8", "VM", "XML", "XMPP", "XSRF",
"XSS"]
dot_import_whitelist = []
http_status_code_whitelist = ["200", "400", "404", "500"]

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)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
// +build !go1.8
package simple
import "go/types"
var structsIdentical = types.Identical

View File

@ -1,7 +0,0 @@
// +build go1.8
package simple
import "go/types"
var structsIdentical = types.IdenticalIgnoreTags

View File

@ -12,9 +12,57 @@ import (
"go/types"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/packages"
"honnef.co/go/tools/ssa"
)
// Packages creates an SSA program for a set of packages loaded from
// source syntax using the golang.org/x/tools/go/packages.Load function.
// It creates and returns an SSA package for each well-typed package in
// the initial list. The resulting list of packages has the same length
// as initial, and contains a nil if SSA could not be constructed for
// the corresponding initial package.
//
// Code for bodies of functions is not built until Build is called
// on the resulting Program.
//
// The mode parameter controls diagnostics and checking during SSA construction.
//
func Packages(initial []*packages.Package, mode ssa.BuilderMode) (*ssa.Program, []*ssa.Package) {
var fset *token.FileSet
if len(initial) > 0 {
fset = initial[0].Fset
}
prog := ssa.NewProgram(fset, mode)
seen := make(map[*packages.Package]*ssa.Package)
var create func(p *packages.Package) *ssa.Package
create = func(p *packages.Package) *ssa.Package {
ssapkg, ok := seen[p]
if !ok {
if p.Types == nil || p.IllTyped {
// not well typed
seen[p] = nil
return nil
}
ssapkg = prog.CreatePackage(p.Types, p.Syntax, p.TypesInfo, true)
seen[p] = ssapkg
for _, imp := range p.Imports {
create(imp)
}
}
return ssapkg
}
var ssapkgs []*ssa.Package
for _, p := range initial {
ssapkgs = append(ssapkgs, create(p))
}
return prog, ssapkgs
}
// CreateProgram returns a new program in SSA form, given a program
// loaded from source. An SSA package is created for each transitively
// error-free package of lprog.

View File

@ -0,0 +1,41 @@
package ssautil
import (
"honnef.co/go/tools/ssa"
)
func Reachable(from, to *ssa.BasicBlock) bool {
if from == to {
return true
}
if from.Dominates(to) {
return true
}
found := false
Walk(from, func(b *ssa.BasicBlock) bool {
if b == to {
found = true
return false
}
return true
})
return found
}
func Walk(b *ssa.BasicBlock, fn func(*ssa.BasicBlock) bool) {
seen := map[*ssa.BasicBlock]bool{}
wl := []*ssa.BasicBlock{b}
for len(wl) > 0 {
b := wl[len(wl)-1]
wl = wl[:len(wl)-1]
if seen[b] {
continue
}
seen[b] = true
if !fn(b) {
continue
}
wl = append(wl, b.Succs...)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,643 @@
package stylecheck // import "honnef.co/go/tools/stylecheck"
import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"honnef.co/go/tools/lint"
. "honnef.co/go/tools/lint/lintdsl"
"honnef.co/go/tools/ssa"
"golang.org/x/tools/go/types/typeutil"
)
type Checker struct {
CheckGenerated bool
}
func NewChecker() *Checker {
return &Checker{}
}
func (*Checker) Name() string { return "stylecheck" }
func (*Checker) Prefix() string { return "ST" }
func (c *Checker) Init(prog *lint.Program) {}
func (c *Checker) Checks() []lint.Check {
return []lint.Check{
{ID: "ST1000", FilterGenerated: false, Fn: c.CheckPackageComment},
{ID: "ST1001", FilterGenerated: true, Fn: c.CheckDotImports},
// {ID: "ST1002", FilterGenerated: true, Fn: c.CheckBlankImports},
{ID: "ST1003", FilterGenerated: true, Fn: c.CheckNames},
// {ID: "ST1004", FilterGenerated: false, Fn: nil, },
{ID: "ST1005", FilterGenerated: false, Fn: c.CheckErrorStrings},
{ID: "ST1006", FilterGenerated: false, Fn: c.CheckReceiverNames},
// {ID: "ST1007", FilterGenerated: true, Fn: c.CheckIncDec},
{ID: "ST1008", FilterGenerated: false, Fn: c.CheckErrorReturn},
// {ID: "ST1009", FilterGenerated: false, Fn: c.CheckUnexportedReturn},
// {ID: "ST1010", FilterGenerated: false, Fn: c.CheckContextFirstArg},
{ID: "ST1011", FilterGenerated: false, Fn: c.CheckTimeNames},
{ID: "ST1012", FilterGenerated: false, Fn: c.CheckErrorVarNames},
{ID: "ST1013", FilterGenerated: true, Fn: c.CheckHTTPStatusCodes},
{ID: "ST1015", FilterGenerated: true, Fn: c.CheckDefaultCaseOrder},
{ID: "ST1016", FilterGenerated: false, Fn: c.CheckReceiverNamesIdentical},
{ID: "ST1017", FilterGenerated: true, Fn: c.CheckYodaConditions},
}
}
func (c *Checker) CheckPackageComment(j *lint.Job) {
// - At least one file in a non-main package should have a package comment
//
// - The comment should be of the form
// "Package x ...". This has a slight potential for false
// positives, as multiple files can have package comments, in
// which case they get appended. But that doesn't happen a lot in
// the real world.
for _, pkg := range j.Program.InitialPackages {
if pkg.Name == "main" {
continue
}
hasDocs := false
for _, f := range pkg.Syntax {
if IsInTest(j, f) {
continue
}
if f.Doc != nil && len(f.Doc.List) > 0 {
hasDocs = true
prefix := "Package " + f.Name.Name + " "
if !strings.HasPrefix(strings.TrimSpace(f.Doc.Text()), prefix) {
j.Errorf(f.Doc, `package comment should be of the form "%s..."`, prefix)
}
f.Doc.Text()
}
}
if !hasDocs {
for _, f := range pkg.Syntax {
if IsInTest(j, f) {
continue
}
j.Errorf(f, "at least one file in a package should have a package comment")
}
}
}
}
func (c *Checker) CheckDotImports(j *lint.Job) {
for _, pkg := range j.Program.InitialPackages {
for _, f := range pkg.Syntax {
imports:
for _, imp := range f.Imports {
path := imp.Path.Value
path = path[1 : len(path)-1]
for _, w := range pkg.Config.DotImportWhitelist {
if w == path {
continue imports
}
}
if imp.Name != nil && imp.Name.Name == "." && !IsInTest(j, f) {
j.Errorf(imp, "should not use dot imports")
}
}
}
}
}
func (c *Checker) CheckBlankImports(j *lint.Job) {
fset := j.Program.Fset()
for _, f := range j.Program.Files {
if IsInMain(j, f) || IsInTest(j, f) {
continue
}
// Collect imports of the form `import _ "foo"`, i.e. with no
// parentheses, as their comment will be associated with the
// (paren-free) GenDecl, not the import spec itself.
//
// We don't directly process the GenDecl so that we can
// correctly handle the following:
//
// import _ "foo"
// import _ "bar"
//
// where only the first import should get flagged.
skip := map[ast.Spec]bool{}
ast.Inspect(f, func(node ast.Node) bool {
switch node := node.(type) {
case *ast.File:
return true
case *ast.GenDecl:
if node.Tok != token.IMPORT {
return false
}
if node.Lparen == token.NoPos && node.Doc != nil {
skip[node.Specs[0]] = true
}
return false
}
return false
})
for i, imp := range f.Imports {
pos := fset.Position(imp.Pos())
if !IsBlank(imp.Name) {
continue
}
// Only flag the first blank import in a group of imports,
// or don't flag any of them, if the first one is
// commented
if i > 0 {
prev := f.Imports[i-1]
prevPos := fset.Position(prev.Pos())
if pos.Line-1 == prevPos.Line && IsBlank(prev.Name) {
continue
}
}
if imp.Doc == nil && imp.Comment == nil && !skip[imp] {
j.Errorf(imp, "a blank import should be only in a main or test package, or have a comment justifying it")
}
}
}
}
func (c *Checker) CheckIncDec(j *lint.Job) {
// TODO(dh): this can be noisy for function bodies that look like this:
// x += 3
// ...
// x += 2
// ...
// x += 1
fn := func(node ast.Node) bool {
assign, ok := node.(*ast.AssignStmt)
if !ok || (assign.Tok != token.ADD_ASSIGN && assign.Tok != token.SUB_ASSIGN) {
return true
}
if (len(assign.Lhs) != 1 || len(assign.Rhs) != 1) ||
!IsIntLiteral(assign.Rhs[0], "1") {
return true
}
suffix := ""
switch assign.Tok {
case token.ADD_ASSIGN:
suffix = "++"
case token.SUB_ASSIGN:
suffix = "--"
}
j.Errorf(assign, "should replace %s with %s%s", Render(j, assign), Render(j, assign.Lhs[0]), suffix)
return true
}
for _, f := range j.Program.Files {
ast.Inspect(f, fn)
}
}
func (c *Checker) CheckErrorReturn(j *lint.Job) {
fnLoop:
for _, fn := range j.Program.InitialFunctions {
sig := fn.Type().(*types.Signature)
rets := sig.Results()
if rets == nil || rets.Len() < 2 {
continue
}
if rets.At(rets.Len()-1).Type() == types.Universe.Lookup("error").Type() {
// Last return type is error. If the function also returns
// errors in other positions, that's fine.
continue
}
for i := rets.Len() - 2; i >= 0; i-- {
if rets.At(i).Type() == types.Universe.Lookup("error").Type() {
j.Errorf(rets.At(i), "error should be returned as the last argument")
continue fnLoop
}
}
}
}
// CheckUnexportedReturn checks that exported functions on exported
// types do not return unexported types.
func (c *Checker) CheckUnexportedReturn(j *lint.Job) {
for _, fn := range j.Program.InitialFunctions {
if fn.Synthetic != "" || fn.Parent() != nil {
continue
}
if !ast.IsExported(fn.Name()) || IsInMain(j, fn) || IsInTest(j, fn) {
continue
}
sig := fn.Type().(*types.Signature)
if sig.Recv() != nil && !ast.IsExported(Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) {
continue
}
res := sig.Results()
for i := 0; i < res.Len(); i++ {
if named, ok := DereferenceR(res.At(i).Type()).(*types.Named); ok &&
!ast.IsExported(named.Obj().Name()) &&
named != types.Universe.Lookup("error").Type() {
j.Errorf(fn, "should not return unexported type")
}
}
}
}
func (c *Checker) CheckReceiverNames(j *lint.Job) {
for _, pkg := range j.Program.InitialPackages {
for _, m := range pkg.SSA.Members {
if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() {
ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
for _, sel := range ms {
fn := sel.Obj().(*types.Func)
recv := fn.Type().(*types.Signature).Recv()
if Dereference(recv.Type()) != T.Type() {
// skip embedded methods
continue
}
if recv.Name() == "self" || recv.Name() == "this" {
j.Errorf(recv, `receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"`)
}
if recv.Name() == "_" {
j.Errorf(recv, "receiver name should not be an underscore, omit the name if it is unused")
}
}
}
}
}
}
func (c *Checker) CheckReceiverNamesIdentical(j *lint.Job) {
for _, pkg := range j.Program.InitialPackages {
for _, m := range pkg.SSA.Members {
names := map[string]int{}
var firstFn *types.Func
if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() {
ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
for _, sel := range ms {
fn := sel.Obj().(*types.Func)
recv := fn.Type().(*types.Signature).Recv()
if Dereference(recv.Type()) != T.Type() {
// skip embedded methods
continue
}
if firstFn == nil {
firstFn = fn
}
if recv.Name() != "" && recv.Name() != "_" {
names[recv.Name()]++
}
}
}
if len(names) > 1 {
var seen []string
for name, count := range names {
seen = append(seen, fmt.Sprintf("%dx %q", count, name))
}
j.Errorf(firstFn, "methods on the same type should have the same receiver name (seen %s)", strings.Join(seen, ", "))
}
}
}
}
func (c *Checker) CheckContextFirstArg(j *lint.Job) {
// TODO(dh): this check doesn't apply to test helpers. Example from the stdlib:
// func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) {
fnLoop:
for _, fn := range j.Program.InitialFunctions {
if fn.Synthetic != "" || fn.Parent() != nil {
continue
}
params := fn.Signature.Params()
if params.Len() < 2 {
continue
}
if types.TypeString(params.At(0).Type(), nil) == "context.Context" {
continue
}
for i := 1; i < params.Len(); i++ {
param := params.At(i)
if types.TypeString(param.Type(), nil) == "context.Context" {
j.Errorf(param, "context.Context should be the first argument of a function")
continue fnLoop
}
}
}
}
func (c *Checker) CheckErrorStrings(j *lint.Job) {
fnNames := map[*ssa.Package]map[string]bool{}
for _, fn := range j.Program.InitialFunctions {
m := fnNames[fn.Package()]
if m == nil {
m = map[string]bool{}
fnNames[fn.Package()] = m
}
m[fn.Name()] = true
}
for _, fn := range j.Program.InitialFunctions {
if IsInTest(j, fn) {
// We don't care about malformed error messages in tests;
// they're usually for direct human consumption, not part
// of an API
continue
}
for _, block := range fn.Blocks {
instrLoop:
for _, ins := range block.Instrs {
call, ok := ins.(*ssa.Call)
if !ok {
continue
}
if !IsCallTo(call.Common(), "errors.New") && !IsCallTo(call.Common(), "fmt.Errorf") {
continue
}
k, ok := call.Common().Args[0].(*ssa.Const)
if !ok {
continue
}
s := constant.StringVal(k.Value)
if len(s) == 0 {
continue
}
switch s[len(s)-1] {
case '.', ':', '!', '\n':
j.Errorf(call, "error strings should not end with punctuation or a newline")
}
idx := strings.IndexByte(s, ' ')
if idx == -1 {
// single word error message, probably not a real
// error but something used in tests or during
// debugging
continue
}
word := s[:idx]
first, n := utf8.DecodeRuneInString(word)
if !unicode.IsUpper(first) {
continue
}
for _, c := range word[n:] {
if unicode.IsUpper(c) {
// Word is probably an initialism or
// multi-word function name
continue instrLoop
}
}
word = strings.TrimRightFunc(word, func(r rune) bool { return unicode.IsPunct(r) })
if fnNames[fn.Package()][word] {
// Word is probably the name of a function in this package
continue
}
// First word in error starts with a capital
// letter, and the word doesn't contain any other
// capitals, making it unlikely to be an
// initialism or multi-word function name.
//
// It could still be a proper noun, though.
j.Errorf(call, "error strings should not be capitalized")
}
}
}
}
func (c *Checker) CheckTimeNames(j *lint.Job) {
suffixes := []string{
"Sec", "Secs", "Seconds",
"Msec", "Msecs",
"Milli", "Millis", "Milliseconds",
"Usec", "Usecs", "Microseconds",
"MS", "Ms",
}
fn := func(T types.Type, names []*ast.Ident) {
if !IsType(T, "time.Duration") && !IsType(T, "*time.Duration") {
return
}
for _, name := range names {
for _, suffix := range suffixes {
if strings.HasSuffix(name.Name, suffix) {
j.Errorf(name, "var %s is of type %v; don't use unit-specific suffix %q", name.Name, T, suffix)
break
}
}
}
}
for _, f := range j.Program.Files {
ast.Inspect(f, func(node ast.Node) bool {
switch node := node.(type) {
case *ast.ValueSpec:
T := TypeOf(j, node.Type)
fn(T, node.Names)
case *ast.FieldList:
for _, field := range node.List {
T := TypeOf(j, field.Type)
fn(T, field.Names)
}
}
return true
})
}
}
func (c *Checker) CheckErrorVarNames(j *lint.Job) {
for _, f := range j.Program.Files {
for _, decl := range f.Decls {
gen, ok := decl.(*ast.GenDecl)
if !ok || gen.Tok != token.VAR {
continue
}
for _, spec := range gen.Specs {
spec := spec.(*ast.ValueSpec)
if len(spec.Names) != len(spec.Values) {
continue
}
for i, name := range spec.Names {
val := spec.Values[i]
if !IsCallToAST(j, val, "errors.New") && !IsCallToAST(j, val, "fmt.Errorf") {
continue
}
prefix := "err"
if name.IsExported() {
prefix = "Err"
}
if !strings.HasPrefix(name.Name, prefix) {
j.Errorf(name, "error var %s should have name of the form %sFoo", name.Name, prefix)
}
}
}
}
}
}
var httpStatusCodes = map[int]string{
100: "StatusContinue",
101: "StatusSwitchingProtocols",
102: "StatusProcessing",
200: "StatusOK",
201: "StatusCreated",
202: "StatusAccepted",
203: "StatusNonAuthoritativeInfo",
204: "StatusNoContent",
205: "StatusResetContent",
206: "StatusPartialContent",
207: "StatusMultiStatus",
208: "StatusAlreadyReported",
226: "StatusIMUsed",
300: "StatusMultipleChoices",
301: "StatusMovedPermanently",
302: "StatusFound",
303: "StatusSeeOther",
304: "StatusNotModified",
305: "StatusUseProxy",
307: "StatusTemporaryRedirect",
308: "StatusPermanentRedirect",
400: "StatusBadRequest",
401: "StatusUnauthorized",
402: "StatusPaymentRequired",
403: "StatusForbidden",
404: "StatusNotFound",
405: "StatusMethodNotAllowed",
406: "StatusNotAcceptable",
407: "StatusProxyAuthRequired",
408: "StatusRequestTimeout",
409: "StatusConflict",
410: "StatusGone",
411: "StatusLengthRequired",
412: "StatusPreconditionFailed",
413: "StatusRequestEntityTooLarge",
414: "StatusRequestURITooLong",
415: "StatusUnsupportedMediaType",
416: "StatusRequestedRangeNotSatisfiable",
417: "StatusExpectationFailed",
418: "StatusTeapot",
422: "StatusUnprocessableEntity",
423: "StatusLocked",
424: "StatusFailedDependency",
426: "StatusUpgradeRequired",
428: "StatusPreconditionRequired",
429: "StatusTooManyRequests",
431: "StatusRequestHeaderFieldsTooLarge",
451: "StatusUnavailableForLegalReasons",
500: "StatusInternalServerError",
501: "StatusNotImplemented",
502: "StatusBadGateway",
503: "StatusServiceUnavailable",
504: "StatusGatewayTimeout",
505: "StatusHTTPVersionNotSupported",
506: "StatusVariantAlsoNegotiates",
507: "StatusInsufficientStorage",
508: "StatusLoopDetected",
510: "StatusNotExtended",
511: "StatusNetworkAuthenticationRequired",
}
func (c *Checker) CheckHTTPStatusCodes(j *lint.Job) {
for _, pkg := range j.Program.InitialPackages {
whitelist := map[string]bool{}
for _, code := range pkg.Config.HTTPStatusCodeWhitelist {
whitelist[code] = true
}
fn := func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return true
}
var arg int
switch CallNameAST(j, call) {
case "net/http.Error":
arg = 2
case "net/http.Redirect":
arg = 3
case "net/http.StatusText":
arg = 0
case "net/http.RedirectHandler":
arg = 1
default:
return true
}
lit, ok := call.Args[arg].(*ast.BasicLit)
if !ok {
return true
}
if whitelist[lit.Value] {
return true
}
n, err := strconv.Atoi(lit.Value)
if err != nil {
return true
}
s, ok := httpStatusCodes[n]
if !ok {
return true
}
j.Errorf(lit, "should use constant http.%s instead of numeric literal %d", s, n)
return true
}
for _, f := range pkg.Syntax {
ast.Inspect(f, fn)
}
}
}
func (c *Checker) CheckDefaultCaseOrder(j *lint.Job) {
fn := func(node ast.Node) bool {
stmt, ok := node.(*ast.SwitchStmt)
if !ok {
return true
}
list := stmt.Body.List
for i, c := range list {
if c.(*ast.CaseClause).List == nil && i != 0 && i != len(list)-1 {
j.Errorf(c, "default case should be first or last in switch statement")
break
}
}
return true
}
for _, f := range j.Program.Files {
ast.Inspect(f, fn)
}
}
func (c *Checker) CheckYodaConditions(j *lint.Job) {
fn := func(node ast.Node) bool {
cond, ok := node.(*ast.BinaryExpr)
if !ok {
return true
}
if cond.Op != token.EQL && cond.Op != token.NEQ {
return true
}
if _, ok := cond.X.(*ast.BasicLit); !ok {
return true
}
if _, ok := cond.Y.(*ast.BasicLit); ok {
// Don't flag lit == lit conditions, just in case
return true
}
j.Errorf(cond, "don't use Yoda conditions")
return true
}
for _, f := range j.Program.Files {
ast.Inspect(f, fn)
}
}

View File

@ -0,0 +1,263 @@
// Copyright (c) 2013 The Go Authors. All rights reserved.
// Copyright (c) 2018 Dominik Honnef. All rights reserved.
package stylecheck
import (
"go/ast"
"go/token"
"strings"
"unicode"
"honnef.co/go/tools/lint"
. "honnef.co/go/tools/lint/lintdsl"
)
// knownNameExceptions is a set of names that are known to be exempt from naming checks.
// This is usually because they are constrained by having to match names in the
// standard library.
var knownNameExceptions = map[string]bool{
"LastInsertId": true, // must match database/sql
"kWh": true,
}
func (c *Checker) CheckNames(j *lint.Job) {
// A large part of this function is copied from
// github.com/golang/lint, Copyright (c) 2013 The Go Authors,
// licensed under the BSD 3-clause license.
allCaps := func(s string) bool {
for _, r := range s {
if !((r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_') {
return false
}
}
return true
}
check := func(id *ast.Ident, thing string, initialisms map[string]bool) {
if id.Name == "_" {
return
}
if knownNameExceptions[id.Name] {
return
}
// Handle two common styles from other languages that don't belong in Go.
if len(id.Name) >= 5 && allCaps(id.Name) && strings.Contains(id.Name, "_") {
j.Errorf(id, "should not use ALL_CAPS in Go names; use CamelCase instead")
return
}
should := lintName(id.Name, initialisms)
if id.Name == should {
return
}
if len(id.Name) > 2 && strings.Contains(id.Name[1:len(id.Name)-1], "_") {
j.Errorf(id, "should not use underscores in Go names; %s %s should be %s", thing, id.Name, should)
return
}
j.Errorf(id, "%s %s should be %s", thing, id.Name, should)
}
checkList := func(fl *ast.FieldList, thing string, initialisms map[string]bool) {
if fl == nil {
return
}
for _, f := range fl.List {
for _, id := range f.Names {
check(id, thing, initialisms)
}
}
}
for _, pkg := range j.Program.InitialPackages {
initialisms := make(map[string]bool, len(pkg.Config.Initialisms))
for _, word := range pkg.Config.Initialisms {
initialisms[word] = true
}
for _, f := range pkg.Syntax {
// Package names need slightly different handling than other names.
if !strings.HasSuffix(f.Name.Name, "_test") && strings.Contains(f.Name.Name, "_") {
j.Errorf(f, "should not use underscores in package names")
}
if strings.IndexFunc(f.Name.Name, unicode.IsUpper) != -1 {
j.Errorf(f, "should not use MixedCaps in package name; %s should be %s", f.Name.Name, strings.ToLower(f.Name.Name))
}
ast.Inspect(f, func(node ast.Node) bool {
switch v := node.(type) {
case *ast.AssignStmt:
if v.Tok != token.DEFINE {
return true
}
for _, exp := range v.Lhs {
if id, ok := exp.(*ast.Ident); ok {
check(id, "var", initialisms)
}
}
case *ast.FuncDecl:
// Functions with no body are defined elsewhere (in
// assembly, or via go:linkname). These are likely to
// be something very low level (such as the runtime),
// where our rules don't apply.
if v.Body == nil {
return true
}
if IsInTest(j, v) && (strings.HasPrefix(v.Name.Name, "Example") || strings.HasPrefix(v.Name.Name, "Test") || strings.HasPrefix(v.Name.Name, "Benchmark")) {
return true
}
thing := "func"
if v.Recv != nil {
thing = "method"
}
if !isTechnicallyExported(v) {
check(v.Name, thing, initialisms)
}
checkList(v.Type.Params, thing+" parameter", initialisms)
checkList(v.Type.Results, thing+" result", initialisms)
case *ast.GenDecl:
if v.Tok == token.IMPORT {
return true
}
var thing string
switch v.Tok {
case token.CONST:
thing = "const"
case token.TYPE:
thing = "type"
case token.VAR:
thing = "var"
}
for _, spec := range v.Specs {
switch s := spec.(type) {
case *ast.TypeSpec:
check(s.Name, thing, initialisms)
case *ast.ValueSpec:
for _, id := range s.Names {
check(id, thing, initialisms)
}
}
}
case *ast.InterfaceType:
// Do not check interface method names.
// They are often constrainted by the method names of concrete types.
for _, x := range v.Methods.List {
ft, ok := x.Type.(*ast.FuncType)
if !ok { // might be an embedded interface name
continue
}
checkList(ft.Params, "interface method parameter", initialisms)
checkList(ft.Results, "interface method result", initialisms)
}
case *ast.RangeStmt:
if v.Tok == token.ASSIGN {
return true
}
if id, ok := v.Key.(*ast.Ident); ok {
check(id, "range var", initialisms)
}
if id, ok := v.Value.(*ast.Ident); ok {
check(id, "range var", initialisms)
}
case *ast.StructType:
for _, f := range v.Fields.List {
for _, id := range f.Names {
check(id, "struct field", initialisms)
}
}
}
return true
})
}
}
}
// lintName returns a different name if it should be different.
func lintName(name string, initialisms map[string]bool) (should string) {
// A large part of this function is copied from
// github.com/golang/lint, Copyright (c) 2013 The Go Authors,
// licensed under the BSD 3-clause license.
// Fast path for simple cases: "_" and all lowercase.
if name == "_" {
return name
}
if strings.IndexFunc(name, func(r rune) bool { return !unicode.IsLower(r) }) == -1 {
return name
}
// Split camelCase at any lower->upper transition, and split on underscores.
// Check each word for common initialisms.
runes := []rune(name)
w, i := 0, 0 // index of start of word, scan
for i+1 <= len(runes) {
eow := false // whether we hit the end of a word
if i+1 == len(runes) {
eow = true
} else if runes[i+1] == '_' && i+1 != len(runes)-1 {
// underscore; shift the remainder forward over any run of underscores
eow = true
n := 1
for i+n+1 < len(runes) && runes[i+n+1] == '_' {
n++
}
// Leave at most one underscore if the underscore is between two digits
if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) {
n--
}
copy(runes[i+1:], runes[i+n+1:])
runes = runes[:len(runes)-n]
} else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) {
// lower->non-lower
eow = true
}
i++
if !eow {
continue
}
// [w,i) is a word.
word := string(runes[w:i])
if u := strings.ToUpper(word); initialisms[u] {
// Keep consistent case, which is lowercase only at the start.
if w == 0 && unicode.IsLower(runes[w]) {
u = strings.ToLower(u)
}
// All the common initialisms are ASCII,
// so we can replace the bytes exactly.
// TODO(dh): this won't be true once we allow custom initialisms
copy(runes[w:], []rune(u))
} else if w > 0 && strings.ToLower(word) == word {
// already all lowercase, and not the first word, so uppercase the first character.
runes[w] = unicode.ToUpper(runes[w])
}
w = i
}
return string(runes)
}
func isTechnicallyExported(f *ast.FuncDecl) bool {
if f.Recv != nil || f.Doc == nil {
return false
}
const export = "//export "
const linkname = "//go:linkname "
for _, c := range f.Doc.List {
if strings.HasPrefix(c.Text, export) && len(c.Text) == len(export)+len(f.Name.Name) && c.Text[len(export):] == f.Name.Name {
return true
}
if strings.HasPrefix(c.Text, linkname) {
return true
}
}
return false
}

View File

@ -0,0 +1,79 @@
package unused
import "go/types"
// lookupMethod returns the index of and method with matching package and name, or (-1, nil).
func lookupMethod(T *types.Interface, pkg *types.Package, name string) (int, *types.Func) {
if name != "_" {
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
if sameId(m, pkg, name) {
return i, m
}
}
}
return -1, nil
}
func sameId(obj types.Object, pkg *types.Package, name string) bool {
// spec:
// "Two identifiers are different if they are spelled differently,
// or if they appear in different packages and are not exported.
// Otherwise, they are the same."
if name != obj.Name() {
return false
}
// obj.Name == name
if obj.Exported() {
return true
}
// not exported, so packages must be the same (pkg == nil for
// fields in Universe scope; this can only happen for types
// introduced via Eval)
if pkg == nil || obj.Pkg() == nil {
return pkg == obj.Pkg()
}
// pkg != nil && obj.pkg != nil
return pkg.Path() == obj.Pkg().Path()
}
func (c *Checker) implements(V types.Type, T *types.Interface) bool {
// fast path for common case
if T.Empty() {
return true
}
if ityp, _ := V.Underlying().(*types.Interface); ityp != nil {
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
_, obj := lookupMethod(ityp, m.Pkg(), m.Name())
switch {
case obj == nil:
return false
case !types.Identical(obj.Type(), m.Type()):
return false
}
}
return true
}
// A concrete type implements T if it implements all methods of T.
ms := c.msCache.MethodSet(V)
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
sel := ms.Lookup(m.Pkg(), m.Name())
if sel == nil {
return false
}
f, _ := sel.Obj().(*types.Func)
if f == nil {
return false
}
if !types.Identical(f.Type(), m.Type()) {
return false
}
}
return true
}

View File

@ -12,7 +12,7 @@ import (
"honnef.co/go/tools/lint"
. "honnef.co/go/tools/lint/lintdsl"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/types/typeutil"
)
@ -31,9 +31,9 @@ func (*LintChecker) Name() string { return "unused" }
func (*LintChecker) Prefix() string { return "U" }
func (l *LintChecker) Init(*lint.Program) {}
func (l *LintChecker) Funcs() map[string]lint.Func {
return map[string]lint.Func{
"U1000": l.Lint,
func (l *LintChecker) Checks() []lint.Check {
return []lint.Check{
{ID: "U1000", FilterGenerated: true, Fn: l.Lint},
}
}
@ -57,7 +57,7 @@ func typString(obj types.Object) string {
}
func (l *LintChecker) Lint(j *lint.Job) {
unused := l.c.Check(j.Program.Prog)
unused := l.c.Check(j.Program)
for _, u := range unused {
name := u.Obj.Name()
if sig, ok := u.Obj.Type().(*types.Signature); ok && sig.Recv() != nil {
@ -158,7 +158,7 @@ type Checker struct {
graph *graph
msCache typeutil.MethodSetCache
lprog *loader.Program
prog *lint.Program
topmostCache map[*types.Scope]*types.Scope
interfaces []*types.Interface
}
@ -199,13 +199,13 @@ func (e Error) Error() string {
return fmt.Sprintf("errors in %d packages", len(e.Errors))
}
func (c *Checker) Check(lprog *loader.Program) []Unused {
func (c *Checker) Check(prog *lint.Program) []Unused {
var unused []Unused
c.lprog = lprog
c.prog = prog
if c.WholeProgram {
c.findExportedInterfaces()
}
for _, pkg := range c.lprog.InitialPackages() {
for _, pkg := range prog.InitialPackages {
c.processDefs(pkg)
c.processUses(pkg)
c.processTypes(pkg)
@ -231,6 +231,7 @@ func (c *Checker) Check(lprog *loader.Program) []Unused {
}
markNodesUsed(roots)
c.markNodesQuiet()
c.deduplicate()
if c.Debug != nil {
c.printDebugGraph(c.Debug)
@ -246,8 +247,8 @@ func (c *Checker) Check(lprog *loader.Program) []Unused {
}
found := false
if !false {
for _, pkg := range c.lprog.InitialPackages() {
if pkg.Pkg == obj.Pkg() {
for _, pkg := range prog.InitialPackages {
if pkg.Types == obj.Pkg() {
found = true
break
}
@ -257,25 +258,14 @@ func (c *Checker) Check(lprog *loader.Program) []Unused {
continue
}
pos := c.lprog.Fset.Position(obj.Pos())
pos := c.prog.Fset().Position(obj.Pos())
if pos.Filename == "" || filepath.Base(pos.Filename) == "C" {
continue
}
generated := false
for _, file := range c.lprog.Package(obj.Pkg().Path()).Files {
if c.lprog.Fset.Position(file.Pos()).Filename != pos.Filename {
continue
}
if len(file.Comments) > 0 {
generated = isGenerated(file.Comments[0].Text())
}
break
}
if generated {
continue
}
unused = append(unused, Unused{Obj: obj, Position: pos})
}
return unused
}
@ -325,16 +315,24 @@ func (c *Checker) useNoCopyFields(typ types.Type) {
}
}
func (c *Checker) useExportedFields(typ types.Type) {
func (c *Checker) useExportedFields(typ types.Type, by types.Type) bool {
any := false
if st, ok := typ.Underlying().(*types.Struct); ok {
n := st.NumFields()
for i := 0; i < n; i++ {
field := st.Field(i)
if field.Anonymous() {
if c.useExportedFields(field.Type(), typ) {
c.graph.markUsedBy(field, typ)
}
}
if field.Exported() {
c.graph.markUsedBy(field, typ)
c.graph.markUsedBy(field, by)
any = true
}
}
}
return any
}
func (c *Checker) useExportedMethods(typ types.Type) {
@ -370,8 +368,8 @@ func (c *Checker) useExportedMethods(typ types.Type) {
}
}
func (c *Checker) processDefs(pkg *loader.PackageInfo) {
for _, obj := range pkg.Defs {
func (c *Checker) processDefs(pkg *lint.Pkg) {
for _, obj := range pkg.TypesInfo.Defs {
if obj == nil {
continue
}
@ -392,7 +390,7 @@ func (c *Checker) processDefs(pkg *loader.PackageInfo) {
// mark them used if an instance of the type was
// accessible via an interface value.
if !c.WholeProgram || c.ConsiderReflection {
c.useExportedFields(obj.Type())
c.useExportedFields(obj.Type(), obj.Type())
}
// TODO(dh): Traditionally we have not marked all exported
@ -420,8 +418,8 @@ func (c *Checker) processDefs(pkg *loader.PackageInfo) {
if obj.Name() == "_" {
node := c.graph.getNode(obj)
node.quiet = true
scope := c.topmostScope(pkg.Pkg.Scope().Innermost(obj.Pos()), pkg.Pkg)
if scope == pkg.Pkg.Scope() {
scope := c.topmostScope(pkg.Types.Scope().Innermost(obj.Pos()), pkg.Types)
if scope == pkg.Types.Scope() {
c.graph.roots = append(c.graph.roots, node)
} else {
c.graph.markUsedBy(obj, scope)
@ -471,15 +469,15 @@ func (c *Checker) processDefs(pkg *loader.PackageInfo) {
}
}
func (c *Checker) processUses(pkg *loader.PackageInfo) {
for ident, usedObj := range pkg.Uses {
func (c *Checker) processUses(pkg *lint.Pkg) {
for ident, usedObj := range pkg.TypesInfo.Uses {
if _, ok := usedObj.(*types.PkgName); ok {
continue
}
pos := ident.Pos()
scope := pkg.Pkg.Scope().Innermost(pos)
scope = c.topmostScope(scope, pkg.Pkg)
if scope != pkg.Pkg.Scope() {
scope := pkg.Types.Scope().Innermost(pos)
scope = c.topmostScope(scope, pkg.Types)
if scope != pkg.Types.Scope() {
c.graph.markUsedBy(usedObj, scope)
}
@ -492,17 +490,17 @@ func (c *Checker) processUses(pkg *loader.PackageInfo) {
func (c *Checker) findExportedInterfaces() {
c.interfaces = []*types.Interface{types.Universe.Lookup("error").Type().(*types.Named).Underlying().(*types.Interface)}
var pkgs []*loader.PackageInfo
var pkgs []*packages.Package
if c.WholeProgram {
for _, pkg := range c.lprog.AllPackages {
pkgs = append(pkgs, pkg)
}
pkgs = append(pkgs, c.prog.AllPackages...)
} else {
pkgs = c.lprog.InitialPackages()
for _, pkg := range c.prog.InitialPackages {
pkgs = append(pkgs, pkg.Package)
}
}
for _, pkg := range pkgs {
for _, tv := range pkg.Types {
for _, tv := range pkg.TypesInfo.Types {
iface, ok := tv.Type.(*types.Interface)
if !ok {
continue
@ -515,10 +513,10 @@ func (c *Checker) findExportedInterfaces() {
}
}
func (c *Checker) processTypes(pkg *loader.PackageInfo) {
func (c *Checker) processTypes(pkg *lint.Pkg) {
named := map[*types.Named]*types.Pointer{}
var interfaces []*types.Interface
for _, tv := range pkg.Types {
for _, tv := range pkg.TypesInfo.Types {
if typ, ok := tv.Type.(interface {
Elem() types.Type
}); ok {
@ -536,8 +534,8 @@ func (c *Checker) processTypes(pkg *loader.PackageInfo) {
}
case *types.Struct:
c.useNoCopyFields(obj)
if pkg.Pkg.Name() != "main" && !c.WholeProgram {
c.useExportedFields(obj)
if pkg.Types.Name() != "main" && !c.WholeProgram {
c.useExportedFields(obj, obj)
}
}
}
@ -547,15 +545,30 @@ func (c *Checker) processTypes(pkg *loader.PackageInfo) {
//
// TODO(dh): For normal operations, that's the best we can do, as
// we have no idea what external users will do with our types. In
// whole-program mode, we could be more conservative, in two ways:
// whole-program mode, we could be more precise, in two ways:
// 1) Only consider interfaces if a type has been assigned to one
// 2) Use SSA and flow analysis and determine the exact set of
// interfaces that is relevant.
fn := func(iface *types.Interface) {
for i := 0; i < iface.NumEmbeddeds(); i++ {
c.graph.markUsedBy(iface.Embedded(i), iface)
}
namedLoop:
for obj, objPtr := range named {
if !types.Implements(obj, iface) && !types.Implements(objPtr, iface) {
continue
switch obj.Underlying().(type) {
case *types.Interface:
// pointers to interfaces have no methods, only checking non-pointer
if !c.implements(obj, iface) {
continue namedLoop
}
default:
// pointer receivers include the method set of non-pointer receivers,
// only checking pointer
if !c.implements(objPtr, iface) {
continue namedLoop
}
}
ifaceMethods := make(map[string]struct{}, iface.NumMethods())
n := iface.NumMethods()
for i := 0; i < n; i++ {
@ -591,23 +604,23 @@ func (c *Checker) processTypes(pkg *loader.PackageInfo) {
}
}
func (c *Checker) processSelections(pkg *loader.PackageInfo) {
func (c *Checker) processSelections(pkg *lint.Pkg) {
fn := func(expr *ast.SelectorExpr, sel *types.Selection, offset int) {
scope := pkg.Pkg.Scope().Innermost(expr.Pos())
c.graph.markUsedBy(expr.X, c.topmostScope(scope, pkg.Pkg))
c.graph.markUsedBy(sel.Obj(), expr.X)
scope := pkg.Types.Scope().Innermost(expr.Pos())
c.graph.markUsedBy(sel, c.topmostScope(scope, pkg.Types))
c.graph.markUsedBy(sel.Obj(), sel)
if len(sel.Index()) > 1 {
typ := sel.Recv()
indices := sel.Index()
for _, idx := range indices[:len(indices)-offset] {
obj := getField(typ, idx)
typ = obj.Type()
c.graph.markUsedBy(obj, expr.X)
c.graph.markUsedBy(obj, sel)
}
}
}
for expr, sel := range pkg.Selections {
for expr, sel := range pkg.TypesInfo.Selections {
switch sel.Kind() {
case types.FieldVal:
fn(expr, sel, 0)
@ -625,9 +638,9 @@ func dereferenceType(typ types.Type) types.Type {
}
// processConversion marks fields as used if they're part of a type conversion.
func (c *Checker) processConversion(pkg *loader.PackageInfo, node ast.Node) {
func (c *Checker) processConversion(pkg *lint.Pkg, node ast.Node) {
if node, ok := node.(*ast.CallExpr); ok {
callTyp := pkg.TypeOf(node.Fun)
callTyp := pkg.TypesInfo.TypeOf(node.Fun)
var typDst *types.Struct
var ok bool
switch typ := callTyp.(type) {
@ -642,7 +655,7 @@ func (c *Checker) processConversion(pkg *loader.PackageInfo, node ast.Node) {
return
}
if typ, ok := pkg.TypeOf(node.Args[0]).(*types.Basic); ok && typ.Kind() == types.UnsafePointer {
if typ, ok := pkg.TypesInfo.TypeOf(node.Args[0]).(*types.Basic); ok && typ.Kind() == types.UnsafePointer {
// This is an unsafe conversion. Assume that all the
// fields are relevant (they are, because of memory
// layout)
@ -653,7 +666,7 @@ func (c *Checker) processConversion(pkg *loader.PackageInfo, node ast.Node) {
return
}
typSrc, ok := dereferenceType(pkg.TypeOf(node.Args[0])).Underlying().(*types.Struct)
typSrc, ok := dereferenceType(pkg.TypesInfo.TypeOf(node.Args[0])).Underlying().(*types.Struct)
if !ok {
return
}
@ -683,10 +696,10 @@ func (c *Checker) processConversion(pkg *loader.PackageInfo, node ast.Node) {
// processCompositeLiteral marks fields as used if the struct is used
// in a composite literal.
func (c *Checker) processCompositeLiteral(pkg *loader.PackageInfo, node ast.Node) {
func (c *Checker) processCompositeLiteral(pkg *lint.Pkg, node ast.Node) {
// XXX how does this actually work? wouldn't it match t{}?
if node, ok := node.(*ast.CompositeLit); ok {
typ := pkg.TypeOf(node)
typ := pkg.TypesInfo.TypeOf(node)
if _, ok := typ.(*types.Named); ok {
typ = typ.Underlying()
}
@ -702,7 +715,7 @@ func (c *Checker) processCompositeLiteral(pkg *loader.PackageInfo, node ast.Node
// processCgoExported marks functions as used if they're being
// exported to cgo.
func (c *Checker) processCgoExported(pkg *loader.PackageInfo, node ast.Node) {
func (c *Checker) processCgoExported(pkg *lint.Pkg, node ast.Node) {
if node, ok := node.(*ast.FuncDecl); ok {
if node.Doc == nil {
return
@ -711,13 +724,13 @@ func (c *Checker) processCgoExported(pkg *loader.PackageInfo, node ast.Node) {
if !strings.HasPrefix(cmt.Text, "//go:cgo_export_") {
return
}
obj := pkg.ObjectOf(node.Name)
obj := pkg.TypesInfo.ObjectOf(node.Name)
c.graph.roots = append(c.graph.roots, c.graph.getNode(obj))
}
}
}
func (c *Checker) processVariableDeclaration(pkg *loader.PackageInfo, node ast.Node) {
func (c *Checker) processVariableDeclaration(pkg *lint.Pkg, node ast.Node) {
if decl, ok := node.(*ast.GenDecl); ok {
for _, spec := range decl.Specs {
spec, ok := spec.(*ast.ValueSpec)
@ -731,11 +744,11 @@ func (c *Checker) processVariableDeclaration(pkg *loader.PackageInfo, node ast.N
value := spec.Values[i]
fn := func(node ast.Node) bool {
if node3, ok := node.(*ast.Ident); ok {
obj := pkg.ObjectOf(node3)
obj := pkg.TypesInfo.ObjectOf(node3)
if _, ok := obj.(*types.PkgName); ok {
return true
}
c.graph.markUsedBy(obj, pkg.ObjectOf(name))
c.graph.markUsedBy(obj, pkg.TypesInfo.ObjectOf(name))
}
return true
}
@ -745,17 +758,17 @@ func (c *Checker) processVariableDeclaration(pkg *loader.PackageInfo, node ast.N
}
}
func (c *Checker) processArrayConstants(pkg *loader.PackageInfo, node ast.Node) {
func (c *Checker) processArrayConstants(pkg *lint.Pkg, node ast.Node) {
if decl, ok := node.(*ast.ArrayType); ok {
ident, ok := decl.Len.(*ast.Ident)
if !ok {
return
}
c.graph.markUsedBy(pkg.ObjectOf(ident), pkg.TypeOf(decl))
c.graph.markUsedBy(pkg.TypesInfo.ObjectOf(ident), pkg.TypesInfo.TypeOf(decl))
}
}
func (c *Checker) processKnownReflectMethodCallers(pkg *loader.PackageInfo, node ast.Node) {
func (c *Checker) processKnownReflectMethodCallers(pkg *lint.Pkg, node ast.Node) {
call, ok := node.(*ast.CallExpr)
if !ok {
return
@ -764,12 +777,12 @@ func (c *Checker) processKnownReflectMethodCallers(pkg *loader.PackageInfo, node
if !ok {
return
}
if !IsType(pkg.TypeOf(sel.X), "*net/rpc.Server") {
if !IsType(pkg.TypesInfo.TypeOf(sel.X), "*net/rpc.Server") {
x, ok := sel.X.(*ast.Ident)
if !ok {
return
}
pkgname, ok := pkg.ObjectOf(x).(*types.PkgName)
pkgname, ok := pkg.TypesInfo.ObjectOf(x).(*types.PkgName)
if !ok {
return
}
@ -791,14 +804,14 @@ func (c *Checker) processKnownReflectMethodCallers(pkg *loader.PackageInfo, node
}
arg = call.Args[1]
}
typ := pkg.TypeOf(arg)
typ := pkg.TypesInfo.TypeOf(arg)
ms := types.NewMethodSet(typ)
for i := 0; i < ms.Len(); i++ {
c.graph.markUsedBy(ms.At(i).Obj(), typ)
}
}
func (c *Checker) processAST(pkg *loader.PackageInfo) {
func (c *Checker) processAST(pkg *lint.Pkg) {
fn := func(node ast.Node) bool {
c.processConversion(pkg, node)
c.processKnownReflectMethodCallers(pkg, node)
@ -808,7 +821,7 @@ func (c *Checker) processAST(pkg *loader.PackageInfo) {
c.processArrayConstants(pkg, node)
return true
}
for _, file := range pkg.Files {
for _, file := range pkg.Syntax {
ast.Inspect(file, fn)
}
}
@ -914,7 +927,7 @@ func (c *Checker) isRoot(obj types.Object) bool {
return true
}
if obj.Exported() {
f := c.lprog.Fset.Position(obj.Pos()).Filename
f := c.prog.Fset().Position(obj.Pos()).Filename
if strings.HasSuffix(f, "_test.go") {
return strings.HasPrefix(obj.Name(), "Test") ||
strings.HasPrefix(obj.Name(), "Benchmark") ||
@ -939,6 +952,33 @@ func markNodesUsed(nodes map[*graphNode]struct{}) {
}
}
// deduplicate merges objects based on their positions. This is done
// to work around packages existing multiple times in go/packages.
func (c *Checker) deduplicate() {
m := map[token.Position]struct{ used, quiet bool }{}
for _, node := range c.graph.nodes {
obj, ok := node.obj.(types.Object)
if !ok {
continue
}
pos := c.prog.Fset().Position(obj.Pos())
m[pos] = struct{ used, quiet bool }{
m[pos].used || node.used,
m[pos].quiet || node.quiet,
}
}
for _, node := range c.graph.nodes {
obj, ok := node.obj.(types.Object)
if !ok {
continue
}
pos := c.prog.Fset().Position(obj.Pos())
node.used = m[pos].used
node.quiet = m[pos].quiet
}
}
func (c *Checker) markNodesQuiet() {
for _, node := range c.graph.nodes {
if node.used {
@ -1058,8 +1098,3 @@ func (c *Checker) printDebugGraph(w io.Writer) {
}
fmt.Fprintln(w, "}")
}
func isGenerated(comment string) bool {
return strings.Contains(comment, "Code generated by") ||
strings.Contains(comment, "DO NOT EDIT")
}