Skip to content

Commit ccac35a

Browse files
committed
Fix false positives with unused identifiers
Issue #265: don't report unused warning when an identifier is used only in tests.
1 parent 3345c71 commit ccac35a

File tree

5 files changed

+84
-14
lines changed

5 files changed

+84
-14
lines changed

.github/ISSUE_TEMPLATE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ Thank you for creating the issue!
22

33
Please include the following information:
44
1. Version of golangci-lint: `golangci-lint --version` (or git commit if you don't use binary distribution)
5-
2. Go environment: `go version && go env`
6-
3. Verbose output of running: `golangci-lint run -v`
5+
2. Config file: `cat .golangci.yml`
6+
3. Go environment: `go version && go env`
7+
4. Verbose output of running: `golangci-lint run -v`

pkg/lint/load.go

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"go/types"
88
"os"
99
"path/filepath"
10+
"regexp"
1011
"strings"
1112
"time"
1213

@@ -25,18 +26,20 @@ import (
2526
)
2627

2728
type ContextLoader struct {
28-
cfg *config.Config
29-
log logutils.Log
30-
debugf logutils.DebugFunc
31-
goenv *goutil.Env
29+
cfg *config.Config
30+
log logutils.Log
31+
debugf logutils.DebugFunc
32+
goenv *goutil.Env
33+
pkgTestIDRe *regexp.Regexp
3234
}
3335

3436
func NewContextLoader(cfg *config.Config, log logutils.Log, goenv *goutil.Env) *ContextLoader {
3537
return &ContextLoader{
36-
cfg: cfg,
37-
log: log,
38-
debugf: logutils.Debug("loader"),
39-
goenv: goenv,
38+
cfg: cfg,
39+
log: log,
40+
debugf: logutils.Debug("loader"),
41+
goenv: goenv,
42+
pkgTestIDRe: regexp.MustCompile(`^(.*) \[(.*)\.test\]`),
4043
}
4144
}
4245

@@ -222,19 +225,65 @@ func (cl ContextLoader) loadPackages(ctx context.Context, loadMode packages.Load
222225
i, pkg.ID, pkg.GoFiles, pkg.CompiledGoFiles, syntaxFiles)
223226
}
224227

225-
var retPkgs []*packages.Package
226228
for _, pkg := range pkgs {
227229
for _, err := range pkg.Errors {
228230
if strings.Contains(err.Msg, "no Go files") {
229231
return nil, errors.Wrapf(exitcodes.ErrNoGoFiles, "package %s", pkg.PkgPath)
230232
}
231233
}
232-
if !shouldSkipPkg(pkg) {
233-
retPkgs = append(retPkgs, pkg)
234+
}
235+
236+
return cl.filterPackages(pkgs), nil
237+
}
238+
239+
func (cl ContextLoader) tryParseTestPackage(pkg *packages.Package) (name, testName string, isTest bool) {
240+
matches := cl.pkgTestIDRe.FindStringSubmatch(pkg.ID)
241+
if matches == nil {
242+
return "", "", false
243+
}
244+
245+
return matches[1], matches[2], true
246+
}
247+
248+
func (cl ContextLoader) filterPackages(pkgs []*packages.Package) []*packages.Package {
249+
packagesWithTests := map[string]bool{}
250+
for _, pkg := range pkgs {
251+
name, testName, isTest := cl.tryParseTestPackage(pkg)
252+
if !isTest {
253+
continue
254+
}
255+
packagesWithTests[name] = true
256+
257+
if name != testName {
258+
cl.log.Infof("pkg ID=%s: %s != %s: %#v", pkg.ID, name, testName, pkg)
259+
}
260+
}
261+
262+
cl.debugf("package with tests: %#v", packagesWithTests)
263+
264+
var retPkgs []*packages.Package
265+
for _, pkg := range pkgs {
266+
if shouldSkipPkg(pkg) {
267+
cl.debugf("skip pkg ID=%s", pkg.ID)
268+
continue
269+
}
270+
271+
_, _, isTest := cl.tryParseTestPackage(pkg)
272+
if !isTest && packagesWithTests[pkg.PkgPath] {
273+
// If tests loading is enabled,
274+
// for package with files a.go and a_test.go go/packages loads two packages:
275+
// 1. ID=".../a" GoFiles=[a.go]
276+
// 2. ID=".../a [.../a.test]" GoFiles=[a.go a_test.go]
277+
// We need only the second package, otherwise we can get warnings about unused variables/fields/functions
278+
// in a.go if they are used only in a_test.go.
279+
cl.debugf("skip pkg ID=%s because we load it with test package", pkg.ID)
280+
continue
234281
}
282+
283+
retPkgs = append(retPkgs, pkg)
235284
}
236285

237-
return retPkgs, nil
286+
return retPkgs
238287
}
239288

240289
//nolint:gocyclo

test/run_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ func TestDeadcodeNoFalsePositivesInMainPkg(t *testing.T) {
163163
checkNoIssuesRun(t, out, exitCode)
164164
}
165165

166+
func TestIdentifierUsedOnlyInTests(t *testing.T) {
167+
out, exitCode := runGolangciLint(t, "--no-config", "--disable-all", "-Eunused",
168+
filepath.Join(testdataDir, "used_only_in_tests"))
169+
checkNoIssuesRun(t, out, exitCode)
170+
}
171+
166172
func TestConfigFileIsDetected(t *testing.T) {
167173
checkGotConfig := func(out string, exitCode int) {
168174
assert.Equal(t, exitcodes.Success, exitCode, out)

test/testdata/used_only_in_tests/a.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package p
2+
3+
func f() bool {
4+
return true
5+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package p
2+
3+
import "testing"
4+
5+
func TestF(t *testing.T) {
6+
if !f() {
7+
t.Fail()
8+
}
9+
}

0 commit comments

Comments
 (0)