Skip to content

Race condition in unexported-return: wrong package qualifier and flaky detection for generic functions #1711

@far4599

Description

@far4599

Describe the bug

The unexported-return rule produces non-deterministic and incorrect results when a directory contains both source files and external test files (package foo + package foo_test):

  1. Wrong package qualifier — the return type is reported as *foo_test.T instead of *foo.T.
  2. Flaky detection — the issue is sometimes not reported at all (~15% of runs in the reproduction below).

Root cause: dots.ResolvePackages returns GoFiles + TestGoFiles + XTestGoFiles collapsed into a single []string. lint.Linter.lintPackage then puts all of them into one lint.Package whose TypeCheck picks anyFile by iterating a Go map (non-deterministic order) and passes that file's package name to go/types.Config.Check. When anyFile is an XTestGoFile (package foo_test) the whole package is type-checked under the foo_test name, and mixing files from two different Go packages in a single types.Config.Check call also causes some types to fail to resolve (yielding nil from TypeOf), which silently drops the issue.

To Reproduce

Steps to reproduce the behavior:

  1. I updated revive go install github.com/mgechev/revive@v1.15.0
  2. Minimal reproduction: https://gist.github.com/far4599/937fb8f53d644c54cddbb07d769ba98d
git clone https://gist.github.com/far4599/937fb8f53d644c54cddbb07d769ba98d revive-repro
cd revive-repro
go mod tidy
bash reproduce.sh
revive-repro/
  go.mod                  # module revive-repro
  pub.go                  # generic function returning unexported type
  pub_internal_test.go    # package pub       (internal test)
  pub_external_test.go    # package pub_test  (external test)
  reproduce.sh            # runs revive 20 times and classifies results

pub.go:

package pub

type impl[T any] struct{ val T }

func New[T any](v T) *impl[T] { return &impl[T]{val: v} }

pub_internal_test.go (package pub):

package pub
import "testing"
func TestInternal(t *testing.T) { _ = New[int](1) }

pub_external_test.go (package pub_test):

package pub_test
import (
    "testing"
    "revive-repro"
)
func TestExternal(t *testing.T) { _ = pub.New[string]("x") }

No custom config file is required — the default rules are enough to trigger the bug.

Expected behavior

Every run produces exactly one issue with the correct package qualifier:

pub.go:6:22: exported func New returns unexported type *pub.impl[T], which can be annoying to use

Logs

Typical bash reproduce.sh output over 20 runs:

Run 1:  WRONG   — pub.go:6:22: ... *pub_test.impl[T] ...
Run 8:  MISSING — issue not reported at all
Run 18: OK      — pub.go:6:22: ... *pub.impl[T] ...

Results:
  OK (correct package name):  1 / 20
  WRONG (pub_test.impl[T]):  16 / 20
  MISSING (race, lost):       3 / 20

Desktop (please complete the following information):

  • OS: macOS 26.4
  • Go: 1.26.0

Additional context

Non-generic unexported return types are not affected — only generic functions trigger the bug, because the go/types package qualifies generic type names with the enclosing package path while non-generic ones often match regardless of which test variant was used.

Downstream impact: this race surfaces in golangci-lint as nolintlint flagging //nolint:revive directives as "unused" on the runs where revive drops the issue, producing intermittent CI failures.

A fix with RED→GREEN test is available at https://github.com/far4599/revive — the repro test fails without the fix and passes with it, and all existing tests continue to pass.

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions