Fix a false-positive from 'unused' (#585)

This false-positive is not present in the upstream stand-alone 'unused'
2019.1.1 program that golangci-lint uses.

pkg/lint.ContextLoader.filterPackages() did two things:
 1. It removed synthetic "testmain" packages (packages with .Name=="main"
    and .PkgPath ending with ".test")
 2. It removed pruned subsumed copies of packages; if a package with files
    "a.go" and "a_test.go", it results in packages.Load giving us two
    packages:
      - ID=".../a" GoFiles=[a.go]
      - ID=".../a [.../a.test]" GoFiles=[a.go a_test.go]
    The first package is subsumed in the second, and leaving it around
    results in duplicated work, and confuses the 'deadcode' linter.

However, the 'unused' linter relies on both the ".../a" and
".../a [.../a.test]" packages being present.  Pruning them causes it to
panic in some situations, which lead to this workaround:
af6baa5dc1

While that workaround got it to not panic, it causes incorrect results.

So, split filterPackages() in to two functions: filterTestMainPackages()
and filterDuplicatePackages().  The linter.Context.Packages list only
gets filterTestMainPackages() called on it, while linter.Context.Program
and linter.Context.SSAProgram get both filters applied.

With the source of the panic fixed, roll back a few now-unnecessary
commits in go-tools.
This commit is contained in:
Luke Shumaker 2019-09-09 09:10:49 -04:00 committed by Isaev Denis
parent 9ae08e9389
commit e87a1cfb83
12 changed files with 77 additions and 42 deletions

2
go.mod
View File

@ -14,7 +14,7 @@ require (
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98

4
go.sum
View File

@ -59,8 +59,8 @@ github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 h1:i2jIkQFb8RG45
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw= github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 h1:9rtVlONXLF1rJZzvLt4tfOXtnAFUEhxCJ64Ibzj6ECo= github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c h1:/7detzz5stiXWPzkTlPTzkBEIIE4WGpppBJYjKqBiPI=
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8= github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8= github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8=

View File

@ -237,10 +237,11 @@ func (m megacheck) runMegacheck(workingPkgs []*packages.Package, checkExportedUn
// TODO: support Ignores option // TODO: support Ignores option
} }
return runMegacheckCheckers(checkers, opts, workingPkgs) return runMegacheckCheckers(checkers, workingPkgs, opts)
} }
// parseIgnore is a copy from megacheck code just to not fork megacheck // parseIgnore is a copy from megacheck honnef.co/go/tools/lint/lintutil.parseIgnore
// just to not fork megacheck.
func parseIgnore(s string) ([]lint.Ignore, error) { func parseIgnore(s string) ([]lint.Ignore, error) {
var out []lint.Ignore var out []lint.Ignore
if s == "" { if s == "" {
@ -258,17 +259,28 @@ func parseIgnore(s string) ([]lint.Ignore, error) {
return out, nil return out, nil
} }
func runMegacheckCheckers(cs []lint.Checker, opt *lintutil.Options, workingPkgs []*packages.Package) ([]lint.Problem, error) { // runMegacheckCheckers is like megacheck honnef.co/go/tools/lint/lintutil.Lint,
// but takes a list of already-parsed packages instead of a list of
// package-paths to parse.
func runMegacheckCheckers(cs []lint.Checker, workingPkgs []*packages.Package, opt *lintutil.Options) ([]lint.Problem, error) {
stats := lint.PerfStats{ stats := lint.PerfStats{
CheckerInits: map[string]time.Duration{}, CheckerInits: map[string]time.Duration{},
} }
if opt == nil {
opt = &lintutil.Options{}
}
ignores, err := parseIgnore(opt.Ignores) ignores, err := parseIgnore(opt.Ignores)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// package-parsing elided here
stats.PackageLoading = 0
var problems []lint.Problem var problems []lint.Problem
// populating 'problems' with parser-problems elided here
if len(workingPkgs) == 0 { if len(workingPkgs) == 0 {
return problems, nil return problems, nil
} }

View File

@ -88,11 +88,6 @@ func (cl ContextLoader) makeFakeLoaderPackageInfo(pkg *packages.Package) *loader
} }
} }
func shouldSkipPkg(pkg *packages.Package) bool {
// it's an implicit testmain package
return pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test")
}
func (cl ContextLoader) makeFakeLoaderProgram(pkgs []*packages.Package) *loader.Program { func (cl ContextLoader) makeFakeLoaderProgram(pkgs []*packages.Package) *loader.Program {
var createdPkgs []*loader.PackageInfo var createdPkgs []*loader.PackageInfo
for _, pkg := range pkgs { for _, pkg := range pkgs {
@ -296,7 +291,7 @@ func (cl ContextLoader) loadPackages(ctx context.Context, loadMode packages.Load
return nil, err return nil, err
} }
return cl.filterPackages(pkgs), nil return cl.filterTestMainPackages(pkgs), nil
} }
func (cl ContextLoader) tryParseTestPackage(pkg *packages.Package) (name, testName string, isTest bool) { func (cl ContextLoader) tryParseTestPackage(pkg *packages.Package) (name, testName string, isTest bool) {
@ -308,7 +303,22 @@ func (cl ContextLoader) tryParseTestPackage(pkg *packages.Package) (name, testNa
return matches[1], matches[2], true return matches[1], matches[2], true
} }
func (cl ContextLoader) filterPackages(pkgs []*packages.Package) []*packages.Package { func (cl ContextLoader) filterTestMainPackages(pkgs []*packages.Package) []*packages.Package {
var retPkgs []*packages.Package
for _, pkg := range pkgs {
if pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test") {
// it's an implicit testmain package
cl.debugf("skip pkg ID=%s", pkg.ID)
continue
}
retPkgs = append(retPkgs, pkg)
}
return retPkgs
}
func (cl ContextLoader) filterDuplicatePackages(pkgs []*packages.Package) []*packages.Package {
packagesWithTests := map[string]bool{} packagesWithTests := map[string]bool{}
for _, pkg := range pkgs { for _, pkg := range pkgs {
name, _, isTest := cl.tryParseTestPackage(pkg) name, _, isTest := cl.tryParseTestPackage(pkg)
@ -322,11 +332,6 @@ func (cl ContextLoader) filterPackages(pkgs []*packages.Package) []*packages.Pac
var retPkgs []*packages.Package var retPkgs []*packages.Package
for _, pkg := range pkgs { for _, pkg := range pkgs {
if shouldSkipPkg(pkg) {
cl.debugf("skip pkg ID=%s", pkg.ID)
continue
}
_, _, isTest := cl.tryParseTestPackage(pkg) _, _, isTest := cl.tryParseTestPackage(pkg)
if !isTest && packagesWithTests[pkg.PkgPath] { if !isTest && packagesWithTests[pkg.PkgPath] {
// If tests loading is enabled, // If tests loading is enabled,
@ -353,22 +358,24 @@ func (cl ContextLoader) Load(ctx context.Context, linters []*linter.Config) (*li
return nil, err return nil, err
} }
if len(pkgs) == 0 { deduplicatedPkgs := cl.filterDuplicatePackages(pkgs)
if len(deduplicatedPkgs) == 0 {
return nil, exitcodes.ErrNoGoFiles return nil, exitcodes.ErrNoGoFiles
} }
var prog *loader.Program var prog *loader.Program
if loadMode&packages.NeedTypes != 0 { if loadMode&packages.NeedTypes != 0 {
prog = cl.makeFakeLoaderProgram(pkgs) prog = cl.makeFakeLoaderProgram(deduplicatedPkgs)
} }
var ssaProg *ssa.Program var ssaProg *ssa.Program
if loadMode&packages.NeedDeps != 0 { if loadMode&packages.NeedDeps != 0 {
ssaProg = cl.buildSSAProgram(pkgs) ssaProg = cl.buildSSAProgram(deduplicatedPkgs)
} }
astLog := cl.log.Child("astcache") astLog := cl.log.Child("astcache")
astCache, err := astcache.LoadFromPackages(pkgs, astLog) astCache, err := astcache.LoadFromPackages(deduplicatedPkgs, astLog)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -117,6 +117,10 @@ func TestIdentifierUsedOnlyInTests(t *testing.T) {
testshared.NewLintRunner(t).Run("--no-config", "--disable-all", "-Eunused", getTestDataDir("used_only_in_tests")).ExpectNoIssues() testshared.NewLintRunner(t).Run("--no-config", "--disable-all", "-Eunused", getTestDataDir("used_only_in_tests")).ExpectNoIssues()
} }
func TestUnusedCheckExported(t *testing.T) {
testshared.NewLintRunner(t).Run("-c", "testdata_etc/unused_exported/golangci.yml", "testdata_etc/unused_exported/...").ExpectNoIssues()
}
func TestConfigFileIsDetected(t *testing.T) { func TestConfigFileIsDetected(t *testing.T) {
checkGotConfig := func(r *testshared.RunResult) { checkGotConfig := func(r *testshared.RunResult) {
r.ExpectExitCode(exitcodes.Success). r.ExpectExitCode(exitcodes.Success).

View File

@ -0,0 +1,7 @@
linters:
disable-all: true
enable:
- unused
linters-settings:
unused:
check-exported: true

View File

@ -0,0 +1 @@
package lib

View File

@ -0,0 +1,13 @@
package lib
import (
"fmt"
)
func PublicFunc() {
privateFunc()
}
func privateFunc() {
fmt.Println("side effect")
}

View File

@ -0,0 +1,9 @@
package main
import (
"github.com/golangci/golangci-lint/test/testdata_etc/unused_exported/lib"
)
func main() {
lib.PublicFunc()
}

View File

@ -9,17 +9,16 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"runtime/debug"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode" "unicode"
"golang.org/x/tools/go/packages"
"github.com/golangci/go-tools/config" "github.com/golangci/go-tools/config"
"github.com/golangci/go-tools/ssa" "github.com/golangci/go-tools/ssa"
"github.com/golangci/go-tools/ssa/ssautil" "github.com/golangci/go-tools/ssa/ssautil"
"golang.org/x/tools/go/packages"
) )
type Job struct { type Job struct {
@ -30,7 +29,6 @@ type Job struct {
problems []Problem problems []Problem
duration time.Duration duration time.Duration
panicErr error
} }
type Ignore interface { type Ignore interface {
@ -453,11 +451,6 @@ func (l *Linter) Lint(initial []*packages.Package, stats *PerfStats) []Problem {
for _, j := range jobs { for _, j := range jobs {
wg.Add(1) wg.Add(1)
go func(j *Job) { go func(j *Job) {
defer func() {
if panicErr := recover(); panicErr != nil {
j.panicErr = fmt.Errorf("panic: %s: %s", panicErr, string(debug.Stack()))
}
}()
defer wg.Done() defer wg.Done()
sem <- struct{}{} sem <- struct{}{}
defer func() { <-sem }() defer func() { <-sem }()
@ -473,10 +466,6 @@ func (l *Linter) Lint(initial []*packages.Package, stats *PerfStats) []Problem {
wg.Wait() wg.Wait()
for _, j := range jobs { for _, j := range jobs {
if j.panicErr != nil {
panic(j.panicErr)
}
if stats != nil { if stats != nil {
stats.Jobs = append(stats.Jobs, JobStat{j.check.ID, j.duration}) stats.Jobs = append(stats.Jobs, JobStat{j.check.ID, j.duration})
} }

View File

@ -252,9 +252,6 @@ func (c *Checker) CheckUnexportedReturn(j *lint.Job) {
func (c *Checker) CheckReceiverNames(j *lint.Job) { func (c *Checker) CheckReceiverNames(j *lint.Job) {
for _, pkg := range j.Program.InitialPackages { for _, pkg := range j.Program.InitialPackages {
if pkg.SSA == nil {
continue
}
for _, m := range pkg.SSA.Members { for _, m := range pkg.SSA.Members {
if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() { if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() {
ms := typeutil.IntuitiveMethodSet(T.Type(), nil) ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
@ -279,10 +276,6 @@ func (c *Checker) CheckReceiverNames(j *lint.Job) {
func (c *Checker) CheckReceiverNamesIdentical(j *lint.Job) { func (c *Checker) CheckReceiverNamesIdentical(j *lint.Job) {
for _, pkg := range j.Program.InitialPackages { for _, pkg := range j.Program.InitialPackages {
if pkg.SSA == nil {
continue
}
for _, m := range pkg.SSA.Members { for _, m := range pkg.SSA.Members {
names := map[string]int{} names := map[string]int{}

2
vendor/modules.txt vendored
View File

@ -61,7 +61,7 @@ github.com/golangci/errcheck/golangci
github.com/golangci/errcheck/internal/errcheck github.com/golangci/errcheck/internal/errcheck
# github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 # github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613
github.com/golangci/go-misc/deadcode github.com/golangci/go-misc/deadcode
# github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 # github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c
github.com/golangci/go-tools/config github.com/golangci/go-tools/config
github.com/golangci/go-tools/lint github.com/golangci/go-tools/lint
github.com/golangci/go-tools/lint/lintutil github.com/golangci/go-tools/lint/lintutil