Unexporting what no importer uses¶
declscope shrink finds the exported declarations of internal/ packages that nothing outside their package uses. With -fix it unexports them.
Why it matters. An exported declaration takes shared scope by default, so the analyzer never reports a boundary on it. An exported name that nothing outside needs hides a declaration from every check here. Unexported, it takes private, and the analyzer checks who reaches it.
Why a subcommand. The analyzer reads one package, and any importer might use an exported name. Inside internal/, Go limits the importers to one directory tree. shrink loads the whole module, so it sees every one of them. In a workspace, it loads every module of the workspace. A nested module whose path lies under an internal/ parent can import the package too, so shrink loads it as well, and counts its uses. go vet and golangci-lint never run it.
// internal/user/user.go
package user
import "fmt"
func Load(id int) string { return Format(id) }
// Format renders an ID for display.
func Format(id int) string { return fmt.Sprint(id) }
type Record struct {
ID int
Name string
}
func Dump(r Record) { fmt.Println(r) }
api/api.go calls user.Load and names user.Dump. Nothing outside user names Format or Record:
$ declscope shrink
internal/user/user.go:8:6: func Format is exported, but nothing outside example.com/app/internal/user uses it
declscope shrink -fix renames Format to format, and the doc comment that opens with it. Record is not reported: fmt.Println reads its fields through reflection, which is a use.
Run it before the analyzer¶
Apply shrink -fix first, then work on the analyzer's reports. A declaration it unexports becomes private to its namespace. Wherever another file uses it, the analyzer now reports a crossing, which declscope -fix or a move settles. The other order takes a second round.
Run them in the same order in CI. shrink exits 3 when it reports anything, as the analyzer does. With -fix, only the reports left without a fix count.
Patterns¶
The patterns only choose which packages are reported. To find uses, shrink always reads every package of the module, whatever the patterns name.
For example, this reports only on internal/user:
$ declscope shrink ./internal/user/...
internal/user/user.go:8:6: func Format is exported, but nothing outside example.com/app/internal/user uses it
Load is not reported, although api/ is outside the pattern. api/api.go calls user.Load, and that call is a use.
Otherwise, a pattern behaves as it does for go vet.
What counts as a use, and when the fix is withheld
A fix is offered only where no use can exist outside the package. Where a use may exist but cannot be proved, the report stays and says why no fix is offered.
| Code | Result |
|---|---|
Not reported. String satisfies fmt.Stringer |
|
Not reported. fmt reads the fields through reflection |
|
| Not reported. A tag says a marshaller reads the struct | |
Not reported. A build-excluded file that writes user.Format uses it |
|
Not reported: Blocked. A const block of 2 or more constants of one type is one set. If the type or any value is used, no value is reported. If not, all of them are reported together.Reported: DefaultStatus. It is outside the block, so it is judged alone |
|
Reported: (no fix: external tests use it) |
|
Reported for Conn.Close: (no fix: a build-excluded file of another package may use it). The file is not type-checked, and a method satisfies an interface without its name |
|
Reported: (no fix: the unexported name is taken or would be captured) |
|
Reported: (no fix: a string variable may be set by -ldflags -X, which names it) |
|
Reported, with no fix for any package in the range, when a nested module builds a copy from outside the tree whose files import an internal package in the range: (no fix: the build of the nested module example.com/app/tools uses example.com/app/gen from /path/to/modcache/example.com/app/[email protected], which imports example.com/app/internal/user). Its uses are of other source. Every fix is withheld when the build list of a nested module cannot be read |
Deleting unused code is out of scope. Once a declaration is unexported, staticcheck's unused and gopls' unusedfunc report it if nothing uses it.
Which packages it does not judge
shrink stands down wherever an importer could be unseen. Each internal/ package it skips is named on stderr with the reason, and the exit status ignores it. Each such line starts with declscope shrink:, which the table leaves out.
| Code | Why | shrink prints on stderr |
|---|---|---|
| Another module may import it | Nothing. Only internal/ packages are listed |
|
-buildmode=plugin looks its exported symbols up by name |
not judged: example.com/app/internal/tool: package main |
|
cgo names Go symbols where go/types does not look |
not judged: example.com/app/internal/c: the package uses cgo |
|
| So does assembly, for any architecture | not judged: example.com/app/internal/fast: the package holds non-Go files |
|
| A nested module may import the package, and this one reads it from another directory, so its uses are of other source. A published version, or a module that fails to load, is the same | not judged: example.com/app/internal/user: the nested module example.com/app/tools may import it, and it reads example.com/app/internal/user from /path/to/elsewhere |
|
The build list names a module whose path lies in the range, and the build reads it from somewhere other than a nested module this run loaded: a published version, a replace outside the tree, or a vendored copy. Its uses are of other source. A build list that cannot be read is the same |
not judged: example.com/app/internal/user: the build uses example.com/app/tools from /path/to/modcache/example.com/app/[email protected] |
|
./... leaves it out, so this run has read none of its uses |
not judged: example.com/app/internal/user/testdata/fixture: ./... leaves it out, so this run does not load it |
An interface's method names, and the test functions of a _test.go file, are never reported either. Every implementation would rename too, and go test finds a test by name.
A package that does not type-check refuses the whole run, since it would show no uses at all. So does a package that imports one that does not, directly or not.
Important
Two uses are outside what shrink can see. Silence either with the ignore below.
- A name written as a string that no interface leads to, such as a template field or a constant given to
reflect.Value.MethodByName. - A module outside this module's directory tree whose path extends an
internal/parent, such as a/v2on another branch, or lies above one and holds its directory, when neither this module's build list nor a nested module's names it. Go checksinternal/by import path. One this module's build list names leaves the range unjudged, and one a nested module's names withholds the fixes, as the tables above show.
Silencing it¶
Write //declscope:ignore overexported // <why> where any other ignore goes: on the declaration, on a field's type, or before the package clause. A bare //declscope:ignore does not reach this rule.
shrink reports an ignore of it that silenced nothing. //declscope:ignore unused answers that report, as it does for the analyzer.
One ignore passes every build configuration. Where a field, a method, or a type a struct embeds is used outside its package only by files behind a build constraint, by name or by passing it to an interface, its ignore is not reported unused, though it silences nothing there:
// internal/a/a.go
type Mode struct {
//declscope:ignore overexported // app/gui.go sets it behind the gui tag
Flag string
}
| Run | What happens to Flag |
|---|---|
declscope shrink ./... |
app/gui.go is left out and read for names only, so Flag would be reported with no fix. The ignore silences it |
GOFLAGS=-tags=gui declscope shrink ./... |
app/gui.go uses Flag. The ignore is kept for the run without the tag |