Skip to content

fix(unexported-return): stabilize results when a directory mixes package foo and foo_test#1712

Open
far4599 wants to merge 1 commit into
mgechev:masterfrom
far4599:fix/unexported-return-mixed-packages
Open

fix(unexported-return): stabilize results when a directory mixes package foo and foo_test#1712
far4599 wants to merge 1 commit into
mgechev:masterfrom
far4599:fix/unexported-return-mixed-packages

Conversation

@far4599

@far4599 far4599 commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Closes #1711

Motivation

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

  1. Wrong package qualifier — the returned type is reported as *foo_test.T instead of *foo.T.
  2. Flaky detection — in ~15% of runs the issue is silently dropped.

Root cause: dots.ResolvePackages collapses GoFiles + TestGoFiles + XTestGoFiles into a single []string per directory. lint.Linter.lintPackage then bundles 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 the whole package is type-checked under the foo_test name; additionally, mixing files from two different Go packages in a single types.Config.Check call causes some types to fail to resolve, so TypeOf returns nil and the issue is dropped.

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.

Change

lint.Linter.lintPackage now groups files by their parsed package name before creating lint.Package instances. Each Go package (e.g. foo and foo_test) gets its own lint.Package and is type-checked independently. Files belonging to different Go packages no longer share a types.Package.

Tests

Added test/unexported_return_mixed_pkg_test.go — a RED/GREEN regression test that exercises the previously flaky path 50 times with a minimal reproduction (one generic constructor + internal test + external test).

  • Without the fix the test fails on the first iteration (either wrong package qualifier or zero issues).
  • With the fix all 50 iterations pass.
  • All existing tests continue to pass: go test -race ./... -count=1 — OK.
  • revive --config revive.toml ./... — 0 issues.
  • golangci-lint run — 0 issues.

Style

Follows the existing coding style of the repository — errgroup.Group for concurrent package linting mirrors the pattern already used in Package.lint.

dots.ResolvePackages collapses GoFiles, TestGoFiles and XTestGoFiles into
a single []string per directory. Until now lintPackage put all of them
into one lint.Package whose TypeCheck picked anyFile via Go map iteration
(non-deterministic) and passed that file's package name to types.Config.Check.

For directories containing both `package foo` and `package foo_test`
files this produced two observable bugs in the unexported-return rule on
generic functions:

  * the returned type was qualified with foo_test instead of foo;
  * types occasionally failed to resolve, causing TypeOf to return nil
    and the issue to be silently dropped (~15% of runs).

Group files by their parsed package name and type-check each group
independently. Adds a RED/GREEN regression test that exercises the
previously flaky path 50 times.

Refs mgechev#1711

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes nondeterministic and incorrect unexported-return results when a single directory contains both package foo files and package foo_test external test files by ensuring type-checking is performed per distinct Go package.

Changes:

  • Update lint.Linter.lintPackage to group files by parsed package name and lint/type-check each group independently.
  • Add a regression test that repeatedly exercises the mixed-package directory scenario to verify stable output.
  • Add minimal testdata fixtures representing pub, pub internal test, and pub_test external test files.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
lint/linter.go Groups directory files by Go package name and lints each package separately to avoid mixed-package type-checking nondeterminism.
test/unexported_return_mixed_pkg_test.go Regression test running the rule multiple times to ensure deterministic, correct failure text when mixed packages are present.
testdata/unexported_return_mixed_pkg/pub.go Repro fixture: exported generic function returning an unexported generic type (expected to trigger unexported-return).
testdata/unexported_return_mixed_pkg/pub_internal_test.go Repro fixture: internal test file in package pub to mirror real-world directories.
testdata/unexported_return_mixed_pkg/pub_external_test.go Repro fixture: external test file in package pub_test to trigger the mixed-package scenario.

@alexandear alexandear self-assigned this Apr 28, 2026
@alexandear

Copy link
Copy Markdown
Collaborator

I'm still working on this. Currently, I'm writing a benchmark suite for Revive to be sure this change won't affect anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants